Files
SplitBit-Emulator/Programs/Examples/tune.asm
T
AnachronautandClaude Opus 5 2f22807458 Give the demo a real tune: the Anachronaut Labs theme
The composition is the user's - a leitmotif they already have variants of,
written into the note table by hand. It replaces the eight note scale that
was there to prove a note could be played at all, and it is a better demo
for the obvious reason and one less obvious one: it is long enough to hear
whether the machine keeps time, which a run of eight notes is not.

Forty five notes, 567 frames, 9.45 seconds, peaking at 19,461 of 32,767.
The test budget goes to twelve million cycles, which is a duration rather
than a guess: at 16,667 cycles a frame the music is 9.45 million, so there
is room to add bars before anybody has to come back here.

Also records what borrowing the screen's frame actually costs, which came
out of writing music rather than out of theory. The frame is not too slow,
it is FIXED: every duration is a whole number of 16.67 ms, so a note worth
a third of a beat cannot be written, and the way round it is choosing a
tempo whose subdivisions land on whole frames - making the tune fit the
machine. That is the argument for the timer peripheral wanting an arbitrary
tick rather than a faster fixed one, and for the screen not being the clock:
a display refresh and a music routine have no reason to share a rate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-29 22:33:33 -04:00

279 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
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
HALT
; 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
; ---- 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