zebra-report/src/chat.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

200 lines
6.6 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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;
}