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 CRC from './CRC.js';
var audioContext;
var sendButton;
var textToSend;
var isListeningCheckbox;
var microphoneStream;
var microphoneNode;
var analyser;
var receivedDataTextarea;
var sentDataTextArea;
var receivedGraph;
var receivedData = [];
var MAX_AMPLITUDE = 300; // Higher than 255 to give us space
const MAXIMUM_PACKETIZATION_SIZE_BITS = 16;
const CRC_BIT_COUNT = 8;
// bits as they arrived
let RECEIVED_SEGMENT_BITS = []; // RECEIVED_SEGMENT_BITS[packetIndex][segmentIndex][channel] = bit
// 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
// interval and timeout ids
var pauseGraphId;
let stopOscillatorsTimeoutId;
var sampleIntervalIds = [];
let EXCLUDED_CHANNELS = [];
var TEXT_TO_SEND = "U";
var MAX_BITS_DISPLAYED_ON_GRAPH = 79;
var SEGMENT_DURATION = 30;
var AMPLITUDE_THRESHOLD_PERCENT = .75;
var AMPLITUDE_THRESHOLD = 160;
var MINIMUM_FREQUENCY = 9000;
var MAXIMUM_FREQUENCY = 15000;
var LAST_SEGMENT_PERCENT = 0.6;
var FFT_SIZE_POWER = 9;
var FREQUENCY_RESOLUTION_MULTIPLIER = 3;
let CHANNEL_FREQUENCY_RESOLUTION_PADDING = 1;
var SMOOTHING_TIME_CONSTANT = 0;
var HAMMING_ERROR_CORRECTION = true;
let PERIODIC_INTERLEAVING = true;
let WAVE_FORM = "triangle";
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 RECEIVED_STREAM_END_MS = -1;
var MINIMUM_INTERVAL_MS = 3; // DO NOT SET THIS BELOW THE BROWSERS MINIMUM "real" INTERVAL
const SAMPLING_INTERVAL_COUNT = 2;
let SAMPLES = [];
let SAMPLE_LAST_COLLECTED = 0; // time when sample was last collected
var bitStart = [];
var PAUSE = false;
var PAUSE_AFTER_END = true;
var PACKET_SIZE_BITS = 5; // 32 bytes, 256 bits
function handleWindowLoad() {
TEXT_TO_SEND = Randomizer.text(5);
// grab dom elements
sendButton = document.getElementById('send-button');
isListeningCheckbox = document.getElementById('is-listening-checkbox');
receivedDataTextarea = document.getElementById('received-data');
receivedGraph = document.getElementById('received-graph');
textToSend = document.getElementById('text-to-send');
textToSend.value = TEXT_TO_SEND;
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('wave-form').value = WAVE_FORM;
document.getElementById('wave-form').addEventListener('change', (event) => {
WAVE_FORM = event.target.value;
configurationChanged();
});
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();
})
document.getElementById('send-via-speaker').checked = SEND_VIA_SPEAKER;
document.getElementById('send-via-speaker').addEventListener('input', event => {
SEND_VIA_SPEAKER = event.target.checked;
configurationChanged();
})
document.getElementById('frequency-resolution-multiplier').value = FREQUENCY_RESOLUTION_MULTIPLIER;
document.getElementById('frequency-resolution-multiplier').addEventListener('input', event => {
FREQUENCY_RESOLUTION_MULTIPLIER = parseInt(event.target.value);
configurationChanged();
})
document.getElementById('channel-frequency-resolution-padding').value = CHANNEL_FREQUENCY_RESOLUTION_PADDING;
document.getElementById('channel-frequency-resolution-padding').addEventListener('input', event => {
CHANNEL_FREQUENCY_RESOLUTION_PADDING = parseInt(event.target.value);
configurationChanged();
})
document.getElementById('bit-duration-text').addEventListener('input', (event) => {
SEGMENT_DURATION = parseInt(event.target.value);
configurationChanged();
});
document.getElementById('max-bits-displayed-on-graph').value= MAX_BITS_DISPLAYED_ON_GRAPH;
document.getElementById('max-bits-displayed-on-graph').addEventListener('input', (event) => {
MAX_BITS_DISPLAYED_ON_GRAPH = parseInt(event.target.value);
})
document.getElementById('bit-duration-text').value = SEGMENT_DURATION;
document.getElementById('amplitude-threshold-text').value = Math.floor(AMPLITUDE_THRESHOLD_PERCENT * 100);
AMPLITUDE_THRESHOLD = Math.floor(AMPLITUDE_THRESHOLD_PERCENT * 255);
document.getElementById('maximum-frequency').value = MAXIMUM_FREQUENCY;
document.getElementById('minimum-frequency').value = MINIMUM_FREQUENCY;
document.getElementById('last-bit-percent').value = Math.floor(LAST_SEGMENT_PERCENT * 100);
document.getElementById('fft-size-power-text').value = FFT_SIZE_POWER;
document.getElementById('smoothing-time-constant-text').value = SMOOTHING_TIME_CONSTANT.toFixed(2);
document.getElementById('amplitude-threshold-text').addEventListener('input', (event) => {
AMPLITUDE_THRESHOLD_PERCENT = parseInt(event.target.value) / 100;
AMPLITUDE_THRESHOLD = Math.floor(AMPLITUDE_THRESHOLD_PERCENT * 255);
configurationChanged();
});
document.getElementById('maximum-frequency').addEventListener('input', (event) => {
MAXIMUM_FREQUENCY = parseInt(event.target.value);
configurationChanged();
});
document.getElementById('minimum-frequency').addEventListener('input', (event) => {
MINIMUM_FREQUENCY = parseInt(event.target.value);
configurationChanged();
});
document.getElementById('last-bit-percent').addEventListener('input', (event) => {
LAST_SEGMENT_PERCENT = parseInt(event.target.value) / 100;
});
document.getElementById('fft-size-power-text').addEventListener('input', (event) => {
FFT_SIZE_POWER = parseInt(event.target.value);
if(analyser) analyser.fftSize = 2 ** FFT_SIZE_POWER;
configurationChanged();
resetGraphData();
});
document.getElementById('smoothing-time-constant-text').addEventListener('input', event => {
SMOOTHING_TIME_CONSTANT = parseFloat(event.target.value);
if(analyser) analyser.smoothingTimeConstant = SMOOTHING_TIME_CONSTANT;
});
document.getElementById('audio-context-sample-rate').innerText = getAudioContext().sampleRate.toLocaleString();
// wire up events
sendButton.addEventListener('click', handleSendButtonClick);
isListeningCheckbox.addEventListener('click', handleListeningCheckbox);
textToSend.addEventListener('input', configurationChanged);
configurationChanged();
}
function updateFrequencyResolution() {
const sampleRate = getAudioContext().sampleRate;
const fftSize = 2 ** FFT_SIZE_POWER;
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 showChannelList() {
const allChannels = getChannels(true);
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 handleAudioSenderStart() {
sendButton.innerText = 'Stop';
}
function handleAudioSenderStop() {
sendButton.innerText = 'Send';
}
function handleAudioSenderSend(bit) {
SENT_TRANSFER_BITS.push(bit);
}
function configurationChanged() {
updatePacketUtils();
updateStreamManager();
updateAudioSender();
showChannelList();
updateFrequencyResolution();
updatePacketStats();
}
function updateAudioSender() {
AudioSender.changeConfiguration({
channels: getChannels(),
destination: SEND_VIA_SPEAKER ? audioContext.destination : getAnalyser(),
startCallback: handleAudioSenderStart,
stopCallback: handleAudioSenderStop,
sendCallback: handleAudioSenderSend,
waveForm: WAVE_FORM
});
}
function updateStreamManager() {
StreamManager.setPacketEncoding(
HAMMING_ERROR_CORRECTION ? HammingEncoding : undefined
);
StreamManager.changeConfiguration({
bitsPerPacket: PacketUtils.getPacketMaxBitCount(),
segmentsPerPacket: PacketUtils.getPacketSegmentCount(),
bitsPerSegment: getChannels().length,
streamHeaders: {
'transfer byte count': {
index: 0,
length: MAXIMUM_PACKETIZATION_SIZE_BITS
},
'transfer byte count crc': {
index: MAXIMUM_PACKETIZATION_SIZE_BITS,
length: CRC_BIT_COUNT
},
}
});
}
function updatePacketUtils() {
PacketUtils.setEncoding(
HAMMING_ERROR_CORRECTION ? HammingEncoding : undefined
);
const bitsPerSegment = getChannels().length;
PacketUtils.changeConfiguration({
segmentDurationMilliseconds: SEGMENT_DURATION,
packetSizeBitCount: PACKET_SIZE_BITS,
dataSizeBitCount: MAXIMUM_PACKETIZATION_SIZE_BITS,
dataSizeCrcBitCount: CRC_BIT_COUNT,
bitsPerSegment,
packetEncoding: HAMMING_ERROR_CORRECTION,
packetEncodingBitCount: ERROR_CORRECTION_BLOCK_SIZE,
packetDecodingBitCount: ERROR_CORRECTION_DATA_SIZE,
});
}
function updatePacketStats() {
const text = textToSend.value;
const bits = textToBits(text);
const byteCount = text.length;
const bitCount = PacketUtils.getPacketizationBitCountFromBitCount(bits.length);;
// # Packetization
document.getElementById('packetization-max-bytes').innerText = Humanize.byteSize(PacketUtils.getDataMaxByteCount());
document.getElementById('packetization-max-packets').innerText = PacketUtils.getMaxPackets().toLocaleString();
document.getElementById('packetization-max-duration').innerText = Humanize.durationMilliseconds(PacketUtils.getMaxDurationMilliseconds());
// ## Packetization Speed
document.getElementById('packetization-speed-bits-per-second').innerText = Humanize.bitsPerSecond(PacketUtils.getBaud());
document.getElementById('packetization-speed-effective-bits-per-second').innerText = Humanize.bitsPerSecond(PacketUtils.getEffectiveBaud());
// Data
document.getElementById('original-byte-count').innerText = textToBytes(text).length.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 Config
document.getElementById('bits-per-packet').innerText = PacketUtils.getPacketMaxBitCount().toLocaleString();
document.getElementById('bytes-per-packet').innerText = Humanize.byteSize(PacketUtils.getPacketMaxByteCount());
// ## Packet Encoding
document.getElementById('packet-encoding').innerText = PacketUtils.isPacketEncoded() ? 'Yes' : 'No';
document.getElementById('packet-encoding-block-count').innerText = PacketUtils.getPacketEncodingBlockCount().toLocaleString();
document.getElementById('packet-encoding-bits-per-block').innerText = PacketUtils.packetEncodingBlockSize().toLocaleString();
document.getElementById('packet-encoding-bit-count').innerText = PacketUtils.getEncodedPacketDataBitCount().toLocaleString();
document.getElementById('bits-per-segment').innerText = PacketUtils.getBitsPerSegment();
// Data
document.getElementById('packet-data-bit-count').innerText = PacketUtils.getPacketDataBitCount().toLocaleString();
document.getElementById('packet-unused-bit-count').innerText = PacketUtils.getPacketUnusedBitCount().toLocaleString();
document.getElementById('last-packet-unused-bit-count').innerText = PacketUtils.fromByteCountGetPacketLastUnusedBitCount(byteCount).toLocaleString();
document.getElementById('last-segment-unused-bit-count').innerText = PacketUtils.getPacketLastSegmentUnusedBitCount().toLocaleString()
document.getElementById('packet-transfer-duration').innerText = Humanize.durationMilliseconds(PacketUtils.getPacketDurationMilliseconds());
document.getElementById('segment-transfer-duration').innerText = Humanize.durationMilliseconds(PacketUtils.getSegmentDurationMilliseconds());
document.getElementById('data-transfer-duration').innerText = Humanize.durationMilliseconds(PacketUtils.getDataTransferDurationMilliseconds(bitCount));
document.getElementById('segments-per-packet').innerText = PacketUtils.getPacketSegmentCount().toLocaleString();
document.getElementById('total-segments').innerText = getTotalSegmentCount(bitCount).toLocaleString();
}
function drawChannels() {
const sampleRate = getAudioContext().sampleRate;
const fftSize = 2 ** FFT_SIZE_POWER;
const frequencyResolution = sampleRate / fftSize;
const channels = getChannels();
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 getChannels(includeExcluded = false) {
var audioContext = getAudioContext();
const sampleRate = audioContext.sampleRate;
const fftSize = 2 ** FFT_SIZE_POWER;
const frequencyResolution = sampleRate / fftSize;
const channels = [];
const pairStep = frequencyResolution * (2 + CHANNEL_FREQUENCY_RESOLUTION_PADDING) * FREQUENCY_RESOLUTION_MULTIPLIER;
let channelId = -1;
for(let hz = MINIMUM_FREQUENCY; hz < MAXIMUM_FREQUENCY; hz+= pairStep) {
const low = hz;
const high = hz + frequencyResolution * FREQUENCY_RESOLUTION_MULTIPLIER;
if(low < MINIMUM_FREQUENCY) continue;
if(high > MAXIMUM_FREQUENCY) continue;
channelId++;
if(!includeExcluded) {
if(EXCLUDED_CHANNELS.includes(channelId)) continue;
}
channels.push([low, high]);
}
return channels;
}
function logSent(text) {
// display what is being sent
sentDataTextArea.value += text + '\n';
sentDataTextArea.scrollTop = sentDataTextArea.scrollHeight;
}
function sendBytes(bytes) {
const byteCount = bytes.length;
if(byteCount === 0) {
logSent('Nothing to send!');
return;
} else if(byteCount > 0xFFFF) {
logSent('Too much to send!');
return;
}
const bits = bytesToBits(bytes);
SENT_ORIGINAL_TEXT = bytesToText(bytes);
SENT_ORIGINAL_BITS = bits.slice();
// packetization headers
// data length
const dataLengthBits = numberToBits(bytes.length, MAXIMUM_PACKETIZATION_SIZE_BITS);
// crc on data length
const dataLengthCrcBits = numberToBits(CRC.check(bitsToBytes(dataLengthBits), CRC_BIT_COUNT), CRC_BIT_COUNT);
// prefix with headers
bits.unshift(...dataLengthBits, ...dataLengthCrcBits);
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);
const channelCount = getChannels().length;
const errorCorrectionBits = [];
AudioSender.beginAt(startSeconds);
// send all packets
for(let i = 0; i < packetCount; i++) {
let packet = PacketUtils.getPacketBits(bits, i);
errorCorrectionBits.push(...packet);
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);
// original bits
document.getElementById('sent-data').innerHTML =
SENT_ORIGINAL_BITS.reduce(bitReducer(
PacketUtils.getPacketMaxBitCount(),
HAMMING_ERROR_CORRECTION ? ERROR_CORRECTION_DATA_SIZE : 8
), '');
// error correcting bits
if(HAMMING_ERROR_CORRECTION) {
document.getElementById('error-correcting-data').innerHTML =
SENT_ENCODED_BITS.reduce(bitReducer(
PacketUtils.getPacketDataBitCount(),
ERROR_CORRECTION_BLOCK_SIZE
), '');
} else {
document.getElementById('error-correcting-data').innerHTML = '';
}
document.getElementById('actual-bits-to-send').innerHTML =
SENT_TRANSFER_BITS.reduce(bitReducer(
PacketUtils.getPacketMaxBitCount() + PacketUtils.getPacketLastSegmentUnusedBitCount(),
channelCount,
(packetIndex, blockIndex) => `${blockIndex === 0 ? '' : '
'}Segment ${blockIndex}: `
), '');
// start the graph moving again
resumeGraph();
}
function sendPacket(bits, packetStartSeconds) {
const channels = getChannels();
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(PERIODIC_INTERLEAVING) {
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;
stopCollectingSamples();
}
function startCollectingSamples() {
for(let i = 0; i < SAMPLING_INTERVAL_COUNT; i++) {
if(sampleIntervalIds[i]) continue;
sampleIntervalIds[i] = window.setInterval(
collectSample,
MINIMUM_INTERVAL_MS + (i/SAMPLING_INTERVAL_COUNT)
);
}
}
function stopCollectingSamples() {
sampleIntervalIds.forEach(window.clearInterval);
sampleIntervalIds = sampleIntervalIds.map(() => {});
}
function resumeGraph() {
if(isListeningCheckbox.checked) {
if(PAUSE) {
PAUSE = false;
startCollectingSamples();
resetGraphData();
requestAnimationFrame(drawFrequencyData);
} else {
PAUSE = false;
}
} else {
PAUSE = false;
}
}
function collectSample() {
const time = performance.now();
// Do nothing if we already collected the sample
if(time === SAMPLE_LAST_COLLECTED) return;
SAMPLE_LAST_COLLECTED = time;
const frequencies = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(frequencies);
const length = audioContext.sampleRate / analyser.fftSize;
// Get amplitude of each channels set of frequencies
const channelAmps = getChannels().map(hzSet => hzSet.map(hz => frequencies[Math.round(hz / length)]));
const hasSignal = channelAmps.some(amps => amps.some(amp => amp > AMPLITUDE_THRESHOLD));
if(hasSignal) {
abandonPauseAfterLastSignalEnded();
if(time > RECEIVED_STREAM_END_MS) {
resetReceivedData();
// New stream
RECEIVED_STREAM_START_MS = time;
// Assume at least 1 full packet arriving
RECEIVED_STREAM_END_MS = getPacketEndMilliseconds(time);
}
} else {
pauseAfterSignalEnds();
}
if(time >= RECEIVED_STREAM_START_MS && time <= RECEIVED_STREAM_END_MS) {
// determine packet/segment index based on time as well as start/end times for packet
const packetIndex = PacketUtils.getPacketIndex(RECEIVED_STREAM_START_MS, time);
const segmentIndex = PacketUtils.getPacketSegmentIndex(RECEIVED_STREAM_START_MS, time);
SAMPLES.unshift({
time,
pairs: channelAmps,
packetIndex,
segmentIndex,
streamStarted: RECEIVED_STREAM_START_MS,
});
}
processSamples();
truncateGraphData();
}
function abandonPauseAfterLastSignalEnded() {
if(pauseGraphId) {
window.clearTimeout(pauseGraphId);
pauseGraphId = undefined;
}
}
function pauseAfterSignalEnds() {
// If we never had a signal, do nothing.
if(RECEIVED_STREAM_START_MS === -1) return;
// If we continue after a signal ends, do nothing.
if(!PAUSE_AFTER_END) return;
// If we are already setup to pause, do nothing
if(pauseGraphId) return;
// pause after waiting for 2 segments to come through
let delay = PacketUtils.getSegmentDurationMilliseconds() * 2;
// Long segments? Pause for no more than 400 milliseconds
delay = Math.min(400, delay);
// we haven't paused yet. Let's prepare to pause
pauseGraphId = window.setTimeout(() => {
pauseGraphId = undefined;
// if user still wants to pause, stop the graph
if(PAUSE_AFTER_END) {
stopGraph();
// are we the sender as well?
// Stop sending the signal.
AudioSender.stop();
}
}, delay);
}
function hasSampleSegmentCompleted(now) {
return ({streamStarted, packetIndex, segmentIndex}) => now >
PacketUtils.getPacketSegmentEndMilliseconds(streamStarted, packetIndex, segmentIndex);
}
function hasSamplePacketCompleted(now) {
return ({streamStarted, packetIndex}) => now >
getPacketIndexEndMilliseconds(streamStarted, packetIndex);
}
function consolidateFotPackets(all, {streamStarted, packetIndex}) {
const isMatch = (fot) => {
fot.streamStarted === streamStarted &&
fot.packetIndex === packetIndex
};
if(!all.some(isMatch))
all.push({streamStarted, packetIndex});
return all;
}
const consolidateUnprocessedSampleSegments = now => (all, {
streamStarted,
packetIndex,
segmentIndex,
processedSegment
}) => {
const isMatch = (sample) => {
sample.streamStarted === streamStarted &&
sample.packetIndex === packetIndex &&
sample.segmentIndex === segmentIndex
};
if(!processedSegment) {
if(!all.some(isMatch)) {
const end = PacketUtils.getPacketSegmentEndMilliseconds(streamStarted, packetIndex, segmentIndex);
if(end < now)
all.push({
streamStarted,
packetIndex,
segmentIndex
});
}
}
return all;
}
const markSampleSegmentProcessed = sample => sample.processedSegment = true;
const hasNotProcessedPacket = sample => sample.processedPacket;
const markSamplePacketProcessed = sample => sample.processedPacket = true;
function processSamples() {
const now = performance.now();
// Process completed segments
SAMPLES
.reduce(consolidateUnprocessedSampleSegments(now), [])
.every(({
streamStarted, packetIndex, segmentIndex
}) => {
processSegmentReceived(streamStarted, packetIndex, segmentIndex);
});
// Process completed packets
SAMPLES
.filter(hasNotProcessedPacket)
.reduce(consolidateFotPackets, [])
.filter(hasSamplePacketCompleted(now))
.every(({
streamStarted, packetIndex
}) => {
processPacketReceived(streamStarted, packetIndex);
});
}
function GET_SEGMENT_BITS(streamStarted, segmentIndex, packetIndex, originalOrder = false) {
const samples = SAMPLES.filter(f =>
f.segmentIndex === segmentIndex &&
f.packetIndex === packetIndex &&
f.streamStarted === streamStarted
);
const channelCount = SAMPLES[0].pairs.length;
const channelFrequencyCount = 2;
const sums = new Array(channelCount)
.fill(0)
.map(() =>
new Array(channelFrequencyCount)
.fill(0)
);
samples.forEach(({pairs}) => {
pairs.forEach((amps, channel) => {
amps.forEach((amp, i) => {
sums[channel][i] += amp;
});
});
});
const bitValues = sums.map((amps) => amps[0] > amps[1] ? 0 : 1);
// if(packetIndex === 0 && segmentIndex === 1) {
// console.log(packetIndex, segmentIndex, bitValues.join(''))
// }
return originalOrder ? bitValues : InterleaverEncoding.decode(bitValues);
}
function resetReceivedData() {
resetStream();
resetPacket();
}
function resetStream() {
RECEIVED_STREAM_END_MS = -1;
RECEIVED_STREAM_START_MS = -1;
RECEIVED_SEGMENT_BITS.length = 0;
}
function resetPacket() {
}
function processPacketReceived(streamStarted, packetIndex) {
SAMPLES.filter(
fot => fot.streamStarted === streamStarted &&
fot.packetIndex === packetIndex
).forEach(markSamplePacketProcessed);
resetPacket();
updateReceivedData();
}
function getTransferredCorrectedBits() {
const bits = [];
const packetCount = StreamManager.getPacketReceivedCount();
for(let packetIndex = 0; packetIndex < packetCount; packetIndex++) {
let packetBits = StreamManager.getPacketBits(packetIndex);
if(HAMMING_ERROR_CORRECTION) {
bits.push(...HammingEncoding.decode(packetBits));
} else {
bits.push(...packetBits);
}
}
return bits;
}
function processSegmentReceived(streamStarted, packetIndex, segmentIndex) {
const isSegment = sample => (
sample.streamStarted === streamStarted &&
sample.packetIndex === packetIndex &&
sample.segmentIndex === segmentIndex
);
let bitValues = GET_SEGMENT_BITS(streamStarted, segmentIndex, packetIndex, segmentIndex, true);
StreamManager.addBits(packetIndex, segmentIndex, bitValues);
// mark samples as processed
SAMPLES.filter(isSegment).every(markSampleSegmentProcessed);
updateReceivedData();
}
function updateReceivedData() {
const channelCount = getChannels().length;
let allRawBits = StreamManager.getStreamBits();
let allEncodedBits = StreamManager.getAllPacketBits();
let allDecodedBits = getTransferredCorrectedBits();
// get packet data before removing decoded bits
const transmissionByteCount = parseTransmissionByteCount(allDecodedBits);
const transmissionByteCountCrc = parseTransmissionByteCountCrc(allDecodedBits)
const transmissionByteCountActualCrc = CRC.check(
bitsToBytes(
numberToBits(
transmissionByteCount,
MAXIMUM_PACKETIZATION_SIZE_BITS
)
), CRC_BIT_COUNT
);
const trustedLength = transmissionByteCountCrc === transmissionByteCountActualCrc;
const totalBitsTransferring = parseTotalBitsTransferring(allDecodedBits);
// 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;
const receivedProgess = document.getElementById('received-progress');
let percentReceived = allRawBits.length / totalBitsTransferring;
receivedProgess.style.width = `${Math.floor(Math.min(1, percentReceived) * 100)}%`;
document.getElementById('received-encoded-segment-bits').innerHTML = allRawBits
.reduce(
bitExpectorReducer(
SENT_TRANSFER_BITS,
PacketUtils.getPacketMaxBitCount() + PacketUtils.getPacketLastSegmentUnusedBitCount(),
channelCount,
(packetIndex, blockIndex) => `${blockIndex === 0 ? '' : '
'}Segment ${blockIndex}: `
),
'');
if(HAMMING_ERROR_CORRECTION) {
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(),
HAMMING_ERROR_CORRECTION ? 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 = '0x' + asHex(2)(transmissionByteCountCrc);
packetCrc.className = trustedLength ? 'bit-correct' : 'bit-wrong';
if(!trustedLength) {
packetCrc.innerText += ' (Expected 0x' + asHex(2)(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();
// ArrayBuffer / ArrayBufferView
const receivedText = bitsToText(allDecodedBits);
document.getElementById('decoded-text').innerHTML = receivedText.split('').reduce(textExpectorReducer(SENT_ORIGINAL_TEXT), '');
}
function asHex(length) {
return (number) => number.toString(16).padStart(length, '0').toUpperCase();
}
function parseDataTransferDurationMilliseconds() {
const decodedBits = getTransferredCorrectedBits();
const byteCount = parseTransmissionByteCount(decodedBits);
return PacketUtils.getDataTransferDurationMillisecondsFromByteCount(byteCount);
}
function parseTotalBitsTransferring() {
const dataByteCount = parseTransmissionByteCount();
const bitCount = PacketUtils.getPacketizationBitCountFromByteCount(dataByteCount);
const segments = getTotalSegmentCount(bitCount);
return segments * getChannels().length;
}
function parseTransmissionByteCountCrc() {
let decodedBits = getTransferredCorrectedBits();
const offset = MAXIMUM_PACKETIZATION_SIZE_BITS;
decodedBits = decodedBits.slice(offset, offset + CRC_BIT_COUNT);
return bitsToInt(decodedBits, CRC_BIT_COUNT);
}
function parseTransmissionByteCount() {
let decodedBits = getTransferredCorrectedBits();
decodedBits = decodedBits.slice(0, MAXIMUM_PACKETIZATION_SIZE_BITS);
while(decodedBits.length < MAXIMUM_PACKETIZATION_SIZE_BITS) {
// assume maximum value possible
// until we have enough bits to find the real size
decodedBits.push(1);
}
return bitsToInt(decodedBits, MAXIMUM_PACKETIZATION_SIZE_BITS);
}
function removeEncodedPadding(bits) {
const sizeBits = MAXIMUM_PACKETIZATION_SIZE_BITS;
const dataSize = ERROR_CORRECTION_DATA_SIZE;
const blockSize = ERROR_CORRECTION_BLOCK_SIZE;
let bitsNeeded = sizeBits;
let blocksNeeded = sizeBits;
// need to calc max bits
if(HAMMING_ERROR_CORRECTION) {
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)
const totalBits = (dataByteCount * 8) + MAXIMUM_PACKETIZATION_SIZE_BITS + CRC_BIT_COUNT;
let encodingBitCount = totalBits;
if(HAMMING_ERROR_CORRECTION) {
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 sizeBits = MAXIMUM_PACKETIZATION_SIZE_BITS;
let bitCount = bits.length / 8;
if(bits.length >= sizeBits) {
bitCount = bitsToInt(bits.slice(0, sizeBits), sizeBits);
}
// remove size and crc header
bits.splice(0, sizeBits + CRC_BIT_COUNT);
// remove excessive bits
bits.splice(bitCount * 8);
return bits;
}
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 truncateGraphData() {
const duration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH;
const now = performance.now();
let length = SAMPLES.length;
while(length !== 0) {
const time = SAMPLES[length-1].time;
if(now - time > duration) length--;
else break;
}
if(length !== SAMPLES.length) {
SAMPLES.length = length;
bitStart.length = length;
}
// remove processed segments
SAMPLES = SAMPLES.filter(s => !s.segmentProcessed);
}
function getAudioContext() {
if(!audioContext) {
audioContext = new (window.AudioContext || webkitAudioContext)();
}
if(audioContext.state === 'suspended') {
audioContext.resume();
}
return audioContext;
}
function bitsToInt(bits, bitLength) {
// only grab the bits we need
const bitString = bits.slice(0, bitLength)
// combine into string
.join('')
// Assume missing bits were zeros
.padEnd(bitLength, '0');
// parse as int
return parseInt(bitString, 2);
}
function intToBytes(int, bitLength) {
const byteCount = Math.ceil(bitLength/8);
const bytes = [];
for(let i = 0; i < byteCount; i++) {
bytes.push((int >> (8 * (byteCount - 1 - i))) & 0xFF);
}
return bytes;
}
function numberToBits(number, bitLength) {
const bits = [];
for(let i = bitLength - 1; i >= 0; i--)
bits.push((number >> i) & 1);
return bits;
}
function bytesToText(bytes) {
return new TextDecoder().decode(bytes);
}
function textToBytes(text) {
return new TextEncoder().encode(text);
}
function bytesToBits(bytes) {
return bytes.reduce((bits, byte) => [
...bits,
...byte.toString(2).padStart(8, '0').split('').map(Number)
], []);
}
function bitsToBytes(bits) {
const bytes = [];
for(let i = 0; i < bits.length; i+= 8) {
bytes.push(parseInt(bits.slice(i, i + 8).join(''), 2));
}
return bytes;
}
function textToBits(text) {
return bytesToBits(textToBytes(text));
}
function bitsToText(bits) {
const bytes = new Uint8Array(bitsToBytes(bits));
return bytesToText(bytes.buffer);
}
function handleSendButtonClick() {
if(stopOscillatorsTimeoutId) {
disconnectOscillators();
return;
}
resetReceivedData();
const text = document.getElementById('text-to-send').value;
sendBytes(textToBytes(text));
}
function getAnalyser() {
if(analyser) return analyser;
analyser = audioContext.createAnalyser();
analyser.smoothingTimeConstant = SMOOTHING_TIME_CONSTANT;
analyser.fftSize = 2 ** FFT_SIZE_POWER;
return analyser;
}
function handleListeningCheckbox(e) {
stopGraph();
var audioContext = getAudioContext();
function handleMicrophoneOn(stream) {
microphoneStream = stream;
microphoneNode = audioContext.createMediaStreamSource(stream);
analyser = getAnalyser();
microphoneNode.connect(analyser);
resumeGraph();
}
function handleMicrophoneError(error) {
console.error('Microphone Error', error);
}
if(e.target.checked) {
navigator.mediaDevices
.getUserMedia({
audio: {
mandatory: {
autoGainControl: false,
echoCancellation: false,
noiseSuppression: false,
suppressLocalAudioPlayback: false,
voiceIsolation: false
},
optional: []
}
})
.then(handleMicrophoneOn)
.catch(handleMicrophoneError)
} else {
if(microphoneStream) {
microphoneStream.getTracks().forEach(track => track.stop());
microphoneStream = undefined;
}
if(analyser && microphoneNode) {
try {
analyser.disconnect(microphoneNode);
} catch(e) {
}
microphoneNode = undefined;
analyser = undefined;
}
}
}
function received(value) {
receivedDataTextarea.value += value;
receivedDataTextarea.scrollTop = receivedDataTextarea.scrollHeight;
}
function canHear(hz, {frequencies, length}) {
var i = Math.round(hz / length);
return frequencies[i] > AMPLITUDE_THRESHOLD;
}
function amplitude(hz, {frequencies, length}) {
var i = Math.round(hz / length);
return frequencies[i];
}
function sum(total, value) {
return total + value;
}
function avgLabel(array) {
const values = array.filter(v => v > 0);
if(values.length === 0) return 'N/A';
return (values.reduce((t, v) => t + v, 0) / values.length).toFixed(2)
}
function drawSegmentIndexes(ctx, width, height) {
// Do/did we have a stream?
if(!RECEIVED_STREAM_START_MS) return;
const latest = SAMPLES[0].time;
// will any of the stream appear?
const segmentCount = PacketUtils.getPacketSegmentCount();
const transferDuration = parseDataTransferDurationMilliseconds();
const lastStreamEnded = RECEIVED_STREAM_START_MS + transferDuration;
const graphDuration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH;
const graphEarliest = latest - graphDuration;
// ended too long ago?
if(lastStreamEnded < graphEarliest) return;
const segmentWidth = width / MAX_BITS_DISPLAYED_ON_GRAPH;
const latestSegmentEnded = Math.min(latest, lastStreamEnded);
for(let time = latestSegmentEnded; time > graphEarliest; time -= SEGMENT_DURATION) {
// too far back?
if(time < RECEIVED_STREAM_START_MS) break;
const transferSegmentIndex = PacketUtils.getTranserSegmentIndex(RECEIVED_STREAM_START_MS, time);
const packetIndex = PacketUtils.getPacketIndex(RECEIVED_STREAM_START_MS, time);
const packetSegmentIndex = PacketUtils.getPacketSegmentIndex(RECEIVED_STREAM_START_MS, time);
const packetStarted = PacketUtils.getPacketStartMilliseconds(RECEIVED_STREAM_START_MS, packetIndex);
const segmentStart = PacketUtils.getPacketSegmentStartMilliseconds(RECEIVED_STREAM_START_MS, packetIndex, packetSegmentIndex);
const segmentEnd = PacketUtils.getPacketSegmentEndMilliseconds(RECEIVED_STREAM_START_MS, packetIndex, packetSegmentIndex);
// where is the segments left x coordinate?
const leftX = ((latest - segmentEnd) / graphDuration) * width;
// Draw segment index
ctx.fontSize = '24px';
if(segmentStart < lastStreamEnded) {
let text = packetSegmentIndex.toString();
let size = ctx.measureText(text);
let textX = leftX + (segmentWidth / 2) - (size.width / 2);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.textBaseline = 'bottom';
let textY = transferSegmentIndex % 2 === 0 ? height : height - 12;
ctx.strokeText(text, textX, textY);
ctx.fillStyle = 'white';
ctx.fillText(text, textX, textY);
}
// draw sample count
const sampleCount = SAMPLES
.filter(fot =>
fot.streamStarted === packetStarted &&
fot.segmentIndex === packetSegmentIndex &&
fot.packetIndex === packetIndex
)
.length;
// if(sampleCount === 0) {
// console.log({
// packetStarted,
// packetSegmentIndex,
// packetIndex,
// startTimes: SAMPLES.reduce((all, fot) => all.includes(fot.streamStarted) ? all : [...all, fot.streamStarted], [])
// })
// }
let text = sampleCount.toString();
let size = ctx.measureText(text);
let textX = leftX + (segmentWidth / 2) - (size.width / 2);
let textY = transferSegmentIndex % 2 === 0 ? 5 : 17;
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.textBaseline = 'top';
ctx.strokeText(text, textX, textY);
if(sampleCount === 0) ctx.fillStyle = 'red';
else if(sampleCount < 3) ctx.fillStyle = 'yellow';
else ctx.fillStyle = 'white';
ctx.fillText(text, textX, textY);
}
}
function drawBitDurationLines(ctx, color) {
const { width, height } = receivedGraph;
const newest = SAMPLES[0].time;
const duration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH;
const streamTimes = SAMPLES.filter(({
streamStarted
}) => {
return streamStarted !== -1
}).reduce((unique, {
streamStarted,
streamEnded = newest
}) => {
if(unique.every(u => u.streamStarted != streamStarted)) {
unique.push({streamStarted, streamEnded})
}
return unique;
}, []);
ctx.strokeStyle = color;
streamTimes.forEach(({ streamStarted, streamEnded = newest}) => {
for(let time = streamStarted; time < streamEnded; time += SEGMENT_DURATION) {
if(newest - time > duration) continue;
const x = ((newest - time) / duration) * width;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
// write end as well
const x = ((newest - streamEnded) / duration) * width;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
});
}
function drawBitStart(ctx, color) {
const { width, height } = receivedGraph;
const newest = SAMPLES[0].time;
const duration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH;
ctx.strokeStyle = color;
for(let i = 0; i < bitStart.length; i++) {
if(!bitStart[i]) continue;
const {time} = SAMPLES[i];
if(newest - time > duration) continue;
const x = ((newest - time) / duration) * width;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
}
function getPercentY(percent) {
const { height } = receivedGraph;
return (1 - percent) * height;
}
function drawFrequencyLineGraph(ctx, channel, highLowIndex, color, lineWidth, dashed) {
const { width, height } = receivedGraph;
const newest = SAMPLES[0].time;
const duration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH;
const isSelected = channel === CHANNEL_SELECTED;
const isOver = channel === CHANNEL_OVER;
if(dashed) {
ctx.setLineDash([5, 5]);
}
ctx.beginPath();
for(let i = 0; i < SAMPLES.length; i++) {
const {pairs, time} = SAMPLES[i];
const x = getTimeX(time, newest);
if(x === -1) continue;
if(channel >= pairs.length) continue;
const amplitude = pairs[channel][highLowIndex];
const y = getPercentY(amplitude / MAX_AMPLITUDE);
if(i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
if(isSelected || isOver) {
ctx.lineWidth = lineWidth + 5;
ctx.strokeStyle = 'white';
ctx.stroke();
}
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.stroke();
if(dashed) {
ctx.setLineDash([]);
}
}
function drawFrequencyDots(ctx, channel, highLowIndex, color) {
const newest = SAMPLES[0].time;
const radius = 2;
const border = 0.5;
ctx.fillStyle = color;
ctx.strokeStyle = 'white';
ctx.lineWidth = border;
const fullCircle = 2 * Math.PI;
for(let i = 0; i < SAMPLES.length; i++) {
const {pairs, time} = SAMPLES[i];
const x = getTimeX(time, newest);
if(x === -1) continue;
const amplitude = pairs[channel][highLowIndex];
const y = getPercentY(amplitude / MAX_AMPLITUDE);
ctx.beginPath();
ctx.arc(x, y, radius, 0, fullCircle);
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, radius + border, 0, fullCircle);
ctx.stroke();
}
}
function getTimeX(time, newest) {
return getTimePercent(time, newest) * receivedGraph.width;
}
function getTimePercent(time, newest) {
const duration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH;
if(newest - time > duration) return -1;
return ((newest - time) / duration);
}
function getPacketSizeSegmentCount() {
const totalBits = PacketUtils.getPacketMaxBitCount();
const channelCount = getChannels().length;
return Math.ceil(totalBits / channelCount);
}
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 = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH;
const graphEarliest = latest - graphDuration;
// ended too long ago?
if(lastStreamEnded < graphEarliest) return;
const channels = getChannels();
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 -= SEGMENT_DURATION) {
// 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 / MAX_BITS_DISPLAYED_ON_GRAPH;
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 / MAX_BITS_DISPLAYED_ON_GRAPH;
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 / MAX_BITS_DISPLAYED_ON_GRAPH;
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 = getChannels();
const channelHeight = height / channelCount;
const segmentWidth = width / MAX_BITS_DISPLAYED_ON_GRAPH;
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();
const ctx = receivedGraph.getContext('2d');
const { width, height } = receivedGraph;
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, width, height);
const thresholdY = (1 - (AMPLITUDE_THRESHOLD/MAX_AMPLITUDE)) * height;
ctx.strokeStyle = 'grey';
ctx.beginPath();
ctx.moveTo(0, thresholdY);
ctx.lineTo(width, thresholdY);
ctx.stroke();
drawBitDurationLines(ctx, 'rgba(255, 255, 0, .25)');
drawBitStart(ctx, 'green');
const frequencies = getChannels();
const high = 1;
const low = 0
const isSelectedOrOver = CHANNEL_OVER !== -1 || CHANNEL_SELECTED !== -1;
const highLuminance = isSelectedOrOver ? 25 : 50;
const lowLuminance = isSelectedOrOver ? 12 : 25;
frequencies.forEach((v, channel) => {
// const hue = channelHue(channel, frequencies.length);
const lowHue = hzHue(v[0]);
const highHue = hzHue(v[1]);
drawFrequencyLineGraph(ctx, channel, high, `hsl(${highHue}, 100%, ${highLuminance}%)`, 2, false);
drawFrequencyLineGraph(ctx, channel, low, `hsl(${lowHue}, 100%, ${lowLuminance}%)`, 1, true);
});
if(CHANNEL_OVER !== -1) {
// const hue = channelHue(CHANNEL_OVER, frequencies.length);
const lowHue = hzHue(frequencies[CHANNEL_OVER][0]);
const highHue = hzHue(frequencies[CHANNEL_OVER][1]);
drawFrequencyLineGraph(ctx, CHANNEL_OVER, high, `hsl(${highHue}, 100%, 50%)`, 2, false);
drawFrequencyLineGraph(ctx, CHANNEL_OVER, low, `hsl(${lowHue}, 100%, 25%)`, 1, true);
} else if(CHANNEL_SELECTED !== -1) {
const lowHue = hzHue(frequencies[CHANNEL_SELECTED][0]);
const highHue = hzHue(frequencies[CHANNEL_SELECTED][1]);
// const hue = channelHue(CHANNEL_SELECTED, frequencies.length);
drawFrequencyLineGraph(ctx, CHANNEL_SELECTED, high, `hsl(${highHue}, 100%, 50%)`, 2, false);
drawFrequencyLineGraph(ctx, CHANNEL_SELECTED, low, `hsl(${lowHue}, 100%, 25%)`, 1, true);
}
drawSegmentIndexes(ctx, width, height);
requestAnimationFrame(drawFrequencyData);
}
function channelHue(channelId, channelCount) {
return Math.floor((channelId / channelCount) * 360);
}
function hzHue(hz) {
return Math.floor((hz / 20000) * 360);
}
function drawReceivedData() {
const ctx = receivedGraph.getContext('2d');
const { width, height } = receivedGraph;
const segmentWidth = (1 / MAX_BITS_DISPLAYED_ON_GRAPH) * width;
ctx.clearRect(0, 0, width, height);
const sorted = receivedData.slice().sort((a, b) => a - b);
const min = sorted[0];
const max = sorted[sorted.length - 1];
const range = max - min;
ctx.beginPath();
for(let i = 0; i < MAX_BITS_DISPLAYED_ON_GRAPH && i < receivedData.length; i++) {
const value = receivedData[i];
const y = (1-(value / range)) * height;
if(i === 0) {
ctx.moveTo(0, y);
} else {
ctx.lineTo(segmentWidth * i, y)
}
}
ctx.stroke();
}
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 = getChannels();
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 = getChannels();
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 = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH;
const graphEarliest = latest - graphDuration;
// ended too long ago?
if(lastStreamEnded < graphEarliest) {
return {
channelIndex,
segmentIndex: -1
};
}
const segmentWidth = width / MAX_BITS_DISPLAYED_ON_GRAPH;
const latestSegmentEnded = Math.min(latest, lastStreamEnded);
for(let time = latestSegmentEnded; time > graphEarliest; time -= SEGMENT_DURATION) {
// 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) / SEGMENT_DURATION));
// when did the segment begin/end
const segmentStart = RECEIVED_STREAM_START_MS + (segmentIndex * SEGMENT_DURATION);
const segmentEnd = segmentStart + SEGMENT_DURATION;
// 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);