summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2025-02-23 15:09:08 -0500
committerpommicket <pommicket@gmail.com>2025-02-25 15:16:08 -0500
commit4db599607e0d25a8da7f47d84f557b702f7e2f6a (patch)
treedf4577930448b43ac4ee17bbf0c4fc20238abe7d
parent27b5aa8289330bc7b9f3499bf98a84f0127f4899 (diff)
pulse simple API
-rw-r--r--README.md4
-rw-r--r--main.c6
-rw-r--r--meson.build4
-rw-r--r--video.c68
4 files changed, 65 insertions, 17 deletions
diff --git a/README.md b/README.md
index 11d237a..de93934 100644
--- a/README.md
+++ b/README.md
@@ -12,10 +12,10 @@ It features
# Building from source
camlet requires meson-build, a C compiler, and the development libraries
-for SDL2, SDL2\_ttf, GL (headers only), v4l2, udev, sodium, jpeglib (from IJG), avcodec, avformat, and fontconfig.
+for SDL2, SDL2\_ttf, GL (headers only), v4l2, udev, sodium, jpeglib (from IJG), avcodec, avformat, pulseaudio, and fontconfig.
These can all be installed on Debian/Ubuntu with
```sh
-sudo apt install clang meson libv4l-dev libudev-dev libsodium-dev libfontconfig-dev libgl-dev libsdl2-dev libsdl2-ttf-dev libjpeg-dev libavcodec-dev libavformat-dev
+sudo apt install clang meson libv4l-dev libudev-dev libsodium-dev libfontconfig-dev libgl-dev libsdl2-dev libsdl2-ttf-dev libjpeg-dev libpulse-dev libavcodec-dev libavformat-dev
```
diff --git a/main.c b/main.c
index d1e91ea..0e049c9 100644
--- a/main.c
+++ b/main.c
@@ -558,7 +558,7 @@ static bool take_picture(State *state) {
return false;
struct tm *tm = localtime((time_t[1]){time(NULL)});
strftime(path + strlen(path), sizeof path - strlen(path), "/%Y-%m-%d-%H-%M-%S", tm);
- const char *extension = state->mode == MODE_VIDEO ? "mkv" : image_format_extensions[state->image_format];
+ const char *extension = state->mode == MODE_VIDEO ? "mp4" : image_format_extensions[state->image_format];
snprintf(path + strlen(path), sizeof path - strlen(path), ".%s", extension);
bool success = false;
switch (state->mode) {
@@ -611,7 +611,6 @@ int main(void) {
if (TTF_Init() < 0) {
fatal_error("couldn't initialize SDL2_ttf: %s\n", TTF_GetError());
}
- state->video = video_init();
{
const char *home = getenv("HOME");
if (home) {
@@ -661,12 +660,15 @@ int main(void) {
if (!window) {
fatal_error("couldn't create window: %s", SDL_GetError());
}
+ state->video = video_init();
+
static const struct {
int maj, min;
} gl_versions_to_try[] = {
{4, 3},
{3, 0}
};
+
SDL_GLContext glctx = NULL;
for (size_t i = 0; !glctx && i < SDL_arraysize(gl_versions_to_try); i++) {
gl.version_major = gl_versions_to_try[i].maj;
diff --git a/meson.build b/meson.build
index 027007f..706a021 100644
--- a/meson.build
+++ b/meson.build
@@ -16,6 +16,8 @@ jpeg = dependency('libjpeg')
avcodec = dependency('libavcodec')
avformat = dependency('libavformat')
avutil = dependency('libavutil')
+pulse = dependency('libpulse')
+pulse_simple = dependency('libpulse-simple')
m_dep = cc.find_library('m', required: false)
if m_dep.found()
add_project_link_arguments('-lm', language: 'c')
@@ -26,5 +28,5 @@ else
debug_def = '-DDEBUG=0'
endif
executable('camlet', 'main.c', 'camera.c', 'video.c', '3rd_party/stb_image_write.c',
- dependencies: [v4l2, sdl2, sdl2_ttf, gl, udev, sodium, fontconfig, jpeg, avcodec, avformat, avutil],
+ dependencies: [v4l2, sdl2, sdl2_ttf, gl, udev, sodium, fontconfig, jpeg, avcodec, avformat, avutil, pulse, pulse_simple],
c_args: ['-Wno-unused-function', '-Wno-format-truncation', '-Wshadow', debug_def])
diff --git a/video.c b/video.c
index 536f0dc..d2dd374 100644
--- a/video.c
+++ b/video.c
@@ -1,24 +1,31 @@
#include "video.h"
-#include "camera.h"
+#include <stdatomic.h>
+#include <SDL.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
+#include <pulse/pulseaudio.h>
+#include <pulse/simple.h>
#include "util.h"
+#include "camera.h"
struct VideoContext {
bool recording;
double start_time;
+ double last_frame_time;
AVFormatContext *avf_context;
AVCodecContext *video_encoder;
AVFrame *video_frame;
AVPacket *av_packet;
AVStream *video_stream;
- int64_t video_pts;
+ int64_t next_video_pts;
+ pa_simple *pulseaudio;
};
VideoContext *video_init(void) {
- return calloc(1, sizeof(VideoContext));
+ VideoContext *ctx = calloc(1, sizeof(VideoContext));
+ return ctx;
}
bool video_start(VideoContext *ctx, const char *filename, int32_t width, int32_t height, int fps, int quality) {
@@ -93,9 +100,23 @@ bool video_start(VideoContext *ctx, const char *filename, int32_t width, int32_t
fprintf(stderr, "error: avformat_write_header: %s\n", av_err2str(err));
return false;
}
+ pa_sample_spec audio_format = {
+ .format = PA_SAMPLE_S16NE,
+ .channels = 2,
+ .rate = 44100
+ };
+ // NOTE: SDL2 audio capture is currently broken
+ // https://github.com/libsdl-org/SDL/issues/9706
+ // once SDL3 becomes commonplace enough, we can switch to that for capturing audio.
+ ctx->pulseaudio = pa_simple_new(NULL, "camlet", PA_STREAM_RECORD, NULL,
+ "microphone", &audio_format, NULL, NULL, &err);
+ if (!ctx->pulseaudio) {
+ fprintf(stderr, "%s", pa_strerror(err));
+ }
+ pa_simple_read(ctx->pulseaudio, (char[1]){0}, 1, &err);
ctx->recording = true;
- ctx->video_pts = 0;
- ctx->start_time = get_time_double();
+ ctx->next_video_pts = 0;
+ ctx->start_time = ctx->last_frame_time = get_time_double();
return true;
}
@@ -127,21 +148,40 @@ static bool write_frame(VideoContext *ctx, AVCodecContext *encoder, AVStream *st
}
bool video_submit_frame(VideoContext *ctx, Camera *camera) {
- if (!ctx || !camera) return false;
- int64_t next_pts = ctx->video_pts;
- int64_t curr_pts = (int64_t)((get_time_double() - ctx->start_time)
+ if (!ctx || !camera || !ctx->recording) return false;
+ double curr_time = get_time_double();
+ double time_since_start = curr_time - ctx->start_time;
+ double time_since_last_frame = curr_time - ctx->last_frame_time;
+ ctx->last_frame_time = curr_time;
+ int64_t video_pts = (int64_t)(time_since_start
* ctx->video_encoder->time_base.den
/ ctx->video_encoder->time_base.num);
- if (curr_pts >= next_pts) {
- int err = av_frame_make_writable(ctx->video_frame);
+ size_t audio_size = ctx->pulseaudio
+ ? (size_t)(time_since_last_frame * 44100 * 2 * sizeof(int16_t))
+ : 0;
+ int err;
+ while (audio_size > 0 && audio_size < SIZE_MAX / 2 /* just in case time goes backwards somehow */) {
+ uint8_t buf[4096];
+ size_t n = audio_size > sizeof buf ? sizeof buf : audio_size;
+ if (pa_simple_read(ctx->pulseaudio, buf, n, &err) < 0) {
+ static atomic_flag warned = ATOMIC_FLAG_INIT;
+ if (!atomic_flag_test_and_set_explicit(&warned, memory_order_relaxed)) {
+ fprintf(stderr, "error: pa_simple_read: %s\n", pa_strerror(err));
+ }
+ break;
+ }
+ audio_size -= n;
+ }
+ if (video_pts >= ctx->next_video_pts) {
+ err = av_frame_make_writable(ctx->video_frame);
if (err < 0) {
fprintf(stderr, "error: av_frame_make_writable: %s\n", av_err2str(err));
return false;
}
- ctx->video_frame->pts = curr_pts;
+ ctx->video_frame->pts = video_pts;
camera_copy_to_av_frame(camera, ctx->video_frame);
write_frame(ctx, ctx->video_encoder, ctx->video_stream, ctx->video_frame);
- ctx->video_pts = curr_pts + 1;
+ ctx->next_video_pts = video_pts + 1;
}
return true;
}
@@ -154,6 +194,10 @@ bool video_is_recording(VideoContext *ctx) {
void video_stop(VideoContext *ctx) {
if (!ctx) return;
if (ctx->recording) {
+ if (ctx->pulseaudio) {
+ pa_simple_free(ctx->pulseaudio);
+ ctx->pulseaudio = NULL;
+ }
ctx->recording = false;
// flush video encoder
write_frame(ctx, ctx->video_encoder, ctx->video_stream, NULL);