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.
317 lines
13 KiB
C
317 lines
13 KiB
C
// sound.c
|
|
// The Voyager's sound device.
|
|
// Written by Anachronaut
|
|
|
|
#include "sound.h"
|
|
#include "synth.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <math.h>
|
|
|
|
// A megahertz, matching the machine. Kept here rather than reaching for machine.h, which
|
|
// would drag the whole front end into a device.
|
|
#define SOUND_CYCLE_RATE 1000000
|
|
|
|
static Synth synth;
|
|
static uint8_t channel;
|
|
static uint8_t parameter;
|
|
|
|
// Where the machine's clock was when the device started, and how many samples have been made
|
|
// since. The next sample is due at start + count * rate / samples, worked out in whole
|
|
// numbers each time rather than by adding an approximation over and over - twenty and five
|
|
// sixths does not add up to anything exact, and a drift of one part in a thousand is four
|
|
// seconds an hour.
|
|
static unsigned long startedAt;
|
|
static unsigned long samplesMade;
|
|
|
|
// ---- What has been made and not yet played ----
|
|
//
|
|
// A ring, written by the machine and read by whatever is playing it. One writer and one
|
|
// reader, which is the only sharing that needs no lock at all.
|
|
//
|
|
// IT DROPS WHEN IT IS FULL, and full means nobody is listening: a headless run makes
|
|
// forty-eight thousand samples a second of emulated time and there is nothing to take them.
|
|
// Dropping is right there. What must not drop is the COUNT, because that is the clock.
|
|
#define SOUND_RING 16384
|
|
static int16_t ring[SOUND_RING];
|
|
static int ringHead, ringTail;
|
|
|
|
// And a copy of everything, for --sound. Only kept when a file was asked for, because a long
|
|
// run makes millions of samples and a machine that hoarded them by default would be a machine
|
|
// that ran out of memory for no reason anybody asked for.
|
|
static int16_t *keeping = NULL;
|
|
static size_t keptCount, keptRoom;
|
|
|
|
void soundReset(void) {
|
|
synthInit(&synth, (float)SOUND_SAMPLE_RATE);
|
|
|
|
// ---- The device's own power-on state ----
|
|
//
|
|
// synthInit leaves soundThing's defaults, which are a patch EDITOR's: one voice set up to
|
|
// be heard and seven silent behind it, waiting for the edited patch to be copied over
|
|
// them. That is right for a program with one instrument on screen and wrong for a device
|
|
// whose four channels are four independent things.
|
|
//
|
|
// Two consequences if it were left alone, both of which the tests caught. Channels 1 to 3
|
|
// would be silent whatever gain was written to them, because their oscillators are not
|
|
// switched on. And channel 0's first oscillator would arrive at full gain while every
|
|
// other one arrived at nothing - an asymmetry with no reason a programmer could work out.
|
|
//
|
|
// So: EVERY CHANNEL ARRIVES ABLE TO MAKE A SOUND. Oscillator 0 on, at full gain;
|
|
// oscillator 1 off, because two oscillators is a choice and one is the plain case. A
|
|
// program that writes a note number hears that note, which is the shortest useful thing
|
|
// this device can be asked to do.
|
|
for (int i = 0; i < SOUND_CHANNELS; i++) {
|
|
synth.voices[i].oscillators[0].active = 1;
|
|
synth.voices[i].oscillators[0].gain = OSC_MAX_GAIN;
|
|
// Off rather than on-and-silent, because the two oscillators are AVERAGED and not
|
|
// added: a second one that is switched on halves the first whatever its gain is.
|
|
// "Active" is structural, and there is no setting of it that costs nothing.
|
|
synth.voices[i].oscillators[1].active = 0;
|
|
synth.voices[i].oscillators[1].gain = 0.0f;
|
|
}
|
|
|
|
channel = 0;
|
|
parameter = 0;
|
|
startedAt = 0;
|
|
samplesMade = 0;
|
|
ringHead = 0;
|
|
ringTail = 0;
|
|
keptCount = 0;
|
|
}
|
|
|
|
void soundKeepSamples(void) {
|
|
keptRoom = 1 << 16;
|
|
keeping = malloc(keptRoom * sizeof(*keeping));
|
|
keptCount = 0;
|
|
}
|
|
|
|
static void pushSample(int16_t sample) {
|
|
const int next = (ringTail + 1) % SOUND_RING;
|
|
if (next != ringHead) {
|
|
ring[ringTail] = sample;
|
|
ringTail = next;
|
|
}
|
|
if (keeping != NULL) {
|
|
if (keptCount == keptRoom) {
|
|
size_t bigger = keptRoom * 2;
|
|
int16_t *grown = realloc(keeping, bigger * sizeof(*keeping));
|
|
if (grown == NULL) {
|
|
return;
|
|
}
|
|
keeping = grown;
|
|
keptRoom = bigger;
|
|
}
|
|
keeping[keptCount++] = sample;
|
|
}
|
|
}
|
|
|
|
void soundTick(unsigned long now) {
|
|
if (startedAt == 0 && samplesMade == 0) {
|
|
startedAt = now;
|
|
}
|
|
for (;;) {
|
|
// When the next one is due, in whole numbers: no accumulated fraction to drift.
|
|
// Sample n is due n periods after the device started, so sample nought is due the
|
|
// moment it starts. Making the first one a period late would put every sample after
|
|
// it a period late too, which is a whole sample of lag for nothing.
|
|
const unsigned long due = startedAt
|
|
+ (unsigned long)(samplesMade * (uint64_t)SOUND_CYCLE_RATE
|
|
/ SOUND_SAMPLE_RATE);
|
|
if (now < due) {
|
|
return;
|
|
}
|
|
int16_t sample;
|
|
synthFillBuffer(&synth, &sample, 1);
|
|
pushSample(sample);
|
|
samplesMade++;
|
|
}
|
|
}
|
|
|
|
// ---- A byte, and what it means ----
|
|
//
|
|
// Everything on this machine is a byte, and a synthesizer wants seconds, hertz and ratios. So
|
|
// each parameter says how its 0 to 255 becomes what the engine needs, and the shapes are
|
|
// chosen for where the USEFUL part of the range is rather than for arithmetic convenience.
|
|
//
|
|
// Times are squared, because the difference between five and fifty milliseconds is the whole
|
|
// character of a percussive sound and the difference between three and four seconds is
|
|
// nothing anybody can hear. Cutoff is exponential for the same reason: pitch is logarithmic
|
|
// and so is where a filter sounds like it is.
|
|
static float overRange(uint8_t value, float lowest, float highest) {
|
|
return lowest + (highest - lowest) * ((float)value / 255.0f);
|
|
}
|
|
|
|
static float squared(uint8_t value, float highest) {
|
|
const float part = (float)value / 255.0f;
|
|
return part * part * highest;
|
|
}
|
|
|
|
static float exponential(uint8_t value, float lowest, float highest) {
|
|
const float part = (float)value / 255.0f;
|
|
return lowest * powf(highest / lowest, part);
|
|
}
|
|
|
|
// Centred on 128, so that half of nothing is no change and either side of it is a direction.
|
|
static float signedRange(uint8_t value, float reach) {
|
|
return ((float)value - 128.0f) / 128.0f * reach;
|
|
}
|
|
|
|
static ModSource sourceFor(uint8_t value) {
|
|
return (value <= MOD_SOURCE_LFO2) ? (ModSource)value : MOD_SOURCE_NONE;
|
|
}
|
|
|
|
static void setOscillator(Oscillator *o, uint8_t which, uint8_t value) {
|
|
switch (which) {
|
|
case SP_OSC_WAVE: o->waveform = (Waveform)(value % WAVE_COUNT); break;
|
|
case SP_OSC_GAIN: o->gain = overRange(value, 0.0f, OSC_MAX_GAIN); break;
|
|
case SP_OSC_DUTY: o->dutyCycle = overRange(value, 0.05f, 0.95f); break;
|
|
// An octave either way, so a step of the byte is 1200/128, about nine cents. Fine
|
|
// enough for the shimmer of two oscillators just apart, which is what detune is
|
|
// mostly for, and wide enough to transpose one of them a whole octave.
|
|
case SP_OSC_DETUNE: o->detune = signedRange(value, 1200.0f); break;
|
|
case SP_OSC_OCTAVE: o->octave = (int)value - 128 < -2 ? -2
|
|
: ((int)value - 128 > 2 ? 2 : (int)value - 128); break;
|
|
case SP_OSC_ACTIVE: o->active = value != 0; break;
|
|
case SP_OSC_PWM_SRC: o->modRouting[0] = sourceFor(value); break;
|
|
case SP_OSC_PWM_DEPTH: o->modDepth[0] = signedRange(value, 0.5f); break;
|
|
case SP_OSC_DET_SRC: o->modRouting[1] = sourceFor(value); break;
|
|
case SP_OSC_DET_DEPTH: o->modDepth[1] = signedRange(value, 1200.0f); break;
|
|
case SP_OSC_GAIN_SRC: o->modRouting[2] = sourceFor(value); break;
|
|
case SP_OSC_GAIN_DEPTH: o->modDepth[2] = signedRange(value, OSC_MAX_GAIN); break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
static void setEnvelope(Envelope *e, uint8_t which, uint8_t value) {
|
|
switch (which) {
|
|
case SP_ENV_ATTACK: e->attackSec = squared(value, 4.0f); break;
|
|
case SP_ENV_DECAY: e->decaySec = squared(value, 4.0f); break;
|
|
case SP_ENV_SUSTAIN: e->sustainLevel = overRange(value, 0.0f, 1.0f); break;
|
|
case SP_ENV_RELEASE: e->releaseSec = squared(value, 4.0f); break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
static void setFilter(Filter *f, uint8_t which, uint8_t value) {
|
|
switch (which) {
|
|
case SP_FILTER_ACTIVE: f->active = value != 0; break;
|
|
case SP_FILTER_TYPE: f->type = (FilterType)(value % FILTER_COUNT); break;
|
|
case SP_FILTER_CUTOFF: f->cutoff = exponential(value, 20.0f, 20000.0f); break;
|
|
case SP_FILTER_RES: f->resonance = overRange(value, 0.0f, 0.99f); break;
|
|
case SP_FILTER_CUT_SRC: f->modRouting = sourceFor(value); break;
|
|
case SP_FILTER_CUT_DEP: f->modDepth = signedRange(value, 8000.0f); break;
|
|
case SP_FILTER_RES_SRC: f->resModRouting = sourceFor(value); break;
|
|
case SP_FILTER_RES_DEP: f->resModDepth = signedRange(value, 0.99f); break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
static void setLfo(LFO *l, uint8_t which, uint8_t value) {
|
|
switch (which) {
|
|
case SP_LFO_ACTIVE: l->active = value != 0; break;
|
|
case SP_LFO_WAVE: l->waveform = (Waveform)(value % WAVE_COUNT); break;
|
|
case SP_LFO_RATE: l->rate = exponential(value, 0.05f, 20.0f); break;
|
|
case SP_LFO_MODE: l->mode = value ? LFO_RETRIGGER : LFO_FREE; break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
static void soundParameter(uint8_t value) {
|
|
Voice *v = &synth.voices[channel];
|
|
const uint8_t group = parameter & 0xF0;
|
|
const uint8_t which = parameter & 0x0F;
|
|
switch (group) {
|
|
case SP_OSC0: setOscillator(&v->oscillators[0], which, value); break;
|
|
case SP_OSC1: setOscillator(&v->oscillators[1], which, value); break;
|
|
case SP_AMPENV: setEnvelope(&v->ampEnv, which, value); break;
|
|
case SP_MODENV: setEnvelope(&v->modEnv, which, value); break;
|
|
case SP_FILTER: setFilter(&v->filter, parameter, value); break;
|
|
case SP_LEVEL_SOURCE:
|
|
// Two settings share a group, because both are about the voice as a whole rather
|
|
// than about one of its parts.
|
|
if (parameter == SP_LEVEL_SOURCE) {
|
|
v->levelSource = sourceFor(value);
|
|
} else if (parameter == SP_VOICE_GATE) {
|
|
v->gate = value ? VOICE_TRIGGER : VOICE_GATE;
|
|
}
|
|
break;
|
|
// The LFOs belong to the device rather than to a channel, so whichever channel is
|
|
// selected makes no difference to these.
|
|
case SP_LFO0: setLfo(&synth.lfos[0], which, value); break;
|
|
case SP_LFO1: setLfo(&synth.lfos[1], which, value); break;
|
|
default:
|
|
// A parameter number nothing answers to does nothing. A sound device is a poor
|
|
// place to stop the machine, the same as a screen.
|
|
break;
|
|
}
|
|
}
|
|
|
|
uint8_t soundWrite(uint8_t value, uint8_t port) {
|
|
switch (port) {
|
|
case SOUND_CHANNEL: channel = value % SOUND_CHANNELS; break;
|
|
case SOUND_PARAMETER: parameter = value; break;
|
|
case SOUND_VALUE: soundParameter(value); break;
|
|
case SOUND_NOTE: synthChannelOn(&synth, channel, value); break;
|
|
case SOUND_GATE:
|
|
if (value) {
|
|
synthChannelOn(&synth, channel, synth.voices[channel].midiNote);
|
|
} else {
|
|
synthChannelOff(&synth, channel);
|
|
}
|
|
break;
|
|
case SOUND_VOLUME: synth.volume = overRange(value, 0.0f, 1.0f); break;
|
|
default: break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
uint8_t soundRead(uint8_t port) {
|
|
switch (port) {
|
|
case SOUND_STATUS: {
|
|
uint8_t status = 0;
|
|
for (int i = 0; i < SOUND_CHANNELS; i++) {
|
|
if (synth.voices[i].active) {
|
|
status |= SOUND_STATUS_SOUNDING;
|
|
}
|
|
}
|
|
return status;
|
|
}
|
|
case SOUND_CHANNEL: return channel;
|
|
case SOUND_PARAMETER: return parameter;
|
|
case SOUND_NOTE: return (uint8_t)synth.voices[channel].midiNote;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
int soundTake(int16_t *into, int wanted) {
|
|
int taken = 0;
|
|
while (taken < wanted && ringHead != ringTail) {
|
|
into[taken++] = ring[ringHead];
|
|
ringHead = (ringHead + 1) % SOUND_RING;
|
|
}
|
|
return taken;
|
|
}
|
|
|
|
int soundWriteSamples(const char *path) {
|
|
// Nothing was kept, which happens if the file was asked for after the machine ran. An
|
|
// empty file is the honest answer: the run made no sound anybody asked to hear.
|
|
if (keeping == NULL) {
|
|
keptCount = 0;
|
|
}
|
|
FILE *file = fopen(path, "wb");
|
|
if (file == NULL) {
|
|
fprintf(stderr, "Error: Couldn't write the sound to: %s\n", path);
|
|
return 1;
|
|
}
|
|
const size_t written = keptCount == 0
|
|
? 0 : fwrite(keeping, sizeof(*keeping), keptCount, file);
|
|
fclose(file);
|
|
if (written != keptCount) {
|
|
fprintf(stderr, "Error: The sound was not written whole to: %s\n", path);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|