diff --git a/AudioSender.js b/AudioSender.js index 6cdbdf1..c58215c 100644 --- a/AudioSender.js +++ b/AudioSender.js @@ -37,6 +37,16 @@ export const changeConfiguration = ({ export const setAudioContext = ctx => audioContext = ctx; +export const setDestination = destination => { + DESTINATION = destination; + const oscillators = getOscillators(); + oscillators.forEach( + oscillator => { + oscillator?.disconnect(); + oscillator?.connect(destination); + } + ) +} function getAudioContext() { if(!audioContext) { throw 'Audio context not provided.'; @@ -128,10 +138,7 @@ export function stopAt(streamEndSeconds) { oscillator?.stop(streamEndSeconds); } stopTimeout(); - stopOscillatorsTimeoutId = window.setTimeout( - stop, - delayMs(streamEndSeconds) - ); + stopOscillatorsTimeoutId = window.setTimeout(stop, delayMs(streamEndSeconds)); } export function stop() { const oscillators = getOscillators(); diff --git a/Panels/BasePanel.js b/Panels/BasePanel.js index 3a83c41..1f2f052 100644 --- a/Panels/BasePanel.js +++ b/Panels/BasePanel.js @@ -231,6 +231,14 @@ class BasePanel { const element = this.getElement(id); element.innerHTML = html; } + getHtmlById = (id) => { + const element = this.getElement(id); + return element.innerHTML; + } + scrollToBottom = id => { + const element = this.getElement(id); + element.scrollIntoView(false); + } getNumberById = id => { const value = this.getValueById(id); return parseFloat(value); diff --git a/Panels/CodePanel.js b/Panels/CodePanel.js index 37f68a3..478fce5 100644 --- a/Panels/CodePanel.js +++ b/Panels/CodePanel.js @@ -1,3 +1,4 @@ +import { htmlEncode } from '../converters'; import BasePanel from './BasePanel'; class CodePanel extends BasePanel { @@ -6,6 +7,18 @@ class CodePanel extends BasePanel { this.addCode('code'); } setCode = (html) => this.setHtmlById('code', html); + appendCode = html => { + let current = this.getHtmlById('code'); + if(current !== '') current += document.createElement('br').outerHTML; + this.setHtmlById('code', current + html); + this.scrollToBottom('code'); + } + appendText = text => { + let current = this.getHtmlById('code'); + if(current !== '') current += document.createElement('br').outerHTML; + this.setHtmlById('code', current + htmlEncode(text)); + this.scrollToBottom('code'); + } } export default CodePanel; \ No newline at end of file diff --git a/Panels/FrequencyGraphPanel.js b/Panels/FrequencyGraphPanel.js index 7396464..740717b 100644 --- a/Panels/FrequencyGraphPanel.js +++ b/Panels/FrequencyGraphPanel.js @@ -109,7 +109,8 @@ class FrequencyGraphPanel extends BasePanel { const canvas = this.getElement('frequency-graph'); const ctx = canvas.getContext('2d'); const {height, width} = canvas; - ctx.clearRect(0, 0, width, height); + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, width, height); let now; if(this.samples.length > 1) { diff --git a/Panels/CommunicationsPanel.js b/Panels/OutputPanel.js similarity index 80% rename from Panels/CommunicationsPanel.js rename to Panels/OutputPanel.js index 0313550..0db3688 100644 --- a/Panels/CommunicationsPanel.js +++ b/Panels/OutputPanel.js @@ -1,8 +1,8 @@ import BasePanel from './BasePanel'; -class CommunicationsPanel extends BasePanel { +class OutputPanel extends BasePanel { constructor() { - super('Audio Sender'); + super('Outut'); this.addRadios('send-via', [ {text: 'Analyzer', id: 'send-via-analyzer', eventName: 'sendAnalyzerChange'}, {text: 'Speakers', id: 'send-via-speaker', eventName: 'sendSpeakersChange'} @@ -12,4 +12,4 @@ class CommunicationsPanel extends BasePanel { setSendAnalyzer = checked => this.setCheckedById('send-via-analyzer', checked); } -export default CommunicationsPanel; \ No newline at end of file +export default OutputPanel; \ No newline at end of file diff --git a/Panels/PacketErrorPanel.js b/Panels/PacketErrorPanel.js index 9ddba57..258de4c 100644 --- a/Panels/PacketErrorPanel.js +++ b/Panels/PacketErrorPanel.js @@ -20,8 +20,8 @@ class PacketErrorPanel extends BasePanel { this.addSection('Packet Retransmission') this.addRadios('repeat', [ - { text: 'Automatic Repeat Request', value: 'arq', checked: true, eventName: 'hi' }, - { text: 'Manual Repeat Request', value: 'manual', checked: true, eventName: 'hi' } + { text: 'Automatic Repeat Request', id:'arq', value: 'arq', checked: true, eventName: 'automaticRepeatRequestChange' }, + { text: 'Manual Repeat Request', id:'manual', value: 'manual', checked: true, eventName: 'manualRepeatRequestChange' } ]); this.openField('Packets'); @@ -30,6 +30,9 @@ class PacketErrorPanel extends BasePanel { this.addButton('request-button', 'Request', 'requestPackets'); } + getAutomaticRepeatRequest = () => { + return this.getCheckedById('arq'); + } reset = () => { this.setFailedPacketIndeces([]); this.setSizeCrcUnavailable(); @@ -41,7 +44,7 @@ class PacketErrorPanel extends BasePanel { } getFailedPacketIndeces = () => { let text = this.getValueById('request-packet-indexes'); - return text.replace(/\s+/g, '').split(',').map(Number); + return text.replace(/\s+/g, '').split(',').filter(v => v !== '').map(Number); } setCrcPassed = (passed) => this.setValueById('crc', passed ? 'Pass' : 'Fail'); setCrcUnavailable = () => this.setValueById('crc', 'N/A'); diff --git a/Panels/ReceivePanel.js b/Panels/ReceivePanel.js index 35904a8..b419cbd 100644 --- a/Panels/ReceivePanel.js +++ b/Panels/ReceivePanel.js @@ -15,7 +15,7 @@ class ReceivePanel extends BasePanel { this.addNewLine(); this.addDynamicText('id-state', 'Offline.'); - this.addProgressBar('progress', .50, .25); + this.addProgressBar('progress', 0, 0); this.addCode('text', '', 'small'); this.addImage('image', undefined, {width: 32, height: 32}); @@ -45,6 +45,10 @@ class ReceivePanel extends BasePanel { this.addEventListener('resetClick', () => { AudioReceiver.reset(); StreamManager.reset(); + this.setReceivedBytes([]); + this.setExpectedPacketCount(0); + this.setFailedPacketCount(0); + this.setSuccessfulPacketCount(0); }); } isOnline = () => this.getCheckedById('is-online'); @@ -58,17 +62,6 @@ class ReceivePanel extends BasePanel { this.setValueById('id-state', 'offline'); } } - - // waitForSignal = () => { - // AudioReceiver.start(); - // } - // reset = () => { - // AudioReceiver.reset(); - // StreamManager.reset(); - // } - // stopWaitingForSignal = () => { - // AudioReceiver.stop(); - // } setProgress = (percent, percent2 = 0) => { this.setProgressById('progress', percent, percent2); } diff --git a/StreamManager.js b/StreamManager.js index 2205434..69edc81 100644 --- a/StreamManager.js +++ b/StreamManager.js @@ -2,13 +2,9 @@ import Dispatcher from "./Dispatcher"; import * as CRC from './CRC'; import * as PacketUtils from './PacketUtils'; import { - bitsToBytes, bitsToInt, bytesToBits, numberToBytes, - numberToHex, - numberToAscii, - bytesToNumber } from "./converters"; const dispatcher = new Dispatcher('StreamManager', [ @@ -41,7 +37,9 @@ export const removeEventListener = dispatcher.removeListener; const isPacketInRange = (packetIndex) => { // Blindly accept. We can't do anything about it for now - if(!isSizeTrusted()) return true; + if(!isSizeTrusted()){ + return packetIndex < PacketUtils.getMaxPackets(); + } const { packetCount } = PacketUtils.packetStats(getSize()); return packetIndex < packetCount; } @@ -125,11 +123,23 @@ export const applyPacket = ({ } delete BITS[packetIndex] } -export const getFailedPacketIndeces = () => FAILED_SEQUENCES; +export const getFailedPacketIndeces = () => { + return FAILED_SEQUENCES.filter(isPacketInRange); +} +export const getNeededPacketIndeces = () => { + if(!isSizeTrusted()) return getFailedPacketIndeces(); + const packetCount = countExpectedPackets(); + let indeces = []; + for(let i = 0; i < packetCount; i++) { + if(SUCCESS_SEQUENCES.includes(i)) continue; + indeces.push(i); + } + return indeces; +}; export const countFailedPackets = () => FAILED_SEQUENCES.length; export const countSuccessfulPackets = () => SUCCESS_SEQUENCES.length; export const countExpectedPackets = () => { - if(!isSizeTrusted()) return 0; + if(!isSizeTrusted()) return PacketUtils.getMaxPackets(); return PacketUtils.packetStats(getSize()).packetCount; } export const setPacketsExpected = packetCount => { @@ -138,11 +148,27 @@ export const setPacketsExpected = packetCount => { SAMPLES_EXPECTED = packetCount * PacketUtils.getPacketSegmentCount(); } +const hasPackets = (start, end) => { + for(let packetIndex = start; packetIndex <= end; packetIndex++) { + // We need this packet, but it failed to transfer + if(FAILED_SEQUENCES.includes(packetIndex)) return false; + // We need this packet, but it hasn't come through yet + if(!SUCCESS_SEQUENCES.includes(packetIndex)) return false; + } + return true; +} +const hasBytes = (index, length) => { + if(DATA.length < index + length) return false; + const packetSize = PacketUtils.getPacketDataByteCount(); + const start = Math.floor(index / packetSize); + const end = Math.floor(index + length / packetSize); + return hasPackets(start, end); +} export const getSizeAvailable = () => { if(DATA_SIZE_BIT_COUNT === 0) return 1; let lastBit = DATA_SIZE_BIT_COUNT; let lastByte = Math.ceil(lastBit / 8); - if(DATA.length < lastByte) return false; + if(!hasBytes(0, lastByte)) return false; // Do we have a crc check on the size? if(DATA_SIZE_CRC_BIT_COUNT !== 0) { @@ -209,21 +235,22 @@ export const getSizeCrcAvailable = () => { if (DATA_SIZE_BIT_COUNT === 0) return false; if (DATA_SIZE_CRC_BIT_COUNT === 0) return false; const bitsNeeded = DATA_SIZE_BIT_COUNT + DATA_SIZE_CRC_BIT_COUNT; - return DATA.length >= Math.ceil(bitsNeeded / 8); + const bytesNeeded = Math.ceil(bitsNeeded / 8); + return hasBytes(0, bytesNeeded); } export const getCrcAvailable = () => { if(DATA_CRC_BIT_COUNT === 0) return false; if(!getSizeAvailable()) return false; let byteCount = getSize(); - if(DATA.length < byteCount) return false; + // Do we have enough bytes for the headers and underlying data? let headerBitCount = DATA_SIZE_BIT_COUNT + DATA_CRC_BIT_COUNT + DATA_SIZE_CRC_BIT_COUNT; if(headerBitCount % 8 !== 0) headerBitCount += 8 - (headerBitCount % 8); const headerByteCount = headerBitCount / 8; byteCount += headerByteCount; - - return DATA.length >= byteCount; + + return hasBytes(0, byteCount); } export const getSizeCrcPassed = () => { if(!getSizeCrcAvailable()) return false; diff --git a/converters.js b/converters.js index 80dc67d..36d0590 100644 --- a/converters.js +++ b/converters.js @@ -89,4 +89,9 @@ export const urlToBytes = src => { export const bytesToUrl = bytes => { const blob = new Blob([new Uint8Array(bytes)]); return URL.createObjectURL(blob); +} +export function htmlEncode(text) { + const element = document.createElement('div'); + element.textContent = text; + return element.innerHTML; } \ No newline at end of file diff --git a/index.html b/index.html index 88d4968..2c44881 100644 --- a/index.html +++ b/index.html @@ -10,12 +10,6 @@

Data Over Audio

-
-

Channel Graph

-
- -
-

Data

@@ -28,18 +22,6 @@ Unused bits in last packet:
-
-

Original Data to send

-
-

-
-
-
-

Encoded packets to send

-
-
-
-

Decoded

@@ -52,18 +34,6 @@ Decoded Packets: N/A%
-
-

Encoded Packets Received

-
-

-
-
-
-

Decoded Packets Received

-
-

-
-

Selected

diff --git a/index.js b/index.js index 792e222..2d5cf8d 100644 --- a/index.js +++ b/index.js @@ -6,8 +6,7 @@ import * as Humanize from './Humanize'; import * as Randomizer from './Randomizer'; import * as AudioSender from './AudioSender'; import * as AudioReceiver from './AudioReceiver'; -import * as CRC from './CRC'; -import CommunicationsPanel from './Panels/CommunicationsPanel'; +import OutputPanel from './Panels/OutputPanel'; import MessagePanel from "./Panels/MessagePanel"; import CodePanel from "./Panels/CodePanel"; import FrequencyPanel from "./Panels/FrequencyPanel"; @@ -19,42 +18,25 @@ import GraphConfigurationPanel from './Panels/GraphConfigurationPanel'; import PacketErrorPanel from './Panels/PacketErrorPanel'; import SpeedPanel from './Panels/SpeedPanel'; import { - bitsToInt, - bytesToBits, bytesToText, - numberToBytes, } from './converters'; import MicrophonePanel from "./Panels/MicrophonePanel"; import ReceivePanel from "./Panels/ReceivePanel"; var audioContext; var analyser; -// bits as they are sent -let SENT_ORIGINAL_TEXT = ''; -let SENT_ORIGINAL_BITS = []; // original bits -let SENT_ENCODED_BITS = []; // bits with error encoding -let SENT_TRANSFER_BITS = []; // bits sent in the transfer - -let EXCLUDED_CHANNELS = []; - const ERROR_CORRECTION_BLOCK_SIZE = 7; const ERROR_CORRECTION_DATA_SIZE = 4; -let CHANNEL_OVER = -1; -let CHANNEL_SELECTED = -1; -let SEGMENT_OVER = -1; -let SEGMENT_SELECTED = -1; var SEND_VIA_SPEAKER = false; -var RECEIVED_STREAM_START_MS = -1; -let SAMPLES = []; -var bitStart = []; var PAUSE = false; -const communicationsPanel = new CommunicationsPanel(); +const outputPanel = new OutputPanel(); const messagePanel = new MessagePanel(); -const bitsSentPanel = new CodePanel('Bits Sent'); -const bitsReceivedPanel = new CodePanel('Bits Received'); +const statusPanel = new CodePanel('Status'); +// const bitsSentPanel = new CodePanel('Bits Sent'); +// const bitsReceivedPanel = new CodePanel('Bits Received'); const frequencyPanel = new FrequencyPanel(); const signalPanel = new SignalPanel(); const packetizationPanel = new PacketizationPanel(); @@ -75,19 +57,20 @@ function handleWindowLoad() { panelContainer.prepend(availableFskPairsPanel.getDomElement()); panelContainer.prepend(frequencyPanel.getDomElement()); panelContainer.prepend(signalPanel.getDomElement()); - panelContainer.prepend(bitsReceivedPanel.getDomElement()); - panelContainer.prepend(bitsSentPanel.getDomElement()); + // panelContainer.prepend(bitsReceivedPanel.getDomElement()); + // panelContainer.prepend(bitsSentPanel.getDomElement()); + panelContainer.prepend(statusPanel.getDomElement()); panelContainer.prepend(packetErrorPanel.getDomElement()); panelContainer.prepend(receivePanel.getDomElement()); panelContainer.prepend(microphonePanel.getDomElement()); - panelContainer.prepend(communicationsPanel.getDomElement()); + panelContainer.prepend(outputPanel.getDomElement()); panelContainer.prepend(messagePanel.getDomElement()); // Initialize Values microphonePanel.setListening(false); - communicationsPanel.setSendSpeakers(false); - communicationsPanel.setSendAnalyzer(true); + outputPanel.setSendSpeakers(false); + outputPanel.setSendAnalyzer(true); messagePanel.setMessageText(Randomizer.text(5)); messagePanel.setDataType('image'); @@ -97,16 +80,16 @@ function handleWindowLoad() { receivePanel.setDataType(dataType); }) receivePanel.setDataType(messagePanel.getDataType()); - receivePanel.setExpectedPacketCount(100); - receivePanel.setFailedPacketCount(25); - receivePanel.setSuccessfulPacketCount(50); + receivePanel.setExpectedPacketCount(0); + receivePanel.setFailedPacketCount(0); + receivePanel.setSuccessfulPacketCount(0); receivePanel.setReceivedHtml('Ready.'); packetErrorPanel.reset(); - bitsSentPanel.setCode(''); - bitsReceivedPanel.setCode(''); + // bitsSentPanel.setCode(''); + // bitsReceivedPanel.setCode(''); frequencyPanel.setMinimumFrequency(2500); frequencyPanel.setMaximumFrequency(23000); @@ -146,8 +129,8 @@ function handleWindowLoad() { // Events - communicationsPanel.addEventListener('sendSpeakersChange', handleChangeSendSpeakers); - communicationsPanel.addEventListener('sendAnalyzerChange', handleChangeSendAnalyzer); + outputPanel.addEventListener('sendSpeakersChange', handleChangeSendSpeakers); + outputPanel.addEventListener('sendAnalyzerChange', handleChangeSendAnalyzer); messagePanel.addEventListener('messageChange', configurationChanged); messagePanel.addEventListener('sendClick', handleSendButtonClick); @@ -157,7 +140,6 @@ function handleWindowLoad() { frequencyPanel.addEventListener('maximumFrequencyChange', configurationChanged); frequencyPanel.addEventListener('fftSizeChange', ({value}) => { configurationChanged(); - resetGraphData(); }); frequencyPanel.addEventListener('fskPaddingChange', configurationChanged); frequencyPanel.addEventListener('multiFskPaddingChange', configurationChanged); @@ -215,18 +197,37 @@ function handleWindowLoad() { receivePanel.addEventListener('receive', handleReceivePanelReceive); receivePanel.addEventListener('end', handleReceivePanelEnd); + packetErrorPanel.addEventListener('requestPackets', requestFailedPackets); + // Setup audio sender AudioSender.addEventListener('begin', () => messagePanel.setSendButtonText('Stop')); - AudioSender.addEventListener('send', handleAudioSenderSend); + // AudioSender.addEventListener('send', () => {}); AudioSender.addEventListener('end', () => messagePanel.setSendButtonText('Send')); + + AudioReceiver.addEventListener('end', () => { + // Signal ended before complete transmission? + const missingIndeces = StreamManager.getNeededPacketIndeces(); + packetErrorPanel.setFailedPacketIndeces(missingIndeces); + if(missingIndeces.length !== 0 && packetErrorPanel.getAutomaticRepeatRequest()) { + statusPanel.appendText(`Automatically Requesting ${missingIndeces.length} failed packet(s).`); + sendPackets(messagePanel.getMessageBytes(), missingIndeces); + } + }) // Setup stream manager - StreamManager.addEventListener('change', handleStreamManagerChange); + StreamManager.addEventListener('sizeReceived', () => { + receivePanel.setExpectedPacketCount(StreamManager.countExpectedPackets()); + }); StreamManager.addEventListener('packetFailed', () => { + receivePanel.setFailedPacketCount(StreamManager.countFailedPackets()); packetErrorPanel.setFailedPacketIndeces(StreamManager.getFailedPacketIndeces()); }); StreamManager.addEventListener('packetReceived', () => { + receivePanel.setSuccessfulPacketCount(StreamManager.countSuccessfulPackets()); + // Failed indices changed? + receivePanel.setFailedPacketCount(StreamManager.countFailedPackets()); packetErrorPanel.setFailedPacketIndeces(StreamManager.getFailedPacketIndeces()); + if(StreamManager.getSizeCrcAvailable()) { packetErrorPanel.setSizeCrcPassed(StreamManager.getSizeCrcPassed()); } else { @@ -237,18 +238,22 @@ function handleWindowLoad() { } else { packetErrorPanel.setCrcUnavailable(); } + receivePanel.setReceivedBytes(StreamManager.getDataBytes()); }); // grab dom elements - const receivedChannelGraph = document.getElementById('received-channel-graph'); - receivedChannelGraph.addEventListener('mouseover', handleReceivedChannelGraphMouseover); - receivedChannelGraph.addEventListener('mouseout', handleReceivedChannelGraphMouseout); - receivedChannelGraph.addEventListener('mousemove', handleReceivedChannelGraphMousemove); - receivedChannelGraph.addEventListener('click', handleReceivedChannelGraphClick); document.getElementById('audio-context-sample-rate').innerText = getAudioContext().sampleRate.toLocaleString(); // wire up events configurationChanged(); } +const requestFailedPackets = () => { + const packetIndeces = packetErrorPanel.getFailedPacketIndeces(); + try { + sendPackets(messagePanel.getMessageBytes(), packetIndeces); + } catch (e) { + statusPanel.appendText(e); + } +} function updateFrequencyResolution() { const sampleRate = getAudioContext().sampleRate; @@ -259,10 +264,6 @@ function updateFrequencyResolution() { document.getElementById('frequency-count').innerText = frequencyCount.toFixed(2); } -function handleAudioSenderSend({bits}) { - SENT_TRANSFER_BITS.push(...bits); - showSentBits(); -} function configurationChanged() { if(analyser) analyser.fftSize = frequencyPanel.getFftSize(); updatePacketUtils(); @@ -292,7 +293,6 @@ const handleReceivePanelReceive = ({signalStart, signalIndex, indexStart, bits}) const handleReceivePanelEnd = (e) => { frequencyGraphPanel.setSignalEnd(e.signalEnd); if(graphConfigurationPanel.getPauseAfterEnd()) { - stopGraph(); frequencyGraphPanel.stop(); receivePanel.setIsOnline(false); } @@ -433,84 +433,80 @@ function percentInFrequency(hz, frequencyResolution) { const percent = hzInSegement / frequencyResolution; return percent; } - function sendBytes(bytes) { const byteCount = bytes.length; - if(byteCount === 0) { - document.getElementById('sent-data').innerText = 'Nothing to send!'; + const { packetCount } = PacketUtils.packetStats(byteCount); + const packetIndeces = new Array(packetCount).fill(0).map((_, i) => i); + sendPackets(bytes, packetIndeces); +} +function sendPackets(bytes, packetIndeces) { + const byteCount = bytes.length; + if(packetIndeces.length === 0) { + statusPanel.appendText('Nothing requested!'); + return; + } else if(byteCount === 0) { + statusPanel.appendText('Nothing to send!'); return; } else if(byteCount > packetizationPanel.getDataSize()) { - document.getElementById('sent-data').innerText = `Attempted to send too much data. Limit is ${Humanize.byteSize(packetizationPanel.getDataSize())}. Tried to send ${Humanize.byteSize(byteCount)}`; + statusPanel.appendText(`Attempted to send too much data. Limit is ${Humanize.byteSize(packetizationPanel.getDataSize())}. Tried to send ${Humanize.byteSize(byteCount)}`); + return; + } + const { + packetDurationSeconds, + packetCount + } = PacketUtils.packetStats(byteCount); + + // dedupe indices + packetIndeces = packetIndeces + // dedupe + .filter((v,i,a) => a.indexOf(v, i+1) === -1) + // integers only + .filter(Number.isInteger) + // in range + .filter(v => v >=0 && v < packetCount); + + // ensure headers come first + packetIndeces.sort((a, b) => a - b); + + // Make sure we still have something to send + if(packetIndeces.length === 0) { + statusPanel.appendText('No valid packets requested.'); return; } - const bits = bytesToBits(bytes); - - SENT_ORIGINAL_TEXT = bytesToText(bytes); - SENT_ORIGINAL_BITS = bits.slice(); - - SENT_TRANSFER_BITS.length = 0; - SENT_ENCODED_BITS.length = 0; + const requestedPacketCount = packetIndeces.length; AudioSender.setAudioContext(getAudioContext()); const startSeconds = AudioSender.now() + 0.1; const packetBitCount = PacketUtils.getPacketMaxBitCount(); - const { - packetCount, - totalDurationSeconds, - packetDurationSeconds - } = PacketUtils.packetStats(byteCount); const packer = PacketUtils.pack(bytes); try { AudioSender.beginAt(startSeconds); // send all packets - for(let i = 0; i < packetCount; i++) { - let packet = packer.getBits(i); + for(let i = 0; i < requestedPacketCount; i++) { + let packet = packer.getBits(packetIndeces[i]); if(packet.length > packetBitCount) { throw new Error(`Too many bits in the packet. tried to send ${packet.length}, limited to ${packetBitCount}`); + } else if(packet.length === 0) { + throw new Error(`Attempted to send packet ${i} but it has no data!.`) } packet.push(...new Array(packetBitCount - packet.length).fill(0)); sendPacket(packet, startSeconds + (i * packetDurationSeconds)); } - AudioSender.stopAt(startSeconds + totalDurationSeconds); + AudioSender.stopAt(startSeconds + (requestedPacketCount * packetDurationSeconds)); } catch (e) { - console.error(e); + statusPanel.addText(e); AudioSender.stop(); return; } - showSentBits(); - // start the graph moving again - resumeGraph(); -} -function showSentBits() { - const channelCount = availableFskPairsPanel.getSelectedFskPairs().length; - - // original bits - document.getElementById('sent-data').innerHTML = - SENT_ORIGINAL_BITS.reduce(bitReducer( - PacketUtils.getPacketMaxBitCount(), - packetizationPanel.getErrorCorrection() ? ERROR_CORRECTION_DATA_SIZE : 8 - ), ''); - - // error correcting bits - if(packetizationPanel.getErrorCorrection()) { - document.getElementById('error-correcting-data').innerHTML = - SENT_ENCODED_BITS.reduce(bitReducer( - PacketUtils.getPacketDataBitCount(), - ERROR_CORRECTION_BLOCK_SIZE - ), ''); - } else { - document.getElementById('error-correcting-data').innerHTML = ''; + // start the graph and receiver + if(graphConfigurationPanel.getPauseAfterEnd()) { + receivePanel.setIsOnline(true); + frequencyGraphPanel.start(); } - bitsSentPanel.setCode( - SENT_TRANSFER_BITS.reduce(bitReducer( - PacketUtils.getPacketMaxBitCount() + PacketUtils.getPacketLastSegmentUnusedBitCount(), - channelCount, - (packetIndex, blockIndex) => `${blockIndex === 0 ? '' : '
'}Segment ${blockIndex}: ` - ), '')); } function sendPacket(bits, packetStartSeconds) { const channels = availableFskPairsPanel.getSelectedFskPairs(); @@ -527,213 +523,6 @@ function sendPacket(bits, packetStartSeconds) { AudioSender.send(segmentBits, packetStartSeconds + offsetSeconds); } } -function getNextPacketStartMilliseconds(priorPacketStartMilliseconds) { - return priorPacketStartMilliseconds + PacketUtils.getPacketDurationMilliseconds(); -} -function getPacketIndexEndMilliseconds(transferStartedMilliseconds, packetIndex) { - const start = transferStartedMilliseconds + (PacketUtils.getPacketDurationMilliseconds() * packetIndex) - return getPacketEndMilliseconds(start); -} -function getPacketEndMilliseconds(packetStartedMilliseconds) { - return getNextPacketStartMilliseconds(packetStartedMilliseconds) - 0.1; -} -function getTotalSegmentCount(bitCount) { - return PacketUtils.packetsNeededToTransferBytes(bitCount/8) * PacketUtils.getPacketSegmentCount(); -} -function padArray(values, length, value) { - values = values.slice();//copy - while(values.length < length) values.push(value); - return values; -} - -function stopGraph() { - PAUSE = true; - receivePanel.setIsOnline(false); -} - -function resumeGraph() { - if(microphonePanel.getListening()) { - if(PAUSE) { - PAUSE = false; - receivePanel.setIsOnline(true); - resetGraphData(); - requestAnimationFrame(drawFrequencyData); - } else { - PAUSE = false; - } - } else { - PAUSE = false; - } -} - -function handleStreamManagerChange() { - receivePanel.setSuccessfulPacketCount(StreamManager.countSuccessfulPackets()); - receivePanel.setExpectedPacketCount(StreamManager.countExpectedPackets()); - receivePanel.setFailedPacketCount(StreamManager.countFailedPackets()); - - const bytes = StreamManager.getDataBytes(); - const receivedText = bytesToText(bytes); - - if(messagePanel.getDataType() === 'text') { - receivePanel.setReceivedHtml( - receivedText.split('').reduce(textExpectorReducer(SENT_ORIGINAL_TEXT), '') - ); - } else { - receivePanel.setReceivedBytes(bytes); - } -} - -function removeEncodedPadding(bits) { - const sizeBits = packetizationPanel.getDataSizePower(); - const dataSize = ERROR_CORRECTION_DATA_SIZE; - const blockSize = ERROR_CORRECTION_BLOCK_SIZE; - let bitsNeeded = sizeBits; - let blocksNeeded = sizeBits; - // need to calc max bits - if(packetizationPanel.getErrorCorrection()) { - blocksNeeded = Math.ceil(sizeBits / dataSize); - bitsNeeded = blocksNeeded * blockSize; - } - if(bits.length < bitsNeeded) { - // unable to parse size just yet - return bits; - } - - // get header bits representing the size - const dataByteCount = StreamManager.getTransferByteCount(); - - // determine how many decoded bits need to be sent (including the size) - let totalBits = (dataByteCount * 8); - totalBits += packetizationPanel.getDataSizePower(); - if(packetizationPanel.getDataSizePower() !== 0) totalBits += packetizationPanel.getDataSizeCrc(); - totalBits += packetizationPanel.getDataCrc(); - let encodingBitCount = totalBits; - if(packetizationPanel.getErrorCorrection()) { - const blocks = Math.ceil(encodingBitCount / dataSize); - encodingBitCount = blocks * blockSize; - } - - // bits are padded - if(bits.length > encodingBitCount) { - // remove padding - bits = bits.slice(); - bits.length = encodingBitCount; - } - return bits; -} -function removeDecodedHeadersAndPadding(bits) { - const sizeBitCount = packetizationPanel.getDataSizePower(); - const sizeCrcBitCount = packetizationPanel.getDataSizeCrc(); - const dataCrcBitCount = packetizationPanel.getDataCrc(); - let byteCount; - let offset = 0; - if(sizeBitCount !== 0) { - offset += sizeBitCount; - // header bits only? - if(bits.length <= offset) return []; - byteCount = bitsToInt(bits.slice(0, sizeBitCount), sizeBitCount); - if(sizeCrcBitCount !== 0) { - offset += sizeBitCount; - // header bits only? - if(bits.length <= offset) return []; - let countCrc = bitsToInt(bits.slice(sizeBitCount, sizeBitCount + sizeCrcBitCount), sizeCrcBitCount); - let actualCountCrc = CRC.check(numberToBytes(sizeCrcBitCount, sizeBitCount), sizeCrcBitCount); - // can we trust the size? - if(countCrc !== actualCountCrc) { - if(dataCrcBitCount !== 0) { - offset += dataCrcBitCount; - if(bits.length <= offset) return []; - } - // Change based off of header bits - byteCount = (bits.length - offset) / 8; - } - } else if(dataCrcBitCount !== 0) { - offset += dataCrcBitCount; - if(bits.length <= offset) return []; - } - // remove headers and excessive bits - const bitCount = byteCount * 8; - return bits.slice(offset, offset + bitCount).splice(bitCount); - } else { - // size not included. Means 1 byte max - // crc not valid on size - // crc valid on byte - const dataCrcBitCount = packetizationPanel.getDataCrc(); - if(dataCrcBitCount === 0) { - // bits are pure data for 1 byte - return bits.slice(0, 8); - } else { - // get byte after data crc - return bits.slice(dataCrcBitCount, dataCrcBitCount + 8); - } - } -} -const bitReducer = (packetBitSize, blockSize, blockCallback) => (all, bit, i) => { - const packetIndex = Math.floor(i / packetBitSize); - if(i % packetBitSize === 0) { - all += `Packet ${packetIndex}`; - } - const packetBitIndex = i % packetBitSize; - if(packetBitIndex % blockSize === 0) { - if(blockCallback) { - const blockIndex = Math.floor(packetBitIndex / blockSize); - return all + blockCallback(packetIndex, blockIndex) + bit; - } - return all + ' ' + bit; - } - return all + bit; -} -const bitExpectorReducer = (expected, packetBitSize, blockSize, blockCallback) => (all, bit, i) => { - const packetIndex = Math.floor(i / packetBitSize); - if(i % packetBitSize === 0) { - all += `Packet ${packetIndex}`; - } - const packetBitIndex = i % packetBitSize; - - if(packetBitIndex % blockSize === 0) { - if(blockCallback) { - const blockIndex = Math.floor(packetBitIndex / blockSize) - all += blockCallback(packetIndex, blockIndex); - } else if (packetBitIndex !== 0) { - all += ' '; - } - } - if(i >= expected.length) { - all += ''; - } else if(expected[i] !== bit) { - all += ''; - } - all += bit.toString(); - if(i >= expected.length || expected[i] !== bit) { - all += ''; - } - return all; -} -const textExpectorReducer = expected => { - - const expectedChars = expected.split(''); - - return (all, char, i) => { - const html = htmlEncode(char); - if(i >= expected.length) { - all += '' + html + ''; - } else if(char !== expectedChars[i]) { - all += '' + html + ''; - } else { - all += html; - } - return all; - }; -} -function htmlEncode(text) { - const element = document.createElement('div'); - element.textContent = text; - return element.innerHTML; -} -function resetGraphData() { - SAMPLES.length = 0; - bitStart.length = 0; -} function getAudioContext() { if(!audioContext) { audioContext = new (window.AudioContext || webkitAudioContext)(); @@ -765,464 +554,12 @@ function getAnalyser() { function handleChangeSendAnalyzer({checked}) { SEND_VIA_SPEAKER = !checked; configurationChanged(); + AudioSender.setDestination(getAnalyser()) } function handleChangeSendSpeakers({checked}) { SEND_VIA_SPEAKER = checked; configurationChanged(); + AudioSender.setDestination(audioContext.destination); } -function drawChannelData() { - // Do/did we have a stream? - if(!RECEIVED_STREAM_START_MS) return; - - const latest = SAMPLES[0].time; - - // will any of the stream appear? - const packetBitCount = PacketUtils.getPacketMaxBitCount(); - - const packetDuration = PacketUtils.getPacketDurationMilliseconds(); - const lastStreamEnded = RECEIVED_STREAM_START_MS + packetDuration; - const graphDuration = graphConfigurationPanel.getDurationMilliseconds(); - const graphEarliest = latest - graphDuration; - // ended too long ago? - if(lastStreamEnded < graphEarliest) return; - - const channels = availableFskPairsPanel.getSelectedFskPairs(); - const channelCount = channels.length; - - const canvas = document.getElementById('received-channel-graph'); - - clearCanvas(canvas); - const ctx = canvas.getContext('2d'); - const {height, width} = canvas; - - // Loop through visible segments - const latestSegmentEnded = Math.min(latest, lastStreamEnded); - for(let time = latestSegmentEnded; time > graphEarliest; time -= signalPanel.getSegmentDurationMilliseconds()) { - // too far back? - if(time < RECEIVED_STREAM_START_MS) break; - - // which segment are we looking at? - const segmentIndex = PacketUtils.getPacketSegmentIndex(RECEIVED_STREAM_START_MS, time); - // when did the segment begin - const packetIndex = PacketUtils.getPacketIndex(RECEIVED_STREAM_START_MS, time); - const segmentEnd = PacketUtils.getPacketSegmentEndMilliseconds(RECEIVED_STREAM_START_MS, packetIndex, segmentIndex); - - // where is the segments left x coordinate? - const leftX = ((latest - segmentEnd) / graphDuration) * width; - - // what bits did we receive for the segment? - let segmentBits = StreamManager.getPacketSegmentBits(packetIndex, segmentIndex); - if(!segmentBits){ - // unprocessed bits - let's grab them from the samples - segmentBits = GET_SEGMENT_BITS(RECEIVED_STREAM_START_MS, segmentIndex, packetIndex, true); - } - // draw segment data background - let expectedBitCount = channelCount; - if(segmentEnd === lastStreamEnded) { - expectedBitCount = packetBitCount % channelCount; - } else if(segmentEnd > lastStreamEnded) { - continue; - } - drawSegmentBackground( - ctx, - segmentIndex, - leftX, - expectedBitCount, - channelCount, - width, - height - ) - - for(let channelIndex = 0; channelIndex < channelCount; channelIndex++) { - // get received bit - const receivedBit = segmentBits[channelIndex]; - // identify expected bit - const bitIndex = channelIndex + (segmentIndex * channelCount); - if(bitIndex >= SENT_TRANSFER_BITS.length) break; - const expectedBit = SENT_TRANSFER_BITS[bitIndex]; - - drawChannelSegmentBackground( - ctx, - leftX, - segmentIndex, - channelIndex, - channelCount, - height, - width, - receivedBit, - expectedBit - ); - - drawChannelSegmentForeground( - ctx, - leftX, - channelIndex, - channelCount, - height, - width, - receivedBit, - expectedBit - ); - } - } - drawChannelByteMarkers(ctx, channelCount, width, height); - drawSelectedChannel(ctx, channelCount, width, height); - drawChannelNumbers(ctx, channelCount, width, height) -} -function clearCanvas(canvas) { - const ctx = canvas.getContext('2d'); - const {height, width} = canvas; - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, width, height); -} -function drawSegmentBackground( - ctx, - segmentIndex, - leftX, - expectedBitCount, - channelCount, - width, - height -) { - const segmentWidth = width / (graphConfigurationPanel.getDurationMilliseconds() / signalPanel.getSegmentDurationMilliseconds()); - - const hue = 120; - let luminance = segmentIndex % 2 === 0 ? 30 : 25; - if(SEGMENT_SELECTED === segmentIndex || SEGMENT_OVER === segmentIndex) luminance += 15; - - ctx.fillStyle = `hsl(${hue}, 100%, ${luminance}%)`; - const segmentHeight = (expectedBitCount / channelCount) * height - ctx.fillRect(leftX, 0, segmentWidth, segmentHeight); -} -function drawChannelSegmentForeground( - ctx, - endX, - channelIndex, - channelCount, - height, - width, - actualBit, - expectedBit -) { - const channelHeight = height / channelCount; - const segmentWidth = width / (graphConfigurationPanel.getDurationMilliseconds() / signalPanel.getSegmentDurationMilliseconds()); - let fontHeight = Math.min(24, channelHeight, segmentWidth); - let top = channelHeight * channelIndex; - ctx.font = `${fontHeight}px Arial`; - const size = ctx.measureText(actualBit.toString()); - ctx.textBaseline = 'middle'; - const textTop = top + (channelHeight / 2); - if(actualBit === expectedBit) { - ctx.strokeStyle = actualBit !== expectedBit ? 'black' : 'black'; - ctx.lineWidth = 2; - ctx.strokeText(actualBit.toString(), endX + (segmentWidth/2) - (size.width / 2), textTop); - } - ctx.fillStyle = actualBit !== expectedBit ? '#2d0c0c' : 'white'; - ctx.fillText(actualBit.toString(), endX + (segmentWidth/2) - (size.width / 2), textTop); - -} -function drawChannelSegmentBackground( - ctx, - endX, - segmentIndex, - channelIndex, - channelCount, - height, - width, - actualBit, - expectedBit -) { - const isSelectedOrOver = - (CHANNEL_OVER === channelIndex && SEGMENT_OVER === segmentIndex) || - (CHANNEL_SELECTED === channelIndex && SEGMENT_SELECTED === segmentIndex); - - const isCorrect = expectedBit === actualBit; - if(isCorrect && !isSelectedOrOver) return; - - // color red if received bit does not match expected bit - const hue = isCorrect ? 120 : 0; - let luminance = isCorrect ? 50 : 80; - if(isSelectedOrOver) luminance += 15; - - const channelHeight = height / channelCount; - const segmentWidth = width / (graphConfigurationPanel.getDurationMilliseconds() / signalPanel.getSegmentDurationMilliseconds()); - let top = channelHeight * channelIndex; - ctx.fillStyle = `hsl(${hue}, 100%, ${luminance}%)`; - ctx.fillRect(endX, top, segmentWidth, channelHeight); - - ctx.lineWidth = 0.5; - ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; - ctx.strokeRect(endX, top, segmentWidth, channelHeight); -} -function drawChannelByteMarkers(ctx, channelCount, width, height) { - const channelHeight = height / channelCount; - for(let channelIndex = 8; channelIndex < channelCount; channelIndex+= 8) { - let top = channelHeight * channelIndex; - ctx.strokeStyle = 'black'; - ctx.lineWidth = 3; - ctx.beginPath(); - ctx.moveTo(0, top); - ctx.lineTo(width, top); - ctx.stroke(); - } -} -function drawSelectedChannel(ctx, channelCount, width, height) { - const channelHeight = height / channelCount; - ctx.globalCompositionOperation = 'overlay'; - ctx.fillStyle = 'hsla(0, 0%, 100%, 0.25)'; - if(CHANNEL_OVER !== -1) { - ctx.fillRect(0, CHANNEL_OVER * channelHeight, width, channelHeight); - } - if(CHANNEL_SELECTED !== -1 && CHANNEL_SELECTED !== CHANNEL_OVER) { - ctx.fillRect(0, CHANNEL_SELECTED * channelHeight, width, channelHeight); - } - ctx.globalCompositionOperation = 'source-over'; -} -function drawChannelNumbers(ctx, channelCount, width, height) { - const offset = 0; - const channels = availableFskPairsPanel.getSelectedFskPairs(); - const channelHeight = height / channelCount; - const segmentWidth = width / (graphConfigurationPanel.getDurationMilliseconds() / signalPanel.getSegmentDurationMilliseconds()); - let fontHeight = Math.min(24, channelHeight, segmentWidth); - ctx.font = `${fontHeight}px Arial`; - ctx.textBaseline = 'middle'; - ctx.fillStyle = 'rgba(0, 0, 0, .5)'; - const maxDigits = (channelCount - 1).toString().length; - ctx.fillRect(offset, 0, (fontHeight * maxDigits), channelHeight * channelCount); - for(let channelIndex = 0; channelIndex < channelCount; channelIndex++) { - let top = channelHeight * channelIndex; - let text = realChannel(channelIndex).toString(); - const textTop = top + (channelHeight / 2); - // const hue = channelHue(channelIndex, channelCount); - const highHue = hzHue(channels[channelIndex][1]); - ctx.fillStyle = `hsl(${highHue}, 100%, 50%)`; - ctx.fillText(text, offset + 5, textTop); - } -} -function realChannel(id) { - EXCLUDED_CHANNELS.sort(compareNumbers); - for(let i = 0; i < EXCLUDED_CHANNELS.length; i++) { - if(EXCLUDED_CHANNELS[i] <= id) id++; - } - return id; -} -function drawFrequencyData(forcedDraw) { - if(PAUSE && forcedDraw !== true) return; - if(SAMPLES.length === 0) { - if(forcedDraw !== true) { - requestAnimationFrame(drawFrequencyData); - } - return; - } - drawChannelData(); - requestAnimationFrame(drawFrequencyData); -} - -function hzHue(hz) { - return Math.floor((hz / 20000) * 360); -} - -function handleReceivedChannelGraphMouseover(e) { - const {channelIndex, segmentIndex} = getChannelAndSegment(e); - CHANNEL_OVER = channelIndex; - SEGMENT_OVER = segmentIndex; - requestAnimationFrame(drawFrequencyData.bind(null, true)); -} -function handleReceivedChannelGraphMouseout(e) { - CHANNEL_OVER = -1; - SEGMENT_OVER = -1; - requestAnimationFrame(drawFrequencyData.bind(null, true)); -} -function handleReceivedChannelGraphMousemove(e) { - const {channelIndex, segmentIndex} = getChannelAndSegment(e); - CHANNEL_OVER = channelIndex; - SEGMENT_OVER = segmentIndex; - requestAnimationFrame(drawFrequencyData.bind(null, true)); -} -function mouseXy({clientX, clientY, target}) { - const rect = target.getBoundingClientRect(); - return { - x: clientX - rect.left, - y: clientY - rect.top - } -} -function handleReceivedChannelGraphClick(e) { - const {channelIndex, segmentIndex} = getChannelAndSegment(e); - CHANNEL_SELECTED = channelIndex; - SEGMENT_SELECTED = segmentIndex; - const channels = availableFskPairsPanel.getSelectedFskPairs(); - const channelCount = channels.length; - - const selectedSamples = document.getElementById('selected-samples'); - selectedSamples.innerHTML = ""; - - function addLowHigh(info, low, high) { - const div = document.createElement('div'); - div.className = 'low-high-set' - const infoDiv = document.createElement('div'); - infoDiv.className = 'ingo'; - const lowDiv = document.createElement('div'); - lowDiv.className = 'low'; - const highDiv = document.createElement('div'); - highDiv.className = 'high'; - infoDiv.innerText = info; - lowDiv.innerText = low; - highDiv.innerText = high; - if(low === 255) lowDiv.classList.add('max'); - if(high === 255) highDiv.classList.add('max'); - if(typeof low === 'number' && typeof high === 'number') { - if(low > high) lowDiv.classList.add('highest'); - else highDiv.classList.add('highest'); - } - div.appendChild(infoDiv); - div.appendChild(lowDiv); - div.appendChild(highDiv); - selectedSamples.appendChild(div); - } - - if(CHANNEL_SELECTED !== -1) { - addLowHigh('', 'Low', 'High'); - addLowHigh('Hz', - channels[CHANNEL_SELECTED][0].toLocaleString(), - channels[CHANNEL_SELECTED][1].toLocaleString() - ) - } - if(SEGMENT_SELECTED === -1) { - document.getElementById('selected-segment').innerText = 'N/A'; - } else { - document.getElementById('selected-segment').innerText = SEGMENT_SELECTED; - if(CHANNEL_SELECTED !== -1) { - - const bitIndex = CHANNEL_SELECTED + (SEGMENT_SELECTED * channelCount); - document.getElementById('selected-bit').innerText = bitIndex.toLocaleString(); - - const samples = SAMPLES - .filter(fot => fot.segmentIndex === SEGMENT_SELECTED) - .map(fot => fot.pairs[CHANNEL_SELECTED]); - samples.forEach(([low, high], i) => { - if(i === 0) { - addLowHigh(`Amplitude ${i}`, low, high); - } else { - [priorLow, priorHigh] = samples[i - 1]; - addLowHigh(`Amplitude ${i}`, - priorLow === low ? '"' : low, - priorHigh === high ? '"' : high - ); - - } - }); - - const expectedBit = SENT_TRANSFER_BITS[bitIndex]; - const receivedBit = packetReceivedBits[bitIndex]; - addLowHigh('Expected Bit', expectedBit === 1 ? '' : '0', expectedBit === 1 ? '1' : '') - addLowHigh('Received Bit', receivedBit === 1 ? '' : '0', receivedBit === 1 ? '1' : '') - - const sums = samples.reduce((sum, [low, high]) => { - sum[0]+= low; - sum[1]+= high; - return sum; - }, [0, 0]); - addLowHigh('Total', sums[0], sums[1]); - - const sorts = samples.reduce((sum, [low, high]) => { - sum.low.push(low); - sum.high.push(high) - return sum; - }, {low: [], high: []}); - sorts.low.sort(compareNumbers); - sorts.high.sort(compareNumbers); - const middleIndex = Math.floor(samples.length / 2); - - addLowHigh('Median', sorts.low[middleIndex], sorts.high[middleIndex]); - - } - } - - if(CHANNEL_SELECTED === -1) { - document.getElementById('selected-channel').innerText = 'N/A'; - } else { - document.getElementById('selected-channel').innerText = realChannel(CHANNEL_SELECTED); - } - - - requestAnimationFrame(drawFrequencyData.bind(null, true)); -} -function compareNumbers(a, b) { - return a - b; -} -function getChannelAndSegment(e) { - const {width, height} = e.target.getBoundingClientRect(); - const {x,y} = mouseXy(e); - if(y < 0 || x < 0 || y > height || x > width) return { - channelIndex: -1, - segmentIndex: -1 - }; - // what channel are we over? - const channels = availableFskPairsPanel.getSelectedFskPairs(); - const channelCount = channels.length; - let channelIndex = Math.floor((y / height) * channelCount); - if(channelIndex === channelCount) channelIndex--; - - // what segment are we over? - // Do/did we have a stream? - if(!RECEIVED_STREAM_START_MS) { - return { - channelIndex, - segmentIndex: -1 - }; - } - const latest = SAMPLES[0]?.time ?? performance.now(); - // will any of the stream appear? - const packetDuration = PacketUtils.getPacketDurationMilliseconds(); - const lastStreamEnded = RECEIVED_STREAM_START_MS + packetDuration; - const graphDuration = graphConfigurationPanel.getDurationMilliseconds(); - const graphEarliest = latest - graphDuration; - // ended too long ago? - if(lastStreamEnded < graphEarliest) { - return { - channelIndex, - segmentIndex: -1 - }; - } - - const segmentWidth = width / (graphConfigurationPanel.getDurationMilliseconds() / signalPanel.getSegmentDurationMilliseconds()); - - const latestSegmentEnded = Math.min(latest, lastStreamEnded); - - for(let time = latestSegmentEnded; time > graphEarliest; time -= signalPanel.getSegmentDurationMilliseconds()) { - // too far back? - if(time < RECEIVED_STREAM_START_MS) { - return { - channelIndex, - segmentIndex: -1 - } - }; - - // which segment are we looking at? - const segmentIndex = Math.floor(((time - RECEIVED_STREAM_START_MS) / signalPanel.getSegmentDurationMilliseconds())); - - // when did the segment begin/end - const segmentStart = RECEIVED_STREAM_START_MS + (segmentIndex * signalPanel.getSegmentDurationMilliseconds()); - const segmentEnd = segmentStart + signalPanel.getSegmentDurationMilliseconds(); - - // where is the segments left x coordinate? - const leftX = ((latest - segmentEnd) / graphDuration) * width; - // where is the segments right x coordinate? - const rightX = leftX + segmentWidth; - - if(x >= leftX && x <= rightX) { - return { - channelIndex, - segmentIndex - } - } - } - - return { - channelIndex, - segmentIndex: -1 - } - -} window.addEventListener('load', handleWindowLoad); \ No newline at end of file