Files
data-over-audio/index.js
2024-05-17 15:47:13 -04:00

572 lines
22 KiB
JavaScript

import * as StreamManager from "./StreamManager.js";
import * as HammingEncoding from './HammingEncoding.js';
import * as InterleaverEncoding from './InterleaverEncoding.js';
import * as PacketUtils from './PacketUtils.js';
import * as Humanize from './Humanize.js';
import * as Randomizer from './Randomizer.js';
import * as AudioSender from './AudioSender.js';
import * as AudioReceiver from './AudioReceiver.js';
import OutputPanel from './Panels/OutputPanel.js';
import MessagePanel from "./Panels/MessagePanel.js";
import CodePanel from "./Panels/CodePanel.js";
import FrequencyPanel from "./Panels/FrequencyPanel.js";
import SignalPanel from "./Panels/SignalPanel.js";
import PacketizationPanel from "./Panels/PacketizationPanel.js";
import AvailableFskPairsPanel from "./Panels/AvailableFskPairsPanel.js";
import FrequencyGraphPanel from "./Panels/FrequencyGraphPanel.js";
import GraphConfigurationPanel from './Panels/GraphConfigurationPanel.js';
import PacketErrorPanel from './Panels/PacketErrorPanel.js';
import SpeedPanel from './Panels/SpeedPanel.js';
import {
bytesToText,
} from './converters.js';
import MicrophonePanel from "./Panels/MicrophonePanel.js";
import ReceivePanel from "./Panels/ReceivePanel.js";
var audioContext;
var analyser;
const ERROR_CORRECTION_BLOCK_SIZE = 7;
const ERROR_CORRECTION_DATA_SIZE = 4;
var SEND_VIA_SPEAKER = false;
var PAUSE = false;
const outputPanel = new OutputPanel();
const messagePanel = new MessagePanel();
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();
const availableFskPairsPanel = new AvailableFskPairsPanel();
const frequencyGraphPanel = new FrequencyGraphPanel();
const graphConfigurationPanel = new GraphConfigurationPanel();
const speedPanel = new SpeedPanel();
const microphonePanel = new MicrophonePanel();
const receivePanel = new ReceivePanel();
const packetErrorPanel = new PacketErrorPanel();
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(statusPanel.getDomElement());
panelContainer.prepend(packetErrorPanel.getDomElement());
panelContainer.prepend(receivePanel.getDomElement());
panelContainer.prepend(microphonePanel.getDomElement());
panelContainer.prepend(outputPanel.getDomElement());
panelContainer.prepend(messagePanel.getDomElement());
// Initialize Values
microphonePanel.setListening(false);
outputPanel.setSendSpeakers(false);
outputPanel.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.setExpectedPacketCount(0);
receivePanel.setFailedPacketCount(0);
receivePanel.setSuccessfulPacketCount(0);
receivePanel.setReceivedHtml('Ready.');
packetErrorPanel.reset();
// bitsSentPanel.setCode('');
// bitsReceivedPanel.setCode('');
frequencyPanel.setMinimumFrequency(300);
frequencyPanel.setMaximumFrequency(3400);
frequencyPanel.setFftSize(2 ** 11);
frequencyPanel.setFskPadding(2);
frequencyPanel.setMultiFskPadding(3);
signalPanel.setWaveform('triangle');
signalPanel.setSegmentDurationMilliseconds(30);
signalPanel.setAmplitudeThreshold(0.78);
signalPanel.setSmoothingTimeConstant(0);
signalPanel.setTimeoutMilliseconds(60);
packetizationPanel.setDataSizePower(12);
packetizationPanel.setDataSizeCrc(8);
packetizationPanel.setDataCrc(16);
packetizationPanel.setSizePower(6);
packetizationPanel.setPacketCrc(8);
packetizationPanel.setSequenceNumberPower(8);
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);
// Events
outputPanel.addEventListener('sendSpeakersChange', handleChangeSendSpeakers);
outputPanel.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();
});
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', () => {
const encoding = packetizationPanel.getInterleaving() ? InterleaverEncoding : undefined;
AudioReceiver.setSampleEncoding(encoding);
AudioSender.setSampleEncoding(encoding)
configurationChanged();
});
packetizationPanel.addEventListener('errorCorrectionChange', configurationChanged);
packetizationPanel.addEventListener('dataSizePowerChange', configurationChanged);
packetizationPanel.addEventListener('dataSizeCrcChange', configurationChanged);
packetizationPanel.addEventListener('dataCrcChange', configurationChanged);
packetizationPanel.addEventListener('packetCrcChange', configurationChanged);
packetizationPanel.addEventListener('sequenceNumberPowerChange', configurationChanged);
AudioReceiver.setTimeoutMilliseconds(signalPanel.getTimeoutMilliseconds());
const encoding = packetizationPanel.getInterleaving() ? InterleaverEncoding : undefined;
AudioReceiver.setSampleEncoding(encoding);
AudioSender.setSampleEncoding(encoding)
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);
receivePanel.addEventListener('resetClick', () => {
AudioReceiver.stop();
AudioReceiver.reset();
StreamManager.reset();
packetErrorPanel.reset();
});
packetErrorPanel.addEventListener('requestPackets', requestFailedPackets);
// Setup audio sender
AudioSender.addEventListener('begin', () => messagePanel.setSendButtonText('Stop'));
// 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('sizeReceived', () => {
receivePanel.setExpectedPacketCount(StreamManager.countExpectedPackets());
});
StreamManager.addEventListener('packetFailed', () => {
receivePanel.setFailedPacketCount(StreamManager.countFailedPackets());
packetErrorPanel.setFailedPacketIndeces(StreamManager.getFailedPacketIndeces());
});
StreamManager.addEventListener('packetReceived', () => {
receivePanel.setSuccessfulPacketCount(StreamManager.countSuccessfulPackets());
packetErrorPanel.setSuccessfulPacketCount(StreamManager.countSuccessfulPackets());
// Failed indices changed?
receivePanel.setFailedPacketCount(StreamManager.countFailedPackets());
packetErrorPanel.setFailedPacketIndeces(StreamManager.getFailedPacketIndeces());
if(StreamManager.getSizeCrcAvailable()) {
packetErrorPanel.setSizeCrcPassed(StreamManager.getSizeCrcPassed());
} else {
packetErrorPanel.setSizeCrcUnavailable();
}
if(StreamManager.getCrcAvailable()) {
packetErrorPanel.setCrcPassed(StreamManager.getCrcPassed());
} else {
packetErrorPanel.setCrcUnavailable();
}
receivePanel.setReceivedBytes(StreamManager.getDataBytes());
});
// grab dom elements
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;
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 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);
StreamManager.addSample(packetIndex, segmentIndex, bits);
}
const handleReceivePanelEnd = (e) => {
frequencyGraphPanel.setSignalEnd(e.signalEnd);
if(graphConfigurationPanel.getPauseAfterEnd()) {
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,
dataCrcBitLength: packetizationPanel.getDataCrc(),
dataSizeBitCount: packetizationPanel.getDataSizePower(),
dataSizeCrcBitCount: packetizationPanel.getDataSizeCrc(),
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,
packetSequenceNumberBitCount: packetizationPanel.getSequenceNumberPower(),
packetCrcBitCount: packetizationPanel.getPacketCrc()
});
speedPanel.setMaximumDurationMilliseconds(PacketUtils.getMaxDurationMilliseconds());
speedPanel.setDataBitsPerSecond(PacketUtils.getEffectiveBaud());
speedPanel.setPacketizationBitsPerSecond(PacketUtils.getBaud());
const {
totalDurationSeconds
} = PacketUtils.packetStats(messagePanel.getMessageBytes().length);
speedPanel.setTransferDurationMilliseconds(totalDurationSeconds * 1000);
}
function updatePacketStats() {
const bytes = messagePanel.getMessageBytes();
const byteCount = bytes.length;
const {
transferBitCount,
transferByteCount,
packetCount,
samplePeriodCount
} = PacketUtils.packetStats(byteCount);
// Data
document.getElementById('original-byte-count').innerText = byteCount.toLocaleString();
document.getElementById('packetization-byte-count').innerText = transferByteCount.toLocaleString();
document.getElementById('packetization-bit-count').innerText = transferBitCount.toLocaleString();
document.getElementById('packet-count').innerText = packetCount.toLocaleString();
document.getElementById('last-packet-unused-bit-count').innerText = PacketUtils.fromByteCountGetPacketLastUnusedBitCount(byteCount).toLocaleString();
document.getElementById('total-segments').innerText = samplePeriodCount.toLocaleString();
frequencyGraphPanel.setSamplePeriodsPerGroup(PacketUtils.getPacketSegmentCount());
}
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;
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()) {
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 requestedPacketCount = packetIndeces.length;
AudioSender.setAudioContext(getAudioContext());
const startSeconds = AudioSender.now() + 0.1;
const packetBitCount = PacketUtils.getPacketMaxBitCount();
const packer = PacketUtils.pack(bytes);
try {
AudioSender.beginAt(startSeconds);
// send all packets
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 + (requestedPacketCount * packetDurationSeconds));
} catch (e) {
statusPanel.addText(e);
AudioSender.stop();
return;
}
// start the graph and receiver
if(graphConfigurationPanel.getPauseAfterEnd()) {
receivePanel.setIsOnline(true);
frequencyGraphPanel.start();
}
}
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(segmentBits.length !== channelCount) {
segmentBits.push(...new Array(channelCount - segmentBits.length).fill(0))
}
const segmentIndex = Math.floor(i / channelCount);
var offsetSeconds = segmentIndex * segmentDurationSeconds;
AudioSender.send(segmentBits, packetStartSeconds + offsetSeconds);
}
}
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();
AudioSender.setDestination(getAnalyser())
}
function handleChangeSendSpeakers({checked}) {
SEND_VIA_SPEAKER = checked;
configurationChanged();
AudioSender.setDestination(audioContext.destination);
}
window.addEventListener('load', handleWindowLoad);