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}