code wiki / (root) / nx_song_grade.nx

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}