videoReset zeroed video memory and then wrote the font and the sixteen colour schemes into it, and the comment above that said out loud what was wrong with it: "everything here is ordinary video memory". RAM does not wake up with anything in it. That was the last piece of magic in this device, and it looked harmless until something wanted the font BACK - a program that redefines a glyph had destroyed the only copy there was. So the device has a character generator, the way the machines this one is pretending to be really did, and the copy into RAM is a thing it DOES rather than a state it mysteriously starts in. Command port 0x39: bit 0 for the font, bit 1 for the schemes. THE RAM IS STILL RAM. A program may overwrite every glyph and every colour and should be able to, which is what makes this a tile engine rather than a text display. What changed is that it is no longer a one way door. NEITHER COMMAND CLEARS WHAT IT DOES NOT OWN. The font used to clear the whole of tile memory before writing itself, which was harmless while it happened only at reset and is wrong the moment a program can ask: a program that defined a tile of its own and then wanted its text back would have paid for it with the tile. The reason it is a chip rather than a file on the disk, which was the other candidate: the boot chain prints before CosmOS exists. Stage one prints "?" when there is nothing to boot, and if the font came off the disk then the message about the disk having failed would be the one thing that could not be drawn. A system that wants its own font still loads one over the top - the ROM is the floor, not the policy. Two things that had been worked around now simply work. The shell asks for both whenever a program exits, so a program that redefined a letter no longer leaves it unable to spell, and Grid no longer needs to have saved the screen to avoid handing back green text on blue. And the fault screen asks for the glyphs first, because a message spelled in somebody's tile graphics is no message at all. Five video checks. Two runs each for the font and the schemes, since the map holds a tile NUMBER and the glyph is looked up when the frame is drawn - so restoring changes every cell using it, including ones drawn before, and what the two runs differ by is the command. The third guards the decision not to clear: tile 200 has to survive the font coming back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
285 lines
8.3 KiB
NASM
285 lines
8.3 KiB
NASM
; tune.asm
|
|
; Playing a melody, which needs a sound device and a clock and has neither by halves.
|
|
; Written by Anachronaut
|
|
;
|
|
; ---- Two devices, because one is not enough ----
|
|
;
|
|
; The sound device knows how to make a note and knows nothing about when. It has no timer and
|
|
; 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.
|
|
;
|
|
; 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.
|
|
;
|
|
; ---- What borrowing it costs ----
|
|
;
|
|
; 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.
|
|
;
|
|
; 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.
|
|
;
|
|
; ---- What a patch costs and what a note costs ----
|
|
;
|
|
; Setting the sound up is twenty-odd writes, done once before a single note is played. After
|
|
; that the inner loop is two: the note, and letting go of it. That split is what the selector
|
|
; and value registers are for - see Making A Noise in the Programming Manual.
|
|
|
|
#Program
|
|
|
|
#Include services.asm
|
|
|
|
#Base 0x4000
|
|
|
|
start:
|
|
|
|
; ---- The instrument ----
|
|
;
|
|
; Channel 0, selected once. Every parameter write below lands on it.
|
|
RSTA
|
|
OUTA 0x41
|
|
|
|
; A saw wave, which has all the harmonics and so is the one to hear a filter on. Writing a
|
|
; port leaves A alone, so the nothing that selected the channel also selects parameter 0,
|
|
; which is oscillator 0's waveform. SplitLint will point out any attempt to put it there
|
|
; twice.
|
|
OUTA 0x42
|
|
INIA 0d2 ; Saw
|
|
OUTA 0x43
|
|
|
|
INIA 0x01 ; Oscillator 0, gain
|
|
OUTA 0x42
|
|
INIA 0xFF ; All of it. A channel arrives at full gain already, so this is
|
|
OUTA 0x43 ; saying so rather than changing it.
|
|
|
|
; A second oscillator a little out of tune with the first, which is the oldest trick there
|
|
; is for making one voice sound like more than one.
|
|
;
|
|
; SWITCHING IT ON IS A SEPARATE WRITE from setting its gain, and it is the one that matters:
|
|
; the two oscillators are averaged rather than added, so `active` is structural. Setting a
|
|
; gain on an oscillator that is off does nothing at all, silently, which is how the first
|
|
; draft of this program came to have a detune in it that could not be heard.
|
|
INIA 0x15 ; Oscillator 1, on
|
|
OUTA 0x42
|
|
INIA 0x01
|
|
OUTA 0x43
|
|
INIA 0x11 ; Oscillator 1, gain
|
|
OUTA 0x42
|
|
INIA 0xC0
|
|
OUTA 0x43
|
|
INIA 0x13 ; Oscillator 1, detune
|
|
OUTA 0x42
|
|
INIA 0d129 ; Centred on 128, and a step is about nine cents, so this is
|
|
; nine cents sharp - a shimmer rather than a wrong note.
|
|
OUTA 0x43
|
|
|
|
; Plucked: no attack to speak of, most of a second of decay, and nothing held.
|
|
;
|
|
; A sustain of nothing does NOT end the note. It goes quiet and keeps sounding, because a
|
|
; voice holding at nothing is what a held key is. Dropping the gate is the only thing that
|
|
; ends a note, which is why the loop below does it whether the sound has faded or not.
|
|
INIA 0x20 ; Amplitude envelope, attack
|
|
OUTA 0x42
|
|
INIA 0d10
|
|
OUTA 0x43
|
|
INIA 0x21 ; Decay
|
|
OUTA 0x42
|
|
INIA 0d120
|
|
OUTA 0x43
|
|
INIA 0x22 ; Sustain: nothing
|
|
OUTA 0x42
|
|
RSTA
|
|
OUTA 0x43
|
|
INIA 0x23 ; Release
|
|
OUTA 0x42
|
|
INIA 0d40
|
|
OUTA 0x43
|
|
|
|
; A low pass with the modulation envelope opening it, so each note starts bright and closes
|
|
; down. This is what the second envelope is for, and it can only be spent this way because
|
|
; the level is shaped by the first one and not by whichever happens to be wired to the
|
|
; output.
|
|
INIA 0x40 ; Filter, on
|
|
OUTA 0x42
|
|
INIA 0x01
|
|
OUTA 0x43
|
|
INIA 0x42 ; Cutoff, low to start with
|
|
OUTA 0x42
|
|
INIA 0d90
|
|
OUTA 0x43
|
|
INIA 0x43 ; A little resonance, to hear it move
|
|
OUTA 0x42
|
|
INIA 0d150
|
|
OUTA 0x43
|
|
INIA 0x44 ; What opens it: the modulation envelope
|
|
OUTA 0x42
|
|
INIA 0d2
|
|
OUTA 0x43
|
|
INIA 0x45 ; And how far, upwards from centre
|
|
OUTA 0x42
|
|
INIA 0d220
|
|
OUTA 0x43
|
|
INIA 0x31 ; That envelope's decay, which is the sweep's length
|
|
OUTA 0x42
|
|
INIA 0d70
|
|
OUTA 0x43
|
|
INIA 0x32 ; and it closes all the way
|
|
OUTA 0x42
|
|
RSTA
|
|
OUTA 0x43
|
|
|
|
INIA 0xC0 ; The device's volume, with room left over the top
|
|
OUTA 0x46
|
|
|
|
; ---- The beat ----
|
|
;
|
|
; Ask the screen to interrupt at each frame, and let interrupts in. The screen does not do
|
|
; this unless it is asked.
|
|
INIA 0x01
|
|
OUTA 0x35
|
|
SIF
|
|
|
|
; ---- The tune ----
|
|
;
|
|
; Data Pointer 0 walks the table, and nothing in this loop is a CALL, so it stays where it
|
|
; was left without being saved anywhere.
|
|
SETD.0 Theme
|
|
|
|
nextNote:
|
|
LDA.0 ; The note. Zero is the end of the tune.
|
|
BRA finished
|
|
OUTA 0x44 ; Writing the note is what starts it.
|
|
INCD.0
|
|
LDB.0 ; How many frames it lasts.
|
|
INCD.0
|
|
|
|
holdNote:
|
|
WAIT ; Nothing at all until the screen says a frame has gone by.
|
|
DECB
|
|
BNB holdNote
|
|
|
|
; Let go. The note is already fading on its own decay, but dropping the gate is what a
|
|
; keyboard does and what the release time is waiting for.
|
|
RSTA
|
|
OUTA 0x45
|
|
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
|
|
lastRing:
|
|
WAIT
|
|
DECB
|
|
BNB lastRing
|
|
|
|
CIF
|
|
RSTA
|
|
OUTA 0x35
|
|
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:
|
|
RETI
|
|
|
|
#Data
|
|
|
|
#Base 0x2000
|
|
|
|
; ---- 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
|
|
; 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
|
|
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+
|
|
; repeat four times.
|
|
0x2A 0d7 ; Gs
|
|
0x33 0d7 ; Ef
|
|
0x3C 0d7 ; C+
|
|
;
|
|
0x2A 0d7 ; Gs
|
|
0x33 0d7 ; Ef
|
|
0x3C 0d7 ; C+
|
|
;
|
|
0x2A 0d7 ; Gs
|
|
0x33 0d7 ; Ef
|
|
0x3C 0d7 ; C+
|
|
;
|
|
0x2A 0d7 ; Gs
|
|
0x33 0d7 ; Ef
|
|
0x3C 0d7 ; C+
|
|
; next chord
|
|
0x2E 0d7 ; Bf
|
|
0x35 0d7 ; F
|
|
0x3E 0d7 ; D+
|
|
; next chord
|
|
0x2E 0d7 ; Bf
|
|
0x35 0d7 ; F
|
|
0x3E 0d7 ; D+
|
|
;
|
|
; next chord
|
|
0x2E 0d7 ; Bf
|
|
0x35 0d7 ; F
|
|
0x3E 0d7 ; D+
|
|
;
|
|
; next chord
|
|
0x2E 0d7 ; Bf
|
|
0x35 0d7 ; F
|
|
0x3E 0d7 ; D+
|
|
; Finally on the C major
|
|
0x30 0d7 ; C
|
|
0x37 0d7 ; G
|
|
0x40 0d7 ; E+
|
|
;
|
|
0x30 0d7 ; C
|
|
0x37 0d7 ; G
|
|
0x40 0d7 ; E+
|
|
;
|
|
0x30 0d7 ; C
|
|
0x37 0d7 ; G
|
|
0x40 0d7 ; E+
|
|
;
|
|
0x30 0d7 ; C
|
|
0x37 0d7 ; G
|
|
0x40 0d7 ; E+
|
|
;
|
|
0x18 0d60 ; C bass
|
|
0x00
|
|
|
|
#Vectors
|
|
|
|
Boot start
|
|
Device 0x30 frame
|