diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-17 16:12:21 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-17 16:12:21 +0100 |
| commit | 03579c4a33ab3955ff9924a6dcd882fe91dd9aaa (patch) | |
| tree | be458d2ac4bc0d7160be8a18526b4e9157af33a5 /tools/mq_editor/index.html | |
| parent | e3f0b002c0998c8553e782273b254869107ffc0f (diff) | |
feat(mq_editor): Phase 1 - MQ extraction and visualization (SPECTRAL_BRUSH_2)
Implement McAulay-Quatieri sinusoidal analysis tool for audio compression.
New files:
- doc/SPECTRAL_BRUSH_2.md: Complete design doc (MQ algorithm, data format, synthesis, roadmap)
- tools/mq_editor/index.html: Web UI (file loader, params, canvas)
- tools/mq_editor/fft.js: Radix-2 Cooley-Tukey FFT (from spectral_editor)
- tools/mq_editor/mq_extract.js: MQ algorithm (peak detection, tracking, bezier fitting)
- tools/mq_editor/viewer.js: Visualization (spectrogram, partials, zoom, axes)
- tools/mq_editor/README.md: Usage and implementation status
Features:
- Load WAV → extract sinusoidal partials → fit cubic bezier curves
- Time-frequency spectrogram with hot colormap (0-16 kHz)
- Horizontal zoom (mousewheel) around mouse position
- Axis ticks with labels (time: seconds, freq: Hz/kHz)
- Mouse tooltip showing time/frequency coordinates
- Real-time adjustable MQ parameters (FFT size, hop, threshold)
Algorithm:
- STFT with Hann windows (2048 FFT, 512 hop)
- Peak detection with parabolic interpolation
- Birth/death/continuation tracking (50 Hz tolerance)
- Cubic bezier fitting (4 control points per trajectory)
Next: Phase 2 (JS synthesizer for audio preview)
handoff(Claude): MQ editor Phase 1 complete. Ready for synthesis implementation.
Diffstat (limited to 'tools/mq_editor/index.html')
| -rw-r--r-- | tools/mq_editor/index.html | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/tools/mq_editor/index.html b/tools/mq_editor/index.html new file mode 100644 index 0000000..d44f19b --- /dev/null +++ b/tools/mq_editor/index.html @@ -0,0 +1,175 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>MQ Spectral Editor</title> + <style> + body { + font-family: monospace; + margin: 20px; + background: #1a1a1a; + color: #ddd; + } + .toolbar { + margin-bottom: 10px; + padding: 10px; + background: #2a2a2a; + border-radius: 4px; + } + button { + background: #3a3a3a; + color: #ddd; + border: 1px solid #555; + padding: 8px 16px; + margin-right: 8px; + cursor: pointer; + border-radius: 4px; + } + button:hover { background: #4a4a4a; } + button:disabled { opacity: 0.5; cursor: not-allowed; } + input[type="file"] { margin-right: 16px; } + .params { + display: inline-block; + margin-left: 20px; + } + label { + margin-right: 8px; + } + input[type="number"], select { + width: 80px; + background: #3a3a3a; + color: #ddd; + border: 1px solid #555; + padding: 4px; + border-radius: 3px; + } + #canvas { + border: 1px solid #555; + background: #000; + cursor: crosshair; + display: block; + margin-top: 10px; + } + #status { + margin-top: 10px; + padding: 8px; + background: #2a2a2a; + border-radius: 4px; + min-height: 20px; + } + .info { + color: #4af; + } + .warn { + color: #fa4; + } + .error { + color: #f44; + } + </style> +</head> +<body> + <h2>MQ Spectral Editor</h2> + + <div class="toolbar"> + <input type="file" id="wavFile" accept=".wav"> + <button id="extractBtn" disabled>Extract Partials</button> + + <div class="params"> + <label>FFT Size:</label> + <select id="fftSize"> + <option value="1024">1024</option> + <option value="2048" selected>2048</option> + <option value="4096">4096</option> + </select> + + <label>Hop:</label> + <input type="number" id="hopSize" value="512" min="64" max="2048" step="64"> + + <label>Threshold (dB):</label> + <input type="number" id="threshold" value="-60" min="-80" max="-20" step="5"> + </div> + </div> + + <canvas id="canvas" width="1400" height="600"></canvas> + + <div id="tooltip" style="position: fixed; display: none; background: #2a2a2a; padding: 4px 8px; border: 1px solid #555; border-radius: 3px; pointer-events: none; font-size: 11px; z-index: 1000;"></div> + + <div id="status">Load a WAV file to begin...</div> + + <script src="fft.js"></script> + <script src="mq_extract.js"></script> + <script src="viewer.js"></script> + <script> + let audioBuffer = null; + let viewer = null; + + const wavFile = document.getElementById('wavFile'); + const extractBtn = document.getElementById('extractBtn'); + const canvas = document.getElementById('canvas'); + const status = document.getElementById('status'); + + const fftSize = document.getElementById('fftSize'); + const hopSize = document.getElementById('hopSize'); + const threshold = document.getElementById('threshold'); + + // Load WAV file + wavFile.addEventListener('change', async (e) => { + const file = e.target.files[0]; + if (!file) return; + + setStatus('Loading WAV...', 'info'); + try { + const arrayBuffer = await file.arrayBuffer(); + const audioContext = new AudioContext(); + audioBuffer = await audioContext.decodeAudioData(arrayBuffer); + + extractBtn.disabled = false; + setStatus(`Loaded: ${audioBuffer.duration.toFixed(2)}s, ${audioBuffer.sampleRate}Hz, ${audioBuffer.numberOfChannels}ch`, 'info'); + + // Create viewer + viewer = new SpectrogramViewer(canvas, audioBuffer); + } catch (err) { + setStatus('Error loading WAV: ' + err.message, 'error'); + console.error(err); + } + }); + + // Extract partials + extractBtn.addEventListener('click', () => { + if (!audioBuffer) return; + + setStatus('Extracting partials...', 'info'); + extractBtn.disabled = true; + + setTimeout(() => { + try { + const params = { + fftSize: parseInt(fftSize.value), + hopSize: parseInt(hopSize.value), + threshold: parseFloat(threshold.value), + sampleRate: audioBuffer.sampleRate + }; + + const partials = extractPartials(audioBuffer, params); + + setStatus(`Extracted ${partials.length} partials`, 'info'); + + // Update viewer + viewer.setPartials(partials); + + } catch (err) { + setStatus('Extraction error: ' + err.message, 'error'); + console.error(err); + } + extractBtn.disabled = false; + }, 50); + }); + + function setStatus(msg, type = '') { + status.innerHTML = msg; + status.className = type; + } + </script> +</body> +</html> |
