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
204 lines
6.7 KiB
NASM
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
|