code wiki / _hdl_build / nx_audio_render.nx
nx_audio_render.nx source
↩ module page · 102 lines · 8706 B
1// nx_audio_render.nx -- SOVEREIGN ZERO-JS audiobook player PAGE (rung 1 of SC33-37). Operator's correction: the
2// player features are NISHI CODE, not browser JS -- the player BRAIN is a server-side Nishi audio daemon
3// (nx_audio_serve, next rung) + our own DSP pre-rendering; the browser is a dumb NATIVE <audio> client. So
4// "browser runs ZERO code" holds (same doctrine as the zero-JS text reader). This emits the static player HTML from
5// reader/<slug>/audio.json: native <audio controls> (SC33 basic playback + the browsers' native speed menu) +
6// chapters + the daemon-API affordances (speed-variant / sleep-timer / bookmark links+forms that target the Nishi
7// daemon -- the "APIs" that do the work server-side). FAIL-CLOSED: scans its OWN bytes for <script and REFUSES to
8// emit any (sovereign zero-JS, like nx_reader_render). Usage: nx_audio_render <slug>. expect_exit: 0 ORIGINAL
9import "nx_syscalls.nx"
10import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
11const K_MAGIC_1024: i64 = 1024
12const K_MAGIC_65536: i64 = 65536
13
14func ar_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
15func ar_p(s: *u8) -> i64 { sys_write(1, s, ar_slen(s)); return 0 }
16// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
17// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
18// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
19// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
20func ar_pn(v: i64) -> i64 { nxi_out(v); return 0 }
21func ensure_dir(path: *u8) -> i64 { __syscall(258, 0-100, path, 0x1ed, 0, 0, 0); return 0 }
22func cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){dst[off+i]=s[i];i=i+1} return off+i }
23func aw(o: *u8, p: *i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){ o[p[0]]=s[i]; p[0]=p[0]+1; i=i+1 } return 0 }
24// HTML-escape a string into the buffer (so a malicious title can't inject markup)
25func awh(o: *u8, p: *i64, s: *u8) -> i64 {
26 var i: i64=0
27 while s[i]!=(0 as u8) { let c: i64 = s[i] as i64
28 if c==0x3c { aw(o,p,"<" as *u8) } else { if c==0x3e { aw(o,p,">" as *u8) } else { if c==0x26 { aw(o,p,"&" as *u8) } else { if c==0x22 { aw(o,p,""" as *u8) } else { o[p[0]]=s[i]; p[0]=p[0]+1 } } } }
29 i=i+1 }
30 return 0
31}
32func jfrom(hay: *u8, hl: i64, start: i64, needle: *u8) -> i64 {
33 let nl: i64 = ar_slen(needle); if nl==0 { return 0-1 }
34 var i: i64 = start; if i<0 { i=0 }
35 while i + nl <= hl { var k: i64=0; var hit: i64=1; while k<nl { if hay[i+k]!=needle[k]{hit=0;k=nl}else{k=k+1} } if hit==1 {return i} i=i+1 }
36 return 0-1
37}
38// extract the JSON string value after key "<key>":" into out (NUL-term). ret len or -1.
39func jval(hay: *u8, hl: i64, key: *u8, out: *u8, cap: i64) -> i64 {
40 let at: i64 = jfrom(hay, hl, 0, key)
41 if at < 0 { out[0]=0 as u8; return 0-1 }
42 var i: i64 = at + ar_slen(key)
43 var o: i64 = 0
44 while i < hl { let c: i64 = hay[i] as i64; if c==0x22 { i=hl } else { if c==0x5c { if i+1<hl { if o<cap-1 { out[o]=hay[i+1]; o=o+1 } i=i+2 } else { i=i+1 } } else { if o<cap-1 { out[o]=hay[i] as u8; o=o+1 } i=i+1 } } }
45 out[o]=0 as u8; return o
46}
47func contains(hay: *u8, hl: i64, needle: *u8) -> i64 {
48 let nl: i64 = ar_slen(needle); if nl==0 { return 0 }
49 var i: i64=0
50 while i+nl<=hl { var k: i64=0; var hit: i64=1; while k<nl { if hay[i+k]!=needle[k]{hit=0;k=nl}else{k=k+1} } if hit==1 {return 1} i=i+1 }
51 return 0
52}
53
54func main(argc: i64, argv: *i64) -> i64 {
55 var slug: *u8 = "nishi_m4b_fixture" as *u8
56 if argc >= 2 { slug = argv[1] as *u8 }
57 let dir: *u8 = sys_mmap(512); var dl: i64 = cat(dir, 0, "knowledge/staging/media/reader/" as *u8); dl = cat(dir, dl, slug); dir[dl]=0 as u8
58 let ajp: *u8 = sys_mmap(K_MAGIC_1024); var al: i64 = cat(ajp, 0, dir as *u8); al = cat(ajp, al, "/audio.json" as *u8); ajp[al]=0 as u8
59 let lp: *i64 = sys_mmap(8) as *i64; lp[0]=0
60 let aj: *u8 = sys_read_file(ajp, lp)
61 if (aj as i64)==0 { ar_p("AUDIO-RENDER-FAIL no audio.json " as *u8); ar_p(ajp); ar_p("\n" as *u8); sys_exit(1); return 1 }
62 let an: i64 = lp[0]
63
64 let title: *u8 = sys_mmap(K_MAGIC_1024); jval(aj, an, "\"title\":\"" as *u8, title, K_MAGIC_1024)
65 let narr: *u8 = sys_mmap(K_MAGIC_1024); jval(aj, an, "\"narrator\":\"" as *u8, narr, K_MAGIC_1024)
66 let album: *u8 = sys_mmap(K_MAGIC_1024); jval(aj, an, "\"album\":\"" as *u8, album, K_MAGIC_1024)
67 let track: *u8 = sys_mmap(512); jval(aj, an, "\"file\":\"" as *u8, track, 512)
68 if track[0]==(0 as u8) { cat(track, 0, "track0.mp3" as *u8) }
69
70 let H: *u8 = sys_mmap(K_MAGIC_65536); let hp: *i64 = sys_mmap(8) as *i64; hp[0]=0
71 aw(H, hp, "<!doctype html><html lang=en><head><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>" as *u8); awh(H, hp, title)
72 aw(H, hp, "</title><style>:root{--bg:#15110d;--fg:#efe7da;--ac:#c89b5a}body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}main{max-width:46rem;margin:0 auto;padding:1.4rem}h1{font-size:1.5rem;margin:.2rem 0}.nar{color:var(--ac);margin:.2rem 0 1rem}audio{width:100%;margin:.6rem 0}section{border:1px solid #3a2f22;border-radius:.6rem;padding:.8rem 1rem;margin:1rem 0;background:#1d1812}h2{font-size:1rem;color:var(--ac);margin:.2rem 0 .6rem}a.sp,button{display:inline-block;background:#2a2218;color:var(--fg);border:1px solid #4a3c2a;border-radius:.4rem;padding:.35rem .7rem;margin:.15rem;text-decoration:none;cursor:pointer}form{display:inline}.badge{color:#9b8e78;font-size:.82rem;border-top:1px solid #2a2218;margin-top:1.4rem;padding-top:.8rem}</style></head><body><main>" as *u8)
73 aw(H, hp, "<h1>" as *u8); awh(H, hp, title); aw(H, hp, "</h1><p class=nar>" as *u8); awh(H, hp, narr)
74 if album[0]!=(0 as u8) { aw(H, hp, " · " as *u8); awh(H, hp, album) }
75 aw(H, hp, "</p>" as *u8)
76 // native player: SC33 basic playback (+ the browser's native speed menu), zero JS
77 aw(H, hp, "<audio controls preload=metadata src=\"" as *u8); awh(H, hp, track); aw(H, hp, "\"></audio>" as *u8)
78 // the daemon-API affordances (the "Nishi code APIs" -- served server-side by nx_audio_serve)
79 aw(H, hp, "<section><h2>Playback speed</h2><p>" as *u8)
80 aw(H, hp, "<a class=sp href=\"/audio/" as *u8); aw(H, hp, slug); aw(H, hp, "/speed/1.0\">1×</a>" as *u8)
81 aw(H, hp, "<a class=sp href=\"/audio/" as *u8); aw(H, hp, slug); aw(H, hp, "/speed/1.25\">1.25×</a>" as *u8)
82 aw(H, hp, "<a class=sp href=\"/audio/" as *u8); aw(H, hp, slug); aw(H, hp, "/speed/1.5\">1.5×</a>" as *u8)
83 aw(H, hp, "<a class=sp href=\"/audio/" as *u8); aw(H, hp, slug); aw(H, hp, "/speed/2.0\">2×</a>" as *u8)
84 aw(H, hp, "</p><p class=badge>Each speed is a variant our DSP renders tempo-shifted (no pitch change); the daemon serves the one you pick. No browser code.</p></section>" as *u8)
85 aw(H, hp, "<section><h2>Sleep timer · bookmark</h2>" as *u8)
86 aw(H, hp, "<form method=get action=\"/audio/" as *u8); aw(H, hp, slug); aw(H, hp, "/sleep\"><label>Stop after <input type=number name=min value=30 min=1 max=240 style=\"width:4rem\"> min</label> <button>Set</button></form> " as *u8)
87 aw(H, hp, "<form method=post action=\"/audio/" as *u8); aw(H, hp, slug); aw(H, hp, "/bookmark\"><button>Bookmark here</button></form>" as *u8)
88 aw(H, hp, "<p class=badge>The Nishi daemon ends the stream after N minutes (sleep) and stores your position (bookmark / cross-device resume) server-side — no client code.</p></section>" as *u8)
89 aw(H, hp, "<footer class=badge>Sovereign Nishi audiobook player · <b>0 JavaScript</b> · native <audio> only · the player logic runs in the Nishi audio daemon + our DSP, never in your browser.</footer>" as *u8)
90 aw(H, hp, "</main></body></html>" as *u8)
91 let hn: i64 = hp[0]
92
93 // FAIL-CLOSED sovereignty: refuse to emit any browser script
94 if contains(H, hn, "<script" as *u8)==1 { ar_p("AUDIO-RENDER-FAIL sovereignty: <script present\n" as *u8); sys_exit(2); return 2 }
95
96 let outp: *u8 = sys_mmap(K_MAGIC_1024); var ol: i64 = cat(outp, 0, dir as *u8); ol = cat(outp, ol, "/player.html" as *u8); outp[ol]=0 as u8
97 let fd: i64 = sys_openat_wr(outp, 0x1a4)
98 if fd < 0 { ar_p("AUDIO-RENDER-FAIL open out\n" as *u8); sys_exit(1); return 1 }
99 sys_write(fd, H, hn); sys_close(fd)
100 ar_p("AUDIO-RENDER slug=" as *u8); ar_p(slug); ar_p(" bytes=" as *u8); ar_pn(hn); ar_p(" 0-script -> " as *u8); ar_p(outp); ar_p("\n" as *u8)
101 ar_p("AUDIO-RENDER-OK\n" as *u8); sys_exit(0); return 0
102}