code wiki / _hdl_build / nx_birdsynth.nx

nx_birdsynth.nx source

↩ module page · 322 lines · 13980 B

1// nx_birdsynth.nx -- SOVEREIGN BAND-LIMITED BIRD-CALL EMITTER (the SKU-2 signal source). 2// 3// Operator: a remote-controlled toy that emits "exact replicas of bird noises based on bird calls". 4// The estate already owns the RULER for that claim -- nx_bioacoustic_bench scores animal-vocalisation 5// realism on ten axes to a per-mille. What it did not own was anything to MEASURE: the only wavs in 6// the corpus are nx_gamemusic.wav and sfx_demo.wav. ★A RULER WITH NOTHING TO MEASURE IS A COMMENT. 7// This organ closes that loop -- emit, measure, iterate -- and the same PCM it writes is what the 8// toy's DAC will eventually clock out. 9// 10// WHY SYNTHESIS AND NOT A SAMPLE. A recording, looped, fails the bench on the axis that matters most: 11// `nonrep`. Real animals never repeat exactly (breath-driven micro-variation), and a dog's habituation 12// is driven precisely by detecting the repeat. So the emitter is GENERATIVE -- per-syllable phase 13// jitter from an LCG means no two utterances are sample-identical -- which is a property a sample 14// player structurally cannot have. 15// 16// ★★BAND-LIMITED ON PURPOSE, AND THAT IS THE WHOLE EXPERIMENT. A harmonic is synthesised ONLY while 17// k*f0 < rate/2. That is correct DSP (no aliasing), and it makes the frequency-cutoff thesis MEASURABLE 18// instead of asserted: render the SAME call at 96 kHz and at 22.05 kHz and the low-rate render cannot 19// carry the upper partials AT ALL -- not "quieter", ABSENT. The bench's ax_ultra / ax_band axes then 20// report that difference as a number. Consumer audio does not fail a dog by being low quality; it fails 21// by being structurally incapable of representing the signal. 22// 23// INTEGER-ONLY (sovereign no-float law). Sine comes from a 65-entry quarter-wave table filled at 24// runtime and mirrored into four quadrants. The table is mmap'd and filled by assignment, NEVER a 25// module-level const array -- a const array here compiles clean and then reads garbage (banked gotcha), 26// and a BSS [N]i64 silently crashes a handler module on startup. 27// 28// usage: nx_birdsynth <out.wav> <rate_hz> <ms> [seed] 29// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 30import "nx_syscalls.nx" 31const BS_MAGIC_1224: i64 = 1224 32const BS_MAGIC_1467: i64 = 1467 33const BS_MAGIC_1710: i64 = 1710 34const BS_MAGIC_1951: i64 = 1951 35const BS_MAGIC_2191: i64 = 2191 36const BS_MAGIC_2430: i64 = 2430 37const BS_MAGIC_2667: i64 = 2667 38const BS_MAGIC_2903: i64 = 2903 39const BS_MAGIC_3137: i64 = 3137 40const BS_MAGIC_3369: i64 = 3369 41const BS_MAGIC_3599: i64 = 3599 42const BS_MAGIC_3827: i64 = 3827 43const BS_MAGIC_4052: i64 = 4052 44const BS_MAGIC_4276: i64 = 4276 45const BS_MAGIC_4496: i64 = 4496 46const BS_MAGIC_4714: i64 = 4714 47const BS_MAGIC_4929: i64 = 4929 48const BS_MAGIC_5141: i64 = 5141 49const BS_MAGIC_5350: i64 = 5350 50const BS_MAGIC_5556: i64 = 5556 51const BS_MAGIC_5758: i64 = 5758 52const BS_MAGIC_5957: i64 = 5957 53const BS_MAGIC_6152: i64 = 6152 54const BS_MAGIC_6344: i64 = 6344 55const BS_MAGIC_6532: i64 = 6532 56const BS_MAGIC_6716: i64 = 6716 57const BS_MAGIC_6895: i64 = 6895 58const BS_MAGIC_7071: i64 = 7071 59const BS_MAGIC_7242: i64 = 7242 60const BS_MAGIC_7410: i64 = 7410 61const BS_MAGIC_7572: i64 = 7572 62const BS_MAGIC_7730: i64 = 7730 63const BS_MAGIC_7883: i64 = 7883 64const BS_MAGIC_8032: i64 = 8032 65const BS_MAGIC_8176: i64 = 8176 66const BS_MAGIC_8315: i64 = 8315 67const BS_MAGIC_8449: i64 = 8449 68const BS_MAGIC_8577: i64 = 8577 69const BS_MAGIC_8701: i64 = 8701 70const BS_MAGIC_8819: i64 = 8819 71const BS_MAGIC_8932: i64 = 8932 72const BS_MAGIC_9040: i64 = 9040 73const BS_MAGIC_9142: i64 = 9142 74const BS_MAGIC_9239: i64 = 9239 75const BS_MAGIC_9330: i64 = 9330 76const BS_MAGIC_9415: i64 = 9415 77const BS_MAGIC_9495: i64 = 9495 78const BS_MAGIC_9569: i64 = 9569 79const BS_MAGIC_9638: i64 = 9638 80const BS_MAGIC_9700: i64 = 9700 81const BS_MAGIC_9757: i64 = 9757 82const BS_MAGIC_9808: i64 = 9808 83const BS_MAGIC_9853: i64 = 9853 84const BS_MAGIC_9892: i64 = 9892 85const BS_MAGIC_9925: i64 = 9925 86const BS_MAGIC_9952: i64 = 9952 87const BS_MAGIC_9973: i64 = 9973 88const BS_MAGIC_9988: i64 = 9988 89const BS_MAGIC_9997: i64 = 9997 90const BS_MAGIC_10000: i64 = 10000 91const BS_MAGIC_1103515245: i64 = 1103515245 92const BS_MAGIC_12345: i64 = 12345 93const BS_MAGIC_8000: i64 = 8000 94const BS_MAGIC_2600: i64 = 2600 95const BS_MAGIC_2000: i64 = 2000 96const BS_MAGIC_2400: i64 = 2400 97const BS_MAGIC_240000: i64 = 240000 98const BS_MAGIC_8500: i64 = 8500 99const BS_MAGIC_20000: i64 = 20000 100const BS_MAGIC_2001: i64 = 2001 101const BS_MAGIC_65536: i64 = 65536 102const BS_MAGIC_100000: i64 = 100000 103const BS_MAGIC_32000: i64 = 32000 104const BS_MAGIC_65535: i64 = 65535 105 106const BS_TABN: i64 = 65 // quarter wave, 0..64 inclusive = 0..90 degrees 107const BS_AMP: i64 = 10000 // sine full-scale (integer) 108const BS_PHONE: i64 = 16777216 // one full cycle in phase units (256 table steps << 16) 109const BS_MAXH: i64 = 8 // harmonics considered 110const BS_MAXMS: i64 = 4000 111const BS_MAXRATE: i64 = 192000 112const BS_HDR: i64 = 44 113 114func bw(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 } 115func bs_atoi(s: *u8) -> i64 { 116 var v: i64 = 0 117 var i: i64 = 0 118 while s[i] != (0 as u8) { 119 let c: i64 = s[i] as i64 120 if c < 48 { return v } 121 if c > 57 { return v } 122 v = v * 10 + (c - 48) 123 i = i + 1 124 } 125 return v 126} 127func bs_num(out: *u8, o: i64, v: i64) -> i64 { 128 var x: i64 = v 129 var p: i64 = o 130 if x < 0 { out[p] = 45 as u8; p = p + 1; x = 0 - x } 131 if x == 0 { out[p] = 48 as u8; return p + 1 } 132 var d: i64 = 0 133 var t: i64 = x 134 while t > 0 { d = d + 1; t = t / 10 } 135 var i: i64 = d - 1 136 while i >= 0 { out[p + i] = ((x % 10) + 48) as u8; x = x / 10; i = i - 1 } 137 return p + d 138} 139func bs_lit(out: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { out[o + i] = s[i]; i = i + 1 } return o + i } 140 141// ---- quarter-wave sine table, filled by ASSIGNMENT (see header: never a const array) ------------- 142func bs_fill_table(t: *i64) -> i64 { 143 t[0]=0 t[1]=245 t[2]=491 t[3]=736 t[4]=980 t[5]=BS_MAGIC_1224 t[6]=BS_MAGIC_1467 t[7]=BS_MAGIC_1710 144 t[8]=BS_MAGIC_1951 t[9]=BS_MAGIC_2191 t[10]=BS_MAGIC_2430 t[11]=BS_MAGIC_2667 t[12]=BS_MAGIC_2903 t[13]=BS_MAGIC_3137 t[14]=BS_MAGIC_3369 t[15]=BS_MAGIC_3599 145 t[16]=BS_MAGIC_3827 t[17]=BS_MAGIC_4052 t[18]=BS_MAGIC_4276 t[19]=BS_MAGIC_4496 t[20]=BS_MAGIC_4714 t[21]=BS_MAGIC_4929 t[22]=BS_MAGIC_5141 t[23]=BS_MAGIC_5350 146 t[24]=BS_MAGIC_5556 t[25]=BS_MAGIC_5758 t[26]=BS_MAGIC_5957 t[27]=BS_MAGIC_6152 t[28]=BS_MAGIC_6344 t[29]=BS_MAGIC_6532 t[30]=BS_MAGIC_6716 t[31]=BS_MAGIC_6895 147 t[32]=BS_MAGIC_7071 t[33]=BS_MAGIC_7242 t[34]=BS_MAGIC_7410 t[35]=BS_MAGIC_7572 t[36]=BS_MAGIC_7730 t[37]=BS_MAGIC_7883 t[38]=BS_MAGIC_8032 t[39]=BS_MAGIC_8176 148 t[40]=BS_MAGIC_8315 t[41]=BS_MAGIC_8449 t[42]=BS_MAGIC_8577 t[43]=BS_MAGIC_8701 t[44]=BS_MAGIC_8819 t[45]=BS_MAGIC_8932 t[46]=BS_MAGIC_9040 t[47]=BS_MAGIC_9142 149 t[48]=BS_MAGIC_9239 t[49]=BS_MAGIC_9330 t[50]=BS_MAGIC_9415 t[51]=BS_MAGIC_9495 t[52]=BS_MAGIC_9569 t[53]=BS_MAGIC_9638 t[54]=BS_MAGIC_9700 t[55]=BS_MAGIC_9757 150 t[56]=BS_MAGIC_9808 t[57]=BS_MAGIC_9853 t[58]=BS_MAGIC_9892 t[59]=BS_MAGIC_9925 t[60]=BS_MAGIC_9952 t[61]=BS_MAGIC_9973 t[62]=BS_MAGIC_9988 t[63]=BS_MAGIC_9997 151 t[64]=BS_MAGIC_10000 152 return 0 153} 154 155// idx is a 0..255 position around the full cycle -> -10000..10000 156func bs_sin(t: *i64, idx: i64) -> i64 { 157 let i: i64 = idx & 255 158 let q: i64 = i & 63 159 let quad: i64 = (i >> 6) & 3 160 if quad == 0 { return t[q] } 161 if quad == 1 { return t[64 - q] } 162 if quad == 2 { return 0 - t[q] } 163 return 0 - t[64 - q] 164} 165 166func bs_lcg(s: i64) -> i64 { return ((s * BS_MAGIC_1103515245) + BS_MAGIC_12345) & 0x7FFFFFFF } 167 168// ---- little-endian writers --------------------------------------------------------------------- 169func bs_u32(b: *u8, o: i64, v: i64) -> i64 { 170 b[o] = (v & 255) as u8 171 b[o+1] = ((v >> 8) & 255) as u8 172 b[o+2] = ((v >> 16) & 255) as u8 173 b[o+3] = ((v >> 24) & 255) as u8 174 return o + 4 175} 176func bs_u16(b: *u8, o: i64, v: i64) -> i64 { 177 b[o] = (v & 255) as u8 178 b[o+1] = ((v >> 8) & 255) as u8 179 return o + 2 180} 181func bs_tag(b: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while i < 4 { b[o+i] = s[i]; i = i + 1 } return o + 4 } 182 183func main(argc: i64, argv: *i64) -> i64 { 184 if argc < 4 { 185 bw(1, "usage: nx_birdsynth <out.wav> <rate_hz> <ms> [seed]\n" as *u8) 186 bw(1, " band-limited additive bird call; a harmonic is emitted ONLY while k*f0 < rate/2,\n" as *u8) 187 bw(1, " so a low-rate render cannot carry the upper partials AT ALL -- that absence is the measurement.\n" as *u8) 188 return 2 189 } 190 let path: *u8 = argv[1] as *u8 191 let rate: i64 = bs_atoi(argv[2] as *u8) 192 var ms: i64 = bs_atoi(argv[3] as *u8) 193 var seed: i64 = BS_MAGIC_12345 194 if argc >= 5 { seed = bs_atoi(argv[4] as *u8) } 195 196 if rate < BS_MAGIC_8000 { bw(2, "nx_birdsynth: rate must be >= 8000\n" as *u8); return 2 } 197 if rate > BS_MAXRATE { bw(2, "nx_birdsynth: rate above 192000 refused\n" as *u8); return 2 } 198 if ms <= 0 { bw(2, "nx_birdsynth: ms must be > 0\n" as *u8); return 2 } 199 if ms > BS_MAXMS { ms = BS_MAXMS } 200 201 let t: *i64 = sys_mmap(BS_TABN * 8) as *i64 202 bs_fill_table(t) 203 204 let nsamp: i64 = (rate * ms) / 1000 205 let datalen: i64 = nsamp * 2 206 let buf: *u8 = sys_mmap(BS_HDR + datalen + 64) 207 208 // ---- RIFF/WAVE header ---- 209 var o: i64 = 0 210 o = bs_tag(buf, o, "RIFF" as *u8) 211 o = bs_u32(buf, o, 36 + datalen) 212 o = bs_tag(buf, o, "WAVE" as *u8) 213 o = bs_tag(buf, o, "fmt " as *u8) 214 o = bs_u32(buf, o, 16) // PCM chunk size 215 o = bs_u16(buf, o, 1) // format = PCM 216 o = bs_u16(buf, o, 1) // channels = mono 217 o = bs_u32(buf, o, rate) 218 o = bs_u32(buf, o, rate * 2) // byte rate 219 o = bs_u16(buf, o, 2) // block align 220 o = bs_u16(buf, o, 16) // bits 221 o = bs_tag(buf, o, "data" as *u8) 222 o = bs_u32(buf, o, datalen) 223 224 // ---- synthesis ---- 225 // A call is a run of short syllables. Within a syllable f0 sweeps; across syllables the start 226 // frequency and length wander, because a bird that emits the same syllable twice is a squeaker. 227 let nyq: i64 = rate / 2 228 let syl_ms: i64 = 70 229 let gap_ms: i64 = 45 230 let syl_n: i64 = (rate * syl_ms) / 1000 231 let gap_n: i64 = (rate * gap_ms) / 1000 232 let period: i64 = syl_n + gap_n 233 234 var ph: i64 = 0 235 var i: i64 = 0 236 var maxh_used: i64 = 0 237 var ultra_h: i64 = 0 // harmonics landing above 20 kHz (dog-audible, human-inaudible) 238 239 while i < nsamp { 240 let pos: i64 = i % period 241 var s: i64 = 0 242 if pos < syl_n { 243 let sylidx: i64 = i / period 244 // per-syllable variation: start frequency wanders 2600..4600 Hz 245 seed = bs_lcg(seed) 246 let f_start: i64 = BS_MAGIC_2600 + ((sylidx * 337 + (seed >> 11)) % BS_MAGIC_2000) 247 // sweep upward across the syllable, plus vibrato 248 let frac: i64 = (pos * 1000) / syl_n // 0..1000 249 let vib: i64 = bs_sin(t, ((i * 256 * 55) / rate) & 255) // ~55 Hz vibrato 250 var f: i64 = f_start + ((f_start * frac) / BS_MAGIC_2400) + ((vib * f_start) / BS_MAGIC_240000) 251 if f < 200 { f = 200 } 252 253 // envelope: fast attack, exponential-ish decay (integer) 254 var env: i64 = BS_MAGIC_10000 255 let att: i64 = syl_n / 12 256 if pos < att { env = (pos * BS_MAGIC_10000) / att } 257 else { 258 let d: i64 = ((pos - att) * BS_MAGIC_10000) / (syl_n - att) 259 env = BS_MAGIC_10000 - ((d * BS_MAGIC_8500) / BS_MAGIC_10000) 260 } 261 262 // additive, BAND-LIMITED 263 var k: i64 = 1 264 var acc: i64 = 0 265 while k <= BS_MAXH { 266 let fk: i64 = f * k 267 if fk < nyq { 268 let idx: i64 = ((ph * k) >> 16) & 255 269 acc = acc + ((bs_sin(t, idx) * (BS_AMP / k)) / BS_AMP) 270 if k > maxh_used { maxh_used = k } 271 if fk > BS_MAGIC_20000 { ultra_h = 1 } 272 } 273 k = k + 1 274 } 275 276 // breath noise -- broadband, so at a high rate it too carries ultrasonic energy 277 seed = bs_lcg(seed) 278 let nz: i64 = ((seed % BS_MAGIC_2001) - 1000) 279 acc = acc + (nz / 3) 280 281 s = (acc * env) / BS_MAGIC_10000 282 283 // phase advance with micro-jitter: the non-repetition axis, by construction 284 seed = bs_lcg(seed) 285 let jit: i64 = (seed % 41) - 20 // +/- 20 ppm-ish on the increment 286 var inc: i64 = (f * 256 * BS_MAGIC_65536) / rate 287 inc = inc + ((inc * jit) / BS_MAGIC_100000) 288 ph = (ph + inc) % BS_PHONE 289 } 290 if s > BS_MAGIC_32000 { s = BS_MAGIC_32000 } 291 if s < (0 - BS_MAGIC_32000) { s = 0 - BS_MAGIC_32000 } 292 let u: i64 = s & BS_MAGIC_65535 293 buf[o] = (u & 255) as u8 294 buf[o+1] = ((u >> 8) & 255) as u8 295 o = o + 2 296 i = i + 1 297 } 298 299 let fd: i64 = sys_openat_wr(path, 420) 300 if fd < 0 { bw(2, "nx_birdsynth: cannot open output\n" as *u8); return 3 } 301 sys_write(fd, buf, o) 302 sys_close(fd) 303 304 let rb: *u8 = sys_mmap(512) 305 var r: i64 = 0 306 r = bs_lit(rb, r, "BIRDSYNTH-OK path=" as *u8) 307 r = bs_lit(rb, r, path) 308 r = bs_lit(rb, r, " rate=" as *u8) 309 r = bs_num(rb, r, rate) 310 r = bs_lit(rb, r, " nyquist=" as *u8) 311 r = bs_num(rb, r, nyq) 312 r = bs_lit(rb, r, " samples=" as *u8) 313 r = bs_num(rb, r, nsamp) 314 r = bs_lit(rb, r, " bytes=" as *u8) 315 r = bs_num(rb, r, o) 316 r = bs_lit(rb, r, " harmonics_rendered=" as *u8) 317 r = bs_num(rb, r, maxh_used) 318 r = bs_lit(rb, r, " any_partial_above_20k=" as *u8) 319 r = bs_num(rb, r, ultra_h) 320 r = bs_lit(rb, r, "\n" as *u8) 321 sys_write(1, rb, r) 322 return 0 323}