setup audio receiver and event dispatcher
This commit is contained in:
225
AudioReceiver.js
Normal file
225
AudioReceiver.js
Normal 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,
|
||||||
|
}
|
||||||
@@ -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
45
Dispatcher.js
Normal 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;
|
||||||
@@ -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;
|
||||||
|
|||||||
25
index.html
25
index.html
@@ -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
318
index.js
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user