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
@@ -7,6 +7,7 @@
|
||||
#include "../Assembler/assembly.h" // For the fault vector numbers.
|
||||
#include "controller.h"
|
||||
#include "video.h"
|
||||
#include "sound.h"
|
||||
#include "font.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -898,6 +899,9 @@ void deviceTick(unsigned long now) {
|
||||
// The screen blinks its cursor on the machine's own clock rather than the host's, so the
|
||||
// picture is the same at the same cycle count however fast anything ran.
|
||||
videoTick(now);
|
||||
// And the sound, which makes whatever samples are due by now. On the machine's clock,
|
||||
// so the same program makes the same sound in the same cycles.
|
||||
soundTick(now);
|
||||
if (diskPending && now >= diskReadyAt) {
|
||||
uint8_t command = diskPending;
|
||||
diskPending = 0;
|
||||
@@ -962,6 +966,7 @@ static const DeviceRecord deviceTable[] = {
|
||||
{ PORT_MEMORY, DEVICE_MEMORY, DEVICE_FLAG_HAS_MEMORY },
|
||||
{ PORT_DISK, DEVICE_DISK, DEVICE_FLAG_HAS_MEMORY },
|
||||
{ PORT_VIDEO, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY },
|
||||
{ PORT_SOUND, DEVICE_SOUND, 0 },
|
||||
{ PORT_REGISTRY, DEVICE_REGISTRY, 0 },
|
||||
};
|
||||
static const int deviceCount = (int)(sizeof(deviceTable) / sizeof(deviceTable[0]));
|
||||
@@ -994,6 +999,9 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
|
||||
// Sixteen ports, one device, and the same rule again.
|
||||
return deviceOnPort(PORT_VIDEO);
|
||||
}
|
||||
if (port > PORT_SOUND && port <= PORT_SOUND_TOP) {
|
||||
return deviceOnPort(PORT_SOUND);
|
||||
}
|
||||
for (int i = 0; i < deviceCount; i++) {
|
||||
if (deviceTable[i].port == port) {
|
||||
return &deviceTable[i];
|
||||
@@ -1027,6 +1035,9 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
|
||||
if (Address >= PORT_VIDEO && Address <= PORT_VIDEO_TOP) {
|
||||
return videoWrite(DataByte, Address);
|
||||
}
|
||||
if (Address >= PORT_SOUND && Address <= PORT_SOUND_TOP) {
|
||||
return soundWrite(DataByte, Address);
|
||||
}
|
||||
// This function sends the DataByte to the appropriate place based on the Port Address.
|
||||
switch(Address) {
|
||||
case CONSOLE_DATA:
|
||||
@@ -1121,6 +1132,9 @@ uint8_t InputHandler(uint8_t Address) {
|
||||
if (Address >= PORT_VIDEO && Address <= PORT_VIDEO_TOP) {
|
||||
return videoRead(Address);
|
||||
}
|
||||
if (Address >= PORT_SOUND && Address <= PORT_SOUND_TOP) {
|
||||
return soundRead(Address);
|
||||
}
|
||||
switch(Address) {
|
||||
case CONSOLE_DATA:
|
||||
// If data is sent here, it should be read from STDIN.
|
||||
|
||||
@@ -202,6 +202,7 @@ void consoleSetInputHook(int (*hook)(int mayWait));
|
||||
#define DEVICE_MEMORY 0x12
|
||||
#define DEVICE_DISK 0x13
|
||||
#define DEVICE_VIDEO 0x14
|
||||
#define DEVICE_SOUND 0x15
|
||||
|
||||
// What a device brings besides itself. This means memory that somebody has to register
|
||||
// with the controller, so the controller's own bank 2 does not count: it is already there.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "controller.h"
|
||||
#include "io.h"
|
||||
#include "video.h"
|
||||
#include "sound.h"
|
||||
#include "utility.h"
|
||||
#include "../Assembler/assembly.h"
|
||||
#include <stdio.h>
|
||||
@@ -133,6 +134,7 @@ static int machineRestart(Machine *m) {
|
||||
return 0;
|
||||
}
|
||||
videoReset();
|
||||
soundReset();
|
||||
consoleHome();
|
||||
consoleResetInput();
|
||||
initializeCPU(&m->cpu, Program, Data);
|
||||
@@ -184,6 +186,10 @@ uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *pro
|
||||
// the device's, and a reset that left last program's screen up would be a reset that
|
||||
// did not happen.
|
||||
videoReset();
|
||||
soundReset();
|
||||
if (options->sound != NULL) {
|
||||
soundKeepSamples();
|
||||
}
|
||||
consoleHome();
|
||||
// The controller has to know where the memories are before anything can reach
|
||||
// them through it. Banks 0 and 1 are those two arrays.
|
||||
@@ -287,6 +293,12 @@ void machineStop(Machine *m) {
|
||||
if (m->options.screen != NULL) {
|
||||
videoWriteImage(m->options.screen);
|
||||
}
|
||||
// Every sample the machine made, for the same reason a picture is saved: there is no
|
||||
// speaker on a machine running tests, and a sound nothing can hear is a sound nothing
|
||||
// can check.
|
||||
if (m->options.sound != NULL) {
|
||||
soundWriteSamples(m->options.sound);
|
||||
}
|
||||
if (keyboardFile != NULL) {
|
||||
consoleSetInputHook(NULL);
|
||||
fclose(keyboardFile);
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
// 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;
|
||||
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:
|
||||
if (parameter == SP_LEVEL_SOURCE) {
|
||||
v->levelSource = sourceFor(value);
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -34,6 +34,9 @@ void printHelp(const char *programName) {
|
||||
printf(" keyboard rather than a terminal. Which means the console does\n");
|
||||
printf(" its own line editing, the way it must when a window is open\n");
|
||||
printf(" and there is no terminal behind it to do it.\n");
|
||||
printf(" -N, --sound FILE Save every sample the machine made, as raw signed 16 bit\n");
|
||||
printf(" at 48kHz. What --screen is for a picture: the only way to\n");
|
||||
printf(" check a sound on a machine with no speaker.\n");
|
||||
printf(" -h, --help Display this help message.\n");
|
||||
}
|
||||
|
||||
@@ -47,6 +50,7 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
||||
{"disk-cycles", required_argument, 0, 'L'},
|
||||
{"screen", required_argument, 0, 'S'},
|
||||
{"keyboard", required_argument, 0, 'K'},
|
||||
{"sound", required_argument, 0, 'N'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0 }
|
||||
};
|
||||
@@ -61,9 +65,10 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
||||
options->diskCycles = 0;
|
||||
options->screen = NULL;
|
||||
options->keyboard = NULL;
|
||||
options->sound = NULL;
|
||||
|
||||
// Parse options
|
||||
while ((opt = getopt_long(argc, argv, "dc:fhD:WL:S:K:", long_options, &option_index)) != -1) {
|
||||
while ((opt = getopt_long(argc, argv, "dc:fhD:WL:S:K:N:", long_options, &option_index)) != -1) {
|
||||
switch (opt) {
|
||||
case 'd':
|
||||
options->debug = 1;
|
||||
@@ -99,6 +104,9 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
||||
case 'K':
|
||||
options->keyboard = optarg;
|
||||
break;
|
||||
case 'N':
|
||||
options->sound = optarg;
|
||||
break;
|
||||
case 'h':
|
||||
printHelp(argv[0]);
|
||||
return OPTIONS_HELP;
|
||||
|
||||
@@ -24,6 +24,7 @@ typedef struct {
|
||||
uint8_t writeProtect; // Attach the disk read only, the way a tab on a floppy would.
|
||||
const char *screen; // Where to save a picture of the screen when the machine stops.
|
||||
const char *keyboard; // Feed the console from this file as a keyboard, not a terminal.
|
||||
const char *sound; // Where to save the samples the machine made, as raw 16 bit.
|
||||
} EmulatorOptions;
|
||||
|
||||
uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options);
|
||||
|
||||
Reference in New Issue
Block a user