mirror of
https://github.com/lewismoten/data-over-audio.git
synced 2026-04-05 12:09:21 +08:00
Add new frequency graph
This commit is contained in:
@@ -5,6 +5,7 @@ class AvailableFskPairsPanel extends BasePanel {
|
||||
super('Available FSK Pairs');
|
||||
this.exclude = [];
|
||||
this.fskPairs = [];
|
||||
this.originalSelectedFskPairs = [];
|
||||
this.sampleRate = 48000;
|
||||
|
||||
this.addCanvas('fsk-spectrum', 200, 32);
|
||||
@@ -28,9 +29,25 @@ class AvailableFskPairsPanel extends BasePanel {
|
||||
} else if(!this.exclude.includes(event.id)) {
|
||||
this.exclude.push(event.id);
|
||||
}
|
||||
this.checkChanges();
|
||||
this.drawFskSpectrum();
|
||||
};
|
||||
|
||||
checkChanges = () => {
|
||||
const selected = this.getSelectedFskPairs();
|
||||
const original = this.originalSelectedFskPairs;
|
||||
let changed = false;
|
||||
if(original.length !== selected.length) {
|
||||
changed = true;
|
||||
} else {
|
||||
const hertz = selected.flat();
|
||||
changed = original.flat().some((hz, i) => hz !== hertz[i]);
|
||||
}
|
||||
if(changed) {
|
||||
this.originalSelectedFskPairs = selected;
|
||||
this.dispatcher.emit('change', {selected});
|
||||
}
|
||||
}
|
||||
getSelectedFskPairs = () => this.fskPairs
|
||||
.filter(this.isSelected);
|
||||
|
||||
@@ -38,6 +55,7 @@ class AvailableFskPairsPanel extends BasePanel {
|
||||
setSelectedIndexes = (values) => {
|
||||
this.exclude = values;
|
||||
this.setFskPairs(this.fskPairs);
|
||||
this.checkChanges();
|
||||
}
|
||||
|
||||
setFskPairs = fskPairs => {
|
||||
@@ -51,6 +69,7 @@ class AvailableFskPairsPanel extends BasePanel {
|
||||
eventName: 'select'
|
||||
}));
|
||||
this.replaceCheckedInputs('checkbox', 'fsk-pairs', items);
|
||||
this.checkChanges();
|
||||
this.drawFskSpectrum();
|
||||
}
|
||||
|
||||
|
||||
@@ -195,6 +195,10 @@ class BasePanel {
|
||||
const element = this.getElement(id);
|
||||
element.innerHTML = html;
|
||||
}
|
||||
getNumberById = id => {
|
||||
const value = this.getValueById(id);
|
||||
return parseFloat(value);
|
||||
}
|
||||
getValueById = (id) => {
|
||||
const element = this.getElement(id);
|
||||
switch(element.tagName) {
|
||||
|
||||
249
Panels/FrequencyGraphPanel.js
Normal file
249
Panels/FrequencyGraphPanel.js
Normal file
@@ -0,0 +1,249 @@
|
||||
import BasePanel from './BasePanel';
|
||||
|
||||
class FrequencyGraphPanel extends BasePanel {
|
||||
constructor() {
|
||||
super('Frequency Graph');
|
||||
this.fskPairs = [];
|
||||
this.sampleRate = 48000;
|
||||
this.samplingPeriod = 30;
|
||||
this.signalStart = performance.now();
|
||||
this.signalEnd = this.signalStart;
|
||||
this.samples = [];
|
||||
this.duration = 200;
|
||||
this.amplitudeThreshold = 0;
|
||||
this.addButton('toggle', 'Start', 'toggle');
|
||||
this.addNewLine();
|
||||
this.addCanvas('frequency-graph', 500, 150);
|
||||
this.addEventListener('toggle', this.handleToggle);
|
||||
};
|
||||
setDurationMilliseconds = (millseconds) => {
|
||||
this.duration = millseconds;
|
||||
this.draw(false);
|
||||
}
|
||||
setSignalStart = milliseconds => {
|
||||
this.signalStart = milliseconds;
|
||||
}
|
||||
setSignalEnd = milliseconds => {
|
||||
this.signalEnd = milliseconds;
|
||||
}
|
||||
setSamplingPeriod = (milliseconds) => this.samplingPeriod = milliseconds;
|
||||
|
||||
setAmplitudeThreshold = value => {
|
||||
this.amplitudeThreshold = value;
|
||||
}
|
||||
setSampleRate = (value) => {
|
||||
this.sampleRate = value;
|
||||
}
|
||||
setFskPairs = fskPairs => {
|
||||
this.fskPairs = fskPairs;
|
||||
}
|
||||
setAnalyser = (analyser) => {
|
||||
this.analyser = analyser;
|
||||
}
|
||||
isRunning = () => !!this.intervalId || !!this.animationFrameId;
|
||||
|
||||
handleToggle = () => {
|
||||
if(this.isRunning()) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
start = () => {
|
||||
this.setValueById('toggle', 'Stop');
|
||||
if(!this.intervalId) {
|
||||
this.intervalId = window.setInterval(this.collectSamples, 5);
|
||||
}
|
||||
if(!this.animationFrameId) {
|
||||
this.animationFrameId = window.requestAnimationFrame(this.draw);
|
||||
}
|
||||
}
|
||||
stop = () => {
|
||||
this.setValueById('toggle', 'Start');
|
||||
if(this.intervalId) {
|
||||
window.clearInterval(this.intervalId);
|
||||
this.intervalId = undefined;
|
||||
}
|
||||
if(this.animationFrameId) {
|
||||
window.cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = undefined;
|
||||
}
|
||||
// final draw
|
||||
this.draw(false);
|
||||
}
|
||||
collectSamples = () => {
|
||||
// Nothing to collect
|
||||
if(this.fskPairs.length === 0) return;
|
||||
// Nothing to collect with
|
||||
const analyser = this.analyser;
|
||||
if(!analyser) return;
|
||||
|
||||
const frequencyResolution = this.sampleRate / 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);
|
||||
const now = performance.now();
|
||||
this.samples.unshift({
|
||||
time: now,
|
||||
fskPairs: this.fskPairs.map(ampsFromManyHz)
|
||||
});
|
||||
|
||||
this.samples = this.samples.filter(sample => now - sample.time < this.duration);
|
||||
}
|
||||
|
||||
draw = () => {
|
||||
const maxAmps = 280; // inflated for height
|
||||
const ultimateFrequency = this.sampleRate / 2;
|
||||
const canvas = this.getElement('frequency-graph');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const {height, width} = canvas;
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
let now;
|
||||
|
||||
if(this.samples.length > 1) {
|
||||
now = this.samples[0].time;
|
||||
this.fskPairs.forEach((fsk, fskIndex) => {
|
||||
fsk.forEach((hz, hzIndex)=> {
|
||||
ctx.beginPath();
|
||||
const hue = Math.floor(hz/ultimateFrequency * 360);
|
||||
if(hzIndex === 0) {
|
||||
ctx.strokeStyle = `hsla(${hue}, 100%, 50%, 50%)`;
|
||||
ctx.setLineDash([5, 5]);
|
||||
} else {
|
||||
ctx.strokeStyle = `hsl(${hue}, 100%, 50%)`;
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
this.samples.forEach(({time, fskPairs}, i) => {
|
||||
fsk = fskPairs[fskIndex];
|
||||
if(!fsk) return; // configuration changed
|
||||
let x = ((now - time) / this.duration) * width;
|
||||
const percent = (fsk[hzIndex] / maxAmps);
|
||||
let y = (1 - percent) * height;
|
||||
if(i === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
});
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
})
|
||||
})
|
||||
};
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Amplitude Threshold
|
||||
ctx.strokeStyle = 'hsla(0, 0%, 100%, 20%)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
let y = height * (1-(this.amplitudeThreshold * 255) / maxAmps);
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(width, y);
|
||||
ctx.stroke();
|
||||
|
||||
// sampling periods
|
||||
if(!now) now = performance.now();
|
||||
let lastPeriodStart = now - ((now - this.signalStart) % this.samplingPeriod);
|
||||
let lastTextX = -1000;
|
||||
ctx.lineWidth = 1;
|
||||
this.lastCountX = -1000;
|
||||
for(let time = lastPeriodStart; time > now - this.duration; time -= this.samplingPeriod) {
|
||||
const end = time + this.samplingPeriod;
|
||||
let rightX = ((now - time) / this.duration) * width;
|
||||
let leftX = ((now - end) / this.duration) * width;
|
||||
// Line for when period started
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(rightX, 0);
|
||||
ctx.lineTo(rightX, height);
|
||||
ctx.strokeStyle = 'hsla(120, 100%, 100%, 50%)';
|
||||
ctx.stroke();
|
||||
|
||||
let samplePeriodWidth = rightX - leftX;
|
||||
ctx.fontSize = '24px';
|
||||
|
||||
// Sample Index
|
||||
if(time >= this.signalStart && (this.signalEnd < this.signalStart || time < this.signalEnd)) {
|
||||
|
||||
const signalIndex = Math.floor((time - this.signalStart) / this.samplingPeriod);
|
||||
let text = signalIndex.toLocaleString();
|
||||
let size = ctx.measureText(text);
|
||||
let textX = leftX + (samplePeriodWidth / 2) - (size.width / 2);
|
||||
// far enough from prior text?
|
||||
if(textX - lastTextX > size.width) {
|
||||
lastTextX = textX;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.textBaseline = 'bottom';
|
||||
let textY = height - 12;
|
||||
ctx.strokeStyle = 'black';
|
||||
ctx.strokeText(text, textX, textY);
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillText(text, textX, textY);
|
||||
}
|
||||
}
|
||||
|
||||
// sample counts
|
||||
this.drawSampleCount(ctx, width, height, time, end, leftX, samplePeriodWidth);
|
||||
}
|
||||
|
||||
this.drawSignalStart(ctx, width, height, now);
|
||||
this.drawSignalEnd(ctx, width, height, now);
|
||||
|
||||
if(this.isRunning()) {
|
||||
this.animationFrameId = requestAnimationFrame(this.draw);
|
||||
}
|
||||
}
|
||||
drawSampleCount = (ctx, width, height, start, end, leftX, samplePeriodWidth) => {
|
||||
const count = this.samples.filter(sample => {
|
||||
return sample.time >= start && sample.time < end;
|
||||
}).length;
|
||||
|
||||
let text = count.toLocaleString();
|
||||
let size = ctx.measureText(text);
|
||||
let textX = leftX + (samplePeriodWidth / 2) - (size.width / 2);
|
||||
|
||||
// far enough from prior text?
|
||||
if(textX - this.lastCountX > size.width) {
|
||||
this.lastCountX = textX;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.textBaseline = 'bottom';
|
||||
let textY = 20;
|
||||
ctx.strokeStyle = 'black';
|
||||
ctx.strokeText(text, textX, textY);
|
||||
if(count === 0) {
|
||||
ctx.fillStyle = 'red';
|
||||
} else if(count < 3) {
|
||||
ctx.fillStyle = 'yellow';
|
||||
} else {
|
||||
ctx.fillStyle = 'white';
|
||||
}
|
||||
ctx.fillText(text, textX, textY);
|
||||
}
|
||||
}
|
||||
drawSignalStart = (ctx, width, height, now) => {
|
||||
if(now - this.signalStart < this.duration) {
|
||||
let x = ((now - this.signalStart) / this.duration) * width;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeStyle = 'hsla(60, 100%, 50%, 50%)';
|
||||
ctx.stroke();
|
||||
}
|
||||
};
|
||||
drawSignalEnd = (ctx, width, height, now) => {
|
||||
if(now - this.signalEnd < this.duration) {
|
||||
let x = ((now - this.signalEnd) / this.duration) * width;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeStyle = 'hsla(60, 100%, 50%, 50%)';
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FrequencyGraphPanel;
|
||||
26
Panels/GraphConfigurationPanel.js
Normal file
26
Panels/GraphConfigurationPanel.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import BasePanel from './BasePanel';
|
||||
|
||||
class GraphConfigurationPanel extends BasePanel {
|
||||
constructor() {
|
||||
super('Graphs');
|
||||
this.addCheckboxes('checkboxes', [
|
||||
{text: 'Pause after signal ends', id: 'pause-after-end', eventName: 'pauseAfterEndChange'}
|
||||
])
|
||||
this.openField('Duration');
|
||||
this.addInputNumber('duration', 1, {min: 0.03, step: 0.001, eventName: 'durationChange'});
|
||||
this.addText('s');
|
||||
this.closeField();
|
||||
};
|
||||
|
||||
getDurationSeconds = () => this.getNumberById('duration');
|
||||
setDurationSeconds = (seconds) => this.setValueById('duration', seconds);
|
||||
|
||||
getDurationMilliseconds = () => this.getDurationSeconds() * 1000;
|
||||
setDurationMilliseconds = (milliseconds) => this.setDurationSeconds(milliseconds / 1000);
|
||||
|
||||
getPauseAfterEnd = () => this.getCheckedById('pause-after-end');
|
||||
setPauseAfterEnd = (value) => this.setCheckedById('pause-after-end', value);
|
||||
|
||||
}
|
||||
|
||||
export default GraphConfigurationPanel;
|
||||
@@ -28,12 +28,19 @@ class SignalPanel extends BasePanel {
|
||||
this.addText('%');
|
||||
this.closeField();
|
||||
|
||||
this.openField('Timeout');
|
||||
this.addInputNumber('timeout', 30, {min: 30, max: 1000, eventName: 'timeoutChange'});
|
||||
this.addText('ms');
|
||||
this.closeField();
|
||||
|
||||
this.openField('Smoothing Time Constant');
|
||||
this.addInputNumber('smoothing-time-constant', 0, {min: 0, max: 100, eventName: 'smothingTimeConstantChange', translation: 'percent'});
|
||||
this.addText('%');
|
||||
this.closeField();
|
||||
};
|
||||
|
||||
getTimeoutMilliseconds = () => this.getNumberById('timeout');
|
||||
setTimeoutMilliseconds = (milliseconds) => this.setValueById('timeout', milliseconds);
|
||||
getWaveform = () => this.getValueById('wave-form');
|
||||
setWaveform = (value) => this.setValueById('wave-form', value);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user