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:
co-authored by
Claude Opus 5
parent
b0d06aa6e5
commit
d388cd3122
@@ -0,0 +1,121 @@
|
||||
// sound.h
|
||||
// The Voyager's sound device.
|
||||
// Written by Anachronaut
|
||||
|
||||
#ifndef SOUND_H
|
||||
#define SOUND_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// ---- What this is ----
|
||||
//
|
||||
// Four channels, each one a full soundThing voice: two oscillators, two envelopes and a
|
||||
// filter. A channel is asked for by number and keeps its patch between notes, which is what
|
||||
// makes it hardware rather than a keyboard - channel two is channel two.
|
||||
//
|
||||
// ---- Why it is not thirty ports ----
|
||||
//
|
||||
// A voice has some forty parameters and the machine has 256 ports, so giving each one a port
|
||||
// of its own would spend a sixth of the whole address space on one device. Instead there is a
|
||||
// SELECTOR AND A VALUE: say which channel, say which parameter, write it. Three writes to
|
||||
// change one thing, which is the right price for something a program does when it loads a
|
||||
// patch and not when it plays a note.
|
||||
//
|
||||
// What a program does per NOTE is cheap on purpose, because that happens in a music routine's
|
||||
// inner loop: select the channel, write the note, write the gate. Three writes and no
|
||||
// parameter machinery at all.
|
||||
#define PORT_SOUND 0x40
|
||||
#define PORT_SOUND_TOP 0x4F
|
||||
|
||||
#define SOUND_STATUS 0x40
|
||||
#define SOUND_CHANNEL 0x41
|
||||
#define SOUND_PARAMETER 0x42
|
||||
#define SOUND_VALUE 0x43
|
||||
#define SOUND_NOTE 0x44
|
||||
#define SOUND_GATE 0x45
|
||||
#define SOUND_VOLUME 0x46
|
||||
|
||||
// Set while any channel is still sounding, so a routine can wait for a note to finish
|
||||
// rather than counting.
|
||||
#define SOUND_STATUS_SOUNDING 0x01
|
||||
|
||||
#define SOUND_CHANNELS 4
|
||||
|
||||
// ---- The parameters ----
|
||||
//
|
||||
// Grouped so that the number says which part of a voice it belongs to: the high nibble picks
|
||||
// the part and the low one picks the setting. Everything is a byte, because everything on
|
||||
// this machine is - what each byte means is in the manual and in soundParameter below.
|
||||
#define SP_OSC0 0x00 // 0x00-0x0F, and 0x10-0x1F for the second oscillator
|
||||
#define SP_OSC1 0x10
|
||||
#define SP_OSC_WAVE 0x00
|
||||
#define SP_OSC_GAIN 0x01
|
||||
#define SP_OSC_DUTY 0x02
|
||||
#define SP_OSC_DETUNE 0x03
|
||||
#define SP_OSC_OCTAVE 0x04
|
||||
#define SP_OSC_ACTIVE 0x05
|
||||
#define SP_OSC_PWM_SRC 0x06
|
||||
#define SP_OSC_PWM_DEPTH 0x07
|
||||
#define SP_OSC_DET_SRC 0x08
|
||||
#define SP_OSC_DET_DEPTH 0x09
|
||||
#define SP_OSC_GAIN_SRC 0x0A
|
||||
#define SP_OSC_GAIN_DEPTH 0x0B
|
||||
|
||||
#define SP_AMPENV 0x20 // 0x20-0x2F amp, 0x30-0x3F mod
|
||||
#define SP_MODENV 0x30
|
||||
#define SP_ENV_ATTACK 0x00
|
||||
#define SP_ENV_DECAY 0x01
|
||||
#define SP_ENV_SUSTAIN 0x02
|
||||
#define SP_ENV_RELEASE 0x03
|
||||
|
||||
#define SP_FILTER 0x40
|
||||
#define SP_FILTER_ACTIVE 0x40
|
||||
#define SP_FILTER_TYPE 0x41
|
||||
#define SP_FILTER_CUTOFF 0x42
|
||||
#define SP_FILTER_RES 0x43
|
||||
#define SP_FILTER_CUT_SRC 0x44
|
||||
#define SP_FILTER_CUT_DEP 0x45
|
||||
#define SP_FILTER_RES_SRC 0x46
|
||||
#define SP_FILTER_RES_DEP 0x47
|
||||
|
||||
// Which source shapes the channel's level: 0 none, 1 envelope 0, 2 envelope 1, 3 and 4 the
|
||||
// LFOs. Nought is the one that could not be said before - see synth.h.
|
||||
#define SP_LEVEL_SOURCE 0x50
|
||||
|
||||
// The LFOs belong to the whole device rather than to a channel, so these ignore whichever
|
||||
// channel is selected.
|
||||
#define SP_LFO0 0x60 // 0x60-0x6F and 0x70-0x7F
|
||||
#define SP_LFO1 0x70
|
||||
#define SP_LFO_ACTIVE 0x00
|
||||
#define SP_LFO_WAVE 0x01
|
||||
#define SP_LFO_RATE 0x02
|
||||
|
||||
// ---- Samples come from the machine's clock ----
|
||||
//
|
||||
// Forty-eight thousand a second against a million cycles: one sample every twenty and five
|
||||
// sixths, worked out in whole numbers so it never drifts. THE HOST'S CLOCK IS NOT INVOLVED,
|
||||
// which is what makes a recorded sound something a test can compare - the same program makes
|
||||
// the same samples in the same cycles however fast anything really ran.
|
||||
#define SOUND_SAMPLE_RATE 48000
|
||||
|
||||
void soundReset(void);
|
||||
|
||||
// Asks the device to keep every sample it makes, for soundWriteSamples. Off unless something
|
||||
// wants a file, because a long run makes millions of them.
|
||||
void soundKeepSamples(void);
|
||||
|
||||
// Called with the machine's clock, and generates whatever samples are due by now.
|
||||
void soundTick(unsigned long now);
|
||||
|
||||
uint8_t soundWrite(uint8_t value, uint8_t port);
|
||||
uint8_t soundRead(uint8_t port);
|
||||
|
||||
// Takes up to `wanted` samples for something that is going to play them, and says how many
|
||||
// there were. A front end with a speaker calls this; nothing else has to.
|
||||
int soundTake(int16_t *into, int wanted);
|
||||
|
||||
// Writes every sample generated so far to a file, as raw signed 16 bit. What --screen is for
|
||||
// a picture: the only way to check a sound on a machine with no speaker.
|
||||
int soundWriteSamples(const char *path);
|
||||
|
||||
#endif // SOUND_H
|
||||
Reference in New Issue
Block a user