Files
SplitBit-Emulator/Tests/sound.sh
T
Anachronaut b2ff8d64e5 Fold soundThing's changes back down, and expose the two new switches
The three changes that went up came back as part of soundThing, along
with two more that they made possible. The engine here is now b73e5c0
character for character, except that em-dashes and arrows in comments are
written as ASCII because this tree is ASCII only - a local rule, not an
improvement, and not sent up.

So synth.h's "what was changed" list is gone. There is nothing to list:
what has to be kept current is only that if either copy changes, the
other one has to be told.

---- What came back ----

A VOICE CAN END ITSELF. Naming the level's source said what shapes a
voice; nothing said what ends one, so the only thing that could ever
finish one was a key coming up. A game is nearly all one-shots and not
one of them wants its length decided by how long a note was held. Exposed
as parameter 0x51: 0 gated, 1 triggered.

AND A ONE-SHOT IS THE SAME ONE-SHOT TWICE. A triggered voice re-arms its
oscillators, and an LFO can be told to start over with each voice -
parameter 3 of either LFO. Both halves are needed and the check proves
it: with the LFO left free, two triggered hits still differ.

Their note warned that whatever applies a patch to a channel has to set
these or they hold synthInit's defaults. Checked: Voyager never calls
synthSyncVoices, so their 0001 is a no-op here as they predicted, and
nothing reaches into an LFO's phase, so the struct split is safe.

---- What it is for ----

Lander's crash is a triggered voice now, so boomOff is gone. Nothing has
to remember to end a bang.

SoundPatch learnt voice_levelSource, voice_gate and lfo<N>_mode, which
the new soundThing writes - without that it would have refused every
patch saved from it, since an unknown field stops the tool on purpose. A
patch from before those fields still converts, and says in its own
comments that it predates the level routing.

Three checks, each seen to fail on its own break: a gated voice still
sounding with nothing holding it, a triggered one down to nothing with no
gate ever dropped, and two hits identical sample for sample.

One test bug worth keeping: the first version of the repeatability check
struck the second note while the first was still ringing, so what it
found and compared as "the second hit" was a point in the middle of the
first one's tail. It now looks for sound after SILENCE rather than sound
after an offset.
2026-09-04 16:02:40 -04:00

506 lines
23 KiB
Bash
Executable File

#!/usr/bin/env bash
# Checks what the sound device actually makes.
#
# THE SUITE HAS NO SPEAKER, and a sound nothing can hear is a sound nothing checks. So the
# device makes its samples against the machine's clock rather than the host's, and the machine
# can be asked to save them with --sound. Every check below runs a program for a fixed number
# of cycles, saves the samples and measures them - no audio hardware, no timing luck, and the
# same answer every time.
#
# This is the same argument as Tests/video.sh, and it has the same consequence: each check is
# a named claim about one behaviour rather than a comparison against a recorded waveform. A
# recorded waveform would say "it sounds different" and leave which of the oscillator, the
# envelope, the filter, the channel selector or the sample clock broke to be found by ear.
#
# Written by Anachronaut
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BUILD="$ROOT/Tests/build/sound"
ASM="$ROOT/Assembler"
EMU="$ROOT/SplitBit"
for tool in "$ASM" "$EMU"; do
[ -x "$tool" ] || { echo "$(basename "$tool") is not built."; exit 1; }
done
rm -rf "$BUILD"; mkdir -p "$BUILD"
PASS=0
FAIL=0
FAILED_NAMES=()
GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m'
[ -t 1 ] || { GREEN=""; RED=""; RESET=""; }
result() {
# result <ok|no> <name> <detail>
if [ "$1" = "ok" ]; then
PASS=$((PASS + 1)); printf " [%sok %s] %-40s %s\n" "$GREEN" "$RESET" "$2" "$3"
else
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$2")
printf " [%sFAIL%s] %-40s %s\n" "$RED" "$RESET" "$2" "$3"
fi
}
# ---- Writing to the device from a program ----
port() {
# port <port> <byte>
printf ' INIA 0x%02X\n OUTA 0x%02X\n' $(( $2 & 0xFF )) $(( $1 & 0xFF ))
}
param() {
# param <parameter> <value> - the selector and value pair, which is three writes.
printf ' INIA 0x%02X\n OUTA 0x42\n INIA 0x%02X\n OUTA 0x43\n' \
$(( $1 & 0xFF )) $(( $2 & 0xFF ))
}
# The one write a sounding program cannot skip: an oscillator arrives silent.
loud() {
param 0x01 0xFF
}
# About a tenth of a second each hundred, in cycles: the inner loop is a DECA and a BNA, which
# is four cycles, 256 times round. The tag is so that more than one can sit in a program.
pause() {
# pause <tag> <hundredths>
printf ' INIB 0d%d\npauseOuter%s:\n RSTA\npauseInner%s:\n DECA\n BNA pauseInner%s\n DECB\n BNB pauseOuter%s\n' \
"$2" "$1" "$1" "$1" "$1"
}
# Says whether anything is sounding, as a letter, because the status bit is not a character
# and a test reads the console.
sounding() {
# sounding <tag>
printf ' INA 0x40\n INIB 0x01\n AND\n BRQ quiet%s\n INIA 0d89\n OUTA 0x00\n BRI after%s\nquiet%s:\n INIA 0d78\n OUTA 0x00\nafter%s:\n' \
"$1" "$1" "$1" "$1"
}
# Most programs here never finish on purpose: they set a sound going and then spin, and the
# cycle limit decides how long was recorded. That makes the sample count exact rather than
# dependent on how long the program took to get there.
spinForever() {
printf 'spinEnd:\n BRI spinEnd\n'
}
epilogue() {
printf ' HALT\n#Vectors\n Boot start\n'
}
# Assembles what is on standard input, runs it for a fixed number of cycles, and leaves the
# samples in $BUILD/<name>.raw.
run() {
# run <name> <cycles>
local name="$1"
cat > "$BUILD/$name.asm"
"$ASM" "$BUILD/$name.asm" -o "$BUILD/$name.bin" >"$BUILD/$name.log" 2>&1 || {
echo "could not assemble $name"; sed 's/^/ /' "$BUILD/$name.log"; return 1; }
timeout 20 "$EMU" --fast --cycles "$2" --sound "$BUILD/$name.raw" "$BUILD/$name.bin" \
> "$BUILD/$name.out" 2>&1
if [ $? -eq 124 ]; then
echo " $name did not finish within twenty seconds"; return 1
fi
return 0
}
# ---- Reading the samples back ----
#
# Raw signed 16 bit, little endian, which is what --sound writes.
measure() {
# measure <name> <what> [from] [to] - samples are given in samples, not seconds.
python3 - "$BUILD/$1.raw" "$2" "${3:-0}" "${4:-0}" <<'PY'
import struct, sys
data = open(sys.argv[1], "rb").read()
count = len(data) // 2
values = struct.unpack("<%dh" % count, data)
what = sys.argv[2]
start = int(sys.argv[3])
stop = int(sys.argv[4]) or count
window = values[start:stop]
if what == "count":
print(count)
elif what == "peak":
print(max((abs(v) for v in window), default=0))
elif what == "pitch":
# ---- Counted with hysteresis, not on the sign ----
#
# A waveform fading through nothing crosses zero many times on its way, and counting
# every sign change would hear a decaying note as a very high one. So a crossing is only
# counted after the signal has been convincingly on one side: above a tenth of the peak,
# then below minus a tenth.
peak = max((abs(v) for v in window), default=0)
if peak == 0:
print("0.0")
else:
gate = peak // 10
crossings, side = 0, 0
for v in window:
if side <= 0 and v > gate:
side = 1
crossings += 1
elif side >= 0 and v < -gate:
side = -1
crossings += 1
print("%.1f" % (crossings / 2.0 / (len(window) / 48000.0)))
PY
}
# A number against another, within a percentage of it.
near() {
# near <value> <wanted> <percent>
python3 -c "import sys; a,b,p = (float(x) for x in sys.argv[1:4]); sys.exit(0 if b and abs(a-b) <= b*p/100.0 else 1)" \
"$1" "$2" "$3"
}
said() {
grep -q -- "$2" "$BUILD/$1.out"
}
echo "Checking what the sound device makes."
echo
# ---- Nothing, until something asks ----
#
# A machine that hummed on its own would make every check below meaningless, and the samples
# would still all be there to count.
{ printf '#Program\nstart:\n'; spinForever; printf '#Vectors\n Boot start\n'; } \
| run quiet 1000000 || exit 1
PEAK="$(measure quiet peak)"
[ "$PEAK" = "0" ] \
&& result ok "silent until something asks" "every sample is nothing" \
|| result no "silent until something asks" "peak was $PEAK"
# ---- The sample clock is the machine's clock ----
#
# Forty-eight thousand a second against a million cycles. This is the claim the whole file
# rests on: if samples came from the host, nothing below would be reproducible.
COUNT="$(measure quiet count)"
[ "$COUNT" = "48000" ] \
&& result ok "samples keep the machine's time" "1,000,000 cycles made exactly 48,000" \
|| result no "samples keep the machine's time" "1,000,000 cycles made $COUNT, wanted 48,000"
{ printf '#Program\nstart:\n'; spinForever; printf '#Vectors\n Boot start\n'; } \
| run quietLonger 3000000 || exit 1
COUNT="$(measure quietLonger count)"
[ "$COUNT" = "144000" ] \
&& result ok "and keeps it over a longer run" "3,000,000 cycles made exactly 144,000" \
|| result no "and keeps it over a longer run" "3,000,000 cycles made $COUNT, wanted 144,000"
# ---- A channel arrives able to make a sound ----
#
# Writing a note number and hearing that note is the shortest useful thing this device can be
# asked to do, and it works from a cold start. soundThing's own defaults do not do this - they
# are a patch editor's, where one voice is set up and the rest wait to be copied over - so the
# device sets its own power-on state and this is the check that it did.
{ printf '#Program\nstart:\n'; port 0x41 0x00; port 0x44 60; spinForever
printf '#Vectors\n Boot start\n'; } | run bareNote 1000000 || exit 1
PEAK="$(measure bareNote peak)"
[ "$PEAK" -gt 1000 ] \
&& result ok "a channel arrives ready to sound" "a note and nothing else made $PEAK" \
|| result no "a channel arrives ready to sound" "peak was only $PEAK"
# ---- And the second oscillator arrives off ----
#
# Which is not the same thing as silent, because the two are averaged rather than added: one
# that is switched on halves the other whatever its gain. So the plain case is one oscillator,
# and asking for two is a thing a program says out loud.
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud
param 0x15 0x01 # oscillator 1, switched on
param 0x11 0x00 # and silent
port 0x44 60; spinForever; printf '#Vectors\n Boot start\n'; } | run twoOscs 1000000 || exit 1
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud; port 0x44 60; spinForever
printf '#Vectors\n Boot start\n'; } | run middleC 1000000 || exit 1
BOTH="$(measure twoOscs peak)"
ONE="$(measure middleC peak)"
near "$BOTH" "$((ONE / 2))" 10 \
&& result ok "two oscillators share the level" "$ONE alone, $BOTH with a silent one beside it" \
|| result no "two oscillators share the level" "$ONE alone and $BOTH with a second, wanted about half"
PEAK="$(measure middleC peak)"
[ "$PEAK" -gt 1000 ] \
&& result ok "a note makes a sound" "peak $PEAK" \
|| result no "a note makes a sound" "peak was only $PEAK"
# ---- The note it was asked for ----
#
# 60 is middle C, 261.63 Hz. Two per cent is well inside a semitone, which is six.
PITCH="$(measure middleC pitch 24000)"
near "$PITCH" 261.63 2 \
&& result ok "and it is the note asked for" "$PITCH Hz, middle C is 261.63" \
|| result no "and it is the note asked for" "$PITCH Hz, wanted 261.63"
# ---- Twelve is an octave ----
#
# The pitch check above would pass on a device that played one fixed tone. This one would not.
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud; port 0x44 72; spinForever
printf '#Vectors\n Boot start\n'; } | run octaveUp 1000000 || exit 1
OCTAVE="$(measure octaveUp pitch 24000)"
near "$OCTAVE" 523.25 2 \
&& result ok "twelve notes up is an octave" "$OCTAVE Hz, twice $PITCH" \
|| result no "twelve notes up is an octave" "$OCTAVE Hz, wanted 523.25"
# ---- The same program makes the same sound ----
#
# The point of a clock that is not the host's. Without this every check above is a check on
# how busy the machine running the suite happened to be.
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud; port 0x44 60; spinForever
printf '#Vectors\n Boot start\n'; } | run middleCAgain 1000000 || exit 1
cmp -s "$BUILD/middleC.raw" "$BUILD/middleCAgain.raw" \
&& result ok "the same program makes the same sound" "two runs, byte for byte" \
|| result no "the same program makes the same sound" "the two runs differ"
# ---- Gain is a level and not a switch ----
{ printf '#Program\nstart:\n'; port 0x41 0x00; param 0x01 0x40; port 0x44 60; spinForever
printf '#Vectors\n Boot start\n'; } | run quarterGain 1000000 || exit 1
FULL="$(measure middleC peak)"
QUARTER="$(measure quarterGain peak)"
near "$QUARTER" "$((FULL / 4))" 20 \
&& result ok "gain sets the level" "a quarter of $FULL is $QUARTER" \
|| result no "gain sets the level" "a quarter of $FULL came out $QUARTER"
# ---- And the device's own volume is over the top of it ----
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud; port 0x46 0x00; port 0x44 60
spinForever; printf '#Vectors\n Boot start\n'; } | run noVolume 1000000 || exit 1
PEAK="$(measure noVolume peak)"
[ "$PEAK" = "0" ] \
&& result ok "volume nothing is silence" "a full note at volume nothing makes nothing" \
|| result no "volume nothing is silence" "peak was $PEAK"
# ---- A channel is its own voice ----
#
# Turn channel 0's gain down, then play on channel 1. Channel 1 should still be at the gain it
# arrived with, and anything else would mean the settings are the device's rather than the
# channel's - the difference between four voices and one that four things fight over.
#
# This is deliberately a comparison of LEVELS and not a check that channel 1 is silent. It was
# the latter once, and it passed for a year's worth of the wrong reason: channel 1 could not
# make a sound at all, so a device with one shared set of settings would have passed it too.
{ printf '#Program\nstart:\n'; port 0x41 0x00; param 0x01 0x40; port 0x41 0x01; port 0x44 60
spinForever; printf '#Vectors\n Boot start\n'; } | run otherChannel 1000000 || exit 1
QUIETED="$(measure quarterGain peak)"
OTHER="$(measure otherChannel peak)"
near "$OTHER" "$FULL" 5 \
&& result ok "a channel keeps its own settings" "channel 0 turned down to $QUIETED, channel 1 still $OTHER" \
|| result no "a channel keeps its own settings" "channel 1 came out $OTHER, channel 0's own gain gives $QUIETED"
# ---- All four of them are there ----
{ printf '#Program\nstart:\n'; port 0x41 0x03; port 0x44 60; spinForever
printf '#Vectors\n Boot start\n'; } | run lastChannel 1000000 || exit 1
PEAK="$(measure lastChannel peak)"
[ "$PEAK" -gt 1000 ] \
&& result ok "the fourth channel is a channel" "peak $PEAK" \
|| result no "the fourth channel is a channel" "peak was only $PEAK"
# ---- Asking for a channel that is not there ----
#
# It wraps rather than faulting or writing past the end of the voices. A sound device is a
# poor place to stop the machine, and a poor place to corrupt memory.
{ printf '#Program\nstart:\n'; port 0x41 6
printf ' INA 0x41\n INIB 0d48\n CCF\n ADD\n OUTQ 0x00\n'
epilogue; } | run wrapChannel 200000 || exit 1
said wrapChannel "2" \
&& result ok "a channel number wraps" "6 selected channel 2" \
|| result no "a channel number wraps" "$(cat "$BUILD/wrapChannel.out")"
# ---- The registers read back ----
#
# So a handler can save the selection and put it back, which it has to, since an interrupt in
# the middle of a patch load would otherwise land the rest of the patch on another channel.
{ printf '#Program\nstart:\n'; port 0x42 0x2A
printf ' INA 0x42\n OUTA 0x00\n'; epilogue; } | run readBack 200000 || exit 1
said readBack '\*' \
&& result ok "the selectors read back" "0x2A came back as itself" \
|| result no "the selectors read back" "$(od -c "$BUILD/readBack.out" | head -1)"
# ---- Status: nothing is sounding until something is ----
{ printf '#Program\nstart:\n'; sounding Before
port 0x41 0x00; loud; port 0x44 60; sounding After; epilogue; } \
| run statusBit 500000 || exit 1
said statusBit "NY" \
&& result ok "status says what is sounding" "nothing before the note, something after" \
|| result no "status says what is sounding" "said $(cat "$BUILD/statusBit.out")"
# ---- Not even a sustain of nothing ends a note ----
#
# THE TRAP, and it caught the person writing this device before it caught anybody else. A
# sustain of nothing is silent, and silence looks exactly like a finished note, so the obvious
# way to play a note and wait for it is to give it no sustain and watch the status bit. It
# never comes down: the voice is holding at nothing, which is a thing a held key does.
#
# There is one rule and this is the check that there are not two. A NOTE SOUNDS UNTIL IT IS
# GATED OFF. What the envelope is doing does not enter into it.
{ printf '#Program\nstart:\n'; port 0x41 0x00
param 0x20 0x00 # attack: none
param 0x21 40 # decay: short
param 0x22 0x00 # sustain: nothing at all
port 0x44 60
printf 'waitOut:\n INA 0x40\n INIB 0x01\n AND\n BNQ waitOut\n'
printf ' INIA 0d90\n OUTA 0x00\n' # 'Z', reached only if the bit came down
epilogue; } | run silentSustain 3000000 || exit 1
said silentSustain "Z" \
&& result no "silence is not the end of a note" "the bit came down without a gate off" \
|| result ok "silence is not the end of a note" "silent for three million cycles and still sounding"
# And the sound really did go quiet, so the check above is about the status bit rather than
# about an envelope that never decayed.
PEAK="$(measure silentSustain peak 24000)"
[ "$PEAK" -lt 100 ] \
&& result ok "even though there is nothing to hear" "faded to $PEAK while still sounding" \
|| result no "even though there is nothing to hear" "still $PEAK, it never decayed"
# ---- A note that is held is not one ----
#
# The trap the manual warns about: a note with sustain sounds until the gate is dropped, so a
# program that waits for it waits for ever. Here the wait is bounded by the cycle limit, and
# the marker not being printed is the whole point.
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud
param 0x22 0xFF # sustain: all of it
port 0x44 60
printf 'held:\n INA 0x40\n INIB 0x01\n AND\n BNQ held\n'
printf ' INIA 0d90\n OUTA 0x00\n'
epilogue; } | run heldNote 3000000 || exit 1
said heldNote "Z" \
&& result no "a held note keeps sounding" "the status bit came down on its own" \
|| result ok "a held note keeps sounding" "still sounding after three million cycles"
# ---- And dropping the gate is what ends it ----
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud
param 0x22 0xFF # sustain: all of it
param 0x23 20 # release: short
port 0x44 60
pause A 100
port 0x45 0x00 # let go
printf 'released:\n INA 0x40\n INIB 0x01\n AND\n BNQ released\n'
printf ' INIA 0d90\n OUTA 0x00\n'
epilogue; } | run gateOff 3000000 || exit 1
said gateOff "Z" \
&& result ok "dropping the gate ends it" "the note released and the bit came down" \
|| result no "dropping the gate ends it" "it was still sounding at the cycle limit"
# ---- The level can be shaped by nothing at all ----
#
# soundThing welded the first envelope to the output, so an envelope spent on a filter sweep
# still had to be shaped like something worth hearing. Setting 0x50 to nothing is what unwelds
# it. The check is that an amplitude envelope which decays to silence immediately does NOT
# silence a channel whose level nothing shapes.
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud
param 0x20 0x00 # attack: none
param 0x21 20 # decay: very short
param 0x22 0x00 # sustain: nothing, so the envelope is at zero
param 0x50 0x00 # and nothing shapes the level
port 0x44 60
spinForever; printf '#Vectors\n Boot start\n'; } | run levelSource 1000000 || exit 1
LATE="$(measure levelSource peak 36000)"
[ "$LATE" -gt 1000 ] \
&& result ok "the level can be shaped by nothing" "still $LATE long after the envelope let go" \
|| result no "the level can be shaped by nothing" "peak $LATE, the envelope silenced it anyway"
# And the same patch with the envelope back on the level is silent by then, which is what
# makes the check above about the setting rather than about the envelope not working.
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud
param 0x20 0x00; param 0x21 20; param 0x22 0x00
param 0x50 1 # the amplitude envelope, which is the normal case
port 0x44 60
spinForever; printf '#Vectors\n Boot start\n'; } | run levelEnvelope 1000000 || exit 1
LATE="$(measure levelEnvelope peak 36000)"
[ "$LATE" -lt 100 ] \
&& result ok "and by the envelope, which is the normal case" "faded to $LATE" \
|| result no "and by the envelope, which is the normal case" "still $LATE, it never faded"
# ---- A triggered voice ends itself ----
#
# Gated is what a keyboard wants: the sound lasts as long as something holds it, and dropping
# the gate is what starts the release. A GAME is nearly all one-shots - a bang, a pickup, a
# door - and not one of them wants its length decided by how long a note was held.
#
# Neither program below ever drops the gate. The gated one is still sounding at the end of it,
# because a sustain above nothing is a voice waiting for a key that is never coming; the
# triggered one runs its decay to nothing and finishes.
for gate in 0 1; do
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud
param 0x51 $gate # gated or triggered
param 0x20 0x00 # attack: instant
param 0x21 60 # decay: short
param 0x22 128 # sustain: half, which a gated voice will hold at
port 0x44 60; spinForever
printf '#Vectors\n Boot start\n'; } | run gate$gate 3000000 || exit 1
done
HELD="$(measure gate0 peak 96000)"
STRUCK="$(measure gate1 peak 96000)"
[ "$HELD" -gt 1000 ] \
&& result ok "a gated voice waits to be let go of" "still $HELD with nothing holding it" \
|| result no "a gated voice waits to be let go of" "faded to $HELD on its own"
[ "$STRUCK" -lt 100 ] \
&& result ok "and a triggered one ends itself" "down to $STRUCK, with no gate ever dropped" \
|| result no "and a triggered one ends itself" "still $STRUCK, so it is waiting for a key"
# ---- And it is the same one-shot twice ----
#
# The point of the whole thing. A triggered voice re-arms its oscillators, so a hit begins in
# the same place every time; a free LFO does not, and reading it at a different moment makes
# the same drum a different drum. Both halves are needed, which is why this asks for the LFO
# to restart as well.
#
# Two hits, compared SAMPLE FOR SAMPLE. Noise is the hard case and the reason the generator is
# seeded: with rand() this could not have been asked at all.
#
# THE GAP HAS TO OUTLAST THE DECAY. The first version struck the second note while the first
# was still ringing, so what it found and compared as "the second hit" was a point in the
# middle of the first one's tail. A short decay and a quarter second between them, and the two
# are found by looking for sound after silence rather than for sound after an offset.
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud
param 0x00 5 # noise
param 0x51 0x01 # triggered
param 0x20 0x00; param 0x21 40; param 0x22 0x00
param 0x60 0x01 # LFO 0 on,
param 0x62 40 # a slow rate,
param 0x63 0x01 # and it starts over with each voice
param 0x44 0x03 # which opens the filter
param 0x40 0x01; param 0x42 60; param 0x45 200
port 0x44 60; pause one 250
port 0x44 60; spinForever
printf '#Vectors\n Boot start\n'; } | run twice 4000000 || exit 1
SAME="$(python3 - "$BUILD/twice.raw" <<'PY'
import struct, sys
data = open(sys.argv[1], "rb").read()
v = struct.unpack("<%dh" % (len(data) // 2), data)
# Find the two hits by where sound starts, then compare the same span of each.
def onsets(v, floor=200, quiet=2000):
# Sound after silence. A run of quiet samples has to separate two hits, or what is found
# is one hit and a place in the middle of its own tail.
at, run, sounding = [], 0, False
for i, x in enumerate(v):
if abs(x) > floor:
if not sounding:
at.append(i)
sounding = True
run = 0
elif sounding:
run += 1
if run >= quiet:
sounding = False
return at
hits = onsets(v)
if len(hits) < 2:
print("onehit" if hits else "silent"); raise SystemExit
first, second = hits[0], hits[1]
span = min(3000, len(v) - second)
a = v[first:first + span]
b = v[second:second + span]
print("same" if a == b else "differ")
PY
)"
[ "$SAME" = "same" ] \
&& result ok "and two hits are the same hit" "sample for sample, noise and all" \
|| result no "and two hits are the same hit" "$SAME"
echo
if [ "$FAIL" -eq 0 ]; then
echo "All $PASS sound checks passed."
exit 0
fi
echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}"
exit 1