diff options
author | pommicket <pommicket@gmail.com> | 2025-02-23 15:09:08 -0500 |
---|---|---|
committer | pommicket <pommicket@gmail.com> | 2025-02-25 15:16:08 -0500 |
commit | 4db599607e0d25a8da7f47d84f557b702f7e2f6a (patch) | |
tree | df4577930448b43ac4ee17bbf0c4fc20238abe7d | |
parent | 27b5aa8289330bc7b9f3499bf98a84f0127f4899 (diff) |
pulse simple API
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | main.c | 6 | ||||
-rw-r--r-- | meson.build | 4 | ||||
-rw-r--r-- | video.c | 68 |
4 files changed, 65 insertions, 17 deletions
@@ -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 ``` @@ -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]) @@ -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); |