code wiki / (root) / nx_chord_grade.nx

nx_chord_grade.nx source

↩ module page · 79 lines · 2839 B

1// nx_chord_grade.nx -- INSTRUMENT-LEARN-GAME rung LG-4: CHORD-AWARE grading. 2// Fuses LG-3 polyphony (nx_polypitch) into the play-along grader so a song made 3// of CHORDS (not just single notes) is scored, with per-note partial credit -- 4// e.g. you nailed 2 of the 3 notes in a chord. A single note is just a chord of 5// size 1, so this generalizes LG-2. 6// 7// SONG layout (caller-owned *i64, CG_SLOT=9 words per slot): 8// [start, dur, n_target, m0, m1, m2, m3, m4, m5] (up to 6 target MIDI notes) 9// 10// nx_chord_grade(song, n_slots, player, fs, tbl, verdicts) -> score_permille 11// per slot: detect the played note SET (nx_polypitch), count how many target 12// notes are present; verdict: 13// HIT = every target note present AND nothing extra 14// PARTIAL = some target notes present (but not a clean full match) 15// WRONG = notes played, none of them a target 16// MISS = nothing played 17// score = total target notes correctly played * 1000 / total target notes. 18// tbl = 128-entry note table from nx_note_table. 19// 20// license_tier: ORIGINAL 21// module: nishi-core.audio.chord_grade 22// depends: nishi-core.audio.polypitch, nishi-core.audio.note 23// capability: AUDIO_LEARN_GAME_POLY 24import "nx_syscalls_x86_64.nx" 25import "nx_polypitch.nx" 26 27const CG_SLOT: i64 = 9 28const CG_HIT: i64 = 0 29const CG_PARTIAL: i64 = 1 30const CG_WRONG: i64 = 2 31const CG_MISS: i64 = 3 32 33const CG_SKIP: i64 = 256 // skip the chord's attack transient 34const CG_N: i64 = 1024 // analysis frame 35const CG_MLO: i64 = 40 // E2 36const CG_MHI: i64 = 76 // E5 37 38func _cg_contains(arr: *i64, n: i64, v: i64) -> i64 { 39 var i: i64 = 0 40 while i < n { 41 if arr[i] == v { return 1 } 42 i = i + 1 43 } 44 return 0 45} 46 47func nx_chord_grade(song: *i64, n_slots: i64, player: *u8, fs: i64, 48 tbl: *i64, verdicts: *i64) -> i64 { 49 let det: *i64 = sys_mmap(16 * 8) as *i64 50 var tot_t: i64 = 0 51 var tot_c: i64 = 0 52 var s: i64 = 0 53 while s < n_slots { 54 let base: i64 = s * CG_SLOT 55 let start: i64 = song[base + 0] 56 let nt: i64 = song[base + 2] 57 let dc: i64 = nx_polypitch(player, start + CG_SKIP, CG_N, fs, tbl, CG_MLO, CG_MHI, det, 16) 58 var matches: i64 = 0 59 var i: i64 = 0 60 while i < nt { 61 if _cg_contains(det, dc, song[base + 3 + i]) == 1 { matches = matches + 1 } 62 i = i + 1 63 } 64 var v: i64 = CG_MISS 65 if dc > 0 { 66 v = CG_WRONG 67 if matches > 0 { 68 v = CG_PARTIAL 69 if matches == nt { if dc == nt { v = CG_HIT } } 70 } 71 } 72 verdicts[s] = v 73 tot_t = tot_t + nt 74 tot_c = tot_c + matches 75 s = s + 1 76 } 77 if tot_t < 1 { return 0 } 78 return tot_c * 1000 / tot_t 79}