summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2025-02-25 22:48:04 -0500
committerpommicket <pommicket@gmail.com>2025-02-25 22:48:04 -0500
commit753651a2c21b5e7fc6725560ffdd5d4c01732359 (patch)
tree7f74f8774022a0388f8b234da3b81439ba5c28fc
parentb88b34dbd672ac2b58d4f7c12f96fa79337cfe1f (diff)
switch back to SDL for audio capture
-rw-r--r--README.md23
-rw-r--r--main.c3
-rw-r--r--video.c148
3 files changed, 74 insertions, 100 deletions
diff --git a/README.md b/README.md
index ac96955..58057e5 100644
--- a/README.md
+++ b/README.md
@@ -8,12 +8,14 @@ It features
- Full selection of resolutions available from camera
- Remembers settings across program launches
-# Debugging
+# Usage
-If you find a bug, please create an issue on GitHub; it will be helpful to have
-your settings and log file, located in `~/.config/camlet`.
-
-You can try deleting/renaming your settings file as a temporary solution, to reset camlet to its default settings.
+- <kbd>F1</kbd> - show this help text
+- <kbd>F2</kbd> - show debug info
+- <kbd>Space</kbd> - take a picture or start/stop recording video
+- <kbd>Escape</kbd> - open/close settings
+- <kbd>Ctrl</kbd>+<kbd>f</kbd> - open picture directory
+- <kbd>Tab</kbd> - switch between picture and video
# Building from source
@@ -30,3 +32,14 @@ You can build the debug version of camlet with `make` (outputs `camlet.debug`),
version with `make release` (outputs `release/camlet`), and install it with
`make install`, or e.g. `INSTALL_PREFIX=~/.local make install` to customize the installation directory (default: `/usr/local`).
You can also build the .deb installer with `make camlet.deb`.
+
+# Debugging
+
+- Known issue: pulseaudio audio capturing is broken on some versions of SDL2
+ https://github.com/libsdl-org/SDL/issues/9706. This bug has been fixed now, so hopefully
+ it will make it to your computer soon.
+
+If you find a bug, please create an issue on GitHub; it will be helpful to have
+your settings and log file, located in `~/.config/camlet`.
+
+You can try deleting/renaming your settings file as a temporary solution, to reset camlet to its default settings.
diff --git a/main.c b/main.c
index 00bcf1b..461fe06 100644
--- a/main.c
+++ b/main.c
@@ -2,7 +2,6 @@
/*
TODO:
-- switch back to SDL for audio now that that bug has been fixed
- application icon (and SDL_SetWindowIcon)
- cmdline argument for starting in video mode
*/
@@ -1570,7 +1569,7 @@ void main() {\n\
}
if (state->curr_menu == MENU_HELP) {
const char *text[] = {
- "F1 - open this help screen",
+ "F1 - show this help text",
"F2 - show debug info",
state->mode == MODE_VIDEO
? "Space - start/stop recording"
diff --git a/video.c b/video.c
index 16f7fa3..166ccee 100644
--- a/video.c
+++ b/video.c
@@ -4,8 +4,7 @@
#include <threads.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
-#include <pulse/pulseaudio.h>
-#include <pulse/simple.h>
+#include <SDL.h>
#include <unistd.h>
#include "log.h"
@@ -13,8 +12,6 @@
// no real harm in making this bigger, other than increased memory usage.
#define AUDIO_QUEUE_SIZE ((size_t)128 << 10)
-#define AUDIO_NOT_RECORDING (UINT32_MAX)
-#define AUDIO_QUIT (UINT32_MAX - 1)
struct VideoContext {
double start_time;
@@ -29,98 +26,62 @@ struct VideoContext {
AVStream *audio_stream;
int64_t next_video_pts;
int64_t next_audio_pts;
- thrd_t audio_thread;
- bool audio_thread_created;
+ SDL_AudioDeviceID audio_device;
bool recording;
// ring buffer of audio data.
float audio_queue[AUDIO_QUEUE_SIZE];
atomic_uint_fast32_t audio_head;
atomic_uint_fast32_t audio_tail;
- char _unused[128]; // reduce false sharing
+ char _unused1[128]; // reduce false sharing
};
-// NOTE: SDL2 pulseaudio capture is broken on some versions: https://github.com/libsdl-org/SDL/issues/9706
-// When SDL3 is widespread enough (e.g. available on Debian stable), we can switch to that for capturing.
-static int audio_thread(void *data) {
+// NOTE: SDL2 pulseaudio capture is broken on some versions of SDL 2.30: https://github.com/libsdl-org/SDL/issues/9706
+static void audio_callback(void *data, Uint8 *stream_u8, int len) {
VideoContext *ctx = data;
- pa_sample_spec audio_format = {
- .format = PA_SAMPLE_FLOAT32NE,
- .rate = 44100,
- .channels = 2,
- };
- // by default pulseaudio uses a crazy large buffer, like 500KB,
- // and doesn't send any audio until it's full.
- const pa_buffer_attr buffer_attr = {
- .maxlength = 8192,
- .fragsize = 4096,
- };
- int err = 0;
- pa_simple *pulseaudio = pa_simple_new(NULL, "camlet", PA_STREAM_RECORD, NULL,
- "microphone", &audio_format, NULL, &buffer_attr, &err);
- if (!pulseaudio) {
- log_error("couldn't connect to pulseaudio: %s", pa_strerror(err));
- return -1;
- }
- uint32_t warned[2] = {0};
- bool quit = false;
- // only this thread writes to ctx->tail, so we can have a local variable which mirrors it.
- uint32_t tail = AUDIO_NOT_RECORDING;
- while (!quit) {
- float buf[1024];
- int result = pa_simple_read(pulseaudio, buf, sizeof buf, &err);
- uint32_t head = atomic_load(&ctx->audio_head);
- if (head == AUDIO_NOT_RECORDING) {
- // not recording
- if (tail != AUDIO_NOT_RECORDING) {
- tail = AUDIO_NOT_RECORDING;
- atomic_store(&ctx->audio_tail, tail);
- }
- continue;
- } else if (head == AUDIO_QUIT) {
- break;
- } else if (tail == AUDIO_NOT_RECORDING) {
- // reset tail now that we are recording
- tail = 0;
+ const float *stream = (const float *)stream_u8;
+ // this call already happens-after any earlier writes to audio_tail, so relaxed is fine.
+ uint32_t tail = atomic_load_explicit(&ctx->audio_tail, memory_order_relaxed);
+ uint32_t head = atomic_load(&ctx->audio_head);
+ if ((tail - head + AUDIO_QUEUE_SIZE) % AUDIO_QUEUE_SIZE > AUDIO_QUEUE_SIZE * 3 / 4) {
+ static int warned;
+ if (warned < 10) {
+ log_warning("audio overrun");
+ warned++;
}
- if ((tail - head + AUDIO_QUEUE_SIZE) % AUDIO_QUEUE_SIZE > AUDIO_QUEUE_SIZE * 3 / 4) {
- if (warned[0] < 10) {
- log_warning("audio overrun");
- warned[0]++;
- }
- } else if (result >= 0) {
- const uint32_t nfloats = sizeof buf / sizeof(float);
- if (tail + nfloats <= AUDIO_QUEUE_SIZE) {
- // easy case
- memcpy(&ctx->audio_queue[tail], buf, sizeof buf);
- tail += nfloats;
- } else {
- // "wrap around" case
- memcpy(&ctx->audio_queue[tail], buf, (AUDIO_QUEUE_SIZE - tail) * sizeof(float));
- memcpy(&ctx->audio_queue[0], &buf[AUDIO_QUEUE_SIZE - tail], (tail + nfloats - AUDIO_QUEUE_SIZE) * sizeof(float));
- tail = tail + nfloats - AUDIO_QUEUE_SIZE;
- }
+ } else {
+ const uint32_t nfloats = (uint32_t)len / sizeof(float);
+ if (tail + nfloats <= AUDIO_QUEUE_SIZE) {
+ // easy case
+ memcpy(&ctx->audio_queue[tail], stream, len);
+ tail += nfloats;
} else {
- if (!warned[1]) {
- log_error("pa_simple_read: %s", pa_strerror(err));
- warned[1]++;
- }
+ // "wrap around" case
+ memcpy(&ctx->audio_queue[tail], stream, (AUDIO_QUEUE_SIZE - tail) * sizeof(float));
+ memcpy(&ctx->audio_queue[0], &stream[AUDIO_QUEUE_SIZE - tail], (tail + nfloats - AUDIO_QUEUE_SIZE) * sizeof(float));
+ tail = tail + nfloats - AUDIO_QUEUE_SIZE;
}
- atomic_store(&ctx->audio_tail, tail);
}
- pa_simple_free(pulseaudio);
- return 0;
+ atomic_store(&ctx->audio_tail, tail);
}
VideoContext *video_init(void) {
VideoContext *ctx = calloc(1, sizeof(VideoContext));
if (!ctx) return NULL;
- atomic_init(&ctx->audio_head, AUDIO_NOT_RECORDING);
- atomic_init(&ctx->audio_tail, AUDIO_NOT_RECORDING);
- if (thrd_create(&ctx->audio_thread, audio_thread, ctx) == thrd_success) {
- ctx->audio_thread_created = true;
- } else {
- log_perror("couldn't create audio thread");
+ atomic_init(&ctx->audio_head, 0);
+ atomic_init(&ctx->audio_tail, 0);
+
+ SDL_AudioSpec desired = {
+ .channels = 2,
+ .freq = 44100,
+ .format = AUDIO_F32,
+ .samples = 2048,
+ .callback = audio_callback,
+ .userdata = ctx,
+ }, obtained = {0};
+ ctx->audio_device = SDL_OpenAudioDevice(NULL, 1, &desired, &obtained, SDL_AUDIO_ALLOW_SAMPLES_CHANGE);
+ if (!ctx->audio_device) {
+ log_error("couldn't create audio device: %s", SDL_GetError());
}
return ctx;
}
@@ -131,11 +92,7 @@ bool video_start(VideoContext *ctx, const char *filename, int32_t width, int32_t
return true;
}
video_stop(ctx);
- // wait for capture thread to acknowledge that we have stopped recording
- if (ctx->audio_thread_created) while (true) {
- if (atomic_load(&ctx->audio_tail) == AUDIO_NOT_RECORDING) break;
- usleep(1000);
- }
+ bool have_audio = false;
int err = avformat_alloc_output_context2(&ctx->avf_context, NULL, NULL, filename);
if (!ctx->avf_context) {
log_error("avformat_alloc_output_context2 \"%s\": %s", filename, av_err2str(err));
@@ -254,6 +211,7 @@ bool video_start(VideoContext *ctx, const char *filename, int32_t width, int32_t
log_error("avcodec_parameters_from_context (audio): %s", av_err2str(err));
goto no_audio;
}
+ have_audio = true;
no_audio:
err = avformat_write_header(ctx->avf_context, NULL);
if (err < 0) {
@@ -265,6 +223,10 @@ no_audio:
ctx->next_audio_pts = 0;
ctx->recording = true;
ctx->start_time = get_time_double();
+ if (have_audio) {
+ // start recording audio
+ SDL_PauseAudioDevice(ctx->audio_device, 0);
+ }
return true;
}
@@ -299,7 +261,7 @@ bool video_submit_frame(VideoContext *ctx, Camera *camera) {
if (!ctx || !camera || !ctx->recording) return false;
double curr_time = get_time_double();
double time_since_start = curr_time - ctx->start_time;
- if (ctx->audio_thread_created) {
+ if (ctx->audio_device) {
// process audio
// only this thread writes to head, so relaxed is fine.
uint32_t head = atomic_load_explicit(&ctx->audio_head, memory_order_relaxed);
@@ -313,9 +275,7 @@ bool video_submit_frame(VideoContext *ctx, Camera *camera) {
ctx->audio_frame->pts = ctx->next_audio_pts;
uint32_t nfloats = (uint32_t)ctx->audio_frame_samples * 2;
bool frame_ready = false;
- if (tail == AUDIO_NOT_RECORDING) {
- // capture thread doesn't even know we're recording yet
- } else if (head + nfloats < AUDIO_QUEUE_SIZE) {
+ if (head + nfloats < AUDIO_QUEUE_SIZE) {
// easy case
frame_ready = head + nfloats <= tail || head > tail /* tail wrapped around */;
if (frame_ready) {
@@ -373,7 +333,12 @@ bool video_is_recording(VideoContext *ctx) {
void video_stop(VideoContext *ctx) {
if (!ctx) return;
if (ctx->recording) {
- atomic_store(&ctx->audio_head, AUDIO_NOT_RECORDING);
+ SDL_PauseAudioDevice(ctx->audio_device, 1);
+ // block until callback finishes.
+ SDL_LockAudioDevice(ctx->audio_device);
+ SDL_UnlockAudioDevice(ctx->audio_device);
+ atomic_store(&ctx->audio_head, 0);
+ atomic_store(&ctx->audio_tail, 0);
ctx->recording = false;
// flush video encoder
write_frame(ctx, ctx->video_encoder, ctx->video_stream, NULL);
@@ -407,11 +372,8 @@ void video_stop(VideoContext *ctx) {
void video_quit(VideoContext *ctx) {
if (!ctx) return;
video_stop(ctx);
- if (ctx->audio_thread_created) {
- atomic_store(&ctx->audio_head, AUDIO_QUIT);
- if (thrd_join(ctx->audio_thread, NULL) != thrd_success) {
- log_perror("thrd_join");
- }
+ if (ctx->audio_device) {
+ SDL_CloseAudioDevice(ctx->audio_device);
}
free(ctx);
}