Files
Anachronaut b2ff8d64e5 Fold soundThing's changes back down, and expose the two new switches
The three changes that went up came back as part of soundThing, along
with two more that they made possible. The engine here is now b73e5c0
character for character, except that em-dashes and arrows in comments are
written as ASCII because this tree is ASCII only - a local rule, not an
improvement, and not sent up.

So synth.h's "what was changed" list is gone. There is nothing to list:
what has to be kept current is only that if either copy changes, the
other one has to be told.

---- What came back ----

A VOICE CAN END ITSELF. Naming the level's source said what shapes a
voice; nothing said what ends one, so the only thing that could ever
finish one was a key coming up. A game is nearly all one-shots and not
one of them wants its length decided by how long a note was held. Exposed
as parameter 0x51: 0 gated, 1 triggered.

AND A ONE-SHOT IS THE SAME ONE-SHOT TWICE. A triggered voice re-arms its
oscillators, and an LFO can be told to start over with each voice -
parameter 3 of either LFO. Both halves are needed and the check proves
it: with the LFO left free, two triggered hits still differ.

Their note warned that whatever applies a patch to a channel has to set
these or they hold synthInit's defaults. Checked: Voyager never calls
synthSyncVoices, so their 0001 is a no-op here as they predicted, and
nothing reaches into an LFO's phase, so the struct split is safe.

---- What it is for ----

Lander's crash is a triggered voice now, so boomOff is gone. Nothing has
to remember to end a bang.

SoundPatch learnt voice_levelSource, voice_gate and lfo<N>_mode, which
the new soundThing writes - without that it would have refused every
patch saved from it, since an unknown field stops the tool on purpose. A
patch from before those fields still converts, and says in its own
comments that it predates the level routing.

Three checks, each seen to fail on its own break: a gated voice still
sounding with nothing holding it, a triggered one down to nothing with no
gate ever dropped, and two hits identical sample for sample.

One test bug worth keeping: the first version of the repeatability check
struck the second note while the first was still ringing, so what it
found and compared as "the second hit" was a point in the middle of the
first one's tail. It now looks for sound after SILENCE rather than sound
after an offset.
2026-09-04 16:02:40 -04:00

143 lines
5.6 KiB
C

// 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
// ---- Whether a note waits to be let go of ----
//
// 0 is gated and 1 is triggered. Gated is what a keyboard wants: the sound lasts as long as
// the finger does, and dropping the gate starts the release. Triggered is what a GAME wants -
// a bang, a pickup, a door - where the note is struck and then plays its own length, and
// nothing has to remember to end it. Sustain and release have no meaning in a triggered
// voice, because both are answers to a question about a key that is not being asked.
//
// A program driving one-shots does not need port 0x45 at all once this is set.
#define SP_VOICE_GATE 0x51
// 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
// ---- Whether it starts over when a voice does ----
//
// 0 free and 1 retriggered. Free is one cycle running under everything, which is what vibrato
// across a held chord wants. Retriggered starts at the beginning of its shape every time a
// voice begins, which is the only way a one-shot sounds the SAME twice: a free LFO is wherever
// the wall clock left it, so the same drum caught at a different moment is a different drum.
//
// The mode belongs to the LFO and the cycle belongs to the voice, so retriggering costs
// nothing to a channel that is not using it.
#define SP_LFO_MODE 0x03
// ---- 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