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
200 lines
6.6 KiB
C
200 lines
6.6 KiB
C
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <pthread.h>
|
||
#include <time.h>
|
||
#include "zebra.h"
|
||
#include "modem.h"
|
||
|
||
/*
|
||
* chat -- bidirectional text over two PulseAudio sink inputs
|
||
*
|
||
* Two separate PA connections (one per direction) avoid mainloop
|
||
* contention between threads.
|
||
*
|
||
* TX side: sets its own browser tab's volume → other side reads it
|
||
* RX side: polls the other browser tab's volume → decodes bytes
|
||
*
|
||
* Example (same machine, two terminals):
|
||
* terminal 1: ./chat -T Firefox -R Chromium -b 10
|
||
* terminal 2: ./chat -T Chromium -R Firefox -b 10
|
||
*/
|
||
|
||
#define CHAT_LINE_MAX 512
|
||
|
||
static pthread_mutex_t print_mu = PTHREAD_MUTEX_INITIALIZER;
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* RX thread *
|
||
* ------------------------------------------------------------------ */
|
||
|
||
typedef struct {
|
||
zebra_pulse_t *z;
|
||
uint32_t sink;
|
||
int baud;
|
||
} rx_arg_t;
|
||
|
||
/* line buffer for incoming bytes */
|
||
static char rx_buf[CHAT_LINE_MAX];
|
||
static int rx_pos = 0;
|
||
|
||
static void on_rx_byte(uint8_t byte, void *ud) {
|
||
(void)ud;
|
||
rx_buf[rx_pos++] = (char)byte;
|
||
if (byte == '\n' || rx_pos >= (int)sizeof(rx_buf) - 1) {
|
||
rx_buf[rx_pos] = '\0';
|
||
pthread_mutex_lock(&print_mu);
|
||
/* \r moves to column 0 so received text overwrites any partial input line */
|
||
printf("\r<<< %s", rx_buf);
|
||
if (rx_buf[rx_pos - 1] != '\n') putchar('\n');
|
||
fflush(stdout);
|
||
pthread_mutex_unlock(&print_mu);
|
||
rx_pos = 0;
|
||
}
|
||
}
|
||
|
||
static void *rx_thread(void *arg) {
|
||
rx_arg_t *a = arg;
|
||
zebra_rx_run(a->z, a->sink, a->baud, on_rx_byte, NULL);
|
||
return NULL;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* helpers *
|
||
* ------------------------------------------------------------------ */
|
||
|
||
static void usage(const char *prog) {
|
||
fprintf(stderr,
|
||
"usage: %s -T TX_SINK -R RX_SINK [-b BAUD]\n"
|
||
"\n"
|
||
" -T NAME|INDEX sink to modulate (your outbound — your browser tab)\n"
|
||
" -R NAME|INDEX sink to monitor (their outbound — their browser tab)\n"
|
||
" -b BAUD symbols/sec, both sides must match (default %d)\n"
|
||
"\n"
|
||
"same-machine setup (Firefox ↔ Chromium):\n"
|
||
" open web/index.html in both browsers, click 'start audio' in each\n"
|
||
" terminal 1: ./chat -T Firefox -R Chromium -b 10\n"
|
||
" terminal 2: ./chat -T Chromium -R Firefox -b 10\n"
|
||
" find sink indices first with: ./tx -l\n",
|
||
prog, ZEBRA_BAUD_DEFAULT);
|
||
}
|
||
|
||
/* resolve NAME-or-INDEX to a PA sink index */
|
||
static uint32_t resolve(zebra_pulse_t *z, const char *arg) {
|
||
char *end;
|
||
long idx = strtol(arg, &end, 10);
|
||
if (*end == '\0' && idx >= 0) return (uint32_t)idx;
|
||
|
||
zebra_sink_t s;
|
||
if (zebra_find_sink(z, arg, &s) < 0) {
|
||
fprintf(stderr, "error: no sink matching '%s' — run ./tx -l to list\n", arg);
|
||
exit(1);
|
||
}
|
||
fprintf(stderr, " found: [%u] %s (%s)\n", s.index, s.name, s.app_name);
|
||
return s.index;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* main *
|
||
* ------------------------------------------------------------------ */
|
||
|
||
int main(int argc, char **argv) {
|
||
int baud = ZEBRA_BAUD_DEFAULT;
|
||
char *tx_arg = NULL;
|
||
char *rx_arg = NULL;
|
||
int opt;
|
||
|
||
while ((opt = getopt(argc, argv, "T:R:b:h")) != -1) {
|
||
switch (opt) {
|
||
case 'T': tx_arg = optarg; break;
|
||
case 'R': rx_arg = optarg; break;
|
||
case 'b': baud = atoi(optarg); break;
|
||
case 'h': usage(argv[0]); return 0;
|
||
default: usage(argv[0]); return 1;
|
||
}
|
||
}
|
||
|
||
if (!tx_arg || !rx_arg) {
|
||
fprintf(stderr, "error: -T and -R are required\n\n");
|
||
usage(argv[0]);
|
||
return 1;
|
||
}
|
||
if (baud < ZEBRA_BAUD_MIN || baud > ZEBRA_BAUD_MAX) {
|
||
fprintf(stderr, "error: baud must be %d–%d\n", ZEBRA_BAUD_MIN, ZEBRA_BAUD_MAX);
|
||
return 1;
|
||
}
|
||
|
||
/* two PA connections: avoid mainloop contention between TX/RX threads */
|
||
zebra_pulse_t ztx, zrx;
|
||
if (zebra_pulse_connect(&ztx, "zebra-chat-tx") < 0) {
|
||
fprintf(stderr, "error: TX PulseAudio connect failed\n");
|
||
return 1;
|
||
}
|
||
if (zebra_pulse_connect(&zrx, "zebra-chat-rx") < 0) {
|
||
fprintf(stderr, "error: RX PulseAudio connect failed\n");
|
||
zebra_pulse_disconnect(&ztx);
|
||
return 1;
|
||
}
|
||
|
||
fprintf(stderr, "resolving TX sink: %s\n", tx_arg);
|
||
uint32_t tx_sink = resolve(&ztx, tx_arg);
|
||
fprintf(stderr, "resolving RX sink: %s\n", rx_arg);
|
||
uint32_t rx_sink = resolve(&zrx, rx_arg);
|
||
|
||
/* cache TX channel count — avoids PA round-trip per symbol */
|
||
uint8_t tx_channels = 2;
|
||
{
|
||
zebra_sink_t buf[64];
|
||
int n = zebra_list_sinks(&ztx, buf, 64);
|
||
for (int i = 0; i < n; i++)
|
||
if (buf[i].index == tx_sink)
|
||
{ tx_channels = buf[i].channels ? buf[i].channels : 2; break; }
|
||
}
|
||
|
||
fprintf(stderr, "chat ready: tx=sink[%u] ch=%u rx=sink[%u] baud=%d\n",
|
||
tx_sink, tx_channels, rx_sink, baud);
|
||
fprintf(stderr, "type and press enter to send (ctrl-d to quit)\n");
|
||
fprintf(stderr, "---\n");
|
||
|
||
/* assert TX idle */
|
||
zebra_set_volume_fast(&ztx, tx_sink, tx_channels, ZEBRA_VOL_MARK);
|
||
|
||
/* start RX thread */
|
||
rx_arg_t rxa = { .z = &zrx, .sink = rx_sink, .baud = baud };
|
||
pthread_t tid;
|
||
if (pthread_create(&tid, NULL, rx_thread, &rxa) != 0) {
|
||
fprintf(stderr, "error: pthread_create failed\n");
|
||
zebra_pulse_disconnect(&ztx);
|
||
zebra_pulse_disconnect(&zrx);
|
||
return 1;
|
||
}
|
||
pthread_detach(tid);
|
||
|
||
/* TX loop: read stdin lines, transmit byte by byte */
|
||
long period_ns = 1000000000L / baud;
|
||
char line[CHAT_LINE_MAX];
|
||
|
||
while (fgets(line, sizeof(line), stdin)) {
|
||
int len = (int)strlen(line);
|
||
if (len == 0) continue;
|
||
|
||
pthread_mutex_lock(&print_mu);
|
||
printf(">>> %s", line);
|
||
if (line[len - 1] != '\n') putchar('\n');
|
||
fflush(stdout);
|
||
pthread_mutex_unlock(&print_mu);
|
||
|
||
struct timespec next;
|
||
clock_gettime(CLOCK_MONOTONIC, &next);
|
||
ts_add_ns(&next, period_ns);
|
||
|
||
for (int i = 0; i < len; i++)
|
||
zebra_send_byte(&ztx, tx_sink, tx_channels, (uint8_t)line[i], &next, period_ns);
|
||
}
|
||
|
||
zebra_set_volume_fast(&ztx, tx_sink, tx_channels, ZEBRA_VOL_MARK);
|
||
zebra_pulse_disconnect(&ztx);
|
||
zebra_pulse_disconnect(&zrx);
|
||
return 0;
|
||
}
|