mirror of
https://github.com/lewismoten/data-over-audio.git
synced 2026-02-02 00:15:50 +08:00
Add new frequency graph
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user