# 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.