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:
Anachronaut
2026-08-29 20:59:17 -04:00
co-authored by Claude Opus 5
parent b0d06aa6e5
commit d388cd3122
14 changed files with 1332 additions and 8 deletions
+14
View File
@@ -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.
+1
View File
@@ -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.
+12
View File
@@ -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);
+311
View File
@@ -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;
}
+121
View File
@@ -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
+9 -1
View File
@@ -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;
+1
View File
@@ -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);