An LFO belongs to its channel, not to the whole device

The two LFOs lived in the Synth, so four channels shared them and whichever patch
loaded last owned them for every voice at once. A sound with its LFO switched off
silenced the trill under a sound that was still playing - which is what made Lunar
Porter's low fuel warning intermittent: the first landing, docking or crash of a run
took its trill away, and it was right again next time the machine started.

The engine fix went upstream to soundThing and has come back. synth.c and synth.h
are re-vendored at 71e3cb2, character for character bar the ASCII transliteration,
and now carry two changes: the LFOs moved into the Voice, and synthSyncVoices
carries a free LFO's cycle down alongside its rate. That second hunk does nothing
here - it only matters to a caller that syncs voices, and this device never does,
because syncing would flatten four channels into one instrument. It is taken so the
vendored file stays identical in both trees, and it is commented as such.

Upstream also found a bug in the original patch, in patchLoad, which is soundThing's
own file and does not travel.

Downstream the LFO parameter groups 0x60 and 0x70 now read the selected channel like
every parameter beside them, so an LFO written to one channel is inaudible on the
other three. Everything else about the device is unchanged.

Lunar Porter keeps loading each patch immediately before its note, but for the
smaller reason that now applies: the bang and the latch share channel three, and a
channel used by two sounds has to be told which of them it is about to be. The
comment that said otherwise, and the manual's warning about sharing, are rewritten
as history rather than as a caveat.

Tests/sound.sh's shared-LFO check is inverted to assert the fixed behaviour, with a
third leg added: after proving another channel's patch leaves this one alone, it
switches this channel's OWN LFO off and requires the pitch to move. Without that,
both checks would pass on a device where writing an LFO did nothing at all. Routing
either group back to voice 0 is caught.

Cost, measured: four channels sounding continuously for 400 seconds of audio takes
5.0 s of wall clock against 4.59 s before, about 9% of total emulator time. Half of
that is wasted on voices that cannot sound, since VOICE_COUNT is 8 and there are
four channels; recovering it would mean diverging the vendored file, which is not
worth it at this price.
This commit is contained in:
Anachronaut
2026-09-04 22:20:26 -04:00
parent 3ca5f193e6
commit d361ea1e46
7 changed files with 184 additions and 104 deletions
+23 -22
View File
@@ -496,20 +496,19 @@ PY
&& 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"
# ---- An LFO belongs to the device, and a patch carries LFO settings ----
# ---- An LFO belongs to its CHANNEL, and a patch carries LFO settings ----
#
# Which is a trap with teeth, and it bit a game before it was written down here. Four voices
# share two LFOs, so a patch loaded onto channel ONE re-tunes what channel NOUGHT hears - and
# a program that sets its instruments up once at startup gets whichever of them was written
# last, for all of them.
# This used to be the other way round, and it was a trap with teeth: four voices shared two
# LFOs, so a patch loaded onto channel ONE re-tuned what channel NOUGHT heard. The symptom was
# the worst kind - a sound that is right until some unrelated thing plays, and right again
# next time the machine starts. In the game it was a warning whose trill vanished after the
# first landing of a run, because the landing's patch carries an LFO switched off.
#
# The symptom is the worst kind: a sound that is right until some unrelated thing plays, and
# right again next time the machine starts. In the game it was a warning whose trill vanished
# after the first landing of a run, because the landing's patch carries an LFO switched off.
#
# Three notes on channel nought, all identical in what THEY were told. Between the first and
# second, a patch is dropped on channel one that switches the LFO off; between the second and
# third, channel nought's own settings are written again.
# Three notes on channel nought, all identical in what THEY were told, and the third leg is
# what keeps this check honest. Between the first and second, a patch is dropped on channel
# ONE that switches the LFO off - which must now do nothing here. Between the second and third
# the same thing is written to channel NOUGHT itself - which must still work, or the two
# checks above would pass just as well on a device where writing an LFO does nothing at all.
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud
param 0x00 1 # triangle, so the LFO's work on the pitch is plain
param 0x51 1 # triggered, so each note ends itself
@@ -524,8 +523,8 @@ PY
port 0x41 0x00
port 0x44 60; pause two 250
port 0x41 0x00 # and its own settings said again, which is the fix
param 0x60 0x01; param 0x61 2; param 0x62 40; param 0x63 0x01
port 0x41 0x00 # and now its OWN LFO switched off, which must be heard
param 0x60 0x00
port 0x44 60; spinForever
printf '#Vectors\n Boot start\n'; } | run lfoshared 8000000 || exit 1
read -r FIRST SECOND THIRD <<EOT
@@ -568,14 +567,16 @@ EOT
[ "$FIRST" -gt 0 ] && [ "$SECOND" -gt 0 ] && [ "$THIRD" -gt 0 ] \
&& result ok "an LFO bends the pitch it is routed to" "$FIRST hertz against the 262 asked for" \
|| result no "an LFO bends the pitch it is routed to" "$FIRST $SECOND $THIRD"
# Switched off by a patch meant for ANOTHER CHANNEL, and the note comes back unbent.
[ "$(( SECOND - FIRST ))" -gt 40 ] \
&& result ok "and another channel's patch takes it away" "$FIRST became $SECOND, which is the note itself" \
|| result no "and another channel's patch takes it away" "$FIRST then $SECOND, so it was not shared"
# And said again, it is bent exactly as before, because a retriggered LFO starts where it did.
[ "$FIRST" = "$THIRD" ] \
&& result ok "and saying it again gets it back" "$SECOND became $THIRD again" \
|| result no "and saying it again gets it back" "$SECOND then $THIRD"
# Switched off by a patch meant for ANOTHER CHANNEL, and nothing here changes. This is the
# whole point: the two used to share, and a sound could not protect itself from its neighbour.
[ "$FIRST" = "$SECOND" ] \
&& result ok "another channel's patch leaves it alone" "$FIRST hertz before and after" \
|| result no "another channel's patch leaves it alone" "$FIRST became $SECOND, so they are shared"
# Switched off on its OWN channel, and the note comes back unbent - so the check above is
# measuring an LFO that really is reachable, rather than one nothing can write to.
[ "$(( THIRD - FIRST ))" -gt 40 ] \
&& result ok "and its own channel's patch does take it away" "$FIRST became $THIRD, which is the note itself" \
|| result no "and its own channel's patch does take it away" "$FIRST then $THIRD"
echo
if [ "$FAIL" -eq 0 ]; then