Compare commits
46
Commits
5e85356245
..
main
@@ -9,6 +9,12 @@
|
|||||||
/SplitDisk
|
/SplitDisk
|
||||||
/SplitLint
|
/SplitLint
|
||||||
/Voyager
|
/Voyager
|
||||||
|
/SoundPatch
|
||||||
|
/TuneC
|
||||||
|
|
||||||
|
# A .score is written and a .tune is what TuneC makes of it, so a .tune is a build product
|
||||||
|
# the way a .sbx is. Not anchored: they turn up wherever somebody is composing.
|
||||||
|
*.tune
|
||||||
/CLAUDE.md
|
/CLAUDE.md
|
||||||
/claudeResume.sh
|
/claudeResume.sh
|
||||||
/codexResume.sh
|
/codexResume.sh
|
||||||
@@ -40,3 +46,4 @@ Source/Emulator/rom.c
|
|||||||
# The personal disk 'make run-voyager' puts in drive 1. Made on demand, never rebuilt, and
|
# The personal disk 'make run-voyager' puts in drive 1. Made on demand, never rebuilt, and
|
||||||
# not cleaned - it is the one place in here where something made ON the machine can live.
|
# not cleaned - it is the one place in here where something made ON the machine can live.
|
||||||
/Disks/
|
/Disks/
|
||||||
|
*.sym
|
||||||
|
|||||||
+353
-28
@@ -48,6 +48,20 @@ start:
|
|||||||
; floor the system gives every program does not.
|
; floor the system gives every program does not.
|
||||||
SWI osTakeScreen
|
SWI osTakeScreen
|
||||||
|
|
||||||
|
; ---- A flight starts where a flight starts, and not where the last one stopped ----
|
||||||
|
;
|
||||||
|
; RUN DOES NOT RELOAD, and that is right: run's job is to run the program that is loaded,
|
||||||
|
; which is why it is a separate word from load and why typing a program's name does both.
|
||||||
|
; What follows from it is that the numbers the assembler wrote into the Data Segment are
|
||||||
|
; LOAD-time values, true once, and a program that wants them true at every start has to say
|
||||||
|
; so itself. This one did not, so a second run began with Flying already nought - the loop
|
||||||
|
; ended before it started and the program handed the machine straight back.
|
||||||
|
;
|
||||||
|
; Only what a flight needs to begin. Anything not named here keeps what the last run left
|
||||||
|
; it, which is deliberate: state surviving a run is sometimes exactly what is wanted, and
|
||||||
|
; the way to have that is for this list to be a decision rather than a sweep.
|
||||||
|
CALL initialise
|
||||||
|
|
||||||
CALL loadState ; Before the view, because a state may say which view to be in.
|
CALL loadState ; Before the view, because a state may say which view to be in.
|
||||||
CALL setView ; The block every gauge below reads its screen numbers out of.
|
CALL setView ; The block every gauge below reads its screen numbers out of.
|
||||||
SETD.0 Wide
|
SETD.0 Wide
|
||||||
@@ -74,17 +88,19 @@ start:
|
|||||||
|
|
||||||
CALL mulReady ; The quarter square table, once. The orbit below needs a product.
|
CALL mulReady ; The quarter square table, once. The orbit below needs a product.
|
||||||
CALL putTiles
|
CALL putTiles
|
||||||
|
|
||||||
|
; Key mode before the splash, because "press anything" needs the keystroke as it happens
|
||||||
|
; rather than a line at the end of one.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x02
|
||||||
|
CALL splash
|
||||||
|
|
||||||
CALL makeMoon
|
CALL makeMoon
|
||||||
CALL carvePads
|
CALL carvePads
|
||||||
CALL drawMoon
|
CALL drawMoon
|
||||||
CALL putLander
|
CALL putLander
|
||||||
CALL putGauge
|
CALL putGauge
|
||||||
CALL putOrbital
|
CALL putOrbital
|
||||||
CALL putThrust ; The instruments, once, the way the tiles are.
|
|
||||||
CALL putAlarm
|
|
||||||
|
|
||||||
INIA 0x01
|
|
||||||
OUTA 0x02 ; Key mode.
|
|
||||||
|
|
||||||
; ---- Is there a controller ----
|
; ---- Is there a controller ----
|
||||||
;
|
;
|
||||||
@@ -2796,20 +2812,36 @@ runPend:
|
|||||||
BNA runPend
|
BNA runPend
|
||||||
RET
|
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
|
; CHANNEL THREE IS TWO INSTRUMENTS. The bang and the latch share it, and a patch is the whole
|
||||||
; goes on when a thruster lights and its gate does not come up until every thruster is out.
|
; voice, so the channel has to be told which of them it is about to be. Loading at the moment
|
||||||
putThrust:
|
; of the note costs fifty writes at a moment already doing more than fifty, and means a sound
|
||||||
RSTA
|
; is what its patch says whatever played before it.
|
||||||
OUTA 0x41
|
;
|
||||||
SETD.0 ThrustOnPatch
|
; The other three channels have one sound each and could be set up once at startup. They are
|
||||||
CALL playPatch
|
; not, because a single rule is worth more here than the writes it saves: every sound loads
|
||||||
INIA 0x01
|
; its own patch, so there is no question of which sounds are safe to set up early and which
|
||||||
OUTA 0x41
|
; are not.
|
||||||
SETD.0 ThrustHoldPatch
|
;
|
||||||
CALL playPatch
|
; ---- What this used to be working around ----
|
||||||
RET
|
;
|
||||||
|
; The LFOs belonged to the DEVICE and not to a channel: two of them against four voices, with
|
||||||
|
; a patch carrying LFO settings the way it carries everything else, so whichever patch loaded
|
||||||
|
; last owned 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 carry an LFO that is switched OFF, 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.
|
||||||
|
;
|
||||||
|
; Loading at the moment of the note made each sound correct AS IT STARTED, which was as far as
|
||||||
|
; a program could get on its own. It could not fix two sounds OVERLAPPING - a warning going off
|
||||||
|
; mid-burn still re-tuned the rumble's LFO for as long as it lasted.
|
||||||
|
;
|
||||||
|
; The device was fixed instead: an LFO now belongs to the channel it was written to, so four
|
||||||
|
; channels are four independent instruments in this respect as in every other. Overlapping
|
||||||
|
; sounds no longer interfere, and the paragraph above is history rather than a caveat.
|
||||||
|
|
||||||
; ---- Lit or not, and only the CHANGES matter ----
|
; ---- Lit or not, and only the CHANGES matter ----
|
||||||
;
|
;
|
||||||
@@ -2852,6 +2884,8 @@ thrustHaveSet:
|
|||||||
BRQ thrustNoPop ; Nothing newly lit, whatever else is still burning.
|
BRQ thrustNoPop ; Nothing newly lit, whatever else is still burning.
|
||||||
RSTA
|
RSTA
|
||||||
OUTA 0x41
|
OUTA 0x41
|
||||||
|
SETD.0 ThrustOnPatch
|
||||||
|
CALL playPatch
|
||||||
INIA 0d36
|
INIA 0d36
|
||||||
OUTA 0x44 ; The bang of it catching.
|
OUTA 0x44 ; The bang of it catching.
|
||||||
thrustNoPop:
|
thrustNoPop:
|
||||||
@@ -2870,6 +2904,8 @@ thrustNoPop:
|
|||||||
INIA 0x01
|
INIA 0x01
|
||||||
STA.0
|
STA.0
|
||||||
OUTA 0x41
|
OUTA 0x41
|
||||||
|
SETD.0 ThrustHoldPatch
|
||||||
|
CALL playPatch ; After the pop's, so the held sound owns the LFO while it runs.
|
||||||
INIA 0d60
|
INIA 0d60
|
||||||
OUTA 0x44 ; Held, until the gate comes up.
|
OUTA 0x44 ; Held, until the gate comes up.
|
||||||
BRI thrustRemember
|
BRI thrustRemember
|
||||||
@@ -2965,16 +3001,11 @@ watchFuelDone:
|
|||||||
; Effects count down from the top: the bang has three and this has two. They are separate
|
; 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
|
; 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.
|
; 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:
|
alarm:
|
||||||
INIA 0x02
|
INIA 0x02
|
||||||
OUTA 0x41
|
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.
|
INIA 0d72 ; An octave over middle C, which is where a warning lives.
|
||||||
OUTA 0x44
|
OUTA 0x44
|
||||||
RET
|
RET
|
||||||
@@ -3182,6 +3213,76 @@ showHeightSized:
|
|||||||
; BINARY, because that is what a file is on this machine. A tool to build one from readable
|
; BINARY, because that is what a file is on this machine. A tool to build one from readable
|
||||||
; text is a small job for another day; until then the tests write the bytes, which is plain
|
; text is a small job for another day; until then the tests write the bytes, which is plain
|
||||||
; enough while the fields are named.
|
; enough while the fields are named.
|
||||||
|
; ---- What a new flight is ----
|
||||||
|
;
|
||||||
|
; Called before loadState, so a saved state still wins: this puts the world back to its
|
||||||
|
; beginning and that puts a remembered one over the top of it.
|
||||||
|
initialise:
|
||||||
|
; Everything that is nought when a flight begins, from a table of WHERE rather than a line
|
||||||
|
; each. A label written in the Data Segment comes out as its address, so the list below is
|
||||||
|
; names and the assembler turns them into pointers - and 0xFFFF ends it, the same way it
|
||||||
|
; ends the music player's order lists, because no useful address can begin up there.
|
||||||
|
SETD.0 ZeroAtStart
|
||||||
|
zeroNext:
|
||||||
|
LDA.0
|
||||||
|
INIB 0xFF
|
||||||
|
XOR
|
||||||
|
BRQ zeroDone
|
||||||
|
LDD.1.0 ; DP1 is the place this entry names.
|
||||||
|
INCD.0
|
||||||
|
INCD.0
|
||||||
|
RSTA
|
||||||
|
STA.1
|
||||||
|
BRI zeroNext
|
||||||
|
zeroDone:
|
||||||
|
|
||||||
|
; The sideways speed is two bytes and the table above deals in one, so it says so here.
|
||||||
|
RSTA
|
||||||
|
SETD.1 SpeedAcross
|
||||||
|
STA.1
|
||||||
|
INCD.1
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
; And the numbers that are not nought: where the lander is, where the station is, a full
|
||||||
|
; tank, the gravity tick, and the flag that says the loop should run at all.
|
||||||
|
INIA 0xA0
|
||||||
|
SETD.1 AcrossLow
|
||||||
|
STA.1
|
||||||
|
INIA 0x14
|
||||||
|
SETD.1 AcrossHigh
|
||||||
|
STA.1
|
||||||
|
INIA 0x02
|
||||||
|
SETD.1 DownHigh
|
||||||
|
STA.1
|
||||||
|
INIA 0xA0
|
||||||
|
SETD.1 StationLow
|
||||||
|
STA.1
|
||||||
|
INIA 0x34
|
||||||
|
SETD.1 StationHigh
|
||||||
|
STA.1
|
||||||
|
INIA 0xFA
|
||||||
|
SETD.1 StationDownHigh
|
||||||
|
STA.1
|
||||||
|
INIA 0xFF
|
||||||
|
SETD.1 Fuel
|
||||||
|
STA.1
|
||||||
|
INIA 0d1
|
||||||
|
SETD.1 FallTick
|
||||||
|
STA.1
|
||||||
|
SETD.1 Flying
|
||||||
|
STA.1 ; A is still one, and this is the one that matters most.
|
||||||
|
|
||||||
|
; ---- And the moon, which is the same moon every time ----
|
||||||
|
;
|
||||||
|
; The seed is a written number rather than anything taken from the world, so a fresh load
|
||||||
|
; always walks out the same terrain. nextRandom moves it, though, so a second run started
|
||||||
|
; from wherever the first one left it and generated a different moon - which was not a
|
||||||
|
; decision anybody made. Put back here, the moon is the moon.
|
||||||
|
INIA 0x5D
|
||||||
|
SETD.1 Seed
|
||||||
|
STA.1
|
||||||
|
RET
|
||||||
|
|
||||||
loadState:
|
loadState:
|
||||||
SETD.0 StateName
|
SETD.0 StateName
|
||||||
SETD.1 StateBuffer
|
SETD.1 StateBuffer
|
||||||
@@ -3444,6 +3545,188 @@ groundLevel:
|
|||||||
STA.0
|
STA.0
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
; ---- The studio, and its theme ----
|
||||||
|
;
|
||||||
|
; What a splash screen is: whose game this is, over whatever music they want it said to. The
|
||||||
|
; game's own title screen is a different thing and comes later.
|
||||||
|
;
|
||||||
|
; IT POLLS THE TIMER RATHER THAN BEING INTERRUPTED BY IT. This program has no vector segment
|
||||||
|
; and waits for the screen by reading port 0x30, so waiting for a beat by reading port 0x50 is
|
||||||
|
; the same shape - and it means the splash brings no handler that would have to be taken away
|
||||||
|
; before the game starts. Reading the status is what answers a tick nobody was interrupted by.
|
||||||
|
;
|
||||||
|
; A tune that is not there is not a failure. The logo still shows, for a moment measured in
|
||||||
|
; frames instead of ticks, and the game starts. That is what /lander.state does too.
|
||||||
|
splash:
|
||||||
|
; ---- Somebody already leaning on something means no splash at all ----
|
||||||
|
;
|
||||||
|
; Asked before anything is drawn or read, so that skipping costs nothing: the alternative
|
||||||
|
; blanks twenty five rows of window and reads a file off the disk before it thinks to look,
|
||||||
|
; which is a fifth of a second spent showing a picture nobody wanted.
|
||||||
|
CALL splashSkip
|
||||||
|
BNQ splashNone
|
||||||
|
|
||||||
|
; ---- The whole screen is window while the splash is up ----
|
||||||
|
;
|
||||||
|
; Port 0x3D is how many window rows are SHOWN, and putGauge sets it to two later for the
|
||||||
|
; gauge. Two is not enough to put a line in the middle of the screen, and until putGauge
|
||||||
|
; runs it is not even that - so the splash asks for all of them, which has the second use of
|
||||||
|
; hiding the map, where the shell's prompt is still sitting.
|
||||||
|
INIA 0d25
|
||||||
|
OUTA 0x3D
|
||||||
|
RSTA
|
||||||
|
OUTA 0x3E ; From the top.
|
||||||
|
|
||||||
|
; Blanked a row at a time, because what is in the window is whatever was there before. A
|
||||||
|
; CALL hands back A and B, so the row and the count both survive it.
|
||||||
|
INIB 0d25 ; A is already nought, from setting the window's top above.
|
||||||
|
splashBlank:
|
||||||
|
SETD.0 Nothing
|
||||||
|
CALL sayAtRow
|
||||||
|
INCA
|
||||||
|
DECB
|
||||||
|
BNB splashBlank
|
||||||
|
|
||||||
|
SETD.0 StudioLine
|
||||||
|
INIA 0d11
|
||||||
|
CALL sayAtRow
|
||||||
|
|
||||||
|
; ---- Where the tune is, asked rather than assumed ----
|
||||||
|
;
|
||||||
|
; This used to name /splash.tune, which was true for exactly as long as the game and its
|
||||||
|
; things both sat at the root of the disk that started the machine. The game lives in a
|
||||||
|
; directory of its own now and what somebody types is a launcher in /Apps, so the only
|
||||||
|
; thing that says where the tune is, is where the game itself came from.
|
||||||
|
;
|
||||||
|
; And it has to be asked rather than worked out from the working directory, because that
|
||||||
|
; is where the PERSON is standing. A player in their own directory is not standing in
|
||||||
|
; Lander's, and should not have to be.
|
||||||
|
SETD.0 MyPath
|
||||||
|
INIB 0d192
|
||||||
|
SWI osWhereAmI
|
||||||
|
SETD.0 MyPath
|
||||||
|
SETD.1 SplashTune
|
||||||
|
SETD.2 TunePath
|
||||||
|
INIB 0d192
|
||||||
|
CALL pathBeside
|
||||||
|
BNQ splashSilent ; A path that did not fit cannot name a tune.
|
||||||
|
|
||||||
|
SETD.0 TunePath
|
||||||
|
SETD.1 SplashBuffer
|
||||||
|
SWI osFileRead
|
||||||
|
BNQ splashSilent
|
||||||
|
|
||||||
|
SETD.0 SplashBuffer
|
||||||
|
CALL useTune ; Which sets the tick out of the tune's own header.
|
||||||
|
BNQ splashSilent
|
||||||
|
|
||||||
|
INIA 0x03
|
||||||
|
OUTA 0x51 ; Run and repeat. NO interrupt: this waits by looking.
|
||||||
|
|
||||||
|
splashPlay:
|
||||||
|
SETD.1 Voice0
|
||||||
|
RSTA
|
||||||
|
CALL stepVoice
|
||||||
|
SETD.1 Voice1
|
||||||
|
INIA 0d1
|
||||||
|
CALL stepVoice
|
||||||
|
SETD.1 Voice2
|
||||||
|
INIA 0d2
|
||||||
|
CALL stepVoice
|
||||||
|
SETD.1 Voice3
|
||||||
|
INIA 0d3
|
||||||
|
CALL stepVoice
|
||||||
|
|
||||||
|
SETD.1 Playing
|
||||||
|
LDA.1
|
||||||
|
BRA splashDone ; Every voice has run out of order list.
|
||||||
|
CALL splashSkip
|
||||||
|
BNQ splashDone
|
||||||
|
CALL waitTick
|
||||||
|
BRI splashPlay
|
||||||
|
|
||||||
|
splashSilent:
|
||||||
|
; No tune on the disk, so the logo is held for about a second and a half of frames.
|
||||||
|
INIB 0d90
|
||||||
|
splashHold:
|
||||||
|
CALL waitFrame
|
||||||
|
CALL splashSkip
|
||||||
|
BNQ splashDone
|
||||||
|
DECB
|
||||||
|
BNB splashHold
|
||||||
|
|
||||||
|
splashDone:
|
||||||
|
RSTA
|
||||||
|
OUTA 0x51 ; The timer stopped, whether or not it was ever started.
|
||||||
|
CALL hushVoices
|
||||||
|
|
||||||
|
; And the window given back, because putGauge is about to ask for the two rows it wants.
|
||||||
|
RSTA
|
||||||
|
OUTA 0x3D
|
||||||
|
splashNone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- Anything pressed, on a pad or a key ----
|
||||||
|
;
|
||||||
|
; THE ANSWER COMES BACK IN Q, because Q is what survives a RET. A CALL saves and restores A,
|
||||||
|
; so a routine that answered there would hand its caller back the A it already had - which
|
||||||
|
; here was the channel number the last stepVoice was given, and the splash ended on its first
|
||||||
|
; pass every time whatever anybody pressed. nextRandom above says the same thing about the
|
||||||
|
; same register, which is where it should have been read.
|
||||||
|
splashSkip:
|
||||||
|
CALL readPad
|
||||||
|
SETD.0 Held
|
||||||
|
LDA.0
|
||||||
|
INIB 0x50 ; A, or Start - the same two waitKey asks about, and for the same
|
||||||
|
AND ; reason: a direction held on a stick is not somebody asking to
|
||||||
|
BNQ splashSkipYes ; begin, and ANY bit counted one as such.
|
||||||
|
INA 0x01
|
||||||
|
INIB 0x01 ; READY
|
||||||
|
AND
|
||||||
|
BRQ splashSkipNo
|
||||||
|
INA 0x00 ; Taken, so it is not still waiting at the prompt afterwards.
|
||||||
|
BRA splashSkipNo ; A NOUGHT IS NOT A KEYPRESS. It is what a recorded keyboard file
|
||||||
|
; holds while nobody is typing, and what readControls has always
|
||||||
|
; ignored - a splash that took it for a key would be a splash
|
||||||
|
; nothing could ever watch.
|
||||||
|
splashSkipYes:
|
||||||
|
INIA 0d1
|
||||||
|
RSTB
|
||||||
|
OR ; Q is one: somebody asked for the game.
|
||||||
|
RET
|
||||||
|
splashSkipNo:
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
OR ; And Q is nought: nobody did.
|
||||||
|
RET
|
||||||
|
|
||||||
|
; A beat, waited for by looking - the same shape as waitFrame, one device along. Reading the
|
||||||
|
; status is what brings the tick down.
|
||||||
|
waitTick:
|
||||||
|
INA 0x50
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
BRQ waitTick
|
||||||
|
RET
|
||||||
|
|
||||||
|
; Every voice let go of. The splash owns all four while it plays and the game wants them back
|
||||||
|
; silent - and every sound in this game loads its own patch before its note, so nothing else
|
||||||
|
; has to be put back.
|
||||||
|
hushVoices:
|
||||||
|
RSTA
|
||||||
|
hushOne:
|
||||||
|
OUTA 0x41
|
||||||
|
PSHA
|
||||||
|
RSTA
|
||||||
|
OUTA 0x45
|
||||||
|
POPA
|
||||||
|
INCA
|
||||||
|
INIB 0d4
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ hushOne
|
||||||
|
RET
|
||||||
|
|
||||||
; ---- A line of text, into the window ----
|
; ---- A line of text, into the window ----
|
||||||
;
|
;
|
||||||
; DP0 names a string. It goes into window row one and the rest of the row is BLANKED, so a
|
; DP0 names a string. It goes into window row one and the rest of the row is BLANKED, so a
|
||||||
@@ -3457,12 +3740,21 @@ groundLevel:
|
|||||||
; The letters are ordinary tiles: the character generator starts at the space, so glyph n is
|
; The letters are ordinary tiles: the character generator starts at the space, so glyph n is
|
||||||
; character n less thirty two.
|
; character n less thirty two.
|
||||||
sayInWindow:
|
sayInWindow:
|
||||||
|
INIA 0d1 ; Row one, which is where a base's message goes.
|
||||||
|
sayAtRow:
|
||||||
|
; A is which window row. Kept in memory across the bank select below, which needs A itself.
|
||||||
|
SETD.1 SayRow
|
||||||
|
STA.1
|
||||||
INIA 0d5
|
INIA 0d5
|
||||||
OUTA 0xE3
|
OUTA 0xE3 ; Which needs A, so the row came out of it a moment ago.
|
||||||
INIA 0xC1
|
LDA.1 ; DP1 still holds SayRow.
|
||||||
|
INIB 0xC0
|
||||||
|
CCF
|
||||||
|
ADD ; Window row n begins at 0xC000 plus n times 256.
|
||||||
|
MVQA
|
||||||
OUTA 0xE4
|
OUTA 0xE4
|
||||||
RSTA
|
RSTA
|
||||||
OUTA 0xE5 ; Window row one begins at 0xC100.
|
OUTA 0xE5
|
||||||
SETD.1 SayAt
|
SETD.1 SayAt
|
||||||
STA.1
|
STA.1
|
||||||
|
|
||||||
@@ -3975,6 +4267,22 @@ Wide:
|
|||||||
;
|
;
|
||||||
; Named rather than numbered, so whatever writes one and this that reads it are describing the
|
; Named rather than numbered, so whatever writes one and this that reads it are describing the
|
||||||
; same fields. Low byte first, the way every other word here is.
|
; same fields. Low byte first, the way every other word here is.
|
||||||
|
SayRow:
|
||||||
|
0d1
|
||||||
|
StudioLine:
|
||||||
|
" ANACHRONAUT"
|
||||||
|
Nothing:
|
||||||
|
0x00
|
||||||
|
; Beside the game, wherever the game turns out to be. See splash, which asks.
|
||||||
|
SplashTune:
|
||||||
|
"splash.tune"
|
||||||
|
MyPath:
|
||||||
|
#Reserve 0d192
|
||||||
|
TunePath:
|
||||||
|
#Reserve 0d192
|
||||||
|
SplashBuffer:
|
||||||
|
#Reserve 0d1024
|
||||||
|
|
||||||
StateName:
|
StateName:
|
||||||
"/lander.state"
|
"/lander.state"
|
||||||
StateBuffer:
|
StateBuffer:
|
||||||
@@ -4028,6 +4336,17 @@ KeyHeld:
|
|||||||
Flying:
|
Flying:
|
||||||
0x01
|
0x01
|
||||||
|
|
||||||
|
; ---- Everything a new flight puts back to nought ----
|
||||||
|
;
|
||||||
|
; Names, which the assembler writes out as addresses. Adding a variable that has to start
|
||||||
|
; empty means adding it here and nowhere else.
|
||||||
|
ZeroAtStart:
|
||||||
|
DownLow SpeedDown SpeedDownHigh StationDownLow
|
||||||
|
Held HasPad KeyHeld
|
||||||
|
PendNote PendIn ThrustWas ThrustNow Rumbling
|
||||||
|
AlarmSaid Wide AtBase Carrying Landed BurstLeft Docked Adrift
|
||||||
|
0xFF 0xFF
|
||||||
|
|
||||||
Terrain:
|
Terrain:
|
||||||
#Reserve 0d128
|
#Reserve 0d128
|
||||||
|
|
||||||
@@ -4372,6 +4691,12 @@ LanderArtAt:
|
|||||||
|
|
||||||
#Include math.asm
|
#Include math.asm
|
||||||
|
|
||||||
|
; Joining a name to the place another thing is in, which is how the tune is found now that
|
||||||
|
; the game does not know where it lives until it asks.
|
||||||
|
#Include path.asm
|
||||||
|
|
||||||
|
#Include player.asm
|
||||||
|
|
||||||
#Include boom.asm
|
#Include boom.asm
|
||||||
|
|
||||||
#Include alarm.asm
|
#Include alarm.asm
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
; Play.asm
|
||||||
|
; Four voices on one clock, which is what music is and one channel cannot be.
|
||||||
|
; Written by Anachronaut
|
||||||
|
;
|
||||||
|
; ---- One tick, four cursors ----
|
||||||
|
;
|
||||||
|
; The sound device has four channels and no idea when. The timer has a period and no idea
|
||||||
|
; what. This is the thing between them: on every tick it walks four voices, and each voice
|
||||||
|
; counts down the note it is holding and reads the next one when the count runs out.
|
||||||
|
;
|
||||||
|
; THE VOICES SHARE NOTHING BUT THE TICK. Each keeps its own place in its own track and its
|
||||||
|
; own count of how much longer the current note lasts, so a voice playing whole notes and a
|
||||||
|
; voice playing eighths cost the same and never have to know about each other. That is why
|
||||||
|
; the tick is the smallest subdivision in the piece rather than a note length: it is the
|
||||||
|
; only unit all four can agree on.
|
||||||
|
;
|
||||||
|
; ---- What a sequence is ----
|
||||||
|
;
|
||||||
|
; Pairs of bytes: what to play, then how many ticks it lasts.
|
||||||
|
;
|
||||||
|
; 0x01 to 0x7F a MIDI note. 60 is middle C and every 12 is an octave.
|
||||||
|
; 0x00 a rest - the ticks pass with nothing sounding
|
||||||
|
; 0x80 to 0xFE a command, which takes no time at all. See below.
|
||||||
|
; 0xFF the sequence is over
|
||||||
|
;
|
||||||
|
; MIDI NOTES ONLY REACH 127, so the top of the byte was free and none of that had to be
|
||||||
|
; invented. Examples/tune.asm spent zero on its end marker and so had no way to write a rest
|
||||||
|
; at all, which one voice can just about live with and four cannot: voices do not all play at
|
||||||
|
; once, and the silences are what make them separate parts rather than a chord.
|
||||||
|
;
|
||||||
|
; A note's duration is its whole life. The gate goes down when the count runs out and the next
|
||||||
|
; event begins on the same tick, so a gap between two notes is WRITTEN, as a rest, rather than
|
||||||
|
; invented by the player out of some fraction it decided on.
|
||||||
|
;
|
||||||
|
; ---- Commands, of which there is one ----
|
||||||
|
;
|
||||||
|
; A command consumes no tick: the reader acts on it and reads the next event straight away, so
|
||||||
|
; commands sit BETWEEN notes in time rather than needing a place of their own.
|
||||||
|
;
|
||||||
|
; 0x80 <address> play the rest of this voice on that patch.
|
||||||
|
;
|
||||||
|
; A patch change reshapes whatever is still ringing on the voice, and nothing can be done
|
||||||
|
; about that here: a channel has one set of parameters and a note in its release is using
|
||||||
|
; them. Deferring the load to the next note would reshape the same tail. So it is a fact about
|
||||||
|
; the hardware rather than a choice about the format, and the cure is a rest long enough for
|
||||||
|
; the release, which is the composer's to write.
|
||||||
|
;
|
||||||
|
; ONE COMMAND, and the other 125 values left alone. A tempo change, a volume ramp and a note
|
||||||
|
; slide are all easy to add and impossible to remove, and no piece of music has asked for one.
|
||||||
|
;
|
||||||
|
; ---- Sequences, and the order they go in ----
|
||||||
|
;
|
||||||
|
; A sequence is one voice's phrase. Each voice has an ORDER LIST of its own - a table of
|
||||||
|
; sequence addresses, ending in a zero - and when a sequence runs out the voice takes the next
|
||||||
|
; address from it. The voice stops when the list does.
|
||||||
|
;
|
||||||
|
; THAT IS WHERE REPETITION COMES FROM, and it costs no notation: the bass below plays the same
|
||||||
|
; sequence in the first bar and the last, and it is written once. A thirty-two bar piece that
|
||||||
|
; reuses four phrases is four phrases and a list.
|
||||||
|
;
|
||||||
|
; PER VOICE RATHER THAN ONE SHARED LIST, because that is the shape this machine likes. A
|
||||||
|
; voice's order cursor is a pointer it advances by itself, which is the same LDD and STD move
|
||||||
|
; everything else here makes; a shared table of four-column rows would have every voice
|
||||||
|
; indexing into one place, which is a worse fit for a machine with four data pointers. The
|
||||||
|
; four columns are how it READS, and how a tracker would show it; they are not how it is
|
||||||
|
; stored. Nothing keeps the voices together except that their sequences add up to the same
|
||||||
|
; length, which is a thing a compiler can check and a hand-written tune has to get right.
|
||||||
|
;
|
||||||
|
; A voice silent through a bar has a sequence that rests for it. There is no need for a way to
|
||||||
|
; say "nothing here" when a rest already says it.
|
||||||
|
;
|
||||||
|
; ---- Where the state lives ----
|
||||||
|
;
|
||||||
|
; Six bytes a voice: two of order cursor, two of sequence cursor, one of count, one of live.
|
||||||
|
; Both cursors are two bytes and at the front because that is what LDD and STD move - a
|
||||||
|
; pointer through a pointer, which is the whole reason this is a loop over four voices instead
|
||||||
|
; of the same code written out four times.
|
||||||
|
;
|
||||||
|
; A voice starts pointed at Empty, which is a sequence of nothing but its end marker. The
|
||||||
|
; first tick runs its count out, finds the end, and goes to the order list for the real first
|
||||||
|
; sequence - so the beginning needs no special case at all.
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
#Base 0x5000
|
||||||
|
|
||||||
|
start:
|
||||||
|
; ---- An instrument for each voice ----
|
||||||
|
;
|
||||||
|
; Four patches, designed in soundThing and converted by SoundPatch, which is the only thing
|
||||||
|
; here that understands what a patch means. The player reads a count and that many parameter
|
||||||
|
; and value pairs and knows nothing else about them.
|
||||||
|
;
|
||||||
|
; WHICH PATCH EACH VOICE STARTS ON IS DECLARED, in VoiceStart below, rather than being four
|
||||||
|
; calls in a row here. That is the shape a tune read from a file will have - a starting
|
||||||
|
; instrument is state and belongs in a header - and saying it once means the two forms do
|
||||||
|
; not drift.
|
||||||
|
;
|
||||||
|
; A PATCH BELONGS TO ITS CHANNEL, which is why four of them can be up at once. It did not
|
||||||
|
; used to: the LFOs belonged to the whole device, so whichever patch was loaded last owned
|
||||||
|
; them for every voice. Kalimba has LFO 0 switched off and the other three have it on, so
|
||||||
|
; under the old device this piece would have played with the arpeggio's setting on all four
|
||||||
|
; parts - which is exactly the fault that made Lunar Porter's low fuel warning stop
|
||||||
|
; trilling after the first landing of a run.
|
||||||
|
; ---- A tune named on the line, or the one in here ----
|
||||||
|
;
|
||||||
|
; Nothing after the name plays what this program was built with. A name reads a file and
|
||||||
|
; plays that instead, which is what makes it a player rather than a program with one song.
|
||||||
|
SETD.0 TuneName
|
||||||
|
INIB 0d63
|
||||||
|
SWI osArgument
|
||||||
|
SETD.0 TuneName
|
||||||
|
LDA.0
|
||||||
|
BRA playBuiltIn ; Nothing was typed after the name.
|
||||||
|
|
||||||
|
SETD.1 TuneBuffer ; DP0 is still the name, which is what osFileRead wants.
|
||||||
|
SWI osFileRead
|
||||||
|
BNQ playNoFile
|
||||||
|
|
||||||
|
SETD.0 TuneBuffer
|
||||||
|
CALL useTune
|
||||||
|
BNQ playNotATune
|
||||||
|
BRI playReady
|
||||||
|
|
||||||
|
playBuiltIn:
|
||||||
|
CALL useBuiltIn
|
||||||
|
playReady:
|
||||||
|
|
||||||
|
INIA 0x60 ; Room over the top for four voices at once.
|
||||||
|
OUTA 0x46
|
||||||
|
|
||||||
|
; ---- The beat ----
|
||||||
|
;
|
||||||
|
; ONLY THE CONTROL BYTE. The period was set by whoever supplied the tune, because a tick
|
||||||
|
; belongs to the tune and not to this program: a file says what it wants in its header.
|
||||||
|
;
|
||||||
|
; It used to be written here as well, AFTER useTune had taken it out of the file - so every
|
||||||
|
; tune played at a sixteenth note at 120 beats a minute whatever its header said. The only
|
||||||
|
; reason nothing noticed is that the one fixture in the suite asks for exactly that.
|
||||||
|
;
|
||||||
|
; Writing control with the run bit set is what loads the period, which is why this comes
|
||||||
|
; after whoever set it and not before.
|
||||||
|
INIA 0x07
|
||||||
|
OUTA 0x51 ; Run, repeat, interrupt.
|
||||||
|
SIF
|
||||||
|
|
||||||
|
; ---- The loop ----
|
||||||
|
;
|
||||||
|
; Step every voice, see whether any of them is still going, and then sleep. Stepping before
|
||||||
|
; waiting is what makes the first note sound on the first tick rather than the second.
|
||||||
|
tickLoop:
|
||||||
|
SETD.1 Voice0
|
||||||
|
RSTA
|
||||||
|
CALL stepVoice
|
||||||
|
SETD.1 Voice1
|
||||||
|
INIA 0d1
|
||||||
|
CALL stepVoice
|
||||||
|
SETD.1 Voice2
|
||||||
|
INIA 0d2
|
||||||
|
CALL stepVoice
|
||||||
|
SETD.1 Voice3
|
||||||
|
INIA 0d3
|
||||||
|
CALL stepVoice
|
||||||
|
|
||||||
|
SETD.1 Playing
|
||||||
|
LDA.1
|
||||||
|
BRA finished
|
||||||
|
WAIT ; Nothing at all until the timer says a tick has gone by.
|
||||||
|
BRI tickLoop
|
||||||
|
|
||||||
|
finished:
|
||||||
|
; ---- Let the last note finish, and MEASURE how long that takes ----
|
||||||
|
;
|
||||||
|
; The gates all went down on the tick the tracks ended, but a gate down is a note released
|
||||||
|
; rather than a note stopped: the oboe and the strings have a release to run. Rendered and
|
||||||
|
; measured, the tail dies out eight tenths of a second after the last event, and eight ticks
|
||||||
|
; of ring is one second - which sounds like enough and is two tenths short of it.
|
||||||
|
;
|
||||||
|
; A program that exits with sound still in the air leaves nothing able to end it: the
|
||||||
|
; program is gone and cannot drop a gate. What that turns into depends on the front end -
|
||||||
|
; behind a window the tail simply finishes, and on a terminal EMULATED TIME STOPS while the
|
||||||
|
; machine blocks on a key, so the tail freezes and comes out a snippet per keystroke.
|
||||||
|
;
|
||||||
|
; Twelve ticks is a second and a half, which covers the measured tail with room over. It is
|
||||||
|
; a number about THESE FOUR INSTRUMENTS, and a patch with a longer release would want more -
|
||||||
|
; the general answer is for the system to quieten the device when a program stops, the way
|
||||||
|
; it puts the screen back, and that is not built.
|
||||||
|
INIB 0d12
|
||||||
|
lastRing:
|
||||||
|
WAIT
|
||||||
|
DECB
|
||||||
|
BNB lastRing
|
||||||
|
|
||||||
|
CIF
|
||||||
|
RSTA
|
||||||
|
OUTA 0x51
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
; ---- What it says when it cannot ----
|
||||||
|
;
|
||||||
|
; Before the timer has been started, both of them, so there is nothing to stop and no handler
|
||||||
|
; to take away.
|
||||||
|
playNoFile:
|
||||||
|
SETD.0 NoFileSaid
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0x01
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
playNotATune:
|
||||||
|
SETD.0 NotATuneSaid
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0x01
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
; ---- The tune that is in this program ----
|
||||||
|
;
|
||||||
|
; What useTune does for a file, said in eight lines for a tune that is already in memory: the
|
||||||
|
; three tables the player asks for, and an order list for each voice. Nothing is relocated,
|
||||||
|
; because the assembler already wrote the addresses.
|
||||||
|
useBuiltIn:
|
||||||
|
; The tick this tune wants. For a file it comes out of the header; for this one it is here,
|
||||||
|
; so that both hand the player the same things.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x52
|
||||||
|
INIA 0xE8
|
||||||
|
OUTA 0x53
|
||||||
|
INIA 0x48
|
||||||
|
OUTA 0x54 ; 0x01E848 is 125,000: a sixteenth note at 120.
|
||||||
|
|
||||||
|
SETD.0 BuiltInPatches
|
||||||
|
SETD.1 PatchTable
|
||||||
|
STD.0.1
|
||||||
|
SETD.0 BuiltInSequences
|
||||||
|
SETD.1 SequenceTable
|
||||||
|
STD.0.1
|
||||||
|
SETD.0 BuiltInVoiceStart
|
||||||
|
SETD.1 VoiceStartAt
|
||||||
|
STD.0.1
|
||||||
|
SETD.0 Order0
|
||||||
|
SETD.1 Voice0
|
||||||
|
STD.0.1
|
||||||
|
SETD.0 Order1
|
||||||
|
SETD.1 Voice1
|
||||||
|
STD.0.1
|
||||||
|
SETD.0 Order2
|
||||||
|
SETD.1 Voice2
|
||||||
|
STD.0.1
|
||||||
|
SETD.0 Order3
|
||||||
|
SETD.1 Voice3
|
||||||
|
STD.0.1
|
||||||
|
CALL startVoices
|
||||||
|
RET
|
||||||
|
|
||||||
|
; The tick has nothing to do: the loop above is the player and WAIT only needs something to
|
||||||
|
; have happened. A handler still has to exist, because an interrupt with nothing installed to
|
||||||
|
; catch it is a fault. Taking it is what brings the line down - a program that POLLED the
|
||||||
|
; timer would have to read 0x50 instead.
|
||||||
|
tick:
|
||||||
|
RETI
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
#Base 0x3000
|
||||||
|
|
||||||
|
; The name that followed "Play", and room for the tune it names. WHOLE BLOCKS: osFileRead
|
||||||
|
; puts 256 bytes down whatever the file's length, so the room here is a multiple of that and
|
||||||
|
; not a guess at how big a tune is.
|
||||||
|
TuneName:
|
||||||
|
#Reserve 0d64
|
||||||
|
TuneBuffer:
|
||||||
|
#Reserve 0d1024
|
||||||
|
|
||||||
|
NoFileSaid:
|
||||||
|
"no such tune"
|
||||||
|
NotATuneSaid:
|
||||||
|
"that is not a tune"
|
||||||
|
|
||||||
|
; ---- The order lists ----
|
||||||
|
;
|
||||||
|
; A table of sequence addresses each, ending in a zero. Read the four of them across and they
|
||||||
|
; are the bars; read one down and it is a part.
|
||||||
|
;
|
||||||
|
; bar 1 2 3 4
|
||||||
|
; melody Mel1 Mel2 Mel3 Mel4
|
||||||
|
; harmony Har1 Har2 Har3 Har4
|
||||||
|
; bass BassC BassF BassG BassC
|
||||||
|
; arp ArpC ArpF ArpG Silent
|
||||||
|
;
|
||||||
|
; THE BASS PLAYS BassC TWICE and it is written once. That is the whole return on having an
|
||||||
|
; order list, and it is worth noticing how little it cost: a table of labels the assembler
|
||||||
|
; fills in, and ten instructions in stepVoice.
|
||||||
|
Order0:
|
||||||
|
0d0 0d1 0d2 0d3 0xFF
|
||||||
|
Order1:
|
||||||
|
0d4 0d5 0d6 0d7 0xFF
|
||||||
|
Order2:
|
||||||
|
0d8 0d9 0d10 0d8 0xFF ; BassC twice, written once.
|
||||||
|
Order3:
|
||||||
|
0d11 0d12 0d13 0d14 0xFF
|
||||||
|
|
||||||
|
; ---- The tables, which are the only places an address lives ----
|
||||||
|
;
|
||||||
|
; Writing these out by hand is exactly the tedium a compiler exists to remove: every sequence
|
||||||
|
; has to be counted into its place above, and moving one means renumbering. That it is
|
||||||
|
; unpleasant is the point of noticing it here rather than after a tool has baked the shape in.
|
||||||
|
; The tables this tune uses. The player holds POINTERS to them rather than the tables, because
|
||||||
|
; a tune read from a file has its tables wherever the file was put - so a built-in one hands
|
||||||
|
; over the same three addresses a loaded one would.
|
||||||
|
BuiltInPatches:
|
||||||
|
OboePatch StringsPatch SquarePatch KalimbaPatch
|
||||||
|
|
||||||
|
BuiltInSequences:
|
||||||
|
Mel1 Mel2 Mel3 Mel4 ; 0 to 3
|
||||||
|
Har1 Har2 Har3 Har4 ; 4 to 7
|
||||||
|
BassC BassF BassG ; 8 to 10
|
||||||
|
ArpC ArpF ArpG Silent ; 11 to 14
|
||||||
|
|
||||||
|
; The patch each voice starts on. Not assumed, and not four calls in a row: a starting
|
||||||
|
; instrument is state, and state belongs somewhere it can be read.
|
||||||
|
BuiltInVoiceStart:
|
||||||
|
0d0 0d1 0d2 0d3
|
||||||
|
|
||||||
|
; ---- Four bars of C, F, G, C ----
|
||||||
|
;
|
||||||
|
; A bar is sixteen ticks, and every sequence in a column adds up to sixteen. Nothing enforces
|
||||||
|
; that here: get one wrong and the voices come apart, quietly, some bars later. It is the
|
||||||
|
; first thing a compiler should check.
|
||||||
|
|
||||||
|
; The melody, in quarters and halves.
|
||||||
|
Mel1:
|
||||||
|
0d64 0d4 ; E
|
||||||
|
0d67 0d4 ; G
|
||||||
|
0d72 0d8 ; C, held
|
||||||
|
0xFF
|
||||||
|
Mel2:
|
||||||
|
0d65 0d4 ; F
|
||||||
|
0d69 0d4 ; A
|
||||||
|
0d72 0d8 ; C
|
||||||
|
0xFF
|
||||||
|
Mel3:
|
||||||
|
0d62 0d4 ; D
|
||||||
|
0d67 0d4 ; G
|
||||||
|
0d71 0d8 ; B
|
||||||
|
0xFF
|
||||||
|
Mel4:
|
||||||
|
0x80 0d1 ; Patch one is Strings: the last bar is a swell, not a reed.
|
||||||
|
0d72 0d16 ; and home
|
||||||
|
0xFF
|
||||||
|
|
||||||
|
; A second part underneath it, in halves.
|
||||||
|
Har1:
|
||||||
|
0d60 0d8 ; C
|
||||||
|
0d64 0d8 ; E
|
||||||
|
0xFF
|
||||||
|
Har2:
|
||||||
|
0d57 0d8 ; A
|
||||||
|
0d60 0d8 ; C
|
||||||
|
0xFF
|
||||||
|
Har3:
|
||||||
|
0d59 0d8 ; B
|
||||||
|
0d62 0d8 ; D
|
||||||
|
0xFF
|
||||||
|
Har4:
|
||||||
|
0d64 0d16 ; E
|
||||||
|
0xFF
|
||||||
|
|
||||||
|
; The bass, one note to a bar - and the first bar's is the last bar's.
|
||||||
|
BassC:
|
||||||
|
0d48 0d16 ; C
|
||||||
|
0xFF
|
||||||
|
BassF:
|
||||||
|
0d41 0d16 ; F
|
||||||
|
0xFF
|
||||||
|
BassG:
|
||||||
|
0d43 0d16 ; G
|
||||||
|
0xFF
|
||||||
|
|
||||||
|
; And an arpeggio in eighths, which is the part that proves the others are not waiting for it.
|
||||||
|
ArpC:
|
||||||
|
0d72 0d2 0d76 0d2 0d79 0d2 0d76 0d2
|
||||||
|
0d72 0d2 0d76 0d2 0d79 0d2 0d76 0d2
|
||||||
|
0xFF
|
||||||
|
ArpF:
|
||||||
|
0d77 0d2 0d81 0d2 0d84 0d2 0d81 0d2
|
||||||
|
0d77 0d2 0d81 0d2 0d84 0d2 0d81 0d2
|
||||||
|
0xFF
|
||||||
|
ArpG:
|
||||||
|
0d79 0d2 0d83 0d2 0d86 0d2 0d83 0d2
|
||||||
|
0d79 0d2 0d83 0d2 0d86 0d2 0d83 0d2
|
||||||
|
0xFF
|
||||||
|
Silent:
|
||||||
|
0d00 0d16 ; Out for the last bar, so it is the other three.
|
||||||
|
0xFF
|
||||||
|
|
||||||
|
; The scheduler, the patch loader and the state a voice keeps. The tune above is this
|
||||||
|
; program's; everything that plays it is shared with whatever else wants to.
|
||||||
|
#Include player.asm
|
||||||
|
|
||||||
|
; The instruments, at the bottom because each brings its own #Data and a base has to come
|
||||||
|
; before anything is in the segment it bases.
|
||||||
|
#Include oboe.asm
|
||||||
|
#Include strings.asm
|
||||||
|
#Include square.asm
|
||||||
|
#Include kalimba.asm
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start
|
||||||
|
Device 0x50 tick
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
; A program that says what is on the disk.
|
||||||
|
;
|
||||||
|
; Which no program could do until now. Everything the system offered took a name a program
|
||||||
|
; already knew - read it, save it, rename it, delete it, ask how big it is - and there was no
|
||||||
|
; way to ask what names there are. dir could list only because it lives in the shell and calls
|
||||||
|
; the filesystem directly. So a file manager, a backup, and the package manager still to come
|
||||||
|
; were all unwritable for want of two services.
|
||||||
|
;
|
||||||
|
; This is those two, used as simply as they can be: walk, say what each thing is, and ask
|
||||||
|
; about it.
|
||||||
|
;
|
||||||
|
; > Walk
|
||||||
|
; f Say.sbx 1
|
||||||
|
; d Apps 0
|
||||||
|
; u halfsaved.txt 3
|
||||||
|
;
|
||||||
|
; ---- And it asks about each one AS IT GOES, which is the point ----
|
||||||
|
;
|
||||||
|
; Where a walk has got to is the system's, and so is where the last file it was asked about
|
||||||
|
; lives - the same eight bytes of filesystem state. A program that walks a directory and asks
|
||||||
|
; osFileInfo about each entry is using both at once, which is the obvious thing to write and
|
||||||
|
; the thing that would quietly go wrong if the two shared a position.
|
||||||
|
;
|
||||||
|
; Calling osFileInfo between two steps of the walk is therefore not decoration here. It is the
|
||||||
|
; check, and a walk that came back wrong afterwards would show up as a short listing or a
|
||||||
|
; repeated name rather than as an error.
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
#Base 0x5000
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 Name
|
||||||
|
INIB 0d24
|
||||||
|
SWI osDirFirst
|
||||||
|
BRI walkCheck
|
||||||
|
|
||||||
|
walkStep:
|
||||||
|
SETD.0 Name
|
||||||
|
INIB 0d24
|
||||||
|
SWI osDirNext
|
||||||
|
|
||||||
|
walkCheck:
|
||||||
|
; The kind is written down before anything else wants the registers: the comparison below
|
||||||
|
; is an ALU operation and Q is where its answer goes.
|
||||||
|
MVQA
|
||||||
|
SETD.0 Kind
|
||||||
|
STA.0
|
||||||
|
INIB 0xFF
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ walkDone
|
||||||
|
|
||||||
|
; f, d or u. A chain rather than a table, because three is not enough to index - and A is
|
||||||
|
; still the kind, since SUB writes Q and leaves it alone.
|
||||||
|
BRA walkFile
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ walkDirectory
|
||||||
|
SETD.0 Unfinished
|
||||||
|
BRI walkSay
|
||||||
|
walkDirectory:
|
||||||
|
SETD.0 Directory
|
||||||
|
BRI walkSay
|
||||||
|
walkFile:
|
||||||
|
SETD.0 File
|
||||||
|
|
||||||
|
walkSay:
|
||||||
|
SWI osPrintString
|
||||||
|
SETD.0 Name
|
||||||
|
SWI osPrintString
|
||||||
|
SETD.0 Space
|
||||||
|
SWI osPrintString
|
||||||
|
|
||||||
|
; ---- And how big it is, asked BETWEEN two steps of the walk ----
|
||||||
|
;
|
||||||
|
; The name is the only thing the walk hands over, which is what keeps it from being a record
|
||||||
|
; both sides have to agree the shape of. A program that wants more asks about the name, the
|
||||||
|
; same way anything else does.
|
||||||
|
;
|
||||||
|
; A directory is not asked about. osFileInfo answers for one perfectly well and says nought
|
||||||
|
; blocks, which is true and reads as a size - and nought is a size a file can genuinely
|
||||||
|
; have, so the two would be indistinguishable in the listing.
|
||||||
|
SETD.0 Kind
|
||||||
|
LDA.0
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ walkNoSize
|
||||||
|
|
||||||
|
SETD.0 Name
|
||||||
|
SWI osFileInfo
|
||||||
|
BNQ walkNoSize
|
||||||
|
|
||||||
|
PSHD.3
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
SWI osPrintNumber
|
||||||
|
BRI walkEnded
|
||||||
|
|
||||||
|
walkNoSize:
|
||||||
|
SETD.0 NoSize
|
||||||
|
SWI osPrintString
|
||||||
|
|
||||||
|
walkEnded:
|
||||||
|
SETD.0 NewLine
|
||||||
|
SWI osPrintString
|
||||||
|
BRI walkStep
|
||||||
|
|
||||||
|
walkDone:
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
#Base 0x3000
|
||||||
|
|
||||||
|
File:
|
||||||
|
"f "
|
||||||
|
Directory:
|
||||||
|
"d "
|
||||||
|
Unfinished:
|
||||||
|
"u "
|
||||||
|
Space:
|
||||||
|
" "
|
||||||
|
NoSize:
|
||||||
|
"-"
|
||||||
|
NewLine:
|
||||||
|
0x0A 0x00
|
||||||
|
|
||||||
|
Kind:
|
||||||
|
0x00
|
||||||
|
Name:
|
||||||
|
#Reserve 0d24
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
; A program that says where it came from.
|
||||||
|
;
|
||||||
|
; The dullest thing on the disk, and that is the point of it: osWhereAmI and pathBeside are
|
||||||
|
; what an application with assets stands on, and both are much easier to check now, with
|
||||||
|
; nothing depending on them, than later inside a game that has gone quiet.
|
||||||
|
;
|
||||||
|
; Where says the path it was loaded from
|
||||||
|
; Where splash.tune says that, and the path of splash.tune beside it
|
||||||
|
;
|
||||||
|
; ---- The thing worth checking ----
|
||||||
|
;
|
||||||
|
; The answer follows the PROGRAM and not the person. Run this from the root and it says one
|
||||||
|
; thing; walk somewhere else and run the same copy and it says the same thing, because the
|
||||||
|
; system worked it out when the program started and a program's own place cannot move under
|
||||||
|
; it. A working directory is where the person is standing, and it is what the argument means:
|
||||||
|
; "Where notes.txt" asks about a notes.txt in the program's place, not in yours, which is
|
||||||
|
; exactly the distinction an application needs and the reason cwd could not serve both.
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
#Base 0x5000
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 MyPath
|
||||||
|
INIB 0d192
|
||||||
|
SWI osWhereAmI
|
||||||
|
|
||||||
|
SETD.0 FromText
|
||||||
|
SWI osPrintString
|
||||||
|
SETD.0 MyPath
|
||||||
|
SWI osPrintString
|
||||||
|
CALL newLine
|
||||||
|
|
||||||
|
; With nothing after it, that is the whole of what it has to say.
|
||||||
|
SETD.0 Given
|
||||||
|
INIB 0d64
|
||||||
|
SWI osArgument
|
||||||
|
SETD.0 Given
|
||||||
|
LDA.0
|
||||||
|
BRA whereDone
|
||||||
|
|
||||||
|
SETD.0 MyPath
|
||||||
|
SETD.1 Given
|
||||||
|
SETD.2 Beside
|
||||||
|
INIB 0d192
|
||||||
|
CALL pathBeside
|
||||||
|
BNQ whereTooLong
|
||||||
|
|
||||||
|
SETD.0 BesideText
|
||||||
|
SWI osPrintString
|
||||||
|
SETD.0 Beside
|
||||||
|
SWI osPrintString
|
||||||
|
CALL newLine
|
||||||
|
|
||||||
|
whereDone:
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
whereTooLong:
|
||||||
|
; A path that did not fit is a wrong answer, and saying it would be worse than saying so.
|
||||||
|
SETD.0 TooLongText
|
||||||
|
SWI osPrintString
|
||||||
|
CALL newLine
|
||||||
|
INIA 0x01
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
newLine:
|
||||||
|
SETD.0 NewLine
|
||||||
|
SWI osPrintString
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
#Base 0x3000
|
||||||
|
|
||||||
|
FromText:
|
||||||
|
"loaded from "
|
||||||
|
BesideText:
|
||||||
|
"beside it: "
|
||||||
|
TooLongText:
|
||||||
|
"that path would not fit"
|
||||||
|
NewLine:
|
||||||
|
0x0A 0x00
|
||||||
|
|
||||||
|
MyPath:
|
||||||
|
#Reserve 0d192
|
||||||
|
Beside:
|
||||||
|
#Reserve 0d192
|
||||||
|
Given:
|
||||||
|
#Reserve 0d64
|
||||||
|
|
||||||
|
; ---- Included last, after both segments have been based ----
|
||||||
|
;
|
||||||
|
; A #Base says where a whole segment begins and so has to be the first thing in it, and a
|
||||||
|
; library brings code and data of its own. Put in front, its #Data would be the first thing
|
||||||
|
; in the Data Segment and this program's own #Base would be too late. This is where Lander
|
||||||
|
; keeps its seven, for the same reason.
|
||||||
|
#Include path.asm
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
; A listing laid out to be read.
|
||||||
|
;
|
||||||
|
; > ls
|
||||||
|
; Copy.sbx Say.sbx Where.sbx
|
||||||
|
; Walk.sbx Lander where.sh
|
||||||
|
;
|
||||||
|
; > ls /Apps
|
||||||
|
;
|
||||||
|
; dir says what is there, one line each, with sizes and a tally of the disk. This says the
|
||||||
|
; names and nothing else, in columns, which is what you want ninety times in a hundred - and
|
||||||
|
; the two are separate programs rather than one with a switch because they answer different
|
||||||
|
; questions and neither answer is a worse version of the other.
|
||||||
|
;
|
||||||
|
; ---- Why this is a program and dir is not ----
|
||||||
|
;
|
||||||
|
; dir is built into the shell, and that is what makes it the one to trust when the disk is
|
||||||
|
; the thing being doubted: it is already in memory. This has to be loaded FROM the disk, so it
|
||||||
|
; cannot list a disk too broken to load it. The daily driver and the diagnostic, and it is
|
||||||
|
; worth having both.
|
||||||
|
;
|
||||||
|
; ---- Two passes, and no buffer at all ----
|
||||||
|
;
|
||||||
|
; Columns need the longest name before the first line can be printed, which usually means
|
||||||
|
; holding every name in memory - twenty four bytes each, and this format allows 1,024 entries
|
||||||
|
; on a disk. So instead the directory is walked TWICE: once to find the longest name and count
|
||||||
|
; them, once to print. A walk costs a read of each directory block and the disk buffer is
|
||||||
|
; already there, where a buffer for the names would be memory this program has to have whether
|
||||||
|
; the directory is large or not.
|
||||||
|
;
|
||||||
|
; ---- Across and then down, rather than down and then across ----
|
||||||
|
;
|
||||||
|
; Real listings go down the columns, so that names next to each other alphabetically are next
|
||||||
|
; to each other on the screen. THAT IS A REASON THAT DEPENDS ON SORTING, and nothing here
|
||||||
|
; sorts: entries come back in the order the directory holds them, which is the order they were
|
||||||
|
; made. With the order arbitrary, down-and-across buys nothing and costs a division to work
|
||||||
|
; out how many rows there are - on a machine that cannot divide.
|
||||||
|
;
|
||||||
|
; So this goes across. If sorting ever arrives, that is the moment to change it, and this
|
||||||
|
; comment is the reason why.
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
#Base 0x5000
|
||||||
|
|
||||||
|
start:
|
||||||
|
; A directory to list, if one was named. The shell puts the working directory back when a
|
||||||
|
; program exits, so this can walk off somewhere and not tidy up after itself.
|
||||||
|
SETD.0 Given
|
||||||
|
INIB 0d64
|
||||||
|
SWI osArgument
|
||||||
|
SETD.0 Given
|
||||||
|
LDA.0
|
||||||
|
BRA lsHere
|
||||||
|
SWI osChangeDir
|
||||||
|
BNQ lsNoSuchPlace
|
||||||
|
|
||||||
|
lsHere:
|
||||||
|
; ---- Pass one: how wide the widest of them is ----
|
||||||
|
RSTA
|
||||||
|
SETD.0 Longest
|
||||||
|
STA.0
|
||||||
|
SETD.0 Seen
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
SETD.0 Name
|
||||||
|
INIB 0d24
|
||||||
|
SWI osDirFirst
|
||||||
|
BRI lsMeasure
|
||||||
|
|
||||||
|
lsMeasureNext:
|
||||||
|
SETD.0 Name
|
||||||
|
INIB 0d24
|
||||||
|
SWI osDirNext
|
||||||
|
|
||||||
|
lsMeasure:
|
||||||
|
MVQA
|
||||||
|
SETD.0 Kind
|
||||||
|
STA.0
|
||||||
|
INIB 0xFF
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ lsMeasured
|
||||||
|
|
||||||
|
SETD.0 Seen
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
CALL nameLength
|
||||||
|
MVQA
|
||||||
|
|
||||||
|
; A directory wears a separator, which is a character of width like any other.
|
||||||
|
SETD.0 Kind
|
||||||
|
LDB.0
|
||||||
|
DECB
|
||||||
|
BNB lsMeasureWidth
|
||||||
|
INCA
|
||||||
|
|
||||||
|
lsMeasureWidth:
|
||||||
|
SETD.0 Longest
|
||||||
|
LDB.0
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRC lsMeasureKeep ; Borrowed, so this one is not the longest.
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
lsMeasureKeep:
|
||||||
|
BRI lsMeasureNext
|
||||||
|
|
||||||
|
lsMeasured:
|
||||||
|
; Nothing at all is said with nothing at all, which is what an empty directory looks like.
|
||||||
|
SETD.0 Seen
|
||||||
|
LDA.0
|
||||||
|
BRA lsDone
|
||||||
|
|
||||||
|
; ---- How many of them fit across ----
|
||||||
|
;
|
||||||
|
; Two spaces between columns, so that names never touch. Worked out by taking the cell width
|
||||||
|
; off the screen width until there is not room for another, because this machine cannot
|
||||||
|
; divide and the answer is never more than a handful.
|
||||||
|
SETD.0 Longest
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
INCA
|
||||||
|
SETD.0 Cell
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
INA 0x32
|
||||||
|
SETD.1 Across
|
||||||
|
STA.1 ; What is left of the line, counted down.
|
||||||
|
RSTA
|
||||||
|
SETD.1 Columns
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
lsFitting:
|
||||||
|
SETD.1 Across
|
||||||
|
LDA.1
|
||||||
|
SETD.1 Cell
|
||||||
|
LDB.1
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRC lsFitted ; Borrowed, so there is not room for another column.
|
||||||
|
MVQA
|
||||||
|
SETD.1 Across
|
||||||
|
STA.1
|
||||||
|
SETD.1 Columns
|
||||||
|
LDA.1
|
||||||
|
INCA
|
||||||
|
STA.1
|
||||||
|
BRI lsFitting
|
||||||
|
|
||||||
|
lsFitted:
|
||||||
|
; A name wider than the screen still gets a column of its own. One that runs over is better
|
||||||
|
; than one that is not shown.
|
||||||
|
SETD.0 Columns
|
||||||
|
LDA.0
|
||||||
|
BNA lsPrint
|
||||||
|
INIA 0d1
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
lsPrint:
|
||||||
|
RSTA
|
||||||
|
SETD.0 At
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
SETD.0 Name
|
||||||
|
INIB 0d24
|
||||||
|
SWI osDirFirst
|
||||||
|
BRI lsSay
|
||||||
|
|
||||||
|
lsSayNext:
|
||||||
|
SETD.0 Name
|
||||||
|
INIB 0d24
|
||||||
|
SWI osDirNext
|
||||||
|
|
||||||
|
lsSay:
|
||||||
|
MVQA
|
||||||
|
SETD.0 Kind
|
||||||
|
STA.0
|
||||||
|
INIB 0xFF
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ lsLastLine
|
||||||
|
|
||||||
|
; ---- The cell the last name started, finished now rather than then ----
|
||||||
|
;
|
||||||
|
; Padding AFTER a name puts spaces at the end of every line that does not fill its last
|
||||||
|
; column, and trailing whitespace on a line is the kind of thing that is invisible until
|
||||||
|
; something else reads the output. Padding before the next name instead means the spaces
|
||||||
|
; only ever go between two things, and a line ends on the last character of a name.
|
||||||
|
SETD.0 At
|
||||||
|
LDA.0
|
||||||
|
BRA lsColour ; First on its line, so there is nothing to finish.
|
||||||
|
CALL padOut
|
||||||
|
|
||||||
|
lsColour:
|
||||||
|
; ---- What it is, said in colour ----
|
||||||
|
;
|
||||||
|
; The attribute reaches a terminal as well as the screen now, so this is one mechanism and
|
||||||
|
; not two. A directory in blue, something that will run in green, an unfinished save in red,
|
||||||
|
; and everything else plain.
|
||||||
|
SETD.0 Kind
|
||||||
|
LDA.0
|
||||||
|
BRA lsFileInk
|
||||||
|
DECA
|
||||||
|
BRA lsBlue
|
||||||
|
INIA 0d1 ; An unfinished save, which is worth looking at.
|
||||||
|
BRI lsInk
|
||||||
|
lsBlue:
|
||||||
|
INIA 0d4
|
||||||
|
BRI lsInk
|
||||||
|
|
||||||
|
lsFileInk:
|
||||||
|
; ---- Whether it will run, asked by looking ----
|
||||||
|
;
|
||||||
|
; This was priced once as needing a RUNNABLE BIT in the directory entry, set by the
|
||||||
|
; filesystem from a list of magic numbers it would have to be taught - which was declined,
|
||||||
|
; twice, for putting format knowledge in the filesystem and giving two implementations a
|
||||||
|
; registry to keep in step.
|
||||||
|
;
|
||||||
|
; It costs nothing of the sort now. The shell decides what to run by READING the first block
|
||||||
|
; of a file, so "will this run" is a question with an answer already, and asking it is the
|
||||||
|
; same question the shell would ask. Nothing new is written down and nothing has to agree
|
||||||
|
; about anything: if the shell learns a third kind of runnable file, this is wrong until
|
||||||
|
; somebody adds two lines, and wrong in the direction of a listing that is less colourful.
|
||||||
|
;
|
||||||
|
; The price is a block read per file, which is what the bit existed to avoid. It is paid
|
||||||
|
; here rather than in dir on purpose: dir is the listing you audit and is built into the
|
||||||
|
; shell, and this is the one you read and is a program that was loaded off the disk anyway.
|
||||||
|
CALL runnable
|
||||||
|
BNQ lsPlain
|
||||||
|
INIA 0d2
|
||||||
|
BRI lsInk
|
||||||
|
lsPlain:
|
||||||
|
RSTA
|
||||||
|
lsInk:
|
||||||
|
OUTA 0x06
|
||||||
|
|
||||||
|
SETD.0 Name
|
||||||
|
SWI osPrintString
|
||||||
|
CALL nameLength
|
||||||
|
MVQA
|
||||||
|
|
||||||
|
SETD.0 Kind
|
||||||
|
LDB.0
|
||||||
|
DECB
|
||||||
|
BNB lsWidth
|
||||||
|
PSHA
|
||||||
|
SETD.0 Slash
|
||||||
|
SWI osPrintString
|
||||||
|
POPA
|
||||||
|
INCA
|
||||||
|
|
||||||
|
lsWidth:
|
||||||
|
; How much of the cell this name took, for whoever finishes it.
|
||||||
|
SETD.0 Used
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
; Back to plain before anything else is written, so that a scheme with paper in it would
|
||||||
|
; not paint the gap between the columns.
|
||||||
|
RSTA
|
||||||
|
OUTA 0x06
|
||||||
|
|
||||||
|
SETD.0 At
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
SETD.0 Columns
|
||||||
|
LDB.0
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ lsSayNext ; Room for another on this line.
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
SETD.0 At
|
||||||
|
STA.0
|
||||||
|
CALL newLine
|
||||||
|
BRI lsSayNext
|
||||||
|
|
||||||
|
lsLastLine:
|
||||||
|
; A line with something on it needs ending. One that ended on its last column does not, and
|
||||||
|
; a blank line under a listing looks like a listing with a gap in it.
|
||||||
|
SETD.0 At
|
||||||
|
LDA.0
|
||||||
|
BRA lsDone
|
||||||
|
CALL newLine
|
||||||
|
|
||||||
|
lsDone:
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
lsNoSuchPlace:
|
||||||
|
SETD.0 NoSuchPlace
|
||||||
|
SWI osPrintString
|
||||||
|
SETD.0 Given
|
||||||
|
SWI osPrintString
|
||||||
|
CALL newLine
|
||||||
|
INIA 0x01
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
; ---- How wide the name in the buffer is ----
|
||||||
|
;
|
||||||
|
; Q COMES BACK THE COUNT, not A. A RET puts A back the way the caller had it, so a subroutine
|
||||||
|
; that answers in A answers with nothing - and this one did, which made every gap between the
|
||||||
|
; columns the same width because the padding was subtracting whatever A happened to hold.
|
||||||
|
; Q is the ALU's output and nothing puts it back, which is why every answer on this machine
|
||||||
|
; comes home in it.
|
||||||
|
nameLength:
|
||||||
|
SETD.0 Name
|
||||||
|
RSTA
|
||||||
|
lengthNext:
|
||||||
|
LDB.0
|
||||||
|
BRB lengthDone
|
||||||
|
INCA
|
||||||
|
INCD.0
|
||||||
|
BRI lengthNext
|
||||||
|
lengthDone:
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- Will the shell start this? ----
|
||||||
|
;
|
||||||
|
; Q is zero if it will. The same two things the shell itself looks for in a first block: SBEX
|
||||||
|
; for a program, "#!" for a script. A file too short to have a first block at all cannot be
|
||||||
|
; either, and osFileBlock saying no is that answer arriving for free.
|
||||||
|
runnable:
|
||||||
|
SETD.0 Name
|
||||||
|
SETD.1 Block
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
SWI osFileBlock
|
||||||
|
BNQ runnableNo
|
||||||
|
|
||||||
|
; The shebang first, because two bytes settle it.
|
||||||
|
SETD.0 Block
|
||||||
|
LDA.0
|
||||||
|
INIB 0d35 ; '#'
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ runnableExec
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
INIB 0d33 ; '!'
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ runnableYes
|
||||||
|
|
||||||
|
runnableExec:
|
||||||
|
SETD.0 Block
|
||||||
|
SETD.1 ExecText
|
||||||
|
INIA 0d4
|
||||||
|
SETD.2 Left
|
||||||
|
STA.2
|
||||||
|
runnableSame:
|
||||||
|
LDA.0
|
||||||
|
LDB.1
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ runnableNo
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
SETD.2 Left
|
||||||
|
LDA.2
|
||||||
|
DECA
|
||||||
|
STA.2
|
||||||
|
BNA runnableSame
|
||||||
|
|
||||||
|
runnableYes:
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
runnableNo:
|
||||||
|
INIA 0x01
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; A is how much of the cell the name took. Fills the rest of it with spaces.
|
||||||
|
padOut:
|
||||||
|
; The cell less what the name took, and IN THAT ORDER. Written the other way round first,
|
||||||
|
; which borrowed on every name that was not the longest - so every one of them took the
|
||||||
|
; "wider than its cell" path below and the whole listing came out separated by one space.
|
||||||
|
SETD.0 Cell
|
||||||
|
LDA.0
|
||||||
|
SETD.0 Used
|
||||||
|
LDB.0
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRC padDone ; Wider than its cell, so one space keeps the names apart.
|
||||||
|
MVQA
|
||||||
|
BRA padOne
|
||||||
|
padLoop:
|
||||||
|
PSHA
|
||||||
|
SETD.0 Space
|
||||||
|
SWI osPrintString
|
||||||
|
POPA
|
||||||
|
DECA
|
||||||
|
BNA padLoop
|
||||||
|
RET
|
||||||
|
padOne:
|
||||||
|
padDone:
|
||||||
|
SETD.0 Space
|
||||||
|
SWI osPrintString
|
||||||
|
RET
|
||||||
|
|
||||||
|
newLine:
|
||||||
|
SETD.0 NewLine
|
||||||
|
SWI osPrintString
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
#Base 0x3000
|
||||||
|
|
||||||
|
ExecText:
|
||||||
|
"SBEX"
|
||||||
|
Slash:
|
||||||
|
"/"
|
||||||
|
Space:
|
||||||
|
" "
|
||||||
|
NoSuchPlace:
|
||||||
|
"there is no such directory: "
|
||||||
|
NewLine:
|
||||||
|
0x0A 0x00
|
||||||
|
|
||||||
|
Kind:
|
||||||
|
0x00
|
||||||
|
Longest:
|
||||||
|
0x00
|
||||||
|
Cell:
|
||||||
|
0x00
|
||||||
|
Columns:
|
||||||
|
0x00
|
||||||
|
Across:
|
||||||
|
0x00
|
||||||
|
At:
|
||||||
|
0x00
|
||||||
|
Seen:
|
||||||
|
0x00
|
||||||
|
Used:
|
||||||
|
0x00
|
||||||
|
Left:
|
||||||
|
0x00
|
||||||
|
Name:
|
||||||
|
#Reserve 0d24
|
||||||
|
Given:
|
||||||
|
#Reserve 0d64
|
||||||
|
; A whole block, because osFileBlock writes one whether the file fills it or not.
|
||||||
|
Block:
|
||||||
|
#Reserve 0d256
|
||||||
@@ -1,5 +1,22 @@
|
|||||||
; The label table: the only thing that survives between the two passes.
|
; The label table: the only thing that survives between the two passes.
|
||||||
;
|
;
|
||||||
|
; ---- KEPT IN ORDER, so that a lookup can halve it ----
|
||||||
|
;
|
||||||
|
; It used to be walked from the front, and that was most of the assembler. Every use of every
|
||||||
|
; label paid a scan of every label defined so far, so the cost grew with the program being
|
||||||
|
; built: assembling CosmOS on the machine took 1.83 billion cycles against the assembler's own
|
||||||
|
; 57 million - three times the source for thirty two times the time.
|
||||||
|
;
|
||||||
|
; Sorted and halved instead, it is 886 million: the walk was 52 per cent of the whole
|
||||||
|
; assembly. Small programs pay about a tenth more, because adding a label now moves the tail
|
||||||
|
; of the index up to make room and a short table was never expensive to walk. That is the
|
||||||
|
; right way round for a trade to fall.
|
||||||
|
;
|
||||||
|
; WHICH END IT IS SORTED FROM DOES NOT MATTER, and that is not carelessness. labAdd takes its
|
||||||
|
; insertion point from labFind, so the comparison that decides the order is the same one that
|
||||||
|
; searches it; turn it round and the table is built backwards and read backwards and nothing
|
||||||
|
; else can tell. Which is why breaking that comparison on purpose changes no output at all.
|
||||||
|
;
|
||||||
; Names are packed end to end in an arena and each index entry holds a pointer into it,
|
; Names are packed end to end in an arena and each index entry holds a pointer into it,
|
||||||
; rather than every entry carrying a field wide enough for the longest name. MEASURED on
|
; rather than every entry carrying a field wide enough for the longest name. MEASURED on
|
||||||
; CosmOS, which is the biggest thing this will ever be asked to assemble: 453 labels
|
; CosmOS, which is the biggest thing this will ever be asked to assemble: 453 labels
|
||||||
@@ -72,10 +89,50 @@ labAddFresh:
|
|||||||
CALL numCompare
|
CALL numCompare
|
||||||
BRC labAddCrowded ; The arena is smaller than where this name would end.
|
BRC labAddCrowded ; The arena is smaller than where this name would end.
|
||||||
|
|
||||||
; The index entry: where the name is about to go, and what it means.
|
; ---- Room made for it where it belongs ----
|
||||||
|
;
|
||||||
|
; The index is kept in order so that looking a name up can halve the table instead of
|
||||||
|
; walking it, and labFind left LabLo at the place this name sorts to. Everything from there
|
||||||
|
; on moves up four bytes, from the END backwards so that nothing is written over before it
|
||||||
|
; has been read.
|
||||||
|
;
|
||||||
|
; It costs about a third of the table copied per label added, which against the walk it
|
||||||
|
; replaces is nothing: the walk was paid once per USE of a label and this is paid once per
|
||||||
|
; label, and there are several uses of each.
|
||||||
SETD.0 LabWhich
|
SETD.0 LabWhich
|
||||||
SETD.2 LabCount
|
SETD.2 LabCount
|
||||||
CALL numSet
|
CALL numSet
|
||||||
|
|
||||||
|
labAddShift:
|
||||||
|
SETD.0 LabWhich
|
||||||
|
SETD.2 LabLo
|
||||||
|
CALL numCompare
|
||||||
|
BRQ labAddPlace ; Down to where it goes, so there is nothing left to move.
|
||||||
|
|
||||||
|
; The one below this place, moved up into it.
|
||||||
|
CALL labEntryAt
|
||||||
|
SETD.1 LabEntry
|
||||||
|
LDD.1.1 ; DP1 is where it goes.
|
||||||
|
SETD.0 LabWhich
|
||||||
|
CALL numBack
|
||||||
|
CALL labEntryAt
|
||||||
|
SETD.0 LabEntry
|
||||||
|
LDD.0.0 ; DP0 is where it comes from.
|
||||||
|
|
||||||
|
INIB 0d4
|
||||||
|
labAddShiftByte:
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
DECB
|
||||||
|
BNB labAddShiftByte
|
||||||
|
BRI labAddShift
|
||||||
|
|
||||||
|
labAddPlace:
|
||||||
|
SETD.0 LabWhich
|
||||||
|
SETD.2 LabLo
|
||||||
|
CALL numSet
|
||||||
CALL labEntryAt
|
CALL labEntryAt
|
||||||
|
|
||||||
SETD.1 LabEntry
|
SETD.1 LabEntry
|
||||||
@@ -149,14 +206,39 @@ labAddNo:
|
|||||||
labFind:
|
labFind:
|
||||||
SETD.2 LabSought
|
SETD.2 LabSought
|
||||||
STD.0.2
|
STD.0.2
|
||||||
SETD.0 LabWhich
|
|
||||||
CALL numZero
|
|
||||||
|
|
||||||
labFindLoop:
|
; ---- Halving the table rather than walking it ----
|
||||||
SETD.0 LabWhich
|
;
|
||||||
|
; The names are kept in order, so a lookup is eleven comparisons against two thousand
|
||||||
|
; entries where a walk was six hundred and seventy. THAT WAS MOST OF THE ASSEMBLER: every
|
||||||
|
; use of every label paid it, and it grew with the program being built, so assembling
|
||||||
|
; CosmOS on the machine cost 1.83 billion cycles against the assembler's own 57 million -
|
||||||
|
; three times the source for thirty two times the time.
|
||||||
|
;
|
||||||
|
; LabLo is left at the place the name WOULD go, which is what adding one needs and what a
|
||||||
|
; walk could never have handed back.
|
||||||
|
SETD.0 LabLo
|
||||||
|
CALL numZero
|
||||||
|
SETD.0 LabHi
|
||||||
SETD.2 LabCount
|
SETD.2 LabCount
|
||||||
|
CALL numSet
|
||||||
|
|
||||||
|
labFindStep:
|
||||||
|
SETD.0 LabLo
|
||||||
|
SETD.2 LabHi
|
||||||
CALL numCompare
|
CALL numCompare
|
||||||
BNC labFindMissing ; Walked the whole table without a match.
|
BNC labFindMissing ; Nothing left between them, so it is not here.
|
||||||
|
|
||||||
|
; The one in the middle. Adding two numbers under two thousand cannot overflow sixteen
|
||||||
|
; bits, so there is nothing to carry into.
|
||||||
|
SETD.0 LabWhich
|
||||||
|
SETD.2 LabLo
|
||||||
|
CALL numSet
|
||||||
|
SETD.0 LabWhich
|
||||||
|
SETD.2 LabHi
|
||||||
|
CALL numAdd
|
||||||
|
SETD.0 LabWhich
|
||||||
|
CALL numHalve
|
||||||
|
|
||||||
CALL labEntryAt
|
CALL labEntryAt
|
||||||
SETD.1 LabEntry
|
SETD.1 LabEntry
|
||||||
@@ -169,16 +251,33 @@ labFindLoop:
|
|||||||
INCD.0
|
INCD.0
|
||||||
STB.0
|
STB.0
|
||||||
|
|
||||||
|
; ---- sameText is already an ordering ----
|
||||||
|
;
|
||||||
|
; Q is the difference at the first character that differed, so zero is a match - and the
|
||||||
|
; Carry Flag from that same subtraction says which way round they were, and survives the
|
||||||
|
; return because nothing puts the Status register back. A name that runs out while the
|
||||||
|
; other carries on borrows against the other's character, which sorts the shorter first,
|
||||||
|
; which is what anybody means by alphabetical.
|
||||||
SETD.1 LabNamePointer
|
SETD.1 LabNamePointer
|
||||||
LDD.0.1
|
LDD.0.1
|
||||||
SETD.1 LabSought
|
SETD.1 LabSought
|
||||||
LDD.1.1
|
LDD.1.1
|
||||||
CALL sameText
|
CALL sameText
|
||||||
BRQ labFindGot
|
BRQ labFindGot
|
||||||
|
BRC labFindAbove ; The one in the table is the smaller, so look above it.
|
||||||
|
|
||||||
SETD.0 LabWhich
|
SETD.0 LabHi
|
||||||
|
SETD.2 LabWhich
|
||||||
|
CALL numSet
|
||||||
|
BRI labFindStep
|
||||||
|
|
||||||
|
labFindAbove:
|
||||||
|
SETD.0 LabLo
|
||||||
|
SETD.2 LabWhich
|
||||||
|
CALL numSet
|
||||||
|
SETD.0 LabLo
|
||||||
CALL numStep
|
CALL numStep
|
||||||
BRI labFindLoop
|
BRI labFindStep
|
||||||
|
|
||||||
labFindGot:
|
labFindGot:
|
||||||
SETD.1 LabEntry
|
SETD.1 LabEntry
|
||||||
@@ -285,6 +384,12 @@ LabEntry:
|
|||||||
0x00 0x00
|
0x00 0x00
|
||||||
LabOffset:
|
LabOffset:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
|
; Where a halving search has got to, and where a name that is not there would go. LabLo is
|
||||||
|
; what labAdd makes room at, which is the whole reason the search hands it back.
|
||||||
|
LabLo:
|
||||||
|
0x00 0x00
|
||||||
|
LabHi:
|
||||||
|
0x00 0x00
|
||||||
LabWhich:
|
LabWhich:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
LabLength:
|
LabLength:
|
||||||
@@ -297,18 +402,21 @@ LabEnd:
|
|||||||
; How many labels there may be, and how many bytes of name between them.
|
; How many labels there may be, and how many bytes of name between them.
|
||||||
;
|
;
|
||||||
; SIZED FOR THE ASSEMBLER ITSELF, which turns out to be the largest thing it is asked to
|
; SIZED FOR THE ASSEMBLER ITSELF, which turns out to be the largest thing it is asked to
|
||||||
; build: 555 labels and about 6,800 bytes of name, against CosmOS's 650 and 8,081 - CosmOS is
|
; build: the assembler is about 555 labels, against CosmOS's 1,341 and 16,758 bytes of name
|
||||||
; the bigger of the two now, and was the smaller when this was written. Running into either
|
; measured 2026-09-06 - CosmOS is much the bigger of the two now, and was the smaller when
|
||||||
|
; this was written. Running into either
|
||||||
; limit says so rather than writing past the end of the table, and that is what it did. The buffers live
|
; limit says so rather than writing past the end of the table, and that is what it did. The buffers live
|
||||||
; above the program rather than inside it - see the scratch map in Asm.asm.
|
; above the program rather than inside it - see the scratch map in Asm.asm.
|
||||||
; Fifteen hundred and thirty six names, and sixteen kilobytes to hold them in. Both were
|
; Two thousand and forty eight names, and twenty six kilobytes to hold them in. Both have
|
||||||
; half that, and the names ran out first: 8,081 bytes of 8,192, which is a hundred and
|
; now been doubled twice, and BOTH TIMES THE NAMES RAN OUT FIRST with the index a couple of
|
||||||
; eleven - and the thing that ran into it was one ordinary piece of work adding eighteen
|
; hundred behind them - 8,081 of 8,192 the first time, 16,758 of 16,384 the second. A name
|
||||||
; labels. THESE TWO MUST AGREE WITH THE SCRATCH MAP, which says where the room actually is.
|
; is thirteen bytes on average and an index entry is four, so the arena will always be the
|
||||||
|
; one that speaks; raising it alone would buy a few hundred labels and then the other wall.
|
||||||
|
; THESE TWO MUST AGREE WITH THE SCRATCH MAP, which says where the room actually is.
|
||||||
LabLimit:
|
LabLimit:
|
||||||
0x06 0x00
|
0x08 0x00
|
||||||
LabRoom:
|
LabRoom:
|
||||||
0x40 0x00
|
0x68 0x00
|
||||||
|
|
||||||
LabNamed:
|
LabNamed:
|
||||||
": "
|
": "
|
||||||
|
|||||||
@@ -112,6 +112,45 @@ numCompare:
|
|||||||
numCompareDone:
|
numCompareDone:
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
; Takes one off the two byte number at DP0. The mirror of numStep, and wanted for the same
|
||||||
|
; reason: walking an index backwards is what moving a run of entries up needs.
|
||||||
|
numBack:
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
BNA numBackLow ; The low half has something to take, so the high half is safe.
|
||||||
|
DECD.0
|
||||||
|
LDA.0
|
||||||
|
DECA
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
numBackLow:
|
||||||
|
DECA
|
||||||
|
STA.0
|
||||||
|
RET
|
||||||
|
|
||||||
|
; The two byte number at DP0 becomes half of itself.
|
||||||
|
;
|
||||||
|
; A and B are a CIRCULAR sixteen bit register, so a rotate right brings bit nought back in at
|
||||||
|
; the top rather than dropping it - which is a halving only once that bit is taken off again.
|
||||||
|
; The numbers this is asked about are index positions, well under thirty two thousand, so the
|
||||||
|
; top bit was nought before the rotate and clearing it afterwards loses nothing.
|
||||||
|
numHalve:
|
||||||
|
LDA.0
|
||||||
|
INCD.0
|
||||||
|
LDB.0
|
||||||
|
SHR
|
||||||
|
|
||||||
|
; The low half goes down FIRST, because taking the wrapped bit off the high half wants B
|
||||||
|
; for the mask and there is nowhere else to keep it.
|
||||||
|
STB.0
|
||||||
|
INIB 0x7F
|
||||||
|
AND
|
||||||
|
MVQA
|
||||||
|
DECD.0
|
||||||
|
STA.0
|
||||||
|
RET
|
||||||
|
|
||||||
; Q is zero if the strings at DP0 and DP1 are the same, both ending in a zero byte.
|
; Q is zero if the strings at DP0 and DP1 are the same, both ending in a zero byte.
|
||||||
;
|
;
|
||||||
; Down here rather than with the label table, where it started, because four separate
|
; Down here rather than with the label table, where it started, because four separate
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
; bad enough; a stale number sitting next to the correction is worse, because whichever
|
; bad enough; a stale number sitting next to the correction is worse, because whichever
|
||||||
; one a reader takes is a coin toss.
|
; one a reader takes is a coin toss.
|
||||||
;
|
;
|
||||||
; 0x5000 6144 the label index, 1536 entries of four
|
; 0x5000 8192 the label index, 2048 entries of four
|
||||||
; 0x6800 16384 the label names, packed end to end
|
; 0x7000 26624 the label names, packed end to end
|
||||||
; 0xA800 256 one block of the output file, on its way to the disk
|
; 0xD800 256 one block of the output file, on its way to the disk
|
||||||
; 0xA900 17152 free
|
; 0xD900 1792 free
|
||||||
; 0xE000 1792 the vector names, 64 entries of twenty eight
|
; 0xE000 1792 the vector names, 64 entries of twenty eight
|
||||||
; 0xE700 2048 the reader's stack, six levels of 301
|
; 0xE700 2048 the reader's stack, six levels of 301
|
||||||
; 0xEF00 368 which files have been included, sixteen names of 23
|
; 0xEF00 368 which files have been included, sixteen names of 23
|
||||||
@@ -69,6 +69,17 @@
|
|||||||
; among buffers that do not need it, because an unclaimed page is available to whichever
|
; among buffers that do not need it, because an unclaimed page is available to whichever
|
||||||
; one turns out to want it.
|
; one turns out to want it.
|
||||||
;
|
;
|
||||||
|
; ---- And one turned out to want it ----
|
||||||
|
;
|
||||||
|
; Sixteen kilobytes of names was half full when that was written and ran out on 2026-09-06,
|
||||||
|
; on an ordinary day's work on the shell: 16,758 bytes of name against 16,384. The index was
|
||||||
|
; 1,341 of 1,536 in the same breath, which is the paragraph above this one happening again -
|
||||||
|
; two ceilings, and lifting only the one that spoke would have bought two hundred labels.
|
||||||
|
;
|
||||||
|
; So both, out of the free page, and both left at about a third clear rather than at half:
|
||||||
|
; the names to 26,624 and the index to 2,048. What is left over is 1,792 bytes, kept
|
||||||
|
; unclaimed for the same reason the seventeen kilobytes were.
|
||||||
|
;
|
||||||
; That ends at 0xF070, with the Stack coming down from 0xFFFF above it - nearly four
|
; That ends at 0xF070, with the Stack coming down from 0xFFFF above it - nearly four
|
||||||
; kilobytes, against the tens of bytes of CALL frames this ever nests.
|
; kilobytes, against the tens of bytes of CALL frames this ever nests.
|
||||||
;
|
;
|
||||||
@@ -78,16 +89,17 @@
|
|||||||
; list moved up rather than the stack simply being asked to fit.
|
; list moved up rather than the stack simply being asked to fit.
|
||||||
;
|
;
|
||||||
; THE TWO THINGS THAT DECIDE THESE SIZES are the largest program it will be asked to build
|
; THE TWO THINGS THAT DECIDE THESE SIZES are the largest program it will be asked to build
|
||||||
; and the largest one it will be asked to read. CosmOS is 475 labels and 9,564 bytes of
|
; and the largest one it will be asked to read. Measured 2026-09-06: CosmOS is 1,341 labels
|
||||||
; output; the assembler itself is 555 labels, about 6,800 bytes of name and 11,648 of output. The
|
; and 16,758 bytes of name, and the assembler itself is far smaller. It was the other way
|
||||||
; second is bigger than the first, which is worth knowing: the hardest thing this assembles
|
; round when this file was written - "the hardest thing this assembles is not the operating
|
||||||
; is not the operating system, it is itself.
|
; system, it is itself" - and the system has grown past it since. The numbers here are worth
|
||||||
|
; measuring again rather than believing, which is why they carry a date.
|
||||||
ScratchLabIndex:
|
ScratchLabIndex:
|
||||||
0x50 0x00
|
0x50 0x00
|
||||||
ScratchLabArena:
|
ScratchLabArena:
|
||||||
0x68 0x00
|
0x70 0x00
|
||||||
ScratchWindow:
|
ScratchWindow:
|
||||||
0xA8 0x00
|
0xD8 0x00
|
||||||
ScratchVecNames:
|
ScratchVecNames:
|
||||||
0xE0 0x00
|
0xE0 0x00
|
||||||
ScratchSrcStack:
|
ScratchSrcStack:
|
||||||
|
|||||||
@@ -105,6 +105,143 @@ srcRewindNo:
|
|||||||
ADD ; Q is not zero: it is not.
|
ADD ; Q is not zero: it is not.
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
; ---- Everything up to and including the next newline, thrown away ----
|
||||||
|
;
|
||||||
|
; Q is zero if a newline was found, and something else if the file ended first.
|
||||||
|
;
|
||||||
|
; THIS IS MOST OF WHAT THE ASSEMBLER READS. Measured on this repository: 87 per cent of what
|
||||||
|
; Say.asm pulls in is comment and blank line, and 59 per cent of CosmOS - the house style is
|
||||||
|
; dense commentary, so the assembler is penalised more than most by its own sources. Every one
|
||||||
|
; of those bytes used to go through tokGet and srcNext and be classified and thrown away.
|
||||||
|
;
|
||||||
|
; The saving is not the classifying, it is the BOOKKEEPING. srcNext loads and stores the
|
||||||
|
; walking pointer through memory for every single character, and asks numCompare whether the
|
||||||
|
; buffer is used up. Here the pointer lives in DP0 for a whole run and the newline lives in B,
|
||||||
|
; so a comment byte costs a load, a compare and two steps.
|
||||||
|
;
|
||||||
|
; A run is capped at 255 so that one byte can count it, which is why the outer loop exists at
|
||||||
|
; all: a full block is 256, one more than a byte holds.
|
||||||
|
srcSkipComment:
|
||||||
|
SETD.0 SrcEnded
|
||||||
|
LDA.0
|
||||||
|
BNA srcSkipEnded
|
||||||
|
|
||||||
|
srcSkipBlock:
|
||||||
|
; What is left of the buffer.
|
||||||
|
SETD.0 SrcSpan
|
||||||
|
SETD.2 SrcCount
|
||||||
|
CALL numSet
|
||||||
|
SETD.0 SrcSpan
|
||||||
|
SETD.2 SrcAt
|
||||||
|
CALL numTake
|
||||||
|
|
||||||
|
SETD.0 SrcSpan
|
||||||
|
LDA.0
|
||||||
|
INCD.0
|
||||||
|
LDB.0
|
||||||
|
OR
|
||||||
|
BRQ srcSkipRefill ; Used up, so the next block or the end.
|
||||||
|
|
||||||
|
SETD.0 SrcSpan
|
||||||
|
LDA.0
|
||||||
|
BNA srcSkipCapped ; A high half at all means 256, which is one too many to count.
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
BRI srcSkipCounted
|
||||||
|
srcSkipCapped:
|
||||||
|
INIA 0xFF
|
||||||
|
srcSkipCounted:
|
||||||
|
SETD.0 SrcRun
|
||||||
|
STA.0
|
||||||
|
SETD.0 SrcTaken
|
||||||
|
STA.0 ; Where the run began, to work out what it swallowed.
|
||||||
|
|
||||||
|
SETD.2 SrcPointer
|
||||||
|
LDD.0.2
|
||||||
|
SETD.1 SrcRun
|
||||||
|
INIB 0x0A ; The newline, held in B for the whole run.
|
||||||
|
|
||||||
|
srcSkipChar:
|
||||||
|
LDA.0
|
||||||
|
XOR
|
||||||
|
BRQ srcSkipNewline
|
||||||
|
INCD.0
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
BNA srcSkipChar
|
||||||
|
|
||||||
|
; The run ended without a newline in it, so the rest of the block is comment too.
|
||||||
|
SETD.2 SrcPointer
|
||||||
|
STD.0.2
|
||||||
|
CALL srcSkipAccount
|
||||||
|
BRI srcSkipBlock
|
||||||
|
|
||||||
|
srcSkipNewline:
|
||||||
|
; Past the newline itself, which belongs to the comment.
|
||||||
|
INCD.0
|
||||||
|
SETD.1 SrcRun
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
SETD.2 SrcPointer
|
||||||
|
STD.0.2
|
||||||
|
CALL srcSkipAccount
|
||||||
|
|
||||||
|
; A newline is what makes the next character part of the next line, counted as it goes past
|
||||||
|
; exactly as srcNext counts it.
|
||||||
|
SETD.0 SrcLine
|
||||||
|
CALL numStep
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
srcSkipRefill:
|
||||||
|
CALL srcLoad
|
||||||
|
BRQ srcSkipBlock ; Another block of the same file.
|
||||||
|
|
||||||
|
; ---- A file that runs out inside a comment ----
|
||||||
|
;
|
||||||
|
; The same thing srcAtEnd does for a character, and it has to be done the same way here: an
|
||||||
|
; included file ending puts the one that included it back on, and the comment carries on
|
||||||
|
; into it exactly as it did when this was read a character at a time.
|
||||||
|
;
|
||||||
|
; NOT DOING THIS WAS THE WHOLE OF THE FIRST VERSION'S BUG. services.asm ends inside a
|
||||||
|
; comment, so an include of it never came back, and every label after the #Include line -
|
||||||
|
; the whole of the program doing the including - was quietly never defined. It surfaced as
|
||||||
|
; "no label of that name is defined anywhere" pointing at a line whose label was three
|
||||||
|
; lines from the end of the file.
|
||||||
|
INIA 0d1
|
||||||
|
SETD.0 SrcEnded
|
||||||
|
STA.0
|
||||||
|
SETD.0 SrcDepth
|
||||||
|
LDA.0
|
||||||
|
BRA srcSkipEnded ; Nothing included this, so there is no more source anywhere.
|
||||||
|
CALL srcPop
|
||||||
|
BRI srcSkipComment
|
||||||
|
|
||||||
|
srcSkipEnded:
|
||||||
|
INIA 0x01
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; How much of the run went by, added to how far into the buffer we have read.
|
||||||
|
srcSkipAccount:
|
||||||
|
SETD.0 SrcTaken
|
||||||
|
LDA.0
|
||||||
|
SETD.0 SrcRun
|
||||||
|
LDB.0
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
MVQA
|
||||||
|
SETD.0 SrcAt
|
||||||
|
CALL numAddByte
|
||||||
|
RET
|
||||||
|
|
||||||
; The next character of the file, into SrcChar. Q is zero if there was one, and something
|
; The next character of the file, into SrcChar. Q is zero if there was one, and something
|
||||||
; else at the end of the file.
|
; else at the end of the file.
|
||||||
srcNext:
|
srcNext:
|
||||||
@@ -112,11 +249,29 @@ srcNext:
|
|||||||
LDA.0
|
LDA.0
|
||||||
BNA srcAtEnd
|
BNA srcAtEnd
|
||||||
|
|
||||||
; Is the buffer used up? SrcAt counts how far into it we have read and SrcCount how many
|
; ---- Is the buffer used up? ----
|
||||||
; of its bytes are the file's, which is 256 for every block but a short last one.
|
;
|
||||||
|
; SrcAt counts how far into it we have read and SrcCount how many of its bytes are the
|
||||||
|
; file's, which is 256 for every block but a short last one.
|
||||||
|
;
|
||||||
|
; ASKED HERE RATHER THAN BY numCompare, because this is the hottest question in the
|
||||||
|
; assembler - every character of every pass asks it - and a call to answer it costs more
|
||||||
|
; than the answer. The high bytes settle it almost every time and settle it FIRST: a full
|
||||||
|
; block is 256, so SrcCount's high byte is one and SrcAt's is nought until the last
|
||||||
|
; sixteenth of the block. Six instructions, where the call was twenty.
|
||||||
SETD.0 SrcAt
|
SETD.0 SrcAt
|
||||||
SETD.2 SrcCount
|
SETD.2 SrcCount
|
||||||
CALL numCompare
|
LDA.0
|
||||||
|
LDB.2
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ srcHaveByte
|
||||||
|
INCD.0
|
||||||
|
INCD.2
|
||||||
|
LDA.0
|
||||||
|
LDB.2
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
BNQ srcHaveByte
|
BNQ srcHaveByte
|
||||||
CALL srcLoad
|
CALL srcLoad
|
||||||
BNQ srcAtEnd
|
BNQ srcAtEnd
|
||||||
@@ -129,19 +284,31 @@ srcHaveByte:
|
|||||||
STD.0.1
|
STD.0.1
|
||||||
SETD.0 SrcChar
|
SETD.0 SrcChar
|
||||||
STA.0
|
STA.0
|
||||||
SETD.0 SrcAt
|
|
||||||
CALL numStep
|
|
||||||
|
|
||||||
; A newline is what makes the next character part of the next line. Counting it here,
|
; ---- A newline is what makes the next character part of the next line ----
|
||||||
; as it is handed out, means the line number always describes the character just given.
|
;
|
||||||
SETD.0 SrcChar
|
; Counted here, as the character is handed out, so the line number always describes the
|
||||||
LDA.0
|
; character just given. A still holds it - nothing since the load has wanted it - where
|
||||||
|
; this used to fetch it back out of SrcChar it had just been put in.
|
||||||
INIB 0x0A
|
INIB 0x0A
|
||||||
XOR
|
XOR
|
||||||
BNQ srcNextDone
|
BNQ srcNextStep
|
||||||
SETD.0 SrcLine
|
SETD.0 SrcLine
|
||||||
CALL numStep
|
CALL numStep
|
||||||
|
|
||||||
|
srcNextStep:
|
||||||
|
; And one further into the buffer, counted the same way and for the same reason.
|
||||||
|
SETD.0 SrcAt
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
BNC srcNextDone
|
||||||
|
DECD.0
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
|
||||||
srcNextDone:
|
srcNextDone:
|
||||||
RSTA
|
RSTA
|
||||||
RSTB
|
RSTB
|
||||||
@@ -576,6 +743,23 @@ SrcStateBytes:
|
|||||||
SrcDepthLimit:
|
SrcDepthLimit:
|
||||||
0d6
|
0d6
|
||||||
|
|
||||||
|
; ---- OUTSIDE the state block above, and that is not an accident ----
|
||||||
|
;
|
||||||
|
; What is left of the buffer, how much of it one run may take, and where that run began.
|
||||||
|
; They live for the length of one call to srcSkipComment and never across an include, so they
|
||||||
|
; are not part of what a file remembers while another one is being read.
|
||||||
|
;
|
||||||
|
; PUT AMONG THE STATE FIRST, which added four bytes to a block that is copied whole by a
|
||||||
|
; count written down somewhere else - so every saved file lost the last four bytes of itself
|
||||||
|
; and an include came back with its buffer pointer wrong. The comment two paragraphs up says
|
||||||
|
; not to do it, and says it in capitals.
|
||||||
|
SrcSpan:
|
||||||
|
0x00 0x00
|
||||||
|
SrcRun:
|
||||||
|
0x00
|
||||||
|
SrcTaken:
|
||||||
|
0x00
|
||||||
|
|
||||||
; Where an include is looked for when it is not beside you. One fixed place rather than a
|
; Where an include is looked for when it is not beside you. One fixed place rather than a
|
||||||
; list somebody sets, for the same reason the shell has one fixed place for programs: a
|
; list somebody sets, for the same reason the shell has one fixed place for programs: a
|
||||||
; list would need somewhere to live between one boot and the next.
|
; list would need somewhere to live between one boot and the next.
|
||||||
|
|||||||
@@ -54,13 +54,17 @@ tokSkip:
|
|||||||
BNQ tokBegin
|
BNQ tokBegin
|
||||||
|
|
||||||
tokComment:
|
tokComment:
|
||||||
CALL tokGet
|
; ---- Thrown away by the reader, not read one character at a time ----
|
||||||
|
;
|
||||||
|
; This used to walk the comment through tokGet and srcNext, classifying every byte of it on
|
||||||
|
; the way to discarding it. Comments are most of what this assembler reads - 87 per cent of
|
||||||
|
; what a small program pulls in, once the libraries it includes are counted - so it was most
|
||||||
|
; of the work, and all of it thrown away.
|
||||||
|
;
|
||||||
|
; A pushed back character cannot be sitting there: this is only reached from tokSkip, which
|
||||||
|
; got the semicolon out of tokGet and did not put anything back.
|
||||||
|
CALL srcSkipComment
|
||||||
BNQ tokEnded
|
BNQ tokEnded
|
||||||
SETD.0 TokChar
|
|
||||||
LDA.0
|
|
||||||
INIB 0x0A
|
|
||||||
XOR
|
|
||||||
BNQ tokComment ; Everything up to the newline belongs to the comment.
|
|
||||||
BRI tokSkip
|
BRI tokSkip
|
||||||
|
|
||||||
tokBegin:
|
tokBegin:
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#! script
|
||||||
|
; The launcher for Lunar Porter, which is what typing "Lander" actually starts.
|
||||||
|
;
|
||||||
|
; An application with things of its own gets a DIRECTORY of its own, and a typed word names
|
||||||
|
; a file rather than a directory - so what sits in /Apps is this, and the game sits with its
|
||||||
|
; tune in /Packages/app.Lander. The shell decides what to run by reading it, so this file
|
||||||
|
; beginning "#!" and Say.sbx beginning "SBEX" go down the same road and neither the shell nor
|
||||||
|
; the person has to know which kind of thing they started.
|
||||||
|
;
|
||||||
|
; That indirection is the point of it. Where the game physically lives stops being part of
|
||||||
|
; its name, so an installer may put it on another disk and change only this line.
|
||||||
|
;
|
||||||
|
; #quiet, because a launcher is machinery and not narration. Without it, starting a game
|
||||||
|
; prints the path of the thing it is about to start.
|
||||||
|
#quiet
|
||||||
|
0:/Packages/app.Lander/Lander.sbx $args
|
||||||
+344
-31
File diff suppressed because one or more lines are too long
+914
-101
File diff suppressed because it is too large
Load Diff
@@ -1916,6 +1916,278 @@ sbfsEntryBounds:
|
|||||||
sbfsBoundsDone:
|
sbfsBoundsDone:
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
; ---- What the whole disk is holding ----
|
||||||
|
;
|
||||||
|
; SbfsUsedBlocks becomes what every entry on the disk holds between them, and SbfsUsedEntries
|
||||||
|
; how many slots are filled. Q is zero if the disk could be read.
|
||||||
|
;
|
||||||
|
; EVERY ENTRY, NOT EVERY ENTRY IN A DIRECTORY. Free space is a fact about the disk and not
|
||||||
|
; about where you are standing, so this walks the directory table itself the way allocating
|
||||||
|
; does, rather than using sbfsFirst and sbfsNext - which stop only on entries whose parent is
|
||||||
|
; the working directory, and would have called the same disk emptier from one directory than
|
||||||
|
; from another.
|
||||||
|
;
|
||||||
|
; A DIRECTORY HOLDS NO BLOCKS and adds nought, which is true by its fields rather than by a
|
||||||
|
; test here. An unfinished save holds all of its, and is counted, because the blocks are
|
||||||
|
; spoken for whatever the entry is called - and a listing that left them out would promise
|
||||||
|
; room that is not there.
|
||||||
|
;
|
||||||
|
; The count in the superblock is not consulted. sbfs.asm calls it "a note rather than the
|
||||||
|
; truth" in three places, and this is the truth: it costs a read of every directory block,
|
||||||
|
; which is what the honest answer costs.
|
||||||
|
sbfsSpace:
|
||||||
|
RSTA
|
||||||
|
SETD.0 SbfsUsedBlocks
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
STA.0
|
||||||
|
SETD.0 SbfsUsedEntries
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
SETD.0 SbfsDirStart
|
||||||
|
SETD.1 SbfsBlock
|
||||||
|
CALL sbfsCopyWord
|
||||||
|
SETD.0 SbfsDirBlocks
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
SETD.1 SbfsLeft
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
sbfsSpaceBlock:
|
||||||
|
CALL sbfsReadBlock
|
||||||
|
BNQ sbfsSpaceFailed
|
||||||
|
SETD.1 SbfsBuffer
|
||||||
|
CALL sbfsBufferOut
|
||||||
|
SETD.2 SbfsBuffer
|
||||||
|
INIA 0d8
|
||||||
|
SETD.1 SbfsCount
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
sbfsSpaceEntry:
|
||||||
|
LDA.2
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
BRQ sbfsSpaceNext ; A free slot holds nothing and fills nothing.
|
||||||
|
|
||||||
|
SETD.0 SbfsUsedEntries
|
||||||
|
CALL sbfsStepWord
|
||||||
|
|
||||||
|
; The blocks this entry holds, which are at the same offset in an entry as they are
|
||||||
|
; everywhere else. sbfsEntryBounds wants DP2 on the entry and gives back both ends, and the
|
||||||
|
; length is the difference - worked out that way rather than read directly, so that one
|
||||||
|
; routine owns where in an entry those fields are.
|
||||||
|
PSHD.2
|
||||||
|
CALL sbfsEntryBounds
|
||||||
|
SETD.0 SbfsEntryEnd
|
||||||
|
SETD.2 SbfsEntryStart
|
||||||
|
CALL sbfsSubWord
|
||||||
|
SETD.0 SbfsUsedBlocks
|
||||||
|
SETD.2 SbfsEntryEnd
|
||||||
|
CALL sbfsAddWord
|
||||||
|
POPD.2
|
||||||
|
|
||||||
|
sbfsSpaceNext:
|
||||||
|
DPUP.2 0d32
|
||||||
|
SETD.1 SbfsCount
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
BNA sbfsSpaceEntry
|
||||||
|
|
||||||
|
SETD.0 SbfsBlock
|
||||||
|
CALL sbfsStepWord
|
||||||
|
SETD.1 SbfsLeft
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
BNA sbfsSpaceBlock
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
sbfsSpaceFailed:
|
||||||
|
INIA 0x01
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- The longest run of free blocks there is ----
|
||||||
|
;
|
||||||
|
; SbfsBiggest becomes the largest number of blocks that are free AND NEXT TO EACH OTHER.
|
||||||
|
; Q is zero if the disk could be read.
|
||||||
|
;
|
||||||
|
; This is the number that says whether a file will fit. Files are laid down contiguously -
|
||||||
|
; first fit, with the directory itself as the map and no allocation table anywhere - so a disk
|
||||||
|
; with a thousand blocks free in ten scattered pieces will refuse a file of two hundred, and
|
||||||
|
; the free total gives no hint of it.
|
||||||
|
;
|
||||||
|
; ---- The same trick allocating uses, for the same reason ----
|
||||||
|
;
|
||||||
|
; There is no sort on this machine and the entries are in no order, so the gaps cannot simply
|
||||||
|
; be listed. Instead a candidate walks the disk: each pass looks for the used extent nearest
|
||||||
|
; at or after it, and anything the candidate is standing inside pushes the candidate to the
|
||||||
|
; far end of it and starts the pass again.
|
||||||
|
;
|
||||||
|
; So the cost is a pass of the directory table per gap, rather than per file - and it is a
|
||||||
|
; measurement that costs nothing on a disk with one gap at the end, which is what a disk that
|
||||||
|
; has only been appended to looks like, and costs more the more fragmented the disk is. Which
|
||||||
|
; is the right way round: it is slow exactly when it has something to say.
|
||||||
|
sbfsLargestRun:
|
||||||
|
RSTA
|
||||||
|
SETD.0 SbfsBiggest
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
STA.0
|
||||||
|
CALL sbfsFirstData
|
||||||
|
|
||||||
|
sbfsRunPass:
|
||||||
|
; Nothing is in the way yet this time round, and the nearest thing ahead is the end of the
|
||||||
|
; disk - which is the answer when there is nothing ahead at all.
|
||||||
|
RSTA
|
||||||
|
SETD.0 SbfsRunMoved
|
||||||
|
STA.0
|
||||||
|
SETD.0 SbfsRunNext
|
||||||
|
SETD.2 SbfsDiskBlocks
|
||||||
|
CALL sbfsSetWord
|
||||||
|
|
||||||
|
SETD.0 SbfsDirStart
|
||||||
|
SETD.1 SbfsBlock
|
||||||
|
CALL sbfsCopyWord
|
||||||
|
SETD.0 SbfsDirBlocks
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
SETD.1 SbfsLeft
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
sbfsRunBlock:
|
||||||
|
CALL sbfsReadBlock
|
||||||
|
BNQ sbfsRunFailed
|
||||||
|
SETD.1 SbfsBuffer
|
||||||
|
CALL sbfsBufferOut
|
||||||
|
SETD.2 SbfsBuffer
|
||||||
|
INIA 0d8
|
||||||
|
SETD.1 SbfsCount
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
sbfsRunEntry:
|
||||||
|
LDA.2
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
BRQ sbfsRunNextEntry ; A free slot is in nobody's way.
|
||||||
|
LDA.2
|
||||||
|
INIB 0x02
|
||||||
|
AND
|
||||||
|
BNQ sbfsRunNextEntry ; Nor is a directory, which holds no blocks - see allocating.
|
||||||
|
|
||||||
|
CALL sbfsEntryBounds
|
||||||
|
PSHD.2 ; The comparisons want DP2, and the walk wants it back.
|
||||||
|
|
||||||
|
; Behind the candidate entirely, so it says nothing about what is ahead.
|
||||||
|
SETD.0 SbfsEntryEnd
|
||||||
|
SETD.2 SbfsCandidate
|
||||||
|
CALL sbfsCompareWord
|
||||||
|
BRC sbfsRunClear
|
||||||
|
BRQ sbfsRunClear
|
||||||
|
|
||||||
|
; Beginning at or before the candidate, so the candidate is standing inside it. It moves to
|
||||||
|
; the far end and the pass begins again, exactly as allocating does.
|
||||||
|
SETD.0 SbfsEntryStart
|
||||||
|
SETD.2 SbfsCandidate
|
||||||
|
CALL sbfsCompareWord
|
||||||
|
BRC sbfsRunStraddles
|
||||||
|
BRQ sbfsRunStraddles
|
||||||
|
|
||||||
|
; Ahead of the candidate. Is it the nearest thing ahead so far?
|
||||||
|
SETD.0 SbfsEntryStart
|
||||||
|
SETD.2 SbfsRunNext
|
||||||
|
CALL sbfsCompareWord
|
||||||
|
BNC sbfsRunClear
|
||||||
|
SETD.0 SbfsRunNext
|
||||||
|
SETD.2 SbfsEntryStart
|
||||||
|
CALL sbfsSetWord
|
||||||
|
BRI sbfsRunClear
|
||||||
|
|
||||||
|
sbfsRunStraddles:
|
||||||
|
SETD.0 SbfsCandidate
|
||||||
|
SETD.2 SbfsEntryEnd
|
||||||
|
CALL sbfsSetWord
|
||||||
|
INIA 0x01
|
||||||
|
SETD.0 SbfsRunMoved
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
sbfsRunClear:
|
||||||
|
POPD.2
|
||||||
|
|
||||||
|
sbfsRunNextEntry:
|
||||||
|
DPUP.2 0d32
|
||||||
|
SETD.1 SbfsCount
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
BNA sbfsRunEntry
|
||||||
|
|
||||||
|
SETD.0 SbfsBlock
|
||||||
|
CALL sbfsStepWord
|
||||||
|
SETD.1 SbfsLeft
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
BNA sbfsRunBlock
|
||||||
|
|
||||||
|
; The pass is over. If the candidate moved, everything measured against where it used to be
|
||||||
|
; was measured against the wrong place, so the pass is worth nothing and is done again.
|
||||||
|
SETD.0 SbfsRunMoved
|
||||||
|
LDA.0
|
||||||
|
BNA sbfsRunPass
|
||||||
|
|
||||||
|
; Nothing moved, so the gap from the candidate to the nearest thing ahead is a real one.
|
||||||
|
SETD.0 SbfsRunGap
|
||||||
|
SETD.2 SbfsRunNext
|
||||||
|
CALL sbfsSetWord
|
||||||
|
SETD.0 SbfsRunGap
|
||||||
|
SETD.2 SbfsCandidate
|
||||||
|
CALL sbfsSubWord
|
||||||
|
|
||||||
|
SETD.0 SbfsBiggest
|
||||||
|
SETD.2 SbfsRunGap
|
||||||
|
CALL sbfsCompareWord
|
||||||
|
BNC sbfsRunKept ; What is already remembered is the same or bigger.
|
||||||
|
SETD.0 SbfsBiggest
|
||||||
|
SETD.2 SbfsRunGap
|
||||||
|
CALL sbfsSetWord
|
||||||
|
|
||||||
|
sbfsRunKept:
|
||||||
|
; Past the end of the disk is the end of the walk. Otherwise the candidate goes to the
|
||||||
|
; thing that stopped it, which the next pass finds itself standing inside and steps over.
|
||||||
|
SETD.0 SbfsRunNext
|
||||||
|
SETD.2 SbfsDiskBlocks
|
||||||
|
CALL sbfsCompareWord
|
||||||
|
BNC sbfsRunDone
|
||||||
|
SETD.0 SbfsCandidate
|
||||||
|
SETD.2 SbfsRunNext
|
||||||
|
CALL sbfsSetWord
|
||||||
|
BRI sbfsRunPass
|
||||||
|
|
||||||
|
sbfsRunDone:
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
sbfsRunFailed:
|
||||||
|
INIA 0x01
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
; Finds a run of SbfsWantBlocks free blocks and puts where it begins in SbfsFileStart.
|
; Finds a run of SbfsWantBlocks free blocks and puts where it begins in SbfsFileStart.
|
||||||
; Q is zero if there was room.
|
; Q is zero if there was room.
|
||||||
;
|
;
|
||||||
@@ -3691,6 +3963,24 @@ SbfsScratch1:
|
|||||||
SbfsPathWanted:
|
SbfsPathWanted:
|
||||||
0x00
|
0x00
|
||||||
|
|
||||||
|
; What every entry on the disk holds between them, and how many slots are filled. Worked out
|
||||||
|
; by sbfsSpace, which reads the whole directory table to do it.
|
||||||
|
; The longest run of free blocks, and the sweep that finds it: where it has got to ahead of
|
||||||
|
; the candidate, whether anything pushed the candidate this pass, and the gap being measured.
|
||||||
|
SbfsBiggest:
|
||||||
|
0x00 0x00
|
||||||
|
SbfsRunNext:
|
||||||
|
0x00 0x00
|
||||||
|
SbfsRunGap:
|
||||||
|
0x00 0x00
|
||||||
|
SbfsRunMoved:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
SbfsUsedBlocks:
|
||||||
|
0x00 0x00
|
||||||
|
SbfsUsedEntries:
|
||||||
|
0x00 0x00
|
||||||
|
|
||||||
SbfsFileStart:
|
SbfsFileStart:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
SbfsFileBlocks:
|
SbfsFileBlocks:
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ scriptSlotAt:
|
|||||||
DECA
|
DECA
|
||||||
BRA scriptSlotDone
|
BRA scriptSlotDone
|
||||||
scriptSlotStep:
|
scriptSlotStep:
|
||||||
DPUP.3 0d71
|
DPUP.3 0d205
|
||||||
DECA
|
DECA
|
||||||
BNA scriptSlotStep
|
BNA scriptSlotStep
|
||||||
scriptSlotDone:
|
scriptSlotDone:
|
||||||
@@ -80,7 +80,7 @@ scriptSlotDone:
|
|||||||
|
|
||||||
; Seventy bytes, DP0 to DP1.
|
; Seventy bytes, DP0 to DP1.
|
||||||
scriptCopyState:
|
scriptCopyState:
|
||||||
INIB 0d71
|
INIB 0d205
|
||||||
scriptCopyByte:
|
scriptCopyByte:
|
||||||
LDA.0
|
LDA.0
|
||||||
STA.1
|
STA.1
|
||||||
@@ -154,8 +154,21 @@ scriptOpenFirst:
|
|||||||
INIB 0d63
|
INIB 0d63
|
||||||
CALL copyText
|
CALL copyText
|
||||||
|
|
||||||
|
; ---- And what it was given ----
|
||||||
|
;
|
||||||
|
; After the push, so that what the script one level up was given is already somewhere safe.
|
||||||
|
; A script started with nothing gets an EMPTY one rather than none at all, which is what
|
||||||
|
; makes "$args" always mean something inside a script while "$1" can be missing.
|
||||||
|
SETD.1 ScriptArgsFrom
|
||||||
|
LDD.0.1
|
||||||
|
SETD.1 ScriptArgs
|
||||||
|
INIB 0d128
|
||||||
|
CALL copyText
|
||||||
|
|
||||||
|
CALL scriptKeepDrive
|
||||||
SETD.0 ScriptName
|
SETD.0 ScriptName
|
||||||
SWI osFileInfo
|
SWI osFileInfo
|
||||||
|
CALL scriptGiveDriveBack
|
||||||
BRQ scriptOpenThere
|
BRQ scriptOpenThere
|
||||||
INIA 0x01
|
INIA 0x01
|
||||||
BRI scriptOpenFailed
|
BRI scriptOpenFailed
|
||||||
@@ -426,6 +439,7 @@ scriptFill:
|
|||||||
OR
|
OR
|
||||||
BRQ scriptFillNoMore
|
BRQ scriptFillNoMore
|
||||||
|
|
||||||
|
CALL scriptKeepDrive
|
||||||
SETD.0 ScriptName
|
SETD.0 ScriptName
|
||||||
SETD.1 ScriptBlock
|
SETD.1 ScriptBlock
|
||||||
SETD.2 ScriptIndex
|
SETD.2 ScriptIndex
|
||||||
@@ -433,6 +447,7 @@ scriptFill:
|
|||||||
INCD.2
|
INCD.2
|
||||||
LDB.2
|
LDB.2
|
||||||
SWI osFileBlock
|
SWI osFileBlock
|
||||||
|
CALL scriptGiveDriveBack
|
||||||
BNQ scriptFillNoMore
|
BNQ scriptFillNoMore
|
||||||
|
|
||||||
; DP3 is how many bytes came back. The nought goes after them.
|
; DP3 is how many bytes came back. The nought goes after them.
|
||||||
@@ -488,6 +503,40 @@ scriptFillNoMore:
|
|||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
; ---- Reading a script must not move the person ----
|
||||||
|
;
|
||||||
|
; The name is resolved afresh for every block, and a name with a drive in front of it moves
|
||||||
|
; the machine to that drive as a side effect of being resolved - sbfsWalk calls sbfsUse on
|
||||||
|
; the way past. So a script found in the system's place on drive 0, started by somebody
|
||||||
|
; standing on a disk of their own, would run its lines on the system disk instead of theirs.
|
||||||
|
;
|
||||||
|
; It matters more now than it used to. `do 0:/Apps/setup.sh` was always able to do this;
|
||||||
|
; typing a name now finds scripts the same three places a program is looked for, and the
|
||||||
|
; third of them is drive-qualified.
|
||||||
|
;
|
||||||
|
; The answer that survives the CALL: Q into A and the byte count onto the stack, because
|
||||||
|
; sbfsUse writes Q and RET puts DP3 back the way it found it.
|
||||||
|
scriptKeepDrive:
|
||||||
|
INA 0x24
|
||||||
|
SETD.1 ScriptDrive
|
||||||
|
STA.1
|
||||||
|
RET
|
||||||
|
|
||||||
|
scriptGiveDriveBack:
|
||||||
|
PSHD.3
|
||||||
|
MVQA
|
||||||
|
PSHA
|
||||||
|
SETD.1 ScriptDrive
|
||||||
|
LDA.1
|
||||||
|
CALL sbfsUse
|
||||||
|
POPA
|
||||||
|
POPD.3
|
||||||
|
; A holds what Q held. Adding nought to it is how it becomes Q again.
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
; Everything up to and including the next line feed, thrown away. Used for the shebang.
|
; Everything up to and including the next line feed, thrown away. Used for the shebang.
|
||||||
scriptSkipLine:
|
scriptSkipLine:
|
||||||
CALL scriptByte
|
CALL scriptByte
|
||||||
@@ -541,11 +590,35 @@ LoudWord:
|
|||||||
ScriptDepth:
|
ScriptDepth:
|
||||||
0x00
|
0x00
|
||||||
|
|
||||||
; ---- Seventy bytes, and they are next to each other on purpose ----
|
; The drive the person is standing on, kept across a block being fetched. Not part of the
|
||||||
|
; saved state below: it is only ever live for the length of one read.
|
||||||
|
ScriptDrive:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
; Where the arguments are, for the length of one call to scriptOpen. A pointer rather than the
|
||||||
|
; text, because the text is still sitting in the line that named the script and copying it
|
||||||
|
; twice would buy nothing.
|
||||||
|
ScriptArgsFrom:
|
||||||
|
0x00 0x00
|
||||||
|
|
||||||
|
; ---- Two hundred and five bytes, and they are next to each other on purpose ----
|
||||||
;
|
;
|
||||||
; Name, blocks left, next block, where in the block, and whether it is echoing: the whole of
|
; Name, blocks left, next block, where in the block, what it was given, and whether it is
|
||||||
; where a script has got to. Saving it is one copy because of this order, and nothing else
|
; echoing: the whole of a running script. Saving it is one copy because of this order, and
|
||||||
; may be put between them.
|
; nothing else may be put between them.
|
||||||
|
;
|
||||||
|
; ---- THREE NUMBERS DESCRIBE THIS BLOCK AND ALL THREE MUST AGREE ----
|
||||||
|
;
|
||||||
|
; scriptCopyState how many bytes one of them is
|
||||||
|
; scriptSlotAt how far apart two saved ones are
|
||||||
|
; ScriptSaved four of them
|
||||||
|
;
|
||||||
|
; They did not agree, and that is what this warning is for. Six bytes of line position were
|
||||||
|
; added in the middle of the block and the count stayed at 71, so the last six bytes of every
|
||||||
|
; saved script were never saved: a nested script's #quiet leaked back out to the script that
|
||||||
|
; called it, which is the opposite of what this file's own comment promises. The line position
|
||||||
|
; that went unsaved with it turned out not to matter - a loop keeps its own copy in the block
|
||||||
|
; record - but nothing said so, and the next field added would have been chosen at random.
|
||||||
ScriptName:
|
ScriptName:
|
||||||
#Reserve 0d64
|
#Reserve 0d64
|
||||||
ScriptBlocks:
|
ScriptBlocks:
|
||||||
@@ -574,10 +647,22 @@ ScriptLineBlocks:
|
|||||||
ScriptQuiet:
|
ScriptQuiet:
|
||||||
0x00
|
0x00
|
||||||
|
|
||||||
|
; ---- What the script was given ----
|
||||||
|
;
|
||||||
|
; The rest of the line that started it, kept whole rather than picked apart, because picking
|
||||||
|
; it apart costs a place to put every piece and a limit on how many there may be. "$1" walks
|
||||||
|
; this and copies out the word it wants; "$args" is this.
|
||||||
|
;
|
||||||
|
; A WHOLE LINE OF ROOM, so that nothing a person can type can be cut short. The line is 127
|
||||||
|
; and the script's own name has already come off the front of it, so this can never be filled
|
||||||
|
; - which is worth 128 bytes a level to never have to explain.
|
||||||
|
ScriptArgs:
|
||||||
|
#Reserve 0d128
|
||||||
|
|
||||||
; Three would do - a save happens on the second script and not the first - but four costs
|
; Three would do - a save happens on the second script and not the first - but four costs
|
||||||
; seventy bytes and removes an off-by-one from the only place it could hide.
|
; one more and removes an off-by-one from the only place it could hide.
|
||||||
ScriptSaved:
|
ScriptSaved:
|
||||||
#Reserve 0d284
|
#Reserve 0d820
|
||||||
ScriptInto:
|
ScriptInto:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
ScriptRoom:
|
ScriptRoom:
|
||||||
|
|||||||
@@ -185,3 +185,60 @@
|
|||||||
; NOT AUTOMATIC, and that is the point: dir and Say print and stop, and their output is the
|
; NOT AUTOMATIC, and that is the point: dir and Say print and stop, and their output is the
|
||||||
; reason you ran them. Only a program that says it took the screen gets the screen put back.
|
; reason you ran them. Only a program that says it took the screen gets the screen put back.
|
||||||
osTakeScreen 0d36
|
osTakeScreen 0d36
|
||||||
|
|
||||||
|
; ---- Where the program itself came from ----
|
||||||
|
;
|
||||||
|
; DP0 says where to put it and B is how much room there is counting the zero, the same
|
||||||
|
; bargain osArgument offers. What comes back is the whole path the program was loaded from,
|
||||||
|
; made absolute: a program found where somebody was standing is named by the bare word that
|
||||||
|
; was typed, and a bare name means the working directory - which a program is entitled to
|
||||||
|
; move out of.
|
||||||
|
;
|
||||||
|
; ---- Why this and not "open a file beside me" ----
|
||||||
|
;
|
||||||
|
; A service that opened a file relative to the program would need a TWIN FOR EVERY FILE
|
||||||
|
; OPERATION there is: read, save, info, block, start, write, done, delete, rename. One
|
||||||
|
; service handing back a path composes with all of them, and joining a name to a place is
|
||||||
|
; then a library rather than a service - Libraries/path.asm, beside print.asm and math.asm.
|
||||||
|
;
|
||||||
|
; ---- And why not just move the program there ----
|
||||||
|
;
|
||||||
|
; Because a program's ASSETS are relative to the program and its ARGUMENTS are relative to
|
||||||
|
; the person, and the working directory can only be one of them. Setting it to the program's
|
||||||
|
; own would mean "Play mytune.tune", typed by somebody in their own directory, looked in
|
||||||
|
; Play's. The working directory stays the person's; this is how a program finds its own.
|
||||||
|
osWhereAmI 0d37
|
||||||
|
|
||||||
|
; ---- Seeing what is on the disk ----
|
||||||
|
;
|
||||||
|
; Everything above takes a name a program already knows. NOTHING HERE COULD FIND OUT WHAT
|
||||||
|
; NAMES THERE ARE - a machine whose programs can read, write, rename and delete files and
|
||||||
|
; cannot ask what files exist. dir could only list because it lives in the shell and calls
|
||||||
|
; the filesystem directly; no loaded program could list anything at all.
|
||||||
|
;
|
||||||
|
; DP0 says where to put the name and B how much room there is, counting the zero, the same
|
||||||
|
; bargain osArgument and osWhereAmI offer.
|
||||||
|
;
|
||||||
|
; Q ANSWERS THE KIND RATHER THAN A YES OR NO, which is one value carrying both "is there
|
||||||
|
; one" and "what is it":
|
||||||
|
;
|
||||||
|
; 0 a file
|
||||||
|
; 1 a directory
|
||||||
|
; 2 a save that stopped before it committed
|
||||||
|
; 0xFF there are no more
|
||||||
|
;
|
||||||
|
; ---- And the size is not in it ----
|
||||||
|
;
|
||||||
|
; A walk hands back a NAME, and a program that wants the size of what it found asks
|
||||||
|
; osFileInfo about that name. The alternative is a record in memory whose shape both sides
|
||||||
|
; have to agree on, which is exactly what this file went out of its way to avoid for file
|
||||||
|
; sizes - and most callers of this want names and nothing else.
|
||||||
|
;
|
||||||
|
; ---- One walk at a time, and the system holds it ----
|
||||||
|
;
|
||||||
|
; Where a walk has got to is the system's, the way an open write is. A program that starts a
|
||||||
|
; second walk before finishing the first gets the second; there is one position, not a handle
|
||||||
|
; per caller. That is the same bargain osFileStart makes and for the same reason: the state
|
||||||
|
; is small, and a program that stops in the middle leaves nothing behind to clean up.
|
||||||
|
osDirFirst 0d38
|
||||||
|
osDirNext 0d39
|
||||||
|
|||||||
+99
-84
@@ -8,26 +8,28 @@
|
|||||||
; does not interrupt, so a program that only had the sound device could play a tune at
|
; does not interrupt, so a program that only had the sound device could play a tune at
|
||||||
; whatever speed the machine happened to run at, which is not a tune.
|
; whatever speed the machine happened to run at, which is not a tune.
|
||||||
;
|
;
|
||||||
; The screen finishes a frame sixty times a second and will say so. That is the only regular
|
; The timer is the other half. A period in cycles, a repeat bit, and a line when one has gone
|
||||||
; beat on this machine, and it is counted in the machine's own cycles, so this plays at the
|
; by: a beat this program SETS rather than one it borrows. Every duration below is a count of
|
||||||
; same speed whether the emulator is running at a megahertz or as fast as it can go. Every
|
; ticks, and the tick is named in the machine's own cycles, so this plays at the same speed
|
||||||
; duration below is in frames: 30 is half a second.
|
; whether the emulator is running at a megahertz or as fast as it can go.
|
||||||
;
|
;
|
||||||
; A programmable timer is the device that ought to be doing this, and it does not exist yet.
|
; ---- The tick is a sixteenth note, and that is the whole point ----
|
||||||
; Borrowing the screen's frame costs nothing and works, which is the whole reason to notice
|
|
||||||
; that a beat is a beat wherever it comes from.
|
|
||||||
;
|
;
|
||||||
; ---- What borrowing it costs ----
|
; 125,000 cycles, which is a sixteenth note at 120 beats a minute.
|
||||||
;
|
;
|
||||||
; The frame is not slow. It is FIXED, and that is the different complaint. Every duration here
|
; This program used to count the SCREEN's frames, because until the timer existed the frame
|
||||||
; is a whole number of 16.67 ms, so a note worth a third of a beat cannot be written at all -
|
; was the only regular beat on the machine. A frame is 16,667 cycles, so a sixteenth note is
|
||||||
; and the way round it is to pick a tempo whose subdivisions happen to land on whole frames,
|
; seven and a half of them and could not be asked for at all. The arpeggio below was written
|
||||||
; which is making the tune fit the machine rather than the other way round. The theme below
|
; as seven frames, the nearest whole one, which is six and a half per cent fast; and the way
|
||||||
; was written to the frame and is a few cents of tempo away from what it wants to be.
|
; round the rest of it was to pick a tempo whose subdivisions happened to land on whole
|
||||||
|
; frames, which is making the tune fit the machine rather than the other way round.
|
||||||
;
|
;
|
||||||
; So what the timer wants is an ARBITRARY tick rather than a faster fixed one, and the reason
|
; SO THE DURATIONS HERE ARE NOT A CONVERSION OF THE OLD ONES. They are what the music wanted
|
||||||
; the screen should not be the clock is that a display refresh and a music routine have no
|
; in the first place, now that it can be written down: an eighth is two ticks, a quarter is
|
||||||
; reason to share a rate.
|
; four, and the arpeggio is one each rather than a fast approximation of one.
|
||||||
|
;
|
||||||
|
; The reason the screen should not have been the clock is not that a frame is slow. It is that
|
||||||
|
; a display refresh and a music routine have no reason to share a rate.
|
||||||
;
|
;
|
||||||
; ---- What a patch costs and what a note costs ----
|
; ---- What a patch costs and what a note costs ----
|
||||||
;
|
;
|
||||||
@@ -143,10 +145,18 @@ start:
|
|||||||
|
|
||||||
; ---- The beat ----
|
; ---- The beat ----
|
||||||
;
|
;
|
||||||
; Ask the screen to interrupt at each frame, and let interrupts in. The screen does not do
|
; The period first and the control byte second, because writing the control byte with the
|
||||||
; this unless it is asked.
|
; run bit set is what LOADS the period. Doing it the other way round starts a timer on
|
||||||
|
; whatever it was holding before.
|
||||||
INIA 0x01
|
INIA 0x01
|
||||||
OUTA 0x35
|
OUTA 0x52
|
||||||
|
INIA 0xE8
|
||||||
|
OUTA 0x53
|
||||||
|
INIA 0x48
|
||||||
|
OUTA 0x54 ; 0x01E848 is 125,000.
|
||||||
|
|
||||||
|
INIA 0x07
|
||||||
|
OUTA 0x51 ; Run, repeat, interrupt.
|
||||||
SIF
|
SIF
|
||||||
|
|
||||||
; ---- The tune ----
|
; ---- The tune ----
|
||||||
@@ -160,11 +170,11 @@ nextNote:
|
|||||||
BRA finished
|
BRA finished
|
||||||
OUTA 0x44 ; Writing the note is what starts it.
|
OUTA 0x44 ; Writing the note is what starts it.
|
||||||
INCD.0
|
INCD.0
|
||||||
LDB.0 ; How many frames it lasts.
|
LDB.0 ; How many ticks it lasts.
|
||||||
INCD.0
|
INCD.0
|
||||||
|
|
||||||
holdNote:
|
holdNote:
|
||||||
WAIT ; Nothing at all until the screen says a frame has gone by.
|
WAIT ; Nothing at all until the timer says a tick has gone by.
|
||||||
DECB
|
DECB
|
||||||
BNB holdNote
|
BNB holdNote
|
||||||
|
|
||||||
@@ -175,9 +185,10 @@ holdNote:
|
|||||||
BRI nextNote
|
BRI nextNote
|
||||||
|
|
||||||
finished:
|
finished:
|
||||||
; Let the last note ring out rather than cutting it off, then put the screen back the way it
|
; Let the last note ring out rather than cutting it off, then give the timer back - stopping
|
||||||
; was found - and stop asking to be interrupted before taking away what catches it.
|
; it before taking away what catches it, because this program is about to stop existing and
|
||||||
INIB 0d45
|
; an interrupt with no handler installed is a fault.
|
||||||
|
INIB 0d6
|
||||||
lastRing:
|
lastRing:
|
||||||
WAIT
|
WAIT
|
||||||
DECB
|
DECB
|
||||||
@@ -185,13 +196,17 @@ lastRing:
|
|||||||
|
|
||||||
CIF
|
CIF
|
||||||
RSTA
|
RSTA
|
||||||
OUTA 0x35
|
OUTA 0x51
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
; Sixty times a second, and it has nothing to do. WAIT only needs something to have happened,
|
; The tick has nothing to do: the loop above is the player, and WAIT only needs something to
|
||||||
; and this is the something. A handler still has to exist: an interrupt with nothing installed
|
; have HAPPENED. A handler still has to exist, because an interrupt with nothing installed to
|
||||||
; to catch it is a fault.
|
; catch it is a fault.
|
||||||
frame:
|
;
|
||||||
|
; Nothing reads the status port here, and that is not an oversight. Taking the interrupt is
|
||||||
|
; what brings the line down. A program that POLLED the timer instead would have to read 0x50,
|
||||||
|
; because looking at it is the only thing that answers a tick nobody was interrupted by.
|
||||||
|
tick:
|
||||||
RETI
|
RETI
|
||||||
|
|
||||||
#Data
|
#Data
|
||||||
@@ -200,85 +215,85 @@ frame:
|
|||||||
|
|
||||||
; ---- Notes and how long they last ----
|
; ---- Notes and how long they last ----
|
||||||
;
|
;
|
||||||
; Pairs: a MIDI note, then a count of frames. 60 is middle C and every 12 is an octave. A zero
|
; Pairs: a MIDI note, then a count of ticks. 60 is middle C and every 12 is an octave. A zero
|
||||||
; note ends it, which is why there are no rests in here - a rest would want a duration with no
|
; note ends it, which is why there are no rests in here - a rest would want a duration with no
|
||||||
; note, and this table has no way to say that. Adding one is a byte of flag or a note number
|
; note, and this table has no way to say that. Adding one is a byte of flag or a note number
|
||||||
; nothing plays, and this program did not need it.
|
; nothing plays, and this program did not need it.
|
||||||
Tune:
|
Tune:
|
||||||
0d60 0d15 ; C
|
0d60 0d2 ; C
|
||||||
0d64 0d15 ; E
|
0d64 0d2 ; E
|
||||||
0d67 0d15 ; G
|
0d67 0d2 ; G
|
||||||
0d72 0d30 ; C, an octave up, held twice as long
|
0d72 0d4 ; C, an octave up, held twice as long
|
||||||
0d71 0d15 ; B
|
0d71 0d2 ; B
|
||||||
0d67 0d15 ; G
|
0d67 0d2 ; G
|
||||||
0d64 0d15 ; E
|
0d64 0d2 ; E
|
||||||
0d60 0d45 ; and home
|
0d60 0d6 ; and home
|
||||||
0x00
|
0x00
|
||||||
|
|
||||||
Theme:
|
Theme:
|
||||||
0x30 0d15 ; C
|
0x30 0d2 ; C
|
||||||
0x35 0d15 ; F
|
0x35 0d2 ; F
|
||||||
0x3C 0d15 ; C+
|
0x3C 0d2 ; C+
|
||||||
0x40 0d30 ; E+
|
0x40 0d4 ; E+
|
||||||
0x48 0d30 ; C++
|
0x48 0d4 ; C++
|
||||||
0x45 0d30 ; A+
|
0x45 0d4 ; A+
|
||||||
0x47 0d30 ; B+
|
0x47 0d4 ; B+
|
||||||
0x43 0d45 ; G+
|
0x43 0d6 ; G+
|
||||||
; repeat four times.
|
; repeat four times.
|
||||||
0x2A 0d7 ; Gs
|
0x2C 0d1 ; Gs
|
||||||
0x33 0d7 ; Ef
|
0x33 0d1 ; Ef
|
||||||
0x3C 0d7 ; C+
|
0x3C 0d1 ; C+
|
||||||
;
|
;
|
||||||
0x2A 0d7 ; Gs
|
0x2C 0d1 ; Gs
|
||||||
0x33 0d7 ; Ef
|
0x33 0d1 ; Ef
|
||||||
0x3C 0d7 ; C+
|
0x3C 0d1 ; C+
|
||||||
;
|
;
|
||||||
0x2A 0d7 ; Gs
|
0x2C 0d1 ; Gs
|
||||||
0x33 0d7 ; Ef
|
0x33 0d1 ; Ef
|
||||||
0x3C 0d7 ; C+
|
0x3C 0d1 ; C+
|
||||||
;
|
;
|
||||||
0x2A 0d7 ; Gs
|
0x2C 0d1 ; Gs
|
||||||
0x33 0d7 ; Ef
|
0x33 0d1 ; Ef
|
||||||
0x3C 0d7 ; C+
|
0x3C 0d1 ; C+
|
||||||
; next chord
|
; next chord
|
||||||
0x2E 0d7 ; Bf
|
0x2E 0d1 ; Bf
|
||||||
0x35 0d7 ; F
|
0x35 0d1 ; F
|
||||||
0x3E 0d7 ; D+
|
0x3E 0d1 ; D+
|
||||||
; next chord
|
; next chord
|
||||||
0x2E 0d7 ; Bf
|
0x2E 0d1 ; Bf
|
||||||
0x35 0d7 ; F
|
0x35 0d1 ; F
|
||||||
0x3E 0d7 ; D+
|
0x3E 0d1 ; D+
|
||||||
;
|
;
|
||||||
; next chord
|
; next chord
|
||||||
0x2E 0d7 ; Bf
|
0x2E 0d1 ; Bf
|
||||||
0x35 0d7 ; F
|
0x35 0d1 ; F
|
||||||
0x3E 0d7 ; D+
|
0x3E 0d1 ; D+
|
||||||
;
|
;
|
||||||
; next chord
|
; next chord
|
||||||
0x2E 0d7 ; Bf
|
0x2E 0d1 ; Bf
|
||||||
0x35 0d7 ; F
|
0x35 0d1 ; F
|
||||||
0x3E 0d7 ; D+
|
0x3E 0d1 ; D+
|
||||||
; Finally on the C major
|
; Finally on the C major
|
||||||
0x30 0d7 ; C
|
0x30 0d1 ; C
|
||||||
0x37 0d7 ; G
|
0x37 0d1 ; G
|
||||||
0x40 0d7 ; E+
|
0x40 0d1 ; E+
|
||||||
;
|
;
|
||||||
0x30 0d7 ; C
|
0x30 0d1 ; C
|
||||||
0x37 0d7 ; G
|
0x37 0d1 ; G
|
||||||
0x40 0d7 ; E+
|
0x40 0d1 ; E+
|
||||||
;
|
;
|
||||||
0x30 0d7 ; C
|
0x30 0d1 ; C
|
||||||
0x37 0d7 ; G
|
0x37 0d1 ; G
|
||||||
0x40 0d7 ; E+
|
0x40 0d1 ; E+
|
||||||
;
|
;
|
||||||
0x30 0d7 ; C
|
0x30 0d1 ; C
|
||||||
0x37 0d7 ; G
|
0x37 0d1 ; G
|
||||||
0x40 0d7 ; E+
|
0x40 0d1 ; E+
|
||||||
;
|
;
|
||||||
0x18 0d60 ; C bass
|
0x18 0d8 ; C bass
|
||||||
0x00
|
0x00
|
||||||
|
|
||||||
#Vectors
|
#Vectors
|
||||||
|
|
||||||
Boot start
|
Boot start
|
||||||
Device 0x30 frame
|
Device 0x50 tick
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
; path.asm
|
||||||
|
; Putting a name together with the place another thing is in.
|
||||||
|
;
|
||||||
|
; DP0 = a path, DP1 = a name, DP2 = where the answer goes, B = how much room, counting
|
||||||
|
; the zero on the end
|
||||||
|
; CALL pathBeside Q is zero if the whole of it fitted
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
;
|
||||||
|
; ---- What this is for ----
|
||||||
|
;
|
||||||
|
; A program asks the system where it came from - SWI osWhereAmI - and is handed the path it
|
||||||
|
; was loaded from. What it actually wants is the path of something NEXT TO that: its tune,
|
||||||
|
; its tiles, its saved state. This is that one step, and it is a library rather than a
|
||||||
|
; service because it is arithmetic on two strings and touches nothing the system owns.
|
||||||
|
;
|
||||||
|
; That split is the whole reason osWhereAmI hands back a path instead of opening files on a
|
||||||
|
; program's behalf. A service that opened a file relative to the program would need a twin
|
||||||
|
; for every file operation there is; a path composes with all of them, and the joining
|
||||||
|
; happens once, here, on a machine that has no string library to do it in each program.
|
||||||
|
;
|
||||||
|
; ---- Everything up to the last separator ----
|
||||||
|
;
|
||||||
|
; The place a thing is in is everything up to and INCLUDING the last separator in its path,
|
||||||
|
; so joining is a copy and not a search backwards from the end. A path with no separator in
|
||||||
|
; it names something in the working directory, and the answer is then the name on its own -
|
||||||
|
; which means exactly the same thing, in the same place, and needs no special case.
|
||||||
|
;
|
||||||
|
; /Packages/app.Lander/Lander + splash.tune = /Packages/app.Lander/splash.tune
|
||||||
|
; /Lander.sbx + splash.tune = /splash.tune
|
||||||
|
; Lander.sbx + splash.tune = splash.tune
|
||||||
|
;
|
||||||
|
; ---- The room is counted once, in one place ----
|
||||||
|
;
|
||||||
|
; Both halves go through pathPut, which counts the room down and always keeps a byte back
|
||||||
|
; for the zero. So an answer that did not fit is still a STRING, endable and printable, and
|
||||||
|
; a caller that ignores Q gets a short path rather than a walk off the end of its buffer.
|
||||||
|
; It gets a wrong answer, which is why Q exists - but not a broken machine.
|
||||||
|
;
|
||||||
|
; The answer is built through a stored pointer rather than DP2, because a CALL puts DP0 to
|
||||||
|
; DP2 back the way it found them and pathPut would otherwise write the same byte every time.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
pathBeside:
|
||||||
|
SETD.3 PathPut
|
||||||
|
STD.2.3
|
||||||
|
SETD.3 PathRoom
|
||||||
|
STB.3
|
||||||
|
|
||||||
|
; ---- How much of the path is the place it is in ----
|
||||||
|
;
|
||||||
|
; Counted rather than pointed at, because what is wanted is a NUMBER OF BYTES TO COPY and
|
||||||
|
; a pointer would have to be turned into one. The count is set afresh at every separator,
|
||||||
|
; so what it holds at the end is the last one - which is the one that matters.
|
||||||
|
PSHD.0 ; The front of the path, to come back to.
|
||||||
|
RSTA
|
||||||
|
SETD.3 PathCut
|
||||||
|
STA.3
|
||||||
|
SETD.3 PathAt
|
||||||
|
STA.3
|
||||||
|
|
||||||
|
pathScan:
|
||||||
|
LDA.0
|
||||||
|
BRA pathScanned
|
||||||
|
INIB 0x2F
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ pathScanStep
|
||||||
|
|
||||||
|
; A separator, so everything up to and including it is the place.
|
||||||
|
SETD.3 PathAt
|
||||||
|
LDA.3
|
||||||
|
INCA
|
||||||
|
SETD.3 PathCut
|
||||||
|
STA.3
|
||||||
|
|
||||||
|
pathScanStep:
|
||||||
|
SETD.3 PathAt
|
||||||
|
LDA.3
|
||||||
|
INCA
|
||||||
|
STA.3
|
||||||
|
INCD.0
|
||||||
|
BRI pathScan
|
||||||
|
|
||||||
|
pathScanned:
|
||||||
|
POPD.0
|
||||||
|
SETD.3 PathCut
|
||||||
|
LDA.3
|
||||||
|
SETD.3 PathLeft
|
||||||
|
STA.3
|
||||||
|
|
||||||
|
pathPlace:
|
||||||
|
SETD.3 PathLeft
|
||||||
|
LDA.3
|
||||||
|
BRA pathName ; All of the place is written, or there was none of it.
|
||||||
|
DECA
|
||||||
|
STA.3
|
||||||
|
LDA.0
|
||||||
|
CALL pathPut
|
||||||
|
BNQ pathNoRoom
|
||||||
|
INCD.0
|
||||||
|
BRI pathPlace
|
||||||
|
|
||||||
|
pathName:
|
||||||
|
PSHD.1
|
||||||
|
POPD.0
|
||||||
|
pathNameChar:
|
||||||
|
LDA.0
|
||||||
|
BRA pathMade
|
||||||
|
CALL pathPut
|
||||||
|
BNQ pathNoRoom
|
||||||
|
INCD.0
|
||||||
|
BRI pathNameChar
|
||||||
|
|
||||||
|
pathMade:
|
||||||
|
CALL pathEnd
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD ; Q is zero: the whole of it fitted.
|
||||||
|
RET
|
||||||
|
|
||||||
|
pathNoRoom:
|
||||||
|
CALL pathEnd
|
||||||
|
INIA 0x01
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; The zero that makes it a string. There is always room for it: pathPut refuses the byte
|
||||||
|
; that would have taken the last one.
|
||||||
|
pathEnd:
|
||||||
|
SETD.3 PathPut
|
||||||
|
LDD.2.3
|
||||||
|
RSTA
|
||||||
|
STA.2
|
||||||
|
RET
|
||||||
|
|
||||||
|
; A holds a character. Puts it where the answer has got to and steps that on. Q is one if
|
||||||
|
; there is no room for it.
|
||||||
|
pathPut:
|
||||||
|
SETD.3 PathHold
|
||||||
|
STA.3 ; The character, across the pointer being fetched and put back.
|
||||||
|
|
||||||
|
SETD.3 PathRoom
|
||||||
|
LDB.3
|
||||||
|
DECB
|
||||||
|
BRB pathPutFull ; Only the zero's worth left, so this byte cannot be written.
|
||||||
|
STB.3
|
||||||
|
|
||||||
|
SETD.3 PathPut
|
||||||
|
LDD.2.3
|
||||||
|
SETD.3 PathHold
|
||||||
|
LDA.3
|
||||||
|
STA.2
|
||||||
|
INCD.2
|
||||||
|
SETD.3 PathPut
|
||||||
|
STD.2.3
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
pathPutFull:
|
||||||
|
INIA 0x01
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
; Where the answer has got to, and how much room is left in it.
|
||||||
|
PathPut:
|
||||||
|
0x00 0x00
|
||||||
|
PathRoom:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
; How much of the path is the place it is in, how far the scan has got, and how much of the
|
||||||
|
; place is still to be written.
|
||||||
|
PathCut:
|
||||||
|
0x00
|
||||||
|
PathAt:
|
||||||
|
0x00
|
||||||
|
PathLeft:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
; One character, across the fetching and putting back of the pointer it is written through.
|
||||||
|
PathHold:
|
||||||
|
0x00
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
; player.asm
|
||||||
|
; Four voices on one clock: the part of a music player that is not the music.
|
||||||
|
; Written by Anachronaut
|
||||||
|
;
|
||||||
|
; A program that includes this supplies the tune - four order lists called Order0 to Order3,
|
||||||
|
; and the sequences they name - and gets the scheduler, the patch loader and the state that
|
||||||
|
; goes with them. It does NOT supply the beat: setting the timer up and waiting on it belongs
|
||||||
|
; to the program, because how long a piece rings at the end and what stops it are its business
|
||||||
|
; and not this file's.
|
||||||
|
;
|
||||||
|
; What a sequence is, what a command is and where repetition comes from are all described in
|
||||||
|
; Apps/Play.asm, which is the first thing to read this.
|
||||||
|
;
|
||||||
|
; The caller must define, in its Data Segment:
|
||||||
|
;
|
||||||
|
; PatchTable one two-byte address per patch
|
||||||
|
; SequenceTable one two-byte address per sequence
|
||||||
|
; VoiceStart four bytes, the patch index each voice starts on
|
||||||
|
;
|
||||||
|
; and point each voice's order cursor at a list of one-byte sequence indices ending in 0xFF.
|
||||||
|
; A tune read from a file needs none of that: useTune does all of it out of the header.
|
||||||
|
;
|
||||||
|
; It must call loadStartPatches once, then stepVoice once a tick for each voice, with A
|
||||||
|
; holding the channel and DP1 the voice's record. Playing counts down as voices run out, and
|
||||||
|
; reaching nought is the piece being over.
|
||||||
|
;
|
||||||
|
; INDICES THROUGH TABLES, RATHER THAN ADDRESSES, because that is the shape a tune read from a
|
||||||
|
; file has to be: the two tables are the only places an address lives, so loading a tune means
|
||||||
|
; adding a base to them and nothing else. Keeping the assembled-in form the same shape is what
|
||||||
|
; makes reading one from a file change no code here at all.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
; ---- One voice, one tick ----
|
||||||
|
;
|
||||||
|
; A is the channel and DP1 is the voice's six bytes. Both survive the CALL that got here,
|
||||||
|
; which is what lets the caller say which voice it means in two instructions.
|
||||||
|
;
|
||||||
|
; +0 order cursor +2 sequence cursor +4 count +5 live
|
||||||
|
stepVoice:
|
||||||
|
SETD.3 ThisChannel
|
||||||
|
STA.3 ; A is wanted for other things between here and using it.
|
||||||
|
|
||||||
|
DPUP.1 0d5
|
||||||
|
LDA.1 ; Live?
|
||||||
|
DPDN.1 0d5
|
||||||
|
BRA stepDone
|
||||||
|
|
||||||
|
DPUP.1 0d4
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1 ; One tick less of whatever is sounding.
|
||||||
|
DPDN.1 0d4
|
||||||
|
BNA stepDone ; Still holding it.
|
||||||
|
|
||||||
|
; ---- The count ran out, so this is a boundary ----
|
||||||
|
;
|
||||||
|
; Select the channel FIRST. Every sound port below writes to whichever channel was last
|
||||||
|
; named, so a voice that forgot would be playing somebody else's part.
|
||||||
|
LDA.3
|
||||||
|
OUTA 0x41
|
||||||
|
RSTA
|
||||||
|
OUTA 0x45 ; Let go of the note that just ended.
|
||||||
|
|
||||||
|
DPUP.1 0d2
|
||||||
|
LDD.0.1 ; DP0 is now this voice's place in its sequence.
|
||||||
|
DPDN.1 0d2
|
||||||
|
|
||||||
|
; ---- Read events until one of them takes time ----
|
||||||
|
;
|
||||||
|
; A command takes none, and neither does the end of a sequence, so both come back here.
|
||||||
|
; A sequence of nothing but commands would spin in this loop for ever, which is a hang
|
||||||
|
; rather than a wrong note - the one malformed sequence worth a compiler refusing.
|
||||||
|
stepEvent:
|
||||||
|
LDA.0
|
||||||
|
INIB 0xFF
|
||||||
|
XOR ; XOR answers in Q and leaves A holding the event.
|
||||||
|
BRQ stepSequenceEnd
|
||||||
|
|
||||||
|
; The top bit says command, and 0xFF is already dealt with above.
|
||||||
|
INIB 0x80
|
||||||
|
AND
|
||||||
|
BNQ stepCommand
|
||||||
|
|
||||||
|
; ---- A note, or a rest, and how long it lasts ----
|
||||||
|
INCD.0
|
||||||
|
LDB.0
|
||||||
|
INCD.0
|
||||||
|
DPUP.1 0d2
|
||||||
|
STD.0.1 ; The sequence cursor, moved past this event.
|
||||||
|
DPUP.1 0d2
|
||||||
|
STB.1 ; And the count it will be held for.
|
||||||
|
|
||||||
|
; A is still the event. Zero is a rest, which is a duration with nothing started.
|
||||||
|
BRA stepDone
|
||||||
|
OUTA 0x44 ; Writing the note is what starts it.
|
||||||
|
stepDone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- 0x80: play the rest of this voice on that patch ----
|
||||||
|
;
|
||||||
|
; The address follows the command. The sequence cursor is put away before the patch is loaded
|
||||||
|
; and taken out again after, because loadPatch walks DP0 and a CALL hands back the pointer it
|
||||||
|
; was given rather than the one this needs next.
|
||||||
|
stepCommand:
|
||||||
|
INCD.0
|
||||||
|
LDA.0 ; Which patch: one byte of index, not an address.
|
||||||
|
INCD.0 ; Past it, on whatever comes next.
|
||||||
|
DPUP.1 0d2
|
||||||
|
STD.0.1
|
||||||
|
DPDN.1 0d2
|
||||||
|
|
||||||
|
SETD.2 PatchTable
|
||||||
|
LDD.2.2 ; The table, wherever this tune put it.
|
||||||
|
DPUA.2
|
||||||
|
DPUA.2 ; Twice, because an entry is two bytes.
|
||||||
|
LDD.2.2 ; And DP2 follows the address it is now holding.
|
||||||
|
|
||||||
|
PSHD.2
|
||||||
|
POPD.0 ; DP0 is the patch.
|
||||||
|
LDA.3 ; And A the channel, which loadPatch selects.
|
||||||
|
CALL loadPatch
|
||||||
|
|
||||||
|
DPUP.1 0d2
|
||||||
|
LDD.0.1 ; Back to where the sequence had got to.
|
||||||
|
DPDN.1 0d2
|
||||||
|
BRI stepEvent
|
||||||
|
|
||||||
|
; ---- The sequence ended, so take the next one from this voice's order list ----
|
||||||
|
;
|
||||||
|
; AN ORDER LIST IS INDICES, one byte each, ending in 0xFF - the same byte that ends a sequence.
|
||||||
|
; SequenceTable is the only place a sequence's address lives.
|
||||||
|
;
|
||||||
|
; That is what makes a tune loadable without walking it: nothing inside a sequence or an order
|
||||||
|
; list is an address, so putting one in memory means adding the load address to two tables and
|
||||||
|
; nothing else. A loader that had to parse sequences looking for addresses to fix up is a
|
||||||
|
; loader that a malformed file can walk off a cliff.
|
||||||
|
;
|
||||||
|
; It was addresses here, terminated by a zero, on the reasoning that no sequence could live
|
||||||
|
; below this player's 0x3000 base. True of a loaded program and false of a boot image based at
|
||||||
|
; zero, so the first test written against it read its own first sequence as the end of the
|
||||||
|
; list and played nothing at all. An address is not a good place to hide a flag unless the
|
||||||
|
; address is impossible in every program rather than in this one.
|
||||||
|
stepSequenceEnd:
|
||||||
|
LDD.0.1 ; DP0 is the order cursor, which is the first thing in the record.
|
||||||
|
LDA.0 ; Which sequence comes next.
|
||||||
|
INIB 0xFF
|
||||||
|
XOR
|
||||||
|
BRQ stepOrderEnd
|
||||||
|
|
||||||
|
INCD.0
|
||||||
|
STD.0.1 ; The order cursor, moved past this entry.
|
||||||
|
|
||||||
|
SETD.2 SequenceTable
|
||||||
|
LDD.2.2 ; The table, wherever this tune put it.
|
||||||
|
DPUA.2
|
||||||
|
DPUA.2
|
||||||
|
LDD.2.2 ; And the sequence that index names.
|
||||||
|
|
||||||
|
DPUP.1 0d2
|
||||||
|
STD.2.1 ; And the sequence cursor set to the new one.
|
||||||
|
DPDN.1 0d2
|
||||||
|
|
||||||
|
PSHD.2
|
||||||
|
POPD.0
|
||||||
|
BRI stepEvent ; Which has events in it, so read one.
|
||||||
|
|
||||||
|
stepOrderEnd:
|
||||||
|
; Nothing left for this voice. The gate is already down, so its last note is fading.
|
||||||
|
DPUP.1 0d5
|
||||||
|
RSTA
|
||||||
|
STA.1
|
||||||
|
SETD.3 Playing
|
||||||
|
LDA.3
|
||||||
|
DECA
|
||||||
|
STA.3
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- The instrument each voice starts on ----
|
||||||
|
;
|
||||||
|
; STARTING STATE IS DECLARED RATHER THAN ASSUMED. A voice whose instrument was never said
|
||||||
|
; would play on whatever the device woke up with, or worse on whatever the last tune left -
|
||||||
|
; and that is a fault this machine has met elsewhere, where a program run a second time starts
|
||||||
|
; with the memory the first run left, because loading is what initialises and running is not.
|
||||||
|
loadStartPatches:
|
||||||
|
RSTA
|
||||||
|
loadStartOne:
|
||||||
|
SETD.3 ThisChannel
|
||||||
|
STA.3
|
||||||
|
|
||||||
|
SETD.2 VoiceStartAt
|
||||||
|
LDD.2.2
|
||||||
|
DPUA.2
|
||||||
|
LDA.2 ; Which patch this voice starts on.
|
||||||
|
SETD.2 PatchTable
|
||||||
|
LDD.2.2
|
||||||
|
DPUA.2
|
||||||
|
DPUA.2
|
||||||
|
LDD.2.2
|
||||||
|
|
||||||
|
PSHD.2
|
||||||
|
POPD.0
|
||||||
|
LDA.3
|
||||||
|
CALL loadPatch ; A survives a CALL, so it is still the channel below.
|
||||||
|
|
||||||
|
INCA
|
||||||
|
INIB 0d4
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ loadStartOne
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- base + offset, into wherever the caller wants it ----
|
||||||
|
;
|
||||||
|
; DP0 names two bytes of offset and DP1 two bytes to put the address in. They may be the same
|
||||||
|
; place, which is how a table is relocated in position: the low byte is written before the
|
||||||
|
; high byte is read, so nothing is clobbered under itself.
|
||||||
|
;
|
||||||
|
; The low bytes carry into the high ones, which is the whole reason ADD takes the Carry Flag.
|
||||||
|
tuneAddr:
|
||||||
|
SETD.2 TuneBase
|
||||||
|
CCF
|
||||||
|
INCD.0
|
||||||
|
LDA.0 ; The offset, low byte.
|
||||||
|
INCD.2
|
||||||
|
LDB.2 ; And the base, low byte.
|
||||||
|
ADD
|
||||||
|
INCD.1
|
||||||
|
STQ.1
|
||||||
|
DECD.0
|
||||||
|
LDA.0 ; The offset, high byte.
|
||||||
|
DECD.2
|
||||||
|
LDB.2
|
||||||
|
ADD ; With the carry the low bytes made.
|
||||||
|
DECD.1
|
||||||
|
STQ.1
|
||||||
|
RET
|
||||||
|
|
||||||
|
; Every entry of the table at DP0 turned from an offset into an address. B is how many.
|
||||||
|
tuneReloc:
|
||||||
|
PSHD.0
|
||||||
|
POPD.1
|
||||||
|
CALL tuneAddr ; A CALL hands DP0 and DP1 back, so both are still the entry.
|
||||||
|
INCD.0
|
||||||
|
INCD.0
|
||||||
|
DECB
|
||||||
|
BNB tuneReloc
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- A tune, from wherever it was put ----
|
||||||
|
;
|
||||||
|
; DP0 names its first byte. EVERYTHING IN A TUNE IS AN OFFSET FROM THERE, so this adds the
|
||||||
|
; base to the two tables and the four order lists and nothing else in the file is touched. No
|
||||||
|
; sequence is walked, and nothing inside one is an address to be found and corrected - which
|
||||||
|
; is what makes a malformed tune something that plays wrongly rather than something that takes
|
||||||
|
; the loader with it.
|
||||||
|
;
|
||||||
|
; Q is nought if the tune was taken, and anything else if the file was not one.
|
||||||
|
useTune:
|
||||||
|
SETD.1 TuneBase
|
||||||
|
STD.0.1
|
||||||
|
|
||||||
|
; "SBTU", and version one. A file that is not a tune has to be refused here, because
|
||||||
|
; everything below reads offsets out of it and jumps to what they name.
|
||||||
|
SETD.1 TuneMagic
|
||||||
|
INIB 0d5
|
||||||
|
useTuneMagic:
|
||||||
|
LDA.0
|
||||||
|
PSHB
|
||||||
|
LDB.1
|
||||||
|
XOR
|
||||||
|
POPB
|
||||||
|
BNQ useTuneNo
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
DECB
|
||||||
|
BNB useTuneMagic
|
||||||
|
|
||||||
|
; The tick, straight into the timer. Three bytes, most significant first, which is the
|
||||||
|
; order the ports take them in.
|
||||||
|
LDA.0
|
||||||
|
OUTA 0x52
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
OUTA 0x53
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
OUTA 0x54
|
||||||
|
|
||||||
|
; How many of each, kept before the pointers move.
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
SETD.1 TunePatches
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
SETD.1 TuneSequences
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
; The two tables.
|
||||||
|
INCD.0
|
||||||
|
SETD.1 PatchTable
|
||||||
|
CALL tuneAddr
|
||||||
|
INCD.0
|
||||||
|
INCD.0
|
||||||
|
SETD.1 SequenceTable
|
||||||
|
CALL tuneAddr
|
||||||
|
|
||||||
|
; And an order list each.
|
||||||
|
INCD.0
|
||||||
|
INCD.0
|
||||||
|
SETD.1 Voice0
|
||||||
|
CALL tuneAddr
|
||||||
|
INCD.0
|
||||||
|
INCD.0
|
||||||
|
SETD.1 Voice1
|
||||||
|
CALL tuneAddr
|
||||||
|
INCD.0
|
||||||
|
INCD.0
|
||||||
|
SETD.1 Voice2
|
||||||
|
CALL tuneAddr
|
||||||
|
INCD.0
|
||||||
|
INCD.0
|
||||||
|
SETD.1 Voice3
|
||||||
|
CALL tuneAddr
|
||||||
|
|
||||||
|
; The starting instruments are not an offset but a place IN the file, so they are found by
|
||||||
|
; counting rather than by adding.
|
||||||
|
SETD.0 TuneBase
|
||||||
|
LDD.0.0
|
||||||
|
DPUP.0 0d22
|
||||||
|
SETD.1 VoiceStartAt
|
||||||
|
STD.0.1
|
||||||
|
|
||||||
|
; Now the tables themselves, whose entries are offsets like everything else.
|
||||||
|
SETD.0 PatchTable
|
||||||
|
LDD.0.0
|
||||||
|
SETD.1 TunePatches
|
||||||
|
LDB.1
|
||||||
|
CALL tuneReloc
|
||||||
|
SETD.0 SequenceTable
|
||||||
|
LDD.0.0
|
||||||
|
SETD.1 TuneSequences
|
||||||
|
LDB.1
|
||||||
|
CALL tuneReloc
|
||||||
|
|
||||||
|
CALL startVoices
|
||||||
|
|
||||||
|
; Q is nought, which is how this says it worked. There is no instruction that sets Q: it is
|
||||||
|
; the ALU's output and nothing else, so saying nought means doing a sum that comes to it.
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
XOR
|
||||||
|
RET
|
||||||
|
|
||||||
|
useTuneNo:
|
||||||
|
; Q is already not nought, because that is what got here.
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- Four voices at the beginning of their order lists ----
|
||||||
|
;
|
||||||
|
; Whoever supplied the tune has set the order cursors; this sets everything else. A voice
|
||||||
|
; starts on Empty with a count of one, so its first tick runs the count out, finds the end of
|
||||||
|
; a sequence, and goes to the order list for the real first one. The beginning of a piece
|
||||||
|
; needs no special case anywhere.
|
||||||
|
startVoices:
|
||||||
|
SETD.1 Voice0
|
||||||
|
CALL startOneVoice
|
||||||
|
SETD.1 Voice1
|
||||||
|
CALL startOneVoice
|
||||||
|
SETD.1 Voice2
|
||||||
|
CALL startOneVoice
|
||||||
|
SETD.1 Voice3
|
||||||
|
CALL startOneVoice
|
||||||
|
INIA 0d4
|
||||||
|
SETD.1 Playing
|
||||||
|
STA.1
|
||||||
|
CALL loadStartPatches
|
||||||
|
RET
|
||||||
|
|
||||||
|
startOneVoice:
|
||||||
|
SETD.0 Empty
|
||||||
|
DPUP.1 0d2
|
||||||
|
STD.0.1
|
||||||
|
INCD.1
|
||||||
|
INCD.1
|
||||||
|
INIA 0d1
|
||||||
|
STA.1 ; A count of one, which runs out on the first tick.
|
||||||
|
INCD.1
|
||||||
|
STA.1 ; And live.
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- A patch, onto the channel named by A ----
|
||||||
|
;
|
||||||
|
; A count, then that many pairs of parameter and value: the format SoundPatch writes and the
|
||||||
|
; one Lander already plays. B holds what is left, which costs nothing to keep - a CALL saves
|
||||||
|
; B, so a caller's count is not disturbed by a patch being loaded inside its loop.
|
||||||
|
;
|
||||||
|
; The channel is selected FIRST. Every parameter write below lands on whichever channel was
|
||||||
|
; named last, so a patch loaded without one would be quietly rewriting somebody else's voice.
|
||||||
|
loadPatch:
|
||||||
|
OUTA 0x41
|
||||||
|
LDB.0
|
||||||
|
INCD.0
|
||||||
|
loadPatchPair:
|
||||||
|
LDA.0
|
||||||
|
OUTA 0x42
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
OUTA 0x43
|
||||||
|
INCD.0
|
||||||
|
DECB
|
||||||
|
BNB loadPatchPair
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
; Six bytes a voice: order cursor, sequence cursor, count, live.
|
||||||
|
;
|
||||||
|
; A LABEL WRITTEN HERE COMES OUT AS ITS ADDRESS, two bytes, most significant first - which is
|
||||||
|
; exactly the shape LDD reads. So a voice starts pointed at its order list and at Empty
|
||||||
|
; without a line of code: nothing relocates on this machine, so the address the assembler
|
||||||
|
; wrote is the address it will have.
|
||||||
|
;
|
||||||
|
; Empty is a sequence of nothing but its end marker, and the count starts at one. The first
|
||||||
|
; tick runs the count out, finds the end, and goes to the order list for the real first
|
||||||
|
; sequence - so the beginning of a piece needs no special case anywhere.
|
||||||
|
Voice0:
|
||||||
|
NoOrder Empty 0d1 0d1
|
||||||
|
Voice1:
|
||||||
|
NoOrder Empty 0d1 0d1
|
||||||
|
Voice2:
|
||||||
|
NoOrder Empty 0d1 0d1
|
||||||
|
Voice3:
|
||||||
|
NoOrder Empty 0d1 0d1
|
||||||
|
|
||||||
|
Playing:
|
||||||
|
0d4
|
||||||
|
ThisChannel:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
; Where this tune's tables are. Pointers rather than the tables themselves, because a tune
|
||||||
|
; read from a file puts them wherever it was put, and the engine must not care which.
|
||||||
|
PatchTable:
|
||||||
|
0x00 0x00
|
||||||
|
SequenceTable:
|
||||||
|
0x00 0x00
|
||||||
|
VoiceStartAt:
|
||||||
|
0x00 0x00
|
||||||
|
|
||||||
|
TuneBase:
|
||||||
|
0x00 0x00
|
||||||
|
TunePatches:
|
||||||
|
0x00
|
||||||
|
TuneSequences:
|
||||||
|
0x00
|
||||||
|
TuneMagic:
|
||||||
|
0x53 0x42 0x54 0x55 0x01 ; "SBTU" and version one.
|
||||||
|
|
||||||
|
; An order list of nothing, which every voice starts pointed at. WHOEVER SUPPLIES THE TUNE
|
||||||
|
; SETS THE REAL ONE - useTune out of a file's header, or a program with a tune of its own in
|
||||||
|
; eight lines - so the records below name nothing that a caller has to have defined. They used
|
||||||
|
; to name Order0 to Order3, which made four order lists part of the contract even for a caller
|
||||||
|
; whose tune comes from a file and never has any.
|
||||||
|
NoOrder:
|
||||||
|
0xFF
|
||||||
|
|
||||||
|
; The sequence a voice starts on, so that its first tick goes through the order list like
|
||||||
|
; every other bar does.
|
||||||
|
Empty:
|
||||||
|
0xFF
|
||||||
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"osc0_waveform": 4,
|
||||||
|
"osc0_dutyCycle": 0.200000,
|
||||||
|
"osc0_detune": 0.000000,
|
||||||
|
"osc0_gain": 3.500000,
|
||||||
|
"osc0_active": 1,
|
||||||
|
"osc0_octave": 0,
|
||||||
|
"osc0_modRouting0": 0,
|
||||||
|
"osc0_modRouting1": 0,
|
||||||
|
"osc0_modRouting2": 0,
|
||||||
|
"osc0_modDepth0": 0.000000,
|
||||||
|
"osc0_modDepth1": 0.000000,
|
||||||
|
"osc0_modDepth2": 0.000000,
|
||||||
|
"osc1_waveform": 5,
|
||||||
|
"osc1_dutyCycle": 0.500000,
|
||||||
|
"osc1_detune": 0.000000,
|
||||||
|
"osc1_gain": 0.000000,
|
||||||
|
"osc1_active": 1,
|
||||||
|
"osc1_octave": 0,
|
||||||
|
"osc1_modRouting0": 0,
|
||||||
|
"osc1_modRouting1": 0,
|
||||||
|
"osc1_modRouting2": 2,
|
||||||
|
"osc1_modDepth0": 0.000000,
|
||||||
|
"osc1_modDepth1": 0.000000,
|
||||||
|
"osc1_modDepth2": 1.500000,
|
||||||
|
"ampEnv_attack": 0.001000,
|
||||||
|
"ampEnv_decay": 0.900000,
|
||||||
|
"ampEnv_sustain": 0.000000,
|
||||||
|
"ampEnv_release": 0.250000,
|
||||||
|
"modEnv_attack": 0.000000,
|
||||||
|
"modEnv_decay": 0.050000,
|
||||||
|
"modEnv_sustain": 0.000000,
|
||||||
|
"modEnv_release": 0.025000,
|
||||||
|
"lfo0_rate": 1.000000,
|
||||||
|
"lfo0_waveform": 0,
|
||||||
|
"lfo0_active": 0,
|
||||||
|
"lfo1_rate": 1.000000,
|
||||||
|
"lfo1_waveform": 0,
|
||||||
|
"lfo1_active": 0,
|
||||||
|
"filter_cutoff": 5000.000000,
|
||||||
|
"filter_resonance": 0.150000,
|
||||||
|
"filter_type": 0,
|
||||||
|
"filter_active": 1,
|
||||||
|
"filter_modRouting": 1,
|
||||||
|
"filter_modDepth": 5000.000000,
|
||||||
|
"filter_resModRouting": 0,
|
||||||
|
"filter_resModDepth": 0.000000,
|
||||||
|
"master_volume": 0.750000,
|
||||||
|
"master_pitchBendRange": 1.000000
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"osc0_waveform": 4,
|
||||||
|
"osc0_dutyCycle": 0.250000,
|
||||||
|
"osc0_detune": -4.000000,
|
||||||
|
"osc0_gain": 2.800000,
|
||||||
|
"osc0_active": 1,
|
||||||
|
"osc0_octave": 0,
|
||||||
|
"osc0_modRouting0": 3,
|
||||||
|
"osc0_modRouting1": 0,
|
||||||
|
"osc0_modRouting2": 0,
|
||||||
|
"osc0_modDepth0": 0.100000,
|
||||||
|
"osc0_modDepth1": 0.000000,
|
||||||
|
"osc0_modDepth2": 0.000000,
|
||||||
|
"osc1_waveform": 4,
|
||||||
|
"osc1_dutyCycle": 0.250000,
|
||||||
|
"osc1_detune": 4.000000,
|
||||||
|
"osc1_gain": 2.800000,
|
||||||
|
"osc1_active": 1,
|
||||||
|
"osc1_octave": 0,
|
||||||
|
"osc1_modRouting0": 0,
|
||||||
|
"osc1_modRouting1": 0,
|
||||||
|
"osc1_modRouting2": 0,
|
||||||
|
"osc1_modDepth0": 0.000000,
|
||||||
|
"osc1_modDepth1": 0.000000,
|
||||||
|
"osc1_modDepth2": 0.000000,
|
||||||
|
"ampEnv_attack": 0.120000,
|
||||||
|
"ampEnv_decay": 0.100000,
|
||||||
|
"ampEnv_sustain": 0.800000,
|
||||||
|
"ampEnv_release": 0.200000,
|
||||||
|
"modEnv_attack": 0.001000,
|
||||||
|
"modEnv_decay": 0.500000,
|
||||||
|
"modEnv_sustain": 0.000000,
|
||||||
|
"modEnv_release": 0.200000,
|
||||||
|
"lfo0_rate": 0.350000,
|
||||||
|
"lfo0_waveform": 0,
|
||||||
|
"lfo0_active": 1,
|
||||||
|
"lfo1_rate": 1.000000,
|
||||||
|
"lfo1_waveform": 0,
|
||||||
|
"lfo1_active": 0,
|
||||||
|
"filter_cutoff": 20000.000000,
|
||||||
|
"filter_resonance": 0.000000,
|
||||||
|
"filter_type": 0,
|
||||||
|
"filter_active": 0,
|
||||||
|
"filter_modRouting": 0,
|
||||||
|
"filter_modDepth": 0.000000,
|
||||||
|
"filter_resModRouting": 0,
|
||||||
|
"filter_resModDepth": 0.000000,
|
||||||
|
"master_volume": 0.700000,
|
||||||
|
"master_pitchBendRange": 1.000000
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"osc0_waveform": 1,
|
||||||
|
"osc0_dutyCycle": 0.500000,
|
||||||
|
"osc0_detune": 0.000000,
|
||||||
|
"osc0_gain": 2.500000,
|
||||||
|
"osc0_active": 1,
|
||||||
|
"osc0_octave": 0,
|
||||||
|
"osc0_modRouting0": 0,
|
||||||
|
"osc0_modRouting1": 0,
|
||||||
|
"osc0_modRouting2": 0,
|
||||||
|
"osc0_modDepth0": 0.000000,
|
||||||
|
"osc0_modDepth1": 0.000000,
|
||||||
|
"osc0_modDepth2": 0.000000,
|
||||||
|
"osc1_waveform": 0,
|
||||||
|
"osc1_dutyCycle": 0.500000,
|
||||||
|
"osc1_detune": 0.000000,
|
||||||
|
"osc1_gain": 0.300000,
|
||||||
|
"osc1_active": 1,
|
||||||
|
"osc1_octave": 2,
|
||||||
|
"osc1_modRouting0": 0,
|
||||||
|
"osc1_modRouting1": 0,
|
||||||
|
"osc1_modRouting2": 2,
|
||||||
|
"osc1_modDepth0": 0.000000,
|
||||||
|
"osc1_modDepth1": 0.000000,
|
||||||
|
"osc1_modDepth2": 2.000000,
|
||||||
|
"ampEnv_attack": 0.003000,
|
||||||
|
"ampEnv_decay": 0.080000,
|
||||||
|
"ampEnv_sustain": 0.800000,
|
||||||
|
"ampEnv_release": 0.250000,
|
||||||
|
"modEnv_attack": 0.000000,
|
||||||
|
"modEnv_decay": 0.090000,
|
||||||
|
"modEnv_sustain": 0.000000,
|
||||||
|
"modEnv_release": 0.050000,
|
||||||
|
"lfo0_rate": 1.000000,
|
||||||
|
"lfo0_waveform": 0,
|
||||||
|
"lfo0_active": 0,
|
||||||
|
"lfo0_mode": 0,
|
||||||
|
"lfo1_rate": 1.000000,
|
||||||
|
"lfo1_waveform": 0,
|
||||||
|
"lfo1_active": 0,
|
||||||
|
"lfo1_mode": 0,
|
||||||
|
"filter_cutoff": 5000.000000,
|
||||||
|
"filter_resonance": 0.150000,
|
||||||
|
"filter_type": 0,
|
||||||
|
"filter_active": 1,
|
||||||
|
"filter_modRouting": 2,
|
||||||
|
"filter_modDepth": 4500.000000,
|
||||||
|
"filter_resModRouting": 0,
|
||||||
|
"filter_resModDepth": 0.000000,
|
||||||
|
"voice_levelSource": 1,
|
||||||
|
"voice_gate": 0,
|
||||||
|
"master_volume": 1.000000,
|
||||||
|
"master_pitchBendRange": 1.000000
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"osc0_waveform": 4,
|
||||||
|
"osc0_dutyCycle": 0.700000,
|
||||||
|
"osc0_detune": 0.000000,
|
||||||
|
"osc0_gain": 3.500000,
|
||||||
|
"osc0_active": 1,
|
||||||
|
"osc0_octave": 0,
|
||||||
|
"osc0_modRouting0": 0,
|
||||||
|
"osc0_modRouting1": 0,
|
||||||
|
"osc0_modRouting2": 0,
|
||||||
|
"osc0_modDepth0": 0.000000,
|
||||||
|
"osc0_modDepth1": 0.000000,
|
||||||
|
"osc0_modDepth2": 0.000000,
|
||||||
|
"osc1_waveform": 4,
|
||||||
|
"osc1_dutyCycle": 0.700000,
|
||||||
|
"osc1_detune": 3.000000,
|
||||||
|
"osc1_gain": 1.500000,
|
||||||
|
"osc1_active": 1,
|
||||||
|
"osc1_octave": 1,
|
||||||
|
"osc1_modRouting0": 0,
|
||||||
|
"osc1_modRouting1": 0,
|
||||||
|
"osc1_modRouting2": 2,
|
||||||
|
"osc1_modDepth0": 0.000000,
|
||||||
|
"osc1_modDepth1": 0.000000,
|
||||||
|
"osc1_modDepth2": 1.000000,
|
||||||
|
"ampEnv_attack": 0.001000,
|
||||||
|
"ampEnv_decay": 0.700000,
|
||||||
|
"ampEnv_sustain": 0.000000,
|
||||||
|
"ampEnv_release": 0.060000,
|
||||||
|
"modEnv_attack": 0.000000,
|
||||||
|
"modEnv_decay": 0.060000,
|
||||||
|
"modEnv_sustain": 0.000000,
|
||||||
|
"modEnv_release": 0.030000,
|
||||||
|
"lfo0_rate": 1.000000,
|
||||||
|
"lfo0_waveform": 0,
|
||||||
|
"lfo0_active": 0,
|
||||||
|
"lfo1_rate": 1.000000,
|
||||||
|
"lfo1_waveform": 0,
|
||||||
|
"lfo1_active": 0,
|
||||||
|
"filter_cutoff": 9000.000000,
|
||||||
|
"filter_resonance": 0.120000,
|
||||||
|
"filter_type": 0,
|
||||||
|
"filter_active": 1,
|
||||||
|
"filter_modRouting": 2,
|
||||||
|
"filter_modDepth": 5000.000000,
|
||||||
|
"filter_resModRouting": 0,
|
||||||
|
"filter_resModDepth": 0.000000,
|
||||||
|
"master_volume": 0.750000,
|
||||||
|
"master_pitchBendRange": 0.000000
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"osc0_waveform": 0,
|
||||||
|
"osc0_dutyCycle": 0.500000,
|
||||||
|
"osc0_detune": 0.000000,
|
||||||
|
"osc0_gain": 4.000000,
|
||||||
|
"osc0_active": 1,
|
||||||
|
"osc0_octave": 1,
|
||||||
|
"osc0_modRouting0": 0,
|
||||||
|
"osc0_modRouting1": 0,
|
||||||
|
"osc0_modRouting2": 0,
|
||||||
|
"osc0_modDepth0": 0.000000,
|
||||||
|
"osc0_modDepth1": 0.000000,
|
||||||
|
"osc0_modDepth2": 0.000000,
|
||||||
|
"osc1_waveform": 0,
|
||||||
|
"osc1_dutyCycle": 0.500000,
|
||||||
|
"osc1_detune": 8.000000,
|
||||||
|
"osc1_gain": 3.423103,
|
||||||
|
"osc1_active": 1,
|
||||||
|
"osc1_octave": 2,
|
||||||
|
"osc1_modRouting0": 0,
|
||||||
|
"osc1_modRouting1": 0,
|
||||||
|
"osc1_modRouting2": 0,
|
||||||
|
"osc1_modDepth0": 0.000000,
|
||||||
|
"osc1_modDepth1": 0.000000,
|
||||||
|
"osc1_modDepth2": 0.000000,
|
||||||
|
"ampEnv_attack": 0.001000,
|
||||||
|
"ampEnv_decay": 0.600000,
|
||||||
|
"ampEnv_sustain": 0.000000,
|
||||||
|
"ampEnv_release": 0.100000,
|
||||||
|
"modEnv_attack": 0.001000,
|
||||||
|
"modEnv_decay": 0.100000,
|
||||||
|
"modEnv_sustain": 0.000000,
|
||||||
|
"modEnv_release": 0.020000,
|
||||||
|
"lfo0_rate": 1.000000,
|
||||||
|
"lfo0_waveform": 0,
|
||||||
|
"lfo0_active": 0,
|
||||||
|
"lfo1_rate": 1.000000,
|
||||||
|
"lfo1_waveform": 0,
|
||||||
|
"lfo1_active": 0,
|
||||||
|
"filter_cutoff": 3000.000000,
|
||||||
|
"filter_resonance": 0.080000,
|
||||||
|
"filter_type": 0,
|
||||||
|
"filter_active": 1,
|
||||||
|
"filter_modRouting": 0,
|
||||||
|
"filter_modDepth": 0.000000,
|
||||||
|
"filter_resModRouting": 0,
|
||||||
|
"filter_resModDepth": 0.000000,
|
||||||
|
"master_volume": 0.700000,
|
||||||
|
"master_pitchBendRange": 2.000000
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"osc0_waveform": 4,
|
||||||
|
"osc0_dutyCycle": 0.500000,
|
||||||
|
"osc0_detune": -8.000000,
|
||||||
|
"osc0_gain": 2.500000,
|
||||||
|
"osc0_active": 1,
|
||||||
|
"osc0_octave": 0,
|
||||||
|
"osc0_modRouting0": 3,
|
||||||
|
"osc0_modRouting1": 0,
|
||||||
|
"osc0_modRouting2": 0,
|
||||||
|
"osc0_modDepth0": 0.280000,
|
||||||
|
"osc0_modDepth1": 0.000000,
|
||||||
|
"osc0_modDepth2": 0.000000,
|
||||||
|
"osc1_waveform": 4,
|
||||||
|
"osc1_dutyCycle": 0.500000,
|
||||||
|
"osc1_detune": 8.000000,
|
||||||
|
"osc1_gain": 2.500000,
|
||||||
|
"osc1_active": 1,
|
||||||
|
"osc1_octave": 0,
|
||||||
|
"osc1_modRouting0": 4,
|
||||||
|
"osc1_modRouting1": 0,
|
||||||
|
"osc1_modRouting2": 0,
|
||||||
|
"osc1_modDepth0": 0.280000,
|
||||||
|
"osc1_modDepth1": 0.000000,
|
||||||
|
"osc1_modDepth2": 0.000000,
|
||||||
|
"ampEnv_attack": 0.420000,
|
||||||
|
"ampEnv_decay": 0.100000,
|
||||||
|
"ampEnv_sustain": 0.880000,
|
||||||
|
"ampEnv_release": 1.300000,
|
||||||
|
"modEnv_attack": 0.000000,
|
||||||
|
"modEnv_decay": 0.500000,
|
||||||
|
"modEnv_sustain": 0.000000,
|
||||||
|
"modEnv_release": 0.400000,
|
||||||
|
"lfo0_rate": 0.290000,
|
||||||
|
"lfo0_waveform": 0,
|
||||||
|
"lfo0_active": 1,
|
||||||
|
"lfo1_rate": 0.370000,
|
||||||
|
"lfo1_waveform": 0,
|
||||||
|
"lfo1_active": 1,
|
||||||
|
"filter_cutoff": 4200.000000,
|
||||||
|
"filter_resonance": 0.080000,
|
||||||
|
"filter_type": 0,
|
||||||
|
"filter_active": 1,
|
||||||
|
"filter_modRouting": 1,
|
||||||
|
"filter_modDepth": 2800.000000,
|
||||||
|
"filter_resModRouting": 0,
|
||||||
|
"filter_resModDepth": 0.000000,
|
||||||
|
"master_volume": 0.720000,
|
||||||
|
"master_pitchBendRange": 2.000000
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"osc0_waveform": 4,
|
||||||
|
"osc0_dutyCycle": 0.250000,
|
||||||
|
"osc0_detune": 0.000000,
|
||||||
|
"osc0_gain": 3.000000,
|
||||||
|
"osc0_active": 1,
|
||||||
|
"osc0_octave": 0,
|
||||||
|
"osc0_modRouting0": 0,
|
||||||
|
"osc0_modRouting1": 3,
|
||||||
|
"osc0_modRouting2": 0,
|
||||||
|
"osc0_modDepth0": 0.000000,
|
||||||
|
"osc0_modDepth1": 5.000000,
|
||||||
|
"osc0_modDepth2": 0.000000,
|
||||||
|
"osc1_waveform": 4,
|
||||||
|
"osc1_dutyCycle": 0.200000,
|
||||||
|
"osc1_detune": 2.000000,
|
||||||
|
"osc1_gain": 2.000000,
|
||||||
|
"osc1_active": 1,
|
||||||
|
"osc1_octave": 0,
|
||||||
|
"osc1_modRouting0": 0,
|
||||||
|
"osc1_modRouting1": 0,
|
||||||
|
"osc1_modRouting2": 0,
|
||||||
|
"osc1_modDepth0": 0.000000,
|
||||||
|
"osc1_modDepth1": 0.000000,
|
||||||
|
"osc1_modDepth2": 0.000000,
|
||||||
|
"ampEnv_attack": 0.055000,
|
||||||
|
"ampEnv_decay": 0.030000,
|
||||||
|
"ampEnv_sustain": 0.920000,
|
||||||
|
"ampEnv_release": 0.150000,
|
||||||
|
"modEnv_attack": 0.000000,
|
||||||
|
"modEnv_decay": 0.500000,
|
||||||
|
"modEnv_sustain": 0.000000,
|
||||||
|
"modEnv_release": 0.200000,
|
||||||
|
"lfo0_rate": 5.000000,
|
||||||
|
"lfo0_waveform": 0,
|
||||||
|
"lfo0_active": 1,
|
||||||
|
"lfo1_rate": 1.000000,
|
||||||
|
"lfo1_waveform": 0,
|
||||||
|
"lfo1_active": 0,
|
||||||
|
"filter_cutoff": 2500.000000,
|
||||||
|
"filter_resonance": 0.400000,
|
||||||
|
"filter_type": 0,
|
||||||
|
"filter_active": 1,
|
||||||
|
"filter_modRouting": 0,
|
||||||
|
"filter_modDepth": 0.000000,
|
||||||
|
"filter_resModRouting": 0,
|
||||||
|
"filter_resModDepth": 0.000000,
|
||||||
|
"master_volume": 0.700000,
|
||||||
|
"master_pitchBendRange": 2.000000
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"osc0_waveform": 4,
|
||||||
|
"osc0_dutyCycle": 0.153236,
|
||||||
|
"osc0_detune": 0.000000,
|
||||||
|
"osc0_gain": 4.000000,
|
||||||
|
"osc0_active": 1,
|
||||||
|
"osc0_octave": 0,
|
||||||
|
"osc0_modRouting0": 1,
|
||||||
|
"osc0_modRouting1": 0,
|
||||||
|
"osc0_modRouting2": 3,
|
||||||
|
"osc0_modDepth0": 0.290788,
|
||||||
|
"osc0_modDepth1": 0.000000,
|
||||||
|
"osc0_modDepth2": -0.833591,
|
||||||
|
"osc1_waveform": 1,
|
||||||
|
"osc1_dutyCycle": 0.500000,
|
||||||
|
"osc1_detune": 0.000000,
|
||||||
|
"osc1_gain": 0.000000,
|
||||||
|
"osc1_active": 0,
|
||||||
|
"osc1_octave": 0,
|
||||||
|
"osc1_modRouting0": 0,
|
||||||
|
"osc1_modRouting1": 0,
|
||||||
|
"osc1_modRouting2": 0,
|
||||||
|
"osc1_modDepth0": 0.000000,
|
||||||
|
"osc1_modDepth1": 0.000000,
|
||||||
|
"osc1_modDepth2": 0.000000,
|
||||||
|
"ampEnv_attack": 0.005000,
|
||||||
|
"ampEnv_decay": 0.100000,
|
||||||
|
"ampEnv_sustain": 0.700000,
|
||||||
|
"ampEnv_release": 0.500000,
|
||||||
|
"modEnv_attack": 0.005000,
|
||||||
|
"modEnv_decay": 0.500000,
|
||||||
|
"modEnv_sustain": 0.000000,
|
||||||
|
"modEnv_release": 0.100000,
|
||||||
|
"lfo0_rate": 2.598532,
|
||||||
|
"lfo0_waveform": 0,
|
||||||
|
"lfo0_active": 1,
|
||||||
|
"lfo1_rate": 1.000000,
|
||||||
|
"lfo1_waveform": 0,
|
||||||
|
"lfo1_active": 0,
|
||||||
|
"filter_cutoff": 8000.000000,
|
||||||
|
"filter_resonance": 0.000000,
|
||||||
|
"filter_type": 0,
|
||||||
|
"filter_active": 0,
|
||||||
|
"filter_modRouting": 0,
|
||||||
|
"filter_modDepth": 0.000000,
|
||||||
|
"filter_resModRouting": 0,
|
||||||
|
"filter_resModDepth": 0.000000,
|
||||||
|
"master_volume": 0.362205,
|
||||||
|
"master_pitchBendRange": 2.000000
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
; KalimbaPatch, converted from Programs/Sounds/Kalimba.json by SoundPatch.
|
||||||
|
; Designed in soundThing, where it can be heard. Do not edit the numbers
|
||||||
|
; here: change the patch and convert it again.
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
KalimbaPatch:
|
||||||
|
0d47 ; how many pairs follow
|
||||||
|
0x00 0d0 ; oscillator 0, waveform
|
||||||
|
0x01 0d255 ; gain
|
||||||
|
0x02 0d128 ; duty
|
||||||
|
0x03 0d128 ; detune, in cents
|
||||||
|
0x04 0d129 ; octave
|
||||||
|
0x05 0d1 ; on
|
||||||
|
0x06 0d0 ; what moves its width
|
||||||
|
0x07 0d128 ; and how far
|
||||||
|
0x08 0d0 ; what moves its pitch
|
||||||
|
0x09 0d128 ; and how far
|
||||||
|
0x0A 0d0 ; what moves its gain
|
||||||
|
0x0B 0d128 ; and how far
|
||||||
|
0x10 0d0 ; oscillator 1, waveform
|
||||||
|
0x11 0d218 ; gain
|
||||||
|
0x12 0d128 ; duty
|
||||||
|
0x13 0d129 ; detune, in cents
|
||||||
|
0x14 0d130 ; octave
|
||||||
|
0x15 0d1 ; on
|
||||||
|
0x16 0d0 ; what moves its width
|
||||||
|
0x17 0d128 ; and how far
|
||||||
|
0x18 0d0 ; what moves its pitch
|
||||||
|
0x19 0d128 ; and how far
|
||||||
|
0x1A 0d0 ; what moves its gain
|
||||||
|
0x1B 0d128 ; and how far
|
||||||
|
0x20 0d4 ; amplitude envelope, attack
|
||||||
|
0x21 0d99 ; decay
|
||||||
|
0x22 0d0 ; sustain
|
||||||
|
0x23 0d40 ; release
|
||||||
|
0x30 0d4 ; modulation envelope, attack
|
||||||
|
0x31 0d40 ; decay
|
||||||
|
0x32 0d0 ; sustain
|
||||||
|
0x33 0d18 ; release
|
||||||
|
0x40 0d1 ; filter, on
|
||||||
|
0x41 0d0 ; type: 0 low, 1 high, 2 band
|
||||||
|
0x42 0d185 ; cutoff, in hertz
|
||||||
|
0x43 0d21 ; resonance
|
||||||
|
0x44 0d0 ; what moves the cutoff
|
||||||
|
0x45 0d128 ; and how far, in hertz
|
||||||
|
0x46 0d0 ; what moves the resonance
|
||||||
|
0x47 0d128 ; and how far
|
||||||
|
0x60 0d0 ; LFO 0, on
|
||||||
|
0x61 0d0 ; waveform
|
||||||
|
0x62 0d127 ; rate, in hertz
|
||||||
|
0x70 0d0 ; LFO 1, on
|
||||||
|
0x71 0d0 ; waveform
|
||||||
|
0x72 0d127 ; rate, in hertz
|
||||||
|
0x50 0d1 ; what shapes the level, which this patch predates
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
; OboePatch, converted from Programs/Sounds/Oboe.json by SoundPatch.
|
||||||
|
; Designed in soundThing, where it can be heard. Do not edit the numbers
|
||||||
|
; here: change the patch and convert it again.
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
OboePatch:
|
||||||
|
0d47 ; how many pairs follow
|
||||||
|
0x00 0d4 ; oscillator 0, waveform
|
||||||
|
0x01 0d191 ; gain
|
||||||
|
0x02 0d57 ; duty
|
||||||
|
0x03 0d128 ; detune, in cents
|
||||||
|
0x04 0d128 ; octave
|
||||||
|
0x05 0d1 ; on
|
||||||
|
0x06 0d0 ; what moves its width
|
||||||
|
0x07 0d128 ; and how far
|
||||||
|
0x08 0d3 ; what moves its pitch
|
||||||
|
0x09 0d129 ; and how far
|
||||||
|
0x0A 0d0 ; what moves its gain
|
||||||
|
0x0B 0d128 ; and how far
|
||||||
|
0x10 0d4 ; oscillator 1, waveform
|
||||||
|
0x11 0d128 ; gain
|
||||||
|
0x12 0d43 ; duty
|
||||||
|
0x13 0d128 ; detune, in cents
|
||||||
|
0x14 0d128 ; octave
|
||||||
|
0x15 0d1 ; on
|
||||||
|
0x16 0d0 ; what moves its width
|
||||||
|
0x17 0d128 ; and how far
|
||||||
|
0x18 0d0 ; what moves its pitch
|
||||||
|
0x19 0d128 ; and how far
|
||||||
|
0x1A 0d0 ; what moves its gain
|
||||||
|
0x1B 0d128 ; and how far
|
||||||
|
0x20 0d30 ; amplitude envelope, attack
|
||||||
|
0x21 0d22 ; decay
|
||||||
|
0x22 0d235 ; sustain
|
||||||
|
0x23 0d49 ; release
|
||||||
|
0x30 0d0 ; modulation envelope, attack
|
||||||
|
0x31 0d90 ; decay
|
||||||
|
0x32 0d0 ; sustain
|
||||||
|
0x33 0d57 ; release
|
||||||
|
0x40 0d1 ; filter, on
|
||||||
|
0x41 0d0 ; type: 0 low, 1 high, 2 band
|
||||||
|
0x42 0d178 ; cutoff, in hertz
|
||||||
|
0x43 0d103 ; resonance
|
||||||
|
0x44 0d0 ; what moves the cutoff
|
||||||
|
0x45 0d128 ; and how far, in hertz
|
||||||
|
0x46 0d0 ; what moves the resonance
|
||||||
|
0x47 0d128 ; and how far
|
||||||
|
0x60 0d1 ; LFO 0, on
|
||||||
|
0x61 0d0 ; waveform
|
||||||
|
0x62 0d196 ; rate, in hertz
|
||||||
|
0x70 0d0 ; LFO 1, on
|
||||||
|
0x71 0d0 ; waveform
|
||||||
|
0x72 0d127 ; rate, in hertz
|
||||||
|
0x50 0d1 ; what shapes the level, which this patch predates
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
; SquarePatch, converted from Programs/Sounds/SidSquare.json by SoundPatch.
|
||||||
|
; Designed in soundThing, where it can be heard. Do not edit the numbers
|
||||||
|
; here: change the patch and convert it again.
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
SquarePatch:
|
||||||
|
0d47 ; how many pairs follow
|
||||||
|
0x00 0d4 ; oscillator 0, waveform
|
||||||
|
0x01 0d255 ; gain
|
||||||
|
0x02 0d29 ; duty
|
||||||
|
0x03 0d128 ; detune, in cents
|
||||||
|
0x04 0d128 ; octave
|
||||||
|
0x05 0d1 ; on
|
||||||
|
0x06 0d1 ; what moves its width
|
||||||
|
0x07 0d202 ; and how far
|
||||||
|
0x08 0d0 ; what moves its pitch
|
||||||
|
0x09 0d128 ; and how far
|
||||||
|
0x0A 0d3 ; what moves its gain
|
||||||
|
0x0B 0d101 ; and how far
|
||||||
|
0x10 0d1 ; oscillator 1, waveform
|
||||||
|
0x11 0d0 ; gain
|
||||||
|
0x12 0d128 ; duty
|
||||||
|
0x13 0d128 ; detune, in cents
|
||||||
|
0x14 0d128 ; octave
|
||||||
|
0x15 0d0 ; on
|
||||||
|
0x16 0d0 ; what moves its width
|
||||||
|
0x17 0d128 ; and how far
|
||||||
|
0x18 0d0 ; what moves its pitch
|
||||||
|
0x19 0d128 ; and how far
|
||||||
|
0x1A 0d0 ; what moves its gain
|
||||||
|
0x1B 0d128 ; and how far
|
||||||
|
0x20 0d9 ; amplitude envelope, attack
|
||||||
|
0x21 0d40 ; decay
|
||||||
|
0x22 0d179 ; sustain
|
||||||
|
0x23 0d90 ; release
|
||||||
|
0x30 0d9 ; modulation envelope, attack
|
||||||
|
0x31 0d90 ; decay
|
||||||
|
0x32 0d0 ; sustain
|
||||||
|
0x33 0d40 ; release
|
||||||
|
0x40 0d0 ; filter, on
|
||||||
|
0x41 0d0 ; type: 0 low, 1 high, 2 band
|
||||||
|
0x42 0d221 ; cutoff, in hertz
|
||||||
|
0x43 0d0 ; resonance
|
||||||
|
0x44 0d0 ; what moves the cutoff
|
||||||
|
0x45 0d128 ; and how far, in hertz
|
||||||
|
0x46 0d0 ; what moves the resonance
|
||||||
|
0x47 0d128 ; and how far
|
||||||
|
0x60 0d1 ; LFO 0, on
|
||||||
|
0x61 0d0 ; waveform
|
||||||
|
0x62 0d168 ; rate, in hertz
|
||||||
|
0x70 0d0 ; LFO 1, on
|
||||||
|
0x71 0d0 ; waveform
|
||||||
|
0x72 0d127 ; rate, in hertz
|
||||||
|
0x50 0d1 ; what shapes the level, which this patch predates
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
; StringsPatch, converted from Programs/Sounds/LushStrings.json by SoundPatch.
|
||||||
|
; Designed in soundThing, where it can be heard. Do not edit the numbers
|
||||||
|
; here: change the patch and convert it again.
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
StringsPatch:
|
||||||
|
0d47 ; how many pairs follow
|
||||||
|
0x00 0d4 ; oscillator 0, waveform
|
||||||
|
0x01 0d159 ; gain
|
||||||
|
0x02 0d128 ; duty
|
||||||
|
0x03 0d127 ; detune, in cents
|
||||||
|
0x04 0d128 ; octave
|
||||||
|
0x05 0d1 ; on
|
||||||
|
0x06 0d3 ; what moves its width
|
||||||
|
0x07 0d200 ; and how far
|
||||||
|
0x08 0d0 ; what moves its pitch
|
||||||
|
0x09 0d128 ; and how far
|
||||||
|
0x0A 0d0 ; what moves its gain
|
||||||
|
0x0B 0d128 ; and how far
|
||||||
|
0x10 0d4 ; oscillator 1, waveform
|
||||||
|
0x11 0d159 ; gain
|
||||||
|
0x12 0d128 ; duty
|
||||||
|
0x13 0d129 ; detune, in cents
|
||||||
|
0x14 0d128 ; octave
|
||||||
|
0x15 0d1 ; on
|
||||||
|
0x16 0d4 ; what moves its width
|
||||||
|
0x17 0d200 ; and how far
|
||||||
|
0x18 0d0 ; what moves its pitch
|
||||||
|
0x19 0d128 ; and how far
|
||||||
|
0x1A 0d0 ; what moves its gain
|
||||||
|
0x1B 0d128 ; and how far
|
||||||
|
0x20 0d83 ; amplitude envelope, attack
|
||||||
|
0x21 0d40 ; decay
|
||||||
|
0x22 0d224 ; sustain
|
||||||
|
0x23 0d145 ; release
|
||||||
|
0x30 0d0 ; modulation envelope, attack
|
||||||
|
0x31 0d90 ; decay
|
||||||
|
0x32 0d0 ; sustain
|
||||||
|
0x33 0d81 ; release
|
||||||
|
0x40 0d1 ; filter, on
|
||||||
|
0x41 0d0 ; type: 0 low, 1 high, 2 band
|
||||||
|
0x42 0d197 ; cutoff, in hertz
|
||||||
|
0x43 0d21 ; resonance
|
||||||
|
0x44 0d1 ; what moves the cutoff
|
||||||
|
0x45 0d173 ; and how far, in hertz
|
||||||
|
0x46 0d0 ; what moves the resonance
|
||||||
|
0x47 0d128 ; and how far
|
||||||
|
0x60 0d1 ; LFO 0, on
|
||||||
|
0x61 0d0 ; waveform
|
||||||
|
0x62 0d75 ; rate, in hertz
|
||||||
|
0x70 0d1 ; LFO 1, on
|
||||||
|
0x71 0d0 ; waveform
|
||||||
|
0x72 0d85 ; rate, in hertz
|
||||||
|
0x50 0d1 ; what shapes the level, which this patch predates
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
; Four bars, and the bass is the same one under two of them.
|
||||||
|
#Tick 0d125000 ; cycles a tick: a sixteenth note at 120 beats a minute
|
||||||
|
|
||||||
|
#Patch Oboe oboe.patch
|
||||||
|
|
||||||
|
#Voice 0d0 Oboe ; which instrument each voice starts on
|
||||||
|
#Voice 0d1 Oboe
|
||||||
|
#Voice 0d2 Oboe
|
||||||
|
#Voice 0d3 Oboe
|
||||||
|
|
||||||
|
#Sequence MotifA
|
||||||
|
0x30 0d2
|
||||||
|
0x00 0d2
|
||||||
|
0x3C 0d2
|
||||||
|
0x00 0d4
|
||||||
|
0x48 0d4
|
||||||
|
0x00 0d4
|
||||||
|
0x47 0d4
|
||||||
|
0x00 0d6
|
||||||
|
; 28 beats
|
||||||
|
|
||||||
|
#Sequence MotifB
|
||||||
|
0x00 0d2
|
||||||
|
0x35 0d2
|
||||||
|
0x00 0d2
|
||||||
|
0x40 0d4
|
||||||
|
0x00 0d4
|
||||||
|
0x45 0d4
|
||||||
|
0x00 0d4
|
||||||
|
0x43 0d6
|
||||||
|
|
||||||
|
|
||||||
|
#Sequence IntroQuiet
|
||||||
|
0x00 0d28
|
||||||
|
|
||||||
|
#Sequence ChordRoot
|
||||||
|
0x2C 0x4
|
||||||
|
0x2E 0x4
|
||||||
|
0x30 0x4
|
||||||
|
|
||||||
|
#Sequence ChordFifth
|
||||||
|
0x33 0x4
|
||||||
|
0x35 0x4
|
||||||
|
0x37 0x4
|
||||||
|
|
||||||
|
#Sequence ChordThird
|
||||||
|
0x3C 0x4
|
||||||
|
0x3E 0x4
|
||||||
|
0x40 0x4
|
||||||
|
|
||||||
|
#Sequence ChordQuiet
|
||||||
|
0x00 0d12
|
||||||
|
|
||||||
|
#Sequence Bass
|
||||||
|
0x24 0x8
|
||||||
|
|
||||||
|
#Order 0d0
|
||||||
|
MotifA ChordRoot
|
||||||
|
|
||||||
|
#Order 0d1
|
||||||
|
MotifB ChordFifth
|
||||||
|
|
||||||
|
#Order 0d2
|
||||||
|
IntroQuiet ChordThird
|
||||||
|
|
||||||
|
#Order 0d3
|
||||||
|
IntroQuiet ChordQuiet Bass
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
; Four bars, and the bass is the same one under two of them.
|
||||||
|
#Tick 0d125000 ; cycles a tick: a sixteenth note at 120 beats a minute
|
||||||
|
|
||||||
|
#Patch Oboe oboe.patch
|
||||||
|
|
||||||
|
#Voice 0d0 Oboe ; which instrument each voice starts on
|
||||||
|
#Voice 0d1 Oboe
|
||||||
|
#Voice 0d2 Oboe
|
||||||
|
|
||||||
|
#Sequence MotifA
|
||||||
|
0x30 0d2
|
||||||
|
0x00 0d2
|
||||||
|
0x3C 0d2
|
||||||
|
0x00 0d6
|
||||||
|
|
||||||
|
|
||||||
|
#Sequence MotifB
|
||||||
|
0x00 0d2
|
||||||
|
0x35 0d2
|
||||||
|
0x00 0d2
|
||||||
|
0x41 0d6
|
||||||
|
|
||||||
|
; Truncate Anachronaut motif to three introductory notes.
|
||||||
|
|
||||||
|
#Sequence IntroQuiet
|
||||||
|
0x00 0d12
|
||||||
|
|
||||||
|
; new Chords are C-maj, A-maj. "Seeeee-gaaaan"
|
||||||
|
|
||||||
|
#Sequence ChordA
|
||||||
|
; C
|
||||||
|
0x30 0d6
|
||||||
|
; A
|
||||||
|
0x2D 0d6
|
||||||
|
|
||||||
|
; One continuous note here makes the chords blend together better.
|
||||||
|
|
||||||
|
#Sequence ChordB
|
||||||
|
; E+
|
||||||
|
0x34 0d12
|
||||||
|
|
||||||
|
#Sequence ChordC
|
||||||
|
; G+
|
||||||
|
0x37 0d6
|
||||||
|
; Cs+
|
||||||
|
0x3D 0x6
|
||||||
|
|
||||||
|
#Sequence ChordQuiet
|
||||||
|
0x00 0d12
|
||||||
|
|
||||||
|
; The extra rests on the end prevent the splash screen from ending before the voices are silent.
|
||||||
|
|
||||||
|
#Order 0d0
|
||||||
|
MotifA ChordA IntroQuiet
|
||||||
|
|
||||||
|
#Order 0d1
|
||||||
|
MotifB ChordB IntroQuiet
|
||||||
|
|
||||||
|
#Order 0d2
|
||||||
|
IntroQuiet ChordC IntroQuiet
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
; What the console's attribute register says down a serial line.
|
||||||
|
;
|
||||||
|
; The attribute has always meant something to the SCREEN and nothing to a terminal: its low
|
||||||
|
; nibble picks one of sixteen ink and paper pairs, and only videoPutCell ever read it. So a
|
||||||
|
; fault printed in red was red in the window and grey down the wire, and every program that
|
||||||
|
; chose a colour was choosing it for half its audience.
|
||||||
|
;
|
||||||
|
; This walks all sixteen schemes and then stops WITH ONE STILL SET, which is the second half
|
||||||
|
; of what is being checked: the machine has to put the terminal back to plain on its way out,
|
||||||
|
; and the reset has to come out AFTER the text rather than in front of it. An atexit handler
|
||||||
|
; runs before stdio flushes what is buffered, so a reset written straight to the file
|
||||||
|
; descriptor would arrive first and colour nothing.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Include print.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
; Banks 0 to 7 are ink on black. Nought is plain and is expected to say nothing at all: a
|
||||||
|
; terminal is assumed to start plain, so a machine that never asks for a colour must be
|
||||||
|
; silent rather than opening every session with a reset nobody wanted.
|
||||||
|
RSTA
|
||||||
|
SETD Scheme
|
||||||
|
STA
|
||||||
|
|
||||||
|
inkNext:
|
||||||
|
SETD Scheme
|
||||||
|
LDA
|
||||||
|
OUTA 0x06
|
||||||
|
SETD InkText
|
||||||
|
CALL printString
|
||||||
|
SETD Scheme
|
||||||
|
LDA
|
||||||
|
CALL printDigit
|
||||||
|
CALL lineFeed
|
||||||
|
|
||||||
|
SETD Scheme
|
||||||
|
LDA
|
||||||
|
INCA
|
||||||
|
STA
|
||||||
|
INIB 0d16
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ inkNext
|
||||||
|
|
||||||
|
; ---- And it stops in colour, on purpose ----
|
||||||
|
;
|
||||||
|
; Fifteen is the last of them and nothing puts it back. What follows this in the recording
|
||||||
|
; is whatever the machine says on its way out, which is the check.
|
||||||
|
HALT
|
||||||
|
|
||||||
|
; A is nought to fifteen, said as ONE HEX CHARACTER. The scheme is a nibble, so a nibble is
|
||||||
|
; what it should read as - and one character means no scratch byte to hold the second digit
|
||||||
|
; while the first goes out, which is what the decimal version needed and got wrong.
|
||||||
|
printDigit:
|
||||||
|
INIB 0d10
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRC digitLow ; Borrowed, so it is below ten.
|
||||||
|
MVQA ; What is left once the ten is taken off.
|
||||||
|
INIB 0d65 ; 'A', because the ten has already gone.
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
OUTA 0x00
|
||||||
|
RET
|
||||||
|
digitLow:
|
||||||
|
INIB 0d48
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
OUTA 0x00
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
InkText:
|
||||||
|
"scheme "
|
||||||
|
Scheme:
|
||||||
|
0x00
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
; Four voices on one tick, staggered so that each can be heard on its own.
|
||||||
|
;
|
||||||
|
; M2 of the music player. What is being checked is that the voices share the tick and NOTHING
|
||||||
|
; ELSE: each keeps its own place in its own track and its own count of how long the note it is
|
||||||
|
; holding lasts, so one can rest while another plays.
|
||||||
|
;
|
||||||
|
; Twelve ticks of 125,000 cycles, which is a sixteenth note at 120 beats a minute:
|
||||||
|
;
|
||||||
|
; ticks 0-3 voice 0 plays middle C voice 1 rests
|
||||||
|
; ticks 4-7 voice 0 rests voice 1 plays the C above it
|
||||||
|
; ticks 8-11 voice 0 plays middle C voice 1 plays the C above it
|
||||||
|
;
|
||||||
|
; BOTH VOICES ARE ASKED FOR NOTE 60. The octave between them is in their patches, which is
|
||||||
|
; M3: a patch belongs to its channel, so four of them can be up at once. Under a device where
|
||||||
|
; it did not, the second patch loaded would own both voices and they would sound in unison.
|
||||||
|
;
|
||||||
|
; So the first two windows have one voice in them and can be measured for pitch, and the third
|
||||||
|
; has both and can be measured for level. Two voices sounding at once cannot be asked their
|
||||||
|
; pitch - a crossing counter given two notes answers with neither.
|
||||||
|
;
|
||||||
|
; THIS IS DELIBERATELY NOT Apps/Play.asm. It shares the design and not the code, and it is
|
||||||
|
; smaller: no track ever ends here, so there is no live flag and no count of what is still
|
||||||
|
; playing. What it has is the part worth pinning - a cursor and a countdown per voice, moved
|
||||||
|
; with LDD and STD.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
; A patch each, in the format SoundPatch writes: a count and that many pairs. They differ in
|
||||||
|
; ONE parameter - the octave - and VoiceStart gives the low one to voice 0 and the high one
|
||||||
|
; to voice 1. If a patch belonged to the device rather than to its channel the second would
|
||||||
|
; overwrite the first and both voices would sound the same.
|
||||||
|
SETD.0 MyPatches
|
||||||
|
SETD.1 PatchTable
|
||||||
|
STD.0.1
|
||||||
|
SETD.0 MySequences
|
||||||
|
SETD.1 SequenceTable
|
||||||
|
STD.0.1
|
||||||
|
SETD.0 MyVoiceStart
|
||||||
|
SETD.1 VoiceStartAt
|
||||||
|
STD.0.1
|
||||||
|
SETD.0 Order0
|
||||||
|
SETD.1 Voice0
|
||||||
|
STD.0.1
|
||||||
|
SETD.0 Order1
|
||||||
|
SETD.1 Voice1
|
||||||
|
STD.0.1
|
||||||
|
SETD.0 Order2
|
||||||
|
SETD.1 Voice2
|
||||||
|
STD.0.1
|
||||||
|
SETD.0 Order3
|
||||||
|
SETD.1 Voice3
|
||||||
|
STD.0.1
|
||||||
|
CALL startVoices
|
||||||
|
|
||||||
|
INIA 0x60
|
||||||
|
OUTA 0x46
|
||||||
|
|
||||||
|
; 125,000 cycles a tick. The period before the control byte, because writing control with
|
||||||
|
; the run bit set is what loads it.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x52
|
||||||
|
INIA 0xE8
|
||||||
|
OUTA 0x53
|
||||||
|
INIA 0x48
|
||||||
|
OUTA 0x54
|
||||||
|
INIA 0x07
|
||||||
|
OUTA 0x51
|
||||||
|
SIF
|
||||||
|
|
||||||
|
; Sixteen ticks. B survives the calls below, because a CALL saves it.
|
||||||
|
INIB 0d16
|
||||||
|
tickLoop:
|
||||||
|
SETD.1 Voice0
|
||||||
|
RSTA
|
||||||
|
CALL stepVoice
|
||||||
|
SETD.1 Voice1
|
||||||
|
INIA 0d1
|
||||||
|
CALL stepVoice
|
||||||
|
DECB
|
||||||
|
BRB lastTick
|
||||||
|
WAIT
|
||||||
|
BRI tickLoop
|
||||||
|
|
||||||
|
lastTick:
|
||||||
|
; The last step started a note and nothing has waited for it yet. Without this the final
|
||||||
|
; tick is never rendered, and a window measuring it runs off the end of the samples.
|
||||||
|
WAIT
|
||||||
|
|
||||||
|
done:
|
||||||
|
CIF
|
||||||
|
RSTA
|
||||||
|
OUTA 0x51
|
||||||
|
HALT
|
||||||
|
|
||||||
|
tick:
|
||||||
|
RETI
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
; Middle C, then out of the way, then back - against the octave above doing the opposite.
|
||||||
|
; ---- The order lists, and what they are for ----
|
||||||
|
;
|
||||||
|
; Four bars of four ticks each, and the SAME TWO SEQUENCES do nearly all of it. Beat is used
|
||||||
|
; twice by voice 0 and twice by voice 1 - four placings of one phrase, written once, and on
|
||||||
|
; two different voices, because the voice is a property of where a sequence is placed and not
|
||||||
|
; of the sequence.
|
||||||
|
;
|
||||||
|
; ticks 0-3 4-7 8-11 12-15
|
||||||
|
; voice 0 Beat Quiet Beat Switch
|
||||||
|
; voice 1 Quiet Beat Beat (its order list ends here)
|
||||||
|
Order0:
|
||||||
|
0d0 0d1 0d0 0d2 0d3 0xFF
|
||||||
|
Order1:
|
||||||
|
0d1 0d0 0d0 0xFF ; And this one ENDS, so the last bar has voice 1 stopped rather
|
||||||
|
; than resting. A player that missed the terminator would read
|
||||||
|
; whatever follows as a sequence index and play it.
|
||||||
|
Order2:
|
||||||
|
0d3 0xFF
|
||||||
|
Order3:
|
||||||
|
0d3 0xFF
|
||||||
|
|
||||||
|
MyPatches:
|
||||||
|
PatchLow PatchHigh
|
||||||
|
|
||||||
|
MySequences:
|
||||||
|
Beat Quiet Switch Tail ; 0 to 3
|
||||||
|
|
||||||
|
MyVoiceStart:
|
||||||
|
0d0 0d1 0d0 0d0
|
||||||
|
|
||||||
|
Beat:
|
||||||
|
0d60 0d4 ; Note 60 on both voices. The octave between them is their patches.
|
||||||
|
0xFF
|
||||||
|
Quiet:
|
||||||
|
0d00 0d4
|
||||||
|
0xFF
|
||||||
|
Switch:
|
||||||
|
0x80 0d1 ; Voice 0 takes patch one, which is voice 1's, mid-piece.
|
||||||
|
0d60 0d4 ; The same note number it has played all along.
|
||||||
|
0xFF
|
||||||
|
Tail:
|
||||||
|
0d00 0xFF ; Long enough that nothing runs off the end of its order list.
|
||||||
|
0xFF
|
||||||
|
|
||||||
|
; Seven pairs each, and only the last of them differs: the octave, centred on 128. So the two
|
||||||
|
; voices below are asked for THE SAME NOTE NUMBER and answer an octave apart, which nothing
|
||||||
|
; but a patch of their own could do.
|
||||||
|
PatchLow:
|
||||||
|
0d8
|
||||||
|
0x00 0d2 ; Saw
|
||||||
|
0x01 0xFF ; at full gain
|
||||||
|
0x05 0d1 ; and switched on.
|
||||||
|
0x20 0d0 ; No attack
|
||||||
|
0x21 0d0 ; no decay
|
||||||
|
0x22 0xFF ; and held at full, so a note lasts exactly its ticks
|
||||||
|
0x23 0d5 ; and goes quickly when the gate drops, which the windows below
|
||||||
|
; depend on: a note still fading into the next voice's stretch is
|
||||||
|
; a second pitch in it, and a crossing counter given two answers
|
||||||
|
; with neither.
|
||||||
|
0x04 0d128 ; Octave, centred.
|
||||||
|
PatchHigh:
|
||||||
|
0d8
|
||||||
|
0x00 0d2
|
||||||
|
0x01 0xFF
|
||||||
|
0x05 0d1
|
||||||
|
0x20 0d0
|
||||||
|
0x21 0d0
|
||||||
|
0x22 0xFF
|
||||||
|
0x23 0d5
|
||||||
|
0x04 0d129 ; And this one an octave up.
|
||||||
|
|
||||||
|
#Include player.asm
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
Boot start
|
||||||
|
Device 0x50 tick
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -25,6 +25,7 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
|
|||||||
| [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
|
| [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
|
||||||
| [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms |
|
| [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms |
|
||||||
| [`Source/Patch`](Source/Patch) | SoundPatch, which turns a soundThing patch into a table the sound device takes |
|
| [`Source/Patch`](Source/Patch) | SoundPatch, which turns a soundThing patch into a table the sound device takes |
|
||||||
|
| [`Source/Tune`](Source/Tune) | TuneC, which turns a written tune into the bytes the music player reads |
|
||||||
| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life, the colours |
|
| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life, the colours |
|
||||||
| [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker |
|
| [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker |
|
||||||
| [`Programs/Sounds`](Programs/Sounds) | Patches as soundThing saved them, and the tables SoundPatch made from them, both checked in so the build never needs soundThing |
|
| [`Programs/Sounds`](Programs/Sounds) | Patches as soundThing saved them, and the tables SoundPatch made from them, both checked in so the build never needs soundThing |
|
||||||
@@ -116,7 +117,7 @@ Manual.
|
|||||||
|
|
||||||
## Getting Started:
|
## Getting Started:
|
||||||
|
|
||||||
Clone it and build the five tools. You need gcc and make, or similar:
|
Clone it and build the six tools. You need gcc and make, or similar:
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git
|
git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git
|
||||||
@@ -306,10 +307,43 @@ this is what turns such a count into a list of routine names.
|
|||||||
|
|
||||||
Without `-o` the output takes the source file's name, in the directory you called the assembler from, with the extension the format asks for: `.bin` for a boot image and `.sbx` for a loadable program. Included files are looked for beside the file that includes them, and then along the directories given with `-I`.
|
Without `-o` the output takes the source file's name, in the directory you called the assembler from, with the extension the format asks for: `.bin` for a boot image and `.sbx` for a loadable program. Included files are looked for beside the file that includes them, and then along the directories given with `-I`.
|
||||||
|
|
||||||
|
## Writing A Tune: TuneC
|
||||||
|
|
||||||
|
```
|
||||||
|
./TuneC [-I <dir>] <tune.score> <output.tune>
|
||||||
|
```
|
||||||
|
|
||||||
|
Four voices on one clock. A tune names its instruments, writes sequences of notes and durations, and gives each voice an order list of sequence names - which is where repetition comes from, since a phrase played four times is written once and named four times.
|
||||||
|
|
||||||
|
```
|
||||||
|
; Four bars, and the bass is the same one under two of them.
|
||||||
|
#Tick 0d125000 ; cycles a tick: a sixteenth note at 120 beats a minute
|
||||||
|
#Patch Oboe oboe.patch
|
||||||
|
#Patch Strings strings.patch
|
||||||
|
#Voice 0d0 Oboe ; which instrument each voice starts on
|
||||||
|
|
||||||
|
#Sequence Verse
|
||||||
|
0d64 0d4 0d67 0d4 0d72 0d8
|
||||||
|
|
||||||
|
#Sequence Ending
|
||||||
|
#Use Strings ; a command, which takes no time at all
|
||||||
|
0d72 0d16
|
||||||
|
|
||||||
|
#Order 0d0
|
||||||
|
Verse Verse Ending
|
||||||
|
```
|
||||||
|
|
||||||
|
`#` is a directive and `;` is a comment, exactly as in SplitBit assembly and in the shell's scripts, and numbers are written the way the assembler writes them - `0d` and `0x`, and no bare numbers. One rule across the machine rather than a third dialect.
|
||||||
|
|
||||||
|
A `.score` is written and a `.tune` is what the machine reads, the same way a `.asm` is written and a `.sbx` is loaded. Patches come from `SoundPatch --blob`, which stays the only thing that understands what a soundThing patch means, and are looked for beside the tune and then on the `-I` path the way an include is.
|
||||||
|
|
||||||
|
**What it refuses is everything the player cannot notice for itself.** The machine has no names, so it cannot say a sequence does not exist; it has no lengths, so it cannot say the voices will come apart four bars after the mistake; and a duration of nought is counted down to 255 and held, which sounds like a hang rather than an error. A voice with a part and no instrument is refused too, because by the time a tune is loaded, "never said" and "instrument nought" are the same byte.
|
||||||
|
|
||||||
## Making A Patch: SoundPatch
|
## Making A Patch: SoundPatch
|
||||||
|
|
||||||
```
|
```
|
||||||
./SoundPatch <patch.json> <label> [output.asm]
|
./SoundPatch <patch.json> <label> [output.asm]
|
||||||
|
./SoundPatch --blob <patch.json> <name> <output.patch>
|
||||||
```
|
```
|
||||||
|
|
||||||
The sound device is soundThing's voice engine with the editor taken off, so a patch designed
|
The sound device is soundThing's voice engine with the editor taken off, so a patch designed
|
||||||
@@ -640,7 +674,7 @@ $(BUILD)/%.bin: %.asm
|
|||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
Builds the five tools, checks they build clean under strict ISO C, and runs nine scripts.
|
Builds the six tools, checks they build clean under strict ISO C, and runs nine scripts.
|
||||||
`Tests/run.sh` assembles and runs every program in `Programs/` and compares the results
|
`Tests/run.sh` assembles and runs every program in `Programs/` and compares the results
|
||||||
against recorded output; six more ask the questions a recorded file cannot answer. Between
|
against recorded output; six more ask the questions a recorded file cannot answer. Between
|
||||||
them they check the two assemblers against each other byte for byte, the two SBFS
|
them they check the two assemblers against each other byte for byte, the two SBFS
|
||||||
@@ -659,7 +693,7 @@ correct.
|
|||||||
make sanitize
|
make sanitize
|
||||||
```
|
```
|
||||||
|
|
||||||
Rebuilds all five tools with the address and undefined behaviour sanitizers and runs the
|
Rebuilds all six tools with the address and undefined behaviour sanitizers and runs the
|
||||||
whole suite under them. It catches reads and writes past the end of an array, use after
|
whole suite under them. It catches reads and writes past the end of an array, use after
|
||||||
free, leaks, and undefined arithmetic, takes about twice as long, and puts the ordinary
|
free, leaks, and undefined arithmetic, takes about twice as long, and puts the ordinary
|
||||||
binaries back when it finishes.
|
binaries back when it finishes.
|
||||||
@@ -676,7 +710,7 @@ in the [SplitBit Test Manual](SplitBit%20Test%20Manual.md).
|
|||||||
|
|
||||||
## Documentation:
|
## Documentation:
|
||||||
|
|
||||||
Four documents, divided by what they are about rather than by who reads them.
|
Five documents, divided by what they are about rather than by who reads them.
|
||||||
|
|
||||||
**[SplitBit Programming Manual](SplitBit%20Programming%20Manual.md)** describes **the machine**: the instruction set, the registers, the vector table, interrupts, devices, the memory controller, the console, storage, and faults. Everything here is true of any SplitBit, whatever is running on it.
|
**[SplitBit Programming Manual](SplitBit%20Programming%20Manual.md)** describes **the machine**: the instruction set, the registers, the vector table, interrupts, devices, the memory controller, the console, storage, and faults. Everything here is true of any SplitBit, whatever is running on it.
|
||||||
|
|
||||||
@@ -684,6 +718,8 @@ Four documents, divided by what they are about rather than by who reads them.
|
|||||||
|
|
||||||
[Programs/CosmOS/README.md](Programs/CosmOS/README.md) describes **the operating system**: its shell, its applications, what a program may ask it for, and the libraries it owns. A different system on the same machine would answer all of that differently, which is why it is documented with the system rather than with the CPU.
|
[Programs/CosmOS/README.md](Programs/CosmOS/README.md) describes **the operating system**: its shell, its applications, what a program may ask it for, and the libraries it owns. A different system on the same machine would answer all of that differently, which is why it is documented with the system rather than with the CPU.
|
||||||
|
|
||||||
|
**[SplitBit Tune Manual](SplitBit%20Tune%20Manual.md)** describes **music**: what a tune is, how to write one, what TuneC refuses and why, and the bytes a tune file holds for anything that would rather write one itself.
|
||||||
|
|
||||||
**[SplitBit Test Manual](SplitBit%20Test%20Manual.md)** describes **the test suite**: what each of its scripts claims, which of those claims are worth the most, how to add to it, and what it is blind to. It is about this repository rather than about the machine, which is why it comes last.
|
**[SplitBit Test Manual](SplitBit%20Test%20Manual.md)** describes **the test suite**: what each of its scripts claims, which of those claims are worth the most, how to add to it, and what it is blind to. It is about this repository rather than about the machine, which is why it comes last.
|
||||||
|
|
||||||
## License:
|
## License:
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -95,15 +95,28 @@ void assemblerCleanup(intermediateElement *intermediateArray, int arraySize, cha
|
|||||||
free(outputFileName);
|
free(outputFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EVERY OPTION HERE NAMES A FILE THE ASSEMBLER WRITES, and the source is the bare argument
|
||||||
|
// at the end. A list of bare flags does not say that: "-S <file>" reads just as naturally as
|
||||||
|
// "dump the symbols of <file>", and asking for it that way hands the source to -S, leaves
|
||||||
|
// nothing positional behind it, and gets answered with "No source file specified" - which
|
||||||
|
// describes the symptom and hides the cause. So the long names are printed, every line says
|
||||||
|
// where the file is written, and an example shows a whole command rather than leaving one to
|
||||||
|
// be assembled out of the parts.
|
||||||
void printUsage(const char *programName) {
|
void printUsage(const char *programName) {
|
||||||
printf("Usage: %s [OPTIONS] <sourcefile>\n", programName);
|
printf("Usage: %s [OPTIONS] <sourcefile>\n", programName);
|
||||||
printf("\n");
|
printf("\n");
|
||||||
|
printf("Assembles one source file. The source is the last argument, on its own; every\n");
|
||||||
|
printf("<file> below is a path the assembler writes, and every <dir> one it searches.\n");
|
||||||
|
printf("\n");
|
||||||
printf("Options:\n");
|
printf("Options:\n");
|
||||||
printf(" -o <file> Write the output to this path instead of alongside the source.\n");
|
printf(" -o, --output <file> Write the assembled output here, instead of alongside the source.\n");
|
||||||
printf(" -I <dir> Look in this directory for included files. May be given more than once.\n");
|
printf(" -I, --include <dir> Search this directory for included files. May be given more than once.\n");
|
||||||
printf(" -M <file> Write the source files this output depends on, as a make rule.\n");
|
printf(" -M, --depend <file> Write a make rule here, naming every source that went into the output.\n");
|
||||||
printf(" -S <file> Write every label and the address it was given, in address order.\n");
|
printf(" -S, --symbols <file> Write every label here with the address it was given, in address order.\n");
|
||||||
printf(" -h, --help Display this help message.\n");
|
printf(" -h, --help Display this help message.\n");
|
||||||
|
printf("\n");
|
||||||
|
printf("Example:\n");
|
||||||
|
printf(" %s -I Libraries -S game.sym -o game.sbx game.asm\n", programName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes a make rule naming every source file that went into the output, so that a
|
// Writes a make rule naming every source file that went into the output, so that a
|
||||||
@@ -207,6 +220,14 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
if (optind >= argc) {
|
if (optind >= argc) {
|
||||||
fprintf(stderr, RED "Error: No source file specified.\n" RESET);
|
fprintf(stderr, RED "Error: No source file specified.\n" RESET);
|
||||||
|
// The commonest way to get here is giving the source to an option that wanted a
|
||||||
|
// path to write, which consumes it and leaves nothing positional. Saying so is the
|
||||||
|
// difference between an error that names the mistake and one that names the hole
|
||||||
|
// the mistake left behind.
|
||||||
|
if (outputFileName || dependencyFileName || symbolFileName) {
|
||||||
|
fprintf(stderr, " -o, -M and -S each name a file to WRITE, not one to read.\n"
|
||||||
|
" The source file is the last argument, on its own.\n");
|
||||||
|
}
|
||||||
printUsage(argv[0]);
|
printUsage(argv[0]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -309,6 +309,15 @@ int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber)
|
|||||||
// Step 3: Handle string literals
|
// Step 3: Handle string literals
|
||||||
if (c == '"') {
|
if (c == '"') {
|
||||||
while ((c = fgetc(file)) != EOF && c != '"') {
|
while ((c = fgetc(file)) != EOF && c != '"') {
|
||||||
|
// A STRING MAY HAVE NEWLINES IN IT, and they are as real as any others. Not
|
||||||
|
// counting them is why every line number after one was short: cosmos.asm has
|
||||||
|
// thirteen of them and its last label was reported thirteen lines early, which
|
||||||
|
// makes an error message point at somebody else's code. Counted here, where
|
||||||
|
// the character is consumed, rather than by scanning the token afterwards -
|
||||||
|
// the same newline must not be counted twice if this loop ever grows a way out.
|
||||||
|
if (c == '\n') {
|
||||||
|
(*lineNumber)++;
|
||||||
|
}
|
||||||
if (i < (int)(sizeof(buffer) - 1)) {
|
if (i < (int)(sizeof(buffer) - 1)) {
|
||||||
buffer[i++] = c;
|
buffer[i++] = c;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+146
-25
@@ -21,25 +21,89 @@ int debugSecondPass = 0;
|
|||||||
Label labelArray[MAX_LABELS];
|
Label labelArray[MAX_LABELS];
|
||||||
int labelCount = 0;
|
int labelCount = 0;
|
||||||
|
|
||||||
|
// Beside the labels rather than beside the code that fills it, because the symbol file
|
||||||
|
// below reads both tables and this one would otherwise not be declared yet.
|
||||||
|
VectorEntry vectorArray[MAX_VECTORS];
|
||||||
|
int vectorArrayCount = 0;
|
||||||
|
|
||||||
// ---- Where everything ended up ----
|
// ---- Where everything ended up ----
|
||||||
//
|
//
|
||||||
// Every label and the address it was given, in address order. The assembler knows this and
|
// Every label with the memory and address it was given and the place it was written, one
|
||||||
// nothing else does: a program on the disk is bytes, and the machine's own monitor can
|
// per line, fields separated by tabs. The assembler knows all of this and nothing else
|
||||||
// disassemble it but has no idea what any of it is called.
|
// does: a program on the disk is bytes, and the machine's own monitor can disassemble it
|
||||||
|
// but has no idea what any of it is called or where it came from.
|
||||||
//
|
//
|
||||||
// WHAT IT IS FOR is telling where a program spends its time. Counting which addresses get
|
// WHAT IT IS FOR is two questions. The first is where a program spends its time - counting
|
||||||
// called says a great deal and names nothing, so the answer arrives as a list of numbers
|
// which addresses get called says a great deal and names nothing, so the answer arrives as
|
||||||
// and somebody has to work out by hand which routine each one is inside. With this, a
|
// a list of numbers and somebody has to work out by hand which routine each one is inside.
|
||||||
// tally of call targets becomes a list of routine names.
|
// The second is where a name lives, which matters more the bigger the program gets: CosmOS
|
||||||
|
// and its libraries define over a thousand names across a dozen files, and finding the one
|
||||||
|
// definition of a routine means grepping for it and reading past every place it is called.
|
||||||
|
// The file and line answer that outright.
|
||||||
//
|
//
|
||||||
// Sorted by address rather than by name, because the question asked of it is always "what
|
// TABS, so that the file is a table every ordinary tool already understands - cut -f3, awk
|
||||||
// is at this address", and a label table is small enough that sorting it is free.
|
// -F'\t', sort -k1,2 - and so that a name never has to be quoted. No header line, for the
|
||||||
static int byAddress(const void *left, const void *right) {
|
// same reason: nothing that reads it should have to know to skip one.
|
||||||
const Label *a = left, *b = right;
|
//
|
||||||
|
// THE KIND IS THE FIRST FIELD BECAUSE THIS IS A HARVARD MACHINE. Program and Data are
|
||||||
|
// separate address spaces, so 0x3000 is two different places and an address alone does not
|
||||||
|
// say which. That is invisible in a loadable program, where the two segments are based
|
||||||
|
// somewhere apart, and immediate in a boot image, where both start at zero and every
|
||||||
|
// address in the file appears twice.
|
||||||
|
//
|
||||||
|
// VECTORS ARE IN HERE TOO, and they are the part nothing else can tell you. A pinned vector
|
||||||
|
// has its number written in the source that pins it, but an automatic one is handed a number
|
||||||
|
// by this assembler and that number appears nowhere at all - not in the source, not in the
|
||||||
|
// binary in any form a reader can find. A vector also costs two hops to follow by hand: the
|
||||||
|
// name in "SWI osFileRead" is not the name of the routine that implements it, so finding the
|
||||||
|
// code means grepping for the vector, reading the handler's name off the Vector Segment, and
|
||||||
|
// grepping again. A vector row gives its number, the handler's name, and the line the two
|
||||||
|
// were tied together on.
|
||||||
|
//
|
||||||
|
// A VECTOR CARRIES BOTH ITS NUMBERS, which is why there is a sixth field. Its number is what
|
||||||
|
// a program writes and what the machine dispatches on; its slot is where the handler's
|
||||||
|
// address is stored in Program Memory, base + number * 2, which is what the loader writes
|
||||||
|
// and what a memory dump shows. Neither can be worked out from the other without knowing
|
||||||
|
// which table it is in, so the file says both.
|
||||||
|
//
|
||||||
|
// THE NUMBER SITS SECOND, beside the kind rather than out at the end, because the two of
|
||||||
|
// them together are what names the thing: "Vector 26" and "Device 32" are single identifiers
|
||||||
|
// in the way "Vector" alone is not. Everything after them - where it lives, what it is
|
||||||
|
// called, where it was written - is something about the thing rather than part of naming it,
|
||||||
|
// so the line reads as an identity followed by its properties. A label has no number and
|
||||||
|
// carries a dash in that column, which is the shape of the answer "this kind is not numbered"
|
||||||
|
// rather than an empty space that could be a missing field.
|
||||||
|
//
|
||||||
|
// Sorted by kind and then address rather than by name, because the question asked of it is
|
||||||
|
// always "what is at this address"; sorting by address alone would interleave separate
|
||||||
|
// address spaces and make the first column flicker between them. The table is small enough
|
||||||
|
// that sorting it is free.
|
||||||
|
|
||||||
|
// The four kinds, in the order they are listed. Program and Data are the two memories, then
|
||||||
|
// the two vector tables.
|
||||||
|
#define ROW_PROGRAM 0
|
||||||
|
#define ROW_DATA 1
|
||||||
|
#define ROW_VECTOR 2
|
||||||
|
#define ROW_DEVICE 3
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int kind;
|
||||||
|
uint16_t address; // Where it lives: a label's address, or a vector's slot.
|
||||||
|
const char *name;
|
||||||
|
const char *fileName;
|
||||||
|
int lineNumber;
|
||||||
|
int number; // The vector or port number, or -1 for a label, which has none.
|
||||||
|
} SymbolRow;
|
||||||
|
|
||||||
|
static int byPlace(const void *left, const void *right) {
|
||||||
|
const SymbolRow *a = left, *b = right;
|
||||||
|
if (a->kind != b->kind) {
|
||||||
|
return a->kind < b->kind ? -1 : 1;
|
||||||
|
}
|
||||||
if (a->address != b->address) {
|
if (a->address != b->address) {
|
||||||
return a->address < b->address ? -1 : 1;
|
return a->address < b->address ? -1 : 1;
|
||||||
}
|
}
|
||||||
return strcmp(a->label, b->label);
|
return strcmp(a->name, b->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeSymbolFile(const char *path) {
|
void writeSymbolFile(const char *path) {
|
||||||
@@ -48,18 +112,61 @@ void writeSymbolFile(const char *path) {
|
|||||||
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, path);
|
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, path);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
Label *sorted = malloc((size_t)labelCount * sizeof(Label));
|
int rowCount = labelCount + vectorArrayCount;
|
||||||
if (!sorted) {
|
SymbolRow *rows = malloc((size_t)rowCount * sizeof(SymbolRow) + 1);
|
||||||
|
if (!rows) {
|
||||||
fprintf(stderr, RED "Error: Out of memory writing the symbol file.\n" RESET);
|
fprintf(stderr, RED "Error: Out of memory writing the symbol file.\n" RESET);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
memcpy(sorted, labelArray, (size_t)labelCount * sizeof(Label));
|
int n = 0;
|
||||||
qsort(sorted, (size_t)labelCount, sizeof(Label), byAddress);
|
|
||||||
for (int i = 0; i < labelCount; i++) {
|
for (int i = 0; i < labelCount; i++) {
|
||||||
fprintf(file, "%04X %s\n", sorted[i].address, sorted[i].label);
|
// A label is put in one of the two segments by populateLabelTable and there is no
|
||||||
|
// third, but the kind is taken from the type rather than assumed, so a label that
|
||||||
|
// somehow arrived as neither is not quietly filed under Data.
|
||||||
|
rows[n].kind = labelArray[i].type == PROGRAM ? ROW_PROGRAM : ROW_DATA;
|
||||||
|
rows[n].address = labelArray[i].address;
|
||||||
|
rows[n].name = labelArray[i].label;
|
||||||
|
rows[n].fileName = labelArray[i].fileName;
|
||||||
|
rows[n].lineNumber = labelArray[i].lineNumber;
|
||||||
|
rows[n].number = -1;
|
||||||
|
n++;
|
||||||
}
|
}
|
||||||
free(sorted);
|
for (int i = 0; i < vectorArrayCount; i++) {
|
||||||
|
int hardware = vectorArray[i].base == HARDWARE_VECTOR_BASE;
|
||||||
|
rows[n].kind = hardware ? ROW_DEVICE : ROW_VECTOR;
|
||||||
|
// The same arithmetic the loader is given, so that what this says a vector's slot is
|
||||||
|
// and what actually gets written there cannot drift apart.
|
||||||
|
rows[n].address = vectorArray[i].base
|
||||||
|
+ (uint16_t)vectorArray[i].index * VECTOR_ENTRY_BYTES;
|
||||||
|
// A device has no name of its own - it is named by the port it is plugged into - so
|
||||||
|
// it is listed under its handler, which is the only name it has.
|
||||||
|
rows[n].name = vectorArray[i].name ? vectorArray[i].name
|
||||||
|
: vectorArray[i].handlerName ? vectorArray[i].handlerName
|
||||||
|
: "?";
|
||||||
|
rows[n].fileName = vectorArray[i].fileName;
|
||||||
|
rows[n].lineNumber = vectorArray[i].lineNumber;
|
||||||
|
rows[n].number = vectorArray[i].index;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
qsort(rows, (size_t)n, sizeof(SymbolRow), byPlace);
|
||||||
|
static const char *kindName[4] = { "Program", "Data", "Vector", "Device" };
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
char number[12]; // Wide enough for any int, which is more than a vector needs.
|
||||||
|
if (rows[i].number < 0) {
|
||||||
|
// A label has no number, and the field says so rather than being left empty: a
|
||||||
|
// run of tabs with nothing between them is the one thing a reader of this file,
|
||||||
|
// human or otherwise, can miscount.
|
||||||
|
snprintf(number, sizeof(number), "-");
|
||||||
|
} else {
|
||||||
|
snprintf(number, sizeof(number), "%d", rows[i].number);
|
||||||
|
}
|
||||||
|
fprintf(file, "%s\t%s\t%04X\t%s\t%s\t%d\n",
|
||||||
|
kindName[rows[i].kind], number, rows[i].address, rows[i].name,
|
||||||
|
rows[i].fileName ? rows[i].fileName : "?",
|
||||||
|
rows[i].lineNumber);
|
||||||
|
}
|
||||||
|
free(rows);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +231,8 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName,
|
|||||||
labelArray[labelCount].label = cleanedLabel;
|
labelArray[labelCount].label = cleanedLabel;
|
||||||
labelArray[labelCount].address = address;
|
labelArray[labelCount].address = address;
|
||||||
labelArray[labelCount].type = type;
|
labelArray[labelCount].type = type;
|
||||||
|
labelArray[labelCount].fileName = fileName;
|
||||||
|
labelArray[labelCount].lineNumber = lineNumber;
|
||||||
if (debugSecondPass) printf("Added label %s with address %04X\n", labelName, labelArray[labelCount].address);
|
if (debugSecondPass) printf("Added label %s with address %04X\n", labelName, labelArray[labelCount].address);
|
||||||
labelCount++;
|
labelCount++;
|
||||||
} else {
|
} else {
|
||||||
@@ -185,9 +294,6 @@ void populateLabelTable(intermediateElement *intermediateArray, int arraySize) {
|
|||||||
|
|
||||||
int findLabelAddress(const char *labelName);
|
int findLabelAddress(const char *labelName);
|
||||||
|
|
||||||
VectorEntry vectorArray[MAX_VECTORS];
|
|
||||||
int vectorArrayCount = 0;
|
|
||||||
|
|
||||||
int vectorCount() {
|
int vectorCount() {
|
||||||
return vectorArrayCount;
|
return vectorArrayCount;
|
||||||
}
|
}
|
||||||
@@ -197,6 +303,9 @@ void freeVectorList() {
|
|||||||
if (vectorArray[i].name) {
|
if (vectorArray[i].name) {
|
||||||
free(vectorArray[i].name);
|
free(vectorArray[i].name);
|
||||||
}
|
}
|
||||||
|
if (vectorArray[i].handlerName) {
|
||||||
|
free(vectorArray[i].handlerName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
vectorArrayCount = 0;
|
vectorArrayCount = 0;
|
||||||
}
|
}
|
||||||
@@ -273,7 +382,8 @@ static int findVector(const char *name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler,
|
static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler,
|
||||||
int declaredOnly, intermediateElement *element) {
|
int declaredOnly, intermediateElement *element,
|
||||||
|
const char *handlerName) {
|
||||||
if (vectorArrayCount >= MAX_VECTORS) {
|
if (vectorArrayCount >= MAX_VECTORS) {
|
||||||
vectorError("Too many vectors defined.", element);
|
vectorError("Too many vectors defined.", element);
|
||||||
}
|
}
|
||||||
@@ -295,6 +405,9 @@ static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler
|
|||||||
vectorArray[vectorArrayCount].base = base;
|
vectorArray[vectorArrayCount].base = base;
|
||||||
vectorArray[vectorArrayCount].handler = handler;
|
vectorArray[vectorArrayCount].handler = handler;
|
||||||
vectorArray[vectorArrayCount].declaredOnly = declaredOnly;
|
vectorArray[vectorArrayCount].declaredOnly = declaredOnly;
|
||||||
|
vectorArray[vectorArrayCount].handlerName = handlerName ? strdup(handlerName) : NULL;
|
||||||
|
vectorArray[vectorArrayCount].fileName = element->fileName;
|
||||||
|
vectorArray[vectorArrayCount].lineNumber = element->lineNumber;
|
||||||
vectorArrayCount++;
|
vectorArrayCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +458,8 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
|
|||||||
int handlerToken = nextVectorToken(intermediateArray, arraySize, portToken + 1);
|
int handlerToken = nextVectorToken(intermediateArray, arraySize, portToken + 1);
|
||||||
uint16_t handler = resolveHandler(intermediateArray, handlerToken, "Device");
|
uint16_t handler = resolveHandler(intermediateArray, handlerToken, "Device");
|
||||||
addVector(NULL, intermediateArray[portToken].byteValue, HARDWARE_VECTOR_BASE,
|
addVector(NULL, intermediateArray[portToken].byteValue, HARDWARE_VECTOR_BASE,
|
||||||
handler, 0, &intermediateArray[i]);
|
handler, 0, &intermediateArray[i],
|
||||||
|
intermediateArray[handlerToken].token);
|
||||||
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -383,6 +497,12 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
|
|||||||
}
|
}
|
||||||
vectorArray[already].handler = resolveHandler(intermediateArray, handlerToken, token);
|
vectorArray[already].handler = resolveHandler(intermediateArray, handlerToken, token);
|
||||||
vectorArray[already].declaredOnly = 0;
|
vectorArray[already].declaredOnly = 0;
|
||||||
|
// The declaration said what it is called; this says where it was implemented,
|
||||||
|
// which is the more useful of the two places to be sent.
|
||||||
|
free(vectorArray[already].handlerName);
|
||||||
|
vectorArray[already].handlerName = strdup(intermediateArray[handlerToken].token);
|
||||||
|
vectorArray[already].fileName = intermediateArray[i].fileName;
|
||||||
|
vectorArray[already].lineNumber = intermediateArray[i].lineNumber;
|
||||||
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -429,12 +549,13 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
|
|||||||
if (!hasHandler) {
|
if (!hasHandler) {
|
||||||
// Nothing follows it on the line, so this says what the vector is called and
|
// Nothing follows it on the line, so this says what the vector is called and
|
||||||
// what number it has, and leaves implementing it to somebody else.
|
// what number it has, and leaves implementing it to somebody else.
|
||||||
addVector(token, index, SOFTWARE_VECTOR_BASE, 0, 1, &intermediateArray[i]);
|
addVector(token, index, SOFTWARE_VECTOR_BASE, 0, 1, &intermediateArray[i], NULL);
|
||||||
i = handlerToken;
|
i = handlerToken;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
uint16_t handler = resolveHandler(intermediateArray, handlerToken, token);
|
uint16_t handler = resolveHandler(intermediateArray, handlerToken, token);
|
||||||
addVector(token, index, SOFTWARE_VECTOR_BASE, handler, 0, &intermediateArray[i]);
|
addVector(token, index, SOFTWARE_VECTOR_BASE, handler, 0, &intermediateArray[i],
|
||||||
|
intermediateArray[handlerToken].token);
|
||||||
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,19 @@ typedef struct {
|
|||||||
char* label;
|
char* label;
|
||||||
uint16_t address;
|
uint16_t address;
|
||||||
int type;
|
int type;
|
||||||
|
// Where the name was written. Only the symbol file reads these, and it is the source
|
||||||
|
// of the whole feature: an address tells you a routine exists and a file and line tell
|
||||||
|
// you where to go and read it.
|
||||||
|
//
|
||||||
|
// NEITHER IS OWNED HERE. fileName points at the copy the include list keeps, the same
|
||||||
|
// storage every intermediateElement points at, and the include list outlives the label
|
||||||
|
// table - assemblerCleanup frees the labels first. So freeLabelList must not free it.
|
||||||
|
const char* fileName;
|
||||||
|
int lineNumber;
|
||||||
} Label;
|
} Label;
|
||||||
|
|
||||||
// Every label and the address it was given, in address order, so that a tally of
|
// Every label and every vector, with where it lives and where it was written, so that a
|
||||||
// addresses can be turned back into a list of routine names.
|
// tally of addresses can be turned back into a list of routine names.
|
||||||
void writeSymbolFile(const char *path);
|
void writeSymbolFile(const char *path);
|
||||||
|
|
||||||
// One line of the Vector Segment, once it has been worked out.
|
// One line of the Vector Segment, once it has been worked out.
|
||||||
@@ -40,6 +49,12 @@ typedef struct {
|
|||||||
uint16_t base; // Which table: software or hardware.
|
uint16_t base; // Which table: software or hardware.
|
||||||
uint16_t handler; // Where the handler ended up.
|
uint16_t handler; // Where the handler ended up.
|
||||||
int declaredOnly; // Named and numbered, with nobody implementing it here.
|
int declaredOnly; // Named and numbered, with nobody implementing it here.
|
||||||
|
// For the symbol file, which is the only thing that reads these. A device has no name
|
||||||
|
// of its own, so the handler's is the only name it can be listed under. fileName is
|
||||||
|
// borrowed from the include list the way a Label's is; handlerName is owned, like name.
|
||||||
|
char* handlerName;
|
||||||
|
const char* fileName;
|
||||||
|
int lineNumber;
|
||||||
} VectorEntry;
|
} VectorEntry;
|
||||||
|
|
||||||
void freeLabelList();
|
void freeLabelList();
|
||||||
|
|||||||
@@ -433,6 +433,56 @@ static uint16_t countFree(const Directory *directory, const Superblock *super) {
|
|||||||
return (uint16_t)(super->diskBlocks - overhead - used);
|
return (uint16_t)(super->diskBlocks - overhead - used);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- The longest run of free blocks there is ----
|
||||||
|
//
|
||||||
|
// The number that says whether a file will fit. Files are laid down contiguously - first fit,
|
||||||
|
// with the directory itself as the map - so a disk with a thousand blocks free in ten
|
||||||
|
// scattered pieces refuses a file of two hundred, and the free total gives no hint of it.
|
||||||
|
//
|
||||||
|
// A sweep rather than a sort: a candidate walks the disk, each pass looking for the used
|
||||||
|
// extent nearest at or after it, and anything the candidate is standing inside pushes it to
|
||||||
|
// the far end and starts the pass again. That is what sbfs.asm does, because that machine has
|
||||||
|
// no sort - and the two implementations of this format are worth more agreeing than they are
|
||||||
|
// each being clever separately.
|
||||||
|
static uint16_t largestRun(const Directory *directory, const Superblock *super) {
|
||||||
|
const uint32_t firstData = (uint32_t)super->directoryStart + super->directoryBlocks;
|
||||||
|
uint32_t candidate = firstData;
|
||||||
|
uint32_t biggest = 0;
|
||||||
|
for (;;) {
|
||||||
|
uint32_t next = super->diskBlocks;
|
||||||
|
int moved = 0;
|
||||||
|
for (int i = 0; i < directory->entries; i++) {
|
||||||
|
const uint8_t *entry = entryAt(directory, i);
|
||||||
|
if (!entryInUse(entry) || entryIsDirectory(entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint32_t start = readWord(entry + SBFS_ENTRY_START);
|
||||||
|
const uint32_t end = start + entryBlocksUsed(entry);
|
||||||
|
if (end <= candidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (start <= candidate) {
|
||||||
|
candidate = end;
|
||||||
|
moved = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (start < next) {
|
||||||
|
next = start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (moved) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (next - candidate > biggest) {
|
||||||
|
biggest = next - candidate;
|
||||||
|
}
|
||||||
|
if (next >= super->diskBlocks) {
|
||||||
|
return (uint16_t)biggest;
|
||||||
|
}
|
||||||
|
candidate = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Commands ----
|
// ---- Commands ----
|
||||||
|
|
||||||
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks,
|
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks,
|
||||||
@@ -647,6 +697,15 @@ static int commandList(const char *path, const char *within) {
|
|||||||
}
|
}
|
||||||
printf(".\n");
|
printf(".\n");
|
||||||
|
|
||||||
|
// Said only when it is not all of it. On a disk that has only been appended to the two
|
||||||
|
// are the same number, and printing it twice would be noise on every healthy disk - a
|
||||||
|
// line that appears only when something is wrong is a line somebody reads.
|
||||||
|
uint16_t run = largestRun(&directory, &super);
|
||||||
|
if (run != counted) {
|
||||||
|
printf("The longest run of free blocks is %u, so nothing bigger than that will"
|
||||||
|
" fit.\n", run);
|
||||||
|
}
|
||||||
|
|
||||||
// A save that stopped between deleting the old entry and naming the new one. The
|
// A save that stopped between deleting the old entry and naming the new one. The
|
||||||
// bytes are all there under the temporary's name and one rename brings them back,
|
// bytes are all there under the temporary's name and one rename brings them back,
|
||||||
// which is the whole of the recovery this format offers - so the thing that matters
|
// which is the whole of the recovery this format offers - so the thing that matters
|
||||||
|
|||||||
+207
-1
@@ -61,6 +61,51 @@ static void consoleReleaseTerminal(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- And giving back the colour along with the terminal ----
|
||||||
|
//
|
||||||
|
// A machine that stops with a scheme set would leave the shell that started it tinted, which
|
||||||
|
// is the same class of failure as leaving it with no echo: a state this program chose,
|
||||||
|
// outliving the program, with nothing to connect it back here.
|
||||||
|
//
|
||||||
|
// ---- Two of them, because the two endings have different rules ----
|
||||||
|
//
|
||||||
|
// A machine STOPPING ON PURPOSE has a buffer of output still in stdio, and atexit handlers
|
||||||
|
// run BEFORE that buffer is flushed - so a reset written straight to the file descriptor
|
||||||
|
// would arrive in front of the text it is meant to follow. That one goes through the same
|
||||||
|
// buffer as everything else and comes out in order.
|
||||||
|
//
|
||||||
|
// A machine DYING ON A SIGNAL may be inside stdio at the moment the signal arrived, so it
|
||||||
|
// writes the four bytes directly and accepts that whatever was buffered may be lost. The
|
||||||
|
// process is ending either way, and a terminal left tinted is the failure being prevented.
|
||||||
|
//
|
||||||
|
// A TERMINAL IS ASSUMED TO START PLAIN, which is why this begins at nought rather than at
|
||||||
|
// "nothing said yet". A machine that never colours anything then says nothing at all, and
|
||||||
|
// the alternative was every session opening with a reset nobody asked for - CosmOS sets the
|
||||||
|
// scheme to nought on its way to the prompt, so the first character would have announced it.
|
||||||
|
// If the assumption is ever wrong, the first colour puts it right: every sequence begins
|
||||||
|
// with a reset.
|
||||||
|
//
|
||||||
|
// Setting it back to nought here is what makes a machine that carries on - resumed after a
|
||||||
|
// stop - say its colour again on the next character rather than believing it already has.
|
||||||
|
static uint8_t attributeTold = 0;
|
||||||
|
|
||||||
|
static void consolePlainAgain(void) {
|
||||||
|
if (attributeTold == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fputs("\033[0m", stdout);
|
||||||
|
attributeTold = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void consolePlainAgainDying(void) {
|
||||||
|
if (attributeTold == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ssize_t wrote = write(STDOUT_FILENO, "\033[0m", 4);
|
||||||
|
(void)wrote;
|
||||||
|
attributeTold = 0;
|
||||||
|
}
|
||||||
|
|
||||||
static void consoleTakeTerminal(void) {
|
static void consoleTakeTerminal(void) {
|
||||||
if (consoleTerminalRaw || !consoleTerminalSaved) {
|
if (consoleTerminalRaw || !consoleTerminalSaved) {
|
||||||
return;
|
return;
|
||||||
@@ -105,6 +150,7 @@ void consoleRestore(void) {
|
|||||||
// is waiting sees the signal it expected rather than a machine that exited quietly.
|
// is waiting sees the signal it expected rather than a machine that exited quietly.
|
||||||
static void consoleFatalSignal(int signalNumber) {
|
static void consoleFatalSignal(int signalNumber) {
|
||||||
consoleReleaseTerminal();
|
consoleReleaseTerminal();
|
||||||
|
consolePlainAgainDying();
|
||||||
signal(signalNumber, SIG_DFL);
|
signal(signalNumber, SIG_DFL);
|
||||||
raise(signalNumber);
|
raise(signalNumber);
|
||||||
}
|
}
|
||||||
@@ -116,6 +162,7 @@ static void consoleContinueSignal(int signalNumber);
|
|||||||
// entitled to have its keys again when it is resumed.
|
// entitled to have its keys again when it is resumed.
|
||||||
static void consoleStopSignal(int signalNumber) {
|
static void consoleStopSignal(int signalNumber) {
|
||||||
consoleReleaseTerminal();
|
consoleReleaseTerminal();
|
||||||
|
consolePlainAgainDying();
|
||||||
signal(SIGCONT, consoleContinueSignal);
|
signal(SIGCONT, consoleContinueSignal);
|
||||||
signal(signalNumber, SIG_DFL);
|
signal(signalNumber, SIG_DFL);
|
||||||
raise(signalNumber);
|
raise(signalNumber);
|
||||||
@@ -302,6 +349,65 @@ static void consoleSayCursor(void) {
|
|||||||
consoleTellTerminal(sequence);
|
consoleTellTerminal(sequence);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Sixteen schemes, and what a terminal can make of them ----
|
||||||
|
//
|
||||||
|
// The attribute's low nibble picks one of sixteen ink and paper pairs. Banks 0 to 7 are ink
|
||||||
|
// on black; banks 8 to 15 are those same colours as PAPER with black ink, which is the
|
||||||
|
// machine's one-bit highlight - attribute XOR 8 turns any of them inside out. ANSI says both
|
||||||
|
// of those, so what is needed here is a table rather than a translation.
|
||||||
|
//
|
||||||
|
// AND THE ORDER WAS ALREADY RIGHT, which is worth saying because it looks like a borrowing
|
||||||
|
// and is not. Both sets are an enumeration of a three bit colour, red green blue counted in
|
||||||
|
// binary: one is red in both, three is yellow in both, six is cyan in both. The same
|
||||||
|
// arithmetic was done twice, forty years apart.
|
||||||
|
//
|
||||||
|
// The one place they differ is slot 0, and that difference is forced: this screen is ink on
|
||||||
|
// black, so ink cannot be black, and slot 0 is grey where ANSI's is black.
|
||||||
|
//
|
||||||
|
// EVERY SEQUENCE BEGINS WITH A RESET. Setting only a foreground would leave the paper from
|
||||||
|
// whatever bank came before it, so going from bank 8 to bank 1 would have written red on
|
||||||
|
// grey. One extra byte, and the sequences stop depending on each other.
|
||||||
|
//
|
||||||
|
// Scheme 0 is sent as a bare reset rather than as grey on black, because plain text on
|
||||||
|
// somebody's terminal means THEIR plain text. Forcing grey on black would fight a terminal
|
||||||
|
// set up the other way round, which is the kind of thing this console exists not to do.
|
||||||
|
static const char *const schemeSequences[16] = {
|
||||||
|
"\033[0m",
|
||||||
|
"\033[0;31m", "\033[0;32m", "\033[0;33m",
|
||||||
|
"\033[0;34m", "\033[0;35m", "\033[0;36m",
|
||||||
|
"\033[0;97m",
|
||||||
|
"\033[0;47;30m",
|
||||||
|
"\033[0;41;30m", "\033[0;42;30m", "\033[0;43;30m",
|
||||||
|
"\033[0;44;30m", "\033[0;45;30m", "\033[0;46;30m",
|
||||||
|
"\033[0;107;30m",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Told once, and only when it matters ----
|
||||||
|
//
|
||||||
|
// The same bargain the cursor gets, for the same reason: a register write only marks it and
|
||||||
|
// the next character sends it. A program that sets a scheme and prints nothing has said
|
||||||
|
// nothing, and one that sets the same scheme twice costs one sequence rather than two.
|
||||||
|
//
|
||||||
|
// Only the SCHEME nibble is looked at. The page bits say which tiles a cell draws from,
|
||||||
|
// which is a fact about the screen's own art and means nothing to a terminal.
|
||||||
|
static void consoleSayAttribute(void) {
|
||||||
|
const uint8_t scheme = consoleAttribute & VIDEO_ATTRIBUTE_SCHEME;
|
||||||
|
if (scheme == attributeTold) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Installed on the first colour rather than at startup, so a run that never asks for one
|
||||||
|
// registers nothing at all - the same bargain the terminal guards make.
|
||||||
|
if (scheme != 0) {
|
||||||
|
static int plainGuardInstalled = 0;
|
||||||
|
if (!plainGuardInstalled) {
|
||||||
|
plainGuardInstalled = 1;
|
||||||
|
atexit(consolePlainAgain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attributeTold = scheme;
|
||||||
|
consoleTellTerminal(schemeSequences[scheme]);
|
||||||
|
}
|
||||||
|
|
||||||
static void consoleDraw(uint8_t byte) {
|
static void consoleDraw(uint8_t byte) {
|
||||||
// ---- Nowhere to put a glyph ----
|
// ---- Nowhere to put a glyph ----
|
||||||
//
|
//
|
||||||
@@ -332,6 +438,31 @@ static void consoleDraw(uint8_t byte) {
|
|||||||
consoleCursorMoved();
|
consoleCursorMoved();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case '\t': {
|
||||||
|
// ---- A tab moves the cursor and marks nothing ----
|
||||||
|
//
|
||||||
|
// The byte reaches standard output either way, so anything reading the serial
|
||||||
|
// line has always seen tabs and a host terminal has always laid them out. The
|
||||||
|
// screen dropped them, which is why a tab separated file - an assembler symbol
|
||||||
|
// table, say - came out of Type or More as its fields run together.
|
||||||
|
//
|
||||||
|
// MOVING RATHER THAN WRITING SPACES is what a terminal does, and the difference
|
||||||
|
// shows the moment anything is already on the line: a tab that wrote blanks
|
||||||
|
// would rub out what it passed over.
|
||||||
|
//
|
||||||
|
// Eight columns is the conventional stop and the one everything that prints a
|
||||||
|
// tab assumes. Reaching or passing the right edge wraps, which is what an
|
||||||
|
// ordinary character does at the edge - the alternative, stopping in the last
|
||||||
|
// column, invents a rule that only tabs obey.
|
||||||
|
int stop = (cursorColumn / CONSOLE_TAB_WIDTH + 1) * CONSOLE_TAB_WIDTH;
|
||||||
|
if (stop >= videoColumns()) {
|
||||||
|
consoleNewLine();
|
||||||
|
} else {
|
||||||
|
cursorColumn = (uint8_t)stop;
|
||||||
|
}
|
||||||
|
consoleCursorMoved();
|
||||||
|
}
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -361,6 +492,12 @@ static int (*inputHook)(int mayWait) = NULL;
|
|||||||
// cursor blinking at somebody who is thinking about what to type.
|
// cursor blinking at somebody who is thinking about what to type.
|
||||||
#define CONSOLE_WAIT_CYCLES 16667
|
#define CONSOLE_WAIT_CYCLES 16667
|
||||||
|
|
||||||
|
// The same span in milliseconds, for waiting on a terminal rather than on a window. The two
|
||||||
|
// have to mean the same length of time: one is what the devices are told went by, the other
|
||||||
|
// is how long was really spent, and a machine whose clock ran at a different rate from the
|
||||||
|
// wall while it was waiting would be a machine whose music slowed down when nobody typed.
|
||||||
|
#define CONSOLE_WAIT_MS 17
|
||||||
|
|
||||||
static unsigned long idleCycles = 0;
|
static unsigned long idleCycles = 0;
|
||||||
|
|
||||||
// The machine's clock as the devices last heard it. Kept here rather than passed about,
|
// The machine's clock as the devices last heard it. Kept here rather than passed about,
|
||||||
@@ -462,6 +599,9 @@ void consoleSetInputHook(int (*hook)(int mayWait)) {
|
|||||||
// Answers a byte, CONSOLE_NOTHING_YET when nothing is waiting and it was told not to wait,
|
// Answers a byte, CONSOLE_NOTHING_YET when nothing is waiting and it was told not to wait,
|
||||||
// or CONSOLE_GONE at the end of input. Deliberately the same three answers a front end's
|
// or CONSOLE_GONE at the end of input. Deliberately the same three answers a front end's
|
||||||
// hook gives, so that everything above this treats a terminal and a window alike.
|
// hook gives, so that everything above this treats a terminal and a window alike.
|
||||||
|
// Whether standard input is really a terminal, defined below and wanted here.
|
||||||
|
static int consoleTranslatingKeys(void);
|
||||||
|
|
||||||
static int consoleFromInput(int mayWait) {
|
static int consoleFromInput(int mayWait) {
|
||||||
if (!mayWait) {
|
if (!mayWait) {
|
||||||
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
|
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
|
||||||
@@ -471,6 +611,37 @@ static int consoleFromInput(int mayWait) {
|
|||||||
}
|
}
|
||||||
unsigned char byte;
|
unsigned char byte;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
// ---- WAITING FOR A KEY IS NOT THE SAME AS TIME STOPPING ----
|
||||||
|
//
|
||||||
|
// This used to block in read, and while it was blocked nothing told the devices that
|
||||||
|
// any time had gone by - so the whole machine stood still. A window has never done
|
||||||
|
// that: consoleGatherLine and the key mode loop both spend a frame and then say so,
|
||||||
|
// and the comment on consoleWaited gives the reason. A display controller blinking a
|
||||||
|
// cursor does not stop because the processor is waiting on a key, and neither does a
|
||||||
|
// disk finishing a read, or a synthesiser finishing a note.
|
||||||
|
//
|
||||||
|
// A note left in its release was the way this showed. Frozen mid-decay, it came out
|
||||||
|
// a snippet at a time, one per keystroke, because a keystroke was the only thing that
|
||||||
|
// let the machine run. Four seconds of sitting at the prompt bought no cycles at all.
|
||||||
|
//
|
||||||
|
// ONLY WHEN THERE IS REALLY A TERMINAL, which is the same rule the escape sequences
|
||||||
|
// and the erase character follow. A file or a pipe must keep blocking: emulated time
|
||||||
|
// would otherwise depend on how fast the other end of the pipe was writing, and every
|
||||||
|
// recorded test on this machine is reproducible because it does not.
|
||||||
|
if (consoleTranslatingKeys()) {
|
||||||
|
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
|
||||||
|
const int ready = poll(&waiting, 1, CONSOLE_WAIT_MS);
|
||||||
|
if (ready == 0) {
|
||||||
|
consoleWaited();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ready < 0) {
|
||||||
|
if (errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return CONSOLE_GONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
const ssize_t got = read(STDIN_FILENO, &byte, 1);
|
const ssize_t got = read(STDIN_FILENO, &byte, 1);
|
||||||
if (got == 1) {
|
if (got == 1) {
|
||||||
return byte;
|
return byte;
|
||||||
@@ -585,14 +756,48 @@ static int consoleKeyFromEscape(void) {
|
|||||||
|
|
||||||
// One key from standard input: a byte as it arrived, or one of the console's own key values
|
// One key from standard input: a byte as it arrived, or one of the console's own key values
|
||||||
// where a terminal sent a sequence meaning one.
|
// where a terminal sent a sequence meaning one.
|
||||||
|
// ---- The terminal's erase key is not the one this machine knows ----
|
||||||
|
//
|
||||||
|
// SplitBit's Backspace is 0x08. That is what Voyager's keyboard sends and what CosmOS's line
|
||||||
|
// editor looks for. A POSIX terminal sends whatever ITS erase character is, and on most of
|
||||||
|
// them that is 0x7F.
|
||||||
|
//
|
||||||
|
// It went unnoticed for as long as the terminal was doing the editing: in canonical mode the
|
||||||
|
// tty eats its own erase character and hands over a finished line. Key mode turns ICANON off,
|
||||||
|
// which is the point of it, and from then on the byte arrives raw and means nothing to the
|
||||||
|
// editor. So Backspace worked in Voyager and stopped working in the console-only emulator on
|
||||||
|
// the day the shell started editing the line itself.
|
||||||
|
//
|
||||||
|
// VERASE IS ASKED RATHER THAN 0x7F ASSUMED, because the terminal is what knows: it was saved
|
||||||
|
// on the way into raw mode, some terminals really are set to 0x08, and a person who has moved
|
||||||
|
// their erase key somewhere else has said where it is.
|
||||||
|
//
|
||||||
|
// ONLY WHEN STANDARD INPUT IS A TERMINAL. A file or a pipe holding 0x7F holds a byte that
|
||||||
|
// somebody wrote, not a key somebody pressed, and rewriting it would corrupt input that has
|
||||||
|
// nothing to do with terminals. consoleTerminalSaved is exactly that question: it is only
|
||||||
|
// ever set after an isatty succeeded.
|
||||||
|
static int consoleEraseNormalized(int got) {
|
||||||
|
if (!consoleTerminalSaved || got < 0) {
|
||||||
|
return got;
|
||||||
|
}
|
||||||
|
const cc_t erase = consoleSavedTerminal.c_cc[VERASE];
|
||||||
|
// A terminal with no erase key at all, which says so this way, has nothing to translate.
|
||||||
|
if (erase == _POSIX_VDISABLE) {
|
||||||
|
return got;
|
||||||
|
}
|
||||||
|
return got == (int)erase ? 0x08 : got;
|
||||||
|
}
|
||||||
|
|
||||||
static int consoleKeyFromInput(int mayWait) {
|
static int consoleKeyFromInput(int mayWait) {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int got;
|
int got;
|
||||||
if (consoleHeldByte >= 0) {
|
if (consoleHeldByte >= 0) {
|
||||||
|
// Already normalized on the way in, and doing it twice would be wrong the day
|
||||||
|
// 0x08 is somebody's erase character: it is its own answer.
|
||||||
got = consoleHeldByte;
|
got = consoleHeldByte;
|
||||||
consoleHeldByte = -1;
|
consoleHeldByte = -1;
|
||||||
} else {
|
} else {
|
||||||
got = consoleFromInput(mayWait);
|
got = consoleEraseNormalized(consoleFromInput(mayWait));
|
||||||
}
|
}
|
||||||
if (got < 0) {
|
if (got < 0) {
|
||||||
return got;
|
return got;
|
||||||
@@ -1452,6 +1657,7 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
|
|||||||
// since it was moved. Here rather than at the move, so setting a row and a
|
// since it was moved. Here rather than at the move, so setting a row and a
|
||||||
// column costs one sequence rather than two.
|
// column costs one sequence rather than two.
|
||||||
consoleSayCursor();
|
consoleSayCursor();
|
||||||
|
consoleSayAttribute();
|
||||||
consoleDraw(DataByte);
|
consoleDraw(DataByte);
|
||||||
putchar(DataByte);
|
putchar(DataByte);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -227,6 +227,12 @@
|
|||||||
// like afterwards and what was typed before are the system's business - see the shell,
|
// like afterwards and what was typed before are the system's business - see the shell,
|
||||||
// which edits its own line - exactly as what is on a disk is the system's business and what
|
// which edits its own line - exactly as what is on a disk is the system's business and what
|
||||||
// a drive IS belongs to the machine.
|
// a drive IS belongs to the machine.
|
||||||
|
// How far apart the console's tab stops are, on the screen. The serial line is not this
|
||||||
|
// device's business - a host terminal lays a tab out with whatever it was configured for -
|
||||||
|
// but the character screen has to be told, and eight is what everything that prints a tab
|
||||||
|
// expects.
|
||||||
|
#define CONSOLE_TAB_WIDTH 8
|
||||||
|
|
||||||
#define CONSOLE_KEY_UP 0x80
|
#define CONSOLE_KEY_UP 0x80
|
||||||
#define CONSOLE_KEY_DOWN 0x81
|
#define CONSOLE_KEY_DOWN 0x81
|
||||||
#define CONSOLE_KEY_LEFT 0x82
|
#define CONSOLE_KEY_LEFT 0x82
|
||||||
|
|||||||
+12
-4
@@ -237,10 +237,18 @@ static void soundParameter(uint8_t value) {
|
|||||||
v->gate = value ? VOICE_TRIGGER : VOICE_GATE;
|
v->gate = value ? VOICE_TRIGGER : VOICE_GATE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// The LFOs belong to the device rather than to a channel, so whichever channel is
|
// ---- The LFOs belong to the channel ----
|
||||||
// selected makes no difference to these.
|
//
|
||||||
case SP_LFO0: setLfo(&synth.lfos[0], which, value); break;
|
// They used to belong to the device, so whichever channel was selected made no
|
||||||
case SP_LFO1: setLfo(&synth.lfos[1], which, value); break;
|
// difference: two channels playing two different sounds shared one pair of LFOs, and
|
||||||
|
// whichever loaded its settings last owned them for both. A sound with its LFO
|
||||||
|
// switched off silenced the trill under a sound that was still playing.
|
||||||
|
//
|
||||||
|
// Four channels are four independent things, which is the rule the rest of this
|
||||||
|
// device already followed. So these read the selected channel like every parameter
|
||||||
|
// above them, and an LFO written to one channel is inaudible on the other three.
|
||||||
|
case SP_LFO0: setLfo(&v->lfos[0], which, value); break;
|
||||||
|
case SP_LFO1: setLfo(&v->lfos[1], which, value); break;
|
||||||
default:
|
default:
|
||||||
// A parameter number nothing answers to does nothing. A sound device is a poor
|
// A parameter number nothing answers to does nothing. A sound device is a poor
|
||||||
// place to stop the machine, the same as a screen.
|
// place to stop the machine, the same as a screen.
|
||||||
|
|||||||
+76
-38
@@ -10,11 +10,11 @@
|
|||||||
//
|
//
|
||||||
// It is copied rather than submoduled. Two files against tying this build to another
|
// It is copied rather than submoduled. Two files against tying this build to another
|
||||||
// repository's history is not a close call, and what a copy costs is that changes have to be
|
// repository's history is not a close call, and what a copy costs is that changes have to be
|
||||||
// carried across on purpose - in BOTH directions, which has now happened once each way.
|
// carried across on purpose - in BOTH directions, which has now happened twice each way.
|
||||||
//
|
//
|
||||||
// ---- What was changed ----
|
// ---- What was changed ----
|
||||||
//
|
//
|
||||||
// Nothing. This is soundThing's engine at b73e5c0, character for character, except that
|
// Nothing. This is soundThing's engine at 71e3cb2, character for character, except that
|
||||||
// em-dashes and arrows in its comments are written as ASCII here because this tree is ASCII
|
// em-dashes and arrows in its comments are written as ASCII here because this tree is ASCII
|
||||||
// only. That rule is local and is not an improvement, so it was not sent upstream.
|
// only. That rule is local and is not an improvement, so it was not sent upstream.
|
||||||
//
|
//
|
||||||
@@ -24,6 +24,13 @@
|
|||||||
// waiting for a key, and a triggered voice that re-arms its oscillators so a one-shot is the
|
// waiting for a key, and a triggered voice that re-arms its oscillators so a one-shot is the
|
||||||
// same one-shot twice. A game is nearly all one-shots, which is why the traffic went that way.
|
// same one-shot twice. A game is nearly all one-shots, which is why the traffic went that way.
|
||||||
//
|
//
|
||||||
|
// The second round trip was the LFOs, which belonged to the Synth and so were shared by every
|
||||||
|
// voice at once. That is right for one instrument played polyphonically and wrong for four
|
||||||
|
// channels playing four different things: whichever patch loaded last owned the LFO for all of
|
||||||
|
// them. They now live in the Voice. It went up from here and came back with a bug fixed and
|
||||||
|
// one hunk that does nothing here - synthSyncVoices carrying the free cycle down, which only
|
||||||
|
// matters to a caller that syncs, and this device never does.
|
||||||
|
//
|
||||||
// So the thing to keep current is no longer a list. It is this: if either copy changes, the
|
// So the thing to keep current is no longer a list. It is this: if either copy changes, the
|
||||||
// other one has to be told.
|
// other one has to be told.
|
||||||
//
|
//
|
||||||
@@ -84,8 +91,18 @@ void synthInit(Synth *s, float sampleRate)
|
|||||||
// it is now said rather than assumed, and can be said differently.
|
// it is now said rather than assumed, and can be said differently.
|
||||||
s->voices[i].levelSource = MOD_SOURCE_AMP_ENV;
|
s->voices[i].levelSource = MOD_SOURCE_AMP_ENV;
|
||||||
s->voices[i].gate = VOICE_GATE;
|
s->voices[i].gate = VOICE_GATE;
|
||||||
for (int l = 0; l < LFO_COUNT; l++)
|
// Every voice is seeded from lfoTriggerSeed(l), which depends on the LFO and NOT on
|
||||||
lfoStateInit(&s->voices[i].lfoRun[l], lfoTriggerSeed(l));
|
// the voice: identical seeds, identical rates and identical arithmetic on every
|
||||||
|
// sample is what keeps the free cycles of a chord in step rather than merely near.
|
||||||
|
for (int l = 0; l < LFO_COUNT; l++) {
|
||||||
|
lfoStateInit(&s->voices[i].lfos[l].run, lfoTriggerSeed(l));
|
||||||
|
s->voices[i].lfos[l].rate = 1.0f;
|
||||||
|
s->voices[i].lfos[l].waveform = WAVE_SINE;
|
||||||
|
s->voices[i].lfos[l].active = 0;
|
||||||
|
// Free unless a patch says otherwise, which is what every patch that exists was
|
||||||
|
// made against.
|
||||||
|
s->voices[i].lfos[l].mode = LFO_FREE;
|
||||||
|
}
|
||||||
s->voices[i].oscillators[0].noiseState = 0x9E3779B9u + (uint32_t)i * 2654435761u;
|
s->voices[i].oscillators[0].noiseState = 0x9E3779B9u + (uint32_t)i * 2654435761u;
|
||||||
s->voices[i].oscillators[1].noiseState = 0x7F4A7C15u + (uint32_t)i * 2246822519u;
|
s->voices[i].oscillators[1].noiseState = 0x7F4A7C15u + (uint32_t)i * 2246822519u;
|
||||||
|
|
||||||
@@ -113,16 +130,6 @@ void synthInit(Synth *s, float sampleRate)
|
|||||||
s->voices[i].filter.resModDepth = 0.0f;
|
s->voices[i].filter.resModDepth = 0.0f;
|
||||||
}
|
}
|
||||||
s->voices[0].oscillators[0].active = 1;
|
s->voices[0].oscillators[0].active = 1;
|
||||||
|
|
||||||
for (int l = 0; l < LFO_COUNT; l++) {
|
|
||||||
lfoStateInit(&s->lfos[l].run, lfoTriggerSeed(l));
|
|
||||||
s->lfos[l].rate = 1.0f;
|
|
||||||
s->lfos[l].waveform = WAVE_SINE;
|
|
||||||
s->lfos[l].active = 0;
|
|
||||||
// Free unless a patch says otherwise, which is what every patch that exists was
|
|
||||||
// made against.
|
|
||||||
s->lfos[l].mode = LFO_FREE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void synthResetPatch(Synth *s)
|
void synthResetPatch(Synth *s)
|
||||||
@@ -167,15 +174,23 @@ void synthResetPatch(Synth *s)
|
|||||||
v->filter.resModDepth = 0.0f;
|
v->filter.resModDepth = 0.0f;
|
||||||
|
|
||||||
for (int l = 0; l < LFO_COUNT; l++) {
|
for (int l = 0; l < LFO_COUNT; l++) {
|
||||||
lfoStateInit(&s->lfos[l].run, lfoTriggerSeed(l));
|
v->lfos[l].rate = 1.0f;
|
||||||
s->lfos[l].rate = 1.0f;
|
v->lfos[l].waveform = WAVE_SINE;
|
||||||
s->lfos[l].waveform = WAVE_SINE;
|
v->lfos[l].active = 0;
|
||||||
s->lfos[l].active = 0;
|
|
||||||
// Free unless a patch says otherwise, which is what every patch that exists was
|
// Free unless a patch says otherwise, which is what every patch that exists was
|
||||||
// made against.
|
// made against.
|
||||||
s->lfos[l].mode = LFO_FREE;
|
v->lfos[l].mode = LFO_FREE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The rest of this function sets voice 0 and lets synthSyncVoices carry it down. The
|
||||||
|
// cycles are the exception, because sync deliberately does not touch them: restarting
|
||||||
|
// only voice 0's would leave it a fraction of a turn from every other voice, and a free
|
||||||
|
// LFO under a chord would stop being one sweep. So they all restart together, which is
|
||||||
|
// what the single cycle this replaced did.
|
||||||
|
for (int i = 0; i < VOICE_COUNT; i++)
|
||||||
|
for (int l = 0; l < LFO_COUNT; l++)
|
||||||
|
lfoStateInit(&s->voices[i].lfos[l].run, lfoTriggerSeed(l));
|
||||||
|
|
||||||
s->volume = 0.8f;
|
s->volume = 0.8f;
|
||||||
s->pitchBendRange = 2.0f;
|
s->pitchBendRange = 2.0f;
|
||||||
}
|
}
|
||||||
@@ -377,8 +392,8 @@ const char *waveformName(Waveform w)
|
|||||||
// patch that exists was made against the old behaviour.
|
// patch that exists was made against the old behaviour.
|
||||||
//
|
//
|
||||||
// Retriggered LFOs restart for THIS voice only, whatever the gate - an LFO starting fresh
|
// Retriggered LFOs restart for THIS voice only, whatever the gate - an LFO starting fresh
|
||||||
// per note is wanted under held notes too, and it is the shared cycle that must not move.
|
// per note is wanted under held notes too, and the other voices' cycles must not move.
|
||||||
static void voiceArm(Synth *s, Voice *v)
|
static void voiceArm(Voice *v)
|
||||||
{
|
{
|
||||||
int oneShot = (v->gate == VOICE_TRIGGER);
|
int oneShot = (v->gate == VOICE_TRIGGER);
|
||||||
v->ampEnv.oneShot = oneShot;
|
v->ampEnv.oneShot = oneShot;
|
||||||
@@ -394,8 +409,8 @@ static void voiceArm(Synth *s, Voice *v)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (int l = 0; l < LFO_COUNT; l++)
|
for (int l = 0; l < LFO_COUNT; l++)
|
||||||
if (s->lfos[l].mode == LFO_RETRIGGER)
|
if (v->lfos[l].mode == LFO_RETRIGGER)
|
||||||
lfoStateInit(&v->lfoRun[l], lfoTriggerSeed(l));
|
lfoStateInit(&v->lfos[l].run, lfoTriggerSeed(l));
|
||||||
|
|
||||||
envelopeNoteOn(&v->ampEnv);
|
envelopeNoteOn(&v->ampEnv);
|
||||||
envelopeNoteOn(&v->modEnv);
|
envelopeNoteOn(&v->modEnv);
|
||||||
@@ -412,7 +427,7 @@ void synthNoteOn(Synth *s, int midiNote)
|
|||||||
s->voices[i].active = 1;
|
s->voices[i].active = 1;
|
||||||
s->voices[i].filter.low = 0.0f;
|
s->voices[i].filter.low = 0.0f;
|
||||||
s->voices[i].filter.band = 0.0f;
|
s->voices[i].filter.band = 0.0f;
|
||||||
voiceArm(s, &s->voices[i]);
|
voiceArm(&s->voices[i]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,7 +440,7 @@ void synthNoteOn(Synth *s, int midiNote)
|
|||||||
s->voices[i].active = 1;
|
s->voices[i].active = 1;
|
||||||
s->voices[i].filter.low = 0.0f;
|
s->voices[i].filter.low = 0.0f;
|
||||||
s->voices[i].filter.band = 0.0f;
|
s->voices[i].filter.band = 0.0f;
|
||||||
voiceArm(s, &s->voices[i]);
|
voiceArm(&s->voices[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -460,7 +475,7 @@ void synthChannelOn(Synth *s, int channel, int midiNote)
|
|||||||
// where the last one left off is how a click gets into the front of every sound.
|
// where the last one left off is how a click gets into the front of every sound.
|
||||||
v->filter.low = 0.0f;
|
v->filter.low = 0.0f;
|
||||||
v->filter.band = 0.0f;
|
v->filter.band = 0.0f;
|
||||||
voiceArm(s, v);
|
voiceArm(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
void synthChannelOff(Synth *s, int channel)
|
void synthChannelOff(Synth *s, int channel)
|
||||||
@@ -481,13 +496,19 @@ void synthFillBuffer(Synth *s, int16_t *out, int frames) {
|
|||||||
for (int i = 0; i < frames; i++) {
|
for (int i = 0; i < frames; i++) {
|
||||||
float mix = 0.0f;
|
float mix = 0.0f;
|
||||||
|
|
||||||
// The shared cycle advances once per sample whatever is listening, so a free LFO is
|
|
||||||
// one sweep under everything and does not stall when nothing is sounding.
|
|
||||||
float freeLfo0 = lfoTick(&s->lfos[0], sr);
|
|
||||||
float freeLfo1 = lfoTick(&s->lfos[1], sr);
|
|
||||||
|
|
||||||
for (int v = 0; v < VOICE_COUNT; v++) {
|
for (int v = 0; v < VOICE_COUNT; v++) {
|
||||||
Voice *vv = &s->voices[v];
|
Voice *vv = &s->voices[v];
|
||||||
|
|
||||||
|
// ---- Before the silent voices are skipped, and that is the load-bearing part ----
|
||||||
|
//
|
||||||
|
// A cycle that only advanced while its voice sounded would sit still between
|
||||||
|
// notes, and the voices of a chord would fall out of step the moment one of them
|
||||||
|
// ended. Advancing every voice's cycle on every sample is what makes a free LFO
|
||||||
|
// still one sweep under everything: identical seeds stepped by identical
|
||||||
|
// arithmetic the same number of times give identical numbers, not close ones.
|
||||||
|
float lfo0 = lfoTick(&vv->lfos[0], sr);
|
||||||
|
float lfo1 = lfoTick(&vv->lfos[1], sr);
|
||||||
|
|
||||||
if (!vv->active) continue;
|
if (!vv->active) continue;
|
||||||
|
|
||||||
float amp = envelopeTick(&vv->ampEnv, sr);
|
float amp = envelopeTick(&vv->ampEnv, sr);
|
||||||
@@ -504,13 +525,6 @@ void synthFillBuffer(Synth *s, int16_t *out, int frames) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A retriggered LFO reads this voice's own cycle, started when it was struck.
|
|
||||||
// A free one reads the shared cycle above, exactly as it always did.
|
|
||||||
float lfo0 = (s->lfos[0].mode == LFO_RETRIGGER)
|
|
||||||
? lfoTickState(&s->lfos[0], &vv->lfoRun[0], sr) : freeLfo0;
|
|
||||||
float lfo1 = (s->lfos[1].mode == LFO_RETRIGGER)
|
|
||||||
? lfoTickState(&s->lfos[1], &vv->lfoRun[1], sr) : freeLfo1;
|
|
||||||
|
|
||||||
float oscMix = 0.0f;
|
float oscMix = 0.0f;
|
||||||
int activeOscs = 0;
|
int activeOscs = 0;
|
||||||
for (int o = 0; o < OSC_COUNT; o++) {
|
for (int o = 0; o < OSC_COUNT; o++) {
|
||||||
@@ -643,6 +657,30 @@ void synthSyncVoices(Synth *s)
|
|||||||
s->voices[v].levelSource = s->voices[0].levelSource;
|
s->voices[v].levelSource = s->voices[0].levelSource;
|
||||||
s->voices[v].gate = s->voices[0].gate;
|
s->voices[v].gate = s->voices[0].gate;
|
||||||
|
|
||||||
|
// Sync the LFO settings but NOT the cycle, for the same reason as the envelopes and
|
||||||
|
// the filter below: what the patch says is shared, where the voice has got to is its
|
||||||
|
// own. Copying the cycle here would also stamp on a retriggered LFO every frame.
|
||||||
|
for (int l = 0; l < LFO_COUNT; l++) {
|
||||||
|
s->voices[v].lfos[l].rate = s->voices[0].lfos[l].rate;
|
||||||
|
s->voices[v].lfos[l].waveform = s->voices[0].lfos[l].waveform;
|
||||||
|
s->voices[v].lfos[l].active = s->voices[0].lfos[l].active;
|
||||||
|
s->voices[v].lfos[l].mode = s->voices[0].lfos[l].mode;
|
||||||
|
|
||||||
|
// ---- and, while it is free, the cycle too ----
|
||||||
|
//
|
||||||
|
// A free LFO is DEFINED as one sweep under everything, and until now that was
|
||||||
|
// true only for as long as every writer of voice 0 remembered to write the other
|
||||||
|
// seven. synthResetPatch remembered; patchLoad did not, and a chord came out of
|
||||||
|
// a patch load permanently out of phase with itself. An invariant that decays the
|
||||||
|
// first time someone forgets is better made structural: carry the cycle down and
|
||||||
|
// it cannot drift, whoever writes what.
|
||||||
|
//
|
||||||
|
// Only while free. A retriggered LFO's cycle is its voice's own business, and
|
||||||
|
// copying it here would stamp on it every frame.
|
||||||
|
if (s->voices[0].lfos[l].mode == LFO_FREE)
|
||||||
|
s->voices[v].lfos[l].run = s->voices[0].lfos[l].run;
|
||||||
|
}
|
||||||
|
|
||||||
// Sync oscillator settings
|
// Sync oscillator settings
|
||||||
for (int o = 0; o < OSC_COUNT; o++) {
|
for (int o = 0; o < OSC_COUNT; o++) {
|
||||||
s->voices[v].oscillators[o].waveform = s->voices[0].oscillators[o].waveform;
|
s->voices[v].oscillators[o].waveform = s->voices[0].oscillators[o].waveform;
|
||||||
|
|||||||
+28
-8
@@ -10,11 +10,11 @@
|
|||||||
//
|
//
|
||||||
// It is copied rather than submoduled. Two files against tying this build to another
|
// It is copied rather than submoduled. Two files against tying this build to another
|
||||||
// repository's history is not a close call, and what a copy costs is that changes have to be
|
// repository's history is not a close call, and what a copy costs is that changes have to be
|
||||||
// carried across on purpose - in BOTH directions, which has now happened once each way.
|
// carried across on purpose - in BOTH directions, which has now happened twice each way.
|
||||||
//
|
//
|
||||||
// ---- What was changed ----
|
// ---- What was changed ----
|
||||||
//
|
//
|
||||||
// Nothing. This is soundThing's engine at b73e5c0, character for character, except that
|
// Nothing. This is soundThing's engine at 71e3cb2, character for character, except that
|
||||||
// em-dashes and arrows in its comments are written as ASCII here because this tree is ASCII
|
// em-dashes and arrows in its comments are written as ASCII here because this tree is ASCII
|
||||||
// only. That rule is local and is not an improvement, so it was not sent upstream.
|
// only. That rule is local and is not an improvement, so it was not sent upstream.
|
||||||
//
|
//
|
||||||
@@ -24,6 +24,13 @@
|
|||||||
// waiting for a key, and a triggered voice that re-arms its oscillators so a one-shot is the
|
// waiting for a key, and a triggered voice that re-arms its oscillators so a one-shot is the
|
||||||
// same one-shot twice. A game is nearly all one-shots, which is why the traffic went that way.
|
// same one-shot twice. A game is nearly all one-shots, which is why the traffic went that way.
|
||||||
//
|
//
|
||||||
|
// The second round trip was the LFOs, which belonged to the Synth and so were shared by every
|
||||||
|
// voice at once. That is right for one instrument played polyphonically and wrong for four
|
||||||
|
// channels playing four different things: whichever patch loaded last owned the LFO for all of
|
||||||
|
// them. They now live in the Voice. It went up from here and came back with a bug fixed and
|
||||||
|
// one hunk that does nothing here - synthSyncVoices carrying the free cycle down, which only
|
||||||
|
// matters to a caller that syncs, and this device never does.
|
||||||
|
//
|
||||||
// So the thing to keep current is no longer a list. It is this: if either copy changes, the
|
// So the thing to keep current is no longer a list. It is this: if either copy changes, the
|
||||||
// other one has to be told.
|
// other one has to be told.
|
||||||
//
|
//
|
||||||
@@ -119,7 +126,7 @@ typedef enum {
|
|||||||
} LfoMode;
|
} LfoMode;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
LfoState run; // the free-running cycle, read by every voice in LFO_FREE
|
LfoState run; // this LFO's own cycle, free-running or reset at note on
|
||||||
float rate; // Hz
|
float rate; // Hz
|
||||||
Waveform waveform;
|
Waveform waveform;
|
||||||
int active;
|
int active;
|
||||||
@@ -194,10 +201,24 @@ typedef struct {
|
|||||||
// said what shapes the sound; this says who ends it. Without it a voice can only ever
|
// said what shapes the sound; this says who ends it. Without it a voice can only ever
|
||||||
// finish because a key came up, which is no use to a drum.
|
// finish because a key came up, which is no use to a drum.
|
||||||
VoiceGate gate;
|
VoiceGate gate;
|
||||||
// One cycle per LFO per voice, used only by the LFOs set to LFO_RETRIGGER. Runtime state
|
// ---- Its own LFOs, like its own envelopes and its own filter ----
|
||||||
// like the envelope stages and the filter's integrators, so synthSyncVoices leaves it
|
//
|
||||||
// alone - it is where this voice is, not what the patch says.
|
// They used to belong to the Synth, which is right for ONE INSTRUMENT played polyphonically
|
||||||
LfoState lfoRun[LFO_COUNT];
|
// and wrong for anything else: a patch carries LFO settings, so loading a patch changed
|
||||||
|
// what every other voice heard. That is invisible here, because every voice is given the
|
||||||
|
// same patch - and it is the whole difficulty for anything driving these voices as separate
|
||||||
|
// parts, where two patches disagree about the rate and the last one loaded wins for both.
|
||||||
|
//
|
||||||
|
// A chord is unaffected. synthSyncVoices copies these like everything else, so every voice
|
||||||
|
// runs identical settings, and their cycles are started together in synthInit and advanced
|
||||||
|
// every sample whether the voice is sounding or not - so they do not merely stay close,
|
||||||
|
// they stay bit for bit identical, and one sweep still runs under the whole chord.
|
||||||
|
//
|
||||||
|
// Each LFO's own cycle lives inside it, like the envelope stages and the filter's
|
||||||
|
// integrators, so synthSyncVoices copies the settings and leaves the cycle alone - it is
|
||||||
|
// where this voice is, not what the patch says. LFO_RETRIGGER resets that same cycle at
|
||||||
|
// note on, which is what it always did, only now to a cycle nothing else is listening to.
|
||||||
|
LFO lfos[LFO_COUNT];
|
||||||
} Voice;
|
} Voice;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -207,7 +228,6 @@ typedef struct {
|
|||||||
int lastStolenVoice;
|
int lastStolenVoice;
|
||||||
Voice voices[VOICE_COUNT];
|
Voice voices[VOICE_COUNT];
|
||||||
float volume; // 0.0 to 1.0, master output level
|
float volume; // 0.0 to 1.0, master output level
|
||||||
LFO lfos[LFO_COUNT];
|
|
||||||
} Synth;
|
} Synth;
|
||||||
|
|
||||||
// Envelope functions:
|
// Envelope functions:
|
||||||
|
|||||||
@@ -152,11 +152,30 @@ static int toByte(const Mapping *m, double value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
|
// ---- Two ways out, one conversion ----
|
||||||
|
//
|
||||||
|
// Assembly for a program that includes the patch, and raw bytes for a tune that embeds
|
||||||
|
// one. THE SAME TABLE EITHER WAY: a count and that many parameter and value pairs, which
|
||||||
|
// is what the sound device is written with. What differs is only whether an assembler or
|
||||||
|
// a tune compiler is going to be the one holding it.
|
||||||
|
//
|
||||||
|
// It matters that this stays the only thing that reads soundThing's JSON. A second
|
||||||
|
// program parsing that format is a second opinion about what a patch means, and the seam
|
||||||
|
// between two opinions is where the last sound bug lived for a fortnight.
|
||||||
|
int raw = 0;
|
||||||
|
if (argc > 1 && strcmp(argv[1], "--blob") == 0) {
|
||||||
|
raw = 1;
|
||||||
|
argv++;
|
||||||
|
argc--;
|
||||||
|
}
|
||||||
if (argc < 3) {
|
if (argc < 3) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"Usage: %s <patch.json> <label> [output.asm]\n\n"
|
"Usage: %s <patch.json> <label> [output.asm]\n"
|
||||||
|
" %s --blob <patch.json> <name> <output.patch>\n\n"
|
||||||
"Turns a soundThing patch into a table for the sound device: a count, then that\n"
|
"Turns a soundThing patch into a table for the sound device: a count, then that\n"
|
||||||
"many parameter and value pairs. Hand the label to playPatch.\n", argv[0]);
|
"many parameter and value pairs. Hand the label to playPatch.\n\n"
|
||||||
|
"With --blob it writes those bytes and nothing else, for TuneC to embed.\n",
|
||||||
|
argv[0], argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
const char *path = argv[1];
|
const char *path = argv[1];
|
||||||
@@ -222,6 +241,28 @@ int main(int argc, char **argv) {
|
|||||||
int count = 0;
|
int count = 0;
|
||||||
for (int i = 0; i < MAPPING_COUNT; i++) if (seen[i]) count++;
|
for (int i = 0; i < MAPPING_COUNT; i++) if (seen[i]) count++;
|
||||||
|
|
||||||
|
// ---- Raw bytes, for something that is not an assembler ----
|
||||||
|
//
|
||||||
|
// A name is still asked for and still ignored here, so that the two forms are converted
|
||||||
|
// by the same command with the same arguments and a build can switch between them by
|
||||||
|
// adding a word.
|
||||||
|
if (raw) {
|
||||||
|
if (argc < 4) {
|
||||||
|
fprintf(stderr, "SoundPatch: --blob needs somewhere to write it.\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
FILE *blob = fopen(argv[3], "wb");
|
||||||
|
if (!blob) { fprintf(stderr, "SoundPatch: cannot write %s\n", argv[3]); return 1; }
|
||||||
|
fputc(count, blob);
|
||||||
|
for (int i = 0; i < MAPPING_COUNT; i++) {
|
||||||
|
if (!seen[i]) continue;
|
||||||
|
fputc(mappings[i].parameter, blob);
|
||||||
|
fputc(values[i], blob);
|
||||||
|
}
|
||||||
|
fclose(blob);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
FILE *out = stdout;
|
FILE *out = stdout;
|
||||||
if (argc > 3) {
|
if (argc > 3) {
|
||||||
out = fopen(argv[3], "w");
|
out = fopen(argv[3], "w");
|
||||||
|
|||||||
@@ -0,0 +1,535 @@
|
|||||||
|
// TuneC.c
|
||||||
|
// Turns a written tune into the bytes SplitBit's music player reads.
|
||||||
|
// Written by Anachronaut
|
||||||
|
//
|
||||||
|
// ---- What a tune is ----
|
||||||
|
//
|
||||||
|
// Three tables and one indirection, which is a tracker's own shape. INSTRUMENTS are patches
|
||||||
|
// designed in soundThing and converted by SoundPatch. SEQUENCES are one voice's phrase, notes
|
||||||
|
// and rests and commands. An ORDER LIST names sequences, one list a voice, and that is where
|
||||||
|
// repetition comes from: a bass line that plays under four different melodies is written once
|
||||||
|
// and named four times.
|
||||||
|
//
|
||||||
|
// ---- The syntax is SplitBit's ----
|
||||||
|
//
|
||||||
|
// `#` is a directive and `;` is a comment, exactly as in SplitBit assembly and in the shell's
|
||||||
|
// scripts. One rule across the machine rather than a third dialect, and numbers are written
|
||||||
|
// the way the assembler writes them: 0d for decimal and 0x for hexadecimal, with no bare
|
||||||
|
// numbers, so that a person reading a tune is reading something they already know.
|
||||||
|
//
|
||||||
|
// A .score is what somebody writes and a .tune is what the machine reads, the same way a
|
||||||
|
// .asm is written and a .sbx is loaded. Handing a player a source is then a mistake it can
|
||||||
|
// name rather than one it has to guess at.
|
||||||
|
//
|
||||||
|
// ; Four bars, and the bass is the same one under two of them.
|
||||||
|
// #Tick 0d125000 ; cycles a tick: a sixteenth note at 120 beats a minute
|
||||||
|
// #Patch Oboe oboe.patch
|
||||||
|
// #Patch Strings strings.patch
|
||||||
|
// #Voice 0 Oboe ; which instrument each voice starts on
|
||||||
|
// #Sequence Verse
|
||||||
|
// 0d64 0d4 0d67 0d4 0d72 0d8
|
||||||
|
// #Sequence Ending
|
||||||
|
// #Use Strings ; a command, which takes no time at all
|
||||||
|
// 0d72 0d16
|
||||||
|
// #Order 0
|
||||||
|
// Verse Verse Ending
|
||||||
|
//
|
||||||
|
// ---- What it checks, and why those ----
|
||||||
|
//
|
||||||
|
// Everything here is something the player cannot notice for itself. It has no names, so it
|
||||||
|
// cannot say that a sequence does not exist; it has no lengths, so it cannot say that the
|
||||||
|
// voices will come apart four bars in; and by the time a tune is loaded, "no starting patch"
|
||||||
|
// and "starting patch nought" are the same byte.
|
||||||
|
//
|
||||||
|
// A DURATION OF NOUGHT IS THE DANGEROUS ONE. The player counts a duration down and reads the
|
||||||
|
// next event when it reaches nought, so a count that starts there is decremented to 255 and
|
||||||
|
// holds for a very long time. It is the one mistake in a tune that sounds like a hang.
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
#define MAX_PATCHES 64
|
||||||
|
#define MAX_SEQUENCES 200 // 0xFF ends an order list, so 255 is the ceiling.
|
||||||
|
#define MAX_EVENTS 1024
|
||||||
|
#define MAX_ORDER 256
|
||||||
|
#define MAX_NAME 64
|
||||||
|
#define VOICES 4
|
||||||
|
#define HEADER_BYTES 28
|
||||||
|
|
||||||
|
#define RED "\x1B[31m"
|
||||||
|
#define RESET "\x1B[0m"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char name[MAX_NAME];
|
||||||
|
unsigned char bytes[512];
|
||||||
|
int length;
|
||||||
|
int line;
|
||||||
|
int bad; // Named, but its bytes could not be had. See doPatch.
|
||||||
|
} Patch;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char name[MAX_NAME];
|
||||||
|
unsigned char bytes[MAX_EVENTS];
|
||||||
|
int length;
|
||||||
|
int ticks; // What it adds up to, which is what the columns are checked on.
|
||||||
|
int line;
|
||||||
|
} Sequence;
|
||||||
|
|
||||||
|
static Patch patches[MAX_PATCHES];
|
||||||
|
static int patchCount = 0;
|
||||||
|
static Sequence sequences[MAX_SEQUENCES];
|
||||||
|
static int sequenceCount = 0;
|
||||||
|
static int order[VOICES][MAX_ORDER];
|
||||||
|
static int orderLength[VOICES];
|
||||||
|
static int voiceStart[VOICES]; // -1 until said, which is how "never said" is told from nought.
|
||||||
|
static long tick = -1;
|
||||||
|
|
||||||
|
static const char *sourceName = "";
|
||||||
|
static int problems = 0;
|
||||||
|
|
||||||
|
// ---- Where a patch is looked for ----
|
||||||
|
//
|
||||||
|
// Beside the tune first, then wherever -I says, which is the assembler's rule for an
|
||||||
|
// #Include. A tune that names its instruments by bare name can then be moved about with them,
|
||||||
|
// and a build that generates its patches somewhere else can say where without the tune having
|
||||||
|
// to know a build directory exists.
|
||||||
|
#define MAX_INCLUDES 16
|
||||||
|
static const char *includes[MAX_INCLUDES];
|
||||||
|
static int includeCount = 0;
|
||||||
|
static char sourceDirectory[512] = "";
|
||||||
|
|
||||||
|
static FILE *openPatch(const char *name, char *found, size_t room) {
|
||||||
|
FILE *in = NULL;
|
||||||
|
if (sourceDirectory[0]) {
|
||||||
|
snprintf(found, room, "%s/%s", sourceDirectory, name);
|
||||||
|
if ((in = fopen(found, "rb"))) return in;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < includeCount; i++) {
|
||||||
|
snprintf(found, room, "%s/%s", includes[i], name);
|
||||||
|
if ((in = fopen(found, "rb"))) return in;
|
||||||
|
}
|
||||||
|
snprintf(found, room, "%s", name);
|
||||||
|
return fopen(found, "rb");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void complain(int line, const char *what, const char *detail) {
|
||||||
|
fprintf(stderr, RED "%s:%d: %s%s%s\n" RESET, sourceName, line, what,
|
||||||
|
detail ? ": " : "", detail ? detail : "");
|
||||||
|
problems++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Tokens ----
|
||||||
|
//
|
||||||
|
// A token is a run of anything that is not a space. A semicolon takes the rest of its line
|
||||||
|
// with it, which is the assembler's rule and the shell's.
|
||||||
|
typedef struct { char text[MAX_NAME]; int line; } Token;
|
||||||
|
static Token tokens[16384];
|
||||||
|
static int tokenCount = 0;
|
||||||
|
static int at = 0;
|
||||||
|
|
||||||
|
static void readTokens(FILE *in) {
|
||||||
|
int c, line = 1;
|
||||||
|
while ((c = fgetc(in)) != EOF) {
|
||||||
|
if (c == '\n') { line++; continue; }
|
||||||
|
if (isspace(c)) continue;
|
||||||
|
if (c == ';') {
|
||||||
|
while ((c = fgetc(in)) != EOF && c != '\n');
|
||||||
|
line++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tokenCount >= (int)(sizeof(tokens) / sizeof(tokens[0]))) {
|
||||||
|
fprintf(stderr, RED "%s: too many words in one tune.\n" RESET, sourceName);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
Token *t = &tokens[tokenCount++];
|
||||||
|
t->line = line;
|
||||||
|
int n = 0;
|
||||||
|
while (c != EOF && !isspace(c) && c != ';') {
|
||||||
|
if (n < MAX_NAME - 1) t->text[n++] = (char)c;
|
||||||
|
c = fgetc(in);
|
||||||
|
}
|
||||||
|
t->text[n] = '\0';
|
||||||
|
if (c == ';') { ungetc(c, in); }
|
||||||
|
else if (c == '\n') { line++; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Token *next(void) { return at < tokenCount ? &tokens[at++] : NULL; }
|
||||||
|
static Token *peek(void) { return at < tokenCount ? &tokens[at] : NULL; }
|
||||||
|
static int isDirective(const Token *t) { return t && t->text[0] == '#'; }
|
||||||
|
|
||||||
|
// ---- Numbers, spelled the way the assembler spells them ----
|
||||||
|
//
|
||||||
|
// 0d and 0x and nothing else. A bare number is refused rather than guessed at, because this
|
||||||
|
// machine has one way of writing one and a tune is not the place to introduce a second.
|
||||||
|
static int number(const Token *t, long *out) {
|
||||||
|
if (!t) return 0;
|
||||||
|
const char *s = t->text;
|
||||||
|
if (s[0] == '0' && (s[1] == 'd' || s[1] == 'D')) {
|
||||||
|
char *end;
|
||||||
|
*out = strtol(s + 2, &end, 10);
|
||||||
|
return *end == '\0' && s[2] != '\0';
|
||||||
|
}
|
||||||
|
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
|
||||||
|
char *end;
|
||||||
|
*out = strtol(s + 2, &end, 16);
|
||||||
|
return *end == '\0' && s[2] != '\0';
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int findPatch(const char *name) {
|
||||||
|
for (int i = 0; i < patchCount; i++) if (strcmp(patches[i].name, name) == 0) return i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
static int findSequence(const char *name) {
|
||||||
|
for (int i = 0; i < sequenceCount; i++) if (strcmp(sequences[i].name, name) == 0) return i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Reading a tune ----
|
||||||
|
|
||||||
|
static void doTick(void) {
|
||||||
|
Token *t = next();
|
||||||
|
long value;
|
||||||
|
if (!number(t, &value)) {
|
||||||
|
complain(t ? t->line : 0, "#Tick wants a number of cycles", t ? t->text : "nothing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value < 1 || value > 0xFFFFFF) {
|
||||||
|
complain(t->line, "a tick has to be between 1 and 16,777,215 cycles", t->text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tick = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void doPatch(void) {
|
||||||
|
Token *name = next();
|
||||||
|
Token *file = next();
|
||||||
|
if (!name || !file || isDirective(name) || isDirective(file)) {
|
||||||
|
complain(name ? name->line : 0, "#Patch wants a name and a file", NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (patchCount >= MAX_PATCHES) {
|
||||||
|
complain(name->line, "too many patches", name->text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (findPatch(name->text) >= 0) {
|
||||||
|
complain(name->line, "that patch is named twice", name->text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ---- A PATCH THAT CANNOT BE READ IS STILL A PATCH THAT WAS NAMED ----
|
||||||
|
//
|
||||||
|
// It is registered either way, with its bytes marked missing. Nothing will be written -
|
||||||
|
// one problem is enough to stop that - but everything below can now resolve the name, so
|
||||||
|
// a single missing file says one thing instead of seven.
|
||||||
|
//
|
||||||
|
// It used to return here, and the damage was out of all proportion: the name went
|
||||||
|
// unregistered, so every #Voice naming it failed as well, and then the check that a voice
|
||||||
|
// has an instrument failed for each of those. One wrong path, seven messages, and only
|
||||||
|
// the first of them worth reading. A compiler that says one thing seven ways teaches
|
||||||
|
// people to read the last line, which is the one that matters least.
|
||||||
|
Patch *p = &patches[patchCount];
|
||||||
|
snprintf(p->name, MAX_NAME, "%s", name->text);
|
||||||
|
p->line = name->line;
|
||||||
|
p->length = 0;
|
||||||
|
p->bad = 0;
|
||||||
|
patchCount++;
|
||||||
|
|
||||||
|
char where[1024];
|
||||||
|
FILE *in = openPatch(file->text, where, sizeof(where));
|
||||||
|
if (!in) {
|
||||||
|
complain(file->line, "cannot find that patch beside the tune or on the -I path",
|
||||||
|
file->text);
|
||||||
|
p->bad = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
p->length = (int)fread(p->bytes, 1, sizeof(p->bytes), in);
|
||||||
|
fclose(in);
|
||||||
|
|
||||||
|
// A patch is a count and that many pairs. Anything else is a file that is not one, and
|
||||||
|
// embedding it would make a tune that writes rubbish at the sound device.
|
||||||
|
if (p->length < 1 || p->length != 1 + p->bytes[0] * 2) {
|
||||||
|
complain(file->line, "that is not a patch: SoundPatch --blob writes one", file->text);
|
||||||
|
p->bad = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void doVoice(void) {
|
||||||
|
Token *which = next();
|
||||||
|
Token *name = next();
|
||||||
|
long v;
|
||||||
|
if (!number(which, &v) || v < 0 || v >= VOICES) {
|
||||||
|
complain(which ? which->line : 0, "#Voice wants a voice from 0d0 to 0d3",
|
||||||
|
which ? which->text : "nothing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!name || isDirective(name)) {
|
||||||
|
complain(which->line, "#Voice wants a patch to start that voice on", NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int p = findPatch(name->text);
|
||||||
|
if (p < 0) {
|
||||||
|
complain(name->line, "no patch of that name", name->text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
voiceStart[v] = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void doSequence(void) {
|
||||||
|
Token *name = next();
|
||||||
|
if (!name || isDirective(name)) {
|
||||||
|
complain(name ? name->line : 0, "#Sequence wants a name", NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sequenceCount >= MAX_SEQUENCES) {
|
||||||
|
complain(name->line, "too many sequences", name->text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (findSequence(name->text) >= 0) {
|
||||||
|
// The first one keeps the name, so an order list naming it still resolves and the
|
||||||
|
// duplicate is reported once rather than once here and again at every use.
|
||||||
|
complain(name->line, "that sequence is named twice", name->text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Sequence *q = &sequences[sequenceCount];
|
||||||
|
snprintf(q->name, MAX_NAME, "%s", name->text);
|
||||||
|
q->length = 0;
|
||||||
|
q->ticks = 0;
|
||||||
|
q->line = name->line;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
Token *t = peek();
|
||||||
|
if (!t) break;
|
||||||
|
if (isDirective(t)) {
|
||||||
|
// #Use is the one directive that belongs INSIDE a sequence. Anything else ends it.
|
||||||
|
if (strcmp(t->text, "#Use") != 0) break;
|
||||||
|
next();
|
||||||
|
Token *what = next();
|
||||||
|
if (!what || isDirective(what)) {
|
||||||
|
complain(t->line, "#Use wants a patch", NULL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int p = findPatch(what->text);
|
||||||
|
if (p < 0) {
|
||||||
|
complain(what->line, "no patch of that name", what->text);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
q->bytes[q->length++] = 0x80;
|
||||||
|
q->bytes[q->length++] = (unsigned char)p;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long note, ticksFor;
|
||||||
|
if (!number(t, ¬e)) {
|
||||||
|
complain(t->line, "a sequence holds notes and durations", t->text);
|
||||||
|
next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
Token *d = next();
|
||||||
|
if (!number(d, &ticksFor)) {
|
||||||
|
complain(d ? d->line : t->line, "that note has no duration after it", t->text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (note < 0 || note > 127) {
|
||||||
|
complain(t->line, "a note is 0d0 for a rest, or 0d1 to 0d127", t->text);
|
||||||
|
}
|
||||||
|
// The one that sounds like a hang. The player counts a duration down and reads the
|
||||||
|
// next event at nought, so a count that starts there wraps to 255 and holds.
|
||||||
|
if (ticksFor < 1 || ticksFor > 255) {
|
||||||
|
complain(d->line, "a duration is from 0d1 to 0d255 ticks", d->text);
|
||||||
|
}
|
||||||
|
if (q->length + 2 >= MAX_EVENTS) {
|
||||||
|
complain(t->line, "that sequence is too long", q->name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
q->bytes[q->length++] = (unsigned char)(note & 0x7F);
|
||||||
|
q->bytes[q->length++] = (unsigned char)(ticksFor & 0xFF);
|
||||||
|
q->ticks += (int)ticksFor;
|
||||||
|
}
|
||||||
|
q->bytes[q->length++] = 0xFF;
|
||||||
|
sequenceCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void doOrder(void) {
|
||||||
|
Token *which = next();
|
||||||
|
long v;
|
||||||
|
if (!number(which, &v) || v < 0 || v >= VOICES) {
|
||||||
|
complain(which ? which->line : 0, "#Order wants a voice from 0d0 to 0d3",
|
||||||
|
which ? which->text : "nothing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
Token *t = peek();
|
||||||
|
if (!t || isDirective(t)) break;
|
||||||
|
next();
|
||||||
|
int q = findSequence(t->text);
|
||||||
|
if (q < 0) {
|
||||||
|
complain(t->line, "no sequence of that name", t->text);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (orderLength[v] >= MAX_ORDER - 1) {
|
||||||
|
complain(t->line, "that order list is too long", t->text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
order[v][orderLength[v]++] = q;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- What the player cannot check for itself ----
|
||||||
|
static void check(void) {
|
||||||
|
if (tick < 0) {
|
||||||
|
fprintf(stderr, RED "%s: no #Tick, so there is no beat to play it at.\n" RESET, sourceName);
|
||||||
|
problems++;
|
||||||
|
}
|
||||||
|
for (int v = 0; v < VOICES; v++) {
|
||||||
|
// The ruling: a voice with a part and no instrument is an error rather than a default.
|
||||||
|
// By the time a tune is loaded this cannot be noticed - "never said" and "patch
|
||||||
|
// nought" are the same byte - so it has to be noticed here or not at all.
|
||||||
|
if (orderLength[v] > 0 && voiceStart[v] < 0) {
|
||||||
|
fprintf(stderr, RED "%s: voice %d has a part and no instrument. Say #Voice 0d%d.\n"
|
||||||
|
RESET, sourceName, v, v);
|
||||||
|
problems++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---- The columns have to add up ----
|
||||||
|
//
|
||||||
|
// Nothing keeps four voices together except that the sequences they play at the same
|
||||||
|
// position last the same number of ticks. Get one wrong and the parts come apart, quietly,
|
||||||
|
// some bars after the mistake - which is the hardest kind of fault to find by ear and the
|
||||||
|
// easiest kind to find here.
|
||||||
|
int longest = 0;
|
||||||
|
for (int v = 0; v < VOICES; v++) if (orderLength[v] > longest) longest = orderLength[v];
|
||||||
|
for (int i = 0; i < longest; i++) {
|
||||||
|
int want = -1, wantVoice = -1;
|
||||||
|
for (int v = 0; v < VOICES; v++) {
|
||||||
|
if (i >= orderLength[v]) continue;
|
||||||
|
Sequence *q = &sequences[order[v][i]];
|
||||||
|
if (want < 0) { want = q->ticks; wantVoice = v; continue; }
|
||||||
|
if (q->ticks != want) {
|
||||||
|
fprintf(stderr, RED "%s:%d: at position %d, %s lasts %d ticks and voice %d's %s"
|
||||||
|
" lasts %d. The voices would come apart here.\n" RESET,
|
||||||
|
sourceName, q->line, i, q->name, q->ticks, wantVoice,
|
||||||
|
sequences[order[wantVoice][i]].name, want);
|
||||||
|
problems++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Laying it out ----
|
||||||
|
|
||||||
|
static unsigned char out[65536];
|
||||||
|
static int outLength = 0;
|
||||||
|
|
||||||
|
static void put(int byte) {
|
||||||
|
if (outLength >= (int)sizeof(out)) {
|
||||||
|
fprintf(stderr, RED "%s: that tune does not fit in 64K.\n" RESET, sourceName);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
out[outLength++] = (unsigned char)byte;
|
||||||
|
}
|
||||||
|
static void putWord(int at, int value) {
|
||||||
|
out[at] = (unsigned char)((value >> 8) & 0xFF);
|
||||||
|
out[at + 1] = (unsigned char)(value & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
while (argc > 2 && strcmp(argv[1], "-I") == 0) {
|
||||||
|
if (includeCount < MAX_INCLUDES) includes[includeCount++] = argv[2];
|
||||||
|
argv += 2;
|
||||||
|
argc -= 2;
|
||||||
|
}
|
||||||
|
if (argc < 3) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage: %s [-I <dir>] <tune.score> <output.tune>\n\n"
|
||||||
|
"Turns a written tune into the bytes SplitBit's player reads: a header, a table\n"
|
||||||
|
"of patches, a table of sequences, and an order list for each of four voices.\n\n"
|
||||||
|
"Patches come from SoundPatch --blob, which is the only thing that knows what a\n"
|
||||||
|
"soundThing patch means. They are looked for beside the tune and then on the -I\n"
|
||||||
|
"path, the way the assembler looks for an include.\n", argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
sourceName = argv[1];
|
||||||
|
const char *slash = strrchr(sourceName, '/');
|
||||||
|
if (slash) {
|
||||||
|
size_t n = (size_t)(slash - sourceName);
|
||||||
|
if (n >= sizeof(sourceDirectory)) n = sizeof(sourceDirectory) - 1;
|
||||||
|
memcpy(sourceDirectory, sourceName, n);
|
||||||
|
sourceDirectory[n] = '\0';
|
||||||
|
}
|
||||||
|
FILE *in = fopen(sourceName, "r");
|
||||||
|
if (!in) { fprintf(stderr, RED "TuneC: cannot read %s\n" RESET, sourceName); return 1; }
|
||||||
|
readTokens(in);
|
||||||
|
fclose(in);
|
||||||
|
|
||||||
|
for (int v = 0; v < VOICES; v++) { voiceStart[v] = -1; orderLength[v] = 0; }
|
||||||
|
|
||||||
|
Token *t;
|
||||||
|
while ((t = next()) != NULL) {
|
||||||
|
if (!isDirective(t)) {
|
||||||
|
complain(t->line, "that is not inside anything", t->text);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (strcmp(t->text, "#Tick") == 0) doTick();
|
||||||
|
else if (strcmp(t->text, "#Patch") == 0) doPatch();
|
||||||
|
else if (strcmp(t->text, "#Voice") == 0) doVoice();
|
||||||
|
else if (strcmp(t->text, "#Sequence") == 0) doSequence();
|
||||||
|
else if (strcmp(t->text, "#Order") == 0) doOrder();
|
||||||
|
else complain(t->line, "no such directive", t->text);
|
||||||
|
}
|
||||||
|
check();
|
||||||
|
if (problems) {
|
||||||
|
fprintf(stderr, RED "TuneC: %d problem%s, so nothing was written.\n" RESET,
|
||||||
|
problems, problems == 1 ? "" : "s");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tables come first so that everything they name can be placed after them and its
|
||||||
|
// offset known as it lands.
|
||||||
|
outLength = HEADER_BYTES;
|
||||||
|
int patchTableAt = outLength; outLength += 2 * patchCount;
|
||||||
|
int sequenceTableAt = outLength; outLength += 2 * sequenceCount;
|
||||||
|
|
||||||
|
for (int i = 0; i < patchCount; i++) {
|
||||||
|
putWord(patchTableAt + 2 * i, outLength);
|
||||||
|
for (int b = 0; b < patches[i].length; b++) put(patches[i].bytes[b]);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < sequenceCount; i++) {
|
||||||
|
putWord(sequenceTableAt + 2 * i, outLength);
|
||||||
|
for (int b = 0; b < sequences[i].length; b++) put(sequences[i].bytes[b]);
|
||||||
|
}
|
||||||
|
int orderAt[VOICES];
|
||||||
|
for (int v = 0; v < VOICES; v++) {
|
||||||
|
orderAt[v] = outLength;
|
||||||
|
for (int i = 0; i < orderLength[v]; i++) put(order[v][i]);
|
||||||
|
put(0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(out, "SBTU", 4);
|
||||||
|
out[4] = 1;
|
||||||
|
out[5] = (unsigned char)((tick >> 16) & 0xFF);
|
||||||
|
out[6] = (unsigned char)((tick >> 8) & 0xFF);
|
||||||
|
out[7] = (unsigned char)(tick & 0xFF);
|
||||||
|
out[8] = (unsigned char)patchCount;
|
||||||
|
out[9] = (unsigned char)sequenceCount;
|
||||||
|
putWord(10, patchTableAt);
|
||||||
|
putWord(12, sequenceTableAt);
|
||||||
|
for (int v = 0; v < VOICES; v++) putWord(14 + 2 * v, orderAt[v]);
|
||||||
|
// A voice with no part still needs a byte here, and nought is as good as any: the player
|
||||||
|
// loads it and never plays a note with it.
|
||||||
|
for (int v = 0; v < VOICES; v++) out[22 + v] = (unsigned char)(voiceStart[v] < 0 ? 0 : voiceStart[v]);
|
||||||
|
out[26] = 0;
|
||||||
|
out[27] = 0;
|
||||||
|
|
||||||
|
FILE *o = fopen(argv[2], "wb");
|
||||||
|
if (!o) { fprintf(stderr, RED "TuneC: cannot write %s\n" RESET, argv[2]); return 1; }
|
||||||
|
fwrite(out, 1, outLength, o);
|
||||||
|
fclose(o);
|
||||||
|
printf("Wrote %s: %d bytes, %d patch%s, %d sequence%s.\n", argv[2], outLength,
|
||||||
|
patchCount, patchCount == 1 ? "" : "es",
|
||||||
|
sequenceCount, sequenceCount == 1 ? "" : "s");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -420,8 +420,51 @@ Assembler [options] <sourcefile>
|
|||||||
| -o, --output \<file\> | Write the output to this path. Without it, the output is named after the source file, in the directory the assembler was run from, taking .bin if it is a boot image and .sbx if it is a loadable program. |
|
| -o, --output \<file\> | Write the output to this path. Without it, the output is named after the source file, in the directory the assembler was run from, taking .bin if it is a boot image and .sbx if it is a loadable program. |
|
||||||
| -I, --include \<dir\> | Look in this directory for included files. May be given more than once, and the directories are searched in the order given. |
|
| -I, --include \<dir\> | Look in this directory for included files. May be given more than once, and the directories are searched in the order given. |
|
||||||
| -M, --depend \<file\> | Write out which source files went into the output, as a make rule. |
|
| -M, --depend \<file\> | Write out which source files went into the output, as a make rule. |
|
||||||
|
| -S, --symbols \<file\> | Write a symbol table to this path: every label and every vector, with where it lives and where it was written. |
|
||||||
| -h, --help | Print the options and stop. |
|
| -h, --help | Print the options and stop. |
|
||||||
|
|
||||||
|
Every option above that takes a \<file\> names a file the assembler WRITES, and the source is the bare argument at the end. So `Assembler -S program.asm` does not dump the symbols of program.asm. It asks for the symbol file to be called program.asm, and then finds it has no source left to assemble.
|
||||||
|
|
||||||
|
-S is for looking at what the assembler decided, and for finding your way around a program that has grown past holding in your head. It writes one line for every label and every vector, with six fields separated by tabs:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| -- | -- |
|
||||||
|
| Kind | Program, Data, Vector or Device. |
|
||||||
|
| Number | Which vector it is: the vector number, or the port for a device. A label has no number and the field is a dash. |
|
||||||
|
| Address | Four hexadecimal digits: where the thing lives. For a label, the address it was given. For a vector, the slot in the vector table that holds its handler's address. |
|
||||||
|
| Name | The label, or the vector's name. A device has no name of its own, so it is listed under its handler. |
|
||||||
|
| File | The source file it was written in, named the way the assembler was given it. |
|
||||||
|
| Line | Which line of that file, counting from one. |
|
||||||
|
|
||||||
|
```
|
||||||
|
Program - 5000 start game.asm 31
|
||||||
|
Program - 670D int16add Libraries/math.asm 5
|
||||||
|
Vector 16 FC20 osPrintString services.asm 24
|
||||||
|
Vector 64 FC80 gameFrame game.asm 402
|
||||||
|
Device 32 FE40 diskDone system.asm 812
|
||||||
|
Data - 3000 Score game.asm 118
|
||||||
|
```
|
||||||
|
|
||||||
|
The kind and the number sit together at the front because the two of them are what names the thing: Vector 16 and Device 32 are identifiers in a way that Vector on its own is not. Everything after them says something about it rather than naming it, so a line reads as a thing followed by what is known about it.
|
||||||
|
|
||||||
|
Tabs rather than aligned columns, so that the file is a table every ordinary tool already reads without being taught anything, and no heading line, so that nothing has to know to skip one. The file field is what makes it worth having on a big program: a name defined once and called in forty places is hard to find by searching, and this says where it was written.
|
||||||
|
|
||||||
|
The kind comes first because Program and Data are separate memories on this machine, so an address on its own does not say where something is. In a loadable program the two segments are usually based far enough apart that the difference is easy to overlook. In a boot image both start at 0x0000, and every address in the table appears twice.
|
||||||
|
|
||||||
|
### Vectors In The Table
|
||||||
|
|
||||||
|
A vector is worth having here for two reasons that labels do not have.
|
||||||
|
|
||||||
|
Its number is the one thing about a program that nothing else can tell you. A vector that was pinned has its number written in the source that pinned it, but a vector the assembler numbered has that number nowhere at all: not in the source, and not in the binary in any form you can read. The table is where you find out that gameFrame became vector 64.
|
||||||
|
|
||||||
|
And a vector takes two hops to follow by hand. The name in `SWI osPrintString` is not the name of the routine that implements it, so finding the code means searching for the vector, reading the handler's name off the Vector Segment, and searching again. A vector's row names the handler and gives the line the two were tied together on.
|
||||||
|
|
||||||
|
Both of a vector's numbers are given, because neither can be worked out from the other without knowing which table the vector is in. The Number is what a program writes and what the machine dispatches on. The Address is where the handler's address is stored - 0xFC00 plus twice the number for a software vector, 0xFE00 plus twice the port for a device - which is what a loader writes and what a memory dump shows.
|
||||||
|
|
||||||
|
A vector that this program only declares, without implementing it, is listed like any other. That is how a program says which vectors it calls, and it is the way to check that two programs agree about a number.
|
||||||
|
|
||||||
|
The table is sorted by kind and then by address, in the order Program, Data, Vector, Device.
|
||||||
|
|
||||||
The assembler stops at the first error, says which file and line it was in, and exits without writing anything.
|
The assembler stops at the first error, says which file and line it was in, and exits without writing anything.
|
||||||
|
|
||||||
## Building With Make:
|
## Building With Make:
|
||||||
|
|||||||
@@ -190,6 +190,12 @@ is how a program says it has finished; WAIT stops only the CPU's use of the bus.
|
|||||||
runs, devices run, and the moment any of them raises a line the CPU carries on with the
|
runs, devices run, and the moment any of them raises a line the CPU carries on with the
|
||||||
instruction after the WAIT.
|
instruction after the WAIT.
|
||||||
|
|
||||||
|
**The same is true of a program waiting on a key.** A console read that has nothing to hand
|
||||||
|
over stops the CPU and not the machine: the clock goes on, the cursor goes on blinking, a disk
|
||||||
|
goes on settling and a note goes on decaying. Time spent waiting for somebody to type is time,
|
||||||
|
and a device that stopped because the processor was waiting would be a device that behaved
|
||||||
|
differently depending on how fast the person at the keyboard was.
|
||||||
|
|
||||||
**A line that is already up means there is nothing to wait for**, and WAIT does nothing at
|
**A line that is already up means there is nothing to wait for**, and WAIT does nothing at
|
||||||
all. That is what makes the ordinary shape of it safe:
|
all. That is what makes the ordinary shape of it safe:
|
||||||
|
|
||||||
@@ -307,6 +313,8 @@ Backspace is 0x08 and always has been. It is a different key from Delete and doe
|
|||||||
|
|
||||||
**The console normalises, which is what it has always done.** Behind a window it turns the key somebody pressed into a byte; on a terminal it turns `ESC [ A` and its neighbours into the same byte. That is the same act it performs on Return and Backspace, and it is why a program does not have to know which of the two it is talking to. The translation happens only when there really is a terminal: a file or a pipe holds exactly the bytes somebody put in it, and a program reading one gets those bytes untouched - which is also how a test presses an arrow key.
|
**The console normalises, which is what it has always done.** Behind a window it turns the key somebody pressed into a byte; on a terminal it turns `ESC [ A` and its neighbours into the same byte. That is the same act it performs on Return and Backspace, and it is why a program does not have to know which of the two it is talking to. The translation happens only when there really is a terminal: a file or a pipe holds exactly the bytes somebody put in it, and a program reading one gets those bytes untouched - which is also how a test presses an arrow key.
|
||||||
|
|
||||||
|
**Backspace is part of that, and the terminal says what it is.** A terminal has an erase character of its own, and while it is doing the editing it consumes that character itself and hands over a finished line. Key mode stops it editing, so from then on the byte arrives as it was sent - usually 0x7F, sometimes 0x08, and whatever the person at the keyboard has configured. The console asks the terminal which one it is rather than assuming, and delivers 0x08. Forward Delete stays 0x86: they are two keys and they stay two keys.
|
||||||
|
|
||||||
**These arrive in key mode only.** Line mode delivers characters, and a program in line mode is being handed a line that something else has already finished editing, so a key meaning "move the cursor left" arrived too late to mean anything. The console drops them there. This is what a terminal does too: it has always given a program in line mode backspace and line kill, and has never given it arrow keys.
|
**These arrive in key mode only.** Line mode delivers characters, and a program in line mode is being handed a line that something else has already finished editing, so a key meaning "move the cursor left" arrived too late to mean anything. The console drops them there. This is what a terminal does too: it has always given a program in line mode backspace and line kill, and has never given it arrow keys.
|
||||||
|
|
||||||
**But asking the status port in line mode does not throw one away.** A key line mode will not deliver is not the same as a key that is gone, because the mode can change: a program that looks at the status port and then asks for key mode - which is exactly what a system does before it reads a line - would otherwise find that the first key it was reaching for had been swallowed by the looking. So the console holds it and delivers it as soon as something is willing to take it. Reading the data port in line mode does discard it, and must: that read is the delivery, and a key held there would be met again forever.
|
**But asking the status port in line mode does not throw one away.** A key line mode will not deliver is not the same as a key that is gone, because the mode can change: a program that looks at the status port and then asks for key mode - which is exactly what a system does before it reads a line - would otherwise find that the first key it was reaching for had been swallowed by the looking. So the console holds it and delivers it as soon as something is willing to take it. Reading the data port in line mode does discard it, and must: that read is the delivery, and a key held there would be met again forever.
|
||||||
@@ -888,15 +896,18 @@ A console on a machine with a screen sends every byte to both, because a machine
|
|||||||
|
|
||||||
At reset the font is expanded into tile memory and the palette is given sixteen ink and paper pairs. See Colour below.
|
At reset the font is expanded into tile memory and the palette is given sixteen ink and paper pairs. See Colour below.
|
||||||
|
|
||||||
The font is in ASCII order, so a byte becomes a glyph by subtracting 32. Bytes below that have no glyph and are not drawn; three of them do something instead.
|
The font is in ASCII order, so a byte becomes a glyph by subtracting 32. Bytes below that have no glyph and are not drawn; four of them do something instead.
|
||||||
|
|
||||||
| Byte | Does |
|
| Byte | Does |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 0x0A | Newline. The cursor goes to the start of the next row, and at the last row the screen scrolls instead. |
|
| 0x0A | Newline. The cursor goes to the start of the next row, and at the last row the screen scrolls instead. |
|
||||||
| 0x0D | Carriage return. The cursor goes to the start of the row it is on. |
|
| 0x0D | Carriage return. The cursor goes to the start of the row it is on. |
|
||||||
| 0x08 | Backspace. The cursor steps back and rubs out what was there. |
|
| 0x08 | Backspace. The cursor steps back and rubs out what was there. |
|
||||||
|
| 0x09 | Tab. The cursor moves to the next stop, eight columns apart. |
|
||||||
|
|
||||||
Writing past the last column wraps to the next row, the same as a newline.
|
Writing past the last column wraps to the next row, the same as a newline. So does a tab whose next stop would reach or pass the last column.
|
||||||
|
|
||||||
|
**A tab moves the cursor and marks nothing.** It does not write spaces, which matters as soon as there is anything on the line already: a carriage return followed by a tab steps over what is there and leaves it, exactly as a terminal does. Something that wants the columns cleared has to write the spaces itself.
|
||||||
|
|
||||||
### Colour:
|
### Colour:
|
||||||
|
|
||||||
@@ -920,6 +931,14 @@ The palette lives at 0xFC00 in video memory, four bytes an entry - red, green, b
|
|||||||
|
|
||||||
`Programs/Examples/colours.asm` does all of that in eighty lines and prints the result. It shows the sixteen pairs, shows what XOR 8 does to each, and then changes one of them by writing three bytes into the palette, so that the difference between using the colours a machine wakes up with and choosing your own is visible in one program.
|
`Programs/Examples/colours.asm` does all of that in eighty lines and prints the result. It shows the sixteen pairs, shows what XOR 8 does to each, and then changes one of them by writing three bytes into the palette, so that the difference between using the colours a machine wakes up with and choosing your own is visible in one program.
|
||||||
|
|
||||||
|
**And it reaches a terminal too.** The attribute used to mean something to the screen and nothing to the serial line, so a fault printed in red was red in the window and grey down the wire. The console now says the scheme in ANSI as well, on the same terms as it says the cursor: a register write only marks it and the next character sends it, so setting a scheme and printing nothing says nothing, and setting the same scheme twice costs one sequence rather than two.
|
||||||
|
|
||||||
|
The mapping is a table rather than a translation, because **the order was already right**. Both sets enumerate a three-bit colour - red, green and blue counted in binary - so one is red in both, three is yellow in both, six is cyan in both. The same arithmetic was done twice, forty years apart. The one place they differ is slot 0, and that difference is forced: this screen is ink on black, so ink cannot be black, and slot 0 is grey where ANSI's is black.
|
||||||
|
|
||||||
|
Scheme 0 is sent as a bare reset rather than as grey on black, and a terminal is assumed to start plain - so a machine that never asks for a colour says nothing at all. **Plain text on somebody's terminal means their plain text**, and forcing grey on black would fight a terminal set up the other way round, which is the kind of thing this console exists not to do. A machine that stops with a scheme still set puts the terminal back to plain on its way out, however it stops.
|
||||||
|
|
||||||
|
What does *not* cross is the page: the attribute's other nibble says which tiles a cell draws from, which is a fact about the screen's own art and means nothing to a terminal.
|
||||||
|
|
||||||
### Moving The Cursor:
|
### Moving The Cursor:
|
||||||
|
|
||||||
Three more registers, because that is how this machine talks to everything else.
|
Three more registers, because that is how this machine talks to everything else.
|
||||||
@@ -1075,9 +1094,9 @@ Anywhere a setting asks *what modulates* something, the answer is one of these:
|
|||||||
| 3 | LFO 0. |
|
| 3 | LFO 0. |
|
||||||
| 4 | LFO 1. |
|
| 4 | LFO 1. |
|
||||||
|
|
||||||
**The two LFOs belong to the device and not to a channel**, so writing 0x60 to 0x7F ignores
|
**Each channel has its own pair of LFOs**, so writing 0x60 to 0x7F sets them on whichever
|
||||||
whichever channel is selected. That is what makes them useful: a vibrato that every voice
|
channel is selected, exactly like every parameter above. Four channels are four independent
|
||||||
shares is one wobble rather than four that drift apart.
|
instruments, and an LFO is part of how an instrument sounds.
|
||||||
|
|
||||||
### What A Byte Means:
|
### What A Byte Means:
|
||||||
|
|
||||||
@@ -1148,6 +1167,31 @@ 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
|
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.
|
can read as machine-gunny. Where variation is wanted, leave that LFO or that voice free.
|
||||||
|
|
||||||
|
### They Used To Be Shared, And The Fault That Caused:
|
||||||
|
|
||||||
|
Worth knowing, because it is the shape of bug this device is built to avoid and the manual
|
||||||
|
said the opposite for a while.
|
||||||
|
|
||||||
|
The two LFOs used to belong to the **device**, so setting one from a patch meant for channel
|
||||||
|
three changed what channel nought heard, immediately and for as long as nothing set it back.
|
||||||
|
A patch naturally carries LFO settings along with everything else, so a program that loaded
|
||||||
|
its instruments once at startup ended up with whichever of them was written *last*, for all
|
||||||
|
of them.
|
||||||
|
|
||||||
|
The failure that 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.
|
||||||
|
|
||||||
|
A program could work around the first half of that by loading each patch immediately before
|
||||||
|
the note that needed it. It could not work around the second: two sounds **overlapping** still
|
||||||
|
interfered, and no amount of care in the program could separate them. So the device was
|
||||||
|
changed rather than the programs, and an LFO now belongs to the channel it was written to.
|
||||||
|
|
||||||
|
**Loading a patch immediately before its note is still good practice**, for the different
|
||||||
|
reason that a channel used by two sounds has to be told which of them it is about to be. A
|
||||||
|
patch is forty-odd writes and costs nothing at a moment already making a sound.
|
||||||
|
|
||||||
### Knowing When It Has Finished:
|
### 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
|
The status port's bit 0 is set while any channel is still sounding, so a routine can wait for
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Everything in between is somewhere on that line.
|
|||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
Builds the five tools - and Voyager, where Raylib is installed - checks they compile under
|
Builds the six tools - and Voyager, where Raylib is installed - checks they compile under
|
||||||
strict ISO C, and runs the scripts in order. Takes a few seconds. Everything must pass; there are no expected failures at the
|
strict ISO C, and runs the scripts in order. Takes a few seconds. Everything must pass; there are no expected failures at the
|
||||||
level of the suite, only tests that record an expected failure of the assembler.
|
level of the suite, only tests that record an expected failure of the assembler.
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ level of the suite, only tests that record an expected failure of the assembler.
|
|||||||
make sanitize
|
make sanitize
|
||||||
```
|
```
|
||||||
|
|
||||||
The same suite with the five tools rebuilt under AddressSanitizer and
|
The same suite with the six tools rebuilt under AddressSanitizer and
|
||||||
UndefinedBehaviorSanitizer. See [The Sanitizer Run](#the-sanitizer-run).
|
UndefinedBehaviorSanitizer. See [The Sanitizer Run](#the-sanitizer-run).
|
||||||
|
|
||||||
Individual scripts can be run on their own, from anywhere:
|
Individual scripts can be run on their own, from anywhere:
|
||||||
@@ -125,7 +125,7 @@ from `make`, not from here.
|
|||||||
### 1. Recorded output
|
### 1. Recorded output
|
||||||
|
|
||||||
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
|
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
|
||||||
everything it printed against a file in `Tests/expected`. 211 tests, of which 149 run, 35
|
everything it printed against a file in `Tests/expected`. 219 tests, of which 157 run, 35
|
||||||
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
|
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
|
||||||
given at all.
|
given at all.
|
||||||
|
|
||||||
@@ -447,7 +447,7 @@ the console's line editing was in when it broke twice in two days.
|
|||||||
|
|
||||||
## Fixture Disks:
|
## Fixture Disks:
|
||||||
|
|
||||||
`Tests/makedisks.sh` builds 27 images with SplitDisk before anything runs, into
|
`Tests/makedisks.sh` builds 29 images with SplitDisk before anything runs, into
|
||||||
`Tests/build/disks`. **That is the point of them.** A SplitBit program reading one of these
|
`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
|
is being checked against a filesystem written by different code from the same written
|
||||||
specification, rather than against itself.
|
specification, rather than against itself.
|
||||||
@@ -540,7 +540,7 @@ which stops the markers outliving the code they were about.
|
|||||||
make sanitize
|
make sanitize
|
||||||
```
|
```
|
||||||
|
|
||||||
Rebuilds all five tools with `-fsanitize=address,undefined` and runs **the whole suite**
|
Rebuilds all six tools with `-fsanitize=address,undefined` and runs **the whole suite**
|
||||||
under them. What it reliably catches is invalid access: reads and writes off the end of an
|
under them. What it reliably catches is invalid access: reads and writes off the end of an
|
||||||
array, use after free, leaks, and arithmetic the standard does not define.
|
array, use after free, leaks, and arithmetic the standard does not define.
|
||||||
|
|
||||||
@@ -552,7 +552,7 @@ deliberate `calloc`. The machine's Program and Data memories are static arrays,
|
|||||||
sanitizers neither fill nor bound-check - which is the same fact, seen from a
|
sanitizers neither fill nor bound-check - which is the same fact, seen from a
|
||||||
different side, as the overrun blind spot below.
|
different side, as the overrun blind spot below.
|
||||||
|
|
||||||
It runs everything because it used to not. It built all five tools sanitized and then ran
|
It runs everything because it used to not. It built all six tools sanitized and then ran
|
||||||
only `run.sh` and `terminal.sh`, so SplitDisk was compiled with the sanitizers and never
|
only `run.sh` and `terminal.sh`, so SplitDisk was compiled with the sanitizers and never
|
||||||
exercised, and `native.sh` - which drives the assembler and the emulator harder than
|
exercised, and `native.sh` - which drives the assembler and the emulator harder than
|
||||||
anything else here - was skipped entirely. Those are exactly where block arithmetic on disk
|
anything else here - was skipped entirely. Those are exactly where block arithmetic on disk
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# The SplitBit Tune Manual
|
||||||
|
|
||||||
|
A tune is four voices on one clock. This is how you write one, what the compiler refuses, and what the bytes look like for anything that wants to read or write them without going through the compiler.
|
||||||
|
|
||||||
|
The player is `Play`, which comes with CosmOS. `Play` on its own plays the tune built into it; `Play <file>` plays a tune from a disk.
|
||||||
|
|
||||||
|
## What A Tune Is:
|
||||||
|
|
||||||
|
Three tables and one indirection, which is a tracker's shape.
|
||||||
|
|
||||||
|
**Instruments** are patches, designed in soundThing and converted by SoundPatch. **Sequences** are one voice's phrase: notes, rests, and commands. An **order list** names sequences, one list to a voice, and playing a tune is walking those four lists.
|
||||||
|
|
||||||
|
That last part is where repetition comes from, and it costs no notation at all. A bass line that plays under four different melodies is written once and named four times. A thirty-two bar piece that reuses four phrases is four phrases and a list.
|
||||||
|
|
||||||
|
**The four voices share the tick and nothing else.** Each keeps its own place in its own sequence and its own count of how much longer the note it is holding lasts, so a voice playing whole notes and a voice playing sixteenths cost the same and never have to know about each other. Nothing holds them together except that the sequences they play at the same position last the same number of ticks - which is the first thing the compiler checks, because it is the mistake that is hardest to hear and easiest to count.
|
||||||
|
|
||||||
|
## Writing One:
|
||||||
|
|
||||||
|
```
|
||||||
|
; Four bars, and the bass is the same one under two of them.
|
||||||
|
#Tick 0d125000 ; cycles a tick: a sixteenth note at 120 beats a minute
|
||||||
|
|
||||||
|
#Patch Oboe oboe.patch
|
||||||
|
#Patch Strings strings.patch
|
||||||
|
|
||||||
|
#Voice 0d0 Oboe ; which instrument each voice starts on
|
||||||
|
#Voice 0d1 Strings
|
||||||
|
|
||||||
|
#Sequence Verse
|
||||||
|
0d64 0d4 0d67 0d4 0d72 0d8
|
||||||
|
|
||||||
|
#Sequence Ending
|
||||||
|
#Use Strings ; a command, which takes no time at all
|
||||||
|
0d72 0d16
|
||||||
|
|
||||||
|
#Order 0d0
|
||||||
|
Verse Verse Ending
|
||||||
|
```
|
||||||
|
|
||||||
|
**`#` is a directive and `;` is a comment**, exactly as in SplitBit assembly and in CosmOS's scripts. One rule across the machine rather than a third dialect.
|
||||||
|
|
||||||
|
**Numbers are written the way the assembler writes them**: `0d` for decimal and `0x` for hexadecimal. A bare number is refused rather than guessed at, so `#Voice 0` is an error and `#Voice 0d0` is what you meant. There is one way to write a number on this machine and a tune is not the place to introduce a second.
|
||||||
|
|
||||||
|
Whitespace and line breaks mean nothing. A sequence runs until the next directive that is not `#Use`.
|
||||||
|
|
||||||
|
## The Directives:
|
||||||
|
|
||||||
|
| Directive | Means |
|
||||||
|
| --- | --- |
|
||||||
|
| #Tick \<cycles\> | How long one tick lasts, in the machine's own cycles. From `0d1` to `0d16777215`, which reaches from one cycle to sixteen and a half seconds. |
|
||||||
|
| #Patch \<name\> \<file\> | Names an instrument and where its bytes are. The file is what `SoundPatch --blob` writes. |
|
||||||
|
| #Voice \<voice\> \<patch\> | Which instrument a voice starts on. Voices are `0d0` to `0d3`. |
|
||||||
|
| #Sequence \<name\> | Begins a sequence. What follows, until the next directive, is its events. |
|
||||||
|
| #Use \<patch\> | Inside a sequence: play the rest of this voice on that instrument. Takes no time. |
|
||||||
|
| #Order \<voice\> | Begins a voice's order list. What follows, until the next directive, is sequence names. |
|
||||||
|
|
||||||
|
## Inside A Sequence:
|
||||||
|
|
||||||
|
Pairs: what to play, then how many ticks it lasts.
|
||||||
|
|
||||||
|
| Written | Means |
|
||||||
|
| --- | --- |
|
||||||
|
| 0d1 to 0d127 | A MIDI note. 60 is middle C and every 12 is an octave. |
|
||||||
|
| 0d0 | A rest. The ticks pass with nothing sounding. |
|
||||||
|
| #Use \<patch\> | A command. It happens between notes and consumes no tick. |
|
||||||
|
|
||||||
|
A duration is from `0d1` to `0d255` ticks.
|
||||||
|
|
||||||
|
**A note's duration is its whole life.** The gate goes down when the count runs out and the next event begins on the same tick, so a gap between two notes is *written*, as a rest, rather than invented by the player out of some fraction it decided on.
|
||||||
|
|
||||||
|
**A patch change reshapes whatever is still ringing on that voice.** Nothing can be done about that: a channel has one set of parameters and a note in its release is using them, so deferring the change to the next note would reshape the same tail. It is a fact about the sound device rather than a choice about the format. If it matters, leave a rest long enough for the release - which is the composer's to write and not the player's to guess.
|
||||||
|
|
||||||
|
## The Tick, And Why It Is In Cycles:
|
||||||
|
|
||||||
|
Cycles are what everything else on this machine is counted in: it is what the cost model counts and what a frame is measured in, so a tick counting anything else would be a second unit to remember.
|
||||||
|
|
||||||
|
At a megahertz, a beat at 120 beats a minute is 500,000 cycles. A sixteenth note is 125,000, which is what the examples here use.
|
||||||
|
|
||||||
|
**Choose a tick that divides what you want to write.** The tick is the smallest subdivision in the piece, because it is the only unit four parts can agree on. A piece with triplets wants a tick that divides by three; a piece with long held notes wants a coarser one, since a duration only reaches 255.
|
||||||
|
|
||||||
|
Before the timer existed this was not a choice. A screen frame is 16,667 cycles, so a sixteenth note at 120 was seven and a half frames and could not be asked for at all, and `Examples/tune.asm` wrote its arpeggio as seven whole frames - six and a half per cent fast.
|
||||||
|
|
||||||
|
## What The Compiler Refuses:
|
||||||
|
|
||||||
|
Everything here is something the player cannot notice for itself, which is why the compiler is the only place it can be caught.
|
||||||
|
|
||||||
|
| Refused | Because |
|
||||||
|
| --- | --- |
|
||||||
|
| Sequences at one position lasting different lengths | The voices would come apart, quietly, some bars after the mistake. The machine has no lengths and cannot tell you. |
|
||||||
|
| A voice with a part and no instrument | By the time a tune is loaded, "never said" and "instrument nought" are the same byte. Only the compiler ever sees the absence. |
|
||||||
|
| A duration of `0d0` | The player counts a duration down and reads the next event at nought, so a count starting there wraps to 255 and holds. It sounds like a hang rather than a mistake. |
|
||||||
|
| A name that is not there | The machine has no names in it at all. |
|
||||||
|
| A bare number | There is one way to write a number on this machine. |
|
||||||
|
| A patch file that is not a patch | A patch is a count and that many pairs. Anything else would write rubbish at the sound device. |
|
||||||
|
|
||||||
|
Nothing is written when anything is refused, so a failed build cannot leave a half-made tune on the disk.
|
||||||
|
|
||||||
|
## Building One:
|
||||||
|
|
||||||
|
```
|
||||||
|
./SoundPatch --blob Programs/Sounds/Oboe.json Oboe oboe.patch
|
||||||
|
./TuneC [-I <dir>] theme.score theme.tune
|
||||||
|
./SplitDisk put mydisk.img theme.tune
|
||||||
|
```
|
||||||
|
|
||||||
|
In this repository, `Programs/Tunes` holds the scores and the build compiles every one of them onto the disk - the source mirrored to `/Source/Tunes` with everything else that was written, and the compiled tune at the root, where a program looking for one expects to find it.
|
||||||
|
|
||||||
|
**A `.score` is written and a `.tune` is what the machine reads**, the same way a `.asm` is written and a `.sbx` or a `.bin` is what it loads. One is a source and the other is bytes, and giving them different names means never wondering which a file is - or handing a player a source and being told, correctly, that it is not a tune.
|
||||||
|
|
||||||
|
Then `Play theme.tune` on the machine.
|
||||||
|
|
||||||
|
Patches are looked for beside the tune first and then on the `-I` path, the way the assembler looks for an include - so a tune can name its instruments by bare name and travel with them.
|
||||||
|
|
||||||
|
**SoundPatch is the only thing that understands soundThing's JSON.** TuneC embeds the bytes it is given and knows nothing about what a patch means. A second program parsing that format would be a second opinion about it, and the seam between two opinions is where a sound bug can live for a fortnight.
|
||||||
|
|
||||||
|
## Limits:
|
||||||
|
|
||||||
|
| Thing | How many |
|
||||||
|
| --- | --- |
|
||||||
|
| Voices | 4 |
|
||||||
|
| Patches | 64 |
|
||||||
|
| Sequences | 200. The ceiling is 255, because 0xFF ends an order list. |
|
||||||
|
| Events in a sequence | 1024 bytes of them |
|
||||||
|
| Entries in an order list | 256 |
|
||||||
|
| Ticks in a duration | 255 |
|
||||||
|
| Cycles in a tick | 16,777,215 |
|
||||||
|
|
||||||
|
## The Bytes:
|
||||||
|
|
||||||
|
For anything that wants to read or write a tune without going through the compiler. `Tests/maketune.py` does exactly that, and the suite checks that it and TuneC agree byte for byte - two implementations of one format, which is the same discipline SplitDisk and `sbfs.asm` are held to, and for the same reason: either alone is only self-consistent.
|
||||||
|
|
||||||
|
| Offset | Size | Holds |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 0 | 4 | `SBTU` |
|
||||||
|
| 4 | 1 | Version, 1 |
|
||||||
|
| 5 | 3 | The tick in cycles, most significant byte first |
|
||||||
|
| 8 | 1 | How many patches |
|
||||||
|
| 9 | 1 | How many sequences |
|
||||||
|
| 10 | 2 | Offset of the patch table |
|
||||||
|
| 12 | 2 | Offset of the sequence table |
|
||||||
|
| 14 | 8 | Offset of each voice's order list, four of them |
|
||||||
|
| 22 | 4 | The patch index each voice starts on, four of them |
|
||||||
|
| 26 | 2 | Reserved, zero |
|
||||||
|
|
||||||
|
The **patch table** is one two-byte offset per patch, each naming a count and that many parameter and value pairs. The **sequence table** is one two-byte offset per sequence. An **order list** is one-byte sequence indices ending in `0xFF`. A **sequence** is events, ending in `0xFF`, where 1 to 127 is a note and 0 is a rest - each followed by a duration - and 0x80 is a command followed by one byte of patch index.
|
||||||
|
|
||||||
|
**Everything in a tune is an offset from its first byte**, so loading one is adding the load address to the two tables and pointing four voices at their order lists. No sequence is walked and nothing inside one is an address, which is what makes a malformed tune something that plays wrongly rather than something that takes the loader with it. The magic is checked first, because from there on the loader follows what the offsets name.
|
||||||
|
|
||||||
|
**An offset of zero means a voice has no part.** That is safe here for a reason worth stating, because the same trick was got wrong once: offset zero is where the magic lives, so no structure can ever begin there. It is impossible by construction rather than by a fact about one program's base address - which is what an earlier version of the order list terminator relied on, and why a boot image based at zero read its own first sequence as the end of the list and played silence.
|
||||||
+17
-5
@@ -104,12 +104,24 @@ for suite in "$@"; do
|
|||||||
die "no such suite: $suite"
|
die "no such suite: $suite"
|
||||||
fi
|
fi
|
||||||
printf -- '---- %s ----\n' "$name"
|
printf -- '---- %s ----\n' "$name"
|
||||||
if "$run" 2>&1 | tee "$KEEP.out" | grep -E '^\s*\[FAIL\]' | sed 's/^ *//'; then
|
"$run" > "$KEEP.out" 2>&1
|
||||||
:
|
status=$?
|
||||||
fi
|
grep -E '^\s*\[FAIL\]' "$KEEP.out" | sed 's/^ *//'
|
||||||
if grep -qE '^[0-9]+ passed, [0-9]+ failed' "$KEEP.out"; then
|
|
||||||
|
# THE SUITE'S EXIT STATUS IS THE SIGNAL, not the shape of its last line. This used to
|
||||||
|
# look for a "N passed, M failed" summary, which is right for the suites that print one
|
||||||
|
# - they print it only when something failed - and impossible for the three that never
|
||||||
|
# do. docs, terminal and voyager report in their own words and say so with their exit
|
||||||
|
# status, so a break they caught loudly was answered with "NOTHING CAUGHT THE BREAK":
|
||||||
|
# the one wrong answer this tool exists never to give, turning up in a third place.
|
||||||
|
if [ "$status" -ne 0 ]; then
|
||||||
NOTICED=1
|
NOTICED=1
|
||||||
grep -E '^[0-9]+ passed, [0-9]+ failed' "$KEEP.out" | tail -1
|
# The count where a suite keeps one, and its own last word where it does not.
|
||||||
|
if grep -qE '^[0-9]+ passed, [0-9]+ failed' "$KEEP.out"; then
|
||||||
|
grep -E '^[0-9]+ passed, [0-9]+ failed' "$KEEP.out" | tail -1
|
||||||
|
else
|
||||||
|
tail -1 "$KEEP.out"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
printf '%s%s did not notice%s\n' "$RED" "$name" "$RESET"
|
printf '%s%s did not notice%s\n' "$RED" "$name" "$RESET"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+210
@@ -172,6 +172,40 @@ else:
|
|||||||
problems.append("%s (0x%02X) is a device class and has no row in the Devices"
|
problems.append("%s (0x%02X) is a device class and has no row in the Devices"
|
||||||
" table" % (name, value))
|
" table" % (name, value))
|
||||||
|
|
||||||
|
# ---- Every option the assembler takes is written down in both places ----
|
||||||
|
#
|
||||||
|
# An option added to getopt is an option nobody knows about until it is said twice: in the
|
||||||
|
# manual's table, and in the usage the assembler prints when it is asked for help or refuses
|
||||||
|
# a command line. -S arrived with neither, and the damage was worse than an undocumented
|
||||||
|
# switch. The usage listed a bare "-S <file>" with no long name and no statement that the
|
||||||
|
# file is one it WRITES, so it read as "dump the symbols of <file>" - which hands the source
|
||||||
|
# to -S, leaves nothing positional behind it, and gets answered with "No source file
|
||||||
|
# specified" on a command line that plainly names one.
|
||||||
|
#
|
||||||
|
# The option table is the source of truth because it is what getopt_long is actually given.
|
||||||
|
assembler = read("Source/Assembler/Assembler.c")
|
||||||
|
options = re.findall(r'^\s*\{"([a-z]+)",\s*\w+,\s*0,\s*\'(\w)\'\s*\},', assembler, re.M)
|
||||||
|
if not options:
|
||||||
|
problems.append("could not find the assembler's option table")
|
||||||
|
elif "void printUsage" not in assembler:
|
||||||
|
problems.append("the assembler has lost its printUsage")
|
||||||
|
elif "## Running the Assembler:" not in am:
|
||||||
|
problems.append("the Assembler Manual has lost its options table")
|
||||||
|
else:
|
||||||
|
usage = assembler.split("void printUsage")[1].split("\n}")[0]
|
||||||
|
section = am.split("## Running the Assembler:")[1].split("\n## ")[0]
|
||||||
|
# Only the table rows count, so a switch merely mentioned in the prose below it does not
|
||||||
|
# pass for a documented one.
|
||||||
|
rows = re.findall(r'^\| (-\w, --[a-z]+)', section, re.M)
|
||||||
|
for longName, shortName in options:
|
||||||
|
spelled = "-%s, --%s" % (shortName, longName)
|
||||||
|
if spelled not in rows:
|
||||||
|
problems.append("the assembler takes %s and the Assembler Manual's option table"
|
||||||
|
" has no row for it" % spelled)
|
||||||
|
if spelled not in usage:
|
||||||
|
problems.append("the assembler takes %s and its own usage message does not name"
|
||||||
|
" it" % spelled)
|
||||||
|
|
||||||
# ---- The vector ranges the manuals quote are the ones the assembler uses ----
|
# ---- The vector ranges the manuals quote are the ones the assembler uses ----
|
||||||
#
|
#
|
||||||
# Both manuals print the boundary between numbers a program may pin and numbers the
|
# Both manuals print the boundary between numbers a program may pin and numbers the
|
||||||
@@ -786,6 +820,182 @@ for line in readme.split("\n"):
|
|||||||
problems.append("the CosmOS README says %s is %s bytes and it is %s"
|
problems.append("the CosmOS README says %s is %s bytes and it is %s"
|
||||||
% (named[0], said.group(1), real.group(1)))
|
% (named[0], said.group(1), real.group(1)))
|
||||||
|
|
||||||
|
# ---- The symbol file has the fields the manual says it has ----
|
||||||
|
#
|
||||||
|
# The manual describes -S field by field, and a program that reads a symbol file is written
|
||||||
|
# against that description rather than against the assembler. So a field reordered, renamed
|
||||||
|
# or added would leave every such program cutting the wrong column, and the manual would go
|
||||||
|
# on describing a format nobody writes.
|
||||||
|
#
|
||||||
|
# The values are not checked, because an example naming real addresses in real files goes
|
||||||
|
# stale the moment either changes and would turn every edit to a program into an edit to the
|
||||||
|
# manual. The SHAPE is the contract: five fields, tab separated, in the documented order.
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
if "## Running the Assembler:" not in am:
|
||||||
|
pass # Already reported above.
|
||||||
|
else:
|
||||||
|
section = am.split("## Running the Assembler:")[1].split("\n## ")[0]
|
||||||
|
said = [row.split(" |")[0] for row in re.findall(r'^\| (\w+ \|.*)$', section, re.M)
|
||||||
|
if row.split(" |")[0] in ("Kind", "Number", "Address", "Name", "File", "Line")]
|
||||||
|
if said != ["Kind", "Number", "Address", "Name", "File", "Line"]:
|
||||||
|
problems.append("the Assembler Manual does not describe the symbol file's six"
|
||||||
|
" fields in order; it lists %s" % (said or "none"))
|
||||||
|
else:
|
||||||
|
# Two programs, because no one of them covers everything the format claims.
|
||||||
|
#
|
||||||
|
# Keys is a small LOADABLE program, where the two segments are based apart, and
|
||||||
|
# the smallest thing that produces all four kinds at once: labels in both
|
||||||
|
# memories, vectors it implements, vectors it only declares, and a device.
|
||||||
|
#
|
||||||
|
# cosmos is a BOOT IMAGE, where both segments start at zero, which is the case
|
||||||
|
# the kind field exists for - and it is the one that holds strings with newlines
|
||||||
|
# inside them, which is what the line numbers below go wrong on.
|
||||||
|
#
|
||||||
|
# Both assemble in a few thousandths of a second, so there is nothing to save by
|
||||||
|
# checking only one.
|
||||||
|
for program in ["Programs/CosmOS/Apps/Keys.asm",
|
||||||
|
"Programs/CosmOS/Source/cosmos.asm"]:
|
||||||
|
with tempfile.TemporaryDirectory() as scratch:
|
||||||
|
dump = os.path.join(scratch, "symbols")
|
||||||
|
built = subprocess.run(["./Assembler", "-I", "Programs/Libraries",
|
||||||
|
"-I", "Programs/CosmOS/Source", "-S", dump,
|
||||||
|
"-o", os.path.join(scratch, "out"),
|
||||||
|
program],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
if built.returncode != 0:
|
||||||
|
problems.append("could not assemble %s to check the symbol file" % program)
|
||||||
|
else:
|
||||||
|
kinds = set()
|
||||||
|
for number, line in enumerate(open(dump).read().splitlines(), 1):
|
||||||
|
fields = line.split("\t")
|
||||||
|
if len(fields) != 6:
|
||||||
|
problems.append("line %d of a symbol file has %d fields and the"
|
||||||
|
" Assembler Manual describes six"
|
||||||
|
% (number, len(fields)))
|
||||||
|
break
|
||||||
|
kind, index, address, name, source, where = fields
|
||||||
|
kinds.add(kind)
|
||||||
|
if kind not in ("Program", "Data", "Vector", "Device"):
|
||||||
|
problems.append("a symbol file calls something a %r, and the"
|
||||||
|
" Assembler Manual allows Program, Data, Vector"
|
||||||
|
" and Device" % kind)
|
||||||
|
break
|
||||||
|
if not re.fullmatch(r'[0-9A-F]{4}', address):
|
||||||
|
problems.append("a symbol file gives %r as an address, and the"
|
||||||
|
" Assembler Manual says four hexadecimal digits"
|
||||||
|
% address)
|
||||||
|
break
|
||||||
|
if not name or not source or not where.isdigit():
|
||||||
|
problems.append("a symbol file line is missing a name, a file or a"
|
||||||
|
" line number: %r" % line)
|
||||||
|
break
|
||||||
|
# A label has no number and says so with a dash; a vector has one, and
|
||||||
|
# the address it sits at has to be the slot that number works out to,
|
||||||
|
# which is the claim the manual makes about both fields at once.
|
||||||
|
if kind in ("Program", "Data"):
|
||||||
|
if index != "-":
|
||||||
|
problems.append("a %s label was given the number %r, and the"
|
||||||
|
" Assembler Manual says a label has none"
|
||||||
|
% (kind, index))
|
||||||
|
break
|
||||||
|
elif not index.isdigit():
|
||||||
|
problems.append("a %s row has %r where its number should be"
|
||||||
|
% (kind, index))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
base = 0xFC00 if kind == "Vector" else 0xFE00
|
||||||
|
if int(address, 16) != base + int(index) * 2:
|
||||||
|
problems.append("%s is number %s and the Assembler Manual puts"
|
||||||
|
" that at 0x%04X, but the symbol file says %s"
|
||||||
|
% (name, index, base + int(index) * 2, address))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Only meaningful if every line was well formed, which is what the else
|
||||||
|
# on the loop says.
|
||||||
|
for wanted in ("Program", "Data", "Vector", "Device"):
|
||||||
|
if wanted not in kinds:
|
||||||
|
problems.append("a program with labels in both memories and a"
|
||||||
|
" Vector Segment produced a symbol file with no"
|
||||||
|
" %s row" % wanted)
|
||||||
|
# ---- And the line really is the line ----
|
||||||
|
#
|
||||||
|
# "Which line of that file, counting from one" is a claim, and it was
|
||||||
|
# wrong for years without anything noticing: a string literal with a
|
||||||
|
# newline inside it was read as one token and the newline was never
|
||||||
|
# counted, so every line number after one was short by one. cosmos.asm
|
||||||
|
# has thirteen, and its last label was reported thirteen lines early -
|
||||||
|
# which had been sending every error message after that point at
|
||||||
|
# somebody else's code, not only the symbol file.
|
||||||
|
#
|
||||||
|
# A name has to actually appear on the line its row names. That is a
|
||||||
|
# weak test of a right answer and a very strong test of a wrong one:
|
||||||
|
# drift lands on a line that has nothing to do with the name.
|
||||||
|
sources = {}
|
||||||
|
for row in open(dump).read().splitlines():
|
||||||
|
kind, index, address, name, source, where = row.split("\t")
|
||||||
|
if source not in sources:
|
||||||
|
try:
|
||||||
|
sources[source] = open(source).read().splitlines()
|
||||||
|
except OSError:
|
||||||
|
sources[source] = None
|
||||||
|
lines = sources[source]
|
||||||
|
if lines is None:
|
||||||
|
problems.append("a symbol file names %r, which cannot be read"
|
||||||
|
% source)
|
||||||
|
break
|
||||||
|
at = int(where)
|
||||||
|
if at < 1 or at > len(lines):
|
||||||
|
problems.append("%s is said to be on line %d of %s, which has"
|
||||||
|
" %d lines" % (name, at, source, len(lines)))
|
||||||
|
break
|
||||||
|
if not re.search(r'\b%s\b' % re.escape(name), lines[at - 1]):
|
||||||
|
problems.append("%s is said to be on line %d of %s, and that"
|
||||||
|
" line is %r"
|
||||||
|
% (name, at, source, lines[at - 1].strip()))
|
||||||
|
break
|
||||||
|
|
||||||
|
# ---- Every directive TuneC takes has a row in the Tune Manual ----
|
||||||
|
#
|
||||||
|
# The same check the assembler's options get, for the same reason: a directive added to the
|
||||||
|
# compiler and not written down is one nobody can use, and one written down and removed is
|
||||||
|
# worse - it is an instruction that fails.
|
||||||
|
#
|
||||||
|
# The manual also carries the limits, which are #defines and exactly the sort of number that
|
||||||
|
# goes stale silently: raising one leaves a sentence looking perfectly reasonable and wrong.
|
||||||
|
tunec = read("Source/Tune/TuneC.c")
|
||||||
|
tm = read("SplitBit Tune Manual.md")
|
||||||
|
|
||||||
|
directives = sorted(set(re.findall(r'strcmp\(t->text, "(#\w+)"\)', tunec)) |
|
||||||
|
set(re.findall(r'strcmp\(t->text, "(#Use)"\)', tunec)))
|
||||||
|
if not directives:
|
||||||
|
problems.append("could not find the directives TuneC understands")
|
||||||
|
else:
|
||||||
|
for directive in directives:
|
||||||
|
if not re.search(r'^\| %s[ \\]' % re.escape(directive), tm, re.M):
|
||||||
|
problems.append("TuneC takes %s and the Tune Manual has no row for it" % directive)
|
||||||
|
|
||||||
|
limits = {name: int(value)
|
||||||
|
for name, value in re.findall(r'^#define (MAX_\w+|VOICES)\s+(\d+)', tunec, re.M)}
|
||||||
|
said = {
|
||||||
|
"VOICES": r'^\| Voices \| (\d+) \|',
|
||||||
|
"MAX_PATCHES": r'^\| Patches \| (\d+) \|',
|
||||||
|
"MAX_SEQUENCES": r'^\| Sequences \| (\d+)\.',
|
||||||
|
"MAX_EVENTS": r'^\| Events in a sequence \| (\d+) ',
|
||||||
|
"MAX_ORDER": r'^\| Entries in an order list \| (\d+) \|',
|
||||||
|
}
|
||||||
|
for name, pattern in said.items():
|
||||||
|
if name not in limits:
|
||||||
|
problems.append("TuneC no longer defines %s, which the Tune Manual quotes" % name)
|
||||||
|
continue
|
||||||
|
m = re.search(pattern, tm, re.M)
|
||||||
|
if not m:
|
||||||
|
problems.append("the Tune Manual has lost its %s limit" % name)
|
||||||
|
elif int(m.group(1)) != limits[name]:
|
||||||
|
problems.append("the Tune Manual says %s is %s and TuneC says %d"
|
||||||
|
% (name, m.group(1), limits[name]))
|
||||||
|
|
||||||
if problems:
|
if problems:
|
||||||
print("The manuals and the code disagree:")
|
print("The manuals and the code disagree:")
|
||||||
for p in problems:
|
for p in problems:
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ a file kept by asking
|
|||||||
renamed it
|
renamed it
|
||||||
deleted it
|
deleted it
|
||||||
and it is gone
|
and it is gone
|
||||||
finished
|
|
||||||
> mkdir Notes
|
> mkdir Notes
|
||||||
made
|
made
|
||||||
> cd Notes
|
> cd Notes
|
||||||
/Notes> dir
|
/Notes> dir
|
||||||
0 files
|
0 files
|
||||||
|
8 of 32 entries, 307 blocks free
|
||||||
/Notes> exit
|
/Notes> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
scheme 0
|
||||||
|
[0;31mscheme 1
|
||||||
|
[0;32mscheme 2
|
||||||
|
[0;33mscheme 3
|
||||||
|
[0;34mscheme 4
|
||||||
|
[0;35mscheme 5
|
||||||
|
[0;36mscheme 6
|
||||||
|
[0;97mscheme 7
|
||||||
|
[0;47;30mscheme 8
|
||||||
|
[0;41;30mscheme 9
|
||||||
|
[0;42;30mscheme A
|
||||||
|
[0;43;30mscheme B
|
||||||
|
[0;44;30mscheme C
|
||||||
|
[0;45;30mscheme D
|
||||||
|
[0;46;30mscheme E
|
||||||
|
[0;107;30mscheme F
|
||||||
|
Execution halted.
|
||||||
|
[0m[exit 0]
|
||||||
+10
-10
@@ -1,12 +1,12 @@
|
|||||||
ordinary highlighted
|
ordinary [0;47;30m highlighted [0m
|
||||||
ordinary highlighted
|
[0;31m ordinary [0;41;30m highlighted [0m
|
||||||
ordinary highlighted
|
[0;32m ordinary [0;42;30m highlighted [0m
|
||||||
ordinary highlighted
|
[0;33m ordinary [0;43;30m highlighted [0m
|
||||||
ordinary highlighted
|
[0;34m ordinary [0;44;30m highlighted [0m
|
||||||
ordinary highlighted
|
[0;35m ordinary [0;45;30m highlighted [0m
|
||||||
ordinary highlighted
|
[0;36m ordinary [0;46;30m highlighted [0m
|
||||||
ordinary highlighted
|
[0;97m ordinary [0;107;30m highlighted [0m
|
||||||
|
[0;32m
|
||||||
bank 2's ink is orange now, because this program said so
|
bank 2's ink is orange now, because this program said so
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[0m[exit 0]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ run [words] start what was loaded, and tell it those words
|
|||||||
cd [path] go to a directory, or to the root with nothing after it
|
cd [path] go to a directory, or to the root with nothing after it
|
||||||
mkdir <path> make a directory
|
mkdir <path> make a directory
|
||||||
rmdir <path> remove an empty one
|
rmdir <path> remove an empty one
|
||||||
do <file> run the lines in a file, which must start with #!
|
do <file> [words] run the lines in a file, which must start with #!
|
||||||
echo [words] say them
|
echo [words] say them
|
||||||
clear empty the screen
|
clear empty the screen
|
||||||
delete <file> take it off the disk
|
delete <file> take it off the disk
|
||||||
@@ -29,6 +29,7 @@ across.txt 700
|
|||||||
empty.txt 0
|
empty.txt 0
|
||||||
aName22CharactersLong! 22
|
aName22CharactersLong! 22
|
||||||
12 files
|
12 files
|
||||||
|
12 of 16 entries, 48 blocks free
|
||||||
>
|
>
|
||||||
> frobnicate
|
> frobnicate
|
||||||
I do not know: frobnicate
|
I do not know: frobnicate
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ A 44 B 55 Q 00 status 00
|
|||||||
DP0 3000 DP1 3037 DP2 3030 DP3 5000 SP FFF5
|
DP0 3000 DP1 3037 DP2 3030 DP3 5000 SP FFF5
|
||||||
press a key
|
press a key
|
||||||
carried on to the end
|
carried on to the end
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> dir
|
> dir
|
||||||
0 files
|
0 files
|
||||||
|
0 of 16 entries, 125 blocks free
|
||||||
> mkdir Apps
|
> mkdir Apps
|
||||||
made
|
made
|
||||||
> mkdir Apps/Deep
|
> mkdir Apps/Deep
|
||||||
@@ -11,10 +12,12 @@ made
|
|||||||
Apps <dir>
|
Apps <dir>
|
||||||
Notes <dir>
|
Notes <dir>
|
||||||
0 files, 2 directories
|
0 files, 2 directories
|
||||||
|
3 of 16 entries, 125 blocks free
|
||||||
> cd Apps
|
> cd Apps
|
||||||
/Apps> dir
|
/Apps> dir
|
||||||
Deep <dir>
|
Deep <dir>
|
||||||
0 files, 1 directory
|
0 files, 1 directory
|
||||||
|
3 of 16 entries, 125 blocks free
|
||||||
/Apps> mkdir Deep
|
/Apps> mkdir Deep
|
||||||
cannot make that: check the path, the name, and whether it is taken
|
cannot make that: check the path, the name, and whether it is taken
|
||||||
/Apps> mkdir /Notes/Deep
|
/Apps> mkdir /Notes/Deep
|
||||||
@@ -23,6 +26,7 @@ made
|
|||||||
/Notes> dir
|
/Notes> dir
|
||||||
Deep <dir>
|
Deep <dir>
|
||||||
0 files, 1 directory
|
0 files, 1 directory
|
||||||
|
4 of 16 entries, 125 blocks free
|
||||||
/Notes> rmdir /Apps
|
/Notes> rmdir /Apps
|
||||||
cannot remove that: it must be a directory, and empty
|
cannot remove that: it must be a directory, and empty
|
||||||
/Notes> rmdir /Apps/Deep
|
/Notes> rmdir /Apps/Deep
|
||||||
@@ -44,6 +48,7 @@ removed
|
|||||||
removed
|
removed
|
||||||
> dir
|
> dir
|
||||||
0 files
|
0 files
|
||||||
|
0 of 16 entries, 125 blocks free
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ CosmOS
|
|||||||
> Claim
|
> Claim
|
||||||
claiming more than was reserved was refused
|
claiming more than was reserved was refused
|
||||||
and the size it really came to was taken
|
and the size it really came to was taken
|
||||||
finished
|
|
||||||
> dir
|
> dir
|
||||||
Claim.sbx 580
|
Claim.sbx 580
|
||||||
claim.dat 266
|
claim.dat 266
|
||||||
2 files
|
2 files
|
||||||
|
2 of 16 entries, 56 blocks free
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -1,43 +1,30 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> Copy /Input/empty.dat /Output/empty.dat
|
> Copy /Input/empty.dat /Output/empty.dat
|
||||||
copied
|
copied
|
||||||
finished
|
|
||||||
> Compare /Input/empty.dat /Output/empty.dat
|
> Compare /Input/empty.dat /Output/empty.dat
|
||||||
the same
|
the same
|
||||||
finished
|
|
||||||
> Copy /Input/exact.dat /Output/exact.dat
|
> Copy /Input/exact.dat /Output/exact.dat
|
||||||
copied
|
copied
|
||||||
finished
|
|
||||||
> Compare /Input/exact.dat /Output/exact.dat
|
> Compare /Input/exact.dat /Output/exact.dat
|
||||||
the same
|
the same
|
||||||
finished
|
|
||||||
> Copy /Input/tail.dat /Output/tail.dat
|
> Copy /Input/tail.dat /Output/tail.dat
|
||||||
copied
|
copied
|
||||||
finished
|
|
||||||
> Compare /Input/tail.dat /Output/tail.dat
|
> Compare /Input/tail.dat /Output/tail.dat
|
||||||
the same
|
the same
|
||||||
finished
|
|
||||||
> Copy /Input/large.dat /Output/large.dat
|
> Copy /Input/large.dat /Output/large.dat
|
||||||
copied
|
copied
|
||||||
finished
|
|
||||||
> Compare /Input/large.dat /Output/large.dat
|
> Compare /Input/large.dat /Output/large.dat
|
||||||
the same
|
the same
|
||||||
finished
|
|
||||||
> Compare /Input/large.dat /Input/different.dat
|
> Compare /Input/large.dat /Input/different.dat
|
||||||
different
|
different
|
||||||
finished
|
|
||||||
> Copy /Input/missing.dat /Output/missing.dat
|
> Copy /Input/missing.dat /Output/missing.dat
|
||||||
copy: cannot find the source
|
copy: cannot find the source
|
||||||
finished
|
|
||||||
> Compare /Input/large.dat /Input/missing.dat
|
> Compare /Input/large.dat /Input/missing.dat
|
||||||
compare: cannot find the second file
|
compare: cannot find the second file
|
||||||
finished
|
|
||||||
> Copy
|
> Copy
|
||||||
copy: give me a source and destination
|
copy: give me a source and destination
|
||||||
finished
|
|
||||||
> Compare /Input/large.dat
|
> Compare /Input/large.dat
|
||||||
compare: give me two files
|
compare: give me two files
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ CosmOS
|
|||||||
> drive 1
|
> drive 1
|
||||||
> Copy 1:/twoblocks.txt 0:/crossed.txt
|
> Copy 1:/twoblocks.txt 0:/crossed.txt
|
||||||
copied
|
copied
|
||||||
finished
|
|
||||||
> drive
|
> drive
|
||||||
1
|
1
|
||||||
> Say from drive one
|
> Say from drive one
|
||||||
it says: from drive one
|
it says: from drive one
|
||||||
finished
|
|
||||||
> drive
|
> drive
|
||||||
1
|
1
|
||||||
> cd 0:/
|
> cd 0:/
|
||||||
@@ -18,6 +16,7 @@ Life.sbx 1396
|
|||||||
Snake.sbx 2164
|
Snake.sbx 2164
|
||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
|
Where.sbx 827
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 571
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
@@ -25,13 +24,14 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Packages <dir>
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
blocks.script 343
|
blocks.script 343
|
||||||
loops.script 272
|
loops.script 272
|
||||||
tune.sbx 306
|
tune.sbx 318
|
||||||
|
Play.sbx 2526
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
Apps <dir>
|
Apps <dir>
|
||||||
hi.script 121
|
hi.script 121
|
||||||
@@ -42,8 +42,14 @@ nonl.script 38
|
|||||||
outer.script 376
|
outer.script 376
|
||||||
inner.script 44
|
inner.script 44
|
||||||
loop.script 35
|
loop.script 35
|
||||||
|
hush.script 42
|
||||||
|
aloud.script 59
|
||||||
|
args.script 64
|
||||||
|
pass.script 20
|
||||||
|
holds.script 87
|
||||||
crossed.txt 560
|
crossed.txt 560
|
||||||
30 files, 1 directory
|
36 files, 2 directories
|
||||||
|
48 of 64 entries, 1886 blocks free
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -4,42 +4,38 @@ Apps <dir>
|
|||||||
A <dir>
|
A <dir>
|
||||||
B <dir>
|
B <dir>
|
||||||
0 files, 3 directories
|
0 files, 3 directories
|
||||||
|
9 of 32 entries, 240 blocks free
|
||||||
> cd /A
|
> cd /A
|
||||||
/A> dir
|
/A> dir
|
||||||
Say.sbx 53
|
Say.sbx 53
|
||||||
notes.txt 25
|
notes.txt 25
|
||||||
2 files
|
2 files
|
||||||
|
9 of 32 entries, 240 blocks free
|
||||||
/A> Type notes.txt
|
/A> Type notes.txt
|
||||||
these are the notes in A
|
these are the notes in A
|
||||||
finished
|
|
||||||
/A> cd /B
|
/A> cd /B
|
||||||
/B> Type notes.txt
|
/B> Type notes.txt
|
||||||
and these are the very different notes in B
|
and these are the very different notes in B
|
||||||
finished
|
|
||||||
/B> Say reached the one in Apps
|
/B> Say reached the one in Apps
|
||||||
it says: reached the one in Apps
|
it says: reached the one in Apps
|
||||||
finished
|
|
||||||
/B> cd /A
|
/B> cd /A
|
||||||
/A> Say reached the one in A
|
/A> Say reached the one in A
|
||||||
Hello, World!
|
Hello, World!
|
||||||
finished
|
|
||||||
/A> cd ..
|
/A> cd ..
|
||||||
> dir
|
> dir
|
||||||
Apps <dir>
|
Apps <dir>
|
||||||
A <dir>
|
A <dir>
|
||||||
B <dir>
|
B <dir>
|
||||||
0 files, 3 directories
|
0 files, 3 directories
|
||||||
|
9 of 32 entries, 240 blocks free
|
||||||
> cd /B
|
> cd /B
|
||||||
/B> Type ../A/notes.txt
|
/B> Type ../A/notes.txt
|
||||||
these are the notes in A
|
these are the notes in A
|
||||||
finished
|
|
||||||
/B> Wander /A
|
/B> Wander /A
|
||||||
moved, and reading a bare name from there:
|
moved, and reading a bare name from there:
|
||||||
these are the notes in A
|
these are the notes in A
|
||||||
finished
|
|
||||||
/B> Type notes.txt
|
/B> Type notes.txt
|
||||||
and these are the very different notes in B
|
and these are the very different notes in B
|
||||||
finished
|
|
||||||
/B> cd nosuchplace
|
/B> cd nosuchplace
|
||||||
no such file
|
no such file
|
||||||
/B> cd notes.txt
|
/B> cd notes.txt
|
||||||
@@ -50,6 +46,7 @@ Apps <dir>
|
|||||||
A <dir>
|
A <dir>
|
||||||
B <dir>
|
B <dir>
|
||||||
0 files, 3 directories
|
0 files, 3 directories
|
||||||
|
9 of 32 entries, 240 blocks free
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ run [words] start what was loaded, and tell it those words
|
|||||||
cd [path] go to a directory, or to the root with nothing after it
|
cd [path] go to a directory, or to the root with nothing after it
|
||||||
mkdir <path> make a directory
|
mkdir <path> make a directory
|
||||||
rmdir <path> remove an empty one
|
rmdir <path> remove an empty one
|
||||||
do <file> run the lines in a file, which must start with #!
|
do <file> [words] run the lines in a file, which must start with #!
|
||||||
echo [words] say them
|
echo [words] say them
|
||||||
clear empty the screen
|
clear empty the screen
|
||||||
delete <file> take it off the disk
|
delete <file> take it off the disk
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Life.sbx 1396
|
|||||||
Snake.sbx 2164
|
Snake.sbx 2164
|
||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
|
Where.sbx 827
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 571
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
@@ -15,13 +16,14 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Packages <dir>
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
blocks.script 343
|
blocks.script 343
|
||||||
loops.script 272
|
loops.script 272
|
||||||
tune.sbx 306
|
tune.sbx 318
|
||||||
|
Play.sbx 2526
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
Apps <dir>
|
Apps <dir>
|
||||||
hi.script 121
|
hi.script 121
|
||||||
@@ -32,7 +34,13 @@ nonl.script 38
|
|||||||
outer.script 376
|
outer.script 376
|
||||||
inner.script 44
|
inner.script 44
|
||||||
loop.script 35
|
loop.script 35
|
||||||
29 files, 1 directory
|
hush.script 42
|
||||||
|
aloud.script 59
|
||||||
|
args.script 64
|
||||||
|
pass.script 20
|
||||||
|
holds.script 87
|
||||||
|
35 files, 2 directories
|
||||||
|
47 of 64 entries, 1889 blocks free
|
||||||
> drive 1
|
> drive 1
|
||||||
> dir
|
> dir
|
||||||
other.txt 28
|
other.txt 28
|
||||||
@@ -40,6 +48,7 @@ notes <dir>
|
|||||||
2things <dir>
|
2things <dir>
|
||||||
twoblocks.txt 560
|
twoblocks.txt 560
|
||||||
2 files, 2 directories
|
2 files, 2 directories
|
||||||
|
4 of 32 entries, 503 blocks free
|
||||||
> cd /notes
|
> cd /notes
|
||||||
/notes> drive 0
|
/notes> drive 0
|
||||||
> drive 1
|
> drive 1
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ poem.txt, new file
|
|||||||
> w
|
> w
|
||||||
written, 22 bytes
|
written, 22 bytes
|
||||||
> q
|
> q
|
||||||
finished
|
|
||||||
> run poem.txt
|
> run poem.txt
|
||||||
poem.txt, 3 lines
|
poem.txt, 3 lines
|
||||||
> l
|
> l
|
||||||
@@ -36,7 +35,6 @@ poem.txt, 3 lines
|
|||||||
> d 99
|
> d 99
|
||||||
there is no such line
|
there is no such line
|
||||||
> q
|
> q
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -2,20 +2,15 @@ CosmOS
|
|||||||
> Edit hello.asm
|
> Edit hello.asm
|
||||||
hello.asm, 31 lines
|
hello.asm, 31 lines
|
||||||
> q
|
> q
|
||||||
finished
|
|
||||||
> Edit hello.asm
|
> Edit hello.asm
|
||||||
hello.asm, 31 lines
|
hello.asm, 31 lines
|
||||||
> q
|
> q
|
||||||
finished
|
|
||||||
> Status
|
> Status
|
||||||
the last program left 0, which is: it did what it was asked
|
the last program left 0, which is: it did what it was asked
|
||||||
finished
|
|
||||||
> Edit long.txt
|
> Edit long.txt
|
||||||
a line in it is longer than this can edit, so it has not been opened
|
a line in it is longer than this can edit, so it has not been opened
|
||||||
finished
|
|
||||||
> Status
|
> Status
|
||||||
the last program left 1, which is: it did not
|
the last program left 1, which is: it did not
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -15,11 +15,9 @@ typed.txt, new file
|
|||||||
> w
|
> w
|
||||||
written, 13 bytes
|
written, 13 bytes
|
||||||
> q
|
> q
|
||||||
finished
|
> [18;3Hrun typed.txt[18;16H
|
||||||
> [19;3Hrun typed.txt[19;16H
|
|
||||||
typed.txt, 2 lines
|
typed.txt, 2 lines
|
||||||
> q
|
> q
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> Crash opcode
|
> Crash opcode
|
||||||
that byte is not an instruction, at 5081
|
[0;31mthat byte is not an instruction, at 5081
|
||||||
A 00 B 0F Q 00
|
A 00 B 0F Q 00
|
||||||
the program was stopped
|
[0mthe program was stopped
|
||||||
> Crash service
|
> Crash service
|
||||||
nothing is installed at service 28, at 5084
|
[0;31mnothing is installed at service 28, at 5084
|
||||||
A 00 B 0F Q 00
|
A 00 B 0F Q 00
|
||||||
the program was stopped
|
[0mthe program was stopped
|
||||||
> Crash bank
|
> Crash bank
|
||||||
a bank that is not there, or an address past its end, at 509E
|
[0;31ma bank that is not there, or an address past its end, at 509E
|
||||||
A 01 B 0F Q 00
|
A 01 B 0F Q 00
|
||||||
the program was stopped
|
[0mthe program was stopped
|
||||||
> Crash device
|
> Crash device
|
||||||
nothing is installed for the device on port 00, at 50A5
|
[0;31mnothing is installed for the device on port 00, at 50A5
|
||||||
A 02 B 0F Q 00
|
A 02 B 0F Q 00
|
||||||
the program was stopped
|
[0mthe program was stopped
|
||||||
> Crash sideways
|
> Crash sideways
|
||||||
Crash opcode | service | bank | device | blind
|
Crash opcode | service | bank | device | blind
|
||||||
finished
|
|
||||||
> dir
|
> dir
|
||||||
greet.sbx 211
|
greet.sbx 211
|
||||||
hello.sbx 53
|
hello.sbx 53
|
||||||
@@ -25,6 +24,7 @@ Life.sbx 1396
|
|||||||
Snake.sbx 2164
|
Snake.sbx 2164
|
||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
|
Where.sbx 827
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 571
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
@@ -32,13 +32,14 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Packages <dir>
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
blocks.script 343
|
blocks.script 343
|
||||||
loops.script 272
|
loops.script 272
|
||||||
tune.sbx 306
|
tune.sbx 318
|
||||||
|
Play.sbx 2526
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
Apps <dir>
|
Apps <dir>
|
||||||
hi.script 121
|
hi.script 121
|
||||||
@@ -49,7 +50,13 @@ nonl.script 38
|
|||||||
outer.script 376
|
outer.script 376
|
||||||
inner.script 44
|
inner.script 44
|
||||||
loop.script 35
|
loop.script 35
|
||||||
29 files, 1 directory
|
hush.script 42
|
||||||
|
aloud.script 59
|
||||||
|
args.script 64
|
||||||
|
pass.script 20
|
||||||
|
holds.script 87
|
||||||
|
35 files, 2 directories
|
||||||
|
47 of 64 entries, 1889 blocks free
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
|
|||||||
bank 00
|
bank 00
|
||||||
* s 4F00 00
|
* s 4F00 00
|
||||||
* g 4F00
|
* g 4F00
|
||||||
that byte is not an instruction, at 4F00
|
[0;31mthat byte is not an instruction, at 4F00
|
||||||
A 01 B 7F Q 00
|
A 01 B 7F Q 00
|
||||||
that was the system itself, so there is nowhere to carry on from. Start the machine again.
|
that was the system itself, so there is nowhere to carry on from. Start the machine again.
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[0m[exit 0]
|
||||||
|
|||||||
@@ -3,17 +3,21 @@ CosmOS
|
|||||||
one.txt 13
|
one.txt 13
|
||||||
two.txt 14
|
two.txt 14
|
||||||
2 files
|
2 files
|
||||||
|
2 of 8 entries, 28 blocks free
|
||||||
> rename one.txt first.txt
|
> rename one.txt first.txt
|
||||||
renamed
|
renamed
|
||||||
> dir
|
> dir
|
||||||
first.txt 13
|
first.txt 13
|
||||||
two.txt 14
|
two.txt 14
|
||||||
2 files
|
2 files
|
||||||
|
2 of 8 entries, 28 blocks free
|
||||||
> delete first.txt
|
> delete first.txt
|
||||||
gone
|
gone
|
||||||
> dir
|
> dir
|
||||||
two.txt 14
|
two.txt 14
|
||||||
1 file
|
1 file
|
||||||
|
1 of 8 entries, 29 blocks free
|
||||||
|
the longest run is 28
|
||||||
> delete first.txt
|
> delete first.txt
|
||||||
no such file
|
no such file
|
||||||
> rename two.txt
|
> rename two.txt
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> Say before Flip
|
> Say before Flip
|
||||||
it says: before Flip
|
it says: before Flip
|
||||||
finished
|
|
||||||
> Flip
|
> Flip
|
||||||
A screen is drawn where you cannot see it. A key shows it, another comes back.
|
A screen is drawn where you cannot see it. A key shows it, another comes back.
|
||||||
finished
|
|
||||||
> Say after Flip
|
> Say after Flip
|
||||||
it says: after Flip
|
it says: after Flip
|
||||||
finished
|
|
||||||
> dir
|
> dir
|
||||||
greet.sbx 211
|
greet.sbx 211
|
||||||
hello.sbx 53
|
hello.sbx 53
|
||||||
@@ -15,6 +12,7 @@ Life.sbx 1396
|
|||||||
Snake.sbx 2164
|
Snake.sbx 2164
|
||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
|
Where.sbx 827
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 571
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
@@ -22,13 +20,14 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Packages <dir>
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
blocks.script 343
|
blocks.script 343
|
||||||
loops.script 272
|
loops.script 272
|
||||||
tune.sbx 306
|
tune.sbx 318
|
||||||
|
Play.sbx 2526
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
Apps <dir>
|
Apps <dir>
|
||||||
hi.script 121
|
hi.script 121
|
||||||
@@ -39,7 +38,13 @@ nonl.script 38
|
|||||||
outer.script 376
|
outer.script 376
|
||||||
inner.script 44
|
inner.script 44
|
||||||
loop.script 35
|
loop.script 35
|
||||||
29 files, 1 directory
|
hush.script 42
|
||||||
|
aloud.script 59
|
||||||
|
args.script 64
|
||||||
|
pass.script 20
|
||||||
|
holds.script 87
|
||||||
|
35 files, 2 directories
|
||||||
|
47 of 64 entries, 1889 blocks free
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> Say before Grid
|
> Say before Grid
|
||||||
it says: before Grid
|
it says: before Grid
|
||||||
finished
|
|
||||||
> Grid
|
> Grid
|
||||||
[2J[Hfinished
|
[2J[H>
|
||||||
>
|
|
||||||
> Say after Grid
|
> Say after Grid
|
||||||
it says: after Grid
|
it says: after Grid
|
||||||
finished
|
|
||||||
> dir
|
> dir
|
||||||
greet.sbx 211
|
greet.sbx 211
|
||||||
hello.sbx 53
|
hello.sbx 53
|
||||||
@@ -15,6 +12,7 @@ Life.sbx 1396
|
|||||||
Snake.sbx 2164
|
Snake.sbx 2164
|
||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
|
Where.sbx 827
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 571
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
@@ -22,13 +20,14 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Packages <dir>
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
blocks.script 343
|
blocks.script 343
|
||||||
loops.script 272
|
loops.script 272
|
||||||
tune.sbx 306
|
tune.sbx 318
|
||||||
|
Play.sbx 2526
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
Apps <dir>
|
Apps <dir>
|
||||||
hi.script 121
|
hi.script 121
|
||||||
@@ -39,7 +38,13 @@ nonl.script 38
|
|||||||
outer.script 376
|
outer.script 376
|
||||||
inner.script 44
|
inner.script 44
|
||||||
loop.script 35
|
loop.script 35
|
||||||
29 files, 1 directory
|
hush.script 42
|
||||||
|
aloud.script 59
|
||||||
|
args.script 64
|
||||||
|
pass.script 20
|
||||||
|
holds.script 87
|
||||||
|
35 files, 2 directories
|
||||||
|
47 of 64 entries, 1889 blocks free
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ CosmOS
|
|||||||
loaded, starting at 5000
|
loaded, starting at 5000
|
||||||
> run
|
> run
|
||||||
Hello, World!
|
Hello, World!
|
||||||
finished
|
|
||||||
> run
|
> run
|
||||||
Hello, World!
|
Hello, World!
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -1,25 +1,34 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> Say hello there
|
> Say hello there
|
||||||
it says: hello there
|
it says: hello there
|
||||||
finished
|
|
||||||
> Say.sbx spelled out in full
|
> Say.sbx spelled out in full
|
||||||
it says: spelled out in full
|
it says: spelled out in full
|
||||||
finished
|
|
||||||
> run once more
|
> run once more
|
||||||
it says: once more
|
it says: once more
|
||||||
finished
|
|
||||||
> dir
|
> dir
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
dir.sbx 156
|
dir.sbx 156
|
||||||
|
Greet 156
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
notes.sbx 21
|
notes.sbx 21
|
||||||
4 files
|
hello.sh 38
|
||||||
|
Apps <dir>
|
||||||
|
6 files, 1 directory
|
||||||
|
8 of 16 entries, 54 blocks free
|
||||||
|
> Greet a program with no extension
|
||||||
|
it says: a program with no extension
|
||||||
|
> hello.sh
|
||||||
|
> echo a script, started by its name
|
||||||
|
a script, started by its name
|
||||||
|
> away.sh
|
||||||
|
> echo a script from the system place
|
||||||
|
a script from the system place
|
||||||
> notes
|
> notes
|
||||||
not a program
|
that is not a program or a script: notes
|
||||||
> notes.sbx
|
> notes.sbx
|
||||||
not a program
|
that is not a program or a script: notes.sbx
|
||||||
> notes.txt
|
> notes.txt
|
||||||
I do not know: notes.txt
|
that is not a program or a script: notes.txt
|
||||||
> nosuchprogram
|
> nosuchprogram
|
||||||
I do not know: nosuchprogram
|
I do not know: nosuchprogram
|
||||||
> abcdefghijklmnopqr
|
> abcdefghijklmnopqr
|
||||||
@@ -34,7 +43,7 @@ run [words] start what was loaded, and tell it those words
|
|||||||
cd [path] go to a directory, or to the root with nothing after it
|
cd [path] go to a directory, or to the root with nothing after it
|
||||||
mkdir <path> make a directory
|
mkdir <path> make a directory
|
||||||
rmdir <path> remove an empty one
|
rmdir <path> remove an empty one
|
||||||
do <file> run the lines in a file, which must start with #!
|
do <file> [words] run the lines in a file, which must start with #!
|
||||||
echo [words] say them
|
echo [words] say them
|
||||||
clear empty the screen
|
clear empty the screen
|
||||||
delete <file> take it off the disk
|
delete <file> take it off the disk
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ loaded, starting at 5000
|
|||||||
keys, by interrupt. q stops.
|
keys, by interrupt. q stops.
|
||||||
ab
|
ab
|
||||||
the console has been handed back
|
the console has been handed back
|
||||||
finished
|
|
||||||
>
|
>
|
||||||
> run
|
> run
|
||||||
keys, by interrupt. q stops.
|
keys, by interrupt. q stops.
|
||||||
cd
|
cd
|
||||||
the console has been handed back
|
the console has been handed back
|
||||||
finished
|
|
||||||
>
|
>
|
||||||
> monitor
|
> monitor
|
||||||
x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
|
x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> Lander
|
> Lander
|
||||||
Crashed.
|
Crashed.
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> Lander
|
> Lander
|
||||||
Crashed.
|
Crashed.
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -868,7 +868,6 @@ loaded, starting at 5000
|
|||||||
##
|
##
|
||||||
|
|
||||||
the board has settled
|
the board has settled
|
||||||
finished
|
|
||||||
>
|
>
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ loaded, starting at 5000
|
|||||||
|
|
||||||
|
|
||||||
stopped
|
stopped
|
||||||
finished
|
|
||||||
>
|
>
|
||||||
>
|
>
|
||||||
halted
|
halted
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
CosmOS
|
||||||
|
> ls
|
||||||
|
[0;32mgreet.sbx[0m [0;32mhello.sbx[0m [0;32mLife.sbx[0m [0;32mSnake.sbx[0m [0;32mKeys.sbx[0m
|
||||||
|
[0;32mSay.sbx[0m [0;32mWhere.sbx[0m [0;32mBreak.sbx[0m [0;32mGrid.sbx[0m [0;32mPress.sbx[0m
|
||||||
|
[0;32mMode.sbx[0m [0;32mFlip.sbx[0m [0;32mSprite.sbx[0m [0;32mDepth.sbx[0m [0;34mPackages/[0m
|
||||||
|
[0;32mPad.sbx[0m [0;32mCrash.sbx[0m [0;32mvars.script[0m [0;32mblocks.script[0m [0;32mloops.script[0m
|
||||||
|
[0;32mtune.sbx[0m [0;32mPlay.sbx[0m notes.txt [0;34mApps/[0m [0;32mhi.script[0m
|
||||||
|
[0;32mbad.script[0m plain.script [0;32mcross.script[0m [0;32mnonl.script[0m [0;32mouter.script[0m
|
||||||
|
[0;32minner.script[0m [0;32mloop.script[0m [0;32mhush.script[0m [0;32maloud.script[0m [0;32margs.script[0m
|
||||||
|
[0;32mpass.script[0m [0;32mholds.script[0m
|
||||||
|
> cd /Apps
|
||||||
|
/Apps> ls
|
||||||
|
[0;32mCopy.sbx[0m [0;32mSay.sbx[0m [0;32mWhere.sbx[0m [0;32mWalk.sbx[0m [0;32mls.sbx[0m [0;32mLander[0m [0;32mwhere.sh[0m
|
||||||
|
/Apps> cd /
|
||||||
|
> ls /Packages/app.Lander
|
||||||
|
[0;32mLander.sbx[0m splash.tune
|
||||||
|
> mkdir /empty
|
||||||
|
made
|
||||||
|
> cd /empty
|
||||||
|
/empty> ls
|
||||||
|
/empty> cd /
|
||||||
|
> ls nowhere
|
||||||
|
there is no such directory: nowhere
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -53,9 +53,9 @@ bank 00
|
|||||||
800B 00 ?
|
800B 00 ?
|
||||||
* g 8000
|
* g 8000
|
||||||
H
|
H
|
||||||
that byte is not an instruction, at 800A
|
[0;31mthat byte is not an instruction, at 800A
|
||||||
A 85 B 3F Q 05
|
A 85 B 3F Q 05
|
||||||
the program was stopped
|
[0mthe program was stopped
|
||||||
* d 8000
|
* d 8000
|
||||||
8000 26 48 INIA 48
|
8000 26 48 INIA 48
|
||||||
8002 D1 00 OUTA 00
|
8002 D1 00 OUTA 00
|
||||||
@@ -92,7 +92,6 @@ that one needs a value after it
|
|||||||
820F 00 ?
|
820F 00 ?
|
||||||
* g 8200
|
* g 8200
|
||||||
hello, typed
|
hello, typed
|
||||||
finished
|
|
||||||
* exit
|
* exit
|
||||||
> dir
|
> dir
|
||||||
greet.sbx 211
|
greet.sbx 211
|
||||||
@@ -101,6 +100,7 @@ Life.sbx 1396
|
|||||||
Snake.sbx 2164
|
Snake.sbx 2164
|
||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
|
Where.sbx 827
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 571
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
@@ -108,13 +108,14 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Packages <dir>
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
blocks.script 343
|
blocks.script 343
|
||||||
loops.script 272
|
loops.script 272
|
||||||
tune.sbx 306
|
tune.sbx 318
|
||||||
|
Play.sbx 2526
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
Apps <dir>
|
Apps <dir>
|
||||||
hi.script 121
|
hi.script 121
|
||||||
@@ -125,7 +126,13 @@ nonl.script 38
|
|||||||
outer.script 376
|
outer.script 376
|
||||||
inner.script 44
|
inner.script 44
|
||||||
loop.script 35
|
loop.script 35
|
||||||
29 files, 1 directory
|
hush.script 42
|
||||||
|
aloud.script 59
|
||||||
|
args.script 64
|
||||||
|
pass.script 20
|
||||||
|
holds.script 87
|
||||||
|
35 files, 2 directories
|
||||||
|
47 of 64 entries, 1889 blocks free
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ CosmOS
|
|||||||
x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
|
x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
|
||||||
* Say hello from the monitor
|
* Say hello from the monitor
|
||||||
it says: hello from the monitor
|
it says: hello from the monitor
|
||||||
finished
|
|
||||||
* Crash opcode
|
* Crash opcode
|
||||||
that byte is not an instruction, at 5081
|
[0;31mthat byte is not an instruction, at 5081
|
||||||
A 00 B 0F Q 00
|
A 00 B 0F Q 00
|
||||||
the program was stopped
|
[0mthe program was stopped
|
||||||
* nonsense
|
* nonsense
|
||||||
I do not know: nonsense
|
I do not know: nonsense
|
||||||
* b program
|
* b program
|
||||||
@@ -20,6 +19,7 @@ Life.sbx 1396
|
|||||||
Snake.sbx 2164
|
Snake.sbx 2164
|
||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
|
Where.sbx 827
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 571
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
@@ -27,13 +27,14 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Packages <dir>
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
blocks.script 343
|
blocks.script 343
|
||||||
loops.script 272
|
loops.script 272
|
||||||
tune.sbx 306
|
tune.sbx 318
|
||||||
|
Play.sbx 2526
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
Apps <dir>
|
Apps <dir>
|
||||||
hi.script 121
|
hi.script 121
|
||||||
@@ -44,7 +45,13 @@ nonl.script 38
|
|||||||
outer.script 376
|
outer.script 376
|
||||||
inner.script 44
|
inner.script 44
|
||||||
loop.script 35
|
loop.script 35
|
||||||
29 files, 1 directory
|
hush.script 42
|
||||||
|
aloud.script 59
|
||||||
|
args.script 64
|
||||||
|
pass.script 20
|
||||||
|
holds.script 87
|
||||||
|
35 files, 2 directories
|
||||||
|
47 of 64 entries, 1889 blocks free
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ line 42: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|||||||
line 43: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 43: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
line 44: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 44: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
-- more --
|
-- more --
|
||||||
finished
|
|
||||||
> run readable.txt
|
> run readable.txt
|
||||||
first line
|
first line
|
||||||
second line
|
second line
|
||||||
@@ -115,7 +114,6 @@ line 56: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|||||||
line 57: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 57: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
line 58: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 58: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
line 59: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 59: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
finished
|
|
||||||
> run readable.txt
|
> run readable.txt
|
||||||
first line
|
first line
|
||||||
second line
|
second line
|
||||||
@@ -167,7 +165,6 @@ line 44: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|||||||
-- more --
|
-- more --
|
||||||
line 45: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 45: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
-- more --
|
-- more --
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ CosmOS
|
|||||||
loaded, starting at 5000
|
loaded, starting at 5000
|
||||||
> run
|
> run
|
||||||
40
|
40
|
||||||
finished
|
|
||||||
> load More.sbx
|
> load More.sbx
|
||||||
loaded, starting at 5000
|
loaded, starting at 5000
|
||||||
> run readable.txt
|
> run readable.txt
|
||||||
@@ -30,7 +29,6 @@ line 17: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|||||||
line 18: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 18: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
line 19: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 19: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
-- more --
|
-- more --
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> Mode
|
> Mode
|
||||||
40
|
40
|
||||||
finished
|
|
||||||
> echo a narrow screen
|
> echo a narrow screen
|
||||||
a narrow screen
|
a narrow screen
|
||||||
> Mode
|
> Mode
|
||||||
80
|
80
|
||||||
finished
|
|
||||||
> echo and a wide one again
|
> echo and a wide one again
|
||||||
and a wide one again
|
and a wide one again
|
||||||
> exit
|
> exit
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ Pads present, as a bit each: 1
|
|||||||
0: 0
|
0: 0
|
||||||
0: 48
|
0: 48
|
||||||
0: 0
|
0: 0
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ keys:
|
|||||||
86 delete
|
86 delete
|
||||||
5A Z
|
5A Z
|
||||||
done
|
done
|
||||||
finished
|
|
||||||
>
|
>
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ keys:
|
|||||||
86 delete
|
86 delete
|
||||||
5A Z
|
5A Z
|
||||||
done
|
done
|
||||||
finished
|
|
||||||
>
|
>
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ CosmOS
|
|||||||
> drive 1
|
> drive 1
|
||||||
> dir
|
> dir
|
||||||
0 files
|
0 files
|
||||||
|
0 of 128 entries, 2031 blocks free
|
||||||
> Copy 0:/Say.sbx 1:/Say.sbx
|
> Copy 0:/Say.sbx 1:/Say.sbx
|
||||||
copied
|
copied
|
||||||
finished
|
|
||||||
> dir
|
> dir
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
1 file
|
1 file
|
||||||
|
1 of 128 entries, 2030 blocks free
|
||||||
> drive
|
> drive
|
||||||
1
|
1
|
||||||
> exit
|
> exit
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Life.sbx 1396
|
|||||||
Snake.sbx 2164
|
Snake.sbx 2164
|
||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
|
Where.sbx 827
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 571
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
@@ -15,13 +16,14 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Packages <dir>
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
blocks.script 343
|
blocks.script 343
|
||||||
loops.script 272
|
loops.script 272
|
||||||
tune.sbx 306
|
tune.sbx 318
|
||||||
|
Play.sbx 2526
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
Apps <dir>
|
Apps <dir>
|
||||||
hi.script 121
|
hi.script 121
|
||||||
@@ -32,7 +34,13 @@ nonl.script 38
|
|||||||
outer.script 376
|
outer.script 376
|
||||||
inner.script 44
|
inner.script 44
|
||||||
loop.script 35
|
loop.script 35
|
||||||
29 files, 1 directory
|
hush.script 42
|
||||||
|
aloud.script 59
|
||||||
|
args.script 64
|
||||||
|
pass.script 20
|
||||||
|
holds.script 87
|
||||||
|
35 files, 2 directories
|
||||||
|
47 of 64 entries, 1889 blocks free
|
||||||
> load
|
> load
|
||||||
load what?
|
load what?
|
||||||
> load nosuch.sbx
|
> load nosuch.sbx
|
||||||
@@ -45,12 +53,10 @@ loaded, starting at 5000
|
|||||||
a program, loaded off a disk, running on the system that loaded it
|
a program, loaded off a disk, running on the system that loaded it
|
||||||
what should I call you? Anachronaut
|
what should I call you? Anachronaut
|
||||||
hello, Anachronaut. that is all I do.
|
hello, Anachronaut. that is all I do.
|
||||||
finished
|
|
||||||
> run
|
> run
|
||||||
a program, loaded off a disk, running on the system that loaded it
|
a program, loaded off a disk, running on the system that loaded it
|
||||||
what should I call you? Claude
|
what should I call you? Claude
|
||||||
hello, Claude. that is all I do.
|
hello, Claude. that is all I do.
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ CosmOS
|
|||||||
loaded, starting at 5000
|
loaded, starting at 5000
|
||||||
> run
|
> run
|
||||||
nothing was said
|
nothing was said
|
||||||
finished
|
|
||||||
> run notes.txt
|
> run notes.txt
|
||||||
it says: notes.txt
|
it says: notes.txt
|
||||||
finished
|
|
||||||
> run a longer thing with spaces
|
> run a longer thing with spaces
|
||||||
it says: a longer thing with spaces
|
it says: a longer thing with spaces
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ CosmOS
|
|||||||
saying what it is doing
|
saying what it is doing
|
||||||
> Say from a script
|
> Say from a script
|
||||||
it says: from a script
|
it says: from a script
|
||||||
finished
|
|
||||||
> echo
|
> echo
|
||||||
|
|
||||||
> Say and again
|
> Say and again
|
||||||
it says: and again
|
it says: and again
|
||||||
finished
|
|
||||||
> exit
|
> exit
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
CosmOS
|
||||||
|
> do args.script red green
|
||||||
|
> echo one is $1
|
||||||
|
one is red
|
||||||
|
> echo two is $2
|
||||||
|
two is green
|
||||||
|
> echo all of it is $args
|
||||||
|
all of it is red green
|
||||||
|
> args.script blue yellow
|
||||||
|
> echo one is $1
|
||||||
|
one is blue
|
||||||
|
> echo two is $2
|
||||||
|
two is yellow
|
||||||
|
> echo all of it is $args
|
||||||
|
all of it is blue yellow
|
||||||
|
> pass.script hello there
|
||||||
|
> Say $args
|
||||||
|
it says: hello there
|
||||||
|
> do holds.script top level
|
||||||
|
> echo outer has $1
|
||||||
|
outer has top
|
||||||
|
> do args.script deep down
|
||||||
|
> echo one is $1
|
||||||
|
one is deep
|
||||||
|
> echo two is $2
|
||||||
|
two is down
|
||||||
|
> echo all of it is $args
|
||||||
|
all of it is deep down
|
||||||
|
> echo outer still has $1 and $args
|
||||||
|
outer still has top and top level
|
||||||
|
> do args.script only
|
||||||
|
> echo one is $1
|
||||||
|
one is only
|
||||||
|
> echo two is $2
|
||||||
|
nothing is set called 2
|
||||||
|
stopped: that line did not work
|
||||||
|
> $1
|
||||||
|
nothing is set called 1
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -2,7 +2,6 @@ CosmOS
|
|||||||
> do bad.script
|
> do bad.script
|
||||||
> Say before
|
> Say before
|
||||||
it says: before
|
it says: before
|
||||||
finished
|
|
||||||
> nosuchcommand
|
> nosuchcommand
|
||||||
I do not know: nosuchcommand
|
I do not know: nosuchcommand
|
||||||
stopped: that line did not work
|
stopped: that line did not work
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user