The warning's trill, and why it only sometimes came out
An LFO belongs to the DEVICE and not to a channel. There are two of them against four voices, and a patch carries LFO settings the way it carries everything else - so whichever patch was loaded last owns both of them, for every voice at once. The warning's trill is a saw LFO on the pitch. The patches on channel three, the bang and the latch, carry an LFO that is switched off, and they load at the moment they are used. So the first landing, docking or crash of a run took the trill away and left a plain tone, and it was right again next time the machine started. Correct until something unrelated plays is the worst shape a fault can have. The same thing had already happened silently at startup: the instruments were set up in order, so the warning's LFO settings, written last, sat on top of the rumble's and the rumble never had its own at all. So nothing is set up once any more. Each sound loads its patch immediately before its note - forty-odd writes at a moment already making a sound - and is then whatever its patch says, whatever played before it. Measured after a landing: 1056, 660, 516 hertz, then up to 1698 and down again. Two sweeps of the saw, which is the trill. What it does not fix, because it cannot: two sounds overlapping still share the LFOs, so a warning going off mid-burn re-tunes the rumble for as long as it lasts. With two between four that is the device. Three checks at the device level, where the trap can be stated exactly: a routed LFO bends a pitch (184 Hz against the 262 the note asked for), another channel's patch takes it away (272, the note itself), and saying it again gets it back (184). Written up in the Programming Manual beside the LFO mode, since the next program to want two sounds will meet it too.
This commit is contained in:
@@ -80,8 +80,6 @@ start:
|
||||
CALL putLander
|
||||
CALL putGauge
|
||||
CALL putOrbital
|
||||
CALL putThrust ; The instruments, once, the way the tiles are.
|
||||
CALL putAlarm
|
||||
|
||||
INIA 0x01
|
||||
OUTA 0x02 ; Key mode.
|
||||
@@ -2796,20 +2794,28 @@ runPend:
|
||||
BNA runPend
|
||||
RET
|
||||
|
||||
; ---- The thruster, which is a bang and then a rumble ----
|
||||
; ---- Why every sound loads its patch at the moment it plays ----
|
||||
;
|
||||
; Both on their own channels because one is struck and the other is HELD: the rumble's note
|
||||
; goes on when a thruster lights and its gate does not come up until every thruster is out.
|
||||
putThrust:
|
||||
RSTA
|
||||
OUTA 0x41
|
||||
SETD.0 ThrustOnPatch
|
||||
CALL playPatch
|
||||
INIA 0x01
|
||||
OUTA 0x41
|
||||
SETD.0 ThrustHoldPatch
|
||||
CALL playPatch
|
||||
RET
|
||||
; THE LFOs BELONG TO THE DEVICE AND NOT TO A CHANNEL. There are two of them against four
|
||||
; voices, and a patch carries LFO settings the way it carries everything else - so whichever
|
||||
; patch was loaded last owns them, for every channel at once.
|
||||
;
|
||||
; That is what made the low fuel warning sound right sometimes and not others. Its trill is a
|
||||
; saw LFO on the pitch, and the patches on channel three - the bang and the latch - carry an
|
||||
; LFO that is switched OFF. Those load at the moment they are used, so the first landing,
|
||||
; docking or crash of a run took the warning's trill away and left a plain tone. Before any of
|
||||
; them it was right, which is exactly the shape an intermittent fault has.
|
||||
;
|
||||
; It also meant the rumble never had its own LFO at all: the instruments were set up in order
|
||||
; at startup and the warning's settings, written last, sat on top of the rumble's.
|
||||
;
|
||||
; So nothing is set up once any more. Each sound loads immediately before its note, which
|
||||
; costs fifty writes at a moment already doing more than that, and means a sound is what its
|
||||
; patch says whatever played before it.
|
||||
;
|
||||
; What that does not fix, because it cannot: two sounds overlapping still share one pair of
|
||||
; LFOs, so a warning going off mid-burn re-tunes the rumble's LFO for as long as it lasts.
|
||||
; With two between four, that is in the nature of the device.
|
||||
|
||||
; ---- Lit or not, and only the CHANGES matter ----
|
||||
;
|
||||
@@ -2852,6 +2858,8 @@ thrustHaveSet:
|
||||
BRQ thrustNoPop ; Nothing newly lit, whatever else is still burning.
|
||||
RSTA
|
||||
OUTA 0x41
|
||||
SETD.0 ThrustOnPatch
|
||||
CALL playPatch
|
||||
INIA 0d36
|
||||
OUTA 0x44 ; The bang of it catching.
|
||||
thrustNoPop:
|
||||
@@ -2870,6 +2878,8 @@ thrustNoPop:
|
||||
INIA 0x01
|
||||
STA.0
|
||||
OUTA 0x41
|
||||
SETD.0 ThrustHoldPatch
|
||||
CALL playPatch ; After the pop's, so the held sound owns the LFO while it runs.
|
||||
INIA 0d60
|
||||
OUTA 0x44 ; Held, until the gate comes up.
|
||||
BRI thrustRemember
|
||||
@@ -2965,16 +2975,11 @@ watchFuelDone:
|
||||
; Effects count down from the top: the bang has three and this has two. They are separate
|
||||
; channels rather than one reused, because a crash while the warning is still sounding should
|
||||
; not cut it off - and on a machine with four voices there is no reason to be clever about it.
|
||||
putAlarm:
|
||||
INIA 0x02
|
||||
OUTA 0x41
|
||||
SETD.0 AlarmPatch
|
||||
CALL playPatch
|
||||
RET
|
||||
|
||||
alarm:
|
||||
INIA 0x02
|
||||
OUTA 0x41
|
||||
SETD.0 AlarmPatch
|
||||
CALL playPatch ; Its own LFO back, whatever the last sound left there.
|
||||
INIA 0d72 ; An octave over middle C, which is where a warning lives.
|
||||
OUTA 0x44
|
||||
RET
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1148,6 +1148,26 @@ The consequence is worth knowing rather than fixing: a retriggered noise source
|
||||
repeatable, so every hit is literally the same noise, the way a sampler is. On a hi-hat that
|
||||
can read as machine-gunny. Where variation is wanted, leave that LFO or that voice free.
|
||||
|
||||
### A Warning About Sharing Them:
|
||||
|
||||
**An LFO belongs to the device and not to a channel.** There are two of them against four
|
||||
voices, so setting one from a patch meant for channel three changes what channel nought hears,
|
||||
immediately and for as long as nothing sets it back.
|
||||
|
||||
That matters because a patch naturally carries LFO settings along with everything else - it is
|
||||
one instrument, and its LFO is part of how it sounds. A program that loads its instruments once
|
||||
at startup therefore ends up with whichever of them was written *last*, for all of them.
|
||||
|
||||
The failure this produces is unpleasant to diagnose, because it is intermittent by nature: a
|
||||
sound is correct until some unrelated thing plays, and correct again next time the machine
|
||||
starts. A game here lost a warning's trill after the first landing of each run, because the
|
||||
landing's patch happened to carry an LFO that was switched off.
|
||||
|
||||
**Load a patch immediately before the note that needs it.** A patch is forty-odd writes and
|
||||
this costs nothing at a moment that is already making a sound. What it cannot fix is two
|
||||
sounds overlapping - they still share the LFOs, and with two between four voices that is in
|
||||
the nature of the device.
|
||||
|
||||
### Knowing When It Has Finished:
|
||||
|
||||
The status port's bit 0 is set while any channel is still sounding, so a routine can wait for
|
||||
|
||||
@@ -25,7 +25,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Lander.sbx 9201
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -15,7 +15,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Lander.sbx 9201
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -32,7 +32,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Lander.sbx 9201
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -22,7 +22,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Lander.sbx 9201
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -22,7 +22,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Lander.sbx 9201
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -108,7 +108,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Lander.sbx 9201
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -27,7 +27,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Lander.sbx 9201
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -15,7 +15,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Lander.sbx 9201
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -13,7 +13,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Lander.sbx 9201
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -22,7 +22,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Lander.sbx 9201
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -496,6 +496,87 @@ 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 ----
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
{ 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
|
||||
param 0x20 0x00; param 0x21 40; param 0x22 0x00
|
||||
param 0x08 3 # pitch follows LFO 0
|
||||
param 0x09 200 # and a long way
|
||||
param 0x60 0x01; param 0x61 2; param 0x62 40; param 0x63 0x01
|
||||
port 0x44 60; pause one 250
|
||||
|
||||
port 0x41 0x01 # somebody else's instrument, which says the LFO is off
|
||||
param 0x60 0x00
|
||||
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 0x44 60; spinForever
|
||||
printf '#Vectors\n Boot start\n'; } | run lfoshared 8000000 || exit 1
|
||||
read -r FIRST SECOND THIRD <<EOT
|
||||
$(python3 - "$BUILD/lfoshared.raw" <<'PY'
|
||||
import struct, sys
|
||||
data = open(sys.argv[1], "rb").read()
|
||||
v = struct.unpack("<%dh" % (len(data) // 2), data)
|
||||
# Three notes, found as sound after silence.
|
||||
at, sounding, quiet = [], False, 0
|
||||
for i, x in enumerate(v):
|
||||
if abs(x) > 300:
|
||||
if not sounding:
|
||||
at.append(i); sounding = True
|
||||
quiet = 0
|
||||
elif sounding:
|
||||
quiet += 1
|
||||
if quiet > 3000: sounding = False
|
||||
if len(at) < 3:
|
||||
print("0 0 0"); raise SystemExit
|
||||
# ---- The PITCH of each, not how far it travels inside one ----
|
||||
#
|
||||
# The LFO here is slower than the note is long, so within one note it barely moves - what it
|
||||
# does is hold the pitch somewhere other than where the note asked for. Which is the honest
|
||||
# thing to measure anyway: a retriggered LFO starts at the same phase every time, so two notes
|
||||
# that agree had the same LFO and one that disagrees did not.
|
||||
def hz(start):
|
||||
w = v[start:start + 3000]
|
||||
peak = max((abs(x) for x in w), default=0)
|
||||
if not peak: return 0
|
||||
gate, cross, side = peak // 10, 0, 0
|
||||
for x in w:
|
||||
if side <= 0 and x > gate: side, cross = 1, cross + 1
|
||||
elif side >= 0 and x < -gate: side, cross = -1, cross + 1
|
||||
return cross * 48000 // (2 * len(w))
|
||||
print(hz(at[0]), hz(at[1]), hz(at[2]))
|
||||
PY
|
||||
)
|
||||
EOT
|
||||
# The note asks for middle C, about 262. With the LFO on it is bent well away from that.
|
||||
[ "$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"
|
||||
|
||||
echo
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo "All $PASS sound checks passed."
|
||||
|
||||
Reference in New Issue
Block a user