import BasePanel from './BasePanel.js'; const SAMPLE_RATE_CAP = 41000; // 48000 on iPhone class FrequencyPanel extends BasePanel { constructor() { super('Frequencies'); this.sampleRate = SAMPLE_RATE_CAP; const ultimateFrequency = this.sampleRate / 2; this.addCanvas('frequency-spectrum', 200, 32); this.addNewLine(); this.openField('Minimum'); this.addInputNumber('minimum-frequency', 0, {min: 0, max: ultimateFrequency, eventName: 'minimumFrequencyChange'}); this.closeField(); this.openField('Maximum'); this.addInputNumber('maximum-frequency', ultimateFrequency, {min: 0, max: ultimateFrequency, eventName: 'maximumFrequencyChange'}); this.closeField(); this.openField('FFT Size'); this.addText('2^'); this.addInputNumber('fft-power', 10, {min: 5, max: 15, eventName: 'fftSizeChange', translation: 'power of 2'}); this.addText(' '); this.addDynamicText('fft-size', 'N/A'); this.closeField(); this.openField('Frequency Resolution'); this.addDynamicText('frequency-resolution-size', 'N/A'); this.addText(' Hz'); this.closeField(); this.openField('FSK Padding'); this.addInputNumber('fsk-padding', 1, {min: 1, max: 20, eventName: 'fskPaddingChange'}); this.closeField(); this.openField('Multi-FSK Padding'); this.addInputNumber('multi-fsk-padding', 0, {min: 0, max: 20, eventName: 'multiFskPaddingChange'}); this.closeField(); this.openField('FSK Pairs Available'); this.addDynamicText('fsk-count', 'N/A'); this.closeField(); this.addEventListener('multiFskPaddingChange', this.checkFskPairsChanged); this.addEventListener('fskPaddingChange', this.checkFskPairsChanged); this.addEventListener('fftSizeChange', this.checkFskPairsChanged); this.addEventListener('fftSizeChange', this.handleFftSizeChanged); this.addEventListener('maximumFrequencyChange', this.checkFskPairsChanged); this.addEventListener('minimumFrequencyChange', this.checkFskPairsChanged); this.originalFskPairs = this.getFskPairs(); this.drawFrequencySpectrum(); }; setSampleRate = (value) => { this.sampleRate = value; this.checkFskPairsChanged(); } getMinimumFrequency = () => parseInt(this.getValueById('minimum-frequency')); setMinimumFrequency = value => { this.setValueById('minimum-frequency', value); this.checkFskPairsChanged(); }; getMaximumFrequency = () => parseInt(this.getValueById('maximum-frequency')); setMaximumFrequency = value => { this.setValueById('maximum-frequency', value); this.checkFskPairsChanged(); } getFftSize = () => 2 ** parseInt(this.getValueById('fft-power')); setFftSize = (value) => { this.setValueById('fft-power', Math.log2(value)); this.handleFftSizeChanged(); this.checkFskPairsChanged(); } handleFftSizeChanged = () => { const fftSize = this.getFftSize(); this.setValueById('fft-size', fftSize.toLocaleString()); this.checkFskPairsChanged(); } getFskPadding = () => parseInt(this.getValueById('fsk-padding')); setFskPadding = (value) => { this.setValueById('fsk-padding', value); this.checkFskPairsChanged(); } getMultiFskPadding = () => parseInt(this.getValueById('multi-fsk-padding')); setMultiFskPadding = (value) => { this.setValueById('multi-fsk-padding', value); this.checkFskPairsChanged(); } checkFskPairsChanged = () => { const original = this.originalFskPairs; const current = this.getFskPairs(); const fftSize = this.getFftSize(); const resolution = this.sampleRate / fftSize; if(this.sampleRate > SAMPLE_RATE_CAP) { const resolutionCap = SAMPLE_RATE_CAP / fftSize; this.setValueById( 'frequency-resolution-size', parseFloat(resolution.toFixed(1)).toLocaleString() + " / " + parseFloat(resolutionCap.toFixed(1)).toLocaleString() ); } else { this.setValueById('frequency-resolution-size', parseFloat(resolution.toFixed(1)).toLocaleString()); } let changed = true; if(original.length !== current.length) { changed = true; } else { const currentHz = current.flat(); changed = original.flat().some((hz, i) => hz !== currentHz[i]); } if(changed) { this.originalFskPairs = current; this.setValueById('fsk-count', current.length.toLocaleString()); this.drawFrequencySpectrum(); this.dispatcher.emit('fskPairsChange', {value: current}); } } getFskPairs = () => { const fftSize = this.getFftSize(); const fskPadding = this.getFskPadding(); const multiFskPadding = this.getMultiFskPadding(); const frequencyResolution = Math.min(this.sampleRate, SAMPLE_RATE_CAP) / fftSize; const fskPairs = []; const multiFskStep = frequencyResolution * (2 + multiFskPadding) * fskPadding; const minimumFrequency = this.getMinimumFrequency(); const maximumFrequency = this.getMaximumFrequency(); for(let hz = minimumFrequency; hz < maximumFrequency; hz+= multiFskStep) { const lowHz = hz; const highHz = hz + frequencyResolution * fskPadding; if(lowHz < minimumFrequency) continue; if(highHz > maximumFrequency) break; fskPairs.push([lowHz, highHz]); } return fskPairs; } drawFrequencySpectrum = () => { const ultimateFrequency = SAMPLE_RATE_CAP / 2; const fskPairs = this.getFskPairs(); const canvas = this.getElement('frequency-spectrum'); const ctx = canvas.getContext('2d'); const {height, width} = canvas; ctx.clearRect(0, 0, width, height); // Human Hearing let x1 = (20/ultimateFrequency) * width; let x2 = (20000/ultimateFrequency) * width; ctx.fillStyle = 'hsla(0, 0%, 100%, 20%)'; ctx.fillRect( x1, 0, x2 - x1, height ); // Telephone x1 = (300/ultimateFrequency) * width; x2 = (3400/ultimateFrequency) * width; ctx.fillStyle = 'hsla(60, 50%, 50%, 20%)'; ctx.fillRect( x1, 0, x2 - x1, height ); ctx.lineWith = 1; const plotHz = hz => { const percent = (hz / ultimateFrequency); const hue = Math.floor(percent * 360); ctx.strokeStyle = `hsl(${hue}, 100%, 50%)`; const x = percent * width; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } fskPairs.forEach(fsk => fsk.forEach(plotHz)); } } export default FrequencyPanel;