1import { aJ as loadGraphModel, aK as slice, aL as concat1d, aM as zeros, aN as tensor, aO as expandDims, aP as signal } from "./vendor-dVNaLJx_.js"; 2import "./vendor-audio-DH3w0BzW.js"; 3import "./vendor-react-Bay9Y7Lb.js"; 4import "./vendor-i18n-R-bMoYp_.js"; 5const OUTPUT_TO_TENSOR_NAME = { 6 contours: "Identity", 7 onsets: "Identity_2", 8 frames: "Identity_1" 9}; 10const NUM_CHANNELS = 1; 11const AUDIO_SAMPLE_RATE$1 = 22050; 12const FFT_HOP$1 = 256; 13const ANNOTATIONS_FPS$1 = Math.floor(AUDIO_SAMPLE_RATE$1 / FFT_HOP$1); 14const AUDIO_WINDOW_LENGTH_SECONDS = 2; 15const AUDIO_N_SAMPLES$1 = AUDIO_SAMPLE_RATE$1 * AUDIO_WINDOW_LENGTH_SECONDS - FFT_HOP$1; 16const N_OVERLAPPING_FRAMES = 30; 17const N_OVERLAP_OVER_2 = Math.floor(N_OVERLAPPING_FRAMES / 2); 18const OVERLAP_LENGTH_FRAMES = N_OVERLAPPING_FRAMES * FFT_HOP$1; 19const HOP_SIZE = AUDIO_N_SAMPLES$1 - OVERLAP_LENGTH_FRAMES; 20class BasicPitch { 21 constructor(modelOrModelPath) { 22 this.model = typeof modelOrModelPath === "string" ? loadGraphModel(modelOrModelPath) : modelOrModelPath; 23 } 24 adjustNoteStart(notes, offsetSeconds) { 25 return notes.map((note) => ({ 26 startTimeSeconds: note.startTimeSeconds + offsetSeconds, 27 durationSeconds: note.durationSeconds, 28 pitch_midi: note.pitchMidi, 29 amplitude: note.amplitude, 30 pitchBends: note.pitchBends 31 })); 32 } 33 async evaluateSingleFrame(reshapedInput, batchNumber) { 34 const model = await this.model; 35 const singleBatch = slice(reshapedInput, batchNumber, 1); 36 const results = model.execute(singleBatch, [ 37 OUTPUT_TO_TENSOR_NAME.frames, 38 OUTPUT_TO_TENSOR_NAME.onsets, 39 OUTPUT_TO_TENSOR_NAME.contours 40 ]); 41 return [results[0], results[1], results[2]]; 42 } 43 async prepareData(singleChannelAudioData) { 44 const wavSamples = concat1d([ 45 zeros([Math.floor(OVERLAP_LENGTH_FRAMES / 2)], "float32"), 46 tensor(singleChannelAudioData) 47 ]); 48 return [ 49 expandDims(signal.frame(wavSamples, AUDIO_N_SAMPLES$1, HOP_SIZE, true, 0), -1), 50 singleChannelAudioData.length 51 ]; 52 } 53 unwrapOutput(result) { 54 let rawOutput = result; 55 rawOutput = result.slice([0, N_OVERLAP_OVER_2, 0], [-1, result.shape[1] - 2 * N_OVERLAP_OVER_2, -1]); 56 const outputShape = rawOutput.shape; 57 return rawOutput.reshape([outputShape[0] * outputShape[1], outputShape[2]]); 58 } 59 async evaluateModel(resampledBuffer, onComplete, percentCallback) { 60 let singleChannelAudioData; 61 if (resampledBuffer instanceof Float32Array) { 62 singleChannelAudioData = resampledBuffer; 63 } else { 64 if (resampledBuffer.sampleRate !== AUDIO_SAMPLE_RATE$1) { 65 throw new Error(`Input audio buffer is not at correct sample rate! Is ${resampledBuffer.sampleRate}. Should be ${AUDIO_SAMPLE_RATE$1}`); 66 } 67 if (resampledBuffer.numberOfChannels !== NUM_CHANNELS) { 68 throw new Error(`Input audio buffer is not mono! Number of channels is ${resampledBuffer.numberOfChannels}. Should be ${NUM_CHANNELS}`); 69 } 70 singleChannelAudioData = resampledBuffer.getChannelData(0); 71 } 72 const [reshapedInput, audioOriginalLength] = await this.prepareData(singleChannelAudioData); 73 const nOutputFramesOriginal = Math.floor(audioOriginalLength * (ANNOTATIONS_FPS$1 / AUDIO_SAMPLE_RATE$1)); 74 let calculatedFrames = 0; 75 for (let i = 0; i < reshapedInput.shape[0]; ++i) { 76 percentCallback(i / reshapedInput.shape[0]); 77 const [resultingFrames, resultingOnsets, resultingContours] = await this.evaluateSingleFrame(reshapedInput, i); 78 let unwrappedResultingFrames = this.unwrapOutput(resultingFrames); 79 let unwrappedResultingOnsets = this.unwrapOutput(resultingOnsets); 80 let unwrappedResultingContours = this.unwrapOutput(resultingContours); 81 const calculatedFramesTmp = unwrappedResultingFrames.shape[0]; 82 if (calculatedFrames >= nOutputFramesOriginal) { 83 continue; 84 } 85 if (calculatedFramesTmp + calculatedFrames >= nOutputFramesOriginal) { 86 const framesToOutput = nOutputFramesOriginal - calculatedFrames; 87 unwrappedResultingFrames = unwrappedResultingFrames.slice([0, 0], [framesToOutput, -1]); 88 unwrappedResultingOnsets = unwrappedResultingOnsets.slice([0, 0], [framesToOutput, -1]); 89 unwrappedResultingContours = unwrappedResultingContours.slice([0, 0], [framesToOutput, -1]); 90 } 91 calculatedFrames += calculatedFramesTmp; 92 onComplete(await unwrappedResultingFrames.array(), await unwrappedResultingOnsets.array(), await unwrappedResultingContours.array()); 93 } 94 percentCallback(1); 95 } 96} 97const MIDI_OFFSET = 21; 98const AUDIO_SAMPLE_RATE = 22050; 99const AUDIO_WINDOW_LENGTH = 2; 100const FFT_HOP = 256; 101const ANNOTATIONS_FPS = Math.floor(AUDIO_SAMPLE_RATE / FFT_HOP); 102const ANNOT_N_FRAMES = ANNOTATIONS_FPS * AUDIO_WINDOW_LENGTH; 103const AUDIO_N_SAMPLES = AUDIO_SAMPLE_RATE * AUDIO_WINDOW_LENGTH - FFT_HOP; 104const WINDOW_OFFSET = FFT_HOP / AUDIO_SAMPLE_RATE * (ANNOT_N_FRAMES - AUDIO_N_SAMPLES / FFT_HOP) + 18e-4; 105const MAX_FREQ_IDX = 87; 106const CONTOURS_BINS_PER_SEMITONE = 3; 107const ANNOTATIONS_BASE_FREQUENCY = 27.5; 108const ANNOTATIONS_N_SEMITONES = 88;
109const N_FREQ_BINS_CONTOURS = ANNOTATIONS_N_SEMITONES * CONTOURS_BINS_PER_SEMITONE; 110const hzToMidi = (hz) => 12 * (Math.log2(hz) - Math.log2(440)) + 69; 111const midiToHz = (midi) => 440 * 2 ** ((midi - 69) / 12); 112const modelFrameToTime = (frame) => frame * FFT_HOP / AUDIO_SAMPLE_RATE - WINDOW_OFFSET * Math.floor(frame / ANNOT_N_FRAMES); 113function argMax(arr) { 114 return arr.length === 0 ? null : arr.reduce((maxIndex, currentValue, index) => arr[maxIndex] > currentValue ? maxIndex : index, -1); 115} 116const argMaxAxis1 = (arr) => arr.map((row) => argMax(row)); 117function whereGreaterThanAxis1(arr2d, threshold) { 118 const outputX = []; 119 const outputY = []; 120 for (let i = 0; i < arr2d.length; i++) { 121 for (let j = 0; j < arr2d[i].length; j++) { 122 if (arr2d[i][j] > threshold) { 123 outputX.push(i); 124 outputY.push(j); 125 } 126 } 127 } 128 return [outputX, outputY]; 129} 130function meanStdDev(array) { 131 const [sum, sumSquared, count] = array.reduce((prev, row) => { 132 const [rowSum, rowSumsSquared, rowCount] = row.reduce((p, value) => [p[0] + value, p[1] + value * value, p[2] + 1], [0, 0, 0]); 133 return [prev[0] + rowSum, prev[1] + rowSumsSquared, prev[2] + rowCount]; 134 }, [0, 0, 0]); 135 const mean = sum / count; 136 const std = Math.sqrt(1 / (count - 1) * (sumSquared - sum * sum / count)); 137 return [mean, std]; 138} 139function globalMax(array) { 140 return array.reduce((prev, row) => Math.max(prev, ...row), 0); 141} 142function min3dForAxis0(array) { 143 const minArray = array[0].map((v) => v.slice()); 144 for (let x = 1; x < array.length; ++x) { 145 for (let y = 0; y < array[0].length; ++y) { 146 for (let z = 0; z < array[0][0].length; ++z) { 147 minArray[y][z] = Math.min(minArray[y][z], array[x][y][z]); 148 } 149 } 150 } 151 return minArray; 152} 153function argRelMax(array, order = 1) { 154 const result = []; 155 for (let col = 0; col < array[0].length; ++col) { 156 for (let row = 0; row < array.length; ++row) { 157 let isRelMax = true; 158 for (let comparisonRow = Math.max(0, row - order); isRelMax && comparisonRow <= Math.min(array.length - 1, row + order); ++comparisonRow) { 159 if (comparisonRow !== row) { 160 isRelMax = isRelMax && array[row][col] > array[comparisonRow][col]; 161 } 162 } 163 if (isRelMax) {
164 result.push([row, col]); 165 } 166 } 167 } 168 return result; 169} 170function max3dForAxis0(array) { 171 const maxArray = array[0].map((v) => v.slice()); 172 for (let x = 1; x < array.length; ++x) { 173 for (let y = 0; y < array[0].length; ++y) { 174 for (let z = 0; z < array[0][0].length; ++z) { 175 maxArray[y][z] = Math.max(maxArray[y][z], array[x][y][z]); 176 } 177 } 178 } 179 return maxArray; 180} 181function isNotNull(t) { 182 return t !== null; 183} 184function constrainFrequency(onsets, frames, maxFreq, minFreq) { 185 if (maxFreq) { 186 const maxFreqIdx = hzToMidi(maxFreq) - MIDI_OFFSET; 187 for (let i = 0; i < onsets.length; i++) { 188 onsets[i].fill(0, maxFreqIdx); 189 } 190 for (let i = 0; i < frames.length; i++) { 191 frames[i].fill(0, maxFreqIdx); 192 } 193 } 194 if (minFreq) { 195 const minFreqIdx = hzToMidi(minFreq) - MIDI_OFFSET; 196 for (let i = 0; i < onsets.length; i++) { 197 onsets[i].fill(0, 0, minFreqIdx); 198 } 199 for (let i = 0; i < frames.length; i++) { 200 frames[i].fill(0, 0, minFreqIdx); 201 } 202 } 203} 204function getInferredOnsets(onsets, frames, nDiff = 2) { 205 const diffs = Array.from(Array(nDiff).keys()).map((n) => n + 1).map((n) => { 206 const framesAppended = Array(n).fill(Array(frames[0].length).fill(0)).concat(frames); 207 const nPlus = framesAppended.slice(n); 208 const minusN = framesAppended.slice(0, -n); 209 if (nPlus.length !== minusN.length) { 210 throw new Error(`nPlus length !== minusN length: ${nPlus.length} !== ${minusN.length}`); 211 }
212 return nPlus.map((row, r) => row.map((v, c) => v - minusN[r][c])); 213 }); 214 let frameDiff = min3dForAxis0(diffs); 215 frameDiff = frameDiff.map((row) => row.map((v) => Math.max(v, 0))); 216 frameDiff = frameDiff.map((row, r) => r < nDiff ? row.fill(0) : row); 217 const onsetMax = globalMax(onsets); 218 const frameDiffMax = globalMax(frameDiff); 219 frameDiff = frameDiff.map((row) => row.map((v) => onsetMax * v / frameDiffMax)); 220 return max3dForAxis0([onsets, frameDiff]); 221} 222function outputToNotesPoly(frames, onsets, onsetThresh = 0.5, frameThresh = 0.3, minNoteLen = 5, inferOnsets = true, maxFreq = null, minFreq = null, melodiaTrick = true, energyTolerance = 11) { 223 let inferredFrameThresh = frameThresh; 224 if (inferredFrameThresh === null) { 225 const [mean, std] = meanStdDev(frames); 226 inferredFrameThresh = mean + std; 227 } 228 const nFrames = frames.length; 229 constrainFrequency(onsets, frames, maxFreq, minFreq); 230 let inferredOnsets = onsets; 231 if (inferOnsets) { 232 inferredOnsets = getInferredOnsets(onsets, frames); 233 } 234 const peakThresholdMatrix = inferredOnsets.map((o) => o.map(() => 0)); 235 argRelMax(inferredOnsets).forEach(([row, col]) => { 236 peakThresholdMatrix[row][col] = inferredOnsets[row][col]; 237 }); 238 const [noteStarts, freqIdxs] = whereGreaterThanAxis1(peakThresholdMatrix, onsetThresh); 239 noteStarts.reverse(); 240 freqIdxs.reverse(); 241 const remainingEnergy = frames.map((frame) => frame.slice()); 242 const noteEvents = noteStarts.map((noteStartIdx, idx) => { 243 const freqIdx = freqIdxs[idx]; 244 if (noteStartIdx >= nFrames - 1) { 245 return null; 246 } 247 let i = noteStartIdx + 1; 248 let k = 0; 249 while (i < nFrames - 1 && k < energyTolerance) { 250 if (remainingEnergy[i][freqIdx] < inferredFrameThresh) { 251 k += 1; 252 } else { 253 k = 0; 254 } 255 i += 1; 256 } 257 i -= k; 258 if (i - noteStartIdx <= minNoteLen) { 259 return null; 260 } 261 for (let j = noteStartIdx; j < i; ++j) { 262 remainingEnergy[j][freqIdx] = 0; 263 if (freqIdx < MAX_FREQ_IDX) { 264 remainingEnergy[j][freqIdx + 1] = 0; 265 } 266 if (freqIdx > 0) { 267 remainingEnergy[j][freqIdx - 1] = 0; 268 } 269 } 270 const amplitude = frames.slice(noteStartIdx, i).reduce((prev, row) => prev + row[freqIdx], 0) / (i - noteStartIdx); 271 return { 272 startFrame: noteStartIdx, 273 durationFrames: i - noteStartIdx, 274 pitchMidi: freqIdx + MIDI_OFFSET, 275 amplitude 276 }; 277 }).filter(isNotNull); 278 if (melodiaTrick === true) { 279 while (globalMax(remainingEnergy) > inferredFrameThresh) { 280 const [iMid, freqIdx] = remainingEnergy.reduce((prevCoord, currRow, rowIdx) => { 281 const colMaxIdx = argMax(currRow); 282 return currRow[colMaxIdx] > remainingEnergy[prevCoord[0]][prevCoord[1]] ? [rowIdx, colMaxIdx] : prevCoord; 283 }, [0, 0]); 284 remainingEnergy[iMid][freqIdx] = 0; 285 let i = iMid + 1; 286 let k = 0; 287 while (i < nFrames - 1 && k < energyTolerance) { 288 if (remainingEnergy[i][freqIdx] < inferredFrameThresh) { 289 k += 1; 290 } else { 291 k = 0; 292 } 293 remainingEnergy[i][freqIdx] = 0; 294 if (freqIdx < MAX_FREQ_IDX) { 295 remainingEnergy[i][freqIdx + 1] = 0; 296 } 297 if (freqIdx > 0) { 298 remainingEnergy[i][freqIdx - 1] = 0; 299 } 300 i += 1; 301 } 302 const iEnd = i - 1 - k; 303 i = iMid - 1; 304 k = 0; 305 while (i > 0 && k < energyTolerance) { 306 if (remainingEnergy[i][freqIdx] < inferredFrameThresh) { 307 k += 1; 308 } else { 309 k = 0; 310 } 311 remainingEnergy[i][freqIdx] = 0; 312 if (freqIdx < MAX_FREQ_IDX) { 313 remainingEnergy[i][freqIdx + 1] = 0; 314 } 315 if (freqIdx > 0) { 316 remainingEnergy[i][freqIdx - 1] = 0; 317 } 318 i -= 1; 319 } 320 const iStart = i + 1 + k; 321 if (iStart < 0) { 322 throw new Error(`iStart is not positive! value: ${iStart}`); 323 } 324 if (iEnd >= nFrames) { 325 throw new Error(`iEnd is past end of times. (iEnd, times.length): (${iEnd}, ${nFrames})`); 326 } 327 const amplitude = frames.slice(iStart, iEnd).reduce((sum, row) => sum + row[freqIdx], 0) / (iEnd - iStart); 328 if (iEnd - iStart <= minNoteLen) { 329 continue; 330 } 331 noteEvents.push({ 332 startFrame: iStart, 333 durationFrames: iEnd - iStart, 334 pitchMidi: freqIdx + MIDI_OFFSET, 335 amplitude 336 }); 337 } 338 } 339 return noteEvents; 340} 341const gaussian = (M, std) => Array.from(Array(M).keys()).map((n) => Math.exp(-1 * (n - (M - 1) / 2) ** 2 / (2 * std ** 2))); 342const midiPitchToContourBin = (pitchMidi) =>
342 12 * CONTOURS_BINS_PER_SEMITONE * Math.log2(midiToHz(pitchMidi) / ANNOTATIONS_BASE_FREQUENCY); 343function addPitchBendsToNoteEvents(contours, notes, nBinsTolerance = 25) { 344 const windowLength = nBinsTolerance * 2 + 1; 345 const freqGaussian = gaussian(windowLength, 5); 346 return notes.map((note) => { 347 const freqIdx = Math.floor(Math.round(midiPitchToContourBin(note.pitchMidi))); 348 const freqStartIdx = Math.max(freqIdx - nBinsTolerance, 0); 349 const freqEndIdx = Math.min(N_FREQ_BINS_CONTOURS, freqIdx + nBinsTolerance + 1); 350 const freqGuassianSubMatrix = freqGaussian.slice(Math.max(0, nBinsTolerance - freqIdx), windowLength - Math.max(0, freqIdx - (N_FREQ_BINS_CONTOURS - nBinsTolerance - 1))); 351 const pitchBendSubmatrix = contours.slice(note.startFrame, note.startFrame + note.durationFrames).map((d) => d.slice(freqStartIdx, freqEndIdx).map((v, col) => v * freqGuassianSubMatrix[col])); 352 const pbShift = nBinsTolerance - Math.max(0, nBinsTolerance - freqIdx); 353 const bends = argMaxAxis1(pitchBendSubmatrix).map((v) => v - pbShift); 354 return Object.assign(Object.assign({}, note), { pitchBends: bends }); 355 }); 356} 357const noteFramesToTime = (notes) => notes.map((note) => { 358 return { 359 pitchMidi: note.pitchMidi, 360 amplitude: note.amplitude, 361 pitchBends: note.pitchBends, 362 startTimeSeconds: modelFrameToTime(note.startFrame), 363 durationSeconds: modelFrameToTime(note.startFrame + note.durationFrames) - modelFrameToTime(note.startFrame) 364 }; 365}); 366export { 367 BasicPitch, 368 addPitchBendsToNoteEvents, 369 noteFramesToTime, 370 outputToNotesPoly 371};
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.