zebra-report/src/carrier.c
Russell Ballestrini b77da42bbe phase 1: unfirehose reconstruction from session JSONL ingest
Source: ~/.unfirehose/unfirehose.db (project_id=81, 4 sessions covering
2026-03-29 through 2026-04-05). Reconstructed via chronological replay
of Write/Edit tool_input on file_paths under /home/fox/zebra-report/.

stats:
  files reconstructed:    20
  writes baselined:       all (zero missing)
  edits applied:          68
  edits unapplied:        8 (1 SKIP pre-baseline, 6 FAIL old_string drift, 1 AMBIGUOUS)

unapplied edits represent small drift in 6 files; baseline content for
each is intact. quality verification deferred to phase 2.

recovered tree:
  CLAUDE.md, Makefile
  src/{tx,rx,pulse,carrier,chat,bt}.c
  include/{modem,zebra}.h
  test/{functional,integration,unit}.c, test/test.h
  web/{index,kernel}.html, web/blog/style.css
  blog/build.py, blog/posts/{001-volume-modem,002-sse-chatroom}.md

report: /tmp/zebra_recover_report.txt
script: /tmp/zebra_recover.py
2026-05-27 13:51:14 -04:00

188 lines
5.8 KiB
C

/*
* carrier — create a named stereo PA sink input and hold it open
*
* Feeds silence into PulseAudio so the stream appears in pavucontrol
* and `./tx -l` as a controllable sink input — no browser required.
*
* Usage:
* ./carrier [-n NAME] [-c CHANNELS]
*
* -n NAME stream name shown in pavucontrol (default: zebra)
* -c CHANNELS 1 or 2 (default: 2, stereo required for Battle Toads)
*
* Run in background:
* ./carrier -n zebra-data &
* ./carrier -n zebra-ctrl &
* ./tx -l # find indices
* ./rx -s <data> -t <ctrl>
* ./tx -s <data> -r <ctrl>
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <pulse/pulseaudio.h>
static volatile int running = 1;
static void on_signal(int s) { (void)s; running = 0; }
/* ------------------------------------------------------------------ *
* PA callbacks *
* ------------------------------------------------------------------ */
static void stream_write_cb(pa_stream *s, size_t nbytes, void *ud) {
(void)ud;
/* feed silence — all zeros at whatever sample width */
void *buf = NULL;
size_t len = nbytes;
if (pa_stream_begin_write(s, &buf, &len) < 0 || !buf) return;
memset(buf, 0, len);
pa_stream_write(s, buf, len, NULL, 0, PA_SEEK_RELATIVE);
}
static void ctx_state_cb(pa_context *ctx, void *ud) {
pa_threaded_mainloop *ml = ud;
(void)ctx;
pa_threaded_mainloop_signal(ml, 0);
}
static void stream_state_cb(pa_stream *s, void *ud) {
pa_threaded_mainloop *ml = ud;
(void)s;
pa_threaded_mainloop_signal(ml, 0);
}
/* ------------------------------------------------------------------ *
* main *
* ------------------------------------------------------------------ */
int main(int argc, char **argv) {
const char *name = "zebra";
int channels = 2;
int opt;
while ((opt = getopt(argc, argv, "n:c:h")) != -1) {
switch (opt) {
case 'n': name = optarg; break;
case 'c': channels = atoi(optarg); break;
case 'h':
fprintf(stderr,
"usage: %s [-n NAME] [-c CHANNELS]\n"
" -n NAME stream name (default: zebra)\n"
" -c CHANNELS 1 or 2 (default: 2)\n",
argv[0]);
return 0;
default:
return 1;
}
}
if (channels < 1 || channels > 2) {
fprintf(stderr, "error: channels must be 1 or 2\n");
return 1;
}
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
pa_threaded_mainloop *ml = pa_threaded_mainloop_new();
if (!ml) { fprintf(stderr, "error: pa_threaded_mainloop_new\n"); return 1; }
pa_mainloop_api *api = pa_threaded_mainloop_get_api(ml);
pa_context *ctx = pa_context_new(api, name);
if (!ctx) { fprintf(stderr, "error: pa_context_new\n"); return 1; }
pa_context_set_state_callback(ctx, ctx_state_cb, ml);
pa_threaded_mainloop_lock(ml);
pa_threaded_mainloop_start(ml);
pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL);
/* wait for context ready */
for (;;) {
pa_context_state_t st = pa_context_get_state(ctx);
if (st == PA_CONTEXT_READY) break;
if (!PA_CONTEXT_IS_GOOD(st)) {
fprintf(stderr, "error: PA context failed\n");
pa_threaded_mainloop_unlock(ml);
return 1;
}
pa_threaded_mainloop_wait(ml);
}
/* create stereo 44100 Hz stream */
pa_sample_spec ss = {
.format = PA_SAMPLE_S16LE,
.rate = 44100,
.channels = (uint8_t)channels
};
pa_channel_map cm;
pa_channel_map_init_stereo(&cm);
if (channels == 1) pa_channel_map_init_mono(&cm);
pa_stream *stream = pa_stream_new(ctx, name, &ss, &cm);
if (!stream) {
fprintf(stderr, "error: pa_stream_new\n");
pa_threaded_mainloop_unlock(ml);
return 1;
}
pa_stream_set_state_callback(stream, stream_state_cb, ml);
pa_stream_set_write_callback(stream, stream_write_cb, NULL);
pa_buffer_attr ba = {
.maxlength = (uint32_t)-1,
.tlength = pa_usec_to_bytes(100000, &ss), /* 100ms latency */
.prebuf = (uint32_t)-1,
.minreq = (uint32_t)-1,
.fragsize = (uint32_t)-1
};
pa_stream_flags_t flags = PA_STREAM_INTERPOLATE_TIMING
| PA_STREAM_AUTO_TIMING_UPDATE
| PA_STREAM_ADJUST_LATENCY;
if (pa_stream_connect_playback(stream, NULL, &ba, flags, NULL, NULL) < 0) {
fprintf(stderr, "error: pa_stream_connect_playback\n");
pa_threaded_mainloop_unlock(ml);
return 1;
}
/* wait for stream ready */
for (;;) {
pa_stream_state_t st = pa_stream_get_state(stream);
if (st == PA_STREAM_READY) break;
if (!PA_STREAM_IS_GOOD(st)) {
fprintf(stderr, "error: stream failed\n");
pa_threaded_mainloop_unlock(ml);
return 1;
}
pa_threaded_mainloop_wait(ml);
}
uint32_t idx = pa_stream_get_index(stream);
pa_threaded_mainloop_unlock(ml);
fprintf(stderr, "carrier: [%u] %s ch=%d rate=44100 (ctrl-c to stop)\n",
idx, name, channels);
/* hold open until signal */
while (running) {
struct timespec t = {0, 100000000L}; /* 100ms */
nanosleep(&t, NULL);
}
pa_threaded_mainloop_lock(ml);
pa_stream_disconnect(stream);
pa_stream_unref(stream);
pa_threaded_mainloop_unlock(ml);
pa_threaded_mainloop_stop(ml);
pa_context_disconnect(ctx);
pa_context_unref(ctx);
pa_threaded_mainloop_free(ml);
fprintf(stderr, "carrier: stopped\n");
return 0;
}