code wiki / (root) / nx_polypitch.nx

nx_polypitch.nx source

↩ module page · 114 lines · 4105 B

1// nx_polypitch.nx -- INSTRUMENT-LEARN-GAME rung LG-3: POLYPHONIC pitch detection 2// (chords). Guitars and pianos play several notes at once; a single-F0 detector 3// (nx_pitch) can't see that. Instead of a big FFT (nx_fft caps at N=32 = 250 Hz 4// bins, useless for notes), we run a GOERTZEL filter bank: evaluate the signal's 5// energy DIRECTLY at each 12-TET note frequency. One cosine per note, O(N) per 6// note -- exact note resolution, no FFT-bin quantization. 7// 8// A note is "present" when its Goertzel power is a local peak across the note 9// grid AND above a fraction of the strongest note's power. The set of present 10// notes = the chord. 11// 12// Pure integer. Imports only the x86_64 syscall layer (mmap scratch). Also 13// exposes a sovereign full-range sin/cos (nx_pp_sinfull / nx_pp_cosfull) reusable 14// across the audio arc. 15// 16// license_tier: ORIGINAL 17// module: nishi-core.audio.polypitch 18// depends: nishi-core.audio.note (note table, caller-built) 19// capability: AUDIO_POLYPHONY 20import "nx_syscalls_x86_64.nx" 21 22const PP_PI: i64 = 205887 // pi in Q16.16 23const PP_HALFPI: i64 = 102944 // pi/2 24const PP_TWOPI: i64 = 411775 // 2*pi 25 26// cos(th), th in Q16.16 over [0, pi]; returns Q16.16 in [-65536, 65536]. 27func nx_pp_cosq16(th: i64) -> i64 { 28 var x: i64 = th 29 var sign: i64 = 1 30 if x > PP_HALFPI { x = PP_PI - x; sign = 0 - 1 } 31 let x2: i64 = (x * x) >> 16 32 let x4: i64 = (x2 * x2) >> 16 33 let x6: i64 = (x4 * x2) >> 16 34 var c: i64 = 65536 35 c = c - (x2 / 2) 36 c = c + (x4 / 24) 37 c = c - (x6 / 720) 38 return sign * c 39} 40// cos over a full turn [0, 2*pi) 41func nx_pp_cosfull(ph: i64) -> i64 { 42 var x: i64 = ph 43 if x >= PP_PI { x = PP_TWOPI - x } // cos(2pi - x) = cos(x) 44 return nx_pp_cosq16(x) 45} 46// sin over a full turn [0, 2*pi): sin(x) = cos(x - pi/2) 47func nx_pp_sinfull(ph: i64) -> i64 { 48 var c: i64 = ph - PP_HALFPI 49 if c < 0 { c = c + PP_TWOPI } 50 return nx_pp_cosfull(c) 51} 52 53func _pp_i16(pcm: *u8, i: i64) -> i64 { 54 let lo: i64 = pcm[i * 2] 55 let hi: i64 = pcm[i * 2 + 1] 56 var v: i64 = lo | (hi << 8) 57 if v >= 0x8000 { v = v - 0x10000 } 58 return v 59} 60 61// Goertzel power at a frequency whose coeff = 2*cos(2*pi*f/fs) in Q14. 62func _pp_goertzel(pcm: *u8, start: i64, N: i64, coeff_q14: i64) -> i64 { 63 var s1: i64 = 0 64 var s2: i64 = 0 65 var n: i64 = 0 66 while n < N { 67 let x: i64 = _pp_i16(pcm, start + n) 68 let s0: i64 = x + ((coeff_q14 * s1) >> 14) - s2 69 s2 = s1 70 s1 = s0 71 n = n + 1 72 } 73 var p: i64 = s1 * s1 + s2 * s2 - ((coeff_q14 * s1 * s2) >> 14) 74 if p < 0 { p = 0 } 75 return p 76} 77 78// nx_polypitch -- detect present notes in [midi_lo, midi_hi] over frame 79// pcm[start..start+N). tbl = 128-entry note table (centi-Hz) from nx_note_table. 80// Writes detected MIDI notes (ascending) into out_notes (up to max_notes); 81// returns the count. 82func nx_polypitch(pcm: *u8, start: i64, N: i64, fs: i64, tbl: *i64, 83 midi_lo: i64, midi_hi: i64, out_notes: *i64, max_notes: i64) -> i64 { 84 let pw: *i64 = sys_mmap(128 * 8) as *i64 85 var maxp: i64 = 0 86 var m: i64 = midi_lo 87 while m <= midi_hi { 88 // theta = 2*pi*f/fs at FULL centi-Hz precision (truncating tbl[m]/100 to 89 // integer Hz detunes the Goertzel ~1 Hz -> sharp resonance loss at large N). 90 let th: i64 = (PP_TWOPI * tbl[m]) / (fs * 100) // 2*pi*f/fs in Q16 (<= pi) 91 let coeff: i64 = nx_pp_cosq16(th) >> 1 // 2*cos in Q14 92 let p: i64 = _pp_goertzel(pcm, start, N, coeff) 93 pw[m] = p 94 if p > maxp { maxp = p } 95 m = m + 1 96 } 97 let thresh: i64 = maxp / 8 // peaks must exceed 12.5% of the strongest 98 var cnt: i64 = 0 99 m = midi_lo + 1 100 while m < midi_hi { 101 if cnt < max_notes { 102 if pw[m] > thresh { 103 if pw[m] > pw[m - 1] { 104 if pw[m] >= pw[m + 1] { 105 out_notes[cnt] = m 106 cnt = cnt + 1 107 } 108 } 109 } 110 } 111 m = m + 1 112 } 113 return cnt 114}