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