import * as StreamManager from "./StreamManager"; import * as HammingEncoding from './HammingEncoding'; import * as InterleaverEncoding from './InterleaverEncoding'; import * as PacketUtils from './PacketUtils'; 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 MessagePanel from "./Panels/MessagePanel"; import CodePanel from "./Panels/CodePanel"; import FrequencyPanel from "./Panels/FrequencyPanel"; import SignalPanel from "./Panels/SignalPanel"; import PacketizationPanel from "./Panels/PacketizationPanel"; import AvailableFskPairsPanel from "./Panels/AvailableFskPairsPanel"; import FrequencyGraphPanel from "./Panels/FrequencyGraphPanel"; import GraphConfigurationPanel from './Panels/GraphConfigurationPanel' import SpeedPanel from './Panels/SpeedPanel'; import { bitsToInt, bitsToBytes, bitsToText, bytesToBits, bytesToText, numberToBytes, numberToBits, numberToHex, textToBits, textToBytes, } from './converters'; import MicrophonePanel from "./Panels/MicrophonePanel"; import ReceivePanel from "./Panels/ReceivePanel"; var audioContext; var microphoneStream; var microphoneNode; var analyser; var sentDataTextArea; // 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 messagePanel = new MessagePanel(); const bitsSentPanel = new CodePanel('Bits Sent'); const bitsReceivedPanel = new CodePanel('Bits Received'); const frequencyPanel = new FrequencyPanel(); const signalPanel = new SignalPanel(); const packetizationPanel = new PacketizationPanel(); const availableFskPairsPanel = new AvailableFskPairsPanel(); const frequencyGraphPanel = new FrequencyGraphPanel(); const graphConfigurationPanel = new GraphConfigurationPanel(); const speedPanel = new SpeedPanel(); const microphonePanel = new MicrophonePanel(); const receivePanel = new ReceivePanel(); function handleWindowLoad() { const panelContainer = document.getElementById('panel-container'); panelContainer.prepend(speedPanel.getDomElement()); panelContainer.prepend(graphConfigurationPanel.getDomElement()); panelContainer.prepend(frequencyGraphPanel.getDomElement()); panelContainer.prepend(packetizationPanel.getDomElement()); panelContainer.prepend(availableFskPairsPanel.getDomElement()); panelContainer.prepend(frequencyPanel.getDomElement()); panelContainer.prepend(signalPanel.getDomElement()); panelContainer.prepend(bitsReceivedPanel.getDomElement()); panelContainer.prepend(bitsSentPanel.getDomElement()); panelContainer.prepend(receivePanel.getDomElement()); panelContainer.prepend(microphonePanel.getDomElement()); panelContainer.prepend(communicationsPanel.getDomElement()); panelContainer.prepend(messagePanel.getDomElement()); // Initialize Values microphonePanel.setListening(false); communicationsPanel.setSendSpeakers(false); communicationsPanel.setSendAnalyzer(true); messagePanel.setMessageText(Randomizer.text(5)); messagePanel.setDataType('image'); messagePanel.setSendButtonText('Send'); messagePanel.addEventListener('dataTypeChange', ({values: [dataType]}) => { receivePanel.setDataType(dataType); }) receivePanel.setDataType(messagePanel.getDataType()); receivePanel.setProgress(0); receivePanel.setReceivedHtml('Ready.'); bitsSentPanel.setCode(''); bitsReceivedPanel.setCode(''); frequencyPanel.setMinimumFrequency(2500); frequencyPanel.setMaximumFrequency(23000); frequencyPanel.setFftSize(2 ** 9); frequencyPanel.setFskPadding(3); frequencyPanel.setMultiFskPadding(4); signalPanel.setWaveform('triangle'); signalPanel.setSegmentDurationMilliseconds(30); signalPanel.setAmplitudeThreshold(0.78); signalPanel.setSmoothingTimeConstant(0); signalPanel.setTimeoutMilliseconds(60); packetizationPanel.setSizePower(5); packetizationPanel.setDataSizePower(16); packetizationPanel.setDataSizeCrc(8); packetizationPanel.setDataCrc(16); packetizationPanel.setErrorCorrection(true); packetizationPanel.setInterleaving(true); availableFskPairsPanel.setFskPairs(frequencyPanel.getFskPairs()); graphConfigurationPanel.setDurationMilliseconds(signalPanel.getSegmentDurationMilliseconds() * 20); graphConfigurationPanel.setPauseAfterEnd(true); frequencyGraphPanel.setFskPairs(availableFskPairsPanel.getSelectedFskPairs()); frequencyGraphPanel.setAmplitudeThreshold(signalPanel.getAmplitudeThreshold()); frequencyGraphPanel.setDurationMilliseconds(graphConfigurationPanel.getDurationMilliseconds()); speedPanel.setMaximumDurationMilliseconds(0); speedPanel.setDataBitsPerSecond(0); speedPanel.setPacketizationBitsPerSecond(0); speedPanel.setTransferDurationMilliseconds(0); AudioReceiver.setTimeoutMilliseconds(signalPanel.getTimeoutMilliseconds()); // Events communicationsPanel.addEventListener('sendSpeakersChange', handleChangeSendSpeakers); communicationsPanel.addEventListener('sendAnalyzerChange', handleChangeSendAnalyzer); messagePanel.addEventListener('messageChange', configurationChanged); messagePanel.addEventListener('sendClick', handleSendButtonClick); messagePanel.addEventListener('stopClick', handleStopButtonClick); frequencyPanel.addEventListener('minimumFrequencyChange', configurationChanged); frequencyPanel.addEventListener('maximumFrequencyChange', configurationChanged); frequencyPanel.addEventListener('fftSizeChange', ({value}) => { configurationChanged(); resetGraphData(); }); frequencyPanel.addEventListener('fskPaddingChange', configurationChanged); frequencyPanel.addEventListener('multiFskPaddingChange', configurationChanged); frequencyPanel.addEventListener('fskPairsChange', ({value}) => { availableFskPairsPanel.setFskPairs(value); }); signalPanel.addEventListener('waveformChange', updateAudioSender); signalPanel.addEventListener('segmentDurationChange', () => { frequencyGraphPanel.setSamplingPeriod(signalPanel.getSegmentDurationMilliseconds()); configurationChanged(); }); signalPanel.addEventListener('amplitudeThresholdChange', ({value}) => { frequencyGraphPanel.setAmplitudeThreshold(value); configurationChanged(); }); signalPanel.addEventListener('smoothingConstantChange', configurationChanged); signalPanel.addEventListener('timeoutChange', () => { AudioReceiver.setTimeoutMilliseconds(signalPanel.getTimeoutMilliseconds()); }) packetizationPanel.addEventListener('sizePowerChange', configurationChanged); packetizationPanel.addEventListener('interleavingChange', () => { StreamManager.setSegmentEncoding( packetizationPanel.getInterleaving() ? InterleaverEncoding : undefined ); configurationChanged(); }); packetizationPanel.addEventListener('errorCorrectionChange', configurationChanged); packetizationPanel.addEventListener('dataSizePowerChange', configurationChanged); packetizationPanel.addEventListener('dataSizeCrcChange', configurationChanged); packetizationPanel.addEventListener('dataCrcChange', configurationChanged); availableFskPairsPanel.addEventListener('change', (event) => { frequencyGraphPanel.setFskPairs(event.selected); configurationChanged(); }); graphConfigurationPanel.addEventListener('pauseAfterEndChange', (event) => { if(!frequencyGraphPanel.isRunning()) { frequencyGraphPanel.start(); } }) graphConfigurationPanel.addEventListener('durationChange', event => { frequencyGraphPanel.setDurationMilliseconds(graphConfigurationPanel.getDurationMilliseconds()); }); receivePanel.addEventListener('start', handleReceivePanelStart); receivePanel.addEventListener('receive', handleReceivePanelReceive); receivePanel.addEventListener('end', handleReceivePanelEnd); // Setup audio sender AudioSender.addEventListener('begin', () => messagePanel.setSendButtonText('Stop')); AudioSender.addEventListener('send', handleAudioSenderSend); AudioSender.addEventListener('end', () => messagePanel.setSendButtonText('Send')); // Setup stream manager StreamManager.addEventListener('change', handleStreamManagerChange); // grab dom elements sentDataTextArea = document.getElementById('sent-data'); 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(); } function updateFrequencyResolution() { const sampleRate = getAudioContext().sampleRate; const fftSize = frequencyPanel.getFftSize(); const frequencyResolution = sampleRate / fftSize; const frequencyCount = (sampleRate/2) / frequencyResolution; document.getElementById('frequency-resolution').innerText = frequencyResolution.toFixed(2); 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(); updateStreamManager(); updateAudioSender(); updateAudioReceiver(); drawChannels(); updateFrequencyResolution(); updatePacketStats(); } function updateAudioSender() { AudioSender.changeConfiguration({ channels: availableFskPairsPanel.getSelectedFskPairs(), destination: SEND_VIA_SPEAKER ? audioContext.destination : getAnalyser(), waveForm: signalPanel.getWaveform() }); } const handleReceivePanelStart = ({signalStart}) => { frequencyGraphPanel.setSignalStart(signalStart); RECEIVED_STREAM_START_MS = signalStart; } const handleReceivePanelReceive = ({signalStart, signalIndex, indexStart, bits}) => { const packetIndex = PacketUtils.getPacketIndex(signalStart, indexStart); const segmentIndex = PacketUtils.getPacketSegmentIndex(signalStart, indexStart); // Getting all 1's for only the first 5 segments? // console.log(signalIndex, packetIndex, segmentIndex, bits.join('')); StreamManager.addBits(packetIndex, segmentIndex, bits); } const handleReceivePanelEnd = (e) => { frequencyGraphPanel.setSignalEnd(e.signalEnd); if(graphConfigurationPanel.getPauseAfterEnd()) { stopGraph(); frequencyGraphPanel.stop(); receivePanel.setIsOnline(false); } } function updateAudioReceiver() { AudioReceiver.changeConfiguration({ fskSets: availableFskPairsPanel.getSelectedFskPairs(), amplitudeThreshold: Math.floor(signalPanel.getAmplitudeThreshold() * 255), analyser: getAnalyser(), signalIntervalMs: signalPanel.getSegmentDurationMilliseconds(), sampleRate: getAudioContext().sampleRate }); } function updateStreamManager() { StreamManager.setPacketEncoding( packetizationPanel.getErrorCorrection() ? HammingEncoding : undefined ); const xferCountLength = packetizationPanel.getDataSizePower(); const xferCountCrcLength = xferCountLength === 0 ? 0 : packetizationPanel.getDataSizeCrc(); const xferCrcLength = packetizationPanel.getDataCrc(); StreamManager.changeConfiguration({ bitsPerPacket: PacketUtils.getPacketMaxBitCount(), segmentsPerPacket: PacketUtils.getPacketSegmentCount(), bitsPerSegment: availableFskPairsPanel.getSelectedFskPairs().length, streamHeaders: { 'transfer byte count': { index: 0, length: xferCountLength }, 'transfer byte count crc': { index: xferCountLength, length: xferCountCrcLength }, 'transfer byte crc': { index: xferCountLength + xferCountCrcLength, length: xferCrcLength }, } }); } function updatePacketUtils() { PacketUtils.setEncoding( packetizationPanel.getErrorCorrection() ? HammingEncoding : undefined ); const bitsPerSegment = availableFskPairsPanel.getSelectedFskPairs().length; PacketUtils.changeConfiguration({ segmentDurationMilliseconds: signalPanel.getSegmentDurationMilliseconds(), packetSizeBitCount: packetizationPanel.getSizePower(), dataSizeBitCount: packetizationPanel.getDataSizePower(), dataSizeCrcBitCount: packetizationPanel.getDataSizeCrc(), dataCrcBitCount: packetizationPanel.getDataCrc(), bitsPerSegment, packetEncoding: packetizationPanel.getErrorCorrection(), packetEncodingBitCount: ERROR_CORRECTION_BLOCK_SIZE, packetDecodingBitCount: ERROR_CORRECTION_DATA_SIZE, }); speedPanel.setMaximumDurationMilliseconds(PacketUtils.getMaxDurationMilliseconds()); speedPanel.setDataBitsPerSecond(PacketUtils.getEffectiveBaud()); speedPanel.setPacketizationBitsPerSecond(PacketUtils.getBaud()); speedPanel.setTransferDurationMilliseconds(PacketUtils.getDataTransferDurationMillisecondsFromByteCount( messagePanel.getMessageBytes().length )); } function updatePacketStats() { const bytes = messagePanel.getMessageBytes(); const bits = bytesToBits(bytes); const byteCount = bytes.length; const bitCount = PacketUtils.getPacketizationBitCountFromBitCount(bits.length);; // Data document.getElementById('original-byte-count').innerText = byteCount.toLocaleString(); document.getElementById('packetization-byte-count').innerText = PacketUtils.getPacketizationByteCountFromBitCount(bits.length).toLocaleString(); document.getElementById('packetization-bit-count').innerText = bitCount.toLocaleString(); document.getElementById('packet-count').innerText = PacketUtils.getPacketCount(bitCount).toLocaleString(); // ## Packet Encoding // Data document.getElementById('last-packet-unused-bit-count').innerText = PacketUtils.fromByteCountGetPacketLastUnusedBitCount(byteCount).toLocaleString(); frequencyGraphPanel.setSamplePeriodsPerGroup(PacketUtils.getPacketSegmentCount()); document.getElementById('total-segments').innerText = getTotalSegmentCount(bitCount).toLocaleString(); } function drawChannels() { const sampleRate = getAudioContext().sampleRate; const fftSize = frequencyPanel.getFftSize(); const frequencyResolution = sampleRate / fftSize; const channels = availableFskPairsPanel.getSelectedFskPairs(); const channelCount = channels.length; const canvas = document.getElementById('channel-frequency-graph'); const ctx = canvas.getContext('2d'); const {height, width} = canvas; const channelHeight = height / channelCount; const bandHeight = channelHeight / 2; for(let i = 0; i < channelCount; i++) { const [low, high] = channels[i]; let top = channelHeight * i; ctx.fillStyle = 'black'; ctx.fillRect(0, top, width, bandHeight); ctx.fillStyle = 'white'; ctx.fillRect(0, top + bandHeight, width, bandHeight); const lowX = percentInFrequency(low, frequencyResolution) * width; ctx.lineWidth = 2; ctx.strokeStyle = 'blue'; ctx.beginPath(); ctx.moveTo(lowX, top); ctx.lineTo(lowX, top + bandHeight); ctx.stroke(); const highX = percentInFrequency(high, frequencyResolution) * width; ctx.lineWidth = 2; ctx.strokeStyle = 'blue'; ctx.beginPath(); ctx.moveTo(highX, top + bandHeight); ctx.lineTo(highX, top + (bandHeight * 2)); ctx.stroke(); } } function percentInFrequency(hz, frequencyResolution) { const index = Math.floor(hz/frequencyResolution); const startHz = index * frequencyResolution; const hzInSegement = hz - startHz; const percent = hzInSegement / frequencyResolution; return percent; } function sendBytes(bytes) { const byteCount = bytes.length; if(byteCount === 0) { document.getElementById('sent-data').innerText = '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)}`; return; } const bits = bytesToBits(bytes); SENT_ORIGINAL_TEXT = bytesToText(bytes); SENT_ORIGINAL_BITS = bits.slice(); // packetization headers // data length let dataLengthBits = []; let dataLengthCrcBits = []; let dataSizeCrcNumber = 0; const dataLengthBitLength = packetizationPanel.getDataSizePower(); if(dataLengthBitLength !== 0) { dataLengthBits = numberToBits(bytes.length, dataLengthBitLength); // crc on data length const dataSizeCrcBitLength = packetizationPanel.getDataSizeCrc(); if(dataSizeCrcBitLength !== 0) { const bytes = bitsToBytes(dataLengthBits); dataSizeCrcNumber = CRC.check(bytes, dataSizeCrcBitLength); dataLengthCrcBits = numberToBits(dataSizeCrcNumber, dataSizeCrcBitLength); } } // crc on data let dataCrcBits = []; const dataCrcBitLength = packetizationPanel.getDataCrc(); let dataCrcNumber = 0; if(dataCrcBitLength !== 0) { dataCrcNumber = CRC.check(bytes, dataCrcBitLength); dataCrcBits = numberToBits(dataCrcNumber, dataCrcBitLength); } console.log('sending headers', { // dataLengthBits, // dataLengthCrcBits, // dataCrcBits, dataCrcHex: numberToHex(packetizationPanel.getDataCrc())(dataCrcNumber), dataSizeCrcHex: numberToHex(packetizationPanel.getDataSizeCrc())(dataSizeCrcNumber), }) // prefix with headers bits.unshift( ...dataLengthBits, ...dataLengthCrcBits, ...dataCrcBits ); const bitCount = bits.length; SENT_TRANSFER_BITS.length = 0; SENT_ENCODED_BITS.length = 0; AudioSender.setAudioContext(getAudioContext()); const startSeconds = AudioSender.now() + 0.1; const packetBitCount = PacketUtils.getPacketMaxBitCount(); const packetDurationSeconds = PacketUtils.getPacketDurationSeconds(); const packetCount = PacketUtils.getPacketCount(bitCount); const totalDurationSeconds = PacketUtils.getDataTransferDurationSeconds(bitCount); AudioSender.beginAt(startSeconds); // send all packets for(let i = 0; i < packetCount; i++) { let packet = PacketUtils.getPacketBits(bits, i); SENT_ENCODED_BITS.push(...packet); if(packet.length > packetBitCount) { console.error('Too many bits in the packet. tried to send %s, limited to %s', packet.length, packetBitCount); AudioSender.stop(); return; } packet = padArray(packet, packetBitCount, 0); sendPacket(packet, startSeconds + (i * packetDurationSeconds)); } AudioSender.stopAt(startSeconds + totalDurationSeconds); 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 = ''; } 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(); const channelCount = channels.length; let bitCount = bits.length; const segmentDurationSeconds = PacketUtils.getSegmentDurationSeconds(); for(let i = 0; i < bitCount; i += channelCount) { let segmentBits = bits.slice(i, i + channelCount); if(packetizationPanel.getInterleaving()) { segmentBits = InterleaverEncoding.encode(segmentBits); } const segmentIndex = Math.floor(i / channelCount); var offsetSeconds = segmentIndex * segmentDurationSeconds; 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.getPacketCount(bitCount) * 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() { const channelCount = availableFskPairsPanel.getSelectedFskPairs().length; let allRawBits = StreamManager.getStreamBits(); let allEncodedBits = StreamManager.getAllPacketBits(); let allDecodedBits = StreamManager.getAllPacketBitsDecoded(); // get packet data before removing decoded bits const transmissionByteCount = StreamManager.getTransferByteCount(); const transmissionByteCountCrc = StreamManager.getTransferByteCountCrc(); const transmissionByteCountActualCrc = StreamManager.getTransferByteCountActualCrc(); const trustedLength = StreamManager.isTransferByteCountTrusted(); const totalBitsTransferring = parseTotalBitsTransferring(allDecodedBits); const transmissionDataCrc = StreamManager.getTransferDataCrc(); const transmissionDataActualCrc = StreamManager.getTransferActualDataCrc(); const transmissionCrc = document.getElementById('received-packet-original-data-crc'); transmissionCrc.innerText = numberToHex(packetizationPanel.getDataCrc())(transmissionDataCrc); const trustedData = StreamManager.isTransferDataTrusted(); transmissionCrc.className = trustedData ? 'bit-correct' : 'bit-wrong'; if(!trustedData) { transmissionCrc.innerText += ' (Expected ' + numberToHex(packetizationPanel.getDataCrc())(transmissionDataActualCrc) + ')'; } // reduce all decoded bits based on original data sent allDecodedBits = removeDecodedHeadersAndPadding(allDecodedBits); // reduce encoded bits based on original data sent allEncodedBits = removeEncodedPadding(allEncodedBits); const encodedBitCount = SENT_ENCODED_BITS.length; const decodedBitCount = SENT_ORIGINAL_BITS.length; const rawBitCount = SENT_TRANSFER_BITS.length; const correctRawBits = allRawBits.filter((b, i) => i < rawBitCount && b === SENT_TRANSFER_BITS[i]).length; const correctEncodedBits = allEncodedBits.filter((b, i) => i < encodedBitCount && b === SENT_ENCODED_BITS[i]).length; const correctedDecodedBits = allDecodedBits.filter((b, i) => i < decodedBitCount && b === SENT_ORIGINAL_BITS[i]).length; let percentReceived = StreamManager.sumTotalBits() / totalBitsTransferring; receivePanel.setProgress(percentReceived); bitsReceivedPanel.setCode(allRawBits .reduce( bitExpectorReducer( SENT_TRANSFER_BITS, PacketUtils.getPacketMaxBitCount() + PacketUtils.getPacketLastSegmentUnusedBitCount(), channelCount, (packetIndex, blockIndex) => `${blockIndex === 0 ? '' : '
'}Segment ${blockIndex}: ` ), '')); if(packetizationPanel.getErrorCorrection()) { document.getElementById('received-encoded-bits').innerHTML = allEncodedBits .reduce( bitExpectorReducer( SENT_ENCODED_BITS, PacketUtils.getPacketDataBitCount(), ERROR_CORRECTION_BLOCK_SIZE ), ''); } else { document.getElementById('received-encoded-bits').innerHTML = 'Not encoded.'; } document.getElementById('received-decoded-bits').innerHTML = allDecodedBits .reduce( bitExpectorReducer( SENT_ORIGINAL_BITS, PacketUtils.getPacketDataBitCount(), packetizationPanel.getErrorCorrection() ? ERROR_CORRECTION_DATA_SIZE : 8 ), ''); document.getElementById('received-packet-original-bytes').innerText = transmissionByteCount.toLocaleString(); const packetCrc = document.getElementById('received-packet-original-bytes-crc'); packetCrc.innerText = numberToHex(packetizationPanel.getDataSizeCrc())(transmissionByteCountCrc); packetCrc.className = trustedLength ? 'bit-correct' : 'bit-wrong'; if(!trustedLength) { packetCrc.innerText += ' (Expected ' + numberToHex(packetizationPanel.getDataSizeCrc())(transmissionByteCountActualCrc) + ')'; } document.getElementById('received-encoded-bits-error-percent').innerText = ( Math.floor((1 - (correctEncodedBits / allEncodedBits.length)) * 10000) * 0.01 ).toLocaleString(); document.getElementById('received-raw-bits-error-percent').innerText = ( Math.floor((1 - (correctRawBits / allRawBits.length)) * 10000) * 0.01 ).toLocaleString(); document.getElementById('received-decoded-bits-error-percent').innerText = ( Math.floor((1 - (correctedDecodedBits / allDecodedBits.length)) * 10000) * 0.01 ).toLocaleString(); 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 parseTotalBitsTransferring() { const dataByteCount = StreamManager.getTransferByteCount(); const bitCount = PacketUtils.getPacketizationBitCountFromByteCount(dataByteCount); const segments = getTotalSegmentCount(bitCount); return segments * availableFskPairsPanel.getSelectedFskPairs().length; } 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)(); frequencyPanel.setSampleRate(audioContext.sampleRate); availableFskPairsPanel.setSampleRate(audioContext.sampleRate); microphonePanel.setAudioContext(audioContext); } if(audioContext.state === 'suspended') { audioContext.resume(); } return audioContext; } function handleStopButtonClick() { AudioSender.stop(); } function handleSendButtonClick() { sendBytes(messagePanel.getMessageBytes()); } function getAnalyser() { if(analyser) return analyser; analyser = audioContext.createAnalyser(); frequencyGraphPanel.setAnalyser(analyser); microphonePanel.setAnalyser(analyser); analyser.smoothingTimeConstant = signalPanel.getSmoothingTimeConstant(); analyser.fftSize = frequencyPanel.getFftSize(); return analyser; } function handleChangeSendAnalyzer({checked}) { SEND_VIA_SPEAKER = !checked; configurationChanged(); } function handleChangeSendSpeakers({checked}) { SEND_VIA_SPEAKER = checked; configurationChanged(); } 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);