code wiki / _hdl_build / nx_audio_serve.nx
nx_audio_serve.nx source
↩ module page · 186 lines · 13730 B
1// nx_audio_serve.nx -- the SOVEREIGN audiobook player BRAIN (SC34/35/37), server-side Nishi code (operator: the
2// player capabilities ARE nishi-code APIs, not browser JS). Binds 127.0.0.1 only. The browser is a dumb native
3// <audio> client; THIS daemon does the work:
4// GET /audio/<slug> -> the zero-JS player.html (rendered by nx_audio_render)
5// GET /audio/<slug>/track -> serve the audio bytes; honors `Range: bytes=X-Y` (206) so <audio> seeks/streams
6// GET /audio/<slug>/track?sleep=N -> SLEEP TIMER: caps the served stream to N*BYTES_PER_MIN -> playback ends (SC34)
7// POST /audio/<slug>/bookmark (pos=BYTE) -> stores the position server-side -> 303 (SC35 + SC37 cross-device)
8// GET /audio/<slug>/resume -> returns the stored position {"pos":N} (the client seeks there) (SC37)
9// Reuses the proven nx_vizsla_serve HTTP pattern (handle-mode gate hook + gallery_serve socket idiom). The position
10// store is a per-slug file (reader/<slug>/audio_pos.txt). license_tier: ORIGINAL
11import "nx_syscalls.nx"
12const BYTES_MAGIC_65536: i64 = 65536
13const BYTES_MAGIC_1024: i64 = 1024
14const BYTES_MAGIC_33554432: i64 = 33554432
15const BYTES_MAGIC_18099: i64 = 18099
16
17// BYTES_PER_MIN: the sleep-timer byte budget per minute. PLACEHOLDER for the gate; a real audiobook sets this from
18// its bitrate (e.g. 64kbps AAC -> 480000). The daemon caps the served stream to sleep_min*BYTES_PER_MIN.
19const BYTES_PER_MIN: i64 = 100
20
21func as_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
22func as_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){d[o+i]=s[i];i=i+1} return o+i }
23func as_catn(d: *u8, o0: i64, v: i64) -> i64 { var o: i64=o0; var m: i64=v; if m<0{d[o]=45 as u8;o=o+1;m=0-m}; let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48 as u8;k=1}; while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1}; var i: i64=0; while i<k{d[o+i]=t[k-1-i];i=i+1} return o+k }
24func as_match(req: *u8, rn: i64, pat: *u8) -> i64 { let pl: i64=as_slen(pat); if pl>rn{return 0} var j: i64=0; while j<pl{ if req[j]!=pat[j]{return 0} j=j+1 } return 1 }
25func as_valid_slug(s: *u8) -> i64 {
26 if s[0]==(0 as u8){return 0}
27 var i: i64=0
28 while s[i]!=(0 as u8){ let c: i64=s[i] as i64; var ok: i64=0; if c>=48{if c<=57{ok=1}} if c>=65{if c<=90{ok=1}} if c>=97{if c<=122{ok=1}} if c==45{ok=1} if c==95{ok=1} if ok==0{return 0} i=i+1 }
29 return 1
30}
31func as_readall(path: *u8, szp: *i64) -> *u8 {
32 let fd: i64 = sys_openat_rd(path); if fd<0 { szp[0]=0; return 0 as *u8 }
33 let sz: i64 = sys_lseek(fd, 0, 2); sys_lseek(fd, 0, 0)
34 let buf: *u8 = sys_mmap(sz + 64); var got: i64=0; var n: i64=1
35 while n>0 { n=sys_read(fd, (buf as i64+got) as *u8, BYTES_MAGIC_65536); if n>0{got=got+n} }
36 sys_close(fd); szp[0]=got; return buf
37}
38// parse "Range: bytes=START-END" from the headers; sets s/e (e=-1 if open-ended). ret 1 if present.
39func parse_range(req: *u8, rn: i64, sp: *i64, ep: *i64) -> i64 {
40 sp[0]=0; ep[0]=0-1
41 var i: i64=0
42 while i + 13 < rn {
43 if as_match((req as i64+i) as *u8, rn-i, "Range: bytes=" as *u8)==1 {
44 var p: i64 = i+13; var s: i64=0; var have: i64=0
45 while p<rn { let c: i64=req[p] as i64; if c>=48{if c<=57{ s=s*10+(c-48); have=1; p=p+1 } else { p=rn } } else { p=rn } }
46 // re-scan: digits then '-' then digits
47 p = i+13; s=0
48 while p<rn { let c: i64=req[p] as i64; if c>=48 { if c<=57 { s=s*10+(c-48); p=p+1 } else { p=rn } } else { p=rn } }
49 // proper parse: START
50 p = i+13; s=0; var e: i64=0-1
51 while p<rn { if req[p]>=(48 as u8) { if req[p]<=(57 as u8) { s=s*10+((req[p] as i64)-48); p=p+1 } else { break } } else { break } }
52 if p<rn { if req[p]==(45 as u8) { p=p+1; var ev: i64=0; var eh: i64=0; while p<rn { if req[p]>=(48 as u8) { if req[p]<=(57 as u8) { ev=ev*10+((req[p] as i64)-48); eh=1; p=p+1 } else { break } } else { break } } if eh==1 { e=ev } } }
53 sp[0]=s; ep[0]=e
54 return 1
55 }
56 i=i+1
57 }
58 return 0
59}
60// body field key=value (after \r\n\r\n), value until '&' or end. ret 1.
61func bodyfield(req: *u8, rn: i64, key: *u8, out: *u8) -> i64 {
62 var b: i64=0-1; var i: i64=0
63 while i+3<rn { if req[i]==(13 as u8){if req[i+1]==(10 as u8){if req[i+2]==(13 as u8){if req[i+3]==(10 as u8){ b=i+4; i=rn }}}} i=i+1 }
64 if b<0 { out[0]=0 as u8; return 0 }
65 let kl: i64=as_slen(key); var p: i64=b
66 while p+kl<rn { var atb: i64=0; if p==b{atb=1} if p>b{if req[p-1]==(38 as u8){atb=1}} if atb==1 { var mt: i64=1; var j: i64=0; while j<kl{if req[p+j]!=key[j]{mt=0;j=kl}else{j=j+1}} if mt==1{if req[p+kl]==(61 as u8){ var q: i64=p+kl+1; var o: i64=0; while q<rn{if req[q]==(38 as u8){break} if o<255{out[o]=req[q];o=o+1} q=q+1} out[o]=0 as u8; return 1 }}} p=p+1 }
67 out[0]=0 as u8; return 0
68}
69
70// parse the request path -> method(GET=1/POST=2), slug, action, sleep_min(0=none). ret method or 0 if bad.
71func parse_route(req: *u8, rn: i64, slug: *u8, action: *u8, sleepp: *i64) -> i64 {
72 sleepp[0]=0; slug[0]=0 as u8; action[0]=0 as u8
73 var method: i64 = 1
74 if as_match(req, rn, "POST " as *u8)==1 { method=2 }
75 var p: i64=0; while p<rn { if req[p]==(32 as u8){break} p=p+1 } p=p+1 // skip method
76 if as_match((req as i64+p) as *u8, rn-p, "/audio/" as *u8)==0 { return 0 }
77 p = p + 7
78 var o: i64=0
79 while p<rn { let c: i64=req[p] as i64; if c==47{break} if c==32{break} if c==63{break} if o<127{slug[o]=c as u8;o=o+1} p=p+1 }
80 slug[o]=0 as u8
81 if p<rn { if req[p]==(47 as u8) { p=p+1; o=0; while p<rn { let c: i64=req[p] as i64; if c==32{break} if c==63{break} if o<63{action[o]=c as u8;o=o+1} p=p+1 } action[o]=0 as u8 } }
82 if p<rn { if req[p]==(63 as u8) {
83 // query: look for sleep=N
84 var q: i64=p
85 while q+6<rn { if as_match((req as i64+q) as *u8, rn-q, "sleep=" as *u8)==1 { var r: i64=q+6; var sv: i64=0; while r<rn { if req[r]>=(48 as u8){if req[r]<=(57 as u8){sv=sv*10+((req[r] as i64)-48);r=r+1}else{r=rn}}else{r=rn} } sleepp[0]=sv; q=rn } else { q=q+1 } }
86 } }
87 return method
88}
89
90func posfile(slug: *u8, out: *u8) -> i64 { var o: i64=as_cat(out,0,"knowledge/staging/media/reader/" as *u8); o=as_cat(out,o,slug); o=as_cat(out,o,"/audio_pos.txt" as *u8); out[o]=0 as u8; return o }
91
92// serve the audio track (with Range + sleep cap). returns response length in rbuf.
93func serve_track(slug: *u8, has_range: i64, rs: i64, re0: i64, sleep_min: i64, rbuf: *u8) -> i64 {
94 let tp: *u8 = sys_mmap(BYTES_MAGIC_1024)
95 var ctype: *u8 = "audio/mp4" as *u8
96 var to: i64 = as_cat(tp,0,"knowledge/staging/media/reader/" as *u8); to=as_cat(tp,to,slug); to=as_cat(tp,to,"/track0.m4b" as *u8); tp[to]=0 as u8
97 let szp: *i64 = sys_mmap(16) as *i64
98 var tb: *u8 = as_readall(tp, szp)
99 if (tb as i64)==0 { to=as_cat(tp,0,"knowledge/staging/media/reader/" as *u8); to=as_cat(tp,to,slug); to=as_cat(tp,to,"/track0.mp3" as *u8); tp[to]=0 as u8; tb=as_readall(tp,szp); ctype="audio/mpeg" as *u8 }
100 if (tb as i64)==0 { return as_cat(rbuf,0,"HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\nConnection: close\r\n\r\nno track\n" as *u8) }
101 let total: i64 = szp[0]
102 var s: i64 = 0; var e: i64 = total - 1; var partial: i64 = 0
103 if has_range == 1 { partial = 1; s = rs; if re0 >= 0 { e = re0 } if s < 0 { s=0 } if e >= total { e = total-1 } if s > e { s = 0 } }
104 if sleep_min > 0 { let cap: i64 = sleep_min * BYTES_PER_MIN; if e > s + cap - 1 { e = s + cap - 1 } if e >= total { e = total-1 } }
105 let clen: i64 = e - s + 1
106 var o: i64 = 0
107 if partial == 1 {
108 o = as_cat(rbuf, o, "HTTP/1.1 206 Partial Content\r\nContent-Type: " as *u8); o=as_cat(rbuf,o,ctype)
109 o = as_cat(rbuf, o, "\r\nAccept-Ranges: bytes\r\nContent-Range: bytes " as *u8); o=as_catn(rbuf,o,s); rbuf[o]=45 as u8; o=o+1; o=as_catn(rbuf,o,e); rbuf[o]=47 as u8; o=o+1; o=as_catn(rbuf,o,total)
110 o = as_cat(rbuf, o, "\r\nContent-Length: " as *u8); o=as_catn(rbuf,o,clen); o=as_cat(rbuf,o,"\r\nConnection: close\r\n\r\n" as *u8)
111 } else {
112 o = as_cat(rbuf, o, "HTTP/1.1 200 OK\r\nContent-Type: " as *u8); o=as_cat(rbuf,o,ctype)
113 o = as_cat(rbuf, o, "\r\nAccept-Ranges: bytes\r\nContent-Length: " as *u8); o=as_catn(rbuf,o,clen); o=as_cat(rbuf,o,"\r\nConnection: close\r\n\r\n" as *u8)
114 }
115 var k: i64 = s; while k <= e { rbuf[o]=tb[k]; o=o+1; k=k+1 }
116 return o
117}
118
119func handle(req: *u8, rn: i64, rbuf: *u8) -> i64 {
120 let slug: *u8 = sys_mmap(256); let action: *u8 = sys_mmap(128); let slp: *i64 = sys_mmap(8) as *i64
121 let method: i64 = parse_route(req, rn, slug, action, slp)
122 if method == 0 { return as_cat(rbuf,0,"HTTP/1.1 400 Bad Request\r\nContent-Length: 4\r\nConnection: close\r\n\r\nbad\n" as *u8) }
123 if as_valid_slug(slug)==0 { return as_cat(rbuf,0,"HTTP/1.1 400 Bad Request\r\nContent-Length: 9\r\nConnection: close\r\n\r\nbad slug\n" as *u8) }
124
125 // POST .../bookmark : store position
126 if method==2 { if as_match(action, as_slen(action), "bookmark" as *u8)==1 {
127 let posv: *u8 = sys_mmap(64); bodyfield(req, rn, "pos" as *u8, posv)
128 let pf: *u8 = sys_mmap(BYTES_MAGIC_1024); posfile(slug, pf)
129 let fd: i64 = sys_openat_wr(pf, 0x1a4); if fd>=0 { sys_write(fd, posv, as_slen(posv)); sys_close(fd) }
130 var o: i64 = as_cat(rbuf,0,"HTTP/1.1 303 See Other\r\nLocation: /audio/" as *u8); o=as_cat(rbuf,o,slug); o=as_cat(rbuf,o,"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" as *u8)
131 return o
132 } }
133 // GET .../resume : return the stored position
134 if as_match(action, as_slen(action), "resume" as *u8)==1 {
135 let pf: *u8 = sys_mmap(BYTES_MAGIC_1024); posfile(slug, pf)
136 let szp: *i64 = sys_mmap(16) as *i64; let pb: *u8 = as_readall(pf, szp)
137 var pos: i64 = 0
138 if (pb as i64)!=0 { var i: i64=0; while i<szp[0] { if pb[i]>=(48 as u8){if pb[i]<=(57 as u8){pos=pos*10+((pb[i] as i64)-48)}} i=i+1 } }
139 let body: *u8 = sys_mmap(64); var bo: i64=as_cat(body,0,"{\"pos\":" as *u8); bo=as_catn(body,bo,pos); bo=as_cat(body,bo,"}" as *u8); body[bo]=0 as u8
140 var o: i64 = as_cat(rbuf,0,"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " as *u8); o=as_catn(rbuf,o,bo); o=as_cat(rbuf,o,"\r\nConnection: close\r\n\r\n" as *u8); o=as_cat(rbuf,o,body)
141 return o
142 }
143 // GET .../track : serve (Range + sleep)
144 if as_match(action, as_slen(action), "track" as *u8)==1 {
145 let sp: *i64 = sys_mmap(8) as *i64; let ep: *i64 = sys_mmap(8) as *i64
146 let hr: i64 = parse_range(req, rn, sp, ep)
147 return serve_track(slug, hr, sp[0], ep[0], slp[0], rbuf)
148 }
149 // GET .../<slug> (or speed/...) : the player page
150 let pp: *u8 = sys_mmap(BYTES_MAGIC_1024); var po: i64=as_cat(pp,0,"knowledge/staging/media/reader/" as *u8); po=as_cat(pp,po,slug); po=as_cat(pp,po,"/player.html" as *u8); pp[po]=0 as u8
151 let szp2: *i64 = sys_mmap(16) as *i64; let hb: *u8 = as_readall(pp, szp2)
152 if (hb as i64)==0 { return as_cat(rbuf,0,"HTTP/1.1 404 Not Found\r\nContent-Length: 10\r\nConnection: close\r\n\r\nno player\n" as *u8) }
153 var o: i64 = as_cat(rbuf,0,"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nCache-Control: no-store\r\nConnection: close\r\nContent-Length: " as *u8)
154 o = as_catn(rbuf, o, szp2[0]); o = as_cat(rbuf, o, "\r\n\r\n" as *u8)
155 var k: i64=0; while k<szp2[0] { rbuf[o]=hb[k]; o=o+1; k=k+1 }
156 return o
157}
158
159func main(argc: i64, argv: *i64) -> i64 {
160 if argc < 2 { let u: *u8="AUDIO-SERVE usage: serve [port] | handle <reqfile>\n" as *u8; sys_write(2,u,as_slen(u)); return 1 }
161 let cmd: *u8 = argv[1] as *u8
162 if as_match(cmd, as_slen(cmd), "handle" as *u8)==1 {
163 if argc < 3 { let u: *u8="AUDIO-SERVE handle needs <reqfile>\n" as *u8; sys_write(2,u,as_slen(u)); return 1 }
164 let szp: *i64 = sys_mmap(16) as *i64; let req: *u8 = as_readall(argv[2] as *u8, szp)
165 let rbuf: *u8 = sys_mmap(BYTES_MAGIC_33554432)
166 let rlen: i64 = handle(req, szp[0], rbuf); sys_write(1, rbuf, rlen); return 0
167 }
168 if as_match(cmd, as_slen(cmd), "serve" as *u8)==0 { let u: *u8="AUDIO-SERVE unknown command\n" as *u8; sys_write(2,u,as_slen(u)); return 1 }
169 var port: i64 = BYTES_MAGIC_18099
170 if argc>=3 { var pv: i64=0; var pi: i64=0; let pa: *u8=argv[2] as *u8; while pa[pi]!=(0 as u8){ if pa[pi]>=(48 as u8){if pa[pi]<=(57 as u8){pv=pv*10+((pa[pi] as i64)-48)}} pi=pi+1 } if pv>0{port=pv} }
171 let addr: *u8 = sys_mmap(16); addr[0]=2 as u8; addr[1]=0 as u8; addr[2]=((port>>8)&0xff) as u8; addr[3]=(port&0xff) as u8; addr[4]=127 as u8; addr[5]=0 as u8; addr[6]=0 as u8; addr[7]=1 as u8
172 var zf: i64=8; while zf<16 { addr[zf]=0 as u8; zf=zf+1 }
173 let lfd: i64 = sys_socket(2,1,0); if lfd<0 { return 10 }
174 let ov: *u8 = sys_mmap(4); ov[0]=1 as u8; ov[1]=0 as u8; ov[2]=0 as u8; ov[3]=0 as u8; sys_setsockopt(lfd,1,2,ov,4)
175 if sys_bind(lfd,addr,16)<0 { return 20 }
176 if sys_listen(lfd,64)<0 { return 30 }
177 let banner: *u8 = "AUDIO-SERVE listening on http://127.0.0.1:" as *u8; sys_write(2,banner,as_slen(banner)); let pb: *u8=sys_mmap(16); var pbo: i64=as_catn(pb,0,port); pb[pbo]=10 as u8; sys_write(2,pb,pbo+1)
178 let req: *u8 = sys_mmap(BYTES_MAGIC_65536); let reapst: *i64 = sys_mmap(16) as *i64
179 while 1==1 {
180 let cfd: i64 = sys_accept(lfd); if cfd<0 { continue }
181 let pid: i64 = sys_fork()
182 if pid==0 { sys_close(lfd); let rn: i64=sys_read(cfd,req,BYTES_MAGIC_65536); let rbuf: *u8=sys_mmap(BYTES_MAGIC_33554432); let rlen: i64=handle(req,rn,rbuf); sys_write(cfd,rbuf,rlen); sys_close(cfd); sys_exit(0) }
183 sys_close(cfd); var reaped: i64=1; while reaped>0 { reaped=sys_wait4(0-1,reapst,1) }
184 }
185 sys_close(lfd); return 0
186}