Files
SplitBit-Emulator/Programs/Examples/tune.asm
T
AnachronautandClaude Opus 5 d388cd3122 Give the machine a sound device
Four channels on ports 0x40 to 0x4F, each one a whole soundThing voice:
two oscillators, two envelopes, a filter and the routing between them. A
channel keeps its patch between notes, so a program sets an instrument up
once and then plays it.

Six ports rather than forty, because a voice has around forty settings and
four of them would spend more than half the port space on one device.
There is a selector and a value instead: say which channel, say which
setting, write it. That is three writes to change a setting and two to
play a note, which is the right way round - patches are loaded, notes are
played in an inner loop.

Samples come from the machine's clock and not the host's: 48,000 a second
of emulated time, worked out in whole numbers so it never drifts. A
million cycles is exactly 48,000 samples on any host at any speed, which
is what makes a sound something a test can compare. --sound writes them
out, the way --screen writes a picture, for the same reason: the suite has
no speaker.

Tests/sound.sh is 22 checks and found three real defects the first time it
ran, all the same shape - a synthesizer written for a patch editor, wired
up as hardware and inheriting the editor's assumptions:

  - Only one voice had an oscillator switched on, so three of the four
    channels could not make a sound whatever was written to them.
  - That voice's oscillator arrived at full gain and every other one
    arrived at nothing, an asymmetry with no reason behind it.
  - A note with no sustain is silent but not over, so the obvious way to
    wait for a sound to finish waits for ever.

The first two are fixed by the device defining its own power-on state
rather than inheriting synthInit's: every channel arrives able to make a
sound, so writing a note number is the whole of playing a note. The third
was already written into the manual as advice, an hour before the check
existed. The check disagreed with the documentation and the check was
right; the manual now says the one rule, which is that a note sounds until
the gate is dropped.

Programs/Examples/tune.asm plays eight notes, taking its tempo from the
screen's frame interrupt because that is the only regular beat this
machine has. It spends 99.8% of its cycles asleep in WAIT.

Voyager has no speaker yet - this is the device and its tests. Playing the
samples out of the window is the next commit.

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

204 lines
6.7 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 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 Tune
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
#Vectors
Boot start
Device 0x30 frame