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
149 lines
6.4 KiB
C
149 lines
6.4 KiB
C
// utility.c
|
|
// Utilities for the SplitBit CPU Emulator
|
|
// Written by Anachronaut
|
|
// 10/15/2024
|
|
|
|
#include "utility.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <getopt.h>
|
|
#include "../Assembler/assembly.h"
|
|
|
|
void printHelp(const char *programName) {
|
|
printf("Usage: %s [OPTIONS] [boot image]\n", programName);
|
|
printf("\n");
|
|
printf("Named an image, it is placed into memory and started, which is what a\n");
|
|
printf("debugger does and how the test suite runs. Given only a disk, the machine\n");
|
|
printf("starts the way hardware would: the built in ROM is shadowed into Program\n");
|
|
printf("Memory, and it reads the disk for everything else.\n");
|
|
printf("\n");
|
|
printf("Options:\n");
|
|
printf(" -d, --debug Enable debug mode.\n");
|
|
printf(" -c, --cycles N Stop after N cycles instead of running until the program halts.\n");
|
|
printf(" -f, --fast Run as fast as possible, ignoring the emulated cycle rate.\n");
|
|
printf(" -D, --disk FILE Attach a disk image, making one if it is not there.\n");
|
|
printf(" -L, --disk-cycles N How many cycles a block read or write takes. Zero, the\n");
|
|
printf(" default, finishes before the next instruction starts.\n");
|
|
printf(" -W, --write-protect Attach the disk read only. A disk the host will not let\n");
|
|
printf(" you write is read only whether you ask for this or not.\n");
|
|
printf(" -S, --screen FILE Save a picture of the screen, as a PPM, when the machine\n");
|
|
printf(" stops. Works with or without a window, which is how the\n");
|
|
printf(" tests look at a screen on a host that has no display.\n");
|
|
printf(" -K, --keyboard FILE Feed the console from this file as though it were a\n");
|
|
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");
|
|
}
|
|
|
|
uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
|
static struct option long_options[] = {
|
|
{"debug", no_argument, 0, 'd'},
|
|
{"cycles", required_argument, 0, 'c'},
|
|
{"fast", no_argument, 0, 'f'},
|
|
{"disk", required_argument, 0, 'D'},
|
|
{"write-protect", no_argument, 0, 'W'},
|
|
{"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 }
|
|
};
|
|
int opt;
|
|
int option_index = 0;
|
|
|
|
options->debug = 0;
|
|
options->fast = 0;
|
|
options->cycles = 0;
|
|
options->disk = NULL;
|
|
options->writeProtect = 0;
|
|
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:N:", long_options, &option_index)) != -1) {
|
|
switch (opt) {
|
|
case 'd':
|
|
options->debug = 1;
|
|
break;
|
|
case 'c': {
|
|
// The count has to be a plain positive number. Anything else is
|
|
// almost certainly a mistyped command line rather than a request
|
|
// to run zero cycles.
|
|
char *end;
|
|
long value = strtol(optarg, &end, 10);
|
|
if (*end != '\0' || value <= 0) {
|
|
fprintf(stderr, "Error: --cycles needs a positive number, not \"%s\".\n", optarg);
|
|
return OPTIONS_ERROR;
|
|
}
|
|
options->cycles = (unsigned long)value;
|
|
}
|
|
break;
|
|
case 'f':
|
|
options->fast = 1;
|
|
break;
|
|
case 'D':
|
|
options->disk = optarg;
|
|
break;
|
|
case 'W':
|
|
options->writeProtect = 1;
|
|
break;
|
|
case 'L':
|
|
options->diskCycles = strtoul(optarg, NULL, 0);
|
|
break;
|
|
case 'S':
|
|
options->screen = optarg;
|
|
break;
|
|
case 'K':
|
|
options->keyboard = optarg;
|
|
break;
|
|
case 'N':
|
|
options->sound = optarg;
|
|
break;
|
|
case 'h':
|
|
printHelp(argv[0]);
|
|
return OPTIONS_HELP;
|
|
default:
|
|
printHelp(argv[0]);
|
|
return OPTIONS_ERROR;
|
|
}
|
|
}
|
|
return OPTIONS_OK;
|
|
}
|
|
|
|
// Writes a byte out as eight binary digits, most significant first. printf's %b is
|
|
// a recent addition to C and not available everywhere, so this does it by hand.
|
|
// The buffer must have room for nine characters.
|
|
static void formatBinary(uint8_t value, char *out) {
|
|
for (int i = 0; i < 8; i++) {
|
|
out[i] = (value & (0x80 >> i)) ? '1' : '0';
|
|
}
|
|
out[8] = '\0';
|
|
}
|
|
|
|
void printRegisters(CPURegisters *cpu, uint8_t *Program, uint8_t *Data) {
|
|
char status[9];
|
|
formatBinary(cpu->Status, status);
|
|
printf("***** CPU Registers *****\n");
|
|
printf("A: 0x%02X\tB: 0x%02X\tQ: 0x%02X\tStatus: 0b%s\n", cpu->A, cpu->B, cpu->Q, status);
|
|
printf("Program Counter: 0x%04X Current Instruction: 0x%02X (%s)\n", cpu->ProgramCounter, Program[cpu->ProgramCounter],getMnemonic(Program[cpu->ProgramCounter]));
|
|
for (int i = 0; i < DATA_POINTERS; i++) {
|
|
printf(" Data Pointer %d: 0x%04X Current Data Value: 0x%02X%s\n",
|
|
i, cpu->DataPointer[i], Data[cpu->DataPointer[i]],
|
|
i >= PRESERVED_DATA_POINTERS ? " (volatile)" : "");
|
|
}
|
|
// The two casts keep these inside Data Memory. The Stack Pointer starts at the
|
|
// very top, so without them the display would read off the end of the array
|
|
// before a single byte has been pushed.
|
|
printf(" Stack Pointer: 0x%04X Current Value: (0x%02X) (0x%02X)\n", cpu->StackPointer,
|
|
Data[(uint16_t)(cpu->StackPointer + 1)], Data[(uint16_t)(cpu->StackPointer + 2)]);
|
|
}
|
|
|