diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-17 19:37:10 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-17 19:37:10 +0100 |
| commit | 94aa832ef673338865b28e5886537c85d6b6d876 (patch) | |
| tree | e064949bfde2626fc90a5fa5b8e2261072f1e5b5 /tools/mq_editor/mq_extract.js | |
| parent | 9416e4ed202d66b20649fb445b6a352f804efd8c (diff) | |
feat(mq_editor): Improve partial tracking and add audio playback
Tracking improvements:
- Frequency-dependent threshold (5% of freq, min 20 Hz)
- Candidate system requiring 3-frame persistence before birth
- Extended death tolerance (5 frames) for robust trajectories
- Minimum 10-frame length filter for valid partials
- Result: cleaner, less scattered partial trajectories
Audio playback:
- Web Audio API integration for original WAV playback
- Play/Stop buttons with proper state management
- Animated red playhead bar during playback
- Keyboard shortcuts: '2' plays original, '1' reserved for synthesis
Visualization:
- Power law (gamma=0.3) for improved spectrogram contrast
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'tools/mq_editor/mq_extract.js')
| -rw-r--r-- | tools/mq_editor/mq_extract.js | 59 |
1 files changed, 52 insertions, 7 deletions
diff --git a/tools/mq_editor/mq_extract.js b/tools/mq_editor/mq_extract.js index 62b275c..c7ead4d 100644 --- a/tools/mq_editor/mq_extract.js +++ b/tools/mq_editor/mq_extract.js @@ -97,7 +97,12 @@ function detectPeaks(frame, fftSize, sampleRate, thresholdDB) { function trackPartials(frames, sampleRate) { const partials = []; const activePartials = []; - const trackingThreshold = 50; // Hz + const candidatePartials = []; // Pre-birth candidates + const trackingThresholdRatio = 0.05; // 5% frequency tolerance + const minTrackingHz = 20; // Minimum 20 Hz + const birthPersistence = 3; // Require 3 consecutive frames to birth + const deathAge = 5; // Allow 5 frame gap before death + const minPartialLength = 10; // Minimum 10 frames for valid partial for (const frame of frames) { const matched = new Set(); @@ -105,6 +110,7 @@ function trackPartials(frames, sampleRate) { // Match peaks to existing partials for (const partial of activePartials) { const lastFreq = partial.freqs[partial.freqs.length - 1]; + const threshold = Math.max(lastFreq * trackingThresholdRatio, minTrackingHz); let bestPeak = null; let bestDist = Infinity; @@ -115,7 +121,7 @@ function trackPartials(frames, sampleRate) { const peak = frame.peaks[i]; const dist = Math.abs(peak.freq - lastFreq); - if (dist < trackingThreshold && dist < bestDist) { + if (dist < threshold && dist < bestDist) { bestPeak = peak; bestDist = dist; partial.matchIdx = i; @@ -135,12 +141,51 @@ function trackPartials(frames, sampleRate) { } } - // Birth new partials from unmatched peaks + // Update candidate partials (pre-birth) + for (let i = candidatePartials.length - 1; i >= 0; --i) { + const candidate = candidatePartials[i]; + const lastFreq = candidate.freqs[candidate.freqs.length - 1]; + const threshold = Math.max(lastFreq * trackingThresholdRatio, minTrackingHz); + + let bestPeak = null; + let bestDist = Infinity; + + for (let i = 0; i < frame.peaks.length; ++i) { + if (matched.has(i)) continue; + + const peak = frame.peaks[i]; + const dist = Math.abs(peak.freq - lastFreq); + + if (dist < threshold && dist < bestDist) { + bestPeak = peak; + bestDist = dist; + candidate.matchIdx = i; + } + } + + if (bestPeak) { + candidate.times.push(frame.time); + candidate.freqs.push(bestPeak.freq); + candidate.amps.push(bestPeak.amp); + matched.add(candidate.matchIdx); + + // Birth if persistent enough + if (candidate.times.length >= birthPersistence) { + activePartials.push(candidate); + candidatePartials.splice(i, 1); + } + } else { + // Candidate died, remove + candidatePartials.splice(i, 1); + } + } + + // Create new candidate partials from unmatched peaks for (let i = 0; i < frame.peaks.length; ++i) { if (matched.has(i)) continue; const peak = frame.peaks[i]; - activePartials.push({ + candidatePartials.push({ times: [frame.time], freqs: [peak.freq], amps: [peak.amp], @@ -151,9 +196,9 @@ function trackPartials(frames, sampleRate) { // Death old partials for (let i = activePartials.length - 1; i >= 0; --i) { - if (activePartials[i].age > 2) { + if (activePartials[i].age > deathAge) { // Move to finished if long enough - if (activePartials[i].times.length >= 4) { + if (activePartials[i].times.length >= minPartialLength) { partials.push(activePartials[i]); } activePartials.splice(i, 1); @@ -163,7 +208,7 @@ function trackPartials(frames, sampleRate) { // Finish remaining active partials for (const partial of activePartials) { - if (partial.times.length >= 4) { + if (partial.times.length >= minPartialLength) { partials.push(partial); } } |
