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}