Open the recording device only when the game starts microphone capture.

The game opens two input streams while it starts and leaves both stopped through ordinary play, and each open took the system's default recording device and held it, paused, until the game ended: Windows shows the microphone as in use for the whole session, and a Bluetooth headset can drop to its call profile. An open now checks that a default input exists and prepares the stream and its bounded buffer; the device is opened at StartInputStream and released at stop or close, after which the game can still read or purge what was buffered. A start that is refused returns an input error and can be tried again.

tests/audio_input_check.py did not compile on this line (it had no stand-in for the microphone setting); with the change it builds and passes: idle streams hold no device, a refused start can be retried, a stop releases the device and keeps the unread frames, two streams are independent. Not checked with a real microphone in the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-10 22:59:02 -05:00
parent 6750e0bee1
commit 093cf949df
4 changed files with 112 additions and 22 deletions

View File

@@ -4,24 +4,27 @@ The runtime connects Ocarina's polling `nn::audio` input calls to SDL3's default
microphone or audio interface as the operating system's default input and enable the game's microphone control.
Both a hummed note and an instrument received by that input go through the game's own note recognition.
Audio capture opens when the game calls `OpenInputStream` and starts only on `StartInputStream`. The runtime does
not write recordings to disk. SDL converts the input device's format to the requested PCM format and sample rate.
At `OpenInputStream` the runtime checks that a default input exists and prepares a buffer. It opens the recording
device at `StartInputStream` and releases it at stop or close. After a stop, the game can still read or purge the
buffered frames and start capture again. Recordings stay in memory. SDL converts the input device's format to the
requested PCM format and sample rate.
The capture callback uses a bounded ring: unread overflow drops the oldest complete frames instead of retaining
an unlimited recording while the game is paused. Stop, purge and close are distinct operations. Purging an unopened
or failed stream is safe, including the null-stream path that previously crashed in `InputStreamImpl::Purge`.
This implements the non-event polling overload used by Ocarina 1.1.1. Event-based streams, per-device selection,
hot-unplug recovery and packed 24-bit PCM are not implemented. Statistics return a neutral 24-byte structure.
Unsupported formats and a denied or missing recording device return an input error rather than reporting a
successfully opened stream. Input currently follows the operating system's default recording device; there is no
new runtime menu setting.
An unsupported format or missing default device returns an error at open. If the device disappears or access is
denied when capture starts, start returns an input error and the game can retry. Input follows the operating system's
default recording device; there is no new runtime menu setting.
## Validation
`python tests/audio_input_check.py --toolchain C:/msys64/ucrt64` builds and runs headless checks using SDL's dummy
audio driver. They require no game files and do not open a physical microphone. They cover sample ordering across
ring wrap and overflow, frame counts, start/stop, repeated open/close, purge, unsupported formats and purge/read
after a failed open. On Linux, omit `--toolchain` and install SDL3's development package and pkg-config.
audio driver. They require no game files or physical microphone. They check that idle streams leave the device
closed, that a denied start can be retried, and that stopping one stream releases its device and preserves unread
frames. They also cover ring wrap and overflow, frame counts, repeated open/close, unsupported formats and
purge/read after a failed open. On Linux, omit `--toolchain` and install SDL3's development package and pkg-config.
A separate local real-device check received mono signed PCM16 at 48 kHz, verified the 4800-frame ring remained
bounded, and opened and closed the device three times. In an Ocarina 1.1.1 Windows session built from the 0.5.0

View File

@@ -6,6 +6,7 @@
struct MicCapture {
SDL_AudioStream *stream;
SDL_AudioDeviceID device;
unsigned char *ring;
int cap, head, used, bpf;
};
@@ -95,24 +96,45 @@ MicCapture *mic_open(int format, int channels, int rate, int frames) {
return NULL;
}
SDL_AudioSpec spec = {.format = sf, .channels = channels, .freq = rate};
m->stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, received, m);
if (!m->stream) {
free(m->ring);
free(m);
SDL_QuitSubSystem(SDL_INIT_AUDIO);
SDL_AudioSpec source;
if (SDL_GetAudioDeviceFormat(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &source, NULL))
m->stream = SDL_CreateAudioStream(&source, &spec);
if (!m->stream || !SDL_SetAudioStreamPutCallback(m->stream, received, m)) {
mic_close(m);
return NULL;
}
return m;
}
int mic_start(MicCapture *m) { return m && SDL_ResumeAudioStreamDevice(m->stream); }
int mic_start(MicCapture *m) {
if (!m)
return 0;
if (m->device)
return 1;
SDL_AudioSpec spec;
if (!SDL_GetAudioStreamFormat(m->stream, NULL, &spec))
return 0;
SDL_AudioDeviceID device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec);
if (!device)
return 0;
if (!SDL_BindAudioStream(device, m->stream)) {
SDL_CloseAudioDevice(device);
return 0;
}
m->device = device;
return 1;
}
void mic_stop(MicCapture *m) {
if (m)
SDL_PauseAudioStreamDevice(m->stream);
if (m && m->device) {
SDL_CloseAudioDevice(m->device); /* unbinds the stream; unread frames remain in the ring */
m->device = 0;
}
}
void mic_close(MicCapture *m) {
if (!m)
return;
SDL_DestroyAudioStream(m->stream); /* joins capture before freeing userdata */
mic_stop(m);
if (m->stream)
SDL_DestroyAudioStream(m->stream);
free(m->ring);
free(m);
SDL_QuitSubSystem(SDL_INIT_AUDIO);

View File

@@ -86,7 +86,7 @@ static void open_input(Cpu *c) {
}
MicCapture *m = mic_open(f[0], f[1], f[2], frames);
if (!m) {
rt_log("mic: cannot open the default recording device: %s", mic_error());
rt_log("mic: cannot prepare the input stream: %s", mic_error());
ret_x(c, BAD_INPUT);
return;
}
@@ -97,7 +97,7 @@ static void open_input(Cpu *c) {
s->next_log = (uint64_t)f[2] * 5;
uint64_t impl = key + 8;
memcpy(ipc_guest(key, 8), &impl, 8);
rt_log("mic: default recording device opened; bounded buffer %d frames", mic_capacity(m));
rt_log("mic: input stream ready; bounded buffer %d frames; device opens when capture starts", mic_capacity(m));
ret_x(c, 0);
}
static void start_input(Cpu *c) {
@@ -115,6 +115,8 @@ static void stop_input(Cpu *c) {
Input *s = find(c->x[0]);
if (s) {
mic_stop(s->mic);
if (s->running)
rt_log("mic: capture stopped; recording device released");
s->running = 0;
}
ret_x(c, 0);

View File

@@ -1,7 +1,21 @@
/* No game data or physical microphone: exercise conversion, bounded capture and the polling ABI. */
#include "../runtime/audio_capture.c"
#include "../runtime/ov_audio_input.c"
#include <SDL3/SDL.h>
#include <assert.h>
static bool deny_recording;
static int device_open_calls;
static SDL_AudioDeviceID open_test_device(SDL_AudioDeviceID device, const SDL_AudioSpec *spec) {
device_open_calls++;
if (deny_recording) {
SDL_SetError("simulated recording permission denial");
return 0;
}
return SDL_OpenAudioDevice(device, spec);
}
#define SDL_OpenAudioDevice open_test_device
#include "../runtime/audio_capture.c"
#undef SDL_OpenAudioDevice
#include "../runtime/ov_audio_input.c"
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
@@ -20,6 +34,10 @@ void *ipc_guest(uint64_t addr, uint64_t size) {
return (void *)(uintptr_t)addr;
}
void rt_log(const char *fmt, ...) { (void)fmt; }
int rt_knob(const char *name, const char *def) {
assert(strcmp(name, "MICROPHONE") == 0);
return atoi(def);
}
static void ring_check(void) {
assert(SDL_InitSubSystem(SDL_INIT_AUDIO));
@@ -59,17 +77,38 @@ static void polling_check(void) {
c.x[5] = 4800;
open_input_locked(&c);
assert(c.x[0] == 0);
MicCapture *m = find(key)->mic;
assert(SDL_GetAudioStreamDevice(m->stream) == 0);
c.x[0] = key;
state_locked(&c);
assert(c.x[0] == 1);
deny_recording = true;
c.x[0] = key;
start_input_locked(&c);
assert(c.x[0] == BAD_INPUT);
assert(SDL_GetAudioStreamDevice(m->stream) == 0);
c.x[0] = key;
state_locked(&c);
assert(c.x[0] == 1);
deny_recording = false;
c.x[0] = key;
start_input_locked(&c);
assert(c.x[0] == 0);
SDL_AudioDeviceID active = SDL_GetAudioStreamDevice(m->stream);
assert(active);
c.x[0] = key;
start_input_locked(&c);
assert(c.x[0] == 0 && SDL_GetAudioStreamDevice(m->stream) == active);
c.x[0] = key;
state_locked(&c);
assert(c.x[0] == 2);
SDL_Delay(350); /* overflow the 100 ms ring, not system memory */
c.x[0] = key;
stop_input_locked(&c);
SDL_AudioSpec spec;
assert(!SDL_GetAudioDeviceFormat(active, &spec, NULL));
assert(SDL_GetAudioStreamDevice(m->stream) == 0);
c.x[0] = key;
filled_locked(&c);
assert(c.x[0] > 0 && c.x[0] <= 4800);
c.x[0] = key;
@@ -78,6 +117,9 @@ static void polling_check(void) {
read_input_locked(&c);
assert(c.x[0] > 0 && c.x[0] <= 4800);
c.x[0] = key;
start_input_locked(&c);
assert(c.x[0] == 0 && SDL_GetAudioStreamDevice(m->stream) != 0);
c.x[0] = key;
stop_input_locked(&c);
c.x[0] = key;
purge_input_locked(&c);
@@ -120,13 +162,34 @@ static void polling_check(void) {
assert(mic_available(NULL) == 0);
}
static void independent_streams_check(void) {
int previous_calls = device_open_calls;
MicCapture *a = mic_open(2, 1, 48000, 4800);
MicCapture *b = mic_open(2, 1, 48000, 4800);
assert(a && b);
assert(device_open_calls == previous_calls);
assert(SDL_GetAudioStreamDevice(a->stream) == 0 && SDL_GetAudioStreamDevice(b->stream) == 0);
assert(mic_start(a));
assert(SDL_GetAudioStreamDevice(b->stream) == 0);
assert(mic_start(b));
assert(device_open_calls == previous_calls + 2);
SDL_AudioDeviceID active = SDL_GetAudioStreamDevice(b->stream);
assert(active && active != SDL_GetAudioStreamDevice(a->stream));
mic_close(a);
assert(SDL_GetAudioStreamDevice(b->stream) == active);
mic_stop(b);
assert(SDL_GetAudioStreamDevice(b->stream) == 0);
mic_close(b);
}
int main(void) {
assert(SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"));
registry_lock = SDL_CreateMutex();
assert(registry_lock);
ring_check();
polling_check();
independent_streams_check();
SDL_DestroyMutex(registry_lock);
puts("PASS: sample order, bounded overflow, polling frame units, reopen, purge and failed-open regression");
puts("PASS: bounded capture, idle streams, start/stop, failed start and retry, independent streams and purge");
return 0;
}