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}