nx_song_grade.nx source
↩ module page · 62 lines · 2503 B
1// nx_song_grade.nx -- INSTRUMENT-LEARN-GAME (Rocksmith-class) rung LG-2: the
2// score-follower / grader. Given a SONG (a sequence of target notes with
3// timings) and the PLAYER's recorded audio, it grades every note the player
4// attempted -- HIT (right note), WRONG (a note, but the wrong one), or MISS
5// (nothing played) -- and returns an overall score. This is the core teaching
6// loop of a learn-game: play along, get graded, see what you missed.
7//
8// Works for ANY monophonic instrument (guitar single notes, bass, violin, voice,
9// wind) -- it stands on nx_pitch (F0) -> nx_note (F0 -> MIDI). Polyphony
10// (chords) is a later rung (LG-3, needs the nx_fft spectral path).
11//
12// SONG layout (caller-owned *i64, 3 words per note):
13// score[i*3+0] = target MIDI note
14// score[i*3+1] = note start sample
15// score[i*3+2] = note duration in samples
16//
17// nx_song_grade(score, n_notes, player_pcm, fs, tbl, verdicts) -> score_permille
18// verdicts[i] = SG_HIT / SG_WRONG / SG_MISS
19// returns hits * 1000 / n_notes (per-mille accuracy)
20// tbl = a 128-entry note table from nx_note_table.
21//
22// license_tier: ORIGINAL
23// module: nishi-core.audio.song_grade
24// depends: nishi-core.audio.note, nishi-core.audio.pitch
25// capability: AUDIO_LEARN_GAME
26import "nx_syscalls_x86_64.nx"
27import "nx_note.nx"
28import "nx_pitch.nx"
29
30const SG_HIT: i64 = 0
31const SG_WRONG: i64 = 1
32const SG_MISS: i64 = 2
33
34const SG_SKIP: i64 = 200 // skip the note's attack/onset transient
35const SG_WIN: i64 = 600 // analysis window
36const SG_MINLAG: i64 = 12 // ~667 Hz max
37const SG_MAXLAG: i64 = 200 // ~40 Hz min (covers bass to treble)
38
39func nx_song_grade(score: *i64, n_notes: i64, player: *u8, fs: i64,
40 tbl: *i64, verdicts: *i64) -> i64 {
41 let pout: *i64 = sys_mmap(16) as *i64
42 let nout: *i64 = sys_mmap(32) as *i64
43 var hits: i64 = 0
44 var i: i64 = 0
45 while i < n_notes {
46 let tgt: i64 = score[i * 3 + 0]
47 let start: i64 = score[i * 3 + 1]
48 let f0: i64 = nx_pitch_f0(player, start + SG_SKIP, SG_WIN, fs, SG_MINLAG, SG_MAXLAG, pout)
49 let voiced: i64 = nx_pitch_is_voiced(pout[0])
50 var v: i64 = SG_MISS
51 if voiced == 1 {
52 let midi: i64 = nx_note_of(f0, tbl, nout)
53 v = SG_WRONG
54 if midi == tgt { v = SG_HIT }
55 }
56 if v == SG_HIT { hits = hits + 1 }
57 verdicts[i] = v
58 i = i + 1
59 }
60 if n_notes < 1 { return 0 }
61 return hits * 1000 / n_notes
62}