code wiki / _hdl_build / nx_bioacoustic_bench.nx
nx_bioacoustic_bench.nx source
↩ module page · 935 lines · 42012 B
1// nx_bioacoustic_bench.nx -- THE RULER for ANIMAL-VOCALISATION REALISM.
2//
3// Operator 2026-07-25: "get nishi audio state of the art so we can generate animal sounds and a dog toy
4// ball that makes the noise when compressed by a dog's mouth". Rung R0. BEFORE any synthesis claim we
5// need a MEASURED NUMBER, because the only animal-sound organ that exists today (nx_audio_bark.nx) says
6// in its own header "First-iteration timbre -- tune by ear". Tuned by ear is not a measurement, and a
7// lane with no ruler cannot honestly say SOTA. This organ is that ruler.
8//
9// WHY THESE AXES. A dog ignores TV animal sound because the signal fails on several acoustic axes at
10// once. Each axis below is one of those failure modes turned into a number that a real recording passes
11// and a synthetic/looped/broadcast one fails:
12//
13// AX_BAND container bandwidth vs the DOG hearing band (dogs hear to ~60 kHz; 48 kHz media carries
14// 24 kHz -- so a 48k file is PHYSICALLY INCAPABLE of >24 kHz no matter how good the synth).
15// AX_ULTRA fraction of energy above 20 kHz -- content a human cannot hear and a dog can.
16// AX_F0DYN pitch-contour movement (stddev of F0). Real calls sweep; a beep does not.
17// AX_HNR harmonic-to-noise proxy (normalised autocorrelation peak).
18// AX_NONLIN NONLINEAR PHENOMENA -- subharmonics, biphonation, deterministic chaos. This is the
19// hallmark of real mammalian/avian vocal production (the source self-oscillates and jumps
20// between regimes) and it is exactly what sample playback and hand-tuned oscillator banks
21// do not reproduce. Highest weight.
22// AX_NONREP non-repetition across successive segments. A looped sample cross-correlates ~1.0 with
23// itself; a real animal never repeats exactly. This is the HABITUATION axis.
24// AX_ATTACK envelope attack sharpness (transient realism).
25// AX_CREST crest factor = peak/RMS. Broadcast compression crushes this.
26// AX_RESON count of resonances (vocal-tract poles) in the long-term average spectrum.
27// AX_FLUX spectral micro-dynamics frame to frame. A static tone has none.
28//
29// HONESTY DISCIPLINE. Every axis is reference-free and INTRINSIC -- it is a property of the signal
30// itself, so no reference corpus is needed to run it and no axis can be satisfied by simply copying a
31// reference. The ruler is only trustworthy if it REFUSES bad input, so nx_bioacoustic_bench_gate feeds
32// it negative controls (pure sine, white noise, an exactly-looped sample, a band-limited 48k signal) and
33// asserts each one scores LOW on the axes it should fail. A ruler that passes everything is a rubber
34// stamp, not an instrument. See [[feedback-cynical-instruments-author-optimism-is-a-bug]].
35//
36// ANTI-GAMING NOTE ON AX_NONLIN. "Loud but aperiodic" alone is NOT evidence of nonlinear vocal
37// production -- white noise is loud and aperiodic. Real nonlinear phenomena are REGIME TRANSITIONS, so
38// a chaotic frame only counts when the same utterance also contains periodic frames. White noise has no
39// periodic frames, so it scores 0 here. That is the biology, and it is what makes the axis honest.
40//
41// NO MAGIC NUMBERS (rule 11). Every threshold, reference value and axis weight is a config entry, read
42// from data/bioacoustic_bench.cfg (one integer per line, positional, documented in bb_cfg_defaults) with
43// code defaults when the file is absent. Config hierarchy per rule 17: file > code defaults.
44//
45// DRY (rule 15). Trig comes from nx_audio_osc.osc_sin_unit (the canonical range-reduced sine -- NOT the
46// raw Taylor osc_cos_s, which is only accurate near zero). Integer sqrt comes from nx_isqrt.nx, the
47// canonical organ; 139 files in this tree carry a private isqrt and this one will not be number 140.
48//
49// Integer/deterministic throughout: no float, so a score is bit-reproducible and a regression baseline
50// means something. license_tier: ORIGINAL expect_exit: 0
51
52import "nx_syscalls.nx"
53import "nx_audio_osc.nx" // osc_sin_unit -- canonical range-reduced trig
54import "nx_isqrt.nx" // nx_isqrt -- canonical integer sqrt
55const BB_MAGIC_8000: i64 = 8000
56const BB_MAGIC_20000: i64 = 20000
57const BB_MAGIC_60000: i64 = 60000
58const BB_MAGIC_65536: i64 = 65536
59const BB_MAGIC_16777216: i64 = 16777216
60const BB_MAGIC_32768: i64 = 32768
61const BB_MAGIC_8388608: i64 = 8388608
62const BB_MAGIC_1024: i64 = 1024
63const BB_MAGIC_4096: i64 = 4096
64const BB_MAGIC_8192: i64 = 8192
65const BB_MAGIC_2000: i64 = 2000
66const BB_MAGIC_3000: i64 = 3000
67
68// ---------------------------------------------------------------- result vector layout
69const BB_R_RATE: i64 = 0
70const BB_R_NSAMP: i64 = 1
71const BB_R_NCH: i64 = 2
72const BB_R_BITS: i64 = 3
73const BB_R_AX_BAND: i64 = 4
74const BB_R_AX_ULTRA: i64 = 5
75const BB_R_AX_F0DYN: i64 = 6
76const BB_R_AX_HNR: i64 = 7
77const BB_R_AX_NONLIN: i64 = 8
78const BB_R_AX_NONREP: i64 = 9
79const BB_R_AX_ATTACK: i64 = 10
80const BB_R_AX_CREST: i64 = 11
81const BB_R_AX_RESON: i64 = 12
82const BB_R_AX_FLUX: i64 = 13
83const BB_R_AX_VOCAL: i64 = 14
84const BB_R_SCORE: i64 = 15
85const BB_R_F0_MEAN: i64 = 16
86const BB_R_F0_SD: i64 = 17
87const BB_R_VOICED: i64 = 18
88const BB_R_FRAMES: i64 = 19
89const BB_R_ULTRAFRAC: i64 = 20
90const BB_R_CRESTX100: i64 = 21
91const BB_R_NRESON: i64 = 22
92const BB_R_XCORR: i64 = 23
93const BB_R_NL_SUBH: i64 = 24
94const BB_R_NL_BIPH: i64 = 25
95const BB_R_NL_CHAOS: i64 = 26
96const BB_R_NL_PERIOD: i64 = 27
97const BB_R_N: i64 = 28
98
99// ---------------------------------------------------------------- config vector layout
100const BB_C_F0_MIN: i64 = 0 // Hz lowest tracked fundamental
101const BB_C_F0_MAX: i64 = 1 // Hz highest tracked fundamental (mouse squeak fundamentals reach 8k)
102const BB_C_VOICE_FLOOR: i64 = 2 // per-mille of peak frame energy above which a frame is "sounding"
103const BB_C_F0SD_REF: i64 = 3 // Hz F0 stddev that scores 1000
104const BB_C_SUBH_LO: i64 = 4 // per-mille min correlation at P/2 to call it a subharmonic
105const BB_C_SUBH_HI: i64 = 5 // per-mille max correlation at P/2 before it is just the true period
106const BB_C_CHAOS_R: i64 = 6 // per-mille peak correlation below this = aperiodic regime
107const BB_C_BIPH_R: i64 = 7 // per-mille min correlation for a second incommensurate peak
108const BB_C_ULTRA_HZ: i64 = 8 // Hz boundary of the "human cannot hear, dog can" band
109const BB_C_DOG_HZ: i64 = 9 // Hz upper edge of canine hearing -- the bandwidth target
110const BB_C_ULTRA_REF: i64 = 10 // per-mille energy above ULTRA_HZ that scores 1000
111const BB_C_ATTACK_REF: i64 = 11 // per-mille envelope rise per frame that scores 1000
112const BB_C_CREST_FLOOR: i64 = 12 // crest x100 at which the axis starts scoring
113const BB_C_CREST_REF: i64 = 13 // crest x100 that scores 1000
114const BB_C_RESON_REF: i64 = 14 // resonance count that scores 1000
115const BB_C_FLUX_REF: i64 = 15 // per-mille spectral flux that scores 1000
116const BB_C_PROM: i64 = 16 // per-mille of spectral max a peak must clear to count as a resonance
117const BB_C_NSEG: i64 = 17 // segments compared for the non-repetition axis
118const BB_C_W_BAND: i64 = 18
119const BB_C_W_ULTRA: i64 = 19
120const BB_C_W_F0DYN: i64 = 20
121const BB_C_W_HNR: i64 = 21
122const BB_C_W_NONLIN: i64 = 22
123const BB_C_W_NONREP: i64 = 23
124const BB_C_W_ATTACK: i64 = 24
125const BB_C_W_CREST: i64 = 25
126const BB_C_W_RESON: i64 = 26
127const BB_C_W_FLUX: i64 = 27
128const BB_C_W_VOCAL: i64 = 28
129const BB_C_N: i64 = 29
130
131const BB_MAXBAND: i64 = 64
132const BB_BANDRATIO: i64 = 77936 // 2^(1/4) in Q16 -- quarter-octave band bank
133const BB_BAND_LO: i64 = 50 // Hz, lowest band centre (dog hearing floor is ~40 Hz)
134const BB_Q: i64 = 65536
135
136// ---------------------------------------------------------------- tiny io helpers
137func bb_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
138func bb_n(v: i64) -> i64 {
139 let b: *u8 = sys_mmap(32)
140 var x: i64 = v
141 if x < 0 { b[0] = 45 as u8; sys_write(1, b, 1); x = 0 - x }
142 if x == 0 { b[0] = 48 as u8; sys_write(1, b, 1); return 0 }
143 var d: i64 = 0
144 var y: i64 = x
145 while y > 0 { d = d + 1; y = y / 10 }
146 var i: i64 = d - 1
147 y = x
148 while i >= 0 { b[i] = (48 + (y % 10)) as u8; y = y / 10; i = i - 1 }
149 sys_write(1, b, d)
150 return 0
151}
152func bb_kvpair(k: *u8, v: i64) -> i64 { bb_w(k); bb_n(v); bb_w(" " as *u8); return 0 }
153func bb_iabs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
154func bb_min(a: i64, b: i64) -> i64 { if a < b { return a } return b }
155func bb_max(a: i64, b: i64) -> i64 { if a > b { return a } return b }
156// clamp a raw ratio into a 0..1000 axis score
157func bb_axis(num: i64, ref: i64) -> i64 {
158 if ref <= 0 { return 0 }
159 if num <= 0 { return 0 }
160 let s: i64 = (num * 1000) / ref
161 if s > 1000 { return 1000 }
162 return s
163}
164
165// ---------------------------------------------------------------- config
166// Defaults carry their provenance in the comments; the file overrides them so a threshold can be
167// re-tuned without a recompile (rule 11 + rule 17).
168func bb_cfg_defaults(c: *i64) -> i64 {
169 c[BB_C_F0_MIN] = 100 // Hz -- below this is a growl/rumble, tracked by AX_FLUX not F0
170 c[BB_C_F0_MAX] = BB_MAGIC_8000 // Hz -- murine/vole squeak fundamentals run 3-8 kHz
171 c[BB_C_VOICE_FLOOR] = 20 // per-mille of peak frame energy
172 c[BB_C_F0SD_REF] = 300 // Hz -- a prey distress call sweeps hundreds of Hz
173 c[BB_C_SUBH_LO] = 450
174 c[BB_C_SUBH_HI] = 950
175 c[BB_C_CHAOS_R] = 400
176 c[BB_C_BIPH_R] = 450
177 c[BB_C_ULTRA_HZ] = BB_MAGIC_20000 // Hz -- top of nominal human hearing
178 c[BB_C_DOG_HZ] = BB_MAGIC_60000 // Hz -- top of canine hearing
179 c[BB_C_ULTRA_REF] = 150 // per-mille of total energy above 20 kHz
180 c[BB_C_ATTACK_REF] = 250 // per-mille of peak envelope gained in one frame
181 c[BB_C_CREST_FLOOR] = 200 // 2.00x -- heavily compressed broadcast audio sits here
182 c[BB_C_CREST_REF] = 800 // 8.00x -- an uncompressed transient prey sound
183 c[BB_C_RESON_REF] = 4 // a vocal tract shows 3-5 resonances
184 c[BB_C_FLUX_REF] = 200 // per-mille
185 c[BB_C_PROM] = 400 // per-mille a peak must rise ABOVE its adjacent valleys (see AX_RESON)
186 c[BB_C_NSEG] = 8
187 // WEIGHTS -- sum to 1000. NONLIN is heaviest because it is the property no sample player and no
188 // ear-tuned oscillator bank reproduces; ULTRA next because it is the dog-specific axis all consumer
189 // media fails; NONREP next because it is the habituation axis that kills a toy's repeat engagement;
190 // VOCAL next because without it the composite has no way to say "this is not a voice at all".
191 c[BB_C_W_BAND] = 90
192 c[BB_C_W_ULTRA] = 130
193 c[BB_C_W_F0DYN] = 90
194 c[BB_C_W_HNR] = 60
195 c[BB_C_W_NONLIN] = 180
196 c[BB_C_W_NONREP] = 120
197 c[BB_C_W_ATTACK] = 70
198 c[BB_C_W_CREST] = 60
199 c[BB_C_W_RESON] = 40
200 c[BB_C_W_FLUX] = 40
201 c[BB_C_W_VOCAL] = 120
202 return 0
203}
204
205// Generic positional-integer loader: reads decimal integers from `path` into dst[0..cap), skipping
206// '#' comment lines, and returns how many were read. Extracted so the regression baseline plane and
207// the bench config plane share ONE parser -- a second copy of a file format is a second set of bugs.
208func bb_load_ints(dst: *i64, cap: i64, path: *u8) -> i64 {
209 let lo: *i64 = sys_mmap(8) as *i64
210 let buf: *u8 = sys_read_file(path, lo)
211 if buf == (0 as *u8) { return 0 }
212 let flen: i64 = lo[0]
213 var i: i64 = 0
214 var slot: i64 = 0
215 while i < flen {
216 if (buf[i] & 0xff) == 35 {
217 while i < flen && (buf[i] & 0xff) != 10 { i = i + 1 }
218 } else {
219 if (buf[i] & 0xff) >= 48 && (buf[i] & 0xff) <= 57 {
220 var v: i64 = 0
221 while i < flen && (buf[i] & 0xff) >= 48 && (buf[i] & 0xff) <= 57 {
222 v = v * 10 + ((buf[i] & 0xff) - 48)
223 i = i + 1
224 }
225 if slot < cap { dst[slot] = v; slot = slot + 1 }
226 } else {
227 i = i + 1
228 }
229 }
230 }
231 return slot
232}
233
234// Load positional integers (one per line, '#' comment lines skipped) over the defaults.
235// Returns the count of values actually overridden from file; 0 means pure code defaults.
236func bb_cfg_load(c: *i64, path: *u8) -> i64 {
237 bb_cfg_defaults(c)
238 return bb_load_ints(c, BB_C_N, path)
239}
240
241
242// ---------------------------------------------------------------- WAV parsing
243// Defensive at the boundary (rule 12): walk the RIFF chunk list rather than assuming the canonical
244// 44-byte layout, so a file with a LIST/fact chunk still parses instead of silently reading garbage.
245// Writes rate/ch/bits/data_off/data_len into meta[0..4]. Returns 0 ok, negative on malformed input.
246func bb_rd_u16(b: *u8, o: i64) -> i64 { return (b[o] & 0xff) + ((b[o+1] & 0xff) * 256) }
247func bb_rd_u32(b: *u8, o: i64) -> i64 {
248 var v: i64 = b[o] & 0xff
249 v = v + ((b[o+1] & 0xff) * 256)
250 v = v + ((b[o+2] & 0xff) * BB_MAGIC_65536)
251 v = v + ((b[o+3] & 0xff) * BB_MAGIC_16777216)
252 return v
253}
254func bb_tag4(b: *u8, o: i64, a: i64, c2: i64, c3: i64, c4: i64) -> i64 {
255 if (b[o] & 0xff) != a { return 0 }
256 if (b[o+1] & 0xff) != c2 { return 0 }
257 if (b[o+2] & 0xff) != c3 { return 0 }
258 if (b[o+3] & 0xff) != c4 { return 0 }
259 return 1
260}
261func bb_wav_parse(buf: *u8, flen: i64, meta: *i64) -> i64 {
262 if flen < 44 { return 0 - 1 }
263 if bb_tag4(buf, 0, 82, 73, 70, 70) == 0 { return 0 - 2 } // "RIFF"
264 if bb_tag4(buf, 8, 87, 65, 86, 69) == 0 { return 0 - 3 } // "WAVE"
265 var off: i64 = 12
266 var have_fmt: i64 = 0
267 var have_data: i64 = 0
268 var have_both: i64 = 0
269 while off + 8 <= flen && have_both == 0 {
270 let csz: i64 = bb_rd_u32(buf, off + 4)
271 if csz < 0 { return 0 - 4 }
272 if bb_tag4(buf, off, 102, 109, 116, 32) == 1 { // "fmt "
273 if off + 8 + 16 > flen { return 0 - 5 }
274 meta[1] = bb_rd_u16(buf, off + 8 + 2) // channels
275 meta[0] = bb_rd_u32(buf, off + 8 + 4) // sample rate
276 meta[2] = bb_rd_u16(buf, off + 8 + 14) // bits per sample
277 have_fmt = 1
278 }
279 if bb_tag4(buf, off, 100, 97, 116, 97) == 1 { // "data"
280 meta[3] = off + 8
281 var dl: i64 = csz
282 if meta[3] + dl > flen { dl = flen - meta[3] }
283 meta[4] = dl
284 have_data = 1
285 }
286 var adv: i64 = csz
287 if (adv % 2) == 1 { adv = adv + 1 } // RIFF chunks are word-aligned
288 off = off + 8 + adv
289 if have_fmt == 1 && have_data == 1 { have_both = 1 }
290 }
291 if have_fmt == 0 { return 0 - 6 }
292 if have_data == 0 { return 0 - 7 }
293 if meta[2] != 16 && meta[2] != 24 { return 0 - 8 }
294 if meta[0] <= 0 { return 0 - 9 }
295 return 0
296}
297
298// Decode channel 0 into out[] as signed i64. Handles 16 and 24 bit. Returns sample count.
299func bb_wav_decode(buf: *u8, meta: *i64, out: *i64, cap: i64) -> i64 {
300 let nch: i64 = meta[1]
301 let bits: i64 = meta[2]
302 let doff: i64 = meta[3]
303 let dlen: i64 = meta[4]
304 let bps: i64 = bits / 8
305 let stride: i64 = bps * nch
306 if stride <= 0 { return 0 }
307 var n: i64 = dlen / stride
308 if n > cap { n = cap }
309 var i: i64 = 0
310 while i < n {
311 let o: i64 = doff + i * stride
312 var v: i64 = 0
313 if bits == 16 {
314 v = bb_rd_u16(buf, o)
315 if v >= BB_MAGIC_32768 { v = v - BB_MAGIC_65536 }
316 } else {
317 v = (buf[o] & 0xff) + ((buf[o+1] & 0xff) * 256) + ((buf[o+2] & 0xff) * BB_MAGIC_65536)
318 if v >= BB_MAGIC_8388608 { v = v - BB_MAGIC_16777216 }
319 v = v / 256 // normalise 24-bit into the 16-bit domain
320 }
321 out[i] = v
322 i = i + 1
323 }
324 return n
325}
326
327// ---------------------------------------------------------------- spectral analysis (Goertzel bank)
328// Quarter-octave band bank from BB_BAND_LO up to Nyquist. Fills centres[] and returns the band count.
329func bb_bands(rate: i64, centres: *i64) -> i64 {
330 let nyq: i64 = rate / 2
331 var f_q16: i64 = BB_BAND_LO * BB_Q
332 var k: i64 = 0
333 while k < BB_MAXBAND && (f_q16 / BB_Q) < nyq {
334 centres[k] = f_q16 / BB_Q
335 f_q16 = (f_q16 * BB_BANDRATIO) / BB_Q
336 k = k + 1
337 }
338 return k
339}
340
341// Goertzel power of band `f` over x[s..s+m). x must already be scaled down for headroom.
342// Returns power >> 10 to keep long accumulations inside i64.
343func bb_goertzel(x: *i64, s: i64, m: i64, f: i64, rate: i64) -> i64 {
344 // phase increment in OSC_PHASE units; cos(w) = sin(w + quarter turn)
345 let ph: i64 = (f * OSC_PHASE) / rate
346 let cw: i64 = osc_sin_unit(ph + (OSC_PHASE / 4)) // Q16 cosine, range-reduced
347 let coeff: i64 = 2 * cw
348 var s1: i64 = 0
349 var s2: i64 = 0
350 var i: i64 = 0
351 while i < m {
352 let s0: i64 = x[s + i] + ((coeff * s1) / BB_Q) - s2
353 s2 = s1
354 s1 = s0
355 i = i + 1
356 }
357 let a1: i64 = s1 / BB_MAGIC_1024
358 let a2: i64 = s2 / BB_MAGIC_1024
359 var p: i64 = (a1 * a1) + (a2 * a2) - (((coeff * a1) / BB_Q) * a2)
360 if p < 0 { p = 0 }
361 return p
362}
363
364// ---------------------------------------------------------------- normalised autocorrelation
365// r(lag) in per-mille over x[s..s+fn), with the frame MEAN removed.
366//
367// The mean removal is not cosmetic. Without it, any signal with a local DC offset correlates with
368// itself at EVERY lag -- the offset is its own best match -- so the argmax lands on an essentially
369// arbitrary short lag while the peak correlation still reads ~0.97. The symptom is a confident,
370// completely wrong pitch and a spuriously high harmonicity score. It went unnoticed because every
371// control in the gate (sine, noise, sweep, clicks) is zero-mean by construction; the first signal that
372// exposed it was a glottal flow pulse train, which is strictly positive. A0-dc-offset-pitch now keeps
373// a non-zero-mean control in the gate permanently.
374func bb_acorr(x: *i64, s: i64, fn: i64, lag: i64, mean: i64) -> i64 {
375 let m: i64 = fn - lag
376 if m <= 8 { return 0 }
377 var num: i64 = 0
378 var e0: i64 = 0
379 var e1: i64 = 0
380 var i: i64 = 0
381 while i < m {
382 let a: i64 = x[s + i] - mean
383 let b: i64 = x[s + i + lag] - mean
384 num = num + a * b
385 e0 = e0 + a * a
386 e1 = e1 + b * b
387 i = i + 1
388 }
389 if e0 <= 0 { return 0 }
390 if e1 <= 0 { return 0 }
391 let g: i64 = nx_isqrt(e0) * nx_isqrt(e1)
392 if g <= 0 { return 0 }
393 var r: i64 = (num * 1000) / g
394 if r > 1000 { r = 1000 }
395 if r < 0 - 1000 { r = 0 - 1000 }
396 return r
397}
398
399// ---------------------------------------------------------------- biphonation (SPECTRAL)
400// WHY NOT AUTOCORRELATION. The obvious detector -- "a second strong autocorrelation peak at an
401// incommensurate lag" -- does not work, and proving that to myself changed the design. For two sources
402// close in frequency (the usual biphonation case, two folds beating) the second peak falls INSIDE the
403// main autocorrelation lobe of the first and is indistinguishable from it; and when the two are far
404// enough apart to resolve, r at each period collapses to (1 + cos(2*pi*f2/f1))/2, which for an
405// incommensurate ratio sits below any threshold that a pure tone would not also trip. Autocorrelation
406// is the wrong instrument for this axis. The spectrum is the right one: biphonation means a prominent
407// partial that is NOT a harmonic of the fundamental.
408//
409// The reference fundamental is taken from the SPECTRUM (the lowest prominent partial), NOT from the
410// autocorrelation. A quasi-periodic two-source signal has a spurious low autocorrelation f0 -- the
411// near-common period of the two sources -- and a harmonic grid built on a low f0 with a realistic
412// tolerance TILES the whole band, so every partial looks harmonic and the test silently answers "no
413// biphonation" for every input. That failure is invisible from the outside: the detector returns a
414// plausible answer while measuring nothing. Anchoring on the lowest prominent partial removes it.
415//
416// Returns 1 if this frame holds a prominent partial that is not an integer multiple of the lowest one.
417func bb_biph_frame(fpow: *i64, base: i64, centres: *i64, nb: i64, biph_r: i64) -> i64 {
418 var mx: i64 = 0
419 var i: i64 = 0
420 while i < nb { if fpow[base + i] > mx { mx = fpow[base + i] } i = i + 1 }
421 if mx <= 0 { return 0 }
422 let thr: i64 = (mx * biph_r) / 1000
423 // lowest prominent local maximum = the spectral fundamental
424 var f0: i64 = 0
425 i = 1
426 while i < nb - 1 {
427 if f0 == 0 {
428 if fpow[base + i] >= thr && fpow[base + i] > fpow[base + i - 1] && fpow[base + i] >= fpow[base + i + 1] {
429 f0 = centres[i]
430 }
431 }
432 i = i + 1
433 }
434 if f0 <= 0 { return 0 }
435 var found: i64 = 0
436 i = 1
437 while i < nb - 1 {
438 if fpow[base + i] >= thr && fpow[base + i] > fpow[base + i - 1] && fpow[base + i] >= fpow[base + i + 1] {
439 var harm: i64 = 0
440 var k: i64 = 1
441 while k <= 12 {
442 let hf: i64 = f0 * k
443 if bb_iabs(centres[i] - hf) <= hf / 8 { harm = 1 } // tolerance = one quarter-octave band
444 k = k + 1
445 }
446 if harm == 0 { found = 1 }
447 }
448 i = i + 1
449 }
450 return found
451}
452
453// ---------------------------------------------------------------- the analysis
454const BB_MAXFRAME: i64 = 512
455const BB_ADJ: i64 = 2 // frames either side that count as "the same utterance" for regime switching
456
457// Regime labels. A frame is classified by HOW its source is oscillating, which is the whole point of
458// AX_NONLIN: real vocal folds/syrinxes jump between these regimes, oscillator banks do not.
459const BB_REG_SILENT: i64 = 0
460const BB_REG_PERIOD: i64 = 1
461const BB_REG_SUBH: i64 = 2
462const BB_REG_BIPH: i64 = 3
463const BB_REG_CHAOS: i64 = 4
464
465// Full measurement. x = decoded channel-0 samples (16-bit domain), n = count.
466// Writes BB_R_N values into res. Returns 0 on success.
467func bb_analyze(x: *i64, n: i64, rate: i64, nch: i64, bits: i64, c: *i64, res: *i64) -> i64 {
468 var i: i64 = 0
469 while i < BB_R_N { res[i] = 0; i = i + 1 }
470 res[BB_R_RATE] = rate
471 res[BB_R_NSAMP] = n
472 res[BB_R_NCH] = nch
473 res[BB_R_BITS] = bits
474 if n < BB_MAGIC_4096 { return 0 - 1 }
475
476 // ---- AX_BAND: what the container can physically carry vs the canine band.
477 let nyq: i64 = rate / 2
478 res[BB_R_AX_BAND] = bb_axis(nyq, c[BB_C_DOG_HZ])
479
480 // ---- pitch-domain copy, scaled down for autocorr headroom
481 let xp: *i64 = sys_mmap(n * 8) as *i64
482 var peakabs: i64 = 0
483 var sumsq: i64 = 0
484 i = 0
485 while i < n {
486 xp[i] = x[i] / 4
487 let a: i64 = bb_iabs(x[i])
488 if a > peakabs { peakabs = a }
489 sumsq = sumsq + (x[i] / 4) * (x[i] / 4)
490 i = i + 1
491 }
492 // ---- AX_CREST: peak / RMS. Broadcast limiting crushes this toward 1.0.
493 let rms4: i64 = nx_isqrt(sumsq / n)
494 if rms4 > 0 {
495 let crest: i64 = (peakabs * 100) / (rms4 * 4)
496 res[BB_R_CRESTX100] = crest
497 res[BB_R_AX_CREST] = bb_axis(crest - c[BB_C_CREST_FLOOR], c[BB_C_CREST_REF] - c[BB_C_CREST_FLOOR])
498 }
499
500 // ---- framing. The frame must hold two full periods of the LOWEST tracked F0 or the
501 // autocorrelation at that lag is computed over too few samples to mean anything.
502 let lag_min: i64 = rate / c[BB_C_F0_MAX]
503 let lag_max: i64 = rate / c[BB_C_F0_MIN]
504 var fn: i64 = 2 * lag_max + 256
505 if fn > BB_MAGIC_8192 { fn = BB_MAGIC_8192 }
506 let hop: i64 = fn / 2
507 if n < fn + hop { return 0 - 2 }
508 var nfr: i64 = (n - fn) / hop
509 if nfr > BB_MAXFRAME { nfr = BB_MAXFRAME }
510 if nfr < 3 { return 0 - 3 }
511 res[BB_R_FRAMES] = nfr
512
513 // ---- band bank
514 let centres: *i64 = sys_mmap(BB_MAXBAND * 8) as *i64
515 let nb: i64 = bb_bands(rate, centres)
516 let fpow: *i64 = sys_mmap(nfr * BB_MAXBAND * 8) as *i64
517 let ltas: *i64 = sys_mmap(BB_MAXBAND * 8) as *i64
518 i = 0
519 while i < nb { ltas[i] = 0; i = i + 1 }
520
521 // ---- per-frame energy, spectrum, pitch, regime
522 let fen: *i64 = sys_mmap(nfr * 8) as *i64
523 let ff0: *i64 = sys_mmap(nfr * 8) as *i64
524 let freg: *i64 = sys_mmap(nfr * 8) as *i64
525 let frm: *i64 = sys_mmap(nfr * 8) as *i64
526
527 var peaken: i64 = 0
528 var f: i64 = 0
529 while f < nfr {
530 let s: i64 = f * hop
531 var e: i64 = 0
532 var j: i64 = 0
533 while j < fn { e = e + xp[s + j] * xp[s + j]; j = j + 1 }
534 fen[f] = e / fn
535 if fen[f] > peaken { peaken = fen[f] }
536 // spectrum
537 var b: i64 = 0
538 while b < nb {
539 let p: i64 = bb_goertzel(xp, s, fn, centres[b], rate)
540 fpow[f * BB_MAXBAND + b] = p
541 ltas[b] = ltas[b] + p
542 b = b + 1
543 }
544 f = f + 1
545 }
546 let floor_en: i64 = (peaken * c[BB_C_VOICE_FLOOR]) / 1000
547
548 // ---- pitch + regime per sounding frame
549 var n_voiced: i64 = 0
550 var n_tonal: i64 = 0
551 var n_subh: i64 = 0
552 var n_biph: i64 = 0
553 var n_chaos: i64 = 0
554 var n_period: i64 = 0
555 var f0_sum: i64 = 0
556 var rm_sum: i64 = 0
557 f = 0
558 while f < nfr {
559 ff0[f] = 0
560 frm[f] = 0
561 freg[f] = BB_REG_SILENT
562 if fen[f] > floor_en {
563 n_voiced = n_voiced + 1
564 // coarse-to-fine argmax of the normalised autocorrelation
565 let s: i64 = f * hop
566 var fmean: i64 = 0
567 var t: i64 = 0
568 while t < fn { fmean = fmean + xp[s + t]; t = t + 1 }
569 fmean = fmean / fn
570 // The winning lag must be a genuine LOCAL MAXIMUM of the correlation, not merely its
571 // largest value. Autocorrelation always decays monotonically away from lag 0, and for a
572 // smooth waveform that decay is still very high at the shortest tracked lag: the physical
573 // voice source measured r=0.938 at lag 12 while its true period sat at lag 232 with
574 // r=0.978. Any frame where jitter nicked the true peak would then report 8000 Hz instead
575 // of 413 Hz -- which is exactly what happened, as f0mean=766 with sd=1597. A point on the
576 // slope from lag 0 can never be a local maximum, so this test excludes the whole artefact.
577 var best: i64 = 0 - BB_MAGIC_2000
578 var bestlag: i64 = 0
579 // Seed BOTH history slots with real correlations from below the range. Seeding only prev2
580 // does nothing: the first iteration assigns prev2 = prev1, so a sentinel left in prev1
581 // immediately overwrites the seed and the first lag is reported as a peak every time --
582 // silently reinstating the artefact this test exists to remove, while the source file
583 // reads as though it were fixed. Both slots, or neither.
584 var prev2: i64 = BB_MAGIC_3000
585 var prev1: i64 = BB_MAGIC_3000
586 if lag_min > 8 {
587 prev2 = bb_acorr(xp, s, fn, lag_min - 8, fmean)
588 prev1 = bb_acorr(xp, s, fn, lag_min - 4, fmean)
589 }
590 var prevlag: i64 = lag_min - 4
591 var lag: i64 = lag_min
592 while lag <= lag_max + 4 {
593 var r: i64 = 0 - BB_MAGIC_3000
594 if lag <= lag_max { r = bb_acorr(xp, s, fn, lag, fmean) }
595 if prev1 > prev2 && prev1 >= r {
596 if prev1 > best { best = prev1; bestlag = prevlag }
597 }
598 prev2 = prev1
599 prev1 = r
600 prevlag = lag
601 lag = lag + 4
602 }
603 // No local maximum anywhere in range means no periodicity to find -- treat as aperiodic
604 // rather than inventing a pitch from the decay slope.
605 if bestlag == 0 { best = 0; bestlag = lag_max }
606 var lo: i64 = bestlag - 3
607 if lo < lag_min { lo = lag_min }
608 var hi: i64 = bestlag + 3
609 if hi > lag_max { hi = lag_max }
610 lag = lo
611 while lag <= hi {
612 let r: i64 = bb_acorr(xp, s, fn, lag, fmean)
613 if r > best { best = r; bestlag = lag }
614 lag = lag + 1
615 }
616 frm[f] = best
617 ff0[f] = rate / bestlag
618 if best >= c[BB_C_CHAOS_R] {
619 n_tonal = n_tonal + 1
620 f0_sum = f0_sum + ff0[f]
621 rm_sum = rm_sum + best
622 // SUBHARMONIC / period doubling: strong-but-not-equal correlation at half the winning
623 // lag means the waveform repeats every 2 cycles, not every cycle. For a perfectly
624 // periodic signal r(P/2) == r(P), so the SUBH_HI ceiling rejects pure tones.
625 var lab: i64 = BB_REG_PERIOD
626 let half: i64 = bestlag / 2
627 if half >= lag_min {
628 let rh: i64 = bb_acorr(xp, s, fn, half, fmean)
629 if rh >= c[BB_C_SUBH_LO] && rh <= (best * c[BB_C_SUBH_HI]) / 1000 {
630 lab = BB_REG_SUBH
631 }
632 }
633 // BIPHONATION: a prominent partial that is not a harmonic of this frame's fundamental
634 // -- two independent oscillators (left/right fold, or the paired avian syrinx).
635 if lab == BB_REG_PERIOD {
636 if bb_biph_frame(fpow, f * BB_MAXBAND, centres, nb, c[BB_C_BIPH_R]) == 1 {
637 lab = BB_REG_BIPH
638 }
639 }
640 freg[f] = lab
641 if lab == BB_REG_PERIOD { n_period = n_period + 1 }
642 if lab == BB_REG_SUBH { n_subh = n_subh + 1 }
643 if lab == BB_REG_BIPH { n_biph = n_biph + 1 }
644 } else {
645 freg[f] = BB_REG_CHAOS
646 n_chaos = n_chaos + 1
647 }
648 }
649 f = f + 1
650 }
651 res[BB_R_VOICED] = n_voiced
652 res[BB_R_NL_SUBH] = n_subh
653 res[BB_R_NL_BIPH] = n_biph
654 res[BB_R_NL_CHAOS] = n_chaos
655 res[BB_R_NL_PERIOD] = n_period
656
657 // ---- AX_HNR: mean normalised autocorrelation peak over tonal frames.
658 if n_tonal > 0 { res[BB_R_AX_HNR] = rm_sum / n_tonal }
659
660 // ---- AX_VOCAL: what fraction of the sounding frames were produced by an OSCILLATING source.
661 // Without this axis the composite has no way to say "this is not a voice at all", and a
662 // weighted mean will happily rank white noise -- wideband, unrepeating, spectrally busy --
663 // above a real animal call. It did, at 435 against 291, until this axis existed.
664 if n_voiced > 0 { res[BB_R_AX_VOCAL] = (n_tonal * 1000) / n_voiced }
665
666 // ---- AX_F0DYN: how much the pitch actually moves.
667 if n_tonal > 0 {
668 let f0m: i64 = f0_sum / n_tonal
669 res[BB_R_F0_MEAN] = f0m
670 var acc: i64 = 0
671 f = 0
672 while f < nfr {
673 if freg[f] != BB_REG_SILENT && freg[f] != BB_REG_CHAOS {
674 let d: i64 = ff0[f] - f0m
675 acc = acc + d * d
676 }
677 f = f + 1
678 }
679 let sd: i64 = nx_isqrt(acc / n_tonal)
680 res[BB_R_F0_SD] = sd
681 res[BB_R_AX_F0DYN] = bb_axis(sd, c[BB_C_F0SD_REF])
682 }
683
684 // ---- AX_NONLIN. Gated on the presence of a tonal regime: "loud and aperiodic" without any
685 // periodic frame anywhere in the utterance is NOISE, not nonlinear vocal production. This
686 // gate is what stops white noise from scoring as the most lifelike animal on earth.
687 // A chaotic frame only counts when a TONAL frame sits within BB_ADJ frames of it, because a
688 // nonlinear phenomenon is a TRANSITION between regimes. Continuous broadband noise has no
689 // tonal neighbour anywhere and therefore contributes nothing here.
690 if n_voiced > 0 && n_tonal > 0 {
691 var n_switch: i64 = 0
692 f = 0
693 while f < nfr {
694 if freg[f] == BB_REG_CHAOS {
695 var lo2: i64 = f - BB_ADJ
696 if lo2 < 0 { lo2 = 0 }
697 var hi2: i64 = f + BB_ADJ
698 if hi2 > nfr - 1 { hi2 = nfr - 1 }
699 var near_tonal: i64 = 0
700 var g: i64 = lo2
701 while g <= hi2 {
702 if freg[g] == BB_REG_PERIOD { near_tonal = 1 }
703 if freg[g] == BB_REG_SUBH { near_tonal = 1 }
704 if freg[g] == BB_REG_BIPH { near_tonal = 1 }
705 g = g + 1
706 }
707 if near_tonal == 1 { n_switch = n_switch + 1 }
708 }
709 f = f + 1
710 }
711 let nl: i64 = n_subh + n_biph + n_switch
712 var s2: i64 = (nl * 1000) / n_voiced
713 if s2 > 1000 { s2 = 1000 }
714 res[BB_R_AX_NONLIN] = s2
715 }
716
717 // ---- AX_ULTRA: energy the dog hears and the human does not.
718 var e_tot: i64 = 0
719 var e_ultra: i64 = 0
720 i = 0
721 while i < nb {
722 e_tot = e_tot + ltas[i]
723 if centres[i] >= c[BB_C_ULTRA_HZ] { e_ultra = e_ultra + ltas[i] }
724 i = i + 1
725 }
726 if e_tot > 0 {
727 let frac: i64 = (e_ultra * 1000) / e_tot
728 res[BB_R_ULTRAFRAC] = frac
729 res[BB_R_AX_ULTRA] = bb_axis(frac, c[BB_C_ULTRA_REF])
730 }
731
732 // ---- AX_RESON: count vocal-tract style resonances in the long-term average spectrum.
733 // PROMINENCE, not absolute height. The first version of this test asked only "is this band a
734 // local maximum above 8% of the spectral peak", and white noise answered YES seven times --
735 // it scored a perfect 1000 on having a vocal tract. A resonance is a peak that STANDS OUT
736 // from its neighbourhood, so the test is now a rise above the adjacent valleys; noise, whose
737 // neighbouring bands sit within a few per cent of each other, no longer qualifies.
738 var nres: i64 = 0
739 i = 2
740 while i < nb - 2 {
741 if ltas[i] > ltas[i-1] && ltas[i] >= ltas[i+1] {
742 var vl: i64 = ltas[i-1]
743 if ltas[i-2] < vl { vl = ltas[i-2] }
744 var vr: i64 = ltas[i+1]
745 if ltas[i+2] < vr { vr = ltas[i+2] }
746 var base: i64 = vl
747 if vr > base { base = vr }
748 if (ltas[i] - base) * 1000 >= ltas[i] * c[BB_C_PROM] {
749 if centres[i] >= 200 && centres[i] <= BB_MAGIC_8000 { nres = nres + 1 }
750 }
751 }
752 i = i + 1
753 }
754 res[BB_R_NRESON] = nres
755 res[BB_R_AX_RESON] = bb_axis(nres, c[BB_C_RESON_REF])
756
757 // ---- AX_FLUX: frame-to-frame spectral movement. A held tone has none.
758 var flux_acc: i64 = 0
759 var flux_cnt: i64 = 0
760 f = 1
761 while f < nfr {
762 if fen[f] > floor_en && fen[f-1] > floor_en {
763 var d: i64 = 0
764 var t: i64 = 0
765 var b: i64 = 0
766 while b < nb {
767 let a1: i64 = fpow[f * BB_MAXBAND + b]
768 let a0: i64 = fpow[(f-1) * BB_MAXBAND + b]
769 d = d + bb_iabs(a1 - a0)
770 t = t + a1 + a0
771 b = b + 1
772 }
773 if t > 0 { flux_acc = flux_acc + (d * 1000) / t; flux_cnt = flux_cnt + 1 }
774 }
775 f = f + 1
776 }
777 if flux_cnt > 0 { res[BB_R_AX_FLUX] = bb_axis(flux_acc / flux_cnt, c[BB_C_FLUX_REF]) }
778
779 // ---- AX_NONREP: does the signal repeat itself? A looped sample cross-correlates ~1.0 with its
780 // own next copy. Real animals never do. This is the habituation axis.
781 let nseg: i64 = c[BB_C_NSEG]
782 var worst: i64 = 0
783 if nseg >= 2 {
784 let seglen: i64 = n / nseg
785 if seglen > fn {
786 var k: i64 = 0
787 while k < nseg - 1 {
788 let s0: i64 = k * seglen
789 var lag: i64 = 0
790 var bestx: i64 = 0
791 // same DC caveat as bb_acorr: without mean removal a positive-only signal looks like a
792 // perfect repeat of itself no matter what it actually does.
793 var m0: i64 = 0
794 var tt: i64 = 0
795 while tt < fn { m0 = m0 + xp[s0 + tt]; tt = tt + 1 }
796 m0 = m0 / fn
797 while lag < seglen - fn {
798 // correlate segment k's head against segment k+1 at increasing offset
799 var num: i64 = 0
800 var e0: i64 = 0
801 var e1: i64 = 0
802 var j: i64 = 0
803 while j < fn {
804 let a: i64 = xp[s0 + j] - m0
805 let b: i64 = xp[s0 + seglen + lag + j] - m0
806 num = num + a * b
807 e0 = e0 + a * a
808 e1 = e1 + b * b
809 j = j + 1
810 }
811 if e0 > 0 && e1 > 0 {
812 let g: i64 = nx_isqrt(e0) * nx_isqrt(e1)
813 if g > 0 {
814 let r: i64 = (num * 1000) / g
815 if r > bestx { bestx = r }
816 }
817 }
818 lag = lag + fn / 4
819 }
820 if bestx > worst { worst = bestx }
821 k = k + 1
822 }
823 }
824 }
825 res[BB_R_XCORR] = worst
826 var nonrep: i64 = 1000 - worst
827 if nonrep < 0 { nonrep = 0 }
828 res[BB_R_AX_NONREP] = nonrep
829
830 // ---- AX_ATTACK: sharpest envelope rise, normalised to the loudest frame.
831 var envmax: i64 = 0
832 f = 0
833 while f < nfr { let e: i64 = nx_isqrt(fen[f]); if e > envmax { envmax = e } f = f + 1 }
834 var rise: i64 = 0
835 if envmax > 0 {
836 f = 1
837 while f < nfr {
838 let d: i64 = nx_isqrt(fen[f]) - nx_isqrt(fen[f-1])
839 if d > rise { rise = d }
840 f = f + 1
841 }
842 res[BB_R_AX_ATTACK] = bb_axis((rise * 1000) / envmax, c[BB_C_ATTACK_REF])
843 }
844
845 // ---- composite
846 // INVARIANT B1: the weight slots BB_C_W_BAND..BB_C_W_VOCAL are contiguous and in the SAME order as
847 // the axis slots BB_R_AX_BAND..BB_R_AX_VOCAL, so weight i pairs with axis i. Reordering either
848 // block silently mis-weights the composite -- the gate asserts this pairing holds.
849 var wsum: i64 = 0
850 var acc: i64 = 0
851 var w: i64 = BB_C_W_BAND
852 var ax: i64 = BB_R_AX_BAND
853 while w <= BB_C_W_VOCAL {
854 wsum = wsum + c[w]
855 acc = acc + res[ax] * c[w]
856 w = w + 1
857 ax = ax + 1
858 }
859 if wsum > 0 { res[BB_R_SCORE] = acc / wsum }
860 return 0
861}
862
863// Convenience: measure a WAV file on disk. Returns 0 ok, negative on a bad/unreadable file.
864func bb_measure_file(path: *u8, c: *i64, res: *i64) -> i64 {
865 let lo: *i64 = sys_mmap(8) as *i64
866 let buf: *u8 = sys_read_file(path, lo)
867 if buf == (0 as *u8) { return 0 - 20 }
868 let flen: i64 = lo[0]
869 let meta: *i64 = sys_mmap(64) as *i64
870 let pr: i64 = bb_wav_parse(buf, flen, meta)
871 if pr < 0 { return pr }
872 let cap: i64 = 4 * BB_MAGIC_1024 * BB_MAGIC_1024
873 let x: *i64 = sys_mmap(cap * 8) as *i64
874 let n: i64 = bb_wav_decode(buf, meta, x, cap)
875 if n <= 0 { return 0 - 21 }
876 return bb_analyze(x, n, meta[0], meta[1], meta[2], c, res)
877}
878
879// One-line machine-readable card + a human table. Both, always: the card is what a gate greps and the
880// table is what a person reads, and publishing only one of them is how a lane loses its evidence trail.
881func bb_report(label: *u8, res: *i64) -> i64 {
882 bb_w("BIOACOUSTIC-CARD subject=" as *u8); bb_w(label); bb_w(" " as *u8)
883 bb_kvpair("rate=" as *u8, res[BB_R_RATE])
884 bb_kvpair("nsamp=" as *u8, res[BB_R_NSAMP])
885 bb_kvpair("bits=" as *u8, res[BB_R_BITS])
886 bb_kvpair("ax_band=" as *u8, res[BB_R_AX_BAND])
887 bb_kvpair("ax_ultra=" as *u8, res[BB_R_AX_ULTRA])
888 bb_kvpair("ax_f0dyn=" as *u8, res[BB_R_AX_F0DYN])
889 bb_kvpair("ax_hnr=" as *u8, res[BB_R_AX_HNR])
890 bb_kvpair("ax_nonlin=" as *u8, res[BB_R_AX_NONLIN])
891 bb_kvpair("ax_nonrep=" as *u8, res[BB_R_AX_NONREP])
892 bb_kvpair("ax_attack=" as *u8, res[BB_R_AX_ATTACK])
893 bb_kvpair("ax_crest=" as *u8, res[BB_R_AX_CREST])
894 bb_kvpair("ax_reson=" as *u8, res[BB_R_AX_RESON])
895 bb_kvpair("ax_flux=" as *u8, res[BB_R_AX_FLUX])
896 bb_kvpair("ax_vocal=" as *u8, res[BB_R_AX_VOCAL])
897 bb_kvpair("score=" as *u8, res[BB_R_SCORE])
898 bb_kvpair("f0mean=" as *u8, res[BB_R_F0_MEAN])
899 bb_kvpair("f0sd=" as *u8, res[BB_R_F0_SD])
900 bb_kvpair("ultrafrac=" as *u8, res[BB_R_ULTRAFRAC])
901 bb_kvpair("crestx100=" as *u8, res[BB_R_CRESTX100])
902 bb_kvpair("nreson=" as *u8, res[BB_R_NRESON])
903 bb_kvpair("xcorr=" as *u8, res[BB_R_XCORR])
904 bb_kvpair("frames=" as *u8, res[BB_R_FRAMES])
905 bb_kvpair("voiced=" as *u8, res[BB_R_VOICED])
906 bb_kvpair("periodic=" as *u8, res[BB_R_NL_PERIOD])
907 bb_kvpair("subh=" as *u8, res[BB_R_NL_SUBH])
908 bb_kvpair("biph=" as *u8, res[BB_R_NL_BIPH])
909 bb_kvpair("chaos=" as *u8, res[BB_R_NL_CHAOS])
910 bb_w("\n" as *u8)
911 return 0
912}
913
914func main(argc: i64, argv: *i64) -> i64 {
915 let c: *i64 = sys_mmap(BB_C_N * 8) as *i64
916 let nfile: i64 = bb_cfg_load(c, "data/bioacoustic_bench.cfg" as *u8)
917 let res: *i64 = sys_mmap(BB_R_N * 8) as *i64
918 if argc < 2 {
919 bb_w("nx_bioacoustic_bench -- THE RULER for animal-vocalisation realism\n" as *u8)
920 bb_w("usage: nx_bioacoustic_bench <file.wav> [label]\n" as *u8)
921 bb_w("axes: band ultra f0dyn hnr nonlin nonrep attack crest reson flux -> score (per-mille)\n" as *u8)
922 bb_w("cfg-overrides-loaded="); bb_n(nfile); bb_w("\n" as *u8)
923 return 0
924 }
925 let path: *u8 = argv[1] as *u8
926 var label: *u8 = path
927 if argc >= 3 { label = argv[2] as *u8 }
928 let rc: i64 = bb_measure_file(path, c, res)
929 if rc < 0 {
930 bb_w("BIOACOUSTIC-CARD subject="); bb_w(label); bb_w(" ERROR rc="); bb_n(rc); bb_w("\n" as *u8)
931 return 2
932 }
933 bb_report(label, res)
934 return 0
935}