mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
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>
179 lines
5.0 KiB
C
179 lines
5.0 KiB
C
#include "audio_capture.h"
|
|
#include <SDL3/SDL.h>
|
|
#include <limits.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
struct MicCapture {
|
|
SDL_AudioStream *stream;
|
|
SDL_AudioDeviceID device;
|
|
unsigned char *ring;
|
|
int cap, head, used, bpf;
|
|
};
|
|
|
|
const char *mic_error(void) { return SDL_GetError(); }
|
|
int mic_device_count(void) {
|
|
if (!SDL_InitSubSystem(SDL_INIT_AUDIO))
|
|
return 0;
|
|
int n = 0;
|
|
SDL_AudioDeviceID *ids = SDL_GetAudioRecordingDevices(&n);
|
|
SDL_free(ids);
|
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
return n;
|
|
}
|
|
|
|
/* The SDL stream lock serializes the callback and every ring operation.
|
|
Discard the oldest frames on overflow, so a paused game cannot grow RAM. */
|
|
static void SDLCALL received(void *userdata, SDL_AudioStream *stream, int added, int total) {
|
|
(void)added;
|
|
(void)total;
|
|
MicCapture *m = userdata;
|
|
unsigned char scratch[8192];
|
|
int chunk = (int)sizeof scratch / m->bpf * m->bpf;
|
|
for (;;) {
|
|
int n = SDL_GetAudioStreamData(stream, scratch, chunk);
|
|
if (n <= 0)
|
|
break;
|
|
n -= n % m->bpf;
|
|
if (!n)
|
|
break;
|
|
const unsigned char *src = scratch;
|
|
if (n > m->cap) {
|
|
src += n - m->cap;
|
|
n = m->cap;
|
|
}
|
|
int drop = m->used + n - m->cap;
|
|
if (drop > 0) {
|
|
m->head = (m->head + drop) % m->cap;
|
|
m->used -= drop;
|
|
}
|
|
int tail = (m->head + m->used) % m->cap;
|
|
int first = n < m->cap - tail ? n : m->cap - tail;
|
|
memcpy(m->ring + tail, src, first);
|
|
memcpy(m->ring, src + first, n - first);
|
|
m->used += n;
|
|
}
|
|
}
|
|
|
|
MicCapture *mic_open(int format, int channels, int rate, int frames) {
|
|
SDL_AudioFormat sf;
|
|
switch (format) {
|
|
case 1:
|
|
sf = SDL_AUDIO_S8;
|
|
break;
|
|
case 2:
|
|
sf = SDL_AUDIO_S16LE;
|
|
break;
|
|
case 4:
|
|
sf = SDL_AUDIO_S32LE;
|
|
break;
|
|
case 5:
|
|
sf = SDL_AUDIO_F32LE;
|
|
break;
|
|
default:
|
|
SDL_SetError("unsupported guest sample format %d", format);
|
|
return NULL;
|
|
}
|
|
if (channels < 1 || channels > 8 || rate < 8000 || rate > 192000 || frames < 1) {
|
|
SDL_SetError("invalid capture format or capacity");
|
|
return NULL;
|
|
}
|
|
if (!SDL_InitSubSystem(SDL_INIT_AUDIO))
|
|
return NULL;
|
|
MicCapture *m = calloc(1, sizeof *m);
|
|
if (!m) {
|
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
return NULL;
|
|
}
|
|
m->bpf = channels * (SDL_AUDIO_BITSIZE(sf) / 8);
|
|
if (frames > rate * 2)
|
|
frames = rate * 2;
|
|
m->cap = frames * m->bpf;
|
|
m->ring = malloc(m->cap);
|
|
if (!m->ring) {
|
|
free(m);
|
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
return NULL;
|
|
}
|
|
SDL_AudioSpec spec = {.format = sf, .channels = channels, .freq = rate};
|
|
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) {
|
|
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 && 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;
|
|
mic_stop(m);
|
|
if (m->stream)
|
|
SDL_DestroyAudioStream(m->stream);
|
|
free(m->ring);
|
|
free(m);
|
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
}
|
|
void mic_purge(MicCapture *m) {
|
|
if (!m)
|
|
return;
|
|
SDL_LockAudioStream(m->stream);
|
|
SDL_ClearAudioStream(m->stream);
|
|
m->head = m->used = 0;
|
|
SDL_UnlockAudioStream(m->stream);
|
|
}
|
|
int mic_available(MicCapture *m) {
|
|
if (!m)
|
|
return 0;
|
|
SDL_LockAudioStream(m->stream);
|
|
int frames = m->used / m->bpf;
|
|
SDL_UnlockAudioStream(m->stream);
|
|
return frames;
|
|
}
|
|
int mic_capacity(MicCapture *m) { return m ? m->cap / m->bpf : 0; }
|
|
int mic_read(MicCapture *m, void *out, int frames) {
|
|
if (!m || !out || frames <= 0)
|
|
return 0;
|
|
SDL_LockAudioStream(m->stream);
|
|
int avail = m->used / m->bpf;
|
|
if (frames > avail)
|
|
frames = avail;
|
|
int n = frames * m->bpf;
|
|
int first = n < m->cap - m->head ? n : m->cap - m->head;
|
|
memcpy(out, m->ring + m->head, first);
|
|
memcpy((unsigned char *)out + first, m->ring, n - first);
|
|
m->head = (m->head + n) % m->cap;
|
|
m->used -= n;
|
|
SDL_UnlockAudioStream(m->stream);
|
|
return frames;
|
|
}
|
|
void mic_gain(MicCapture *m, float gain) {
|
|
if (m)
|
|
SDL_SetAudioStreamGain(m->stream, gain);
|
|
}
|