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}