summaryrefslogtreecommitdiff
path: root/tools/mq_editor/viewer.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/mq_editor/viewer.js')
-rw-r--r--tools/mq_editor/viewer.js113
1 files changed, 68 insertions, 45 deletions
diff --git a/tools/mq_editor/viewer.js b/tools/mq_editor/viewer.js
index 459cd9e..451421d 100644
--- a/tools/mq_editor/viewer.js
+++ b/tools/mq_editor/viewer.js
@@ -2,10 +2,11 @@
// Handles all visualization: spectrogram, partials, zoom, mouse interaction
class SpectrogramViewer {
- constructor(canvas, audioBuffer) {
+ constructor(canvas, audioBuffer, stftCache) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.audioBuffer = audioBuffer;
+ this.stftCache = stftCache;
this.partials = [];
// Fixed time bounds
@@ -30,6 +31,11 @@ class SpectrogramViewer {
// Playhead
this.playheadTime = -1; // -1 = not playing
+ // Spectrum viewer
+ this.spectrumCanvas = document.getElementById('spectrumCanvas');
+ this.spectrumCtx = this.spectrumCanvas ? this.spectrumCanvas.getContext('2d') : null;
+ this.spectrumTime = 0; // Time to display spectrum for
+
// Setup event handlers
this.setupMouseHandlers();
@@ -40,6 +46,9 @@ class SpectrogramViewer {
setPlayheadTime(time) {
this.playheadTime = time;
+ if (time >= 0) {
+ this.spectrumTime = time;
+ }
this.render();
}
@@ -88,6 +97,7 @@ class SpectrogramViewer {
this.renderPartials();
this.drawAxes();
this.drawPlayhead();
+ this.renderSpectrum();
}
drawPlayhead() {
@@ -108,46 +118,36 @@ class SpectrogramViewer {
// Render spectrogram background
renderSpectrogram() {
- const {canvas, ctx, audioBuffer} = this;
+ const {canvas, ctx, stftCache} = this;
const width = canvas.width;
const height = canvas.height;
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, width, height);
- const signal = getMono(audioBuffer);
- const fftSize = 1024;
- const hopSize = 256;
- const sampleRate = audioBuffer.sampleRate;
+ if (!stftCache) return;
- const numFrames = Math.floor((signal.length - fftSize) / hopSize);
+ const sampleRate = this.audioBuffer.sampleRate;
+ const hopSize = stftCache.hopSize;
+ const fftSize = stftCache.fftSize;
const frameDuration = hopSize / sampleRate;
const viewDuration = this.t_view_max - this.t_view_min;
// Map view bounds to frame indices
const startFrameIdx = Math.floor(this.t_view_min * sampleRate / hopSize);
const endFrameIdx = Math.ceil(this.t_view_max * sampleRate / hopSize);
+ const numFrames = stftCache.getNumFrames();
for (let frameIdx = startFrameIdx; frameIdx < endFrameIdx; ++frameIdx) {
if (frameIdx < 0 || frameIdx >= numFrames) continue;
- const offset = frameIdx * hopSize;
- if (offset + fftSize > signal.length) break;
+ const frame = stftCache.getFrameAtIndex(frameIdx);
+ if (!frame) continue;
- const frame = signal.slice(offset, offset + fftSize);
-
- // Windowing
- const windowed = new Float32Array(fftSize);
- for (let i = 0; i < fftSize; ++i) {
- const w = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / fftSize);
- windowed[i] = frame[i] * w;
- }
-
- // FFT
- const spectrum = realFFT(windowed);
+ const spectrum = frame.spectrum;
// Compute frame time range
- const frameTime = frameIdx * hopSize / sampleRate;
+ const frameTime = frame.time;
const frameTimeEnd = frameTime + frameDuration;
const xStart = Math.floor((frameTime - this.t_view_min) / viewDuration * width);
@@ -350,6 +350,12 @@ class SpectrogramViewer {
const freq = this.canvasToFreq(y);
const intensity = this.getIntensityAt(time, freq);
+ // Update spectrum time when not playing
+ if (this.playheadTime < 0) {
+ this.spectrumTime = time;
+ this.renderSpectrum();
+ }
+
tooltip.style.left = (e.clientX + 10) + 'px';
tooltip.style.top = (e.clientY + 10) + 'px';
tooltip.style.display = 'block';
@@ -408,38 +414,55 @@ class SpectrogramViewer {
}
getIntensityAt(time, freq) {
- const signal = getMono(this.audioBuffer);
- const fftSize = 1024;
- const hopSize = 256;
- const sampleRate = this.audioBuffer.sampleRate;
+ if (!this.stftCache) return -80;
+ return this.stftCache.getMagnitudeDB(time, freq);
+ }
- const frameIdx = Math.floor(time * sampleRate / hopSize);
- const offset = frameIdx * hopSize;
+ renderSpectrum() {
+ if (!this.spectrumCtx || !this.stftCache) return;
- if (offset < 0 || offset + fftSize > signal.length) return -80;
+ const canvas = this.spectrumCanvas;
+ const ctx = this.spectrumCtx;
+ const width = canvas.width;
+ const height = canvas.height;
- const frame = signal.slice(offset, offset + fftSize);
+ // Clear
+ ctx.fillStyle = '#1e1e1e';
+ ctx.fillRect(0, 0, width, height);
- // Apply Hann window
- const windowed = new Float32Array(fftSize);
- for (let i = 0; i < fftSize; ++i) {
- const w = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / fftSize);
- windowed[i] = frame[i] * w;
- }
+ const spectrum = this.stftCache.getFFT(this.spectrumTime);
+ if (!spectrum) return;
- // FFT
- const spectrum = realFFT(windowed);
+ const fftSize = this.stftCache.fftSize;
- // Find closest bin
- const bin = Math.round(freq * fftSize / sampleRate);
- if (bin < 0 || bin >= fftSize / 2) return -80;
+ // Draw bars
+ const numBars = 100;
+ const barWidth = width / numBars;
+ const numBins = fftSize / 2;
- const re = spectrum[bin * 2];
- const im = spectrum[bin * 2 + 1];
- const mag = Math.sqrt(re * re + im * im);
- const magDB = 20 * Math.log10(Math.max(mag, 1e-10));
+ for (let i = 0; i < numBars; ++i) {
+ const binIdx = Math.floor(i * numBins / numBars);
+ const re = spectrum[binIdx * 2];
+ const im = spectrum[binIdx * 2 + 1];
+ const mag = Math.sqrt(re * re + im * im);
+ const magDB = 20 * Math.log10(Math.max(mag, 1e-10));
- return magDB;
+ // Normalize to [0, 1]
+ const normalized = (magDB + 80) / 80;
+ const clamped = Math.max(0, Math.min(1, normalized));
+ const intensity = Math.pow(clamped, 0.3);
+
+ const barHeight = intensity * height;
+ const x = i * barWidth;
+
+ // Gradient from bottom (cyan) to top (yellow)
+ const gradient = ctx.createLinearGradient(0, height - barHeight, 0, height);
+ gradient.addColorStop(0, '#4af');
+ gradient.addColorStop(1, '#fa4');
+
+ ctx.fillStyle = gradient;
+ ctx.fillRect(x, height - barHeight, barWidth - 1, barHeight);
+ }
}
// Utilities