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.