. "$BST_LIB" # BST-TIMEOUT: 300 # measured 22s on target; the default 60 is only 2.7x that # speexdsp 1.2.1 (poky recipes-multimedia/speex) -- the resampler, the MDF # acoustic echo canceller, the preprocessor and the jitter buffer, driven # through upstream's own five test programs, which this image installs as # speexdsp-test{resample,resample2,denoise,echo,jitter} (see # recipes-multimedia/speexdsp/speexdsp_%.bbappend for why a library-only port # ships binaries at all, and why they are renamed). # # WHAT A DSP LIBRARY CAN BE ASSERTED TO DO, AND WHAT IT CANNOT. Every driver # here is a pure function of its input, so the tempting oracle is a stored # digest -- and the ticket names one. A digest is used below, but only where it # is a VALID oracle: WITHIN one image, across two runs. It is deliberately NOT # stored as a cross-machine reference, for the reason libvorbis established next # door and which applies with more force here. speexdsp's float API runs its # sinc tables, its FFTs and its adaptive filter through libm's sin/cos/exp/sqrt, # whose x86-64 glibc carries assembly and IFUNC variants wasm32-hwjs does not, # and configure selects -O flags and SSE/NEON kernels by $host. A digest # recorded on the build host and asserted on the target would be a claim that # two different libm implementations agree to the last bit. A red from that # would say nothing whatever about this port. # # So each subsystem is settled by three kinds of case instead, and each catches # something the others cannot: # # 1. EXACT INTEGER STRUCTURE. Output lengths and the rate-sweep trace are # driven by integer counters, not by arithmetic on samples, so they ARE # bit-exact on any machine and are asserted to the byte and to the line. # This is what catches a resampler that silently produced no output, or a # driver that stopped a block early. # 2. DETERMINISM. Two runs of one driver over one fixture must be identical. # This is the ticket's digest criterion, applied where a digest holds. # 3. A MEASURED SIGNAL PROPERTY, computed on target from the samples # themselves -- band-limiting for the resampler, echo return loss for the # canceller, signal-to-noise improvement for the preprocessor. These are # what a library that links, runs, is perfectly deterministic and computes # nonsense would still fail. Every bound below carries multiples of # headroom over a value measured natively (recorded in the design doc), so # a different libm cannot trip one and a broken DSP path cannot pass one. # # EVERYTHING IS OFFLINE. No audio device, no ALSA, no /dev/dsp, no network, no # peer: every driver reads a file (or nothing) and writes a file. require speexdsp-testresample speexdsp/present tmp=${TMPDIR:-/tmp} w=$tmp/bst-speexdsp.$$ mkdir -p "$w" 2>/dev/null RS=$BST_DATA/speexdsp-resample-96k.raw DN=$BST_DATA/speexdsp-denoise-8k.raw EM=$BST_DATA/speexdsp-echo-mic.raw ES=$BST_DATA/speexdsp-echo-spk.raw # --------------------------------------------------------------- fixtures --- # Shipped rather than generated in-shell: every one is band-limited sine content # or a seeded pseudo-random noise floor, and a POSIX shell has no sin(). data/ # is installed 0644 and the rootfs is read-only at browser runtime, which is # fine -- nothing writes to them. src/speexdsp_mkfixtures.py regenerates all # four byte for byte, and documents the geometry each driver hardcodes. miss= for f in "$RS" "$DN" "$EM" "$ES"; do [ -f "$f" ] || miss="$miss $f" done if [ -n "$miss" ]; then bst_notok speexdsp/fixtures "not on the image:$miss -- remaining cases not run" rm -rf "$w" exit 0 fi check_eq speexdsp/fixture-resample 16384 "$(wc -c < "$RS" 2>/dev/null | tr -d ' ')" check_eq speexdsp/fixture-denoise 32000 "$(wc -c < "$DN" 2>/dev/null | tr -d ' ')" check_eq speexdsp/fixture-echo-mic 32000 "$(wc -c < "$EM" 2>/dev/null | tr -d ' ')" check_eq speexdsp/fixture-echo-spk 32000 "$(wc -c < "$ES" 2>/dev/null | tr -d ' ')" # ---------------------------------------------------------------- helpers --- # od -An -v -td2 -w2 prints one signed 16-bit sample per line, host byte order, # which is little-endian here and matches the fixtures. -v is load-bearing: # without it od collapses repeated lines to `*` and a silent stretch would be # silently shortened. pcm2txt() { od -An -v -td2 -w2 "$1" > "$2" 2>/dev/null; } # NOTHING BELOW CALLS awk's sqrt, sin OR cos, AND THAT IS DELIBERATE. busybox # awk can be built without math support -- `awk: Math support is not compiled # in` -- and where it is, the whole expression yields nothing and every # measurement case reports an EMPTY number rather than a failure anyone can # read. That is exactly the shape of red that gets misread as a port defect. # The image ships gawk today, so this is insurance rather than a live fault; # it costs two small workarounds and it makes the build-host dry-run under # busybox ash a true proxy for the target instead of a diverging one. # # Workaround one: every ratio is computed in the MEAN-SQUARE domain, where no # root is needed, and the square root is taken afterwards in the shell by # Newton's method on integers. Values are scaled to parts-per-million before # the root so the result comes back in parts-per-thousand. isqrt() { _iq_n=$1 if [ "${_iq_n:-0}" -le 0 ]; then echo 0; return; fi _iq_x=$_iq_n; _iq_y=$(( (_iq_x + 1) / 2 )) while [ "$_iq_y" -lt "$_iq_x" ]; do _iq_x=$_iq_y; _iq_y=$(( (_iq_x + _iq_n / _iq_x) / 2 )) done echo "$_iq_x" } # ms_ppm # -> 1e6 * meansquare(cmp range) / meansquare(ref range), capped at 1e9 so the # shell's integer arithmetic cannot overflow on a 32-bit target; -1 when the # reference range is silent (a ratio against zero is not a measurement and # must not be reported as one). Ranges are half-open sample indices; -1 as a # high bound means "to the end". ms_ppm() { awk -v la="$2" -v ha="$3" -v lb="$5" -v hb="$6" ' NR==FNR { i=FNR-1; if (i>=la && (ha<0||i=lb && (hb<0||i 1000000000) r = 1000000000 printf "%d\n", r }' "$1" "$4" 2>/dev/null } # rms_ppt -> 1000 * rms(cmp)/rms(ref), or -1. rms_ppt() { _rp=$(ms_ppm "$@") if [ "${_rp:--1}" -lt 0 ]; then echo -1; else isqrt "$_rp"; fi } # tone_noise -> " " # A Goertzel-style correlation against a sine and a cosine splits the range's # energy into the 500 Hz tone and everything else, which is what makes "did the # preprocessor improve the SNR" answerable at all: plain RMS cannot tell an # attenuated noise floor from an attenuated signal. # # Workaround two: the fixture's tone is 500 Hz at 8 kHz, so its period is # EXACTLY 16 samples and the correlation needs only sixteen fixed values of # sin/cos rather than a trig call per sample. The tables below are # sin(pi*k/8) and cos(pi*k/8) for k = 0..15. That is why the fixture uses 500 # Hz and why speexdsp_mkfixtures.py must not be re-tuned to some other tone # without regenerating these. tone_noise() { awk -v lo="$2" -v hi="$3" ' BEGIN { split("0 0.38268343 0.70710678 0.92387953 1 0.92387953 0.70710678 0.38268343 " \ "0 -0.38268343 -0.70710678 -0.92387953 -1 -0.92387953 -0.70710678 -0.38268343", S) split("1 0.92387953 0.70710678 0.38268343 0 -0.38268343 -0.70710678 -0.92387953 " \ "-1 -0.92387953 -0.70710678 -0.38268343 0 0.38268343 0.70710678 0.92387953", C) } { i=FNR-1; if (i>=lo && (hi<0||i 1000000000) r = 1000000000 printf "%d %d\n", amp2, r }' "$1" 2>/dev/null } # -------------------------------------------------------------- resampler --- # testresample: speex_resampler_init(1, 8000, 12000, quality 10) then # set_rate(96000, 44100) and skip_zeros(), reading 256-sample blocks off stdin. speexdsp-testresample < "$RS" > "$w/rs1.raw" 2>"$w/rs1.err"; rc=$? if [ $rc -eq 0 ]; then bst_ok speexdsp/resample-run else bst_notok speexdsp/resample-run "rc=$rc $(_bst_1line "$(cat "$w/rs1.err" 2>/dev/null)")"; fi rsbytes=$(wc -c < "$w/rs1.raw" 2>/dev/null | tr -d ' ') if [ -z "$rsbytes" ] || [ "$rsbytes" -lt 64 ]; then bst_notok speexdsp/resample-output "resampler produced ${rsbytes:-0} bytes -- remaining resampler cases not run" else # 8192 in at 96 kHz -> 3635 out at 44.1 kHz = 7270 bytes. Not 8192*44100/96000 # = 3762: skip_zeros() advances past the filter's start-up latency, and the # tail short of a full output block is never flushed. All of that bookkeeping # is integer, so this is exact on any machine -- which is exactly why it is # asserted to the byte while the sample VALUES are not. check_eq speexdsp/resample-length 7270 "$rsbytes" speexdsp-testresample < "$RS" > "$w/rs2.raw" 2>/dev/null if [ -s "$w/rs1.raw" ] && cmp -s "$w/rs1.raw" "$w/rs2.raw"; then bst_ok speexdsp/resample-deterministic else bst_notok speexdsp/resample-deterministic "two resamples of one fixture differ" fi # THE BAND-LIMIT CASE, which is the one that distinguishes a resampler from a # decimator. The fixture carries 1 kHz and 5 kHz (both inside the 22.05 kHz # output Nyquist, both must survive) and 30 kHz (above it, must be filtered # AWAY rather than folded down). Dropping only the third tone's energy puts # the output/input RMS ratio at sqrt(200/264) = 870 ppt, and that is what was # measured natively: 870. Naive decimation would alias 30 kHz to 14.1 kHz, # keep the energy and land near 1000; a resampler that lost the passband # would land far below. The window is generous in both directions because the # exact figure depends on the filter's rolloff, and tight enough that neither # failure fits inside it. pcm2txt "$RS" "$w/rs-in.txt" pcm2txt "$w/rs1.raw" "$w/rs-out.txt" ppt=$(rms_ppt "$w/rs-in.txt" 0 -1 "$w/rs-out.txt" 0 -1) if [ "${ppt:--1}" -ge 780 ] && [ "$ppt" -le 960 ]; then bst_ok speexdsp/resample-band-limit else bst_notok speexdsp/resample-band-limit "out/in RMS = ${ppt}/1000 (want 780..960; native 870)" fi # A resampler that returned its input unchanged would satisfy determinism and # could be argued past the ratio; it cannot be the same bytes. if cmp -s "$RS" "$w/rs1.raw"; then bst_notok speexdsp/resample-not-copy "output is byte-identical to the input -- the resampler did not run" else bst_ok speexdsp/resample-not-copy fi fi # testresample2 drives 1271 resampler RECONFIGURATIONS (48 kHz -> 1000..128000 # Hz in 100 Hz steps) over an internally generated sine, exercising the # set_rate/ratio-change path the single-ratio case above never touches. Its # ~7 MB of float output is discarded: the assertion is its stderr trace, whose # every field is integer bookkeeping and therefore exact. speexdsp-testresample2 > /dev/null 2>"$w/rs2.err"; rc=$? if [ $rc -eq 0 ]; then bst_ok speexdsp/resample2-run else bst_notok speexdsp/resample2-run "rc=$rc $(_bst_1line "$(tail -3 "$w/rs2.err" 2>/dev/null)")"; fi check_eq speexdsp/resample2-trace-lines 1271 "$(wc -l < "$w/rs2.err" 2>/dev/null | tr -d ' ')" check_eq speexdsp/resample2-trace-first "1000 0 1024 22 -> 1024 0" \ "$(head -1 "$w/rs2.err" 2>/dev/null)" check_eq speexdsp/resample2-trace-last "128000 1024 1024 2731 -> 1024 2730" \ "$(tail -1 "$w/rs2.err" 2>/dev/null)" # ----------------------------------------------------------- preprocessor --- # testdenoise: speex_preprocess with DENOISE on, AGC and DEREVERB off, 8 kHz, # 160-sample blocks, in place. The fixture is 1 s of noise alone followed by 1 s # of a 500 Hz tone over the same noise floor. speexdsp-testdenoise < "$DN" > "$w/dn1.raw" 2>"$w/dn1.err"; rc=$? if [ $rc -eq 0 ]; then bst_ok speexdsp/denoise-run else bst_notok speexdsp/denoise-run "rc=$rc $(_bst_1line "$(cat "$w/dn1.err" 2>/dev/null)")"; fi dnbytes=$(wc -c < "$w/dn1.raw" 2>/dev/null | tr -d ' ') if [ -z "$dnbytes" ] || [ "$dnbytes" -lt 64 ]; then bst_notok speexdsp/denoise-output "preprocessor produced ${dnbytes:-0} bytes -- remaining preprocessor cases not run" else # The preprocessor filters in place and emits every block it read: 100 blocks # of 160 samples in, the same out, exactly. check_eq speexdsp/denoise-length 32000 "$dnbytes" speexdsp-testdenoise < "$DN" > "$w/dn2.raw" 2>/dev/null if [ -s "$w/dn1.raw" ] && cmp -s "$w/dn1.raw" "$w/dn2.raw"; then bst_ok speexdsp/denoise-deterministic else bst_notok speexdsp/denoise-deterministic "two preprocessor runs over one fixture differ" fi if cmp -s "$DN" "$w/dn1.raw"; then bst_notok speexdsp/denoise-modifies "output is byte-identical to the input -- the preprocessor did not run" else bst_ok speexdsp/denoise-modifies fi # THE SNR CASE. Plain RMS is NOT an oracle for a denoiser and asserting it # would be worse than asserting nothing: measured natively, this fixture's # noise-only half drops only 9% while the tone half drops 16%, so an RMS # reading says the denoiser attenuates SIGNAL harder than noise. What it # actually does is visible only once the tone and the noise are separated: # the 500 Hz tone-to-noise ratio goes 2816 -> 3270 ppt (8.99 -> 10.29 dB). # So the assertion is the RATIO improving, not any absolute level, which is # also the form that survives a different libm: both sides move together. pcm2txt "$DN" "$w/dn-in.txt" pcm2txt "$w/dn1.raw" "$w/dn-out.txt" set -- $(tone_noise "$w/dn-in.txt" 8000 16000) inamp=$(isqrt "${1:-0}"); intn=$(isqrt "${2:-0}") set -- $(tone_noise "$w/dn-out.txt" 8000 16000) outamp=$(isqrt "${1:-0}"); outtn=$(isqrt "${2:-0}") if [ "$intn" -gt 0 ] && [ "$outtn" -gt "$intn" ]; then bst_ok speexdsp/denoise-snr else bst_notok speexdsp/denoise-snr "500 Hz tone/noise ${intn} -> ${outtn} ppt (must increase; native 2816 -> 3270)" fi # ...and the tone itself must still be standing. A denoiser that improved the # ratio by deleting everything would pass the case above and fail this one. # Native: 8005 -> 6792, i.e. 848 ppt. if [ "$inamp" -gt 0 ] && [ $((1000 * outamp)) -ge $((600 * inamp)) ]; then bst_ok speexdsp/denoise-tone-survives else bst_notok speexdsp/denoise-tone-survives "500 Hz amplitude ${inamp} -> ${outamp} (want >= 60%; native 85%)" fi fi # ---------------------------------------------------- echo canceller (MDF) --- # testecho mic.sw speaker.sw out.sw -- speex_echo_cancellation over 128-sample # frames with a 1024-sample tail, followed by the preprocessor with the echo # state attached. The fixture's mic is the speaker signal through a fixed # two-tap room (0.60 at 200 samples, 0.25 at 350, both well inside the tail), # plus a 700 Hz near-end tone that starts exactly half way. So samples # 0..8000 are echo alone -- which is the only condition under which echo return # loss is measurable -- and 8000..16000 are echo plus near-end speech. speexdsp-testecho > "$w/ec-usage.err" 2>&1; rc=$? if [ $rc -ne 0 ]; then bst_ok speexdsp/echo-usage else bst_notok speexdsp/echo-usage "testecho with no arguments exited 0 instead of refusing"; fi check_out speexdsp/echo-usage-msg "mic_signal.sw speaker_signal.sw output.sw" cat "$w/ec-usage.err" speexdsp-testecho "$EM" "$ES" "$w/ec1.raw" 2>"$w/ec1.err"; rc=$? if [ $rc -eq 0 ]; then bst_ok speexdsp/echo-run else bst_notok speexdsp/echo-run "rc=$rc $(_bst_1line "$(cat "$w/ec1.err" 2>/dev/null)")"; fi ecbytes=$(wc -c < "$w/ec1.raw" 2>/dev/null | tr -d ' ') if [ -z "$ecbytes" ] || [ "$ecbytes" -lt 64 ]; then bst_notok speexdsp/echo-output "canceller produced ${ecbytes:-0} bytes -- remaining canceller cases not run" else # 125 frames of 128 samples in, 126 frames out: upstream's loop tests feof # only after a read, so the final short read still runs one cancellation over # the previous frame's buffers and writes it. That is upstream behaviour, it # is deterministic, and pinning it here is what would catch it CHANGING. check_eq speexdsp/echo-length 32256 "$ecbytes" speexdsp-testecho "$EM" "$ES" "$w/ec2.raw" 2>/dev/null if [ -s "$w/ec1.raw" ] && cmp -s "$w/ec1.raw" "$w/ec2.raw"; then bst_ok speexdsp/echo-deterministic else bst_notok speexdsp/echo-deterministic "two cancellation runs over one fixture pair differ" fi pcm2txt "$EM" "$w/ec-mic.txt" pcm2txt "$w/ec1.raw" "$w/ec-out.txt" # ECHO RETURN LOSS, measured over 4000..8000 -- echo-only, and past the # adaptive filter's convergence, which the native run showed complete well # before sample 4000. Residual was 33 ppt of the mic there (~29.5 dB of # cancellation). The bound is 200 ppt: 6x headroom over what was measured, # and a canceller that did nothing at all sits at 1000. ppt=$(rms_ppt "$w/ec-mic.txt" 4000 8000 "$w/ec-out.txt" 4000 8000) if [ "${ppt:--1}" -ge 0 ] && [ "$ppt" -le 200 ]; then bst_ok speexdsp/echo-erle else bst_notok speexdsp/echo-erle "residual/mic over the echo-only window = ${ppt}/1000 (want <= 200; native 33)" fi # ...and the near end must survive it. This is the case that fails for a # canceller that simply gates its output to silence, which is the cheapest # way to score a perfect ERLE. Native: 629 ppt of the mic retained. ppt=$(rms_ppt "$w/ec-mic.txt" 8000 16000 "$w/ec-out.txt" 8000 16000) if [ "${ppt:--1}" -ge 300 ]; then bst_ok speexdsp/echo-nearend-kept else bst_notok speexdsp/echo-nearend-kept "near-end half retained ${ppt}/1000 of the mic RMS (want >= 300; native 629)" fi # The two windows read against each other, which is the same claim stated # without reference to the mic at all: the canceller's output must be far # louder where there is near-end speech than where there is only echo. # Native: 2272 vs 80, a factor of 28. ppt=$(rms_ppt "$w/ec-out.txt" 4000 8000 "$w/ec-out.txt" 8000 16000) if [ "${ppt:--1}" -ge 4000 ]; then bst_ok speexdsp/echo-nearend-vs-echo else bst_notok speexdsp/echo-nearend-vs-echo "near-end/echo-window output RMS = ${ppt}/1000 (want >= 4000; native ~28000)" fi fi # ---------------------------------------------------------- jitter buffer --- # testjitter needs no input and no fixture: it builds its own 100-packet stream, # freezes the sender for 100 ticks, then resynchronises. It reports FAILURES by # printing them, and success by printing one line -- so both the presence of the # success line and the absence of any complaint have to be asserted. speexdsp-testjitter > "$w/jt.out" 2>"$w/jt.err"; rc=$? if [ $rc -eq 0 ]; then bst_ok speexdsp/jitter-run else bst_notok speexdsp/jitter-run "rc=$rc $(_bst_1line "$(cat "$w/jt.err" 2>/dev/null)")"; fi check_out speexdsp/jitter-resync "Frozen sender: Jitter 0" cat "$w/jt.out" if grep -qE "failed|Failed|expected" "$w/jt.out" 2>/dev/null; then bst_notok speexdsp/jitter-no-complaints "$(_bst_1line "$(grep -E 'failed|Failed|expected' "$w/jt.out" 2>/dev/null)")" else bst_ok speexdsp/jitter-no-complaints fi # ------------------------------------------------------------ what shipped --- for b in testresample testresample2 testdenoise testecho testjitter; do if [ -x "/usr/bin/speexdsp-$b" ]; then bst_ok "speexdsp/bin-$b" else bst_notok "speexdsp/bin-$b" "/usr/bin/speexdsp-$b is not on the image"; fi done # speexdsp-dev and speexdsp-staticdev are named in EMCRAFT_TOOLS so the archive # and the headers are on target for anything that wants to link them. if [ -f /usr/lib/libspeexdsp.a ]; then bst_ok speexdsp/archive else bst_notok speexdsp/archive "/usr/lib/libspeexdsp.a is not on the image"; fi for h in speex_resampler.h speex_echo.h speex_preprocess.h speex_jitter.h speexdsp_types.h; do if [ -f "/usr/include/speex/$h" ]; then bst_ok "speexdsp/header-$h" else bst_notok "speexdsp/header-$h" "/usr/include/speex/$h is not on the image"; fi done # No shared object on a distro that configures --disable-shared image-wide. One # appearing here would mean the static-only rule quietly stopped holding. if [ -e /usr/lib/libspeexdsp.so ] || [ -e /usr/lib/libspeexdsp.so.1 ]; then bst_notok speexdsp/static-only "a shared libspeexdsp shipped on a --disable-shared distro" else bst_ok speexdsp/static-only fi rm -rf "$w"