Compare commits

...
46 Commits
Author SHA1 Message Date
AnachronautandClaude Opus 5 09c72e4951 srcNext asks its own questions instead of calling for the answers
Two calls per character of every pass: numCompare to ask whether the
buffer is used up, and numStep to move one further into it. Inlined, the
compare settles almost every time on the high bytes and settles them
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.

And the newline test uses A, which still holds the character, where it
used to fetch it back out of the SrcChar it had just been put in.

              before        after
  hello      586,184      574,737
  Say      4,506,702    4,333,689
  Files    7,188,351    6,863,744
  Keys    15,069,880   14,399,801
  cosmos 789,982,899  772,826,841

Two to four and a half per cent, which is worth having and is much less
than expected - AND THAT IS THE INTERESTING PART. It says the per
character cost is not in the reading. Now that comments no longer come
through here at all, what is left is the tokenizer above it, and the
remaining time is there rather than in getting the bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 21:43:29 -04:00
AnachronautandClaude Opus 5 2e9cb9af7e Comments are thrown away by the reader, not read one byte at a time
A comment used to walk through tokGet and srcNext and be classified,
character by character, on the way to being discarded. Comments are most
of what this assembler reads: 87 per cent of what Say.asm pulls in once
services.asm is counted, 59 per cent of CosmOS. The house style here is
dense commentary, so the assembler is penalised more than most by its own
sources.

The saving is not the classifying, it is the BOOKKEEPING. srcNext loads
and stores the walking pointer through memory for every character and
asks numCompare whether the buffer is used up. srcSkipComment keeps the
pointer in a data pointer for a whole run and the newline in B, so a
comment byte costs a load, a compare and two steps. A run is capped at
255 so one byte can count it, which is the only reason it loops.

Measured, each version with its own rebuilt images:

              without        with
  hello       791,957     586,184
  Say       9,924,401   4,506,702   2.20x
  Files    13,197,710   7,188,351   1.84x
  Keys     23,091,447  15,069,880   1.53x
  cosmos  886,498,996 789,982,899   1.12x

Which tracks the comment ratios: Say gains most and cosmos least, in
proportion to how much of each is prose.

---- And two mistakes worth keeping ----

The scratch went among the READER'S STATE, which is a block 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 pointer
wrong. The comment above that block says not to do this, in capitals.
That is twice this week: scriptCopyState had the same shape this morning.

And a file that ends inside a comment has to put back the file that
included it, exactly as srcAtEnd does for a character. NOTHING IN THIS
REPOSITORY ENDS THAT WAY - every source here ends on a line of code with
a newline after it - so break.sh could not catch an error in that path
because nothing reached it. tail.asm is generated with no newline on its
last line for that reason, and usestail.asm names a label after the
include, which is what goes missing when the include never returns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 21:33:15 -04:00
AnachronautandClaude Opus 5 e20c9bac1f The label table is kept in order, and halved instead of walked
labFind walked the index from the front, so every use of every label
cost a scan of every label defined so far, each with a string compare.
The cost grew with the program being built, which is what made it hurt:
assembling CosmOS on the machine took 1,833,691,267 cycles against the
assembler assembling itself at 57,257,133 - three times the source for
thirty two times the time.

Sorted and halved, the same build is 886,498,996. THE WALK WAS 52 PER
CENT OF THE WHOLE ASSEMBLY, which settles a suspicion this project has
carried unverified for weeks and puts a number on it.

Eleven comparisons against two thousand entries where a walk averaged six
hundred and seventy. The search hands back where a name WOULD go, which
is what adding one needs and what a walk could never have offered, so
labAdd gets its insertion point for nothing.

sameText was already an ordering and did not have to change: Q is the
difference at the first character that differed, and the Carry Flag from
that same subtraction 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.

Small programs pay about a tenth more - 57.3M to 63.5M for the assembler
on itself - because adding a label now moves the tail of the index up and
a short table was never expensive to walk. That is the right way round
for a trade to fall.

numHalve and numBack are new: a rotate right on a CIRCULAR sixteen bit
register brings bit nought back in at the top, so halving means taking
that bit off again, and the low half has to go down first because the
mask wants B.

WHICH END THE TABLE IS SORTED FROM DOES NOT MATTER. 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 no output changes. Tests/break.sh says
so, correctly, by not noticing.

Verified by CosmOS builds CosmOS and second generation staying byte
identical. An indexing bug cannot hide behind a fixed point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 21:02:05 -04:00
AnachronautandClaude Opus 5 563bc20a75 Tab lists its matches in columns, and says what it can in colour
Tab's listing was six instructions: print the name, print two spaces. It
was the crown of this shell for a while and looks plain next to ls.

Columns cost NOTHING EXTRA. Tab already walks its candidates twice - once
to find the answer, once to show the matches, and tabRunSources exists
because those are the same walk asked two questions. The longest match is
counted on the first, which already visits every one of them, so the
listing needs no walk of its own to know how wide a column should be.
Padding goes before a name rather than after, so a row ends on a name.

Directories are blue and the shell's own commands are green. Both are
free: Tab appends the separator itself, and a built-in is not a file at
all - which is also the only way anybody could know it will run.

A FILE IS NOT COLOURED, and that is a decision. Whether a file will run
is a read of its first block, which is what ls does and what makes ls
cost twice what it otherwise would. Here it would be worse than slow:
candidates are offered from inside a directory walk, and looking a file
up would overwrite the very fields holding the walk's own position. Doing
it safely means holding every candidate name in memory, which is a buffer
the shell would carry whether anybody pressed Tab or not - and ls is one
keystroke away. A Tab press already costs about 200,000 cycles, so the
read was not the objection.

---- And the machine could no longer build itself ----

Found by make test, not by reading. The native assembler ran out of room
for label names on cosmos.asm: 16,758 bytes against 16,384. The index was
1,341 of 1,536 in the same breath.

Which is scratch.asm's own warning happening a second time - "two
ceilings a hundred bytes apart look like one ceiling until the first is
lifted" - so both were raised, out of the seventeen kilobyte page that
file deliberately left unclaimed against exactly this. Names to 26,624
and the index to 2,048, both left about a third clear, with 1,792 bytes
still unclaimed for the same reason.

A name is thirteen bytes on average and an index entry is four, so the
arena will always be the one that speaks first. That is now written down
where the two numbers are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 20:18:56 -04:00
AnachronautandClaude Opus 5 eaeb473176 ls says in green what the shell will start
Directories were already blue. Something the shell will run is green now,
and it is asked by LOOKING rather than looked up.

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 - 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 any more. The shell decides what to run by
reading a file's first block, so "will this run" is a question with an
answer already, and this asks the same one the shell would: SBEX for a
program, "#!" for a script. Nothing is written down and nothing has to
agree about anything.

The price is a block read per file, which is exactly what the bit existed
to avoid: a listing of thirty seven files went from 260,593 cycles to
635,321. It is paid in ls and not in dir on purpose - dir is the listing
you audit and is built into the shell, this is the one you read and was
loaded off the disk anyway. The fast one stays fast.

plain.script is the proof on the test disk: it sits among a dozen scripts
that are green and is not one, because it is the fixture with no shebang
and the shell will not start it. The extension is decoration and the
colour is the truth, which is the whole of what running by content means,
finally visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 19:53:33 -04:00
AnachronautandClaude Opus 5 afda6ca83b Finishing says nothing, which is what finishing looks like
The shell printed "finished" every time a program gave the machine back.
That made sense when starting a program and getting the machine back was
most of what the machine did and the prompt was the only other thing on
the screen. Under a listing, between two commands, it is noise.

And it was printed whatever the program made of it, so a program that had
just explained what went wrong was answered with the word "finished" -
"there is no such directory: nowhere" and then, immediately, "finished".

The prompt coming back is what says a program is over. It is the same
argument osLastStatus is made of: a program that failed has already said
so in words, and anything the shell adds beside that is the shell talking
over it. A program that FAULTED still says so, because the fault screen
printed in red above and a program that is gone should not look like one
that ended.

A NEWLINE ONLY IF ONE IS WANTED. A program that stopped part way along a
line would leave the prompt sitting in the middle of its last output,
which the word used to prevent by accident. The console knows which
column the cursor is in, so the shell asks - where a blank line printed
every time would be right about half of the time, and an empty directory
listed with ls would be followed by a blank line for no reason.

Fifty nine recorded sessions lose the word. Two of them move a cursor up
a row with it, which is the same fact seen from the screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 19:49:43 -04:00
AnachronautandClaude Opus 5 a13bbeb4db ls, a listing laid out to be read
> ls
  Copy.sbx   Say.sbx    Where.sbx  Walk.sbx   ls.sbx     Lander     where.sh

dir says what is there one line each, with sizes and a tally of the disk.
This says the names in columns and nothing else, which is what you want
ninety times in a hundred. Two programs rather than one with a switch,
because they answer different questions and neither answer is a worse
version of the other.

AND THIS IS A PROGRAM WHERE DIR IS BUILT IN, which is the difference that
matters when the disk is what you are doubting: dir is already in memory
and this has to be loaded off the disk it is about to list. The daily
driver and the diagnostic.

Two passes over the directory and no buffer at all. Columns need the
longest name before the first line can be printed, which usually means
holding every name - twenty four bytes each against a format that allows
1,024 entries. Walking twice costs a read of each directory block, into a
buffer that is already there.

Across and not down. Real listings go down the columns so that names next
to each other alphabetically are next to each other on the screen, and
that reason depends on sorting - which nothing here does. With the order
arbitrary, down-and-across buys nothing and costs a division.

Directories in blue and wearing a separator, unfinished saves in red,
everything else plain: whether a file is runnable cannot be known without
opening it, and opening every file in a directory to colour a listing is
a price nobody agreed to pay. The colour reaches a terminal as well as
the screen, so it is one mechanism and not two.

---- Three bugs, and one of them is this machine's oldest trap ----

nameLength answered in A, and a RET puts A back the way the caller had
it. So it answered with nothing, and every gap between the columns came
out 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 here comes home in it.

The padding subtracted the other way round - the name from the cell -
which borrowed on every name that was not the longest, so all of them
took the "wider than its cell" path and the listing came out separated by
one space.

And padding after a name left trailing spaces on every line that did not
fill its last column. It goes before the next name now, so spaces only
ever fall between two things and a line ends on a name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 19:29:02 -04:00
AnachronautandClaude Opus 5 323d7a0330 A listing says what is left of the disk, and what will fit
dir said what was there and nothing about what was left. SplitDisk has
printed the free figure since it was written, so the machine's own
listing was the poorer of the two implementations at describing the same
disk.

  43 files, 3 directories
  46 of 64 entries, 1893 blocks free

Entries first, because they are the ceiling nobody notices until they hit
it: a disk of small files runs out of directory slots long before it runs
out of blocks.

COUNTED RATHER THAN ASKED. The superblock keeps a free count and this
file calls it "a note rather than the truth" in three places. sbfsSpace
reads the whole directory table instead, which costs a read per directory
block and is the answer rather than a guess. SplitDisk goes on reading
the note and saying when it is stale, which is the right place for that
check - the host tool is what you audit a disk with.

---- And it is a fact about the disk, not about where you are ----

The first version added the blocks up as the LISTING walked past them,
which cost no extra read and was wrong: that walk stops only on entries
in the working directory, so the same disk came out as 1,996 blocks free
from the root and 2,025 from /Apps. Comparing against SplitDisk is what
said so, which is what having two implementations is for.

---- The longest run, which is what decides whether a file fits ----

  4 of 16 entries, 37 blocks free
  the longest run is 25

Files are laid down contiguously, so the free total does not say whether
a file will fit. Both implementations learn it, from one specification.

Said only when it differs from the free total. Deleting is what fragments
a contiguous store, and a disk that has only been appended to has one gap
at the end - so on a healthy disk this is silent, and a line that appears
only when something is wrong is a line somebody reads.

There is no sort on this machine and the entries are in no order, so a
candidate walks the disk: each pass finds the used extent nearest at or
after it, and anything the candidate stands inside pushes it to the far
end and starts the pass again. The same trick allocating uses. So it
costs a pass per gap rather than per file - nearly nothing on a disk with
one gap, more the more fragmented the disk is, which is the right way
round.

holes.img is six files with the second and fourth deleted, because no
other disk here can show any of this: none of them has ever had anything
deleted from it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 18:19:04 -04:00
AnachronautandClaude Opus 5 2ad8edf9bc A program can see a directory
Every file service took a name a program already knew - read it, save it,
rename it, delete it, ask how big it is - and none of them could find out
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 each unwritable for want of this.

osDirFirst and osDirNext. DP0 says where to put the name and B how much
room, the same bargain osArgument and osWhereAmI offer.

Q ANSWERS THE KIND rather than a yes or no, so one value says both
whether there is an entry and what it is: 0 a file, 1 a directory, 2 a
save that stopped before it committed, 0xFF nothing more. A caller that
only wants names tests for 0xFF and ignores the rest.

The size is deliberately not in it. A walk hands back a name, and a
program that wants the size asks osFileInfo about that name - the
alternative being a record in memory whose shape both sides have to agree
on, which services.asm went out of its way to avoid for file sizes.

Walk.asm is the first program that can see a directory, and it asks
osFileInfo about each entry BETWEEN two steps of the walk. That is the
hazard rather than decoration: where a walk has got to and where the last
file asked about lives are both held by the system, and two things
sharing one position would show as a listing that stopped early or said a
name twice. Then the same walk in /Apps, since one that only ever ran at
the root would not have proved it walks where you are.

A directory is not asked about at all. 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.

The lint baseline moves by one. The kind is decided by a chain of bit
tests in the shape dir already uses five hundred lines away, and arms of
a comparison chain each loading the same variable are the case this
project's own rule says not to collapse: the repetition is what lets a
new arm be dropped in anywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 17:55:41 -04:00
AnachronautandClaude Opus 5 b83ba5bf7a A colour reaches a terminal as well as the screen
The console's attribute has always meant something to the screen and
nothing to the serial line: its low nibble picks one of sixteen ink and
paper pairs, and only videoPutCell ever read it. So the fault screen's
red was red in the window and grey down the wire, and Examples/colours
printed " ordinary  highlighted " with nothing to tell them apart.

It is said in ANSI now, on the same terms the cursor is said in: 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. Only the scheme nibble crosses
- the page bits say which tiles a cell draws from, which is a fact about
the screen's own art.

THE ORDER WAS ALREADY RIGHT, which is worth saying because it looks like
a borrowing and is not. Both sets enumerate 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 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, so going from bank 8 to bank 1 does
not write red on the grey paper bank 8 left behind.

Scheme 0 is a bare reset rather than grey on black, and a terminal is
assumed to start plain - so a machine that never asks for a colour says
nothing at all, and one that does put the terminal back on its way out.
Two ways out, because the two endings have different rules: stopping on
purpose goes through stdio, since atexit runs BEFORE the buffer is
flushed and a reset written to the file descriptor would arrive in front
of the text it is meant to follow. Dying on a signal writes the four
bytes directly and accepts that the buffer may be lost.

colourTest walks all sixteen and then halts WITH ONE STILL SET, so the
recording shows the reset after the halt line rather than before it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 17:24:44 -04:00
AnachronautandClaude Opus 5 94bdf71356 Lunar Porter moves into a directory of its own
The first application with things of its own, and the first customer for
all three rungs before it at once.

  /Apps/Lander                      a launcher
  /Packages/app.Lander/Lander.sbx   the program
  /Packages/app.Lander/splash.tune  its tune

Typing "Lander" starts the launcher, which needs no new rule: the shell
runs what it reads, so a launcher 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. It passes on what it was told with
$args, and says #quiet, because a launcher is machinery and not
narration. The game then asks where it came from and joins its tune's
name to that, so nothing anywhere names /splash.tune.

Every recorded Lander test still types just "Lander" and knows nothing
about any of this, which is exactly the claim.

---- Why the directory is not in /Apps ----

A launcher and a directory of the same name cannot both be there; SBFS
refuses the second. And /Apps is the directory the shell walks for every
word it does not know and Tab walks for every first word, so doubling
what is in it is a cost on the path that runs most.

The "app." goes in FRONT rather than behind because Tab matches the start
of a name: a prefix is a namespace and a suffix is a collision. Copy.app
beside Copy.sbx makes "Copy" and Tab complete to the shared "Copy." and
hand you a broken word.

The launcher names drive 0, so a game started from a disk of your own is
looked for where the game is rather than where you are - its lines run on
your disk, which is what makes everything else in a script work.

/lander.state stays at the root. A saved position belongs to whoever
saved it and is found where they are standing; a tune belongs to the
program and is found beside it. That pair is the whole distinction.

---- And what it cost ----

Two hundred thousand cycles, about twelve frames, between the machine
starting and the game drawing: a script opened, a deeper path walked. Six
video captures moved out by that much. The drift bar needed something
else - its pad is counted from the MACHINE starting rather than the game,
so the burn is now twelve frames shorter from the game's point of view,
and the recording holds the button longer instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 15:55:19 -04:00
AnachronautandClaude Opus 5 2defbb49e2 A program can ask where it came from
SWI osWhereAmI hands back the path the program was loaded from, on the
same terms as osArgument, and Libraries/path.asm joins a name to the
place another thing is in. Between them an application can find its own
assets: ask where you are, then pathBeside that and the file's name.

The answer FOLLOWS THE PROGRAM AND NOT THE PERSON, which is the whole
point and the reason the working directory could not serve. A program's
assets are relative to the program and its arguments are relative to
whoever ran it, and cwd 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.

It is made absolute before the program starts, because the path the
search settled on may be a bare name: a program found where somebody was
standing is named by the word that was typed, and a bare name means the
working directory - which a program is entitled to move out of. Worked
out once, at the start, since where a program came from is a fact about
its start and cannot change afterwards.

Joining is a LIBRARY and not a service. 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 - while
one service handing back a path composes with all of them.

---- And the root's own path was "//" ----

Found by the first caller that asks. shellPath prepends a separator in
front of whatever string it is given, so being handed the separator
itself wrote two of them. Nothing saw it while the only caller was the
prompt, which asks where it is only when that is not the root. It is
handed an empty string now, and cosmosWhere runs a program from the root.

Where.sbx exists to be run rather than read, and is on the test disk
twice: at the root, where it is found by the bare word typed, and in
/Apps, where it is found by a path that already says where it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 15:27:57 -04:00
AnachronautandClaude Opus 5 d9ebc76cf5 A script is given words, and keeps them
"do build.sh cosmos.asm" looked for a file called "build.sh cosmos.asm",
because scriptOpen copied the whole rest of the line into the name. So a
script could be told nothing, and a launcher - a script whose entire job
is to hand on what it was told - could not exist.

Now the name is cut off the front and what follows is kept whole. $1 to
$9 are the words of it, walked out on demand, and $args is all of them.
Nothing is stored per parameter, so there is no limit on how many a
script may be handed and no second number to keep in step. Both ways of
starting a script pass them on: "do" and typing the name.

A word that was not given is an error that stops the script, like every
other name this shell does not know. Expanding it to nothing would let a
command run with an argument missing and then report success, which is
what stop-on-failure exists to prevent. $args is always set inside a
script, empty if it was given nothing, so "if same $args" can be asked.

---- And the count that describes the block was already wrong ----

Found while adding a field to it. The state one script keeps for another
is saved by a single copy of a fixed number of bytes, and that number was
71 against a block of 77: six bytes of line position had been added in
the middle of it years after the count was written. So the tail of every
saved script was never saved, and #quiet in a helper stayed behind in the
script that called it - the opposite of what this file's own comment
promises and the README documents.

The unsaved line position turned out not to matter, because a loop keeps
its own copy in the block record. Nothing said so. Three numbers describe
this block and all three now say so in a comment, and cosmosScriptNest
ends on a helper that goes quiet and a caller that must not stay that
way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 15:06:32 -04:00
AnachronautandClaude Opus 5 06af7e7fbb Reading a script must not move the person who started it
A script is fetched a block at a time while its lines run, and its name
is resolved afresh for every block. A name with a drive in front of it
moves the machine to that drive on the way past - sbfsWalk calls sbfsUse
- so a script found in the system's place on drive 0, started by somebody
standing on a disk of their own, ran its lines on the system disk.

Always possible with "do 0:/Apps/setup.sh", and reachable by typing a
name now that the search finds scripts the same three places it finds
programs. The drive is kept across each fetch and put back after it, at
both places a script's name is resolved.

The test has to work for it. A script that fits in one block is read
entirely while it is being opened, and the opening was never the hard
part; and the keep in scriptFill cannot be broken on its own, because
scriptOpen has already written the variable down. So the script on the
disk crosses two block boundaries and moves itself between them: what it
says about where it is standing is 1 before the move and 0 after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 13:23:31 -04:00
AnachronautandClaude Opus 5 fd962f0084 The shell runs what it reads, not what a name ends in
A typed word had ".sbx" pasted on the end of it before anything went
looking, which is why "notes.txt" sent the shell after a notes.txt.sbx
that was never going to exist, and why a script could only be started
with "do". The extension was what made a file reachable by name.

Now the word as typed is asked for first and the word with the extension
on it only after that misses. What comes back is dispatched on what is
inside it: SBEX loads and starts, "#!" is read as lines. The loader
already refuses anything that is not SBEX and the script reader already
refuses anything without the shebang, so the two kinds of runnable file
turn each other away and neither has to know the other exists.

The suffix can only ever be a second guess, so a file that is really
there always beats one that would have to be invented and every program
already on a disk still starts by the short name people type for it. It
costs a second walk of each directory when a word is not found in it.

Tab completion offers a file under the name it actually has, and the
suffix stripper is gone: a first word can now be any file at all, and
offering only the ones ending .sbx would hide the scripts.

The video capture windows moved out a hundred thousand cycles, because
starting Lander now walks four names where it walked two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 13:23:14 -04:00
AnachronautandClaude Opus 5 4e61158b11 The splash stops sounding before it ends, so listen earlier
The check asked whether music was still playing four seconds in, and the
score it listens to ends its every voice on a rest - twelve ticks of
silence that hold the logo up while the voices decay. So it was measuring
exactly the quiet the score asks for and calling a perfect tune a failure.

Three seconds instead, which is still twice the second and a half the
silent path holds the logo for, and which is what the check is actually
about: that this is the tune and not the hold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 13:22:56 -04:00
AnachronautandClaude Opus 5 f351ee8844 Every score on the disk, not a named one
The makefile compiled splash.score by name, so intro.score sat beside it
turning into nothing - which is exactly the failure the mirror three
lines above exists to prevent, and its comment says so: a list in a
makefile goes stale the moment somebody adds a file, and what they forgot
is invisible until they go looking for it on the machine.

Found by the user going looking for it on the machine.

Every .score in Programs/Tunes is compiled and put on the disk now, and
every patch in Programs/Sounds is converted first so a score can name any
of them. The source is mirrored to /Source/Tunes with everything else
that was written; the tune goes to the root, where a program looking for
one expects it - which is worth writing down, because the two being in
different places is the thing that sent somebody hunting.

two.score moved to Tests. It is a fixture, it names patches that only
exist in the test build, and it was in the music tree only because that
is where scores were when it was written - which the game disk build
found immediately by failing to compile it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 11:15:47 -04:00
AnachronautandClaude Opus 5 dfc9d9ef7f A score is written and a tune is what the machine reads
The convention, at the user's asking, and it is the one this project
already has everywhere else: a .asm is written and a .sbx or a .bin is
what the machine loads. A .score and a .tune are the same pair one
subject along.

It is not only tidiness. I read the user's splash.tune as a compiled tune
yesterday, dumped its header, and got a tick of seven and a half million
cycles and ninety seven patches out of what was plainly a text file.
Different names make that a thing nobody has to notice.

AND THE SPLASH WAS SILENT ON THE DISK THAT MATTERS. The play disk mirrors
every .asm and puts every app, and nothing on it put a compiled tune - so
make run-cosmos and make run-voyager both booted a Lander that read
/splash.tune, did not find one, and held the logo in silence. Only the
test disk had it, because I had added it there and stopped.

The makefile now compiles Programs/Tunes/splash.score with TuneC and puts
the result on the disk, and the mirror's prerequisite list learned about
.score files so that changing the music rebuilds the disk. That is the
same failure the mirror was built for, in a file type the mirror did not
know about yet: tune.asm went into Examples once, the image was not
remade, and it was simply not there.

Measured on the real disk: music from 0.9 s to 9.6 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 10:24:39 -04:00
AnachronautandClaude Opus 5 00b31b88af Lander opens on a splash, with music
The first customer for the player outside the program it was pulled out
of, and the argument for doing it: a splash needs no resident player at
all. Nothing else is happening while the logo is up, so Lander owns the
timer and all four channels exactly as Play does. Music UNDER a running
game is still deferred, and still the harder problem.

It polls the timer rather than being interrupted by it. Lander has no
vector segment and waits for the screen by reading port 0x30, so it waits
for a beat by reading port 0x50 - the same shape, and it brings no
handler that would have to be taken away before the game starts.

The tune is read off the disk. A missing one means the logo and silence
and the game starts anyway, the way a missing /lander.state means the
defaults stand.

FOUR THINGS THIS COST, each found by running it:

A subroutine cannot answer in A. CALL saves and restores it, so splashSkip
handed its caller back the A it already had - the channel number of the
last stepVoice - and the splash ended on its first pass whatever anybody
pressed. Q is what survives a RET, which nextRandom says twenty lines
away and I did not read.

Port 0x3D is how many window rows are SHOWN, and it is two once putGauge
runs and nothing before. A line written to row eleven went somewhere real
and was displayed nowhere.

A nought is not a keypress. It is what a recorded keyboard file holds
while nobody is typing, and a splash that took it for a key is one no
test could ever watch.

And skipping has to be free. Asked after blanking the window and reading
the file, a skipped splash still cost a fifth of a second - enough to
push the thruster test's early capture past the frame it looks at. Asked
first, it costs a pad read.

player.asm no longer asks a caller for Order0 to Order3: the voice records
name NoOrder instead, so a program whose tune comes from a file does not
have to define four order lists it never uses. That was the file case
finding a wart in the contract.

Every other Lander test now skips the splash with a space - a key the
game itself ignores, since it answers to q, z and the arrows - so they go
on testing what they tested. The one in sound.sh presses nothing, which
is what makes it the one that hears the music.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 10:06:20 -04:00
AnachronautandClaude Opus 5 6bb1565dea A tune's tick is the tune's own
useTune took the period out of a file's header and then Play wrote its
own straight over it, so every tune played at a sixteenth note at 120
beats a minute whatever it asked for. A tune with a #Tick of 0d250000
lasted half as long as it said.

Nothing noticed because every fixture in the suite asked for exactly the
tick Play had written into itself. A test that agrees with the bug by
coincidence is not a test, and the way to find out is a fixture that
wants something else - so slow.tune is two.tune with twice the period and
nothing else changed, and it has to last twice as long.

The period now belongs to whoever supplied the tune: useTune sets it from
the header, useBuiltIn sets its own, and the start code writes only the
control byte - which has to come after either of them, because writing
control with the run bit set is what loads the period.

Found while reading Play to see how a splash screen would drive the
player, which is a reminder that the second reader of a piece of code is
worth more than the first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 09:37:08 -04:00
AnachronautandClaude Opus 5 87467a7d3a Lander starts a flight where a flight starts
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 is that the numbers the
assembler wrote into a Data Segment are LOAD-time values, true once, and
a program that wants them true at every start has to say so itself.

Lander did not. Flying is 0x01 in the data and is only ever cleared, so a
second run began with the loop already over: the program started and
handed the machine straight back. Reproduced by running it, quitting, and
running again - the leftover game keys land at the shell prompt, which is
what a program that never read them looks like.

AND THE TEST FOUND A SECOND ONE, quieter and worse. The terrain seed is a
written number, so a fresh load always walks out the same moon - but
nextRandom moves it, and a second run generated a DIFFERENT moon. Nobody
decided that. A test comparing the whole screen found it without anybody
having had to think of it in advance, which is the argument for comparing
the picture rather than the variables somebody remembered to check.

Only what a flight needs to begin is put back. Anything not on the list
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
the list to be a decision rather than a sweep. The things that are simply
nought are a table of NAMES that the assembler turns into addresses, so
adding a variable that must start empty means adding it there and nowhere
else.

video.sh now runs Lander, quits, runs it again, and requires the same
picture a hundred frames in - the same moon, the same lander in the same
place, the same gauges. break.sh confirms it by taking the seed line back
out: 37,995 bytes of the picture differ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 09:02:50 -04:00
AnachronautandClaude Opus 5 b160c66110 The arpeggio's first chord is the note its comment says it is
tune.asm wrote 0x2A four times, labelled "; Gs" each time. 0x2A is F#2.
G# is 0x2C. The file has disagreed with itself since it was written.

It is not a near miss. The progression is Ab, Bb, C - a flat six and a
flat seven resolving home - and the other two chords are right: 0x2E is
A# and 0x30 is C. With F# the first one is a tritone against its own
third and stops being major at all.

Found by the user transcribing the piece into the new tune language,
where writing the notes out again by ear is what made the wrong one
audible. The number was carried across faithfully; the intent was not in
the number.

Nothing churns: it is one value and not a length, so the binary is the
same size and the recordings are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 22:15:58 -04:00
AnachronautandClaude Opus 5 fee2b1ef10 One missing patch says one thing
A patch file that could not be read left the name 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 produced seven
messages and only the first was worth reading.

A patch that cannot be read is still a patch that was NAMED. It is
registered either way now, with its bytes marked missing, so everything
below resolves the name and says nothing. Nothing is written regardless -
one problem is enough to stop that - so a patch with no bytes never
reaches a file.

The damage from the old behaviour was not the extra lines. It is that a
compiler which says one thing seven ways teaches people to read the last
line, which is the one that matters least.

Checked by counting: one missing patch, three voices using it, and the
count of messages mentioning it has to be one. break.sh confirms it by
putting the old behaviour back on the failure path alone - the first
attempt at that break stopped every tune compiling and the disk build
failed before any test ran, which is break.sh being right about a break
that proved nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 22:09:18 -04:00
AnachronautandClaude Opus 5 fb672d7761 A manual for writing tunes
The fifth document, and the one somebody would want first if they had a
piece of music and this machine. What a tune is, how to write one, what
the compiler refuses and why, how to build and play one, and the bytes it
holds for anything that would rather write one itself.

The last of those matters more than it looks: Tests/maketune.py writes
tunes without going through TuneC, and the suite checks the two agree
byte for byte. A format with two implementations needs a specification
they are both held to rather than one of them being the specification.

The refusals get a table of their own with the reason beside each,
because every one of them is something the PLAYER cannot notice - it has
no names, no lengths, and no way to tell "no starting instrument" from
"instrument nought" once a tune is loaded. Documented as reasons rather
than as rules, so that somebody meeting one knows what it saved them
from.

docs.sh now settles the manual against the compiler both ways: every
directive TuneC takes has a row, and the limits the manual quotes are the
compiler's own #defines. Verified with break.sh - a directive removed
from the manual and a limit raised in the compiler are both caught.

One that was not caught first time and should not have been: removing the
#Use row from one table left it documented in the other, which is the
check being right and my break being wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 21:20:55 -04:00
AnachronautandClaude Opus 5 2808688fa1 TuneC: a written tune becomes the bytes the player reads
The compiler, and the last thing the ladder was waiting for. 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.

  #Tick 0d125000
  #Patch Oboe oboe.patch
  #Voice 0d0 Oboe
  #Sequence Verse
    0d64 0d4  0d67 0d4  0d72 0d8
  #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. One rule across the machine rather than a third dialect -
and the rule earned itself immediately: the first tune I wrote said
"#Voice 0" and was refused, correctly, for a bare number.

WHAT IT REFUSES IS EVERYTHING THE PLAYER CANNOT NOTICE. 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.
A duration of nought is counted down to 255 and held, which sounds like a
hang rather than an error. And by the time a tune is loaded, "no starting
instrument" and "instrument nought" are the same byte - so the user's
ruling, that a voice with a part and no instrument is an error, can only
be kept here.

SoundPatch gains --blob, writing the same table as raw bytes. It 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 LFO bug lived for a fortnight. Patches
are found beside the tune and then on a -I path, the way an include is.

THE TEST IS THAT TWO IMPLEMENTATIONS AGREE. maketune.py lays the fixture
out by hand and TuneC compiles a written source, and the suite checks
they match byte for byte - the discipline SplitDisk and sbfs.asm are held
to, for the same reason: either alone is only self-consistent. The
fixture predates the compiler, so this is also TuneC checked against
something written before it existed. Four more checks cover the four
refusals.

Also: the SoundPatch binary was tracked, alone among the six tools, and
.gitignore lists every other one. Untracked, and TuneC added beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 21:11:36 -04:00
AnachronautandClaude Opus 5 bfc46d982e Play reads a tune from a file
The loader, and the reason it is small: everything in a tune is an offset
from wherever it was put, so taking one means adding the base to two
tables and pointing four voices at their order lists. No sequence is
walked. Nothing inside one is an address to be found and corrected, which
is the difference between a malformed tune that plays wrongly and one
that takes the loader with it.

  "SBTU", version, the tick in cycles, how many patches and sequences,
  offsets to the two tables, an order list each, and the patch each
  voice starts on.

The magic is checked before anything else, because from there on the
loader follows what the offsets name. break.sh shows what that guard is
worth: without it, handing Play a PROGRAM runs the machine away until the
cycle limit, rather than saying it is not a tune.

PatchTable, SequenceTable and VoiceStart became pointers, so the engine
does not care whether a tune came out of a file or was assembled in.
Play's built-in tune now hands over the same three addresses a loaded one
would, in eight lines - which is what keeps the two paths from drifting,
and what made this rung change no scheduler code at all.

Tests/maketune.py lays the fixture out byte by byte. IT IS NOT THE
COMPILER: the sequences and the patches are literal bytes and the only
thing computed is where each piece lands. That is the point - the loader
is checked by something that does not share its idea of the format, which
is the same reason SplitDisk and sbfs.asm share nothing but a
specification.

Both notes in the fixture are number 60, so the octave between them is a
0x80 command loading the second patch out of the file: the header, the
tick, the relocation, an order list and a patch from a file, measured in
one go. Play and the tune live on quiet.img rather than cosmos.img, so a
fixture does not move ten recordings every time it changes size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 20:58:24 -04:00
AnachronautandClaude Opus 5 5d9b39514b The player speaks indices, which is the shape a file has to be
Order lists hold one-byte sequence indices and 0x80 holds a one-byte
patch index. Two tables, PatchTable and SequenceTable, are the only
places an 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 arrays and nothing else. The alternative
is a loader that parses every sequence looking for addresses to correct,
which is a loader a malformed file can walk off a cliff.

Doing it now, while the tune is still assembled in, means the file form
and the assembled form are the same shape - so reading a tune from a file
will change no engine code at all. The whole point of the rung.

The patch each voice starts on moved from four calls in a row into
VoiceStart, four declared bytes, and loadStartPatches reads them. A
starting instrument is state and belongs where state goes: the user's
ruling is that a voice with undefined state is an error, prompted by
noticing that a program run a second time starts with the memory the
first run left, because loading is what initialises and running is not.

Order lists also halved in size, which was not the reason but is welcome.

Hand-writing the two tables is exactly the tedium the compiler exists to
remove - every sequence counted into its place, and moving one means
renumbering. Better to feel that here than after a tool has baked the
shape in.

Verified by rendering: bar for bar the same piece. break.sh confirms the
scaling, since an index is doubled to reach a two-byte entry and halving
that step lands on the wrong sequence and fails four checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 20:45:40 -04:00
AnachronautandClaude Opus 5 8029063bdc Sequences, order lists, and a command that changes the instrument
The rung between M3 and M4, and the point of doing it before the format:
the engine learns the tracker's model with the tune still assembled in,
so M4 becomes serialising a thing that exists rather than designing a
thing that does not.

A voice no longer walks one long track. 0xFF now means THIS SEQUENCE
ended, and the voice takes the next address from an order list of its
own. That is where repetition comes from, and it costs no notation: the
bass plays the same sequence in the first bar and the last and it is
written once. Per voice rather than one shared table of four-column rows,
because a voice's order cursor is then a pointer it advances by itself -
the same LDD and STD move everything else here makes. Four columns is how
it reads, not how it is stored.

Sequences also carry COMMANDS, which take no tick: the reader acts and
reads the next event on the same boundary. One is defined, 0x80, which
plays the rest of that voice on another patch, and the other 125 values
are left alone. A patch change reshapes whatever is still ringing on the
voice and nothing can be done about that - a channel has one set of
parameters and a note in its release is using them - so it is a fact
about the hardware and the cure is a rest, which is the composer's.

The engine moved to Libraries/player.asm rather than being copied into
the test a second time, now that it is big enough to drift. Play supplies
the tune and the beat; the library supplies the scheduler, the patch
loader and a voice's state. Verified by rendering: bar for bar identical
across the move.

AND THE TEST FOUND A REAL FLAW IN THE FORMAT, which is the whole argument
for building the reader first. The order list ended with 0x0000, on the
reasoning that no sequence could live below the 0x3000 this program is
based at. True of a loaded program, false of a boot image whose data
starts 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. It is 0xFFFF
now, which mirrors the 0xFF ending a sequence and is impossible
everywhere: a sequence at 0xFF00 or above has fewer bytes left than it
needs. An address is a poor place to hide a flag unless the address is
impossible in every program, not just this one.

One order list in the test now really ends, because otherwise nothing
reached the terminator at all: every voice sat on a long rest and the
break went unnoticed. With it, breaking the test reads garbage past the
end and the counter sums two notes at 781 hertz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 19:54:38 -04:00
AnachronautandClaude Opus 5 f9b08cf7f9 CosmOS lets every voice go when a program stops
A gate is a register on the sound device and only a program can drop one.
A program that has stopped cannot: it is gone. So a note left held
sustained until something else said otherwise, and nothing else did -
one program could leave the machine sounding for as long as it ran, with
nothing the person at it could do.

The shell already puts back the Stack, the vectors, the drive, the
working directory and the screen. This is the same list and the same
argument, and the fault path calls it too, for the stronger version of
the argument: a program that CRASHED is exactly the one that cannot tidy
up after itself, and a machine that will not stop humming is a poor place
to read an error message.

It does not make the device silent at once and does not pretend to.
Dropping a gate RELEASES a note rather than stopping it, so the patch's
release still runs. A bounded tail rather than an endless one is the part
the system can be responsible for without knowing what instrument the
program had built.

Hum exits while holding a note; Pause makes no sound and takes a couple
of million cycles, because the machine stops the moment the shell runs
out of input and a note quietened at that instant leaves no samples
behind to say whether it was. They are on a disk of their own so that a
fixture does not move the ten recordings that quote cosmos.img's listing.

THE CHECK CAUGHT ITSELF PASSING WRONGLY FIRST. Pause was missing from the
disk, the machine halted immediately, and the window that should have
held the tail was past the end of a render a twentieth of a second long -
an empty window's peak is nought, which is indistinguishable from silence.
So the sample count is asserted before anything is read from it. Verified
with break.sh: without the call the note is still ringing at 9869.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 18:54:46 -04:00
AnachronautandClaude Opus 5 7f96499438 Waiting for a key is not the same as time stopping
A console read that had nothing to hand over blocked in read(), and
while it was blocked nothing told the devices any time had gone by. The
whole machine stood still: the clock, the cursor, a disk settling, a note
decaying. Measured, four real seconds of sitting at the prompt bought
zero cycles.

A window has never done this. 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. The terminal path simply never got it.

How it showed was a note left in its release after a program exited.
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 at all.

So a terminal read now polls a frame at a time and tells the devices
about each one. Four seconds now buys 3.85 million cycles, which is the
wall clock, which is what the window has always done.

ONLY WHEN THERE IS REALLY A TERMINAL, the same rule the escape sequences
and the erase character follow. A file or a pipe keeps blocking: every
recorded test on this machine is reproducible because emulated time does
not depend on how fast the other end of a pipe is writing, and that must
stay true. The full suite is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 18:54:31 -04:00
AnachronautandClaude Opus 5 41d65d6346 Play waits for its own sound to finish before handing back
The gates all go down on the tick the tracks end, but a gate down is a
note RELEASED and not a note stopped: the oboe and the strings have a
release left to run. Rendered and measured, the tail dies eight tenths of
a second after the last event, and the ring was eight ticks - one second,
which sounds like enough and was two tenths short.

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 becomes
depends on the front end. Behind a window the tail finishes on its own.
On a terminal it does not, because emulated time stops while the machine
blocks on a key - measured, and the reason the leftover sound came out a
snippet per keystroke rather than fading.

Twelve ticks, which is a second and a half and covers the measured tail
with room over. It is a number about THESE FOUR INSTRUMENTS: a patch with
a longer release would want more, and the general answer is for the
system to quieten the device when a program stops, the way it puts the
screen back. That is not built and is worth deciding on rather than
guessing at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 16:13:10 -04:00
AnachronautandClaude Opus 5 ce1c0517aa M3: an instrument for each voice
Play loads a patch per channel before a note is played: an oboe for the
melody, strings under it, a square wave for the bass and a kalimba for
the arpeggio. It reads a count and that many parameter and value pairs -
the format SoundPatch writes - and understands nothing else about them,
which keeps SoundPatch the only thing that knows what soundThing's JSON
means.

FOUR PATCHES CAN BE UP AT ONCE, and that is the whole rung. It is the
first use of d361ea1: before it the LFOs belonged to the whole device, so
whichever patch loaded last owned them for every voice. Kalimba has LFO 0
switched off and the other three have it on, so on the old device this
piece would have played all four parts with the arpeggio's setting -
which is exactly the fault that stopped Lunar Porter's low fuel warning
trilling after the first landing of a run.

The eight patches are the user's, brought over from soundThing; four are
used here and the rest are a palette for the next piece.

The test now asks BOTH VOICES FOR NOTE 60 and gets an octave, because
their patches differ in one parameter and the lower is loaded first. A
device where a patch was global would have the second overwrite the first
and the two would answer in unison. Verified with break.sh: loading both
patches onto one channel flips the octave onto the wrong voice and the
ratio inverts to 0.499.

The test patches carry a release, and that was measured rather than
assumed: without one the first voice's note was still fading into the
second voice's window, which read 634 hertz - not a note, not an octave,
and a reminder that a crossing counter given two notes answers with
neither.

Play.sbx went 379 -> 740 bytes, which is four patches of 47 pairs each,
and is why ten recordings moved. makedisks.sh needed the Sounds include
path that Lander's line already had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 16:02:06 -04:00
AnachronautandClaude Opus 5 1687422619 A voice starts pointed at its track, without code to point it
There is no assembler bug. I reported one and was wrong.

A label written in the Data Segment does come out as its address, two
bytes, most significant first - implemented in populateOutputBuffers,
documented in the Assembler Manual, and correct. What misled me was the
test I checked it with: the label was the first thing in an unbased
Data Segment, so its address really was 0x0000, and I read the right
answer as an unfilled placeholder.

So Play was doing at run time what the assembler had already offered to
do at assembly time. The voice records now carry their track labels
directly, which is exactly the shape LDD reads, and nothing relocates on
this machine so the address written is the address it will have. That
takes out startVoice, its four call sites, and the eight SETDs that fed
them: 450 bytes to 379, and the initial state of a voice is now
something you can read rather than something you have to follow the code
to work out.

The comment claiming otherwise is gone from Play.asm, and the same
change is made in fourVoiceTest.

The music is unchanged - bar by bar the render matches to within one per
cent, which is the program loading a shade sooner because it is smaller.
Ten recordings moved for the same reason: 446 to 379, and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 15:24:02 -04:00
AnachronautandClaude Opus 5 79d1e2639b M2: four voices on one clock
Play is the scheduler: one tick, four cursors. Every voice keeps its own
place in its own track and its own count of how much longer the note it
is holding lasts, so a voice playing whole notes and a voice playing
eighths cost the same and never have to know about each other. They
share the tick and nothing else.

That is what makes the tick the smallest subdivision in the piece rather
than a note length - it is the only unit four parts can agree on.

A track is pairs of bytes: what to play, then how many ticks it lasts.
MIDI notes stop at 127, so the top of the byte was free and neither the
rest nor the end marker had to be invented - 0 is a rest and 0xFF ends
the track. Examples/tune.asm spent zero on its end marker and so could
not write a rest at all, which one voice can live with and four cannot:
the silences are what make them separate parts rather than a chord.

The state is four bytes a voice, cursor first because that is what LDD
and STD move - a pointer through a pointer, which is what lets this be a
loop over four voices instead of the same code four times. CALL preserves
A and DP0-2, so a caller says which voice it means in two instructions.

The piece is four bars of C, F, G, C with the parts moving at four
different rates, because that is the thing one channel cannot do. All
four channels get the same instrument, which is exactly what M3 replaces.

fourVoiceTest staggers two voices so each gets a stretch alone: middle C
while the other rests, the octave while the first is silent, then both.
The first two are measured for pitch and the third for level, because
TWO NOTES CANNOT BE ASKED THEIR PITCH - the crossing counter adds them
and answers 785 hertz, which is 262 plus 523 and a fact about nothing.
Verified with break.sh: dropping the channel select trips one check,
pointing both voices at one cursor trips three.

It shares Play's design and not its code, and is smaller - no track ends
in it, so there is no live flag and no count of what is still playing.

Play.sbx on the disk is why ten recordings moved: one added line each,
and the file count with it. Nothing else in them changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 15:03:12 -04:00
AnachronautandClaude Opus 5 1b251fb290 M1: the tune keeps a beat it sets, instead of one it borrowed
tune.asm counted the screen's frames, because until 6b51d63 the frame
was the only regular beat on the machine. It is now driven by the timer.

THE DURATIONS ARE NOT A CONVERSION OF THE OLD ONES. A frame is 16,667
cycles, so a sixteenth note at 120 beats a minute is seven and a half of
them and could not be asked for at all: the arpeggio was written as
seven whole frames, which is six and a half per cent fast, and the rest
of the piece was written to a tempo picked so its subdivisions landed on
whole frames. The tick is now 125,000 cycles, which IS that sixteenth
note, and the durations are what the music wanted in the first place -
an eighth is two ticks, a quarter four, the arpeggio one each.

The player's loop did not change shape: a note table, a wait on each
beat, the gate dropped at the end. Only what it waits on. The handler
stays a bare RETI, because taking the interrupt is what brings the line
down - a program that POLLED the timer would have to read 0x50, and the
comment now says so, since that is the trap.

Three checks in sound.sh, on a program that is the player's loop rather
than the tune: eight ticks last eight periods to within a tenth of a per
cent, and the seventh and eighth notes are measured where the ticks put
them. The middle one is the one that fails on the old timing - at seven
whole frames the boundary slides under the window and it reads 384 Hz,
which is neither note and is what a crossing counter says when given
two. Verified with break.sh by moving the timer's period a byte.

tune.sbx grew 306 -> 318 bytes, which is why ten cosmos recordings moved:
they quote a directory listing. Nothing else in them changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 14:42:45 -04:00
AnachronautandClaude Opus 5 35c6708e46 The shell takes spaces off both ends of a line, not just the front
Tab completion leaves a space after the word it finished, which is right
when another word is coming. When nothing else was coming, that space
stayed on the end of the line and a command taking a file name looked
for one whose name ended in a space:

  load greet.sbx      loaded, starting at 5000
  load greet.sbx      no such file      (the same line, finished with Tab)

Which took most of the good out of completion, since finishing a name
and pressing Return is the whole of what it is for. The existing tab
tests all typed more characters after completing, so none of them ever
submitted a completed line.

lineTrim already took the leading spaces off for indented blocks, so the
trailing ones come off in the same place, on the whole line, rather than
at each of the dozen commands that take a name. Walking back needs no
guard against running off the front: by then the first character cannot
be a space, and a line that was nothing but spaces has already become an
empty one.

CONSEQUENCE WORTH SEEING: echo no longer prints a trailing space it was
given, which is why cosmosTabPath's recording moved. That is the
conventional behaviour and it means what echo is handed is what somebody
would have typed, but it is a real change and not only a bug fix.

cosmosTrim covers a line completed and run straight away, the same
trailing space typed by hand, spaces at both ends at once, and a line of
nothing but spaces, which still has to do nothing rather than fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 12:23:28 -04:00
AnachronautandClaude Opus 5 8631a78229 Backspace, whatever the terminal calls it
CosmOS's line editor looks for 0x08, which is what Voyager's keyboard
sends. A POSIX terminal sends its own erase character instead, and on
most of them that is 0x7F.

It stayed hidden while the terminal was doing the editing: canonical
mode consumes the erase character itself and hands over a finished
line. Key mode turns ICANON off, which is the point of it, so from the
day the shell started editing its own line - 7360374 - Backspace worked
in Voyager and did nothing at all in the console-only emulator. The
Programming Manual already claimed the console normalised Backspace on a
terminal the way it normalises the arrow keys; the code did not.

The erase character is read from the terminal's own VERASE rather than
assumed to be 0x7F, because some terminals are set to 0x08 and a person
who has moved their erase key has said where it is. Forward Delete is
untouched and stays 0x86: two keys, two values.

Only when standard input really is a terminal. A file or a pipe holding
0x7F holds a byte somebody wrote, not a key somebody pressed.

Three checks in terminal.sh under a pseudo-terminal whose VERASE is set
on the slave side: erase at 0x7F arrives as Backspace, erase at 0x08
still does, and a 0x7F read from a file is left alone. The last one is
what fails on the obvious wrong fix of translating 0x7F unconditionally,
which was verified with break.sh along with the fix's removal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 11:56:17 -04:00
AnachronautandClaude Opus 5 019c93a587 The console draws a tab instead of dropping it
consoleDraw gave meanings to newline, carriage return and backspace and
dropped every other byte below the first glyph. A tab was one of those,
so it left no mark on the screen at all - while the same byte went down
the serial line, where a host terminal laid it out perfectly.

That is why a tab separated file read correctly and displayed wrongly.
Type and More were never at fault: they hand the file's bytes to the
console unchanged, and the console is where the tabs stopped. An
assembler symbol table came out with its fields run together.

A tab now moves the cursor to the next stop, eight columns apart, and
wraps when the next stop would reach or pass the last column - which is
what an ordinary character does at the edge, rather than a rule only
tabs obey. It MOVES rather than writing spaces, the way a terminal does:
a carriage return followed by a tab steps over what is on the line and
leaves it.

Kept in the console rather than expanded by Type, More, and every future
program that prints text.

Three checks in video.sh, each of which fails on a different mistake: a
tab renders the same screen as the spaces it stands for, one that runs
off the edge renders the same screen as a newline, and sixteen letters
tabbed across still have their ink - which is the one that fails if a
tab is implemented by writing spaces. Verified with break.sh both ways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 11:55:23 -04:00
AnachronautandClaude Opus 5 7dca141aae The vector's number goes beside its kind, not at the end of the line
Kind and Number together are what names a thing: Vector 16 and Device 32
are identifiers in a way that Vector on its own is not. Everything after
them - where it lives, what it is called, where it was written - says
something about it rather than naming it, so with the number at the far
end a line opened with a bare kind and closed with the fact that would
have told you what you were reading.

Fields are now Kind, Number, Address, Name, File, Line. A label still
carries a dash where its number would be, which reads as "this kind is
not numbered" rather than as a field that went missing.

Manual and the docs check follow. Verified with break.sh by swapping the
number and the address back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 11:03:11 -04:00
AnachronautandClaude Opus 5 f3d8985bc4 Vectors in the symbol table, with both of the numbers they have
A vector is the one thing about a program that nothing else can tell
you. A pinned vector has its number in the source that pinned it, but a
vector the assembler numbered has that number nowhere at all - not in
the source, not in the binary in any form a reader can find. Until now
there was no way to learn that a vector became number 64.

It also cost 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
meant searching for the vector, reading the handler's name off the
Vector Segment, and searching again. A vector row now names the handler
and gives the line the two were tied together on.

Both numbers, at the user's asking, 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 the machine dispatches on, the Address is
where the handler's address is stored, base plus twice the number. The
slot is computed with the same expression the loader is given, so what
the table says and what gets written there cannot drift apart. A vector
a program only declares is listed too - that is how a program says which
vectors it calls, and how two programs can be checked against each other
for agreeing about a number.

A device has no name of its own, being named by the port it is plugged
into, so it is listed under its handler.

The first field is now Kind rather than Memory, because Vector and
Device are not memories. Sorted Program, Data, Vector, Device.

docs.sh checks the six fields against the manual and against real dumps
of two programs - Keys, a loadable program with all four kinds, and
cosmos, a boot image whose segments both start at zero. It now also
checks that a row's name really appears on the line the row names, which
is what catches the string-newline bug fixed in ca6c8ca coming back.
Verified with break.sh four ways: wrong slot arithmetic, vectors
dropped, a field renamed in the manual, and that bug reintroduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 10:51:50 -04:00
AnachronautandClaude Opus 5 ca6c8ca6ad A newline inside a string is still a newline
readToken consumed a string literal character by character and counted
none of the newlines in it, so every line number after a multi-line
string was short by one - and by one more for each one after that.

cosmos.asm has seven such strings spanning thirteen lines, so by the end
of the file the assembler was reporting labels thirteen lines early.
That was never only a cosmetic problem: the same counter is what every
error message names, so an error anywhere after the help text was
pointing at somebody else's code, thirteen lines away, with no sign that
it was doing so.

Found because the symbol table started publishing line numbers, which
made a wrong one something you could look at. Verified against all 797
of cosmos.asm's labels, every one of which now names the line it is
actually on. The check that keeps it that way comes with the next
commit, which is what gives it something to check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 10:51:36 -04:00
AnachronautandClaude Opus 5 3527812c41 A symbol table says which memory, and where the name was written
The dump was an address and a name. Both of the questions it gets asked
were only half answered.

"What is at this address" was ambiguous, because Program and Data are
separate memories and an address alone does not say which one. That is
easy to miss in a loadable program, where the segments are usually based
far apart - and immediate in a boot image, where both start at zero:
replCalculator has a Program 0003 and a Data 0003 and the old file
printed both as "0003 <name>".

"Where is this defined" was not answered at all, and it is the one that
matters more as a program grows. A name defined once and called in forty
places is hard to find by searching. Lander's table names five files
besides its own; CosmOS and its libraries define over a thousand names
across a dozen.

So: memory, address, name, file, line, separated by tabs, sorted by
memory and then address with Program first. Tabs because that makes it a
table cut, awk and sort already read, and no heading line because
nothing should have to know to skip one. Everything needed was already
being passed to addLabel and thrown away; the file name points at the
copy the include list owns, which outlives the label table.

The manual describes the five fields, and docs.sh now settles that
description against a real dump - the shape, not the values, so that an
example cannot go stale and turn editing a program into editing a
manual. Verified with break.sh three ways: a reordered field, a dropped
field, and a field renamed in the manual.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 10:21:06 -04:00
AnachronautandClaude Opus 5 7a55cfe151 Say that an option's file is one the assembler writes, and check we said it
-S was added without a row in the Assembler Manual, and the usage it
printed listed a bare "-S <file>" with no long name and no statement of
what the file is for. That is not merely incomplete, it is misleading:
"-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 is answered with "No source file specified" on a command
line that plainly names a source. The error described the hole the
mistake left and hid the mistake.

So the usage now prints the long names, says outright that every <file>
is a path it writes and the source is the last argument on its own, and
ends with a whole example command. When the source is missing and a
file-taking option was given, the error says which options take a path
to write. The manual gains the -S row it never had, a warning in the
same words, and a sentence on what a symbol dump is for.

Documenting it twice is how it went wrong once, so docs.sh now settles
both against getopt's own option table: every option the assembler takes
has a row in the manual and a line in its own usage. Verified with
break.sh against the manual row and the usage line separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 09:40:21 -04:00
AnachronautandClaude Opus 5 cdee9acfae break.sh believes a suite's exit status, not the shape of its output
It decided whether a suite noticed by grepping its output for a
"N passed, M failed" summary line. That is right for the suites that
print one, since they print it only when something failed - and
impossible for the three that never print one at all. docs, terminal
and voyager report in their own words, so a break any of them caught
loudly was reported as "NOTHING CAUGHT THE BREAK".

That is the one wrong answer the tool exists never to give, and it was
turning up in a third place: the header already tells the story of the
first two. It made the whole docs suite unverifiable by the harness the
project uses to decide whether a check is worth having.

Every suite already exits nonzero when it fails, so that is the signal
now. The summary line is still printed where a suite keeps a count, and
the suite's own last line stands in where it does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 09:40:10 -04:00
Anachronaut d361ea1e46 An LFO belongs to its channel, not to the whole device
The two LFOs lived in the Synth, so four channels shared them and whichever patch
loaded last owned them for every voice at once. A sound with its LFO switched off
silenced the trill under a sound that was still playing - which is what made Lunar
Porter's low fuel warning intermittent: the first landing, docking or crash of a run
took its trill away, and it was right again next time the machine started.

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

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

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

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

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

Cost, measured: four channels sounding continuously for 400 seconds of audio takes
5.0 s of wall clock against 4.59 s before, about 9% of total emulator time. Half of
that is wasted on voices that cannot sound, since VOICE_COUNT is 8 and there are
four channels; recovering it would mean diverging the vendored file, which is not
worth it at this price.
2026-09-04 22:20:26 -04:00
Anachronaut 3ca5f193e6 The warning's trill, and why it only sometimes came out
An LFO belongs to the DEVICE and not to a channel. There are two of them
against four voices, and a patch carries LFO settings the way it carries
everything else - so whichever patch was loaded last owns both of them,
for every voice at once.

The warning's trill is a saw LFO on the pitch. The patches on channel
three, the bang and the latch, carry an LFO that is switched off, and
they load at the moment they are used. So the first landing, docking or
crash of a run took the trill away and left a plain tone, and it was
right again next time the machine started. Correct until something
unrelated plays is the worst shape a fault can have.

The same thing had already happened silently at startup: the instruments
were set up in order, so the warning's LFO settings, written last, sat on
top of the rumble's and the rumble never had its own at all.

So nothing is set up once any more. Each sound loads its patch
immediately before its note - forty-odd writes at a moment already making
a sound - and is then whatever its patch says, whatever played before it.
Measured after a landing: 1056, 660, 516 hertz, then up to 1698 and down
again. Two sweeps of the saw, which is the trill.

What it does not fix, because it cannot: two sounds overlapping still
share the LFOs, so a warning going off mid-burn re-tunes the rumble for
as long as it lasts. With two between four that is the device.

Three checks at the device level, where the trap can be stated exactly: a
routed LFO bends a pitch (184 Hz against the 262 the note asked for),
another channel's patch takes it away (272, the note itself), and saying
it again gets it back (184). Written up in the Programming Manual beside
the LFO mode, since the next program to want two sounds will meet it too.
2026-09-04 19:44:02 -04:00
148 changed files with 8962 additions and 630 deletions
+7
View File
@@ -9,6 +9,12 @@
/SplitDisk
/SplitLint
/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
/claudeResume.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
# not cleaned - it is the one place in here where something made ON the machine can live.
/Disks/
*.sym
+353 -28
View File
@@ -48,6 +48,20 @@ start:
; floor the system gives every program does not.
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 setView ; The block every gauge below reads its screen numbers out of.
SETD.0 Wide
@@ -74,17 +88,19 @@ start:
CALL mulReady ; The quarter square table, once. The orbit below needs a product.
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 carvePads
CALL drawMoon
CALL putLander
CALL putGauge
CALL putOrbital
CALL putThrust ; The instruments, once, the way the tiles are.
CALL putAlarm
INIA 0x01
OUTA 0x02 ; Key mode.
; ---- Is there a controller ----
;
@@ -2796,20 +2812,36 @@ runPend:
BNA runPend
RET
; ---- The thruster, which is a bang and then a rumble ----
; ---- Why every sound loads its patch at the moment it plays ----
;
; Both on their own channels because one is struck and the other is HELD: the rumble's note
; goes on when a thruster lights and its gate does not come up until every thruster is out.
putThrust:
RSTA
OUTA 0x41
SETD.0 ThrustOnPatch
CALL playPatch
INIA 0x01
OUTA 0x41
SETD.0 ThrustHoldPatch
CALL playPatch
RET
; CHANNEL THREE IS TWO INSTRUMENTS. The bang and the latch share it, and a patch is the whole
; voice, so the channel has to be told which of them it is about to be. Loading at the moment
; of the note costs fifty writes at a moment already doing more than fifty, and means a sound
; is what its patch says whatever played before it.
;
; The other three channels have one sound each and could be set up once at startup. They are
; not, because a single rule is worth more here than the writes it saves: every sound loads
; its own patch, so there is no question of which sounds are safe to set up early and which
; are not.
;
; ---- What this used to be working around ----
;
; 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 ----
;
@@ -2852,6 +2884,8 @@ thrustHaveSet:
BRQ thrustNoPop ; Nothing newly lit, whatever else is still burning.
RSTA
OUTA 0x41
SETD.0 ThrustOnPatch
CALL playPatch
INIA 0d36
OUTA 0x44 ; The bang of it catching.
thrustNoPop:
@@ -2870,6 +2904,8 @@ thrustNoPop:
INIA 0x01
STA.0
OUTA 0x41
SETD.0 ThrustHoldPatch
CALL playPatch ; After the pop's, so the held sound owns the LFO while it runs.
INIA 0d60
OUTA 0x44 ; Held, until the gate comes up.
BRI thrustRemember
@@ -2965,16 +3001,11 @@ watchFuelDone:
; Effects count down from the top: the bang has three and this has two. They are separate
; channels rather than one reused, because a crash while the warning is still sounding should
; not cut it off - and on a machine with four voices there is no reason to be clever about it.
putAlarm:
INIA 0x02
OUTA 0x41
SETD.0 AlarmPatch
CALL playPatch
RET
alarm:
INIA 0x02
OUTA 0x41
SETD.0 AlarmPatch
CALL playPatch ; Its own LFO back, whatever the last sound left there.
INIA 0d72 ; An octave over middle C, which is where a warning lives.
OUTA 0x44
RET
@@ -3182,6 +3213,76 @@ showHeightSized:
; 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
; 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:
SETD.0 StateName
SETD.1 StateBuffer
@@ -3444,6 +3545,188 @@ groundLevel:
STA.0
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 ----
;
; 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
; character n less thirty two.
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
OUTA 0xE3
INIA 0xC1
OUTA 0xE3 ; Which needs A, so the row came out of it a moment ago.
LDA.1 ; DP1 still holds SayRow.
INIB 0xC0
CCF
ADD ; Window row n begins at 0xC000 plus n times 256.
MVQA
OUTA 0xE4
RSTA
OUTA 0xE5 ; Window row one begins at 0xC100.
OUTA 0xE5
SETD.1 SayAt
STA.1
@@ -3975,6 +4267,22 @@ Wide:
;
; 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.
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:
"/lander.state"
StateBuffer:
@@ -4028,6 +4336,17 @@ KeyHeld:
Flying:
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:
#Reserve 0d128
@@ -4372,6 +4691,12 @@ LanderArtAt:
#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 alarm.asm
+411
View File
@@ -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
+137
View File
@@ -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
+100
View File
@@ -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
+458
View File
@@ -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
+124 -16
View File
@@ -1,5 +1,22 @@
; 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,
; 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
@@ -72,10 +89,50 @@ labAddFresh:
CALL numCompare
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.2 LabCount
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
SETD.1 LabEntry
@@ -149,14 +206,39 @@ labAddNo:
labFind:
SETD.2 LabSought
STD.0.2
SETD.0 LabWhich
CALL numZero
labFindLoop:
SETD.0 LabWhich
; ---- Halving the table rather than walking it ----
;
; 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
CALL numSet
labFindStep:
SETD.0 LabLo
SETD.2 LabHi
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
SETD.1 LabEntry
@@ -169,16 +251,33 @@ labFindLoop:
INCD.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
LDD.0.1
SETD.1 LabSought
LDD.1.1
CALL sameText
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
BRI labFindLoop
BRI labFindStep
labFindGot:
SETD.1 LabEntry
@@ -285,6 +384,12 @@ LabEntry:
0x00 0x00
LabOffset:
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:
0x00 0x00
LabLength:
@@ -297,18 +402,21 @@ LabEnd:
; 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
; build: 555 labels and about 6,800 bytes of name, against CosmOS's 650 and 8,081 - CosmOS is
; the bigger of the two now, and was the smaller when this was written. Running into either
; build: the assembler is about 555 labels, against CosmOS's 1,341 and 16,758 bytes of name
; 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
; 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
; half that, and the names ran out first: 8,081 bytes of 8,192, which is a hundred and
; eleven - and the thing that ran into it was one ordinary piece of work adding eighteen
; labels. THESE TWO MUST AGREE WITH THE SCRATCH MAP, which says where the room actually is.
; Two thousand and forty eight names, and twenty six kilobytes to hold them in. Both have
; now been doubled twice, and BOTH TIMES THE NAMES RAN OUT FIRST with the index a couple of
; hundred behind them - 8,081 of 8,192 the first time, 16,758 of 16,384 the second. A name
; 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:
0x06 0x00
0x08 0x00
LabRoom:
0x40 0x00
0x68 0x00
LabNamed:
": "
+39
View File
@@ -112,6 +112,45 @@ numCompare:
numCompareDone:
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.
;
; Down here rather than with the label table, where it started, because four separate
+22 -10
View File
@@ -20,10 +20,10 @@
; bad enough; a stale number sitting next to the correction is worse, because whichever
; one a reader takes is a coin toss.
;
; 0x5000 6144 the label index, 1536 entries of four
; 0x6800 16384 the label names, packed end to end
; 0xA800 256 one block of the output file, on its way to the disk
; 0xA900 17152 free
; 0x5000 8192 the label index, 2048 entries of four
; 0x7000 26624 the label names, packed end to end
; 0xD800 256 one block of the output file, on its way to the disk
; 0xD900 1792 free
; 0xE000 1792 the vector names, 64 entries of twenty eight
; 0xE700 2048 the reader's stack, six levels of 301
; 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
; 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
; 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.
;
; 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
; output; the assembler itself is 555 labels, about 6,800 bytes of name and 11,648 of output. The
; second is bigger than the first, which is worth knowing: the hardest thing this assembles
; is not the operating system, it is itself.
; and the largest one it will be asked to read. Measured 2026-09-06: CosmOS is 1,341 labels
; and 16,758 bytes of name, and the assembler itself is far smaller. It was the other way
; round when this file was written - "the hardest thing this assembles is not the operating
; 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:
0x50 0x00
ScratchLabArena:
0x68 0x00
0x70 0x00
ScratchWindow:
0xA8 0x00
0xD8 0x00
ScratchVecNames:
0xE0 0x00
ScratchSrcStack:
+194 -10
View File
@@ -105,6 +105,143 @@ srcRewindNo:
ADD ; Q is not zero: it is not.
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
; else at the end of the file.
srcNext:
@@ -112,11 +249,29 @@ srcNext:
LDA.0
BNA srcAtEnd
; Is the buffer used up? 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.
; ---- Is the buffer used up? ----
;
; 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.2 SrcCount
CALL numCompare
LDA.0
LDB.2
CCF
SUB
BNQ srcHaveByte
INCD.0
INCD.2
LDA.0
LDB.2
CCF
SUB
BNQ srcHaveByte
CALL srcLoad
BNQ srcAtEnd
@@ -129,19 +284,31 @@ srcHaveByte:
STD.0.1
SETD.0 SrcChar
STA.0
SETD.0 SrcAt
CALL numStep
; A newline is what makes the next character part of the next line. Counting it here,
; as it is handed out, means the line number always describes the character just given.
SETD.0 SrcChar
LDA.0
; ---- A newline is what makes the next character part of the next line ----
;
; Counted here, as the character is handed out, so the line number always describes the
; 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
XOR
BNQ srcNextDone
BNQ srcNextStep
SETD.0 SrcLine
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:
RSTA
RSTB
@@ -576,6 +743,23 @@ SrcStateBytes:
SrcDepthLimit:
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
; 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.
+10 -6
View File
@@ -54,13 +54,17 @@ tokSkip:
BNQ tokBegin
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
SETD.0 TokChar
LDA.0
INIB 0x0A
XOR
BNQ tokComment ; Everything up to the newline belongs to the comment.
BRI tokSkip
tokBegin:
+16
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+290
View File
@@ -1916,6 +1916,278 @@ sbfsEntryBounds:
sbfsBoundsDone:
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.
; Q is zero if there was room.
;
@@ -3691,6 +3963,24 @@ SbfsScratch1:
SbfsPathWanted:
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:
0x00 0x00
SbfsFileBlocks:
+93 -8
View File
@@ -72,7 +72,7 @@ scriptSlotAt:
DECA
BRA scriptSlotDone
scriptSlotStep:
DPUP.3 0d71
DPUP.3 0d205
DECA
BNA scriptSlotStep
scriptSlotDone:
@@ -80,7 +80,7 @@ scriptSlotDone:
; Seventy bytes, DP0 to DP1.
scriptCopyState:
INIB 0d71
INIB 0d205
scriptCopyByte:
LDA.0
STA.1
@@ -154,8 +154,21 @@ scriptOpenFirst:
INIB 0d63
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
SWI osFileInfo
CALL scriptGiveDriveBack
BRQ scriptOpenThere
INIA 0x01
BRI scriptOpenFailed
@@ -426,6 +439,7 @@ scriptFill:
OR
BRQ scriptFillNoMore
CALL scriptKeepDrive
SETD.0 ScriptName
SETD.1 ScriptBlock
SETD.2 ScriptIndex
@@ -433,6 +447,7 @@ scriptFill:
INCD.2
LDB.2
SWI osFileBlock
CALL scriptGiveDriveBack
BNQ scriptFillNoMore
; DP3 is how many bytes came back. The nought goes after them.
@@ -488,6 +503,40 @@ scriptFillNoMore:
ADD
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.
scriptSkipLine:
CALL scriptByte
@@ -541,11 +590,35 @@ LoudWord:
ScriptDepth:
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
; where a script has got to. Saving it is one copy because of this order, and nothing else
; may be put between them.
; Name, blocks left, next block, where in the block, what it was given, and whether it is
; echoing: the whole of a running script. Saving it is one copy because of this order, and
; 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:
#Reserve 0d64
ScriptBlocks:
@@ -574,10 +647,22 @@ ScriptLineBlocks:
ScriptQuiet:
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
; 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:
#Reserve 0d284
#Reserve 0d820
ScriptInto:
0x00 0x00
ScriptRoom:
+57
View File
@@ -185,3 +185,60 @@
; 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.
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
View File
@@ -8,26 +8,28 @@
; 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.
;
; The screen finishes a frame sixty times a second and will say so. That is the only regular
; beat on this machine, and it is counted in the machine's own cycles, so this plays at the
; same speed whether the emulator is running at a megahertz or as fast as it can go. Every
; duration below is in frames: 30 is half a second.
; The timer is the other half. A period in cycles, a repeat bit, and a line when one has gone
; by: a beat this program SETS rather than one it borrows. Every duration below is a count of
; ticks, and the tick is named in the machine's own cycles, so this plays at the same speed
; 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.
; 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.
; ---- The tick is a sixteenth note, and that is the whole point ----
;
; ---- 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
; is a whole number of 16.67 ms, so a note worth a third of a beat cannot be written at all -
; and the way round it is to pick a tempo whose subdivisions happen to land on whole frames,
; which is making the tune fit the machine rather than the other way round. The theme below
; was written to the frame and is a few cents of tempo away from what it wants to be.
; This program used to count the SCREEN's frames, because until the timer existed the frame
; was the only regular beat on the machine. A frame is 16,667 cycles, so a sixteenth note is
; seven and a half of them and could not be asked for at all. The arpeggio below was written
; as seven frames, the nearest whole one, which is six and a half per cent fast; and the way
; 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
; the screen should not be the clock is that a display refresh and a music routine have no
; reason to share a rate.
; SO THE DURATIONS HERE ARE NOT A CONVERSION OF THE OLD ONES. They are what the music wanted
; in the first place, now that it can be written down: an eighth is two ticks, a quarter is
; 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 ----
;
@@ -143,10 +145,18 @@ start:
; ---- The beat ----
;
; Ask the screen to interrupt at each frame, and let interrupts in. The screen does not do
; this unless it is asked.
; The period first and the control byte second, because writing the control byte with the
; 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
OUTA 0x35
OUTA 0x52
INIA 0xE8
OUTA 0x53
INIA 0x48
OUTA 0x54 ; 0x01E848 is 125,000.
INIA 0x07
OUTA 0x51 ; Run, repeat, interrupt.
SIF
; ---- The tune ----
@@ -160,11 +170,11 @@ nextNote:
BRA finished
OUTA 0x44 ; Writing the note is what starts it.
INCD.0
LDB.0 ; How many frames it lasts.
LDB.0 ; How many ticks it lasts.
INCD.0
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
BNB holdNote
@@ -175,9 +185,10 @@ holdNote:
BRI nextNote
finished:
; Let the last note ring out rather than cutting it off, then put the screen back the way it
; was found - and stop asking to be interrupted before taking away what catches it.
INIB 0d45
; Let the last note ring out rather than cutting it off, then give the timer back - stopping
; it before taking away what catches it, because this program is about to stop existing and
; an interrupt with no handler installed is a fault.
INIB 0d6
lastRing:
WAIT
DECB
@@ -185,13 +196,17 @@ lastRing:
CIF
RSTA
OUTA 0x35
OUTA 0x51
SWI osExit
; Sixty times a second, and it has nothing to do. WAIT only needs something to have happened,
; and this is the something. A handler still has to exist: an interrupt with nothing installed
; to catch it is a fault.
frame:
; 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.
;
; 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
#Data
@@ -200,85 +215,85 @@ frame:
; ---- 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, 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.
Tune:
0d60 0d15 ; C
0d64 0d15 ; E
0d67 0d15 ; G
0d72 0d30 ; C, an octave up, held twice as long
0d71 0d15 ; B
0d67 0d15 ; G
0d64 0d15 ; E
0d60 0d45 ; and home
0d60 0d2 ; C
0d64 0d2 ; E
0d67 0d2 ; G
0d72 0d4 ; C, an octave up, held twice as long
0d71 0d2 ; B
0d67 0d2 ; G
0d64 0d2 ; E
0d60 0d6 ; and home
0x00
Theme:
0x30 0d15 ; C
0x35 0d15 ; F
0x3C 0d15 ; C+
0x40 0d30 ; E+
0x48 0d30 ; C++
0x45 0d30 ; A+
0x47 0d30 ; B+
0x43 0d45 ; G+
0x30 0d2 ; C
0x35 0d2 ; F
0x3C 0d2 ; C+
0x40 0d4 ; E+
0x48 0d4 ; C++
0x45 0d4 ; A+
0x47 0d4 ; B+
0x43 0d6 ; G+
; repeat four times.
0x2A 0d7 ; Gs
0x33 0d7 ; Ef
0x3C 0d7 ; C+
0x2C 0d1 ; Gs
0x33 0d1 ; Ef
0x3C 0d1 ; C+
;
0x2A 0d7 ; Gs
0x33 0d7 ; Ef
0x3C 0d7 ; C+
0x2C 0d1 ; Gs
0x33 0d1 ; Ef
0x3C 0d1 ; C+
;
0x2A 0d7 ; Gs
0x33 0d7 ; Ef
0x3C 0d7 ; C+
0x2C 0d1 ; Gs
0x33 0d1 ; Ef
0x3C 0d1 ; C+
;
0x2A 0d7 ; Gs
0x33 0d7 ; Ef
0x3C 0d7 ; C+
0x2C 0d1 ; Gs
0x33 0d1 ; Ef
0x3C 0d1 ; C+
; next chord
0x2E 0d7 ; Bf
0x35 0d7 ; F
0x3E 0d7 ; D+
0x2E 0d1 ; Bf
0x35 0d1 ; F
0x3E 0d1 ; D+
; next chord
0x2E 0d7 ; Bf
0x35 0d7 ; F
0x3E 0d7 ; D+
0x2E 0d1 ; Bf
0x35 0d1 ; F
0x3E 0d1 ; D+
;
; next chord
0x2E 0d7 ; Bf
0x35 0d7 ; F
0x3E 0d7 ; D+
0x2E 0d1 ; Bf
0x35 0d1 ; F
0x3E 0d1 ; D+
;
; next chord
0x2E 0d7 ; Bf
0x35 0d7 ; F
0x3E 0d7 ; D+
0x2E 0d1 ; Bf
0x35 0d1 ; F
0x3E 0d1 ; D+
; Finally on the C major
0x30 0d7 ; C
0x37 0d7 ; G
0x40 0d7 ; E+
0x30 0d1 ; C
0x37 0d1 ; G
0x40 0d1 ; E+
;
0x30 0d7 ; C
0x37 0d7 ; G
0x40 0d7 ; E+
0x30 0d1 ; C
0x37 0d1 ; G
0x40 0d1 ; E+
;
0x30 0d7 ; C
0x37 0d7 ; G
0x40 0d7 ; E+
0x30 0d1 ; C
0x37 0d1 ; G
0x40 0d1 ; E+
;
0x30 0d7 ; C
0x37 0d7 ; G
0x40 0d7 ; E+
0x30 0d1 ; C
0x37 0d1 ; G
0x40 0d1 ; E+
;
0x18 0d60 ; C bass
0x18 0d8 ; C bass
0x00
#Vectors
Boot start
Device 0x30 frame
Device 0x50 tick
+194
View File
@@ -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
+472
View File
@@ -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
+50
View File
@@ -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
}
+50
View File
@@ -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
}
+54
View File
@@ -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
}
+50
View File
@@ -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
}
+50
View File
@@ -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
}
+50
View File
@@ -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
}
+50
View File
@@ -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
}
+50
View File
@@ -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
}
+55
View File
@@ -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
+55
View File
@@ -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
+55
View File
@@ -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
+55
View File
@@ -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
+67
View File
@@ -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
+60
View File
@@ -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
+82
View File
@@ -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
+179
View File
@@ -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
+70
View File
@@ -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
+47
View File
@@ -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
+40 -4
View File
@@ -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/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/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/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 |
@@ -116,7 +117,7 @@ Manual.
## 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
@@ -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`.
## 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
```
./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
@@ -640,7 +674,7 @@ $(BUILD)/%.bin: %.asm
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
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
@@ -659,7 +693,7 @@ correct.
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
free, leaks, and undefined arithmetic, takes about twice as long, and puts the ordinary
binaries back when it finishes.
@@ -676,7 +710,7 @@ in the [SplitBit Test Manual](SplitBit%20Test%20Manual.md).
## 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.
@@ -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.
**[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.
## License:
BIN
View File
Binary file not shown.
+26 -5
View File
@@ -95,15 +95,28 @@ void assemblerCleanup(intermediateElement *intermediateArray, int arraySize, cha
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) {
printf("Usage: %s [OPTIONS] <sourcefile>\n", programName);
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(" -o <file> Write the output to this path instead of alongside the source.\n");
printf(" -I <dir> Look in 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(" -S <file> Write every label and the address it was given, in address order.\n");
printf(" -h, --help Display this help message.\n");
printf(" -o, --output <file> Write the assembled output here, instead of alongside the source.\n");
printf(" -I, --include <dir> Search this directory for included files. May be given more than once.\n");
printf(" -M, --depend <file> Write a make rule here, naming every source that went into the output.\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("\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
@@ -207,6 +220,14 @@ int main(int argc, char *argv[]) {
if (optind >= argc) {
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]);
return 1;
}
+9
View File
@@ -309,6 +309,15 @@ int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber)
// Step 3: Handle string literals
if (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)) {
buffer[i++] = c;
} else {
+146 -25
View File
@@ -21,25 +21,89 @@ int debugSecondPass = 0;
Label labelArray[MAX_LABELS];
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 ----
//
// Every label and the address it was given, in address order. The assembler knows this and
// nothing else 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.
// Every label with the memory and address it was given and the place it was written, one
// per line, fields separated by tabs. The assembler knows all of this and nothing else
// 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
// called says a great deal and names nothing, so the answer arrives as a list of numbers
// and somebody has to work out by hand which routine each one is inside. With this, a
// tally of call targets becomes a list of routine names.
// WHAT IT IS FOR is two questions. The first is where a program spends its time - counting
// which addresses get called says a great deal and names nothing, so the answer arrives as
// a list of numbers and somebody has to work out by hand which routine each one is inside.
// 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
// is at this address", and a label table is small enough that sorting it is free.
static int byAddress(const void *left, const void *right) {
const Label *a = left, *b = right;
// TABS, so that the file is a table every ordinary tool already understands - cut -f3, awk
// -F'\t', sort -k1,2 - and so that a name never has to be quoted. No header line, for the
// same reason: nothing that reads it should have to know to skip one.
//
// 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) {
return a->address < b->address ? -1 : 1;
}
return strcmp(a->label, b->label);
return strcmp(a->name, b->name);
}
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);
exit(1);
}
Label *sorted = malloc((size_t)labelCount * sizeof(Label));
if (!sorted) {
int rowCount = labelCount + vectorArrayCount;
SymbolRow *rows = malloc((size_t)rowCount * sizeof(SymbolRow) + 1);
if (!rows) {
fprintf(stderr, RED "Error: Out of memory writing the symbol file.\n" RESET);
fclose(file);
exit(1);
}
memcpy(sorted, labelArray, (size_t)labelCount * sizeof(Label));
qsort(sorted, (size_t)labelCount, sizeof(Label), byAddress);
int n = 0;
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);
}
@@ -124,6 +231,8 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName,
labelArray[labelCount].label = cleanedLabel;
labelArray[labelCount].address = address;
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);
labelCount++;
} else {
@@ -185,9 +294,6 @@ void populateLabelTable(intermediateElement *intermediateArray, int arraySize) {
int findLabelAddress(const char *labelName);
VectorEntry vectorArray[MAX_VECTORS];
int vectorArrayCount = 0;
int vectorCount() {
return vectorArrayCount;
}
@@ -197,6 +303,9 @@ void freeVectorList() {
if (vectorArray[i].name) {
free(vectorArray[i].name);
}
if (vectorArray[i].handlerName) {
free(vectorArray[i].handlerName);
}
}
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,
int declaredOnly, intermediateElement *element) {
int declaredOnly, intermediateElement *element,
const char *handlerName) {
if (vectorArrayCount >= MAX_VECTORS) {
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].handler = handler;
vectorArray[vectorArrayCount].declaredOnly = declaredOnly;
vectorArray[vectorArrayCount].handlerName = handlerName ? strdup(handlerName) : NULL;
vectorArray[vectorArrayCount].fileName = element->fileName;
vectorArray[vectorArrayCount].lineNumber = element->lineNumber;
vectorArrayCount++;
}
@@ -345,7 +458,8 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
int handlerToken = nextVectorToken(intermediateArray, arraySize, portToken + 1);
uint16_t handler = resolveHandler(intermediateArray, handlerToken, "Device");
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);
continue;
}
@@ -383,6 +497,12 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
}
vectorArray[already].handler = resolveHandler(intermediateArray, handlerToken, token);
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);
continue;
}
@@ -429,12 +549,13 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
if (!hasHandler) {
// 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.
addVector(token, index, SOFTWARE_VECTOR_BASE, 0, 1, &intermediateArray[i]);
addVector(token, index, SOFTWARE_VECTOR_BASE, 0, 1, &intermediateArray[i], NULL);
i = handlerToken;
continue;
}
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);
}
}
+17 -2
View File
@@ -27,10 +27,19 @@ typedef struct {
char* label;
uint16_t address;
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;
// Every label and the address it was given, in address order, so that a tally of
// addresses can be turned back into a list of routine names.
// Every label and every vector, with where it lives and where it was written, so that a
// tally of addresses can be turned back into a list of routine names.
void writeSymbolFile(const char *path);
// 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 handler; // Where the handler ended up.
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;
void freeLabelList();
+59
View File
@@ -433,6 +433,56 @@ static uint16_t countFree(const Directory *directory, const Superblock *super) {
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 ----
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");
// 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
// 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
+207 -1
View File
@@ -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) {
if (consoleTerminalRaw || !consoleTerminalSaved) {
return;
@@ -105,6 +150,7 @@ void consoleRestore(void) {
// is waiting sees the signal it expected rather than a machine that exited quietly.
static void consoleFatalSignal(int signalNumber) {
consoleReleaseTerminal();
consolePlainAgainDying();
signal(signalNumber, SIG_DFL);
raise(signalNumber);
}
@@ -116,6 +162,7 @@ static void consoleContinueSignal(int signalNumber);
// entitled to have its keys again when it is resumed.
static void consoleStopSignal(int signalNumber) {
consoleReleaseTerminal();
consolePlainAgainDying();
signal(SIGCONT, consoleContinueSignal);
signal(signalNumber, SIG_DFL);
raise(signalNumber);
@@ -302,6 +349,65 @@ static void consoleSayCursor(void) {
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) {
// ---- Nowhere to put a glyph ----
//
@@ -332,6 +438,31 @@ static void consoleDraw(uint8_t byte) {
consoleCursorMoved();
}
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:
break;
}
@@ -361,6 +492,12 @@ static int (*inputHook)(int mayWait) = NULL;
// cursor blinking at somebody who is thinking about what to type.
#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;
// 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,
// 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.
// Whether standard input is really a terminal, defined below and wanted here.
static int consoleTranslatingKeys(void);
static int consoleFromInput(int mayWait) {
if (!mayWait) {
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
@@ -471,6 +611,37 @@ static int consoleFromInput(int mayWait) {
}
unsigned char byte;
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);
if (got == 1) {
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
// 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) {
for (;;) {
int got;
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;
consoleHeldByte = -1;
} else {
got = consoleFromInput(mayWait);
got = consoleEraseNormalized(consoleFromInput(mayWait));
}
if (got < 0) {
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
// column costs one sequence rather than two.
consoleSayCursor();
consoleSayAttribute();
consoleDraw(DataByte);
putchar(DataByte);
break;
+6
View File
@@ -227,6 +227,12 @@
// 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
// 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_DOWN 0x81
#define CONSOLE_KEY_LEFT 0x82
+12 -4
View File
@@ -237,10 +237,18 @@ static void soundParameter(uint8_t value) {
v->gate = value ? VOICE_TRIGGER : VOICE_GATE;
}
break;
// The LFOs belong to the device rather than to a channel, so whichever channel is
// selected makes no difference to these.
case SP_LFO0: setLfo(&synth.lfos[0], which, value); break;
case SP_LFO1: setLfo(&synth.lfos[1], which, value); break;
// ---- The LFOs belong to the channel ----
//
// They used to belong to the device, so whichever channel was selected made no
// 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:
// A parameter number nothing answers to does nothing. A sound device is a poor
// place to stop the machine, the same as a screen.
+76 -38
View File
@@ -10,11 +10,11 @@
//
// 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
// 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 ----
//
// 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
// 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
// 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
// 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.
s->voices[i].levelSource = MOD_SOURCE_AMP_ENV;
s->voices[i].gate = VOICE_GATE;
for (int l = 0; l < LFO_COUNT; l++)
lfoStateInit(&s->voices[i].lfoRun[l], lfoTriggerSeed(l));
// Every voice is seeded from lfoTriggerSeed(l), which depends on the LFO and NOT on
// 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[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[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)
@@ -167,15 +174,23 @@ void synthResetPatch(Synth *s)
v->filter.resModDepth = 0.0f;
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;
v->lfos[l].rate = 1.0f;
v->lfos[l].waveform = WAVE_SINE;
v->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;
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->pitchBendRange = 2.0f;
}
@@ -377,8 +392,8 @@ const char *waveformName(Waveform w)
// patch that exists was made against the old behaviour.
//
// 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.
static void voiceArm(Synth *s, Voice *v)
// per note is wanted under held notes too, and the other voices' cycles must not move.
static void voiceArm(Voice *v)
{
int oneShot = (v->gate == VOICE_TRIGGER);
v->ampEnv.oneShot = oneShot;
@@ -394,8 +409,8 @@ static void voiceArm(Synth *s, Voice *v)
}
for (int l = 0; l < LFO_COUNT; l++)
if (s->lfos[l].mode == LFO_RETRIGGER)
lfoStateInit(&v->lfoRun[l], lfoTriggerSeed(l));
if (v->lfos[l].mode == LFO_RETRIGGER)
lfoStateInit(&v->lfos[l].run, lfoTriggerSeed(l));
envelopeNoteOn(&v->ampEnv);
envelopeNoteOn(&v->modEnv);
@@ -412,7 +427,7 @@ void synthNoteOn(Synth *s, int midiNote)
s->voices[i].active = 1;
s->voices[i].filter.low = 0.0f;
s->voices[i].filter.band = 0.0f;
voiceArm(s, &s->voices[i]);
voiceArm(&s->voices[i]);
return;
}
}
@@ -425,7 +440,7 @@ void synthNoteOn(Synth *s, int midiNote)
s->voices[i].active = 1;
s->voices[i].filter.low = 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.
v->filter.low = 0.0f;
v->filter.band = 0.0f;
voiceArm(s, v);
voiceArm(v);
}
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++) {
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++) {
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;
float amp = envelopeTick(&vv->ampEnv, sr);
@@ -504,13 +525,6 @@ void synthFillBuffer(Synth *s, int16_t *out, int frames) {
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;
int activeOscs = 0;
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].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
for (int o = 0; o < OSC_COUNT; o++) {
s->voices[v].oscillators[o].waveform = s->voices[0].oscillators[o].waveform;
+28 -8
View File
@@ -10,11 +10,11 @@
//
// 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
// 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 ----
//
// 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
// 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
// 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
// other one has to be told.
//
@@ -119,7 +126,7 @@ typedef enum {
} LfoMode;
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
Waveform waveform;
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
// finish because a key came up, which is no use to a drum.
VoiceGate gate;
// One cycle per LFO per voice, used only by the LFOs set to LFO_RETRIGGER. Runtime state
// 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.
LfoState lfoRun[LFO_COUNT];
// ---- Its own LFOs, like its own envelopes and its own filter ----
//
// They used to belong to the Synth, which is right for ONE INSTRUMENT played polyphonically
// 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;
typedef struct {
@@ -207,7 +228,6 @@ typedef struct {
int lastStolenVoice;
Voice voices[VOICE_COUNT];
float volume; // 0.0 to 1.0, master output level
LFO lfos[LFO_COUNT];
} Synth;
// Envelope functions:
+43 -2
View File
@@ -152,11 +152,30 @@ static int toByte(const Mapping *m, double value) {
}
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) {
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"
"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;
}
const char *path = argv[1];
@@ -222,6 +241,28 @@ int main(int argc, char **argv) {
int count = 0;
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;
if (argc > 3) {
out = fopen(argv[3], "w");
+535
View File
@@ -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, &note)) {
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;
}
+43
View File
@@ -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. |
| -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. |
| -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. |
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.
## Building With Make:
+49 -5
View File
@@ -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
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
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.
**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.
**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.
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 |
| --- | --- |
| 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. |
| 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:
@@ -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.
**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:
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. |
| 4 | LFO 1. |
**The two LFOs belong to the device and not to a channel**, so writing 0x60 to 0x7F ignores
whichever channel is selected. That is what makes them useful: a vibrato that every voice
shares is one wobble rather than four that drift apart.
**Each channel has its own pair of LFOs**, so writing 0x60 to 0x7F sets them on whichever
channel is selected, exactly like every parameter above. Four channels are four independent
instruments, and an LFO is part of how an instrument sounds.
### 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
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:
The status port's bit 0 is set while any channel is still sounding, so a routine can wait for
+6 -6
View File
@@ -37,7 +37,7 @@ Everything in between is somewhere on that line.
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
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
```
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).
Individual scripts can be run on their own, from anywhere:
@@ -125,7 +125,7 @@ from `make`, not from here.
### 1. Recorded output
`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
given at all.
@@ -447,7 +447,7 @@ the console's line editing was in when it broke twice in two days.
## Fixture Disks:
`Tests/makedisks.sh` builds 27 images with SplitDisk before anything runs, into
`Tests/makedisks.sh` builds 29 images with SplitDisk before anything runs, into
`Tests/build/disks`. **That is the point of them.** A SplitBit program reading one of these
is being checked against a filesystem written by different code from the same written
specification, rather than against itself.
@@ -540,7 +540,7 @@ which stops the markers outliving the code they were about.
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
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
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
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
+149
View File
@@ -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
View File
@@ -104,12 +104,24 @@ for suite in "$@"; do
die "no such suite: $suite"
fi
printf -- '---- %s ----\n' "$name"
if "$run" 2>&1 | tee "$KEEP.out" | grep -E '^\s*\[FAIL\]' | sed 's/^ *//'; then
:
fi
if grep -qE '^[0-9]+ passed, [0-9]+ failed' "$KEEP.out"; then
"$run" > "$KEEP.out" 2>&1
status=$?
grep -E '^\s*\[FAIL\]' "$KEEP.out" | sed 's/^ *//'
# 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
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
printf '%s%s did not notice%s\n' "$RED" "$name" "$RESET"
fi
+210
View File
@@ -172,6 +172,40 @@ else:
problems.append("%s (0x%02X) is a device class and has no row in the Devices"
" 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 ----
#
# 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"
% (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:
print("The manuals and the code disagree:")
for p in problems:
+1 -1
View File
@@ -9,12 +9,12 @@ a file kept by asking
renamed it
deleted it
and it is gone
finished
> mkdir Notes
made
> cd Notes
/Notes> dir
0 files
8 of 32 entries, 307 blocks free
/Notes> exit
halted
Execution halted.
+18
View File
@@ -0,0 +1,18 @@
scheme 0
scheme 1
scheme 2
scheme 3
scheme 4
scheme 5
scheme 6
scheme 7
scheme 8
scheme 9
scheme A
scheme B
scheme C
scheme D
scheme E
scheme F
Execution halted.
[exit 0]
+10 -10
View File
@@ -1,12 +1,12 @@
ordinary highlighted
ordinary highlighted
ordinary highlighted
ordinary highlighted
ordinary highlighted
ordinary highlighted
ordinary highlighted
ordinary highlighted
ordinary  highlighted 
 ordinary  highlighted 
 ordinary  highlighted 
 ordinary  highlighted 
 ordinary  highlighted 
 ordinary  highlighted 
 ordinary  highlighted 
 ordinary  highlighted 

bank 2's ink is orange now, because this program said so
Execution halted.
[exit 0]
[exit 0]
+2 -1
View File
@@ -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
mkdir <path> make a directory
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
clear empty the screen
delete <file> take it off the disk
@@ -29,6 +29,7 @@ across.txt 700
empty.txt 0
aName22CharactersLong! 22
12 files
12 of 16 entries, 48 blocks free
>
> frobnicate
I do not know: frobnicate
-1
View File
@@ -12,7 +12,6 @@ A 44 B 55 Q 00 status 00
DP0 3000 DP1 3037 DP2 3030 DP3 5000 SP FFF5
press a key
carried on to the end
finished
> exit
halted
Execution halted.
+5
View File
@@ -1,6 +1,7 @@
CosmOS
> dir
0 files
0 of 16 entries, 125 blocks free
> mkdir Apps
made
> mkdir Apps/Deep
@@ -11,10 +12,12 @@ made
Apps <dir>
Notes <dir>
0 files, 2 directories
3 of 16 entries, 125 blocks free
> cd Apps
/Apps> dir
Deep <dir>
0 files, 1 directory
3 of 16 entries, 125 blocks free
/Apps> mkdir Deep
cannot make that: check the path, the name, and whether it is taken
/Apps> mkdir /Notes/Deep
@@ -23,6 +26,7 @@ made
/Notes> dir
Deep <dir>
0 files, 1 directory
4 of 16 entries, 125 blocks free
/Notes> rmdir /Apps
cannot remove that: it must be a directory, and empty
/Notes> rmdir /Apps/Deep
@@ -44,6 +48,7 @@ removed
removed
> dir
0 files
0 of 16 entries, 125 blocks free
> exit
halted
Execution halted.
+1 -1
View File
@@ -2,11 +2,11 @@ CosmOS
> Claim
claiming more than was reserved was refused
and the size it really came to was taken
finished
> dir
Claim.sbx 580
claim.dat 266
2 files
2 of 16 entries, 56 blocks free
> exit
halted
Execution halted.
-13
View File
@@ -1,43 +1,30 @@
CosmOS
> Copy /Input/empty.dat /Output/empty.dat
copied
finished
> Compare /Input/empty.dat /Output/empty.dat
the same
finished
> Copy /Input/exact.dat /Output/exact.dat
copied
finished
> Compare /Input/exact.dat /Output/exact.dat
the same
finished
> Copy /Input/tail.dat /Output/tail.dat
copied
finished
> Compare /Input/tail.dat /Output/tail.dat
the same
finished
> Copy /Input/large.dat /Output/large.dat
copied
finished
> Compare /Input/large.dat /Output/large.dat
the same
finished
> Compare /Input/large.dat /Input/different.dat
different
finished
> Copy /Input/missing.dat /Output/missing.dat
copy: cannot find the source
finished
> Compare /Input/large.dat /Input/missing.dat
compare: cannot find the second file
finished
> Copy
copy: give me a source and destination
finished
> Compare /Input/large.dat
compare: give me two files
finished
> exit
halted
Execution halted.
+11 -5
View File
@@ -2,12 +2,10 @@ CosmOS
> drive 1
> Copy 1:/twoblocks.txt 0:/crossed.txt
copied
finished
> drive
1
> Say from drive one
it says: from drive one
finished
> drive
1
> cd 0:/
@@ -18,6 +16,7 @@ Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Where.sbx 827
Break.sbx 149
Grid.sbx 571
Press.sbx 872
@@ -25,13 +24,14 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 9220
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
tune.sbx 306
tune.sbx 318
Play.sbx 2526
notes.txt 21
Apps <dir>
hi.script 121
@@ -42,8 +42,14 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
hush.script 42
aloud.script 59
args.script 64
pass.script 20
holds.script 87
crossed.txt 560
30 files, 1 directory
36 files, 2 directories
48 of 64 entries, 1886 blocks free
> exit
halted
Execution halted.
+4 -7
View File
@@ -4,42 +4,38 @@ Apps <dir>
A <dir>
B <dir>
0 files, 3 directories
9 of 32 entries, 240 blocks free
> cd /A
/A> dir
Say.sbx 53
notes.txt 25
2 files
9 of 32 entries, 240 blocks free
/A> Type notes.txt
these are the notes in A
finished
/A> cd /B
/B> Type notes.txt
and these are the very different notes in B
finished
/B> Say reached the one in Apps
it says: reached the one in Apps
finished
/B> cd /A
/A> Say reached the one in A
Hello, World!
finished
/A> cd ..
> dir
Apps <dir>
A <dir>
B <dir>
0 files, 3 directories
9 of 32 entries, 240 blocks free
> cd /B
/B> Type ../A/notes.txt
these are the notes in A
finished
/B> Wander /A
moved, and reading a bare name from there:
these are the notes in A
finished
/B> Type notes.txt
and these are the very different notes in B
finished
/B> cd nosuchplace
no such file
/B> cd notes.txt
@@ -50,6 +46,7 @@ Apps <dir>
A <dir>
B <dir>
0 files, 3 directories
9 of 32 entries, 240 blocks free
> exit
halted
Execution halted.
+1 -1
View File
@@ -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
mkdir <path> make a directory
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
clear empty the screen
delete <file> take it off the disk
+12 -3
View File
@@ -8,6 +8,7 @@ Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Where.sbx 827
Break.sbx 149
Grid.sbx 571
Press.sbx 872
@@ -15,13 +16,14 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 9220
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
tune.sbx 306
tune.sbx 318
Play.sbx 2526
notes.txt 21
Apps <dir>
hi.script 121
@@ -32,7 +34,13 @@ nonl.script 38
outer.script 376
inner.script 44
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
> dir
other.txt 28
@@ -40,6 +48,7 @@ notes <dir>
2things <dir>
twoblocks.txt 560
2 files, 2 directories
4 of 32 entries, 503 blocks free
> cd /notes
/notes> drive 0
> drive 1
-2
View File
@@ -26,7 +26,6 @@ poem.txt, new file
> w
written, 22 bytes
> q
finished
> run poem.txt
poem.txt, 3 lines
> l
@@ -36,7 +35,6 @@ poem.txt, 3 lines
> d 99
there is no such line
> q
finished
> exit
halted
Execution halted.
-5
View File
@@ -2,20 +2,15 @@ CosmOS
> Edit hello.asm
hello.asm, 31 lines
> q
finished
> Edit hello.asm
hello.asm, 31 lines
> q
finished
> Status
the last program left 0, which is: it did what it was asked
finished
> Edit long.txt
a line in it is longer than this can edit, so it has not been opened
finished
> Status
the last program left 1, which is: it did not
finished
> exit
halted
Execution halted.
+1 -3
View File
@@ -15,11 +15,9 @@ typed.txt, new file
> w
written, 13 bytes
> q
finished
> run typed.txt
> run typed.txt
typed.txt, 2 lines
> q
finished
> exit
halted
Execution halted.
+19 -12
View File
@@ -1,23 +1,22 @@
CosmOS
> Crash opcode
that byte is not an instruction, at 5081
that byte is not an instruction, at 5081
A 00 B 0F Q 00
the program was stopped
the program was stopped
> Crash service
nothing is installed at service 28, at 5084
nothing is installed at service 28, at 5084
A 00 B 0F Q 00
the program was stopped
the program was stopped
> Crash bank
a bank that is not there, or an address past its end, at 509E
a bank that is not there, or an address past its end, at 509E
A 01 B 0F Q 00
the program was stopped
the program was stopped
> Crash device
nothing is installed for the device on port 00, at 50A5
nothing is installed for the device on port 00, at 50A5
A 02 B 0F Q 00
the program was stopped
the program was stopped
> Crash sideways
Crash opcode | service | bank | device | blind
finished
> dir
greet.sbx 211
hello.sbx 53
@@ -25,6 +24,7 @@ Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Where.sbx 827
Break.sbx 149
Grid.sbx 571
Press.sbx 872
@@ -32,13 +32,14 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 9220
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
tune.sbx 306
tune.sbx 318
Play.sbx 2526
notes.txt 21
Apps <dir>
hi.script 121
@@ -49,7 +50,13 @@ nonl.script 38
outer.script 376
inner.script 44
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
halted
Execution halted.
+2 -2
View File
@@ -5,8 +5,8 @@ x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
bank 00
* s 4F00 00
* g 4F00
that byte is not an instruction, at 4F00
that byte is not an instruction, at 4F00
A 01 B 7F Q 00
that was the system itself, so there is nowhere to carry on from. Start the machine again.
Execution halted.
[exit 0]
[exit 0]
+4
View File
@@ -3,17 +3,21 @@ CosmOS
one.txt 13
two.txt 14
2 files
2 of 8 entries, 28 blocks free
> rename one.txt first.txt
renamed
> dir
first.txt 13
two.txt 14
2 files
2 of 8 entries, 28 blocks free
> delete first.txt
gone
> dir
two.txt 14
1 file
1 of 8 entries, 29 blocks free
the longest run is 28
> delete first.txt
no such file
> rename two.txt
+11 -6
View File
@@ -1,13 +1,10 @@
CosmOS
> Say before Flip
it says: before Flip
finished
> Flip
A screen is drawn where you cannot see it. A key shows it, another comes back.
finished
> Say after Flip
it says: after Flip
finished
> dir
greet.sbx 211
hello.sbx 53
@@ -15,6 +12,7 @@ Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Where.sbx 827
Break.sbx 149
Grid.sbx 571
Press.sbx 872
@@ -22,13 +20,14 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 9220
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
tune.sbx 306
tune.sbx 318
Play.sbx 2526
notes.txt 21
Apps <dir>
hi.script 121
@@ -39,7 +38,13 @@ nonl.script 38
outer.script 376
inner.script 44
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
halted
Execution halted.
+12 -7
View File
@@ -1,13 +1,10 @@
CosmOS
> Say before Grid
it says: before Grid
finished
> Grid
finished
>
>
> Say after Grid
it says: after Grid
finished
> dir
greet.sbx 211
hello.sbx 53
@@ -15,6 +12,7 @@ Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Where.sbx 827
Break.sbx 149
Grid.sbx 571
Press.sbx 872
@@ -22,13 +20,14 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 9220
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
tune.sbx 306
tune.sbx 318
Play.sbx 2526
notes.txt 21
Apps <dir>
hi.script 121
@@ -39,7 +38,13 @@ nonl.script 38
outer.script 376
inner.script 44
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
halted
Execution halted.
-2
View File
@@ -3,10 +3,8 @@ CosmOS
loaded, starting at 5000
> run
Hello, World!
finished
> run
Hello, World!
finished
> exit
halted
Execution halted.
+17 -8
View File
@@ -1,25 +1,34 @@
CosmOS
> Say hello there
it says: hello there
finished
> Say.sbx spelled out in full
it says: spelled out in full
finished
> run once more
it says: once more
finished
> dir
Say.sbx 156
dir.sbx 156
Greet 156
notes.txt 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
not a program
that is not a program or a script: notes
> notes.sbx
not a program
that is not a program or a script: notes.sbx
> notes.txt
I do not know: notes.txt
that is not a program or a script: notes.txt
> nosuchprogram
I do not know: nosuchprogram
> 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
mkdir <path> make a directory
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
clear empty the screen
delete <file> take it off the disk
-2
View File
@@ -5,13 +5,11 @@ loaded, starting at 5000
keys, by interrupt. q stops.
ab
the console has been handed back
finished
>
> run
keys, by interrupt. q stops.
cd
the console has been handed back
finished
>
> monitor
x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
-1
View File
@@ -1,7 +1,6 @@
CosmOS
> Lander
Crashed.
finished
> exit
halted
Execution halted.
-1
View File
@@ -1,7 +1,6 @@
CosmOS
> Lander
Crashed.
finished
> exit
halted
Execution halted.
-1
View File
@@ -868,7 +868,6 @@ loaded, starting at 5000
##
the board has settled
finished
>
halted
Execution halted.
-1
View File
@@ -20,7 +20,6 @@ loaded, starting at 5000
stopped
finished
>
>
halted
+27
View File
@@ -0,0 +1,27 @@
CosmOS
> ls
greet.sbx hello.sbx Life.sbx Snake.sbx Keys.sbx
Say.sbx Where.sbx Break.sbx Grid.sbx Press.sbx
Mode.sbx Flip.sbx Sprite.sbx Depth.sbx Packages/
Pad.sbx Crash.sbx vars.script blocks.script loops.script
tune.sbx Play.sbx notes.txt Apps/ hi.script
bad.script plain.script cross.script nonl.script outer.script
inner.script loop.script hush.script aloud.script args.script
pass.script holds.script
> cd /Apps
/Apps> ls
Copy.sbx Say.sbx Where.sbx Walk.sbx ls.sbx Lander where.sh
/Apps> cd /
> ls /Packages/app.Lander
Lander.sbx 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]
+13 -6
View File
@@ -53,9 +53,9 @@ bank 00
800B 00 ?
* g 8000
H
that byte is not an instruction, at 800A
that byte is not an instruction, at 800A
A 85 B 3F Q 05
the program was stopped
the program was stopped
* d 8000
8000 26 48 INIA 48
8002 D1 00 OUTA 00
@@ -92,7 +92,6 @@ that one needs a value after it
820F 00 ?
* g 8200
hello, typed
finished
* exit
> dir
greet.sbx 211
@@ -101,6 +100,7 @@ Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Where.sbx 827
Break.sbx 149
Grid.sbx 571
Press.sbx 872
@@ -108,13 +108,14 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 9220
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
tune.sbx 306
tune.sbx 318
Play.sbx 2526
notes.txt 21
Apps <dir>
hi.script 121
@@ -125,7 +126,13 @@ nonl.script 38
outer.script 376
inner.script 44
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
halted
Execution halted.
+13 -6
View File
@@ -3,11 +3,10 @@ CosmOS
x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
* Say hello from the monitor
it says: hello from the monitor
finished
* Crash opcode
that byte is not an instruction, at 5081
that byte is not an instruction, at 5081
A 00 B 0F Q 00
the program was stopped
the program was stopped
* nonsense
I do not know: nonsense
* b program
@@ -20,6 +19,7 @@ Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Where.sbx 827
Break.sbx 149
Grid.sbx 571
Press.sbx 872
@@ -27,13 +27,14 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 9220
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
tune.sbx 306
tune.sbx 318
Play.sbx 2526
notes.txt 21
Apps <dir>
hi.script 121
@@ -44,7 +45,13 @@ nonl.script 38
outer.script 376
inner.script 44
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
halted
Execution halted.
-3
View File
@@ -50,7 +50,6 @@ line 42: ABCDEFGHIJKLMNOPQRSTUVWXYZ
line 43: ABCDEFGHIJKLMNOPQRSTUVWXYZ
line 44: ABCDEFGHIJKLMNOPQRSTUVWXYZ
-- more --
finished
> run readable.txt
first line
second line
@@ -115,7 +114,6 @@ line 56: ABCDEFGHIJKLMNOPQRSTUVWXYZ
line 57: ABCDEFGHIJKLMNOPQRSTUVWXYZ
line 58: ABCDEFGHIJKLMNOPQRSTUVWXYZ
line 59: ABCDEFGHIJKLMNOPQRSTUVWXYZ
finished
> run readable.txt
first line
second line
@@ -167,7 +165,6 @@ line 44: ABCDEFGHIJKLMNOPQRSTUVWXYZ
-- more --
line 45: ABCDEFGHIJKLMNOPQRSTUVWXYZ
-- more --
finished
> exit
halted
Execution halted.
-2
View File
@@ -3,7 +3,6 @@ CosmOS
loaded, starting at 5000
> run
40
finished
> load More.sbx
loaded, starting at 5000
> run readable.txt
@@ -30,7 +29,6 @@ line 17: ABCDEFGHIJKLMNOPQRSTUVWXYZ
line 18: ABCDEFGHIJKLMNOPQRSTUVWXYZ
line 19: ABCDEFGHIJKLMNOPQRSTUVWXYZ
-- more --
finished
> exit
halted
Execution halted.
-2
View File
@@ -1,12 +1,10 @@
CosmOS
> Mode
40
finished
> echo a narrow screen
a narrow screen
> Mode
80
finished
> echo and a wide one again
and a wide one again
> exit
-1
View File
@@ -8,7 +8,6 @@ Pads present, as a bit each: 1
0: 0
0: 48
0: 0
finished
> exit
halted
Execution halted.
-1
View File
@@ -8,7 +8,6 @@ keys:
86 delete
5A Z
done
finished
>
> exit
halted
-1
View File
@@ -8,7 +8,6 @@ keys:
86 delete
5A Z
done
finished
>
> exit
halted
+2 -1
View File
@@ -2,12 +2,13 @@ CosmOS
> drive 1
> dir
0 files
0 of 128 entries, 2031 blocks free
> Copy 0:/Say.sbx 1:/Say.sbx
copied
finished
> dir
Say.sbx 156
1 file
1 of 128 entries, 2030 blocks free
> drive
1
> exit
+11 -5
View File
@@ -8,6 +8,7 @@ Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Where.sbx 827
Break.sbx 149
Grid.sbx 571
Press.sbx 872
@@ -15,13 +16,14 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 9220
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
tune.sbx 306
tune.sbx 318
Play.sbx 2526
notes.txt 21
Apps <dir>
hi.script 121
@@ -32,7 +34,13 @@ nonl.script 38
outer.script 376
inner.script 44
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 what?
> load nosuch.sbx
@@ -45,12 +53,10 @@ loaded, starting at 5000
a program, loaded off a disk, running on the system that loaded it
what should I call you? Anachronaut
hello, Anachronaut. that is all I do.
finished
> run
a program, loaded off a disk, running on the system that loaded it
what should I call you? Claude
hello, Claude. that is all I do.
finished
> exit
halted
Execution halted.
-3
View File
@@ -3,13 +3,10 @@ CosmOS
loaded, starting at 5000
> run
nothing was said
finished
> run notes.txt
it says: notes.txt
finished
> run a longer thing with spaces
it says: a longer thing with spaces
finished
> exit
halted
Execution halted.
-2
View File
@@ -4,12 +4,10 @@ CosmOS
saying what it is doing
> Say from a script
it says: from a script
finished
> echo
> Say and again
it says: and again
finished
> exit
halted
Execution halted.
+42
View File
@@ -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]
-1
View File
@@ -2,7 +2,6 @@ CosmOS
> do bad.script
> Say before
it says: before
finished
> nosuchcommand
I do not know: nosuchcommand
stopped: that line did not work

Some files were not shown because too many files have changed in this diff Show More