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
This commit is contained in:
Anachronaut
2026-08-29 20:59:17 -04:00
co-authored by Claude Opus 5
parent b0d06aa6e5
commit d388cd3122
14 changed files with 1332 additions and 8 deletions
+198 -1
View File
@@ -513,6 +513,7 @@ If nothing is installed for the vector a device refused with, the machine stops
| 0x13 | The machine itself. Writing 1 asks it to start over: whatever put the first instruction in memory does it again, and the CPU begins where the boot vector points. A port rather than a service, because a reset has to work when the system does not - and a program that owns the whole machine has no system to ask. The disk is not unplugged and keeps what was written to it; the vector table is cleared, because a handler left behind would aim an interrupt into a program that is no longer running. | 0x04 |
| 0x12 | A device that owns 256 bytes of memory. Writing to its port fills that memory with the byte written, standing in for a disk controller reading a sector. Its memory is unreachable until it is registered as a bank. | 0x12 |
| 0x30 - 0x3F | The screen. See The Screen. It brings video memory, which is unreachable until it is registered as a bank. | 0x14 |
| 0x40 - 0x4F | The sound device. See Making A Noise. Four channels, played by writing to ports; it brings no memory. | 0x15 |
| 0xE0 - 0xEF | The memory controller. See The Memory Controller. | 0x03 |
| 0xFF | The bus registry. See Asking What Is There. | 0x01 |
@@ -677,6 +678,200 @@ What a program on the other end of an actual serial line sees is a different que
**It is one screen.** A program that writes its own tiles and its own map has taken the screen, and a console still writing characters into it will scribble on what that program drew. This is not an oversight to be worked around - it is what one screen means, and it is why a program that wants the screen takes it.
## Making A Noise:
Four channels on ports 0x40 to 0x4F. Each one is a whole voice - two oscillators, two
envelopes, a filter and the routing between them - and it keeps its settings between notes.
Channel two is channel two: a program sets up a sound once and then plays it, the same way it
sets up a tile once and then places it.
### Why It Is Six Ports And Not Forty:
A voice has around forty settings and there are four of them, so a port for each would spend
more than half of the machine's whole port space on one device. Instead there is a **selector
and a value**: say which channel, say which setting, write it. Three writes to change one
thing.
That is the right price because of *when* a program pays it. Patches are loaded; notes are
played. Changing a setting happens when a program starts or when an instrument changes, and
three writes there costs nothing anybody can hear. Playing a note happens in the inner loop of
a music routine, and that is two writes with no selector machinery at all.
### Registers:
| Port | Register |
| --- | --- |
| 0x40 | Status. Bit 0, some channel is still sounding. |
| 0x41 | Channel, 0 to 3. Anything larger wraps, so a program cannot select a channel that is not there. |
| 0x42 | Which setting the next write to 0x43 means. |
| 0x43 | The value of that setting, for the selected channel. |
| 0x44 | Note. Writing a MIDI note number **starts it**: 60 is middle C, and every 12 is an octave. |
| 0x45 | Gate. Writing zero releases the note and lets it fade; writing anything else starts the last note again. |
| 0x46 | Volume, for the whole device. |
Reading 0x41, 0x42 and 0x44 gives back what is in them, so a routine can save and restore the
selection around an interrupt.
### The Shortest Program That Makes A Sound:
```
RSTA
OUTA 0x41 ; Channel 0
INIA 0d60
OUTA 0x44 ; Middle C, which starts it
```
**Every channel arrives able to make a sound**: one oscillator switched on at full gain, a
plain triangle wave, an envelope that fades in and holds. Writing a note number is the whole
of playing a note, and a program only reaches for the settings when it wants a different
sound rather than a sound at all.
The second oscillator arrives switched off, and that is not the same as arriving silent. **The
two oscillators are averaged rather than added**, so switching the second one on halves the
first whatever gain it has - which is what keeps two of them from clipping, and which means
there is no setting of `active` that costs nothing. One oscillator is the plain case, and
asking for two is something a program says out loud:
```
INIA 0x15
OUTA 0x42 ; Oscillator 1, on
INIA 0x01
OUTA 0x43
INIA 0x11
OUTA 0x42 ; and how loud
INIA 0xC0
OUTA 0x43
```
### Settings:
The high nibble says which part of the voice, the low nibble which setting of it.
| Number | Part |
| --- | --- |
| 0x00 - 0x0F | Oscillator 0. |
| 0x10 - 0x1F | Oscillator 1. |
| 0x20 - 0x2F | The amplitude envelope. |
| 0x30 - 0x3F | The modulation envelope. |
| 0x40 - 0x4F | The filter. |
| 0x50 | What shapes the channel's level. |
| 0x60 - 0x6F | LFO 0. |
| 0x70 - 0x7F | LFO 1. |
| Oscillator | Setting |
| --- | --- |
| 0 | Waveform: 0 sine, 1 triangle, 2 saw, 3 ramp, 4 pulse, 5 noise. Anything larger wraps. |
| 1 | Gain. Silent at zero, which is where it starts. |
| 2 | Pulse width, for the pulse wave. |
| 3 | Detune, centred on 128, an octave either way. A step is about nine cents. |
| 4 | Octave, centred on 128, two either way. |
| 5 | On, or off at zero. |
| 6, 7 | What modulates the pulse width, and how much. |
| 8, 9 | What modulates the detune, and how much. |
| 10, 11 | What modulates the gain, and how much. |
| Envelope | Setting |
| --- | --- |
| 0 | Attack. |
| 1 | Decay. |
| 2 | Sustain, the level it holds at while the note is held. |
| 3 | Release. |
| Filter | Setting |
| --- | --- |
| 0x40 | On, or off at zero. |
| 0x41 | Type: 0 low pass, 1 high pass, 2 band pass. Anything larger wraps. |
| 0x42 | Cutoff. |
| 0x43 | Resonance. |
| 0x44, 0x45 | What modulates the cutoff, and how much. |
| 0x46, 0x47 | What modulates the resonance, and how much. |
| LFO | Setting |
| --- | --- |
| 0 | On, or off at zero. |
| 1 | Waveform, from the same six. |
| 2 | Rate. |
Anywhere a setting asks *what modulates* something, the answer is one of these:
| Value | Source |
| --- | --- |
| 0 | Nothing. |
| 1 | The amplitude envelope. |
| 2 | The modulation envelope. |
| 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.
### What A Byte Means:
Everything here is a byte, and a synthesizer wants seconds and hertz. How the one becomes the
other is chosen for **where the useful part of the range is**, not for whatever arithmetic is
tidiest.
| Kind of setting | 0 to 255 becomes |
| --- | --- |
| Times: attack, decay, release | Nought to four seconds, squared. |
| Levels: gain, sustain, resonance, volume | Nought to the most there is, evenly. |
| Cutoff, LFO rate | 20 Hz to 20 kHz, and 0.05 Hz to 20 Hz: exponential. |
| Detune, octave, and every modulation depth | Centred on 128, so half is no change and either side is a direction. |
Times are squared because the difference between five and fifty milliseconds is the whole
character of a percussive sound, and the difference between three seconds and four is nothing
anybody can hear. A byte spread evenly over four seconds would spend nine tenths of itself on
the part that does not matter. Cutoff and rate are exponential for the same reason, since
pitch is logarithmic and so is where a filter sounds like it is.
### Level:
Setting 0x50 says what shapes the channel's level, out of the same list of sources. It is
normally the amplitude envelope, which is what an amplitude envelope is for, and it can be set
to **nothing** - a channel whose level nothing shapes plays flat out until it is gated off.
That sounds like a small thing and is not. Without it the amplitude envelope is welded to the
output, so an envelope routed somewhere useful - opening the filter, bending a pitch - still
has to be shaped like something you would want to hear, and a snare that wants a click of
filter sweep and a flat body cannot have both.
### 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
a sound to end rather than counting cycles.
There is one rule about when a note ends, and it is worth stating on its own because the
obvious guess is wrong. **A note sounds until the gate is dropped.** What the envelope is
doing does not come into it.
In particular, a note whose sustain is nothing goes quiet and *keeps sounding*. Silence and
being finished look identical from outside and are not the same thing: the voice is holding at
nothing, which is exactly what a held key does on any instrument. A program that plays such a
note and then waits for the status bit waits for ever.
So a routine that means to wait for a sound does this, in this order: play the note, wait
however long the note is meant to last, write nothing to the gate at 0x45, and *then* wait for
the bit to come down - which it does when the release has finished. `Programs/Examples/tune.asm`
is that loop with the waiting done on the screen's frame.
### The Sound Comes From The Machine's Clock:
Samples are made against cycles, not against however fast the host really ran: forty-eight
thousand a second of *emulated* time, worked out in whole numbers so it never drifts. Three
million cycles make exactly one hundred and forty-four thousand samples.
This is the same decision as the screen writing a picture out, and it buys the same thing. A
sound is **something a test can compare**: the same program makes the same samples every time,
on any host, at any speed, and `Tests/sound.sh` reads them back and measures the pitch. It
also means a machine that is paused makes no sound rather than a held note, which is right - a
stopped machine's oscillators are stopped too.
**The device does not interrupt.** Nothing about a note finishing needs the CPU's attention
urgently enough to be worth a line, and a program that wants to play in time has the screen's
frame interrupt, which is 60 a second and already there. A programmable timer is the proper
answer and is a device that does not exist yet.
## Asking What Is There:
A program that only ever runs on one machine can be told where everything is. A program meant to run on more than one has to ask, and the bus registry on port 0xFF is what it asks.
@@ -718,7 +913,9 @@ One thing to be careful of: the registry remembers which port it was asked about
| 0x11 | Test device, which refuses everything. |
| 0x12 | Test device, which owns memory. |
| 0x13 | Disk. |
| 0x14 - 0xFF | Peripherals. |
| 0x14 | Screen. |
| 0x15 | Sound. |
| 0x16 - 0xFF | Peripherals. |
## The Memory Controller: