diff --git a/Source/Emulator/voyager.c b/Source/Emulator/voyager.c index 5b6fd0a..60510c1 100644 --- a/Source/Emulator/voyager.c +++ b/Source/Emulator/voyager.c @@ -22,6 +22,7 @@ #include "machine.h" #include "video.h" +#include "sound.h" #include "io.h" #include "utility.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) { // 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 @@ -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 // to find out that a default was left as it was found. 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 // 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); @@ -347,13 +398,10 @@ int main(int argc, char *argv[]) { consoleSetInputHook(voyagerKey); // ---- A slice a frame ---- // - // The machine gets its turn, then the window gets its turn. Closing the window - // stops the machine, and 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. - // 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. + // The machine gets its turn, then the window gets its turn. Closing the window stops + // the machine, and 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()) { // 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. @@ -362,10 +410,19 @@ int main(int argc, char *argv[]) { machineRunSlice(&machine); } presentFrame(); + // After the slice, so what the machine just made is what gets played. + if (speakerOn) { + feedSpeaker(); + } } windowOpen = 0; // Taken back before the machine stops, so nothing can ask a window that has gone. consoleSetInputHook(NULL); + if (speakerOn) { + UnloadAudioStream(soundStream); + speakerOn = 0; + } + CloseAudioDevice(); UnloadTexture(screenTexture); CloseWindow(); }