From f9b08cf7f9fb498e286ed75af561f3bdfd7fbb36 Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Sat, 5 Sep 2026 18:54:46 -0400 Subject: [PATCH] CosmOS lets every voice go when a program stops A gate is a register on the sound device and only a program can drop one. A program that has stopped cannot: it is gone. So a note left held sustained until something else said otherwise, and nothing else did - one program could leave the machine sounding for as long as it ran, with nothing the person at it could do. The shell already puts back the Stack, the vectors, the drive, the working directory and the screen. This is the same list and the same argument, and the fault path calls it too, for the stronger version of the argument: a program that CRASHED is exactly the one that cannot tidy up after itself, and a machine that will not stop humming is a poor place to read an error message. It does not make the device silent at once and does not pretend to. Dropping a gate RELEASES a note rather than stopping it, so the patch's release still runs. A bounded tail rather than an endless one is the part the system can be responsible for without knowing what instrument the program had built. Hum exits while holding a note; Pause makes no sound and takes a couple of million cycles, because the machine stops the moment the shell runs out of input and a note quietened at that instant leaves no samples behind to say whether it was. They are on a disk of their own so that a fixture does not move the ten recordings that quote cosmos.img's listing. THE CHECK CAUGHT ITSELF PASSING WRONGLY FIRST. Pause was missing from the disk, the machine halted immediately, and the window that should have held the tail was past the end of a render a twentieth of a second long - an empty window's peak is nought, which is indistinguishable from silence. So the sample count is asserted before anything is read from it. Verified with break.sh: without the call the note is still ringing at 9869. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW --- Programs/CosmOS/README.md | 4 +- Programs/CosmOS/Source/cosmos.asm | 42 +++++++++++++++++ Programs/testPrograms/humTest.asm | 70 +++++++++++++++++++++++++++++ Programs/testPrograms/pauseTest.asm | 47 +++++++++++++++++++ SplitBit Test Manual.md | 2 +- Tests/makedisks.sh | 17 +++++++ Tests/sound.sh | 53 ++++++++++++++++++++++ 7 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 Programs/testPrograms/humTest.asm create mode 100644 Programs/testPrograms/pauseTest.asm diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index e677fc6..e101399 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -767,7 +767,7 @@ the program was stopped most faults meets the very instruction that failed and fails again, so carrying on is not on offer - but the machine is almost never what is broken. Everything the shell puts back when a program exits, which is the Stack, any vectors it installed, the drive, the working directory, -the console and the screen, is exactly what wants putting back after one dies. So you are +the console, the screen and the sound, is exactly what wants putting back after one dies. So you are returned to the prompt, and the program is recorded as having stopped rather than finished. A fault *below* where programs load is the system's own code, and there is nothing to go back @@ -1261,7 +1261,7 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w | --- | --- | | osPrintString | DP0 names a string ending in a zero byte. Prints it. | | osReadLine | DP0 names somewhere to put a line, B says how much room there is. Reads one from the console, with the shell's own editing - arrows, Home, End, Delete - but not its history. Q comes back holding how long it was. The console is left in whatever mode it was found in. | -| osExit | Gives the machine back. Does not return. | +| osExit | Gives the machine back. Does not return. The system puts back what the program was borrowing - the Stack, its vectors, the drive, the working directory, the screen - and lets every voice of the sound device go, because a gate is a register only a program can drop and a program that has stopped cannot drop one. | | osArgument | DP0 names somewhere to put whatever followed the run command, B says how much room there is. | | osFileRead | DP0 names a file, DP1 says where to put it. Q is zero if it read, and DP3 comes back holding how many bytes there were. It writes WHOLE BLOCKS: a sixteen byte file still puts 256 bytes where it is told, so the room given has to be the length rounded up to the next 256. | | osFileSave | DP0 names a file, DP1 is the bytes, A and B together are how many. Q is zero if it saved, whether or not it was there before. | diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 321fbfc..845065d 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -1496,6 +1496,10 @@ faultSay: ; read, and a program that faulted may have left the screen with nowhere to put a letter. CALL faultScreen + ; And quiet, before them. A program that faulted mid-note left one sounding and is no longer + ; in a position to stop it. + CALL soundQuiet + CALL printString SETD.1 FaultNumbered LDA.1 @@ -5154,6 +5158,40 @@ screenGiveDone: screenGiveNone: RET +; ---- Every voice let go of ---- +; +; The same argument as the screen, and for a stronger reason. A program that took the sound +; device and stopped has nothing left that could end a note it was holding: a gate is a +; register on the device and only a program can drop one. A held note sustains until somebody +; says otherwise, so what this bounds is the difference between a machine that rings for a +; moment and one that rings until it is switched off. +; +; IT DOES NOT MAKE THE DEVICE SILENT AT ONCE, and does not pretend to. Dropping a gate +; RELEASES a note rather than stopping it, so whatever release the patch was given still runs. +; A bounded tail rather than an endless one is the part the system can be responsible for +; without knowing what instrument the program had built. +; +; A PROGRAM THAT FAULTS CANNOT TIDY UP AFTER ITSELF, which is why this is called from the +; fault path too. A machine that will not stop humming is a poor place to read an error +; message, and the program that would have quietened it is exactly the one that just died. +; +; Safe from a fault handler: four port writes, no service, no disk, and nothing that can +; refuse. +soundQuiet: + RSTA +soundQuietOne: + OUTA 0x41 ; This channel. Every sound port writes to whichever was last. + PSHA + RSTA + OUTA 0x45 ; Let go of whatever it was holding. + POPA + INCA + INIB 0d4 + CCF + SUB + BNQ soundQuietOne + RET + ; The registers a console cannot work without, put back. Not the picture - that is ; screenGive's, and only happens for a program that asked. screenSane: @@ -6266,6 +6304,10 @@ handleExit: ; What was on the screen before this program had it, if it asked for that. CALL screenGive + ; And every voice let go of, for the same reason the screen is: a program that has stopped + ; cannot drop a gate it left up. + CALL soundQuiet + ; The drive the person was on, whatever the program did with it. PSHA SETD.1 RunDrive diff --git a/Programs/testPrograms/humTest.asm b/Programs/testPrograms/humTest.asm new file mode 100644 index 0000000..5c0156a --- /dev/null +++ b/Programs/testPrograms/humTest.asm @@ -0,0 +1,70 @@ +; A program that stops while a note is still held down. +; +; The gate is a register on the sound device, and only a program can drop one. This one does +; not: it starts a note whose envelope sustains at full and then hands the machine back, so +; the note is left sounding with nothing left in the world able to end it. +; +; WHAT SHOULD HAPPEN IS THAT COSMOS ENDS IT, the way it puts the screen back and the drive and +; the vectors - and for a stronger reason, since a program that faults cannot tidy up after +; itself either. Without that this note sustains for as long as the machine runs. +; +; Sustain at full and a short release, so the difference between the two answers is loud: a +; gate left up rings for ever and a gate dropped is quiet within a fraction of a second. +; +; Written by Anachronaut + +#Include services.asm + +#Program + + #Base 0x5000 + +start: + RSTA + OUTA 0x41 ; Channel 0 + OUTA 0x42 + INIA 0d2 + OUTA 0x43 ; Saw + INIA 0x01 + OUTA 0x42 + INIA 0xFF + OUTA 0x43 ; at full gain + INIA 0x05 + OUTA 0x42 + INIA 0x01 + OUTA 0x43 ; and on. + INIA 0x20 + OUTA 0x42 + RSTA + OUTA 0x43 ; No attack + INIA 0x21 + OUTA 0x42 + RSTA + OUTA 0x43 ; no decay + INIA 0x22 + OUTA 0x42 + INIA 0xFF + OUTA 0x43 ; and held at full, for ever, until a gate says otherwise. + INIA 0x23 + OUTA 0x42 + INIA 0d5 + OUTA 0x43 ; A short release, so a dropped gate is quiet almost at once. + + INIA 0xC0 + OUTA 0x46 + + INIA 0d60 + OUTA 0x44 ; Middle C, started and never let go of. + + SWI osExit + +#Data + + #Base 0x3000 + +Unused: + 0x00 + +#Vectors + + Boot start diff --git a/Programs/testPrograms/pauseTest.asm b/Programs/testPrograms/pauseTest.asm new file mode 100644 index 0000000..f7f7c19 --- /dev/null +++ b/Programs/testPrograms/pauseTest.asm @@ -0,0 +1,47 @@ +; A program that takes a while and makes no sound. +; +; Fixture. Something has to happen after a program that left a note sounding has stopped, or +; there is nothing for the sound device to be measured DURING: the machine halts the moment +; the shell runs out of input, and a note quietened at that instant leaves no samples behind +; to say whether it was quietened at all. +; +; So this burns about two million cycles and exits. Two nested counts and a third around +; them, because a byte only reaches 256 and a fifth of a second is a great deal more than +; that. +; +; Written by Anachronaut + +#Include services.asm + +#Program + + #Base 0x5000 + +start: + INIA 0d16 +outer: + PSHA ; The outer count, which the inner ones need A for. + RSTB +middle: + RSTA +inner: + DECA + BNA inner + DECB + BNB middle + POPA + DECA + BNA outer + + SWI osExit + +#Data + + #Base 0x3000 + +Unused: + 0x00 + +#Vectors + + Boot start diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index f358b24..589a8de 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -447,7 +447,7 @@ the console's line editing was in when it broke twice in two days. ## Fixture Disks: -`Tests/makedisks.sh` builds 27 images with SplitDisk before anything runs, into +`Tests/makedisks.sh` builds 28 images with SplitDisk before anything runs, into `Tests/build/disks`. **That is the point of them.** A SplitBit program reading one of these is being checked against a filesystem written by different code from the same written specification, rather than against itself. diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index 2a3ad66..6991377 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -685,6 +685,23 @@ printf 'system /System/Boot/cosmos.bin\n' > "$WORK/lonely.cfg" "$TOOL" put "$DISKS/nofallback.img" "$WORK/lonely.cfg" /System/Boot/boot.cfg >/dev/null "$TOOL" bootstate "$DISKS/nofallback.img" 1 >/dev/null +# ---- A program that stops while a note is still held ---- +# +# Its own disk rather than a place on cosmos.img, because everything on that one appears in a +# directory listing and ten recorded tests quote those. A fixture that exists to be run by one +# check should not move ten others every time it changes size. +"$TOOL" format "$DISKS/quiet.img" 128 2 >/dev/null +"$TOOL" mkdir "$DISKS/quiet.img" /Apps >/dev/null +"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ + "$ROOT/Programs/testPrograms/humTest.asm" -o "$WORK/Hum.sbx" >/dev/null +"$TOOL" put "$DISKS/quiet.img" "$WORK/Hum.sbx" /Apps/Hum.sbx >/dev/null +# And something silent to run after it, because the machine stops the moment the shell runs +# out of input - a note quietened at that instant leaves no samples behind to say whether it +# was quietened at all. +"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ + "$ROOT/Programs/testPrograms/pauseTest.asm" -o "$WORK/Pause.sbx" >/dev/null +"$TOOL" put "$DISKS/quiet.img" "$WORK/Pause.sbx" /Apps/Pause.sbx >/dev/null + # ---- What a program made of it ---- # # A status is only worth having if it survives the program that set it, so this runs three diff --git a/Tests/sound.sh b/Tests/sound.sh index 04336d7..1f7c52d 100755 --- a/Tests/sound.sh +++ b/Tests/sound.sh @@ -702,6 +702,59 @@ QUIETER="$ALONE0" && result ok "and then both at once" "peak $TOGETHER against $ALONE0 and $ALONE1 alone" \ || result no "and then both at once" "peak $TOGETHER, which is not above either of $ALONE0 and $ALONE1" +# ---- The system quietens a note the program that made it cannot ---- +# +# A gate is a register on the sound device, and only a program can drop one. A program that +# has stopped cannot: it is gone. So a note left held sustains until something else says +# otherwise, and nothing else did - which means one program could leave the machine sounding +# for as long as it ran, with no way for the person at it to stop it. +# +# CosmOS now lets every voice go when a program hands the machine back, the same way it puts +# the screen and the drive and the vectors back, and from the fault path too - because a +# program that CRASHED is exactly the one that cannot tidy up after itself. +# +# Hum starts a note whose envelope sustains at full and exits while holding it. Pause makes no +# sound and takes a couple of million cycles, and it is there because the machine stops the +# moment the shell runs out of input: a note quietened at that instant would leave no samples +# behind to say whether it had been. +# +# ITS OWN DISK, so that a fixture run by one check does not move the ten recorded tests that +# quote cosmos.img's directory listing. +if [ ! -f "$ROOT/Tests/build/disks/quiet.img" ]; then + result no "the system quietens what a program left sounding" "quiet.img is missing; run Tests/makedisks.sh" +else + "$ASM" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \ + -o "$BUILD/cosmos.bin" "$ROOT/Programs/CosmOS/Source/cosmos.asm" >/dev/null 2>&1 + printf 'Hum\nPause\nexit\n' > "$BUILD/quiet.in" + timeout 60 "$EMU" --fast --cycles 20000000 --sound "$BUILD/quiet.raw" \ + --disk "$ROOT/Tests/build/disks/quiet.img" "$BUILD/cosmos.bin" \ + < "$BUILD/quiet.in" > "$BUILD/quiet.out" 2>&1 + + # ---- PROOF THAT ANYTHING WAS MEASURED AT ALL ---- + # + # This check reads a window near the end of the render, and a window past the end of the + # samples is empty, and an empty window's peak is nought - which is indistinguishable from + # silence and passes. It happened: Pause was missing from the disk, the machine halted the + # moment the shell ran out of input, and the check reported a quietened note on a render + # a twentieth of a second long. So the length is asserted before anything is read from it. + SAMPLES="$(measure quiet count)" + [ "$SAMPLES" -gt 150000 ] \ + || result no "the quieting check measured a real run" "only $SAMPLES samples, so the windows below are empty" + + # The note has to have been made, or silence afterwards proves nothing at all. This is the + # half that would let the check pass on a machine where the sound device did not work. + STARTED="$(measure quiet peak 0 40000)" + [ "$STARTED" -gt 1000 ] \ + && result ok "a program can leave a note sounding" "peak $STARTED while it held one" \ + || result no "a program can leave a note sounding" "peak $STARTED, so nothing sounded and the rest proves nothing" + + # And by the end there is nothing, though the program that started it never let go. + LEFT="$(measure quiet peak 100000 0)" + [ "$LEFT" -lt 200 ] \ + && result ok "the system quietens what a program left sounding" "peak $LEFT after it handed back" \ + || result no "the system quietens what a program left sounding" "peak $LEFT, so it is still ringing" +fi + echo if [ "$FAIL" -eq 0 ]; then echo "All $PASS sound checks passed."