Give Voyager a speaker

The device already made the samples; this takes them out of its ring and
hands them to Raylib, a sub-buffer at a time. Nothing here decides what a
sound is, the same as nothing in presentFrame decides what the screen
looks like - which is why the headless binary and this one make the same
sound, and why the suite can check a device with no speaker in it.

Asked for rather than assumed: a host with no audio device still gets a
window, because a machine worth looking at should not need one to start.

When the ring runs short the missing samples are filled by holding the
last one rather than by zeroes. It is still a glitch; a jump to silence
and back is a click, and a held level is not.

Also removes a comment that had been left in twice above the frame loop.

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 21:05:38 -04:00
co-authored by Claude Opus 5
parent 8366bf7721
commit 62a657f1a7
+64 -7
View File
@@ -22,6 +22,7 @@
#include "machine.h" #include "machine.h"
#include "video.h" #include "video.h"
#include "sound.h"
#include "io.h" #include "io.h"
#include "utility.h" #include "utility.h"
#include "raylib.h" #include "raylib.h"
@@ -181,6 +182,43 @@ static void checkResetButton(void) {
} }
} }
// ---- The speaker ----
//
// The device makes its samples on emulated cycles and puts them in a ring; this takes them
// out and hands them to Raylib. Nothing here decides what a sound is, the same as nothing in
// presentFrame decides what the screen looks like - which is why the headless binary and this
// one make the same sound, and why Tests/sound.sh can check a device with no speaker in it.
//
// A sub-buffer at a time, because that is the unit Raylib hands back when it has finished
// playing one. At 48,000 a second and sixty frames, a frame of machine time is 800 samples,
// so 1,024 is a little over one and there are two of them.
#define SOUND_BUFFER 1024
static AudioStream soundStream;
static int speakerOn;
// ---- When the machine cannot keep up, and when it runs away ----
//
// Both directions happen and neither should be a crash. The machine runs a slice per frame
// against the wall clock, so a host that stalls leaves the ring short and a host running
// --fast fills it faster than anything can play it. The device drops when full, which is the
// runaway case. This is the other one: what is missing is filled by HOLDING THE LAST SAMPLE
// rather than by zeroes, because a jump to silence and back is a click and a held level is
// not. It is still a glitch; it is the quieter kind.
static void feedSpeaker(void) {
static int16_t buffer[SOUND_BUFFER];
static int16_t lastSample;
while (IsAudioStreamProcessed(soundStream)) {
const int taken = soundTake(buffer, SOUND_BUFFER);
if (taken > 0) {
lastSample = buffer[taken - 1];
}
for (int i = taken; i < SOUND_BUFFER; i++) {
buffer[i] = lastSample;
}
UpdateAudioStream(soundStream, buffer, SOUND_BUFFER);
}
}
static void presentFrame(void) { static void presentFrame(void) {
// The device turns video memory into pixels; this puts them on the glass. Everything // The device turns video memory into pixels; this puts them on the glass. Everything
// that decides what the screen looks like is in the machine, where the suite can // that decides what the screen looks like is in the machine, where the suite can
@@ -335,6 +373,19 @@ int main(int argc, char *argv[]) {
// ended by one of them, taking whatever was in memory with it - which is a poor way // ended by one of them, taking whatever was in memory with it - which is a poor way
// to find out that a default was left as it was found. // to find out that a default was left as it was found.
SetExitKey(KEY_NULL); SetExitKey(KEY_NULL);
// ---- And a speaker, if the host has one ----
//
// Asked for rather than assumed: a machine with no audio device is a perfectly good
// machine to look at, and a front end that refused to start without one would make
// the window depend on something the picture does not need.
InitAudioDevice();
if (IsAudioDeviceReady()) {
SetAudioStreamBufferSizeDefault(SOUND_BUFFER);
soundStream = LoadAudioStream(SOUND_SAMPLE_RATE, 16, 1);
PlayAudioStream(soundStream);
speakerOn = 1;
}
// One texture, updated in place. Making a new one every frame would be a new // One texture, updated in place. Making a new one every frame would be a new
// allocation sixty times a second for a picture that is the same size every time. // allocation sixty times a second for a picture that is the same size every time.
Image blank = GenImageColor(VIDEO_MAX_WIDTH, VIDEO_MAX_HEIGHT, BLACK); Image blank = GenImageColor(VIDEO_MAX_WIDTH, VIDEO_MAX_HEIGHT, BLACK);
@@ -347,13 +398,10 @@ int main(int argc, char *argv[]) {
consoleSetInputHook(voyagerKey); consoleSetInputHook(voyagerKey);
// ---- A slice a frame ---- // ---- A slice a frame ----
// //
// The machine gets its turn, then the window gets its turn. Closing the window // The machine gets its turn, then the window gets its turn. Closing the window stops
// stops the machine, and the machine halting leaves the window up so that whatever // the machine, and the machine halting leaves the window up so that whatever it drew
// it drew is still there to look at - a program that ends should not take its // is still there to look at - a program that ends should not take its output off the
// output off the screen with it. // screen with it.
// A slice, then a frame. The machine halting leaves the window up so that whatever
// it drew is still there to look at - a program that ends should not take its output
// off the screen with it.
while (windowOpen && !WindowShouldClose()) { while (windowOpen && !WindowShouldClose()) {
// Before the running check, not after it: a machine that has stopped is the one // Before the running check, not after it: a machine that has stopped is the one
// worth restarting, and it is the one that cannot notice a reset by itself. // worth restarting, and it is the one that cannot notice a reset by itself.
@@ -362,10 +410,19 @@ int main(int argc, char *argv[]) {
machineRunSlice(&machine); machineRunSlice(&machine);
} }
presentFrame(); presentFrame();
// After the slice, so what the machine just made is what gets played.
if (speakerOn) {
feedSpeaker();
}
} }
windowOpen = 0; windowOpen = 0;
// Taken back before the machine stops, so nothing can ask a window that has gone. // Taken back before the machine stops, so nothing can ask a window that has gone.
consoleSetInputHook(NULL); consoleSetInputHook(NULL);
if (speakerOn) {
UnloadAudioStream(soundStream);
speakerOn = 0;
}
CloseAudioDevice();
UnloadTexture(screenTexture); UnloadTexture(screenTexture);
CloseWindow(); CloseWindow();
} }