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.
This commit is contained in:
@@ -409,6 +409,93 @@ LATE="$(measure levelEnvelope peak 36000)"
|
||||
&& 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."
|
||||
|
||||
Reference in New Issue
Block a user