Spaces:
Running
Running
Download basicpitch/notes.js from SegerEnd/basic-pitch-wasm: direct link, hf CLI and curl.
- Browser
- Download file 4.16 kB
-
https://huggingface.co/spaces/SegerEnd/basic-pitch-wasm/resolve/main/basicpitch/notes.js
- Command line
-
hf download hf://spaces/SegerEnd/basic-pitch-wasm/basicpitch/notes.js
-
curl -L -o notes.js https://huggingface.co/spaces/SegerEnd/basic-pitch-wasm/resolve/main/basicpitch/notes.js
4.16 kB
| // notes.js - Basic Pitch's outputToNotesPoly. | |
| const NOTES = 88, LOWEST_MIDI = 21; | |
| const SAMPLE_RATE = 22050, HOP = 256, FPS = Math.floor(SAMPLE_RATE / HOP); | |
| // frame -> seconds, with Basic Pitch's correction per 2 s window | |
| const WINDOW_FRAMES = 2 * FPS, WINDOW_OFFSET = HOP / SAMPLE_RATE * (WINDOW_FRAMES - (2 * SAMPLE_RATE - HOP) / HOP) + 0.0018; | |
| const frameTime = (frame) => frame * HOP / SAMPLE_RATE - WINDOW_OFFSET * Math.floor(frame / WINDOW_FRAMES); | |
| const keyOf = (hz) => Math.trunc(12 * (Math.log2(hz) - Math.log2(440)) + 69 - LOWEST_MIDI); | |
| export function notesFromOutput({ frames, onsets, rows }, onsetThreshold, frameThreshold, minFrames, minFrequency, maxFrequency, boostOnsets, energyTolerance = 11) { | |
| const at = (row, key) => row * NOTES + key; | |
| frames = frames.slice(0, rows * NOTES); | |
| onsets = onsets.slice(0, rows * NOTES); | |
| // frequency limits (clamped: Basic Pitch wipes almost every key for a minimum under 27.5 Hz) | |
| if (maxFrequency) { | |
| const from = Math.min(Math.max(keyOf(maxFrequency), 0), NOTES); | |
| for (let r = 0; r < rows; r++) { frames.fill(0, at(r, from), at(r + 1, 0)); onsets.fill(0, at(r, from), at(r + 1, 0)); } | |
| } | |
| if (minFrequency) { | |
| const to = Math.min(Math.max(keyOf(minFrequency), 0), NOTES); | |
| for (let r = 0; r < rows; r++) { frames.fill(0, at(r, 0), at(r, to)); onsets.fill(0, at(r, 0), at(r, to)); } | |
| } | |
| let onset = onsets; | |
| if (boostOnsets) { | |
| let onsetMax = 0, riseMax = 0; | |
| const rise = new Float32Array(rows * NOTES); | |
| for (const v of onsets) onsetMax = Math.max(onsetMax, v); | |
| for (let r = 2; r < rows; r++) | |
| for (let k = 0; k < NOTES; k++) { | |
| const v = frames[at(r, k)], d = Math.max(Math.min(v - frames[at(r - 1, k)], v - frames[at(r - 2, k)]), 0); | |
| rise[at(r, k)] = d; | |
| riseMax = Math.max(riseMax, d); | |
| } | |
| onset = onsets.map((v, i) => Math.max(v, riseMax ? onsetMax * rise[i] / riseMax : 0)); | |
| } | |
| const starts = []; | |
| for (let r = 0; r < rows; r++) | |
| for (let k = 0; k < NOTES; k++) { | |
| const v = onset[at(r, k)]; | |
| if (v > onsetThreshold && (r === 0 || v > onset[at(r - 1, k)]) && (r === rows - 1 || v > onset[at(r + 1, k)])) starts.push([r, k]); | |
| } | |
| starts.reverse(); | |
| const left = frames.slice(), notes = []; // energy no note has taken yet | |
| const take = (r, k) => { left[at(r, k)] = 0; if (k < NOTES - 1) left[at(r, k + 1)] = 0; if (k > 0) left[at(r, k - 1)] = 0; }; | |
| const add = (start, end, k) => { | |
| let sum = 0; | |
| for (let r = start; r < end; r++) sum += frames[at(r, k)]; | |
| notes.push({ start, end, key: k, amplitude: sum / (end - start) }); | |
| }; | |
| for (const [start, k] of starts) { | |
| if (start >= rows - 1) continue; | |
| let r = start + 1, quiet = 0; | |
| while (r < rows - 1 && quiet < energyTolerance) { quiet = left[at(r, k)] < frameThreshold ? quiet + 1 : 0; r++; } | |
| const end = r - quiet; | |
| if (end - start <= minFrames) continue; | |
| for (let i = start; i < end; i++) take(i, k); | |
| add(start, end, k); | |
| } | |
| // melodia trick, loudest first (sorted once, same order as Basic Pitch's repeated scans) | |
| const peaks = []; | |
| for (let i = 0; i < rows * NOTES; i++) if (left[i] > frameThreshold) peaks.push(i); | |
| peaks.sort((a, b) => left[b] - left[a] || Math.floor(a / NOTES) - Math.floor(b / NOTES) || b - a); | |
| const level = Float32Array.from(peaks, (i) => left[i]); | |
| peaks.forEach((peak, p) => { | |
| if (left[peak] !== level[p]) return; // already taken | |
| const mid = Math.floor(peak / NOTES), k = peak % NOTES; | |
| left[peak] = 0; | |
| let r = mid + 1, quiet = 0; | |
| while (r < rows - 1 && quiet < energyTolerance) { quiet = left[at(r, k)] < frameThreshold ? quiet + 1 : 0; take(r, k); r++; } | |
| const end = r - 1 - quiet; | |
| r = mid - 1; quiet = 0; | |
| while (r > 0 && quiet < energyTolerance) { quiet = left[at(r, k)] < frameThreshold ? quiet + 1 : 0; take(r, k); r--; } | |
| const start = r + 1 + quiet; | |
| if (end - start > minFrames) add(start, end, k); | |
| }); | |
| return notes.map((n) => ({ | |
| pitchMidi: n.key + LOWEST_MIDI, | |
| startTimeSeconds: frameTime(n.start), | |
| durationSeconds: frameTime(n.end) - frameTime(n.start), | |
| amplitude: n.amplitude, | |
| })); | |
| } | |