code wiki / (root) / nx_pitch.nx

nx_pitch.nx source

↩ module page · 90 lines · 3600 B

1// nx_pitch.nx -- sovereign integer F0 (fundamental-frequency / pitch) 2// detector via the YIN cumulative-mean-normalized difference function 3// (CMNDF). Pure integer fixed-point, NO floats, NO syscalls, NO imports. 4// 5// This is the SHARED bottom rung of the Nishi audio arc -- one primitive 6// that three threads all stand on: 7// * voice clone -> a speaker's pitch range/contour is part of identity 8// * singing teacher -> "am I on the right note?" = sung F0 vs target F0 9// * voice recording -> pitch stability / monotone detection 10// (no-orphans "shared primitive" rule: build once, reuse everywhere.) 11// 12// Re-derived integer-only from de Cheveigne & Kawahara 2002 (YIN), 13// patent-clean. Not CREPE, not pYIN, not any proprietary tracker. 14// 15// API (all buffers caller-owned; the organ touches no OS state): 16// nx_pitch_f0(pcm,start,win,fs,min_lag,max_lag,out) -> F0_hz 17// pcm: *u8 i16-LE mono PCM 18// start: first sample index of the analysis window 19// win: window length in samples (>= 2*max_lag recommended) 20// fs: sample rate (Hz) 21// min_lag: smallest lag searched (= fs / F0max) 22// max_lag: largest lag searched (= fs / F0min) 23// out: *i64 [0]=confidence Q14 (16384 - cmndf_min, clamped >=0) 24// [1]=best lag (period, samples) 25// returns: F0 = fs / best_lag (Hz), or 0 if degenerate 26// nx_pitch_is_voiced(conf_q14) -> 1 voiced / 0 unvoiced 27// 28// license_tier: ORIGINAL 29// module: nishi-core.audio.pitch 30// capability: AUDIO_F0_PITCH 31 32const NX_PITCH_Q14: i64 = 16384 33// Voicing decision threshold on the confidence (Q14). Tuned against the 34// sovereign test vectors in nx_pitch_gate.nx: clean voiced frames land 35// ~conf 12000-15000, noise / unvoiced ~conf 0-6000. 9000 is the gap. 36const NX_PITCH_VOICED_CONF_MIN: i64 = 9000 37 38// load one i16-LE signed sample at sample index `idx` 39func _px_i16(pcm: *u8, idx: i64) -> i64 { 40 let lo: i64 = pcm[idx * 2] 41 let hi: i64 = pcm[idx * 2 + 1] 42 var v: i64 = lo | (hi << 8) 43 if v >= 0x8000 { v = v - 0x10000 } 44 return v 45} 46 47// nx_pitch_f0 -- YIN CMNDF over lags [min_lag, max_lag]; pick the global 48// minimum of the normalized difference function. Integer-exact. 49func nx_pitch_f0(pcm: *u8, start: i64, win: i64, fs: i64, 50 min_lag: i64, max_lag: i64, out: *i64) -> i64 { 51 var best_tau: i64 = min_lag 52 var best_cmndf: i64 = 0x7fffffffffffffff 53 var cum: i64 = 0 54 var tau: i64 = 1 55 while tau <= max_lag { 56 // difference function d(tau) = sum (x[j] - x[j+tau])^2 57 var d: i64 = 0 58 var j: i64 = 0 59 while j < win { 60 let a: i64 = _px_i16(pcm, start + j) 61 let b: i64 = _px_i16(pcm, start + j + tau) 62 let diff: i64 = a - b 63 d = d + diff * diff 64 j = j + 1 65 } 66 cum = cum + d 67 if tau >= min_lag { 68 // cmndf(tau) = d(tau) * tau / cum (real value ~1.0) 69 // Q14: cmndf_q14 = d * tau * 16384 / cum 70 var cm: i64 = NX_PITCH_Q14 // 1.0 fallback if cum==0 (silence) 71 if cum > 0 { cm = (d * tau * NX_PITCH_Q14) / cum } 72 if cm < best_cmndf { 73 best_cmndf = cm 74 best_tau = tau 75 } 76 } 77 tau = tau + 1 78 } 79 var conf: i64 = NX_PITCH_Q14 - best_cmndf 80 if conf < 0 { conf = 0 } 81 out[0] = conf 82 out[1] = best_tau 83 if best_tau <= 0 { return 0 } 84 return fs / best_tau 85} 86 87func nx_pitch_is_voiced(conf_q14: i64) -> i64 { 88 if conf_q14 >= NX_PITCH_VOICED_CONF_MIN { return 1 } 89 return 0 90}