code wiki / (root) / nx_pitch_grade.nx

nx_pitch_grade.nx source

↩ module page · 43 lines · 1932 B

1// nx_pitch_grade.nx -- AUDIO arc, singing-tutor thread: grade how on-pitch a 2// sung/spoken note is against a target note. Composes the rung-0 F0 detector 3// (nx_pitch) -- "am I on the right note?" is exactly target-F0 vs measured-F0. 4// 5// cents error = 1200 * log2(f_meas / f_target). We avoid a runtime log by the 6// first-order form cents ~= (1200/ln2) * (f_meas - f_target) / f_target 7// = 1731 * (f_meas - f_target) / f_target 8// The SIGN is always exact (sharp vs flat), and the MAGNITUDE is accurate to a 9// few cents inside the tuning regime (|err| < ~100 cents) -- which is the regime 10// a tuner cares about. A later rung can swap in fx_log2 for wide-range exactness. 11// 12// nx_pitch_grade(pcm,start,win,fs,min_lag,max_lag,target_hz,out) -> cents 13// out[0] = verdict (PG_ON / PG_FLAT / PG_SHARP / PG_NOPITCH) 14// out[1] = measured F0 (Hz) 15// 16// license_tier: ORIGINAL 17// module: nishi-core.audio.pitch_grade 18// depends: nishi-core.audio.pitch 19// capability: AUDIO_PITCH_GRADE 20import "nx_pitch.nx" 21 22const PG_TOL_CENTS: i64 = 25 // within +-25 cents = on pitch 23const PG_CENTS_K: i64 = 1731 // round(1200 / ln2) 24const PG_ON: i64 = 0 25const PG_FLAT: i64 = 1 26const PG_SHARP: i64 = 2 27const PG_NOPITCH: i64 = 3 28 29func nx_pitch_grade(pcm: *u8, start: i64, win: i64, fs: i64, 30 min_lag: i64, max_lag: i64, target_hz: i64, out: *i64) -> i64 { 31 let f0: i64 = nx_pitch_f0(pcm, start, win, fs, min_lag, max_lag, out) 32 let conf: i64 = out[0] 33 let voiced: i64 = nx_pitch_is_voiced(conf) 34 out[1] = f0 35 if voiced == 0 { out[0] = PG_NOPITCH; return 0 } 36 if target_hz < 1 { out[0] = PG_NOPITCH; return 0 } 37 let cents: i64 = PG_CENTS_K * (f0 - target_hz) / target_hz 38 var verdict: i64 = PG_ON 39 if cents < (0 - PG_TOL_CENTS) { verdict = PG_FLAT } 40 if cents > PG_TOL_CENTS { verdict = PG_SHARP } 41 out[0] = verdict 42 return cents 43}