From a9a60dfd2df938ef1e3ecc0a06d3d50cc329ef30 Mon Sep 17 00:00:00 2001 From: skal Date: Wed, 18 Feb 2026 14:00:18 +0100 Subject: feat(mq_editor): Implement phase-coherent partial tracking Implements a more robust partial tracking algorithm by using phase coherence to validate and link spectral peaks across frames. This significantly improves tracking accuracy, especially for crossing or closely-spaced partials. Key changes: - : The STFT cache now computes and stores the phase for each frequency bin alongside the magnitude. - : The peak detection logic now interpolates the phase for sub-bin accuracy. - : The core tracking algorithm was replaced with a phase-aware model. It predicts the expected phase for a partial in the next frame and uses a cost function combining both frequency and phase error to find the best match. This implementation follows the design outlined in the new document . handoff(Gemini): Phase prediction implemented. Further tuning of the cost function weights may be beneficial. --- tools/mq_editor/PHASE_TRACKING_PLAN.md | 73 ++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tools/mq_editor/PHASE_TRACKING_PLAN.md (limited to 'tools/mq_editor/PHASE_TRACKING_PLAN.md') diff --git a/tools/mq_editor/PHASE_TRACKING_PLAN.md b/tools/mq_editor/PHASE_TRACKING_PLAN.md new file mode 100644 index 0000000..5111692 --- /dev/null +++ b/tools/mq_editor/PHASE_TRACKING_PLAN.md @@ -0,0 +1,73 @@ +# Implementation Plan: Phase-Coherent Partial Tracking + +This document outlines the plan to integrate phase prediction into the existing MQ tracking algorithm. The core idea is to use phase coherence as a primary factor for linking peaks across frames, making the tracking more robust, especially for crossing or closely-spaced partials. + +--- + +### **Stage 1: Cache Per-Bin Phase in `fft.js`** + +**Objective:** Augment the `STFTCache` to compute and store the phase angle for every frequency bin in every frame, making it available for the tracking algorithm. + +1. **Locate the FFT Processing Loop:** + * In `tools/mq_editor/fft.js`, within the `STFTCache` class (likely in the constructor or an initialization method), find the loop that iterates through each frame to compute the FFT. + * This is where `squaredAmplitude` is currently being calculated from the `real` and `imag` components. + +2. **Compute and Store Phase:** + * In the same loop, immediately after calculating the squared amplitude, calculate the phase for each bin. + * Create a new `Float32Array` for phase, let's call it `ph`. + * Inside the bin loop, compute: `ph[k] = Math.atan2(imag[k], real[k]);` + * Store this new phase array in the frame object, parallel to the existing `squaredAmplitude` array. + + **Resulting Change in `fft.js`:** + + ```javascript + // Inside the STFTCache frame processing loop... + + // Existing code: + const sq = new Float32Array(this.fftSize / 2); + for (let k = 0; k < this.fftSize / 2; k++) { + sq[k] = real[k] * real[k] + imag[k] * imag[k]; + } + + // New code to add: + const ph = new Float32Array(this.fftSize / 2); + for (let k = 0; k < this.fftSize / 2; k++) { + ph[k] = Math.atan2(imag[k], real[k]); + } + + // Update the stored frame object + this.frames.push({ + time: t, + squaredAmplitude: sq, + phase: ph // The newly cached phase data + }); + ``` + +--- + +### **Stage 2: Utilize Phase for Tracking in `mq_extract.js`** + +**Objective:** Modify the main forward/backward tracking algorithm to use phase coherence for identifying and linking peaks. + +1. **Extract Interpolated Peak Phase:** + * In `tools/mq_editor/mq_extract.js`, find the function responsible for peak detection within a single frame (e.g., `findPeaks`). + * This function currently takes a `squaredAmplitude` array. It must now also access the corresponding `phase` array from the cached frame data. + * When a peak is found at bin `k`, use the same parabolic interpolation logic that calculates the true frequency and amplitude to also calculate the **true phase**. This involves interpolating the phase values from bins `k-1`, `k`, and `k+1`. + * **Crucially, this interpolation must handle phase wrapping.** A helper function will be needed to correctly find the shortest angular distance between phase values. + +2. **Update Tracking Data Structures:** + * The data structures holding candidate and live partials must be updated to store the phase of each point in the trajectory, not just frequency and amplitude. + +3. **Implement Phase Prediction Logic:** + * In the main tracking loop that steps from frame `n` to `n+1`: + * For each active partial, calculate its `predictedPhase` for frame `n+1`. + * `phase_delta = (2 * Math.PI * last_freq * params.hopSize) / params.sampleRate;` + * `predictedPhase = last_phase + phase_delta;` + +4. **Refine the Candidate Matching Score:** + * Modify the logic that links a partial to peaks in the next frame. + * Instead of matching based on frequency proximity alone, calculate a `cost` based on both frequency and phase deviation: + * `freqError = Math.abs(peak.freq - partial.last_freq);` + * `phaseError = Math.abs(normalize_angle(peak.phase - predictedPhase));` // Difference on a circle + * `cost = (freq_weight * freqError) + (phase_weight * phaseError);` + * The peak with the lowest `cost` below a certain threshold is the correct continuation. The `phase_weight` should be high, as a low phase error is a strong indicator of a correct match. -- cgit v1.2.3