PageSourceSearch

https://app.quetschn.academy/assets/algorithms/pitch-tracker/pitch-tracker-node.js

js quetschn.academy collected 2026-10-07 13:21:32 UTC 7,127 bytes, 206 lines download raw bytes

1import { PitchDecision } from "./pitchDecision.js";
2export { PitchDecision } from "./pitchDecision.js";
3
4// WebAudio's render quantum size.
5const RENDER_QUANTUM_FRAMES = 128;
6
7/**
8 * Adds pitch-tracker-processor module to audio context.
9 * Has to be executed before instantiating @class PitchTrackerNode
10 * @param {AudioContext} context
11 */
12export async function addPitchTrackerModule(context) {
13    const processorFileURL = new URL(
14        "./pitch-tracker-processor.js",
15        import.meta.url
16    );
17    try {
18        await context.audioWorklet.addModule(processorFileURL);
19    } catch (e) {
20        throw new Error(
21            `Failed to load pitch-tracker worklet at url: ${processorFileURL}. Further info: ${e.message}`
22        );
23    }
24}
25
26/**
27 * AudioWorkletProcessor for pitch-tracker.
28 *
29 * @class PitchTrackerNode
30 * @extends AudioWorkletNode
31 */
32
33export class PitchTrackerNode extends AudioWorkletNode {
34    /**
35     * @constructor
36     * @param {AudioContext} context // audio context
37     * @param {Object} params // pitch-tracker parameters
38     * @param {Function} spectrogram_callback // callback function for chroma visualization
39     */
40
41    constructor(context, params, spectrogram_callback) {
42        // fft size must be large enough - lowest chroma bandwidth > 2 * fft bin width
43        let f_lowest_tone =
44            params.f0 * 2 ** ((params.min_max_midi[0] - 69) / 12);
45        let lowest_chroma_width =
46            f_lowest_tone * (2 ** (1 / 24) - 2 ** (-1 / 24));
47        let max_bin_with = lowest_chroma_width / 2;
48        let Nfft = 2 ** Math.ceil(Math.log2(context.sampleRate / max_bin_with));
49
50        // calculate upper frequency limit for chroma
51        let f_highest_tone =
52            params.f0 * 2 ** ((params.min_max_midi[1] - 69) / 12);
53        let freq_upper_lim = Math.min(
54            f_highest_tone * params.N_harmonics * 2 ** (1 / 24),
55            context.sampleRate / 2
56        );
57        // calculate semitone step bounds for chroma
58        let semi_steps = [
59            params.min_max_midi[0] - 69,
60            params.min_max_midi[1] -
61                69 +
62                Math.round(Math.log2(params.N_harmonics)),
63        ];
64
65        let hopSize =
66            Math.max(
67                Math.round(
68                    params.hop_time /
69                        1000 /
70                        (RENDER_QUANTUM_FRAMES / context.sampleRate)
71                ),
72                1
73            ) * RENDER_QUANTUM_FRAMES;
74
75        // call AudioWorkletNode constructor and pipe parameters to AudioWorkletProcessor
76        super(context, "pitch-tracker", {
77            processorOptions: {
78                Nfft: Nfft,
79                hopSize: hopSize,
80                chromaParams: {
81                    f0: params.f0,
82                    freq_upper_lim: freq_upper_lim,
83                    semi_steps: semi_steps,
84                },
85                decisionMakerParams: {
86                    N_harmonics: params.N_harmonics,
87                    first_pitch_level_threshold:
88                        params.first_pitch_level_threshold,
89                    second_pitch_level_threshold:
90                        params.second_pitch_level_threshold,
91                    harmonic_energy_ratio_threshold:
92                        params.harmonic_energy_ratio_threshold,
93                    coincident_pitch_level_threshold:
94                        params.coincident_pitch_level_threshold,
95                    harmonic_energy_threshold:
96                        params.harmonic_energy_ratio_threshold,
97                },
98            },
99        });
100
101        // Listen to messages sent from the audio processor.
102        this.port.onmessage = (event) => this.onmessage(event.data);
103
104        // store parameters
105        this.spectrogram_callback = spectrogram_callback;
106        this.min_max_midi = params.min_max_midi;
107
108        // init result
109        this.decision = PitchDecision.NOT_VALID;
110    }
111
112    async init() {
113        // load wasm code from file
114        const wasmFileURL = new URL("./pkg/chroma_bg.wasm", import.meta.url);
115        const response = await fetch(wasmFileURL);
116        const wasmBytes = await response.arrayBuffer();
117        // send wasm code to processor node on audio thread
118        this.port.postMessage({
119            type: "send-wasm-module",
120            wasmBytes,
121        });
122    }
123
124    setTargetTones(target_tones) {
125        // check values
126        if (target_tones.lenght > 2) {
127            target_tones = target_ones.slice(0, 2);
128            console.warn(
129                "Received more than 2 target tones, only first two considered!"
130            );
131        }
132        target_tones = target_tones.filter((midi) => midi > 0);
133
134        if (
135            !target_tones.every(
136                (midi) =>
137                    midi >= this.min_max_midi[0] && midi <= this.min_max_midi[1]
138            )
139        ) {
140            console.warn("Target tones out of range are ignored!");
141            target_tones = target_tones.filter(
142                (midi) =>
143                    midi >= this.min_max_midi[0] && midi <= this.min_max_midi[1]
144            );
145        }
146        target_tones.sort();
147
148        // console.log(target_tones);
149
150        // send target tones to processor
151        this.port.postMessage({
152            type: "set-target-tones",
153            target_tones: target_tones,
154        });
155    }
156
157    getDecision() {
158        // request decision from processor
159        this.port.postMessage({
160            type: "get-decision",
161        });
162    }
163
164    // Handle an uncaught exception thrown in the PitchProcessor.
165    onprocessorerror(err) {
166        console.log(
167            `An error from AudioWorkletProcessor.process() occurred: ${err}`
168        );
169    }
170
171    onmessage(event) {
172        // receiving messages from the audio processor on the  running on the AudioWorklet thread.
173        switch (event.type) {
174            case "wasm-module-loaded":
175                // After the Wasm module was successfully sent to the audio processor,
176                // trigger wasm pitch tracker instantiation.
177                this.port.postMessage({
178                    type: "init-pitch-tracker",
179                    sampleRate: this.context.sampleRate,
180                });
181                break;
182
183            case "console-log":
184                // helper to  receive log-messages from audio processor and print to console
185                console.log(`Received from Processor: ${event.msg}`);
186                break;
187
188            case "send-spectrum":
189                // receive chroma spectrum and update spectrogram visualization
190                // (only for debugging purposes)
191                if (this.spectrogram_callback === undefined) {
192                    break;
193                }
194                this.spectrogram_callback(event.spectrum);
195                break;
196
197            case "send-decision":
198                // receive decision from audio processor
199                this.decision = event.decision;
200                break;
201            default:
202                // handle unknown messages
203                console.log(`Received unknown Message: ${event.type}`);
204        }
205    }
206}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.