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}