setup audio receiver and event dispatcher

This commit is contained in:
Lewis Moten
2024-05-11 03:33:40 -04:00
parent fdb6723e87
commit 6647e5b51d
6 changed files with 376 additions and 279 deletions

225
AudioReceiver.js Normal file
View File

@@ -0,0 +1,225 @@
import Dispatcher from "./Dispatcher";
const dispatcher = new Dispatcher('AudioReceiver', ['begin', 'end', 'receive']);
let sampleIntervalIds = [];
let SAMPLE_LAST_COLLECTED = 0;
let AMPLITUDE_THRESHOLD = 50;
let FSK_SETS = [];
let SIGNAL_INTERVAL_MS = 30;
let SIGNAL_TIMEOUT_MS = 400;
let HAS_SIGNAL = false;
let SIGNAL_START_MS = -1;
let ANALYSER;
let SAMPLE_RATE;
let signalTimeoutId;
let SAMPLES = [];
const changeConfiguration = ({
fskSets,
signalIntervalMs,
amplitudeThreshold,
analyser,
sampleRate
}) => {
FSK_SETS = fskSets;
AMPLITUDE_THRESHOLD = amplitudeThreshold;
SIGNAL_INTERVAL_MS = signalIntervalMs;
ANALYSER = analyser;
SAMPLE_RATE = sampleRate;
}
function start() {
// Browsers generally do not run any less than 3 milliseconds
const MINIMUM_INTERVAL_MS = 3;
// Running two intervals gives us a small increase in sample rate
// Running more than two intervals was negligible
const SAMPLING_INTERVAL_COUNT = 2;
for(let i = 0; i < SAMPLING_INTERVAL_COUNT; i++) {
// already started?
if(sampleIntervalIds[i]) continue;
// set interval
sampleIntervalIds[i] = window.setInterval(
collectSample,
MINIMUM_INTERVAL_MS + (i/SAMPLING_INTERVAL_COUNT)
);
}
}
function stop() {
sampleIntervalIds.forEach(window.clearInterval);
sampleIntervalIds = sampleIntervalIds.map(() => {});
}
const reset =() => {
HAS_SIGNAL = false;
SIGNAL_START_MS = -1;
SAMPLES.length = 0;
SAMPLE_LAST_COLLECTED = -1;
}
const getAnalyser = () => ANALYSER;
const getFrequencyShiftKeyingSets = () => FSK_SETS;
function analyzeAudioFrequenciesAsBits() {
const analyser = getAnalyser();
const frequencyResolution = SAMPLE_RATE / analyser.fftSize;
const frequencies = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(frequencies);
const indexOfHz = hz => Math.round(hz/frequencyResolution);
const ampsFromHz = hz => frequencies[indexOfHz(hz)];
const ampsFromManyHz = fsk => fsk.map(ampsFromHz);
return getFrequencyShiftKeyingSets().map(ampsFromManyHz);
}
const ampMeetsTheshold = amp => amp > AMPLITUDE_THRESHOLD;
const anyAmpMeetsThreshold = amps => amps.some(ampMeetsTheshold);
const anySetOfAmpsMeetsThreshold = bitStates => bitStates.some(anyAmpMeetsThreshold);
function collectSample() {
const time = performance.now();
// Do nothing if we already collected the sample
if(time === SAMPLE_LAST_COLLECTED) return;
SAMPLE_LAST_COLLECTED = time;
// Get amplitude of each channels set of frequencies
const bitStates = analyzeAudioFrequenciesAsBits();
const hasSignal = anySetOfAmpsMeetsThreshold(bitStates);
handleSignalState(time, hasSignal);
if(hasSignal) {
const duration = time - SIGNAL_START_MS;
const index = Math.floor(duration / SIGNAL_INTERVAL_MS);
const start = SIGNAL_START_MS + (index * SIGNAL_INTERVAL_MS);
const end = start + SIGNAL_INTERVAL_MS;
SAMPLES.unshift({
signalStart: SIGNAL_START_MS,
index,
start,
time,
end,
bitStates,
bs: bitStates.map(ss => '[' + ss.join(':') + ']').join('')
});
}
processSamples(time);
removeSamples(time);
}
const isExpiredSample = time => {
const duration = Math.max(30, SIGNAL_INTERVAL_MS * 2);
const expired = time - duration;
return sample => sample.time < expired;
};
const removeSamples = time => {
// remove expired samples
let length = SAMPLES.findIndex(isExpiredSample(time));
if(length !== -1) SAMPLES.length = length;
// Don't let long signal intervals take over memory
if(SAMPLES.length > 1024) SAMPLES.length = 1024;
}
const uniqueSamplesReady = time => (all, {
signalStart,
index,
end
}) => {
// still collecting samples?
if(end > time) return all;
const isSameSample = sample => sample.signalStart === signalStart && sample.index === index;
// sample exists?
if(!all.some(isSameSample)) {
all.push({ signalStart, index });
}
return all;
}
function processSamples(time) {
SAMPLES
.reduce(uniqueSamplesReady(time), [])
.every(processSample);
}
function processSample({ signalStart, index }) {
const isSegment = sample => (
sample.signalStart === signalStart &&
sample.index === index
);
const samples = SAMPLES.filter(isSegment);
if(samples.length === 0) return;
let bits = evaluateBits(samples);
const { start, end } = samples[0];
dispatcher.emit('receive', {
signalStart,
signalIndex: index,
indexStart: start,
indexEnd: end,
bits,
});
// remove processed samples
const isNotSegment = sample => !isSegment(sample);
SAMPLES = SAMPLES.filter(isNotSegment)
}
const newSingleBitState = () => new Array(2).fill(0);
const newMultiBitStates = count => new Array(count).fill(0).map(newSingleBitState);
const mapBitValue = (bitStates) => bitStates[0] > bitStates[1] ? 0 : 1
const evaluateBits = (samples) => {
if(samples.length === 0) return;
const {bitStates: { length: bitCount }} = SAMPLES[0];
if(bitCount === 0) return;
const bitSums = newMultiBitStates(bitCount);
samples.forEach(({bitStates}) => {
bitStates.forEach((strength, bitIndex) => {
strength.forEach((value, bitState) => {
bitSums[bitIndex][bitState] += value;
});
});
});
return bitSums.map(mapBitValue);
}
const handleSignalState = (time, hasSignal) => {
if(hasSignal) {
handleSignalOn(time);
} else {
handleSignalOff(time);
}
}
const handleSignalOn = time => {
if(signalTimeoutId) {
window.clearTimeout(signalTimeoutId);
signalTimeoutId = undefined;
}
if(!HAS_SIGNAL) {
HAS_SIGNAL = true;
SIGNAL_START_MS = time;
dispatcher.emit('begin', { signalStart: time });
}
}
const handleSignalOff = time => {
if(HAS_SIGNAL && !signalTimeoutId) {
signalTimeoutId = window.setTimeout(handleSignalLost, SIGNAL_TIMEOUT_MS, time);
}
}
const handleSignalLost = time => {
if(signalTimeoutId) {
window.clearTimeout(signalTimeoutId);
signalTimeoutId = undefined;
}
if(HAS_SIGNAL) {
HAS_SIGNAL = false;
dispatcher.emit('end', {
signalStart: SIGNAL_START_MS,
signalEnd: time
});
}
}
const addEventListener = dispatcher.addListener;
const removeEventListener = dispatcher.removeListener;
export {
changeConfiguration,
start,
stop,
reset,
addEventListener,
removeEventListener,
}

View File

@@ -1,3 +1,7 @@
import Dispatcher from "./Dispatcher";
const dispatcher = new Dispatcher('AudioSender', ['begin', 'end', 'send']);
let audioContext; let audioContext;
let CHANNELS = []; let CHANNELS = [];
let DESTINATION; let DESTINATION;
@@ -9,6 +13,9 @@ let WAVE_FORM;
let stopOscillatorsTimeoutId; let stopOscillatorsTimeoutId;
export const addEventListener = dispatcher.addListener;
export const removeEventListener = dispatcher.removeListener;
export const changeConfiguration = ({ export const changeConfiguration = ({
channels, channels,
destination, destination,
@@ -58,7 +65,7 @@ export function beginAt(streamStartSeconds) {
oscillator.start(streamStartSeconds); oscillator.start(streamStartSeconds);
oscillators.push(oscillator); oscillators.push(oscillator);
} }
callFn(ON_START); dispatcher.emit('begin');
return oscillators; return oscillators;
} }
function getOscillators() { function getOscillators() {
@@ -66,10 +73,11 @@ function getOscillators() {
} }
export function send(bits, startSeconds) { export function send(bits, startSeconds) {
const oscillators = getOscillators(); const oscillators = getOscillators();
const sentBits = [];
getChannels().forEach((channel, i) => { getChannels().forEach((channel, i) => {
// send missing bits as zero // send missing bits as zero
const isHigh = bits[i] ?? 0; const isHigh = bits[i] ?? 0;
callFn(ON_SEND, isHigh); sentBits.push(isHigh);
const oscillator = oscillators[i]; const oscillator = oscillators[i];
// already at correct frequency // already at correct frequency
if(oscillator.on === isHigh) return; if(oscillator.on === isHigh) return;
@@ -77,6 +85,7 @@ export function send(bits, startSeconds) {
const hz = channel[isHigh ? 1 : 0]; const hz = channel[isHigh ? 1 : 0];
oscillator.frequency.setValueAtTime(hz, startSeconds); oscillator.frequency.setValueAtTime(hz, startSeconds);
}); });
dispatcher.emit('send', {bits: sentBits, startSeconds});
} }
const stopTimeout = () => { const stopTimeout = () => {
if(stopOscillatorsTimeoutId) { if(stopOscillatorsTimeoutId) {
@@ -109,7 +118,6 @@ export function stop() {
} }
) )
oscillators.length = 0; oscillators.length = 0;
callFn(ON_STOP); dispatcher.emit('end');
stopTimeout(); stopTimeout();
} }
const callFn = (fn, ...args) => typeof fn === 'function' ? fn(...args) : 0;

45
Dispatcher.js Normal file
View File

@@ -0,0 +1,45 @@
class Dispatcher {
constructor(domain, allowedEvents=[]) {
this.LISTENERS = {};
this.allowedEvents = allowedEvents;
this.domain = domain;
}
emit = (eventName, ...args) => {
// console.log(`${this.domain}.${eventName}`, ...args);
if(!this.LISTENERS[eventName]) return;
this.LISTENERS[eventName].forEach(callback => callback(...args));
}
addListener = (eventName, callback) => {
if(this.allowedEvents.length !== 0) {
if(!this.allowedEvents.includes(eventName)) {
throw new Error(`Event "${eventName}" is not allowed for ${this.domain}.`)
}
}
if(typeof callback !== 'function')
throw new Error('Must provide a function');
if(!this.LISTENERS[eventName]) {
this.LISTENERS[eventName] = [];
}
if(this.LISTENERS[eventName].includes(callback)) return;
this.LISTENERS[eventName].push(callback);
}
removeListener = (eventName, callback) => {
if(!this.LISTENERS[eventName]) return;
const i = this.LISTENERS[eventName].indexOf(callback);
if(i === -1) return;
this.LISTENERS[eventName].splice(i, 1);
}
clearEventListeners = eventName => {
if(!this.LISTENERS[eventName]) return;
delete this.LISTENERS[eventName];
}
clearAllEventListeners = () => {
Object
.keys(this.LISTENERS)
.forEach(
eventName => this.clearEventListeners(eventName)
);
}
}
export default Dispatcher;

View File

@@ -1,5 +1,8 @@
import Dispatcher from "./Dispatcher";
import { bitsToInt } from "./converters"; import { bitsToInt } from "./converters";
const dispatcher = new Dispatcher('StreamManager', ['change']);
const BITS = []; const BITS = [];
let BITS_PER_PACKET = 0; let BITS_PER_PACKET = 0;
let SEGMENTS_PER_PACKET = 0; let SEGMENTS_PER_PACKET = 0;
@@ -14,6 +17,16 @@ let PACKET_ENCODING = {
decode: bits => bits decode: bits => bits
} }
export const addEventListener = dispatcher.addListener;
export const removeEventListener = dispatcher.removeListener;
export const reset = () => {
if(BITS.length !== 0) {
BITS.length = 0;
dispatcher.emit('change');
}
}
export const changeConfiguration = ({ export const changeConfiguration = ({
segmentsPerPacket, segmentsPerPacket,
bitsPerPacket, bitsPerPacket,
@@ -42,7 +55,20 @@ export const addBits = (
if(BITS[packetIndex] === undefined) { if(BITS[packetIndex] === undefined) {
BITS[packetIndex] = []; BITS[packetIndex] = [];
} }
const oldBits = BITS[packetIndex][segmentIndex];
BITS[packetIndex][segmentIndex] = bits; BITS[packetIndex][segmentIndex] = bits;
if(hasNewBits(oldBits, bits))
dispatcher.emit('change');
}
const hasNewBits = (oldBits = [], bits = []) => {
if(oldBits.length === 0 && bits.length === BITS_PER_SEGMENT)
return true;
for(let i = 0; i < BITS_PER_SEGMENT; i++) {
let a = oldBits[i] ?? 0;
let b = bits[i] ?? 0;
if(a !== b) return true;
}
return false;
} }
export const getPacketReceivedCount = () => { export const getPacketReceivedCount = () => {
if(BITS.length === 0) return 1; if(BITS.length === 0) return 1;

View File

@@ -40,6 +40,19 @@
</div> </div>
</div> </div>
<div>
<h2>Encoded segments to send</h2>
<div>
<div class="raw-data" id="actual-bits-to-send"></div><br />
</div>
</div>
<div>
<h2>Encoded Segments Received</h2>
<div>
<div class="raw-data" id="received-encoded-segment-bits"></div><br />
</div>
</div>
<div> <div>
<h2>Configuration</h2> <h2>Configuration</h2>
<div> <div>
@@ -154,12 +167,6 @@
<div class="raw-data" id="error-correcting-data"></div> <div class="raw-data" id="error-correcting-data"></div>
</div> </div>
</div> </div>
<div>
<h2>Encoded segments to send</h2>
<div>
<div class="raw-data" id="actual-bits-to-send"></div><br />
</div>
</div>
<div> <div>
<h2>Decoded</h2> <h2>Decoded</h2>
<div> <div>
@@ -171,12 +178,6 @@
Decoded Packets: <span id="received-decoded-bits-error-percent">N/A</span>%<br /> Decoded Packets: <span id="received-decoded-bits-error-percent">N/A</span>%<br />
</div> </div>
</div> </div>
<div>
<h2>Encoded Segments Received</h2>
<div>
<div class="raw-data" id="received-encoded-segment-bits"></div><br />
</div>
</div>
<div> <div>
<h2>Encoded Packets Received</h2> <h2>Encoded Packets Received</h2>
<div> <div>

318
index.js
View File

@@ -5,6 +5,7 @@ import * as PacketUtils from './PacketUtils';
import * as Humanize from './Humanize'; import * as Humanize from './Humanize';
import * as Randomizer from './Randomizer'; import * as Randomizer from './Randomizer';
import * as AudioSender from './AudioSender'; import * as AudioSender from './AudioSender';
import * as AudioReceiver from './AudioReceiver';
import * as CRC from './CRC.js'; import * as CRC from './CRC.js';
var audioContext; var audioContext;
@@ -32,9 +33,7 @@ let SENT_ENCODED_BITS = []; // bits with error encoding
let SENT_TRANSFER_BITS = []; // bits sent in the transfer let SENT_TRANSFER_BITS = []; // bits sent in the transfer
// interval and timeout ids // interval and timeout ids
var pauseGraphId;
let stopOscillatorsTimeoutId; let stopOscillatorsTimeoutId;
var sampleIntervalIds = [];
let EXCLUDED_CHANNELS = []; let EXCLUDED_CHANNELS = [];
@@ -63,11 +62,7 @@ let SEGMENT_SELECTED = -1;
var SEND_VIA_SPEAKER = false; var SEND_VIA_SPEAKER = false;
var RECEIVED_STREAM_START_MS = -1; 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 SAMPLES = [];
let SAMPLE_LAST_COLLECTED = 0; // time when sample was last collected
var bitStart = []; var bitStart = [];
var PAUSE = false; var PAUSE = false;
@@ -76,6 +71,16 @@ var PACKET_SIZE_BITS = 5; // 32 bytes, 256 bits
function handleWindowLoad() { function handleWindowLoad() {
TEXT_TO_SEND = Randomizer.text(5); TEXT_TO_SEND = Randomizer.text(5);
// Setup audio sender
AudioSender.addEventListener('begin', () => sendButton.innerText = 'Stop');
AudioSender.addEventListener('send', handleAudioSenderSend);
AudioSender.addEventListener('end', () => sendButton.innerText = 'Send');
// Setup audio receiver
AudioReceiver.addEventListener('begin', handleAudioReceiverStart);
AudioReceiver.addEventListener('receive', handleAudioReceiverReceive);
AudioReceiver.addEventListener('end', handleAudioReceiverEnd);
// Setup stream manager
StreamManager.addEventListener('change', handleStreamManagerChange);
// grab dom elements // grab dom elements
sendButton = document.getElementById('send-button'); sendButton = document.getElementById('send-button');
@@ -222,19 +227,14 @@ function showChannelList() {
drawChannels(); drawChannels();
} }
function handleAudioSenderStart() { function handleAudioSenderSend({bits}) {
sendButton.innerText = 'Stop'; SENT_TRANSFER_BITS.push(...bits);
}
function handleAudioSenderStop() {
sendButton.innerText = 'Send';
}
function handleAudioSenderSend(bit) {
SENT_TRANSFER_BITS.push(bit);
} }
function configurationChanged() { function configurationChanged() {
updatePacketUtils(); updatePacketUtils();
updateStreamManager(); updateStreamManager();
updateAudioSender(); updateAudioSender();
updateAudioReceiver();
showChannelList(); showChannelList();
updateFrequencyResolution(); updateFrequencyResolution();
updatePacketStats(); updatePacketStats();
@@ -243,12 +243,39 @@ function updateAudioSender() {
AudioSender.changeConfiguration({ AudioSender.changeConfiguration({
channels: getChannels(), channels: getChannels(),
destination: SEND_VIA_SPEAKER ? audioContext.destination : getAnalyser(), destination: SEND_VIA_SPEAKER ? audioContext.destination : getAnalyser(),
startCallback: handleAudioSenderStart,
stopCallback: handleAudioSenderStop,
sendCallback: handleAudioSenderSend,
waveForm: WAVE_FORM waveForm: WAVE_FORM
}); });
} }
const logFn = text => (...args) => {
// console.log(text, ...args);
}
const handleAudioReceiverStart = ({signalStart}) => {
StreamManager.reset();
RECEIVED_STREAM_START_MS = signalStart;
}
const handleAudioReceiverReceive = ({signalStart, signalIndex, indexStart, bits}) => {
const packetIndex = PacketUtils.getPacketIndex(signalStart, indexStart);
const segmentIndex = PacketUtils.getPacketSegmentIndex(signalStart, indexStart);
// Getting all 1's for only the first 5 segments?
// console.log(signalIndex, packetIndex, segmentIndex, bits.join(''));
StreamManager.addBits(packetIndex, segmentIndex, bits);
}
const handleAudioReceiverEnd = () => {
if(PAUSE_AFTER_END) {
stopGraph();
AudioSender.stop();
}
}
function updateAudioReceiver() {
AudioReceiver.changeConfiguration({
fskSets: getChannels(),
segmentDurationMs: SEGMENT_DURATION,
amplitudeThreshold: AMPLITUDE_THRESHOLD,
analyser: getAnalyser(),
signalIntervalMs: SEGMENT_DURATION,
sampleRate: getAudioContext().sampleRate
});
}
function updateStreamManager() { function updateStreamManager() {
StreamManager.setPacketEncoding( StreamManager.setPacketEncoding(
HAMMING_ERROR_CORRECTION ? HammingEncoding : undefined HAMMING_ERROR_CORRECTION ? HammingEncoding : undefined
@@ -522,26 +549,14 @@ function padArray(values, length, value) {
function stopGraph() { function stopGraph() {
PAUSE = true; PAUSE = true;
stopCollectingSamples(); AudioReceiver.stop();
}
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() { function resumeGraph() {
if(isListeningCheckbox.checked) { if(isListeningCheckbox.checked) {
if(PAUSE) { if(PAUSE) {
PAUSE = false; PAUSE = false;
startCollectingSamples(); AudioReceiver.start();
resetGraphData(); resetGraphData();
requestAnimationFrame(drawFrequencyData); requestAnimationFrame(drawFrequencyData);
} else { } else {
@@ -551,196 +566,6 @@ function resumeGraph() {
PAUSE = false; 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() { function getTransferredCorrectedBits() {
const bits = []; const bits = [];
@@ -755,25 +580,8 @@ function getTransferredCorrectedBits() {
} }
return bits; return bits;
} }
function processSegmentReceived(streamStarted, packetIndex, segmentIndex) {
const isSegment = sample => ( function handleStreamManagerChange() {
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; const channelCount = getChannels().length;
let allRawBits = StreamManager.getStreamBits(); let allRawBits = StreamManager.getStreamBits();
let allEncodedBits = StreamManager.getAllPacketBits(); let allEncodedBits = StreamManager.getAllPacketBits();
@@ -1005,22 +813,6 @@ function resetGraphData() {
SAMPLES.length = 0; SAMPLES.length = 0;
bitStart.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() { function getAudioContext() {
if(!audioContext) { if(!audioContext) {
audioContext = new (window.AudioContext || webkitAudioContext)(); audioContext = new (window.AudioContext || webkitAudioContext)();
@@ -1081,14 +873,14 @@ function bitsToText(bits) {
return bytesToText(bytes.buffer); return bytesToText(bytes.buffer);
} }
function handleSendButtonClick() { function handleSendButtonClick() {
if(stopOscillatorsTimeoutId) { if(sendButton.innerText === 'Stop') {
disconnectOscillators(); AudioSender.stop();
return; } else {
AudioReceiver.reset();
StreamManager.reset();
const text = document.getElementById('text-to-send').value;
sendBytes(textToBytes(text));
} }
resetReceivedData();
const text = document.getElementById('text-to-send').value;
sendBytes(textToBytes(text));
} }
function getAnalyser() { function getAnalyser() {
if(analyser) return analyser; if(analyser) return analyser;