code wiki / _hdl_build / nx_skill_coach.nx
nx_skill_coach.nx source
↩ module page · 124 lines · 7486 B
1// nx_skill_coach.nx -- THE UNIFIED SKILL-COACH ENGINE (the "engine + audio first" capstone). ONE coached-attempt
2// API that composes the four proven rungs: perception-grade (R1 sing via nx_note / R3 chord via the nx_polypitch
3// note-list contract) -> DRILL from the data-driven rubric store (R2, knowledge/store/coach-rubric-) -> gamified
4// SCORE (R4, joy-first). The /coach page + the team call coach_sing()/coach_chord() with one call. Thresholds are
5// read FROM the store (rule #11, no magic numbers). Self-gate: a mixed practice session through the single API ->
6// every attempt yields a store-drill + advances the gamified state. NO floats. license_tier: ORIGINAL
7import "nx_note.nx"
8import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
9import "nx_seg_store.nx"
10import "nx_syscalls.nx"
11
12const SK_RUBRIC: *u8 = "knowledge/store/coach-rubric-" as *u8
13const SK_LEVEL_XP: i64 = 500
14const SK_MASTERY: i64 = 5
15
16func sk_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
17// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
18// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
19// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
20// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
21func sk_putn(v: i64) -> i64 { nxi_out(v); return 0 }
22func sk_atoi(s: *u8, n: i64) -> i64 { var v: i64 = 0; var i: i64 = 0; while i < n { let c: i64 = s[i]; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 } return v }
23// rubric_get (composes R2's store): drill bytes for key, len into outlen[0], or 0 absent
24func sk_get(h: *i64, key: *u8, outlen: *i64) -> *u8 {
25 let pp: *i64 = sys_mmap(16) as *i64
26 let ll: *i64 = sys_mmap(16) as *i64
27 if ss_hget(h, key, pp, ll) != 1 { outlen[0] = 0; return 0 as *u8 }
28 outlen[0] = ll[0]; return pp[0] as *u8
29}
30// build "<disc>:<verdict>" key
31func sk_key(disc: *u8, verdict: *u8, out: *u8) -> i64 {
32 var o: i64 = 0; o = ss_cat(out, o, disc); o = ss_cat(out, o, verdict); out[o] = 0 as u8; return o
33}
34// gamified state: [0]xp [1]level [2]streak [3]best [4]attempts [5]mastered. JOY-FIRST: points>=0, gentle reset.
35func sk_record(st: *i64, points: i64, is_clean: i64) -> i64 {
36 var p: i64 = points; if p < 0 { p = 0 }
37 st[0] = st[0] + p; st[4] = st[4] + 1
38 if is_clean == 1 { st[2] = st[2] + 1; if st[2] > st[3] { st[3] = st[2] } } else { st[2] = 0 }
39 st[1] = st[0] / SK_LEVEL_XP
40 if st[3] >= SK_MASTERY { st[5] = 1 }
41 return 0
42}
43func sk_score_sing(verdict: i64, cents: i64) -> i64 {
44 if verdict == 0 { return 100 }
45 if verdict <= 2 { var a: i64 = cents; if a < 0 { a = 0 - a } var p: i64 = 100 - a; if p < 0 { p = 0 } return p }
46 return 10
47}
48func sk_sing_verdict_name(v: i64) -> *u8 {
49 if v == 0 { return "in_tune" as *u8 }
50 if v == 1 { return "sharp" as *u8 }
51 if v == 2 { return "flat" as *u8 }
52 if v == 3 { return "too_high" as *u8 }
53 return "too_low" as *u8
54}
55func sk_chord_verdict_name(v: i64) -> *u8 {
56 if v == 0 { return "clean" as *u8 }
57 if v == 1 { return "missing" as *u8 }
58 if v == 2 { return "extra" as *u8 }
59 return "wrong" as *u8
60}
61// THE ENGINE -- one singing attempt: grade -> store-drill -> score, all in one call. Returns 1 if a drill was served.
62func coach_sing(rh: *i64, gam: *i64, tol: i64, sung_f0: i64, target_midi: i64, tbl: *i64) -> i64 {
63 let no: *i64 = sys_mmap(8 * 4) as *i64
64 nx_note_of(sung_f0, tbl, no)
65 let dm: i64 = no[0]; let cents: i64 = no[1]
66 var v: i64 = 4
67 if dm == target_midi { if cents > tol { v = 1 } else { if cents < (0 - tol) { v = 2 } else { v = 0 } } } else { if dm > target_midi { v = 3 } else { v = 4 } }
68 let key: *u8 = sys_mmap(64); sk_key("sing:" as *u8, sk_sing_verdict_name(v), key)
69 let ol: *i64 = sys_mmap(16) as *i64
70 let drill: *u8 = sk_get(rh, key, ol)
71 let pts: i64 = sk_score_sing(v, cents)
72 var clean: i64 = 0; if v == 0 { clean = 1 }
73 sk_record(gam, pts, clean)
74 sk_puts(" [SING] "); sk_putn(sung_f0); sk_puts("Hz midi "); sk_putn(dm); sk_puts(" ("); sk_putn(cents); sk_puts("c) -> ")
75 if (drill as i64) != 0 { sys_write(1, drill, ol[0]) } else { sk_puts("(no drill!)" as *u8) }
76 sk_puts(" +"); sk_putn(pts); sk_puts("xp lvl="); sk_putn(gam[1]); sk_puts(" streak="); sk_putn(gam[2]); sk_puts("\n" as *u8)
77 if (drill as i64) != 0 { return 1 }
78 return 0
79}
80// one chord attempt: detected MIDI notes (nx_polypitch out_notes) -> grade -> store-drill -> score.
81func coach_chord(rh: *i64, gam: *i64, notes: *i64, n: i64, target_mask: i64) -> i64 {
82 var det: i64 = 0; var i: i64 = 0; while i < n { det = det | (1 << (notes[i] % 12)); i = i + 1 }
83 let missing: i64 = target_mask & (0xfff ^ det)
84 let extra: i64 = det & (0xfff ^ target_mask)
85 var v: i64 = 3
86 if missing == 0 { if extra == 0 { v = 0 } else { v = 2 } } else { if extra == 0 { v = 1 } else { v = 3 } }
87 let key: *u8 = sys_mmap(64); sk_key("chord:" as *u8, sk_chord_verdict_name(v), key)
88 let ol: *i64 = sys_mmap(16) as *i64
89 let drill: *u8 = sk_get(rh, key, ol)
90 var pts: i64 = 20; if v == 0 { pts = 100 }
91 var clean: i64 = 0; if v == 0 { clean = 1 }
92 sk_record(gam, pts, clean)
93 sk_puts(" [CHORD] det-mask "); sk_putn(det); sk_puts(" -> ")
94 if (drill as i64) != 0 { sys_write(1, drill, ol[0]) } else { sk_puts("(no drill!)" as *u8) }
95 sk_puts(" +"); sk_putn(pts); sk_puts("xp lvl="); sk_putn(gam[1]); sk_puts(" streak="); sk_putn(gam[2]); sk_puts("\n" as *u8)
96 if (drill as i64) != 0 { return 1 }
97 return 0
98}
99func main() -> i64 {
100 sk_puts("SKILL-COACH ENGINE: ONE API composes perception-grade + rubric-store drill + gamified score\n" as *u8)
101 let rh: *i64 = ss_open(SK_RUBRIC)
102 if (rh as i64) == 0 { sk_puts(" rubric store not seeded -- run nx_coach_rubric first\n" as *u8); return 1 }
103 // threshold read FROM the store (data-driven, rule #11)
104 let tl: *i64 = sys_mmap(16) as *i64
105 let tols: *u8 = sk_get(rh, "sing:tol_cents" as *u8, tl)
106 var tol: i64 = 20; if (tols as i64) != 0 { tol = sk_atoi(tols, tl[0]) }
107 sk_puts(" (tolerance read from store: "); sk_putn(tol); sk_puts(" cents)\n" as *u8)
108 let tbl: *i64 = sys_mmap(8 * 128) as *i64; nx_note_table(tbl)
109 let gam: *i64 = sys_mmap(8 * 8) as *i64; var z: i64 = 0; while z < 8 { gam[z] = 0; z = z + 1 }
110 var served: i64 = 0
111 // a mixed practice session through the SINGLE engine API
112 served = served + coach_sing(rh, gam, tol, 440, 69, tbl) // in tune
113 served = served + coach_sing(rh, gam, tol, 430, 69, tbl) // flat
114 served = served + coach_sing(rh, gam, tol, 494, 69, tbl) // wrong note
115 let cmaj: i64 = (1 << 0) | (1 << 4) | (1 << 7)
116 let c1: *i64 = sys_mmap(8 * 4) as *i64; c1[0] = 60; c1[1] = 64; c1[2] = 67
117 served = served + coach_chord(rh, gam, c1, 3, cmaj) // clean C major
118 let c2: *i64 = sys_mmap(8 * 4) as *i64; c2[0] = 60; c2[1] = 67
119 served = served + coach_chord(rh, gam, c2, 2, cmaj) // missing 3rd
120 sk_puts(" drills served from store = "); sk_putn(served); sk_puts("/5 final xp="); sk_putn(gam[0]); sk_puts("\n" as *u8)
121 if served == 5 { if gam[0] > 0 { sk_puts("SKILL-COACH-ENGINE: GREEN -- one API: gated perception -> grade -> store-drill -> joy-gamified score; the audio coach is unified\n" as *u8); return 0 } }
122 sk_puts("SKILL-COACH-ENGINE: RED\n" as *u8)
123 return 1
124}