From 3ca5f193e6c4023a208f2ba941044edda7c68cf3 Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Fri, 4 Sep 2026 19:44:02 -0400 Subject: [PATCH] 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. --- Programs/CosmOS/Apps/Lander.asm | 49 +++++++++-------- Programs/CosmOS/README.md | 2 +- SplitBit Programming Manual.md | 20 +++++++ Tests/expected/cosmosCrossDisk.out | 2 +- Tests/expected/cosmosDrives.out | 2 +- Tests/expected/cosmosFault.out | 2 +- Tests/expected/cosmosFlip.out | 2 +- Tests/expected/cosmosGrid.out | 2 +- Tests/expected/cosmosMonitor.out | 2 +- Tests/expected/cosmosMonitorRun.out | 2 +- Tests/expected/cosmosRun.out | 2 +- Tests/expected/cosmosSlowDisk.out | 2 +- Tests/expected/cosmosSprite.out | 2 +- Tests/sound.sh | 81 +++++++++++++++++++++++++++++ 14 files changed, 139 insertions(+), 33 deletions(-) diff --git a/Programs/CosmOS/Apps/Lander.asm b/Programs/CosmOS/Apps/Lander.asm index 24cee7a..ade49fc 100644 --- a/Programs/CosmOS/Apps/Lander.asm +++ b/Programs/CosmOS/Apps/Lander.asm @@ -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 diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index fe2c12f..afe2dda 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -727,7 +727,7 @@ from every assembly file in it. Several are old programs written for the bare ma | Grid | The first program to use the screen as a screen. Redefines a tile above the font, fills all 128 map rows, and scrolls it diagonally a pixel at a time. | | Sprite | Moves a ball across the shell's own text, writing not one byte of the map to do it. It leaves the sprite in the table on the way out, because clearing them is the system's job - see below. | | Pad | Says what the controllers are doing, printing a line whenever one changes. It tells apart the three things that look identical from inside a game that is not responding: a pad nobody noticed, a pad mapped to nothing, and a mapping that is wrong. | -| Lander | Lunar Porter, rung one: a lander over a moon that wraps. Flown with a controller if there is one - a held thruster burns every tick it is held for - and with the arrow keys if there is not, where one press is one burn and that is the most the console can say. A bar at the bottom is the sideways drift, drawn as a sprite stretched to the speed - a moon has no air, so a drift never stops by itself and stopping one means cancelling it exactly, which is hard to do blind. It lands or crashes on arrival, and what decides is the speed at the moment it touches: gentler than three quarters of a pixel a frame downwards and half of one sideways, or it is a lander on its side. A fuel gauge sits in the window layer, where the moon turning underneath cannot scroll it away, and every thruster costs a unit of fuel every tick it fires. It turns from green to red at a quarter of a tank, and a warning sounds ONCE on the way down through it - once, because a lander is at its most careful in the last seconds before it touches and something repeating in its ear through that is a distraction rather than a warning. Filling up at a base allows it again. The sound is the moment it happened and the colour is how things stand, and both come off the same number so they cannot disagree. An empty tank is not an ending: it is a lander still flying that can no longer do anything about where. Four landing pads are carved into the moon after it is generated, levelled to whatever height their first column happened to have, and marked in cyan by an attribute rather than a tile of their own. The lander starts above one, because that is where a porter's day begins. Each pad is a base, told apart by the colour it is drawn in, and landing at one either loads cargo for the base across the moon or delivers what is aboard and pays eighty units of fuel. A landing is not an ending: the lander rests where it is until the throttle opens again, and A or Start says "read it" as readily as a key does. What a base says goes in the window rather than out of the console: the console draws into the map, so a message printed while flying is one the lander then flies over, and printing scrolls the whole world up a row. Opening the throttle wipes the line. Two bars read the speeds and are green while a landing would survive and red while it would not, so "can I put down" is a glance rather than a sum. A third runs up the left edge and is how much sky is under the lander, half a pixel of bar to a pixel of it; it reads nought the moment the lander is down, and it exists because the orbit takes the lander off the top of the screen and a panel that only works while the ground is in sight is no use above it. And gravity here is the pull MINUS the swing outwards, which is what makes an orbit rather than a one way trip: under four pixels a frame sideways the pull wins and the lander falls, over it the swing wins and the lander climbs, and at it they cancel and it circles. Falling buys sideways speed and climbing spends it - at a rate set by the product of the two, so the trade stops by itself as the sideways speed runs out - and that is the cycle: a fall carries the lander past orbital speed and becomes a climb, the climb pays it back and becomes a fall, periapse and apoapse, round and round. Orbital speed is marked on the drift bar, sixty four pixels either side of the middle, because a number nothing points at is folklore. Gravity never falls off and the moon wraps, so a circular orbit is the same length at any height and one orbital speed serves everywhere. There is a ceiling 192 pixels above the world's origin, which is the top of the wide view - it was sixty four while sixty four was all the sky a forty column screen had over the origin, which was a fact about the view rather than about the world. It pins rather than ends: leaving upward is recoverable, so the lander is held there and told so, and one sideways is spent every tick it tries to climb - without that the pin wiped the climb, the trade saw neither fall nor climb, and the speed pushing it up never changed. What kills you out here is running dry a long way from the ground. B, or z on the keyboard, swaps the screen between forty columns and eighty, which is the same map, the same cells and the same engine at half the size: nearly two thirds of the moon at once for the orbit, and twice as big for the landing. The twenty extra rows that buys go to the SKY and not to the moon - the wide view starts twenty four rows above the world's origin rather than at it, so the ground sits near the bottom and the whole flyable band is on the screen. Drawn down from the origin instead it was 71 per cent rock, which is a zoom that shows you more of the thing you cannot fly through. A lander at the ceiling is off the top of a forty column screen, which is where the orbit lives and why the altitude bar had to exist; zoomed out it is in the picture. And there is a station up there, twelve rows above the world's origin - about two thirds of the way from the ground to the ceiling, clear of work near the surface - going round at orbital speed - which needs no physics of its own, because a body at 64 sixteenths is exactly what a circular orbit IS under these rules, at any height, since gravity never falls off and the moon wraps. It is off the top of a forty column screen too, so it is somewhere to go that the zoom is needed to see. Docking it is landing on something that is moving: close enough, and slow enough RELATIVE TO THE STATION, or it is a wreck. Its speed is orbital speed, which is the number the marks on the drift bar point at, so the instrument for it was on the screen before there was anything to dock with. A dock pays eighty units of fuel and holds the lander a tile under the station until a thruster lets go, sliding it into line at half a pixel a frame rather than snapping - a dock is allowed eight pixels out either way, so putting the lander exactly in place the instant it took hold moved it a whole tile in one frame, right at the moment the player was being told they had been careful. It settles squarely under the port, which it did not used to: the lander is drawn from half a screen LESS HALF A TILE, because that is what centres it, and the station was drawn from half a screen exactly, so a perfectly flown dock still looked four pixels out. Getting there is a two burn manoeuvre and not a straight line: climbing SPENDS sideways speed, so a lander cannot rise while matched - it arrives slower than orbital every time, and has to raise the far side of its orbit and then circularise at the top. Which is also why it reads /lander.state if the disk has one: sixteen bytes of position, velocity, station, fuel and screen, laid straight over the numbers it would otherwise start with, so a rendezvous can be examined without flying one first. A disk without the file is the game as it always was. The buffer is a whole block wide because osFileRead lands a file in blocks of 256, and sixteen bytes of room for it wrote over everything that followed. A landing kicks up dust, sideways and up off the lander's feet, because that is where kicked dust goes and there is ground in the way of the rest of it. Letting go of the station vents gas instead, evenly in every direction, since nothing is in the way of a docking port - and that one does NOT stop the world the way the others do, because it goes off on the frame a thruster is pressed and freezing then would be felt as the controls sticking. There is none on docking: a dock is a catch and not a touchdown, and there is nothing under it to kick. A lander also has to get two tiles clear of the station before it can take hold of it again, because a docked one sits exactly one tile under it - so letting go upwards docked it straight back on, took the fuel again, and sat waiting to be told the message had been read. A thruster bangs when it lights and rumbles while it burns, and EVERY thruster that catches bangs - a pilot already burning upwards who then adds a sideways one has lit an engine, so what is watched is which of them are lit rather than whether any is. The rumble is struck once and runs until every one is out, since restriking it when a second joins would start its attack over, which is a stutter rather than an engine, and the rumble is a HELD note: its gate goes down when one lights and does not come up until every one is out - or until anything stops to wait, because a held note outlives the loop that was holding it and landing on the thruster used to leave it roaring under the verdict. Arriving somewhere latches two notes quickly, middle C then the C above for taking hold of the station and the same pair reversed for letting go, two octaves lower for the ground. The second note is pending rather than played, since at an undocking the pilot is mid-burn and stopping the world for an eighth of a second would be felt as the controls sticking. Four channels for five sounds: the bang and the latch share one, because a lander arriving either arrives or does not and a crash ends the run outright. A crash also makes a noise, which was the first sound this game had, and it is a TRIGGERED voice - struck once and then playing its own length, so nothing has to come back and end it. That the program says so rather than the patch is deliberate: the patch decides what the bang sounds like and the program decides that it is a bang. It is noise through a low pass that the modulation envelope shuts as the level falls, so the bright part is only at the front and it is a boom rather than a hiss. The instrument is built at startup the way the tiles are, because a patch is twenty odd writes and a note is two - which is what the selector and value registers are for. It plays on channel THREE: effects count down from the top so that music, if it ever arrives, can take nought and count up and the two never have to negotiate. A crash takes the lander apart: it goes, and six pieces of it leave in a rough hexagon at its own colour for about a second before the verdict is said. It used to be a line of text and a lander still sitting there in one piece, so a watcher had to read the words to know what had happened. The world is not running while that plays - it is a loop of its own, so nothing else has to know how to be half destroyed. A plume hangs off whichever side the engine is pushing from - under the lander to lift, over it to retro, and on the far side from the way it is being pushed sideways, since that is where the gas leaves. Every other reading here is a number drawn as a bar and all of them say what is happening TO the lander, so without this a watcher has to read gauges to work out that a thruster is even lit. It is drawn while the BUTTON IS HELD rather than while the engine fires: the engine fires one frame in ten, because that is the tick gravity is applied on, and a flame that honest is a fault lamp rather than a rocket. An empty tank draws none, and neither does the retro thruster on the ground, because in both cases the button really is doing nothing. Down is a retro thruster at half the strength of up - one sixteenth a tick against two, which is exactly gravity's own step, so it can stop a climb and hurry a descent and can never turn a landing approach into a crash faster than letting go would. Arresting a rise otherwise meant a sideways burn and a wait for the orbit to come round, which is how a rendezvous really is flown and a lot to ask of somebody who has not flown one. It does nothing at all to a lander on the ground, which is not politeness: touchdown has already had its say and will not speak again, so without that guard a landed lander holding it goes straight through the moon. | +| Lander | Lunar Porter, rung one: a lander over a moon that wraps. Flown with a controller if there is one - a held thruster burns every tick it is held for - and with the arrow keys if there is not, where one press is one burn and that is the most the console can say. A bar at the bottom is the sideways drift, drawn as a sprite stretched to the speed - a moon has no air, so a drift never stops by itself and stopping one means cancelling it exactly, which is hard to do blind. It lands or crashes on arrival, and what decides is the speed at the moment it touches: gentler than three quarters of a pixel a frame downwards and half of one sideways, or it is a lander on its side. A fuel gauge sits in the window layer, where the moon turning underneath cannot scroll it away, and every thruster costs a unit of fuel every tick it fires. It turns from green to red at a quarter of a tank, and a warning sounds ONCE on the way down through it - once, because a lander is at its most careful in the last seconds before it touches and something repeating in its ear through that is a distraction rather than a warning. Filling up at a base allows it again. The sound is the moment it happened and the colour is how things stand, and both come off the same number so they cannot disagree. An empty tank is not an ending: it is a lander still flying that can no longer do anything about where. Four landing pads are carved into the moon after it is generated, levelled to whatever height their first column happened to have, and marked in cyan by an attribute rather than a tile of their own. The lander starts above one, because that is where a porter's day begins. Each pad is a base, told apart by the colour it is drawn in, and landing at one either loads cargo for the base across the moon or delivers what is aboard and pays eighty units of fuel. A landing is not an ending: the lander rests where it is until the throttle opens again, and A or Start says "read it" as readily as a key does. What a base says goes in the window rather than out of the console: the console draws into the map, so a message printed while flying is one the lander then flies over, and printing scrolls the whole world up a row. Opening the throttle wipes the line. Two bars read the speeds and are green while a landing would survive and red while it would not, so "can I put down" is a glance rather than a sum. A third runs up the left edge and is how much sky is under the lander, half a pixel of bar to a pixel of it; it reads nought the moment the lander is down, and it exists because the orbit takes the lander off the top of the screen and a panel that only works while the ground is in sight is no use above it. And gravity here is the pull MINUS the swing outwards, which is what makes an orbit rather than a one way trip: under four pixels a frame sideways the pull wins and the lander falls, over it the swing wins and the lander climbs, and at it they cancel and it circles. Falling buys sideways speed and climbing spends it - at a rate set by the product of the two, so the trade stops by itself as the sideways speed runs out - and that is the cycle: a fall carries the lander past orbital speed and becomes a climb, the climb pays it back and becomes a fall, periapse and apoapse, round and round. Orbital speed is marked on the drift bar, sixty four pixels either side of the middle, because a number nothing points at is folklore. Gravity never falls off and the moon wraps, so a circular orbit is the same length at any height and one orbital speed serves everywhere. There is a ceiling 192 pixels above the world's origin, which is the top of the wide view - it was sixty four while sixty four was all the sky a forty column screen had over the origin, which was a fact about the view rather than about the world. It pins rather than ends: leaving upward is recoverable, so the lander is held there and told so, and one sideways is spent every tick it tries to climb - without that the pin wiped the climb, the trade saw neither fall nor climb, and the speed pushing it up never changed. What kills you out here is running dry a long way from the ground. B, or z on the keyboard, swaps the screen between forty columns and eighty, which is the same map, the same cells and the same engine at half the size: nearly two thirds of the moon at once for the orbit, and twice as big for the landing. The twenty extra rows that buys go to the SKY and not to the moon - the wide view starts twenty four rows above the world's origin rather than at it, so the ground sits near the bottom and the whole flyable band is on the screen. Drawn down from the origin instead it was 71 per cent rock, which is a zoom that shows you more of the thing you cannot fly through. A lander at the ceiling is off the top of a forty column screen, which is where the orbit lives and why the altitude bar had to exist; zoomed out it is in the picture. And there is a station up there, twelve rows above the world's origin - about two thirds of the way from the ground to the ceiling, clear of work near the surface - going round at orbital speed - which needs no physics of its own, because a body at 64 sixteenths is exactly what a circular orbit IS under these rules, at any height, since gravity never falls off and the moon wraps. It is off the top of a forty column screen too, so it is somewhere to go that the zoom is needed to see. Docking it is landing on something that is moving: close enough, and slow enough RELATIVE TO THE STATION, or it is a wreck. Its speed is orbital speed, which is the number the marks on the drift bar point at, so the instrument for it was on the screen before there was anything to dock with. A dock pays eighty units of fuel and holds the lander a tile under the station until a thruster lets go, sliding it into line at half a pixel a frame rather than snapping - a dock is allowed eight pixels out either way, so putting the lander exactly in place the instant it took hold moved it a whole tile in one frame, right at the moment the player was being told they had been careful. It settles squarely under the port, which it did not used to: the lander is drawn from half a screen LESS HALF A TILE, because that is what centres it, and the station was drawn from half a screen exactly, so a perfectly flown dock still looked four pixels out. Getting there is a two burn manoeuvre and not a straight line: climbing SPENDS sideways speed, so a lander cannot rise while matched - it arrives slower than orbital every time, and has to raise the far side of its orbit and then circularise at the top. Which is also why it reads /lander.state if the disk has one: sixteen bytes of position, velocity, station, fuel and screen, laid straight over the numbers it would otherwise start with, so a rendezvous can be examined without flying one first. A disk without the file is the game as it always was. The buffer is a whole block wide because osFileRead lands a file in blocks of 256, and sixteen bytes of room for it wrote over everything that followed. A landing kicks up dust, sideways and up off the lander's feet, because that is where kicked dust goes and there is ground in the way of the rest of it. Letting go of the station vents gas instead, evenly in every direction, since nothing is in the way of a docking port - and that one does NOT stop the world the way the others do, because it goes off on the frame a thruster is pressed and freezing then would be felt as the controls sticking. There is none on docking: a dock is a catch and not a touchdown, and there is nothing under it to kick. A lander also has to get two tiles clear of the station before it can take hold of it again, because a docked one sits exactly one tile under it - so letting go upwards docked it straight back on, took the fuel again, and sat waiting to be told the message had been read. A thruster bangs when it lights and rumbles while it burns, and EVERY thruster that catches bangs - a pilot already burning upwards who then adds a sideways one has lit an engine, so what is watched is which of them are lit rather than whether any is. The rumble is struck once and runs until every one is out, since restriking it when a second joins would start its attack over, which is a stutter rather than an engine, and the rumble is a HELD note: its gate goes down when one lights and does not come up until every one is out - or until anything stops to wait, because a held note outlives the loop that was holding it and landing on the thruster used to leave it roaring under the verdict. Arriving somewhere latches two notes quickly, middle C then the C above for taking hold of the station and the same pair reversed for letting go, two octaves lower for the ground. The second note is pending rather than played, since at an undocking the pilot is mid-burn and stopping the world for an eighth of a second would be felt as the controls sticking. Four channels for five sounds: the bang and the latch share one, because a lander arriving either arrives or does not and a crash ends the run outright. No instrument is set up once at startup: each sound loads its patch immediately before its note, because an LFO belongs to the DEVICE and not to a channel, so whichever patch was loaded last owns both of them for every voice at once. Not doing that is what made the warning's trill vanish after the first landing of a run - the landing's patch carries an LFO switched off - which is an intermittent fault of the worst kind, right until something unrelated plays and right again next time the machine starts. A crash also makes a noise, which was the first sound this game had, and it is a TRIGGERED voice - struck once and then playing its own length, so nothing has to come back and end it. That the program says so rather than the patch is deliberate: the patch decides what the bang sounds like and the program decides that it is a bang. It is noise through a low pass that the modulation envelope shuts as the level falls, so the bright part is only at the front and it is a boom rather than a hiss. The instrument is built at startup the way the tiles are, because a patch is twenty odd writes and a note is two - which is what the selector and value registers are for. It plays on channel THREE: effects count down from the top so that music, if it ever arrives, can take nought and count up and the two never have to negotiate. A crash takes the lander apart: it goes, and six pieces of it leave in a rough hexagon at its own colour for about a second before the verdict is said. It used to be a line of text and a lander still sitting there in one piece, so a watcher had to read the words to know what had happened. The world is not running while that plays - it is a loop of its own, so nothing else has to know how to be half destroyed. A plume hangs off whichever side the engine is pushing from - under the lander to lift, over it to retro, and on the far side from the way it is being pushed sideways, since that is where the gas leaves. Every other reading here is a number drawn as a bar and all of them say what is happening TO the lander, so without this a watcher has to read gauges to work out that a thruster is even lit. It is drawn while the BUTTON IS HELD rather than while the engine fires: the engine fires one frame in ten, because that is the tick gravity is applied on, and a flame that honest is a fault lamp rather than a rocket. An empty tank draws none, and neither does the retro thruster on the ground, because in both cases the button really is doing nothing. Down is a retro thruster at half the strength of up - one sixteenth a tick against two, which is exactly gravity's own step, so it can stop a climb and hurry a descent and can never turn a landing approach into a crash faster than letting go would. Arresting a rise otherwise meant a sideways burn and a wait for the orbit to come round, which is how a rendezvous really is flown and a lot to ask of somebody who has not flown one. It does nothing at all to a lander on the ground, which is not politeness: touchdown has already had its say and will not speak again, so without that guard a landed lander holding it goes straight through the moon. | | Depth | Four pillars at four distances and a ball walking past them, behind the near ones and in front of the far ones. The ball is sprite nought and every pillar is numbered after it, so table order puts it in front of all four - what actually decides is the depth buffer, asked a column at a time. | | Flip | Draws a whole screen into the bank that is not being shown, waits, and then shows it in one byte out of one port. It writes nothing else at all - not a tile, not a colour - so it does not ask for the screen to be saved, and the line it printed is still there when it comes back. It deliberately does not put the displayed screen back either, because that is the system's to restore: a program that faulted while flipped could not have. | | Edit | A line editor. | diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index 77885b0..68fe857 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -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 diff --git a/Tests/expected/cosmosCrossDisk.out b/Tests/expected/cosmosCrossDisk.out index 6b1c050..09f5559 100644 --- a/Tests/expected/cosmosCrossDisk.out +++ b/Tests/expected/cosmosCrossDisk.out @@ -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 diff --git a/Tests/expected/cosmosDrives.out b/Tests/expected/cosmosDrives.out index 6157e6a..607e7b9 100644 --- a/Tests/expected/cosmosDrives.out +++ b/Tests/expected/cosmosDrives.out @@ -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 diff --git a/Tests/expected/cosmosFault.out b/Tests/expected/cosmosFault.out index 28f5bfd..d557d6c 100644 --- a/Tests/expected/cosmosFault.out +++ b/Tests/expected/cosmosFault.out @@ -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 diff --git a/Tests/expected/cosmosFlip.out b/Tests/expected/cosmosFlip.out index b808c15..b6ba903 100644 --- a/Tests/expected/cosmosFlip.out +++ b/Tests/expected/cosmosFlip.out @@ -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 diff --git a/Tests/expected/cosmosGrid.out b/Tests/expected/cosmosGrid.out index 92a7f4b..65f372d 100644 --- a/Tests/expected/cosmosGrid.out +++ b/Tests/expected/cosmosGrid.out @@ -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 diff --git a/Tests/expected/cosmosMonitor.out b/Tests/expected/cosmosMonitor.out index 3b58319..13b5e00 100644 --- a/Tests/expected/cosmosMonitor.out +++ b/Tests/expected/cosmosMonitor.out @@ -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 diff --git a/Tests/expected/cosmosMonitorRun.out b/Tests/expected/cosmosMonitorRun.out index 6d78e30..2ae0d28 100644 --- a/Tests/expected/cosmosMonitorRun.out +++ b/Tests/expected/cosmosMonitorRun.out @@ -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 diff --git a/Tests/expected/cosmosRun.out b/Tests/expected/cosmosRun.out index 1cfccd6..1626970 100644 --- a/Tests/expected/cosmosRun.out +++ b/Tests/expected/cosmosRun.out @@ -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 diff --git a/Tests/expected/cosmosSlowDisk.out b/Tests/expected/cosmosSlowDisk.out index cb1da76..da684c1 100644 --- a/Tests/expected/cosmosSlowDisk.out +++ b/Tests/expected/cosmosSlowDisk.out @@ -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 diff --git a/Tests/expected/cosmosSprite.out b/Tests/expected/cosmosSprite.out index 12f5e5d..7232959 100644 --- a/Tests/expected/cosmosSprite.out +++ b/Tests/expected/cosmosSprite.out @@ -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 diff --git a/Tests/sound.sh b/Tests/sound.sh index 7f16357..749ed3d 100755 --- a/Tests/sound.sh +++ b/Tests/sound.sh @@ -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 < 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."