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}