diff --git a/Humanize.js b/Humanize.js index 44b3745..c2ead57 100644 --- a/Humanize.js +++ b/Humanize.js @@ -9,6 +9,17 @@ export function byteSize(count) { count = Math.floor(count * 10) * 0.1 return `${count.toLocaleString()} ${units[unitIndex]}` } +export function hertz(hz) { + let unitIndex = 0; + const units = ['Hz', 'kHz', 'mHz', 'gHz', 'tHz', 'pHz']; + while(hz >= 1000) { + hz /= 1000; + unitIndex++; + if(unitIndex === units.length - 1) break; + } + hz = Math.floor(hz * 1000) * 0.001 + return `${hz.toLocaleString()} ${units[unitIndex]}` +} export function bitsPerSecond(bps) { let unitIndex = 0; const units = ['baud', 'Kbps', 'Mbps', 'Gbps', 'Tbps', 'Pbps']; diff --git a/Panels/AvailableFskPairsPanel.js b/Panels/AvailableFskPairsPanel.js new file mode 100644 index 0000000..55a45c4 --- /dev/null +++ b/Panels/AvailableFskPairsPanel.js @@ -0,0 +1,123 @@ +import BasePanel from './BasePanel'; +import { hertz } from '../Humanize'; +class AvailableFskPairsPanel extends BasePanel { + constructor() { + super('Available FSK Pairs'); + this.exclude = []; + this.fskPairs = []; + this.sampleRate = 48000; + + this.addCanvas('fsk-spectrum', 200, 32); + this.addNewLine(); + + this.addCheckboxes('fsk-pairs', this.fskPairs); + this.addDynamicText('fsk-available', 'None'); + + this.addEventListener('select', this.handleSelect); + + this.drawFskSpectrum(); + }; + setSampleRate = (value) => { + this.sampleRate = value; + this.drawFskSpectrum(); + } + + handleSelect = (event) => { + if(event.checked) { + this.exclude = this.exclude.filter(id => id !== event.id) + } else if(!this.exclude.includes(event.id)) { + this.exclude.push(event.id); + } + this.drawFskSpectrum(); + }; + + getSelectedFskPairs = () => this.fskPairs + .filter(this.isSelected); + + getSelectedIndexes = () => this.fskPairs.map((_, id) => id).filter(id => !this.exclude.includes(id)); + setSelectedIndexes = (values) => { + this.exclude = values; + this.setFskPairs(this.fskPairs); + } + + setFskPairs = fskPairs => { + this.fskPairs = fskPairs; + this.setValueById('fsk-available', fskPairs.length === 0 ? 'None' : ''); + const items = fskPairs.map(([lowHz, highHz], index) => ({ + text: `${index}: ${hertz(lowHz)} / ${hertz(highHz)}`, + id: index, + value: index, + checked: !this.exclude.includes(index), + eventName: 'select' + })); + this.replaceCheckedInputs('checkbox', 'fsk-pairs', items); + this.drawFskSpectrum(); + } + + drawFskSpectrum = () => { + + const ultimateFrequency = this.sampleRate / 2; + const fskPairs = this.fskPairs; + const canvas = this.getElement('fsk-spectrum'); + const ctx = canvas.getContext('2d'); + const {height, width} = canvas; + ctx.clearRect(0, 0, width, height); + + if(fskPairs.length === 0) return; + const minHz = fskPairs.reduce( + (min, fsk) => { + const lowestHz = fsk.reduce((min, hz) => Math.min(min, hz), Infinity) + return Math.min(min, lowestHz) + }, Infinity + ); + const maxHz = fskPairs.reduce( + (max, fsk) => { + const lowestHz = fsk.reduce((max, hz) => Math.max(max, hz), -Infinity) + return Math.max(max, lowestHz) + }, -Infinity + ); + const range = maxHz - minHz; + + // Human Hearing + let x1 = Math.max(0, ((20-minHz)/range) * width); + let x2 = Math.min(width, ((20000-minHz)/range) * width); + if(x1 !== x2) { + ctx.fillStyle = 'hsla(0, 0%, 100%, 20%)'; + ctx.fillRect( + x1, + 0, + x2 - x1, + height + ); + } + + // Telephone + x1 = Math.max(0, ((300-minHz)/range) * width); + x2 = Math.min(width, ((3400-minHz)/range) * width); + if(x1 !== x2) { + ctx.fillStyle = 'hsla(60, 50%, 50%, 20%)'; + ctx.fillRect( + x1, + 0, + x2 - x1, + height + ); + } + + ctx.lineWith = 1; + const plotHz = hz => { + const hue = Math.floor(hz/ultimateFrequency * 360); + ctx.strokeStyle = `hsl(${hue}, 100%, 50%)`; + const x = ((hz-minHz) / range) * width; + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, height); + ctx.stroke(); + } + + fskPairs.filter(this.isSelected).forEach(fsk => fsk.forEach(plotHz)); + } + isSelected = (delude, allude) => !this.exclude.includes(allude); +} + +export default AvailableFskPairsPanel; \ No newline at end of file diff --git a/Panels/BasePanel.js b/Panels/BasePanel.js index d4afe5a..c83d775 100644 --- a/Panels/BasePanel.js +++ b/Panels/BasePanel.js @@ -65,6 +65,20 @@ class BasePanel { this.append(select); } addCheckedInputs = (type, name, items, value) => { + const div = document.createElement('div'); + div.id = this.childId(name); + this.createCheckedInputs(type, name, items, value) + .forEach(element => div.appendChild(element)); + this.append(div); + } + replaceCheckedInputs = (type, name, items, value) => { + const div = this.getElement(name); + div.innerHTML = ''; + this.createCheckedInputs(type, name, items, value) + .forEach(element => div.appendChild(element)); + } + createCheckedInputs = (type, name, items, value) => { + const elements = []; items.forEach(({id, text, checked = false, eventName = 'change'})=> { const label = document.createElement('label'); label.for = this.childId(id); @@ -72,9 +86,10 @@ class BasePanel { label.appendChild(input); const textNode = document.createTextNode(text); label.append(textNode); - this.append(label); - this.addNewLine(); + elements.push(label); + elements.push(document.createElement('br')); }); + return elements; } addInputText = (id, value, options = {}) => { this.append(this.createInput(id, value, {...options, type: 'text'})); diff --git a/Panels/FrequencyPanel.js b/Panels/FrequencyPanel.js index cad059f..4d5e32c 100644 --- a/Panels/FrequencyPanel.js +++ b/Panels/FrequencyPanel.js @@ -1,11 +1,14 @@ import BasePanel from './BasePanel'; class FrequencyPanel extends BasePanel { - constructor(sampleRate = 48000) { + constructor() { super('Frequencies'); - this.sampleRate = sampleRate; - const ultimateFrequency = sampleRate / 2; + this.sampleRate = 48000; + const ultimateFrequency = this.sampleRate / 2; + + this.addCanvas('frequency-spectrum', 200, 32); + this.addNewLine(); this.openField('Minimum'); this.addInputNumber('minimum-frequency', 0, {min: 0, max: ultimateFrequency, eventName: 'minimumFrequencyChange'}); @@ -35,9 +38,6 @@ class FrequencyPanel extends BasePanel { this.addInputNumber('multi-fsk-padding', 0, {min: 0, max: 20, eventName: 'multiFskPaddingChange'}); this.closeField(); - this.addCanvas('frequency-spectrum', 200, 32); - this.addNewLine(); - this.openField('FSK Pairs Available'); this.addDynamicText('fsk-count', 'N/A'); this.closeField(); @@ -51,6 +51,11 @@ class FrequencyPanel extends BasePanel { this.originalFskPairs = this.getFskPairs(); this.drawFrequencySpectrum(); }; + setSampleRate = (value) => { + this.sampleRate = value; + this.checkFskPairsChanged(); + } + getMinimumFrequency = () => parseInt(this.getValueById('minimum-frequency')); setMinimumFrequency = value => { this.setValueById('minimum-frequency', value); @@ -96,10 +101,8 @@ class FrequencyPanel extends BasePanel { if(original.length !== current.length) { changed = true; } else { - changed = original.some( - (fsk, fskIndex) => { - return fsk.some((hz, hzIndex) => hz !== original[fskIndex][hzIndex]); - }) + const currentHz = current.flat(); + changed = original.flat().some((hz, i) => hz !== currentHz[i]); } if(changed) { this.originalFskPairs = current; diff --git a/Panels/PacketizationPanel.js b/Panels/PacketizationPanel.js new file mode 100644 index 0000000..55d5c30 --- /dev/null +++ b/Panels/PacketizationPanel.js @@ -0,0 +1,43 @@ +import BasePanel from './BasePanel'; +import { byteSize } from '../Humanize'; + +class PacketizationPanel extends BasePanel { + constructor() { + super('Packetization'); + + this.openField('Packet Size'); + this.addText('2^'); + this.addInputNumber('size-power', 5, {min: 0, max: 16, eventName: 'sizePowerChange', translation: 'power of 2'}); + this.addText(' '); + this.addDynamicText('size') + this.closeField(); + + this.addSection('Encoding'); + + this.addCheckboxes('packet-encoding', [ + { text: 'Interleaving', id: 'interleaving', checked: true, eventName: 'interleavingChange' }, + { text: 'Error Correction', id: 'error-correction', checked: true, eventName: 'errorCorrectionChange' }, + ]); + + this.addEventListener('sizePowerChange', this.handleSizePowerChange); + this.dispatcher.emit('sizePowerChange', {value: this.getSize()}); + }; + + getSizePower = () => parseInt(this.getValueById('size-power')); + setSizePower = (value) => { + this.setValueById('size-power', value); + this.handleSizePowerChange({value}); + } + getSize = () => 2 ** this.getSizePower(); + handleSizePowerChange = () => { + this.setValueById('size', byteSize(this.getSize())); + } + + getInterleaving = () => this.getCheckedById('interleaving'); + setInterleaving = (value) => this.setCheckedById('interleaving', value); + + getErrorCorrection = () => this.getCheckedById('error-correction'); + setErrorCorrection = (value) => this.setCheckedById('error-correction', value); +} + +export default PacketizationPanel; \ No newline at end of file diff --git a/index.html b/index.html index 63f7dfc..bab1d9e 100644 --- a/index.html +++ b/index.html @@ -10,28 +10,6 @@

Data Over Audio

-
-

Packetizaton

-
- Packet Size: - 2^ - -
-

Encoding

-
-
-
-
-
-

Channels

-
-
    -
    -

    Frequency Graph

    diff --git a/index.js b/index.js index a911a99..1848832 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,8 @@ 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"; var audioContext; var microphoneStream; @@ -32,8 +34,6 @@ let SENT_TRANSFER_BITS = []; // bits sent in the transfer let EXCLUDED_CHANNELS = []; var MAX_BITS_DISPLAYED_ON_GRAPH = 79; -var HAMMING_ERROR_CORRECTION = true; -let PERIODIC_INTERLEAVING = true; const ERROR_CORRECTION_BLOCK_SIZE = 7; const ERROR_CORRECTION_DATA_SIZE = 4; @@ -49,7 +49,6 @@ let SAMPLES = []; var bitStart = []; var PAUSE = false; var PAUSE_AFTER_END = true; -var PACKET_SIZE_BITS = 5; // 32 bytes, 256 bits let USED_FSK = []; let AVAILABLE_FSK = []; @@ -60,9 +59,13 @@ 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(); function handleWindowLoad() { const panelContainer = document.getElementById('panel-container'); + panelContainer.prepend(availableFskPairsPanel.getDomElement()); + panelContainer.prepend(packetizationPanel.getDomElement()); panelContainer.prepend(signalPanel.getDomElement()); panelContainer.prepend(frequencyPanel.getDomElement()); panelContainer.prepend(bitsReceivedPanel.getDomElement()); @@ -94,6 +97,12 @@ function handleWindowLoad() { signalPanel.setAmplitudeThreshold(0.78); signalPanel.setSmoothingTimeConstant(0); + packetizationPanel.setSizePower(5); + packetizationPanel.setErrorCorrection(true); + packetizationPanel.setInterleaving(true); + + availableFskPairsPanel.setFskPairs(frequencyPanel.getFskPairs()); + // Communications Events communicationsPanel.addEventListener('listeningChange', handleChangeListening); communicationsPanel.addEventListener('sendSpeakersChange', handleChangeSendSpeakers); @@ -110,12 +119,24 @@ function handleWindowLoad() { }); frequencyPanel.addEventListener('fskPaddingChange', configurationChanged); frequencyPanel.addEventListener('multiFskPaddingChange', configurationChanged); + frequencyPanel.addEventListener('fskPairsChange', ({value}) => { + availableFskPairsPanel.setFskPairs(value); + }); signalPanel.addEventListener('waveformChange', updateAudioSender); signalPanel.addEventListener('segmentDurationChange', configurationChanged); signalPanel.addEventListener('amplitudeThresholdChange', configurationChanged); signalPanel.addEventListener('smoothingConstantChange', configurationChanged); + packetizationPanel.addEventListener('sizePowerChange', configurationChanged); + packetizationPanel.addEventListener('interleavingChange', () => { + StreamManager.setSegmentEncoding( + packetizationPanel.getInterleaving() ? InterleaverEncoding : undefined + ); + configurationChanged(); + }); + packetizationPanel.addEventListener('errorCorrectionChange', configurationChanged); + // Setup audio sender AudioSender.addEventListener('begin', () => messagePanel.setSendButtonText('Stop')); AudioSender.addEventListener('send', handleAudioSenderSend); @@ -135,27 +156,7 @@ function handleWindowLoad() { receivedChannelGraph.addEventListener('mouseout', handleReceivedChannelGraphMouseout); receivedChannelGraph.addEventListener('mousemove', handleReceivedChannelGraphMousemove); receivedChannelGraph.addEventListener('click', handleReceivedChannelGraphClick); - document.getElementById('packet-size-power').value = PACKET_SIZE_BITS; - document.getElementById('packet-size').innerText = Humanize.byteSize(2 ** PACKET_SIZE_BITS); - document.getElementById('packet-size-power').addEventListener('input', event => { - PACKET_SIZE_BITS = parseInt(event.target.value); - document.getElementById('packet-size').innerText = Humanize.byteSize(2 ** PACKET_SIZE_BITS); - configurationChanged(); - }); document.getElementById('pause-after-end').checked = PAUSE_AFTER_END; - document.getElementById('error-correction-hamming').checked = HAMMING_ERROR_CORRECTION; - document.getElementById('error-correction-hamming').addEventListener('change', event => { - HAMMING_ERROR_CORRECTION = event.target.checked; - configurationChanged(); - }) - document.getElementById('periodic-interleaving').checked = PERIODIC_INTERLEAVING; - document.getElementById('periodic-interleaving').addEventListener('change', event => { - PERIODIC_INTERLEAVING = event.target.checked; - configurationChanged(); - StreamManager.setSegmentEncoding( - PERIODIC_INTERLEAVING ? InterleaverEncoding : undefined - ); - }); document.getElementById('pause-after-end').addEventListener('change', event => { PAUSE_AFTER_END = event.target.checked; if(!PAUSE_AFTER_END) resumeGraph(); @@ -178,33 +179,6 @@ function updateFrequencyResolution() { document.getElementById('frequency-count').innerText = frequencyCount.toFixed(2); } -function showChannelList() { - const allChannels = AVAILABLE_FSK; - const channelList = document.getElementById('channel-list'); - channelList.innerHTML = ""; - allChannels.forEach(([low, high], i) => { - const li = document.createElement('li'); - const label = document.createElement('label'); - li.appendChild(label); - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.checked = !EXCLUDED_CHANNELS.includes(i); - checkbox.addEventListener('input', event => { - if(event.target.checked) { - EXCLUDED_CHANNELS = EXCLUDED_CHANNELS.filter(channel => channel !== i) - } else { - EXCLUDED_CHANNELS.push(i); - } - configurationChanged(); - }) - label.append(checkbox); - const text = document.createTextNode(`Low: ${low} Hz High: ${high} Hz`); - label.append(text); - channelList.appendChild(li); - }) - drawChannels(); -} - function handleAudioSenderSend({bits}) { SENT_TRANSFER_BITS.push(...bits); showSentBits(); @@ -217,7 +191,7 @@ function configurationChanged() { updateStreamManager(); updateAudioSender(); updateAudioReceiver(); - showChannelList(); + drawChannels(); updateFrequencyResolution(); updatePacketStats(); } @@ -259,7 +233,7 @@ function updateAudioReceiver() { } function updateStreamManager() { StreamManager.setPacketEncoding( - HAMMING_ERROR_CORRECTION ? HammingEncoding : undefined + packetizationPanel.getErrorCorrection() ? HammingEncoding : undefined ); StreamManager.changeConfiguration({ bitsPerPacket: PacketUtils.getPacketMaxBitCount(), @@ -279,16 +253,16 @@ function updateStreamManager() { } function updatePacketUtils() { PacketUtils.setEncoding( - HAMMING_ERROR_CORRECTION ? HammingEncoding : undefined + packetizationPanel.getErrorCorrection() ? HammingEncoding : undefined ); const bitsPerSegment = USED_FSK.length; PacketUtils.changeConfiguration({ segmentDurationMilliseconds: signalPanel.getSegmentDuration(), - packetSizeBitCount: PACKET_SIZE_BITS, + packetSizeBitCount: packetizationPanel.getSizePower(), dataSizeBitCount: MAXIMUM_PACKETIZATION_SIZE_BITS, dataSizeCrcBitCount: CRC_BIT_COUNT, bitsPerSegment, - packetEncoding: HAMMING_ERROR_CORRECTION, + packetEncoding: packetizationPanel.getErrorCorrection(), packetEncodingBitCount: ERROR_CORRECTION_BLOCK_SIZE, packetDecodingBitCount: ERROR_CORRECTION_DATA_SIZE, }); @@ -479,11 +453,11 @@ function showSentBits() { document.getElementById('sent-data').innerHTML = SENT_ORIGINAL_BITS.reduce(bitReducer( PacketUtils.getPacketMaxBitCount(), - HAMMING_ERROR_CORRECTION ? ERROR_CORRECTION_DATA_SIZE : 8 + packetizationPanel.getErrorCorrection() ? ERROR_CORRECTION_DATA_SIZE : 8 ), ''); // error correcting bits - if(HAMMING_ERROR_CORRECTION) { + if(packetizationPanel.getErrorCorrection()) { document.getElementById('error-correcting-data').innerHTML = SENT_ENCODED_BITS.reduce(bitReducer( PacketUtils.getPacketDataBitCount(), @@ -506,7 +480,7 @@ function sendPacket(bits, packetStartSeconds) { const segmentDurationSeconds = PacketUtils.getSegmentDurationSeconds(); for(let i = 0; i < bitCount; i += channelCount) { let segmentBits = bits.slice(i, i + channelCount); - if(PERIODIC_INTERLEAVING) { + if(packetizationPanel.getInterleaving()) { segmentBits = InterleaverEncoding.encode(segmentBits); } const segmentIndex = Math.floor(i / channelCount); @@ -558,7 +532,7 @@ function getTransferredCorrectedBits() { const packetCount = StreamManager.getPacketReceivedCount(); for(let packetIndex = 0; packetIndex < packetCount; packetIndex++) { let packetBits = StreamManager.getPacketBits(packetIndex); - if(HAMMING_ERROR_CORRECTION) { + if(packetizationPanel.getErrorCorrection()) { bits.push(...HammingEncoding.decode(packetBits)); } else { bits.push(...packetBits); @@ -613,7 +587,7 @@ function handleStreamManagerChange() { (packetIndex, blockIndex) => `${blockIndex === 0 ? '' : '
    '}Segment ${blockIndex}: ` ), '')); - if(HAMMING_ERROR_CORRECTION) { + if(packetizationPanel.getErrorCorrection()) { document.getElementById('received-encoded-bits').innerHTML = allEncodedBits .reduce( bitExpectorReducer( @@ -630,7 +604,7 @@ function handleStreamManagerChange() { bitExpectorReducer( SENT_ORIGINAL_BITS, PacketUtils.getPacketDataBitCount(), - HAMMING_ERROR_CORRECTION ? ERROR_CORRECTION_DATA_SIZE : 8 + packetizationPanel.getErrorCorrection() ? ERROR_CORRECTION_DATA_SIZE : 8 ), ''); document.getElementById('received-packet-original-bytes').innerText = transmissionByteCount.toLocaleString(); @@ -693,7 +667,7 @@ function removeEncodedPadding(bits) { let bitsNeeded = sizeBits; let blocksNeeded = sizeBits; // need to calc max bits - if(HAMMING_ERROR_CORRECTION) { + if(packetizationPanel.getErrorCorrection()) { blocksNeeded = Math.ceil(sizeBits / dataSize); bitsNeeded = blocksNeeded * blockSize; } @@ -708,7 +682,7 @@ function removeEncodedPadding(bits) { // determine how many decoded bits need to be sent (including the size) const totalBits = (dataByteCount * 8) + MAXIMUM_PACKETIZATION_SIZE_BITS + CRC_BIT_COUNT; let encodingBitCount = totalBits; - if(HAMMING_ERROR_CORRECTION) { + if(packetizationPanel.getErrorCorrection()) { const blocks = Math.ceil(encodingBitCount / dataSize); encodingBitCount = blocks * blockSize; } @@ -803,6 +777,8 @@ function resetGraphData() { function getAudioContext() { if(!audioContext) { audioContext = new (window.AudioContext || webkitAudioContext)(); + frequencyPanel.setSampleRate(audioContext.sampleRate); + availableFskPairsPanel.setSampleRate(audioContext.sampleRate); } if(audioContext.state === 'suspended') { audioContext.resume();