271 lines
8.2 KiB
JavaScript
271 lines
8.2 KiB
JavaScript
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.samplesPerGroup = 1;
|
|
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;
|
|
if(!this.isRunning()) this.draw();
|
|
}
|
|
setSignalStart = milliseconds => {
|
|
this.signalStart = milliseconds;
|
|
}
|
|
setSignalEnd = milliseconds => {
|
|
this.signalEnd = milliseconds;
|
|
}
|
|
setSamplingPeriod = (milliseconds) => {
|
|
this.samplingPeriod = milliseconds;
|
|
if(!this.isRunning()) this.draw();
|
|
}
|
|
setSamplePeriodsPerGroup = count => {
|
|
this.samplesPerGroup = count;
|
|
if(!this.isRunning()) this.draw();
|
|
}
|
|
setAmplitudeThreshold = value => {
|
|
this.amplitudeThreshold = value;
|
|
if(!this.isRunning()) this.draw();
|
|
}
|
|
setSampleRate = (value) => {
|
|
this.sampleRate = value;
|
|
}
|
|
setFskPairs = fskPairs => {
|
|
this.fskPairs = fskPairs;
|
|
if(!this.isRunning()) this.draw();
|
|
}
|
|
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 = 290; // inflated for height
|
|
const ultimateFrequency = this.sampleRate / 2;
|
|
const canvas = this.getElement('frequency-graph');
|
|
const ctx = canvas.getContext('2d');
|
|
const {height, width} = canvas;
|
|
ctx.fillStyle = 'black';
|
|
ctx.fillRect(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;
|
|
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%, 10%)';
|
|
ctx.lineWidth = 1;
|
|
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);
|
|
|
|
const indexInGroup = signalIndex % this.samplesPerGroup;
|
|
if(indexInGroup === 0) {
|
|
// Line for when group started
|
|
ctx.beginPath();
|
|
ctx.moveTo(rightX, 0);
|
|
ctx.lineTo(rightX, height);
|
|
ctx.strokeStyle = 'hsla(180, 100%, 50%, 50%)';
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
}
|
|
|
|
let text = indexInGroup.toLocaleString();
|
|
let size = ctx.measureText(text);
|
|
let textX = leftX + (samplePeriodWidth / 2) - (size.width / 2);
|
|
// far enough from prior text?
|
|
if(textX - lastTextX > (size.width * 2)) {
|
|
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, 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, 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 * 2)) {
|
|
this.lastCountX = textX;
|
|
ctx.lineWidth = 2;
|
|
ctx.textBaseline = 'bottom';
|
|
let textY = 10;
|
|
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; |