code wiki / _hdl_build / nx_audio_book.nx
nx_audio_book.nx source
↩ module page · 105 lines · 6510 B
1// nx_audio_book.nx -- SOVEREIGN audiobook/audio (MP3 + ID3v2) -> Nishi format. Operator goal: liberate purchased
2// audiobooks (the operator's ~153 audiobook files) into the owned .nmz, as the @kind=audio media kind. Reads the
3// ID3v2.3/2.4 tag (TIT2 title, TPE1 artist/narrator, TALB album) and CARRIES the audio bytes through unchanged
4// (the browser/player plays MP3 last-mile -- no codec to build, container unifies + codec specializes). Emits the
5// audio.json manifest (with "file":"X" track entries, the same convention book.json/cbx.json use) + track0.mp3, so
6// nx_nmz_pack packs it into .nmz @kind=audio. Rung 1: single MP3 -> one track; multi-file audiobook zips + M4B
7// (MP4 atoms) + per-chapter splitting = next rungs. LEGAL LINE: liberates DRM-FREE audio; no DRM circumvention.
8// Usage: nx_audio_book <mp3-path> <slug>. expect_exit: 0 license_tier: ORIGINAL
9import "nx_syscalls.nx"
10import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
11const K_MAGIC_1024: i64 = 1024
12
13func ab_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
14func ab_w(fd: i64, s: *u8) -> i64 { sys_write(fd, s, ab_slen(s)); return 0 }
15// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
16// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
17// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
18// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
19func ab_n(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 }
20func ab_p(s: *u8) -> i64 { ab_w(1, s); return 0 }
21func ab_pn(v: i64) -> i64 { ab_n(1, v); return 0 }
22func ensure_dir(path: *u8) -> i64 { __syscall(258, 0-100, path, 0x1ed, 0, 0, 0); return 0 }
23func er_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 }
24func be32(b: *u8, o: i64) -> i64 { return ((b[o] as i64)<<24)|((b[o+1] as i64)<<16)|((b[o+2] as i64)<<8)|(b[o+3] as i64) }
25func er_wjson(fd: i64, s: *u8) -> i64 {
26 var i: i64 = 0
27 while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c == 0x22 { sys_write(fd, "\\\"" as *u8, 2) } else { if c == 0x5c { sys_write(fd, "\\\\" as *u8, 2) } else { if c < 0x20 { sys_write(fd, " " as *u8, 1) } else { sys_write(fd, (s as i64 + i) as *u8, 1) } } } i = i + 1 }
28 return 0
29}
30func id4eq(z: *u8, p: i64, id: *u8) -> i64 { var k: i64=0; while k<4 { if z[p+k]!=id[k] { return 0 } k=k+1 } return 1 }
31// copy an ID3 text-frame value: data[off+1 .. off+flen) (skip the 1-byte encoding), NUL bytes stripped (handles
32// the interspersed NULs of UTF-16 text for ASCII content), into out (NUL-terminated).
33func frame_text(z: *u8, off: i64, flen: i64, out: *u8, cap: i64) -> i64 {
34 var o: i64 = 0; var i: i64 = off + 1; let e: i64 = off + flen
35 while i < e { let c: i64 = z[i] as i64; if c != 0 { if o < cap-1 { out[o]=z[i]; o=o+1 } } i=i+1 }
36 out[o]=0 as u8; return o
37}
38
39func do_audio(path: *u8, slug: *u8) -> i64 {
40 let lb: *i64 = sys_mmap(16) as *i64
41 let z: *u8 = sys_read_file(path, lb)
42 if (z as i64)==0 { ab_p("AUDIO-FAIL read " as *u8); ab_p(path); ab_p("\n" as *u8); return 0-1 }
43 let zl: i64 = lb[0]
44
45 let title: *u8 = sys_mmap(K_MAGIC_1024); title[0]=0 as u8
46 let narrator: *u8 = sys_mmap(K_MAGIC_1024); narrator[0]=0 as u8
47 let album: *u8 = sys_mmap(K_MAGIC_1024); album[0]=0 as u8
48
49 // ---- ID3v2 tag (if present) ----
50 if zl > 10 { if z[0]==(0x49 as u8) { if z[1]==(0x44 as u8) { if z[2]==(0x33 as u8) {
51 // header size is syncsafe (7 bits/byte)
52 let tagsize: i64 = ((z[6] as i64)<<21)|((z[7] as i64)<<14)|((z[8] as i64)<<7)|(z[9] as i64)
53 let endf: i64 = 10 + tagsize
54 var p: i64 = 10
55 while p + 10 <= endf {
56 if z[p]==(0 as u8) { p = endf } else { // padding -> stop
57 let flen: i64 = be32(z, p+4) // v2.3 plain-BE frame size
58 let dat: i64 = p + 10
59 if flen <= 0 { p = endf } else {
60 if id4eq(z, p, "TIT2" as *u8)==1 { frame_text(z, dat, flen, title, K_MAGIC_1024) }
61 if id4eq(z, p, "TPE1" as *u8)==1 { frame_text(z, dat, flen, narrator, K_MAGIC_1024) }
62 if id4eq(z, p, "TALB" as *u8)==1 { frame_text(z, dat, flen, album, K_MAGIC_1024) }
63 p = dat + flen
64 }
65 }
66 }
67 } } } }
68 if title[0]==(0 as u8) { var k: i64=0; while slug[k]!=(0 as u8){ title[k]=slug[k]; k=k+1 } title[k]=0 as u8 }
69
70 // ---- emit reader/<slug>/{track0.mp3, audio.json} ----
71 ensure_dir("knowledge/staging/media/reader" as *u8)
72 let dir: *u8 = sys_mmap(512)
73 var dl: i64 = er_cat(dir, 0, "knowledge/staging/media/reader/" as *u8); dl = er_cat(dir, dl, slug); dir[dl]=0 as u8
74 ensure_dir(dir)
75
76 let tp: *u8 = sys_mmap(K_MAGIC_1024)
77 var tpl: i64 = er_cat(tp, 0, dir as *u8); tpl = er_cat(tp, tpl, "/track0.mp3" as *u8); tp[tpl]=0 as u8
78 let tfd: i64 = sys_openat_wr(tp, 0x1a4)
79 if tfd >= 0 { sys_write(tfd, z, zl); sys_close(tfd) } // carry the audio bytes through unchanged
80
81 let jp: *u8 = sys_mmap(K_MAGIC_1024)
82 var jl: i64 = er_cat(jp, 0, dir as *u8); jl = er_cat(jp, jl, "/audio.json" as *u8); jp[jl]=0 as u8
83 let jfd: i64 = sys_openat_wr(jp, 0x1a4)
84 if jfd < 0 { ab_p("AUDIO-FAIL open-json\n" as *u8); return 0-1 }
85 ab_w(jfd, "{\"title\":\"" as *u8); er_wjson(jfd, title)
86 ab_w(jfd, "\",\"narrator\":\"" as *u8); er_wjson(jfd, narrator)
87 ab_w(jfd, "\",\"album\":\"" as *u8); er_wjson(jfd, album)
88 ab_w(jfd, "\",\"format\":\"audio\",\"slug\":\"" as *u8); er_wjson(jfd, slug)
89 ab_w(jfd, "\",\"tracks\":[{\"idx\":0,\"file\":\"track0.mp3\",\"bytes\":" as *u8); ab_n(jfd, zl)
90 ab_w(jfd, "}],\"ntracks\":1}" as *u8)
91 sys_close(jfd)
92
93 ab_p("AUDIO-BOOK title=\"" as *u8); ab_w(1, title); ab_p("\" narrator=\"" as *u8); ab_w(1, narrator)
94 ab_p("\" album=\"" as *u8); ab_w(1, album); ab_p("\" audio_bytes=" as *u8); ab_pn(zl); ab_p(" -> " as *u8); ab_p(dir); ab_p("\n" as *u8)
95 return 0
96}
97
98func main(argc: i64, argv: *i64) -> i64 {
99 var path: *u8 = "knowledge/fixtures/nishi_audio.mp3" as *u8
100 var slug: *u8 = "nishi_audio_fixture" as *u8
101 if argc >= 3 { path = argv[1] as *u8; slug = argv[2] as *u8 }
102 let r: i64 = do_audio(path, slug)
103 if r == 0 { ab_p("AUDIO-BOOK-OK\n" as *u8); sys_exit(0); return 0 }
104 ab_p("AUDIO-BOOK-FAIL\n" as *u8); sys_exit(1); return 1
105}