···11+// recorder.c — On-device MP4 recording (H.264 + AAC in fragmented MP4)
22+// Encoder runs in a dedicated thread so the main 60fps loop is never blocked.
33+44+#ifdef HAVE_AVCODEC
55+66+#include "recorder.h"
77+#include <stdio.h>
88+#include <stdlib.h>
99+#include <string.h>
1010+#include <pthread.h>
1111+#include <stdatomic.h>
1212+#include <time.h>
1313+#include <sys/stat.h>
1414+1515+#include <libavcodec/avcodec.h>
1616+#include <libavformat/avformat.h>
1717+#include <libavutil/opt.h>
1818+#include <libavutil/imgutils.h>
1919+#include <libavutil/channel_layout.h>
2020+#include <libswscale/swscale.h>
2121+#include <libswresample/swresample.h>
2222+2323+// ── Ring buffer for audio ──
2424+#define AUDIO_RING_SAMPLES (192000 * 2) // ~1 second at 192kHz stereo (interleaved)
2525+#define AUDIO_RING_MASK (AUDIO_RING_SAMPLES - 1)
2626+// Ensure power of 2 (192000*2=384000 is not power of 2, so we use modulo instead)
2727+2828+// ── Triple-buffer for video frames ──
2929+#define VIDEO_SLOTS 3
3030+3131+extern void ac_log(const char *fmt, ...);
3232+3333+struct ACRecorder {
3434+ // Config
3535+ int width, height, fps;
3636+ unsigned int audio_src_rate;
3737+3838+ // State
3939+ volatile int recording;
4040+ volatile int stopping;
4141+ struct timespec start_time;
4242+4343+ // Video triple-buffer
4444+ uint32_t *video_buf[VIDEO_SLOTS];
4545+ int video_stride;
4646+ atomic_int video_write_idx; // main thread writes here (mod VIDEO_SLOTS)
4747+ atomic_int video_read_idx; // encoder reads here (mod VIDEO_SLOTS)
4848+ atomic_int video_ready; // number of frames ready to encode
4949+5050+ // Audio ring buffer (interleaved int16 stereo at source rate)
5151+ int16_t *audio_ring;
5252+ int audio_ring_size;
5353+ atomic_int audio_write_pos; // monotonic write position
5454+ atomic_int audio_read_pos; // encoder thread read position
5555+5656+ // Encoder thread
5757+ pthread_t thread;
5858+ volatile int thread_running;
5959+6060+ // ffmpeg contexts
6161+ AVFormatContext *fmt_ctx;
6262+ AVCodecContext *video_enc;
6363+ AVCodecContext *audio_enc;
6464+ AVStream *video_st;
6565+ AVStream *audio_st;
6666+ struct SwsContext *sws;
6767+ SwrContext *swr;
6868+ AVFrame *video_frame; // YUV420P frame for encoder
6969+ AVFrame *audio_frame; // float planar frame for AAC encoder
7070+ int64_t video_pts;
7171+ int64_t audio_pts;
7272+7373+ // Audio resampler output buffer
7474+ int16_t *audio_resample_buf;
7575+ int audio_resample_buf_size;
7676+};
7777+7878+// ── Forward declarations ──
7979+static void *encoder_thread(void *arg);
8080+static int setup_video_stream(ACRecorder *rec);
8181+static int setup_audio_stream(ACRecorder *rec);
8282+static void encode_video_frame(ACRecorder *rec, const uint32_t *pixels, int stride);
8383+static void encode_audio_chunk(ACRecorder *rec);
8484+static void flush_encoders(ACRecorder *rec);
8585+8686+// ── Public API ──
8787+8888+ACRecorder *recorder_create(int width, int height, int fps, unsigned int audio_rate) {
8989+ ACRecorder *rec = calloc(1, sizeof(ACRecorder));
9090+ if (!rec) return NULL;
9191+9292+ rec->width = width;
9393+ rec->height = height;
9494+ rec->fps = fps;
9595+ rec->audio_src_rate = audio_rate;
9696+9797+ // Allocate video triple-buffer
9898+ size_t frame_bytes = (size_t)width * height * sizeof(uint32_t);
9999+ for (int i = 0; i < VIDEO_SLOTS; i++) {
100100+ rec->video_buf[i] = malloc(frame_bytes);
101101+ if (!rec->video_buf[i]) {
102102+ for (int j = 0; j < i; j++) free(rec->video_buf[j]);
103103+ free(rec);
104104+ return NULL;
105105+ }
106106+ }
107107+ rec->video_stride = width;
108108+109109+ // Allocate audio ring buffer
110110+ rec->audio_ring_size = AUDIO_RING_SAMPLES;
111111+ rec->audio_ring = calloc(rec->audio_ring_size, sizeof(int16_t));
112112+ if (!rec->audio_ring) {
113113+ for (int i = 0; i < VIDEO_SLOTS; i++) free(rec->video_buf[i]);
114114+ free(rec);
115115+ return NULL;
116116+ }
117117+118118+ return rec;
119119+}
120120+121121+int recorder_start(ACRecorder *rec, const char *path) {
122122+ if (!rec || rec->recording) return -1;
123123+124124+ ac_log("[recorder] starting: %s (%dx%d @ %dfps, audio %uHz→48kHz)\n",
125125+ path, rec->width, rec->height, rec->fps, rec->audio_src_rate);
126126+127127+ // Create output format context (fragmented MP4)
128128+ int ret = avformat_alloc_output_context2(&rec->fmt_ctx, NULL, "mp4", path);
129129+ if (ret < 0 || !rec->fmt_ctx) {
130130+ ac_log("[recorder] failed to create output context\n");
131131+ return -1;
132132+ }
133133+134134+ // Setup streams
135135+ if (setup_video_stream(rec) < 0) {
136136+ avformat_free_context(rec->fmt_ctx);
137137+ rec->fmt_ctx = NULL;
138138+ return -1;
139139+ }
140140+ if (setup_audio_stream(rec) < 0) {
141141+ avcodec_free_context(&rec->video_enc);
142142+ avformat_free_context(rec->fmt_ctx);
143143+ rec->fmt_ctx = NULL;
144144+ return -1;
145145+ }
146146+147147+ // Open output file
148148+ if (!(rec->fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
149149+ ret = avio_open(&rec->fmt_ctx->pb, path, AVIO_FLAG_WRITE);
150150+ if (ret < 0) {
151151+ ac_log("[recorder] failed to open output file: %s\n", path);
152152+ avcodec_free_context(&rec->video_enc);
153153+ avcodec_free_context(&rec->audio_enc);
154154+ avformat_free_context(rec->fmt_ctx);
155155+ rec->fmt_ctx = NULL;
156156+ return -1;
157157+ }
158158+ }
159159+160160+ // Set fragmented MP4 options (crash-safe)
161161+ AVDictionary *opts = NULL;
162162+ av_dict_set(&opts, "movflags", "frag_keyframe+empty_moov+default_base_moof", 0);
163163+164164+ ret = avformat_write_header(rec->fmt_ctx, &opts);
165165+ av_dict_free(&opts);
166166+ if (ret < 0) {
167167+ ac_log("[recorder] failed to write header\n");
168168+ avio_closep(&rec->fmt_ctx->pb);
169169+ avcodec_free_context(&rec->video_enc);
170170+ avcodec_free_context(&rec->audio_enc);
171171+ avformat_free_context(rec->fmt_ctx);
172172+ rec->fmt_ctx = NULL;
173173+ return -1;
174174+ }
175175+176176+ // Reset state
177177+ rec->video_pts = 0;
178178+ rec->audio_pts = 0;
179179+ atomic_store(&rec->video_write_idx, 0);
180180+ atomic_store(&rec->video_read_idx, 0);
181181+ atomic_store(&rec->video_ready, 0);
182182+ atomic_store(&rec->audio_write_pos, 0);
183183+ atomic_store(&rec->audio_read_pos, 0);
184184+ rec->stopping = 0;
185185+186186+ clock_gettime(CLOCK_MONOTONIC, &rec->start_time);
187187+ rec->recording = 1;
188188+189189+ // Start encoder thread
190190+ rec->thread_running = 1;
191191+ if (pthread_create(&rec->thread, NULL, encoder_thread, rec) != 0) {
192192+ ac_log("[recorder] failed to create encoder thread\n");
193193+ rec->recording = 0;
194194+ rec->thread_running = 0;
195195+ av_write_trailer(rec->fmt_ctx);
196196+ avio_closep(&rec->fmt_ctx->pb);
197197+ avcodec_free_context(&rec->video_enc);
198198+ avcodec_free_context(&rec->audio_enc);
199199+ avformat_free_context(rec->fmt_ctx);
200200+ rec->fmt_ctx = NULL;
201201+ return -1;
202202+ }
203203+204204+ ac_log("[recorder] recording started\n");
205205+ return 0;
206206+}
207207+208208+void recorder_submit_video(ACRecorder *rec, const uint32_t *pixels, int stride) {
209209+ if (!rec || !rec->recording) return;
210210+211211+ // Copy into next write slot
212212+ int slot = atomic_load(&rec->video_write_idx) % VIDEO_SLOTS;
213213+214214+ // Copy row by row in case stride differs
215215+ for (int y = 0; y < rec->height; y++) {
216216+ memcpy(rec->video_buf[slot] + y * rec->width,
217217+ pixels + y * stride,
218218+ rec->width * sizeof(uint32_t));
219219+ }
220220+221221+ atomic_fetch_add(&rec->video_write_idx, 1);
222222+ atomic_fetch_add(&rec->video_ready, 1);
223223+}
224224+225225+void recorder_submit_audio(ACRecorder *rec, const int16_t *pcm, int frames) {
226226+ if (!rec || !rec->recording) return;
227227+228228+ int samples = frames * 2; // stereo interleaved
229229+ int wp = atomic_load(&rec->audio_write_pos);
230230+231231+ for (int i = 0; i < samples; i++) {
232232+ rec->audio_ring[(wp + i) % rec->audio_ring_size] = pcm[i];
233233+ }
234234+ atomic_fetch_add(&rec->audio_write_pos, samples);
235235+}
236236+237237+void recorder_stop(ACRecorder *rec) {
238238+ if (!rec || !rec->recording) return;
239239+240240+ ac_log("[recorder] stopping...\n");
241241+ rec->stopping = 1;
242242+ rec->recording = 0;
243243+244244+ // Wait for encoder thread to finish
245245+ if (rec->thread_running) {
246246+ pthread_join(rec->thread, NULL);
247247+ rec->thread_running = 0;
248248+ }
249249+250250+ // Flush remaining frames
251251+ flush_encoders(rec);
252252+253253+ // Finalize MP4
254254+ if (rec->fmt_ctx) {
255255+ av_write_trailer(rec->fmt_ctx);
256256+ if (!(rec->fmt_ctx->oformat->flags & AVFMT_NOFILE))
257257+ avio_closep(&rec->fmt_ctx->pb);
258258+ }
259259+260260+ // Free encoder resources
261261+ if (rec->sws) { sws_freeContext(rec->sws); rec->sws = NULL; }
262262+ if (rec->swr) { swr_free(&rec->swr); }
263263+ if (rec->video_frame) { av_frame_free(&rec->video_frame); }
264264+ if (rec->audio_frame) { av_frame_free(&rec->audio_frame); }
265265+ if (rec->video_enc) { avcodec_free_context(&rec->video_enc); }
266266+ if (rec->audio_enc) { avcodec_free_context(&rec->audio_enc); }
267267+ if (rec->fmt_ctx) { avformat_free_context(rec->fmt_ctx); rec->fmt_ctx = NULL; }
268268+ if (rec->audio_resample_buf) { free(rec->audio_resample_buf); rec->audio_resample_buf = NULL; }
269269+270270+ ac_log("[recorder] stopped, file finalized\n");
271271+}
272272+273273+int recorder_is_recording(ACRecorder *rec) {
274274+ return rec ? rec->recording : 0;
275275+}
276276+277277+double recorder_elapsed(ACRecorder *rec) {
278278+ if (!rec || !rec->recording) return 0.0;
279279+ struct timespec now;
280280+ clock_gettime(CLOCK_MONOTONIC, &now);
281281+ return (now.tv_sec - rec->start_time.tv_sec) +
282282+ (now.tv_nsec - rec->start_time.tv_nsec) / 1e9;
283283+}
284284+285285+void recorder_destroy(ACRecorder *rec) {
286286+ if (!rec) return;
287287+ if (rec->recording) recorder_stop(rec);
288288+289289+ for (int i = 0; i < VIDEO_SLOTS; i++) free(rec->video_buf[i]);
290290+ free(rec->audio_ring);
291291+ free(rec);
292292+}
293293+294294+// ── Internal: stream setup ──
295295+296296+static int setup_video_stream(ACRecorder *rec) {
297297+ const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
298298+ if (!codec) {
299299+ // Fallback to MPEG4 if H.264 not available (ffmpeg-free on Fedora)
300300+ codec = avcodec_find_encoder(AV_CODEC_ID_MPEG4);
301301+ if (!codec) {
302302+ ac_log("[recorder] no video encoder found\n");
303303+ return -1;
304304+ }
305305+ ac_log("[recorder] H.264 not available, using MPEG-4\n");
306306+ }
307307+308308+ rec->video_st = avformat_new_stream(rec->fmt_ctx, NULL);
309309+ if (!rec->video_st) return -1;
310310+311311+ rec->video_enc = avcodec_alloc_context3(codec);
312312+ if (!rec->video_enc) return -1;
313313+314314+ rec->video_enc->width = rec->width;
315315+ rec->video_enc->height = rec->height;
316316+ rec->video_enc->time_base = (AVRational){1, rec->fps};
317317+ rec->video_enc->framerate = (AVRational){rec->fps, 1};
318318+ rec->video_enc->pix_fmt = AV_PIX_FMT_YUV420P;
319319+ rec->video_enc->gop_size = rec->fps; // Keyframe every second
320320+ rec->video_enc->max_b_frames = 0; // No B-frames for low latency
321321+322322+ // Encoder-specific options
323323+ if (codec->id == AV_CODEC_ID_H264) {
324324+ av_opt_set(rec->video_enc->priv_data, "preset", "ultrafast", 0);
325325+ av_opt_set(rec->video_enc->priv_data, "tune", "zerolatency", 0);
326326+ rec->video_enc->bit_rate = 4000000; // 4 Mbps for crisp pixel art
327327+ } else {
328328+ rec->video_enc->bit_rate = 4000000;
329329+ }
330330+331331+ if (rec->fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
332332+ rec->video_enc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
333333+334334+ int ret = avcodec_open2(rec->video_enc, codec, NULL);
335335+ if (ret < 0) {
336336+ ac_log("[recorder] failed to open video encoder\n");
337337+ return -1;
338338+ }
339339+340340+ ret = avcodec_parameters_from_context(rec->video_st->codecpar, rec->video_enc);
341341+ if (ret < 0) return -1;
342342+ rec->video_st->time_base = rec->video_enc->time_base;
343343+344344+ // Allocate YUV frame
345345+ rec->video_frame = av_frame_alloc();
346346+ rec->video_frame->format = AV_PIX_FMT_YUV420P;
347347+ rec->video_frame->width = rec->width;
348348+ rec->video_frame->height = rec->height;
349349+ av_frame_get_buffer(rec->video_frame, 0);
350350+351351+ // Setup color space converter (ARGB → YUV420P)
352352+ // Note: our ARGB32 is stored as 0xAARRGGBB in memory, which on little-endian
353353+ // is byte order B, G, R, A → AV_PIX_FMT_BGRA
354354+ rec->sws = sws_getContext(
355355+ rec->width, rec->height, AV_PIX_FMT_BGRA,
356356+ rec->width, rec->height, AV_PIX_FMT_YUV420P,
357357+ SWS_FAST_BILINEAR, NULL, NULL, NULL);
358358+ if (!rec->sws) {
359359+ ac_log("[recorder] failed to create sws context\n");
360360+ return -1;
361361+ }
362362+363363+ ac_log("[recorder] video: %s %dx%d @ %dfps\n",
364364+ codec->name, rec->width, rec->height, rec->fps);
365365+ return 0;
366366+}
367367+368368+static int setup_audio_stream(ACRecorder *rec) {
369369+ const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
370370+ if (!codec) {
371371+ ac_log("[recorder] AAC encoder not found, trying mp2\n");
372372+ codec = avcodec_find_encoder(AV_CODEC_ID_MP2);
373373+ if (!codec) {
374374+ ac_log("[recorder] no audio encoder found\n");
375375+ return -1;
376376+ }
377377+ }
378378+379379+ rec->audio_st = avformat_new_stream(rec->fmt_ctx, NULL);
380380+ if (!rec->audio_st) return -1;
381381+382382+ rec->audio_enc = avcodec_alloc_context3(codec);
383383+ if (!rec->audio_enc) return -1;
384384+385385+ rec->audio_enc->sample_rate = 48000;
386386+ rec->audio_enc->bit_rate = 128000;
387387+ AVChannelLayout stereo = AV_CHANNEL_LAYOUT_STEREO;
388388+ av_channel_layout_copy(&rec->audio_enc->ch_layout, &stereo);
389389+ rec->audio_enc->sample_fmt = codec->sample_fmts ? codec->sample_fmts[0] : AV_SAMPLE_FMT_FLTP;
390390+ rec->audio_enc->time_base = (AVRational){1, 48000};
391391+392392+ if (rec->fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
393393+ rec->audio_enc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
394394+395395+ int ret = avcodec_open2(rec->audio_enc, codec, NULL);
396396+ if (ret < 0) {
397397+ ac_log("[recorder] failed to open audio encoder\n");
398398+ return -1;
399399+ }
400400+401401+ ret = avcodec_parameters_from_context(rec->audio_st->codecpar, rec->audio_enc);
402402+ if (ret < 0) return -1;
403403+ rec->audio_st->time_base = rec->audio_enc->time_base;
404404+405405+ // Allocate audio frame
406406+ rec->audio_frame = av_frame_alloc();
407407+ rec->audio_frame->format = rec->audio_enc->sample_fmt;
408408+ av_channel_layout_copy(&rec->audio_frame->ch_layout, &rec->audio_enc->ch_layout);
409409+ rec->audio_frame->sample_rate = 48000;
410410+ rec->audio_frame->nb_samples = rec->audio_enc->frame_size;
411411+ if (rec->audio_frame->nb_samples == 0)
412412+ rec->audio_frame->nb_samples = 1024;
413413+ av_frame_get_buffer(rec->audio_frame, 0);
414414+415415+ // Setup resampler: source rate stereo int16 → 48kHz stereo float planar
416416+ ret = swr_alloc_set_opts2(&rec->swr,
417417+ &stereo, rec->audio_enc->sample_fmt, 48000,
418418+ &stereo, AV_SAMPLE_FMT_S16, rec->audio_src_rate,
419419+ 0, NULL);
420420+ if (ret < 0 || !rec->swr) {
421421+ ac_log("[recorder] failed to create resampler\n");
422422+ return -1;
423423+ }
424424+ ret = swr_init(rec->swr);
425425+ if (ret < 0) {
426426+ ac_log("[recorder] failed to init resampler\n");
427427+ return -1;
428428+ }
429429+430430+ ac_log("[recorder] audio: %s %uHz→48kHz, %d-sample frames\n",
431431+ codec->name, rec->audio_src_rate, rec->audio_frame->nb_samples);
432432+ return 0;
433433+}
434434+435435+// ── Internal: encoder thread ──
436436+437437+static void *encoder_thread(void *arg) {
438438+ ACRecorder *rec = (ACRecorder *)arg;
439439+440440+ while (rec->recording || atomic_load(&rec->video_ready) > 0) {
441441+ int did_work = 0;
442442+443443+ // Encode pending video frames
444444+ while (atomic_load(&rec->video_ready) > 0) {
445445+ int slot = atomic_load(&rec->video_read_idx) % VIDEO_SLOTS;
446446+ encode_video_frame(rec, rec->video_buf[slot], rec->width);
447447+ atomic_fetch_add(&rec->video_read_idx, 1);
448448+ atomic_fetch_sub(&rec->video_ready, 1);
449449+ did_work = 1;
450450+ }
451451+452452+ // Encode pending audio
453453+ int avail = atomic_load(&rec->audio_write_pos) - atomic_load(&rec->audio_read_pos);
454454+ if (avail >= rec->audio_frame->nb_samples * 2) { // *2 for stereo
455455+ encode_audio_chunk(rec);
456456+ did_work = 1;
457457+ }
458458+459459+ if (!did_work) {
460460+ // Sleep ~2ms to avoid busy-waiting
461461+ struct timespec ts = {0, 2000000};
462462+ nanosleep(&ts, NULL);
463463+ }
464464+ }
465465+466466+ rec->thread_running = 0;
467467+ return NULL;
468468+}
469469+470470+static void encode_video_frame(ACRecorder *rec, const uint32_t *pixels, int stride) {
471471+ // Convert ARGB32 → YUV420P
472472+ const uint8_t *src_data[1] = { (const uint8_t *)pixels };
473473+ int src_linesize[1] = { stride * 4 };
474474+475475+ av_frame_make_writable(rec->video_frame);
476476+ sws_scale(rec->sws, src_data, src_linesize, 0, rec->height,
477477+ rec->video_frame->data, rec->video_frame->linesize);
478478+479479+ rec->video_frame->pts = rec->video_pts++;
480480+481481+ // Send frame to encoder
482482+ int ret = avcodec_send_frame(rec->video_enc, rec->video_frame);
483483+ if (ret < 0) return;
484484+485485+ // Read all available packets
486486+ AVPacket *pkt = av_packet_alloc();
487487+ while (avcodec_receive_packet(rec->video_enc, pkt) == 0) {
488488+ av_packet_rescale_ts(pkt, rec->video_enc->time_base, rec->video_st->time_base);
489489+ pkt->stream_index = rec->video_st->index;
490490+ av_interleaved_write_frame(rec->fmt_ctx, pkt);
491491+ av_packet_unref(pkt);
492492+ }
493493+ av_packet_free(&pkt);
494494+}
495495+496496+static void encode_audio_chunk(ACRecorder *rec) {
497497+ int frame_samples = rec->audio_frame->nb_samples;
498498+ int src_samples_needed = frame_samples * 2; // stereo interleaved
499499+500500+ // How many source samples do we need for one output frame?
501501+ // At 192kHz→48kHz that's a 4:1 ratio, so we need 4x the output frame size
502502+ int ratio = (rec->audio_src_rate + 47999) / 48000; // ceil
503503+ int src_needed = frame_samples * ratio * 2; // stereo interleaved
504504+505505+ int rp = atomic_load(&rec->audio_read_pos);
506506+ int avail = atomic_load(&rec->audio_write_pos) - rp;
507507+ if (avail < src_needed) return;
508508+509509+ // Copy source samples from ring buffer into a contiguous buffer
510510+ if (!rec->audio_resample_buf || rec->audio_resample_buf_size < src_needed) {
511511+ free(rec->audio_resample_buf);
512512+ rec->audio_resample_buf_size = src_needed * 2; // over-allocate
513513+ rec->audio_resample_buf = malloc(rec->audio_resample_buf_size * sizeof(int16_t));
514514+ }
515515+516516+ for (int i = 0; i < src_needed; i++) {
517517+ rec->audio_resample_buf[i] = rec->audio_ring[(rp + i) % rec->audio_ring_size];
518518+ }
519519+ atomic_fetch_add(&rec->audio_read_pos, src_needed);
520520+521521+ // Resample and encode
522522+ av_frame_make_writable(rec->audio_frame);
523523+524524+ const uint8_t *in_data[1] = { (const uint8_t *)rec->audio_resample_buf };
525525+ int in_samples = src_needed / 2; // frames (not samples)
526526+527527+ int out_samples = swr_convert(rec->swr,
528528+ rec->audio_frame->data, frame_samples,
529529+ in_data, in_samples);
530530+531531+ if (out_samples <= 0) return;
532532+533533+ rec->audio_frame->nb_samples = out_samples;
534534+ rec->audio_frame->pts = rec->audio_pts;
535535+ rec->audio_pts += out_samples;
536536+537537+ int ret = avcodec_send_frame(rec->audio_enc, rec->audio_frame);
538538+ if (ret < 0) return;
539539+540540+ AVPacket *pkt = av_packet_alloc();
541541+ while (avcodec_receive_packet(rec->audio_enc, pkt) == 0) {
542542+ av_packet_rescale_ts(pkt, rec->audio_enc->time_base, rec->audio_st->time_base);
543543+ pkt->stream_index = rec->audio_st->index;
544544+ av_interleaved_write_frame(rec->fmt_ctx, pkt);
545545+ av_packet_unref(pkt);
546546+ }
547547+ av_packet_free(&pkt);
548548+}
549549+550550+static void flush_encoders(ACRecorder *rec) {
551551+ AVPacket *pkt = av_packet_alloc();
552552+553553+ // Flush video encoder
554554+ avcodec_send_frame(rec->video_enc, NULL);
555555+ while (avcodec_receive_packet(rec->video_enc, pkt) == 0) {
556556+ av_packet_rescale_ts(pkt, rec->video_enc->time_base, rec->video_st->time_base);
557557+ pkt->stream_index = rec->video_st->index;
558558+ av_interleaved_write_frame(rec->fmt_ctx, pkt);
559559+ av_packet_unref(pkt);
560560+ }
561561+562562+ // Flush remaining audio through resampler
563563+ if (rec->swr) {
564564+ av_frame_make_writable(rec->audio_frame);
565565+ int flushed = swr_convert(rec->swr,
566566+ rec->audio_frame->data, rec->audio_frame->nb_samples,
567567+ NULL, 0);
568568+ if (flushed > 0) {
569569+ rec->audio_frame->nb_samples = flushed;
570570+ rec->audio_frame->pts = rec->audio_pts;
571571+ rec->audio_pts += flushed;
572572+ avcodec_send_frame(rec->audio_enc, rec->audio_frame);
573573+ }
574574+ }
575575+576576+ // Flush audio encoder
577577+ avcodec_send_frame(rec->audio_enc, NULL);
578578+ while (avcodec_receive_packet(rec->audio_enc, pkt) == 0) {
579579+ av_packet_rescale_ts(pkt, rec->audio_enc->time_base, rec->audio_st->time_base);
580580+ pkt->stream_index = rec->audio_st->index;
581581+ av_interleaved_write_frame(rec->fmt_ctx, pkt);
582582+ av_packet_unref(pkt);
583583+ }
584584+585585+ av_packet_free(&pkt);
586586+}
587587+588588+#endif /* HAVE_AVCODEC */
+58
fedac/native/src/recorder.h
···11+// recorder.h — On-device video recording (framebuffer + audio → MP4)
22+// Uses libavcodec/libavformat for H.264 + AAC encoding in fragmented MP4.
33+44+#ifndef AC_RECORDER_H
55+#define AC_RECORDER_H
66+77+#ifdef HAVE_AVCODEC
88+99+#include <stdint.h>
1010+1111+typedef struct ACRecorder ACRecorder;
1212+1313+// Create recorder for given framebuffer dimensions and audio sample rate.
1414+// fps is the target video frame rate (typically 60).
1515+// audio_rate is the source PCM sample rate (e.g., 192000).
1616+ACRecorder *recorder_create(int width, int height, int fps, unsigned int audio_rate);
1717+1818+// Start recording to the given path (e.g., "/mnt/rec/2026-03-20_14-30-00.mp4").
1919+// Returns 0 on success, -1 on failure.
2020+int recorder_start(ACRecorder *rec, const char *path);
2121+2222+// Submit a video frame (ARGB32 pixels, stride in pixels).
2323+// Non-blocking: copies pixels and signals the encoder thread.
2424+void recorder_submit_video(ACRecorder *rec, const uint32_t *pixels, int stride);
2525+2626+// Submit audio samples (interleaved int16 stereo PCM at source rate).
2727+// Non-blocking: copies into ring buffer for the encoder thread.
2828+void recorder_submit_audio(ACRecorder *rec, const int16_t *pcm, int frames);
2929+3030+// Stop recording: flush encoder, finalize MP4, join thread.
3131+// Blocks until the file is fully written.
3232+void recorder_stop(ACRecorder *rec);
3333+3434+// Returns 1 if currently recording, 0 otherwise.
3535+int recorder_is_recording(ACRecorder *rec);
3636+3737+// Returns recording duration in seconds (0 if not recording).
3838+double recorder_elapsed(ACRecorder *rec);
3939+4040+// Destroy recorder and free all resources.
4141+void recorder_destroy(ACRecorder *rec);
4242+4343+#else /* !HAVE_AVCODEC */
4444+4545+// Stub API when ffmpeg is not available
4646+typedef struct ACRecorder ACRecorder;
4747+static inline ACRecorder *recorder_create(int w, int h, int fps, unsigned int ar) { (void)w; (void)h; (void)fps; (void)ar; return (void*)0; }
4848+static inline int recorder_start(ACRecorder *r, const char *p) { (void)r; (void)p; return -1; }
4949+static inline void recorder_submit_video(ACRecorder *r, const uint32_t *px, int s) { (void)r; (void)px; (void)s; }
5050+static inline void recorder_submit_audio(ACRecorder *r, const int16_t *pcm, int f) { (void)r; (void)pcm; (void)f; }
5151+static inline void recorder_stop(ACRecorder *r) { (void)r; }
5252+static inline int recorder_is_recording(ACRecorder *r) { (void)r; return 0; }
5353+static inline double recorder_elapsed(ACRecorder *r) { (void)r; return 0.0; }
5454+static inline void recorder_destroy(ACRecorder *r) { (void)r; }
5555+5656+#endif /* HAVE_AVCODEC */
5757+5858+#endif /* AC_RECORDER_H */
+1
papers/SCORE.md
···40404141| Paper | Format | PDF | Source |
4242|-------|--------|-----|--------|
4343+| Five Years from Now: What Aesthetic Computer Probably Becomes | arXiv (LaTeX) | `arxiv-futures/futures.pdf` | `arxiv-futures/futures.tex` |
4344| Reading the Score: A Critical Analysis of SCORE.md | arXiv (LaTeX) | `arxiv-score-analysis/score-analysis.pdf` | `arxiv-score-analysis/score-analysis.tex` |
4445| KidLisp Cards: Programs That Fit on a Card | arXiv (LaTeX) | `arxiv-kidlisp-cards/kidlisp-cards.pdf` | `arxiv-kidlisp-cards/kidlisp-cards.tex` |
4546| PLORKing the Planet: From Laptop Orchestra to Planetary Organ | arXiv (LaTeX, 8pp) | `arxiv-plork/plork.pdf` | `arxiv-plork/plork.tex` |