// sound.c // The Voyager's sound device. // Written by Anachronaut #include "sound.h" #include "synth.h" #include #include #include #include // 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; }