226 lines
6.2 KiB
JavaScript
226 lines
6.2 KiB
JavaScript
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,
|
|
}
|