File size: 4,155 Bytes
de6631a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// 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,
  }));
}