summaryrefslogtreecommitdiff
path: root/tools/mq_editor/PHASE_TRACKING_PLAN.md
blob: 5111692f087e4d1d19236b774acfd4b5fdc6ad66 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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.