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
124 lines
5.5 KiB
C
124 lines
5.5 KiB
C
#pragma once
|
||
|
||
/*
|
||
* zebra -- volume-modulated modem over PulseAudio sink inputs
|
||
*
|
||
* Firefox exposes each tab as a named sink input in the PulseAudio mixer.
|
||
* A <video> element keeps audio flowing so the tab stays visible in
|
||
* pavucontrol. The C clients modulate/read that sink's volume as the
|
||
* signal carrier.
|
||
*
|
||
* Wire format (UART):
|
||
* idle = MARK (high volume, ZEBRA_VOL_MARK %)
|
||
* start = SPACE (low volume, ZEBRA_VOL_SPACE %) -- falling edge triggers rx
|
||
* data = 8 bits LSB-first, 0=SPACE 1=MARK
|
||
* stop = MARK
|
||
* frame = 10 symbols per byte
|
||
*
|
||
* 101 discrete levels (0–100%) available — "101 dalmatians".
|
||
* Binary encoding uses two far-apart levels for maximum noise margin.
|
||
*/
|
||
|
||
#include <stdint.h>
|
||
#include <pulse/pulseaudio.h>
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* signal constants *
|
||
* ------------------------------------------------------------------ */
|
||
#define ZEBRA_VOL_MARK 80 /* % — logic-1 / idle / stop bit */
|
||
#define ZEBRA_VOL_SPACE 20 /* % — logic-0 / start bit */
|
||
#define ZEBRA_VOL_THRESHOLD 50 /* % — bit decision boundary */
|
||
#define ZEBRA_DALMATIANS 101 /* discrete volume steps: 0–100 */
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* baud rate limits *
|
||
* ------------------------------------------------------------------ */
|
||
#define ZEBRA_BAUD_DEFAULT 10
|
||
#define ZEBRA_BAUD_MIN 1
|
||
#define ZEBRA_BAUD_MAX 100000
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* handshake / auto-negotiation *
|
||
* ------------------------------------------------------------------ */
|
||
#define ZEBRA_BAUD_HANDSHAKE 50 /* fixed baud for negotiation phase */
|
||
#define ZEBRA_HS_MAGIC_0 0x5A /* 'Z' */
|
||
#define ZEBRA_HS_MAGIC_1 0x42 /* 'B' */
|
||
#define ZEBRA_HS_TYPE_OFFER 0x01 /* RX→TX: here is my max baud */
|
||
#define ZEBRA_HS_FRAME_LEN 6 /* magic(2) type(1) baud_le(2) xor(1)*/
|
||
#define ZEBRA_HS_SETTLE_MS 500 /* quiet gap after handshake */
|
||
#define ZEBRA_HS_TYPE_READY 0x02 /* RX→TX: receive loop is active */
|
||
#define ZEBRA_HS_READY_WAIT 5000 /* ms TX waits for READY after OFFER */
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* types *
|
||
* ------------------------------------------------------------------ */
|
||
|
||
/* one PulseAudio sink input entry */
|
||
typedef struct {
|
||
uint32_t index;
|
||
char name[256];
|
||
char app_name[128];
|
||
uint8_t volume_pct; /* 0–100 */
|
||
uint8_t channels;
|
||
} zebra_sink_t;
|
||
|
||
/* PulseAudio connection state */
|
||
typedef struct {
|
||
pa_threaded_mainloop *loop;
|
||
pa_context *ctx;
|
||
} zebra_pulse_t;
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* pulse.c: PulseAudio interface *
|
||
* ------------------------------------------------------------------ */
|
||
|
||
/* connect to the default PulseAudio server; returns 0 on success */
|
||
int zebra_pulse_connect(zebra_pulse_t *z, const char *app_name);
|
||
void zebra_pulse_disconnect(zebra_pulse_t *z);
|
||
|
||
/* enumerate all sink inputs; returns count or -1 */
|
||
int zebra_list_sinks(zebra_pulse_t *z, zebra_sink_t *buf, int max);
|
||
|
||
/* find first sink input whose name or app_name contains match */
|
||
int zebra_find_sink(zebra_pulse_t *z, const char *match, zebra_sink_t *out);
|
||
|
||
/* get/set volume percent (0–100); returns 0 on success */
|
||
int zebra_get_volume(zebra_pulse_t *z, uint32_t sink_index, uint8_t *pct);
|
||
int zebra_set_volume(zebra_pulse_t *z, uint32_t sink_index, uint8_t pct);
|
||
|
||
/* fast TX path: caller supplies channel count, waits for PA confirmation */
|
||
int zebra_set_volume_fast(zebra_pulse_t *z, uint32_t sink_index,
|
||
uint8_t channels, uint8_t pct);
|
||
|
||
/* fire-and-forget TX: enqueues volume command, returns immediately.
|
||
* PA processes async — symbol timing governed entirely by clock_nanosleep. */
|
||
int zebra_set_volume_noack(zebra_pulse_t *z, uint32_t sink_index,
|
||
uint8_t channels, uint8_t pct);
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* Battle Toads: per-channel stereo API *
|
||
* ------------------------------------------------------------------ */
|
||
|
||
/* Read L and R channel volumes independently (single PA IPC call).
|
||
* Falls back: if sink is mono, both *left and *right get the same value. */
|
||
int zebra_get_volume_lr(zebra_pulse_t *z, uint32_t sink_index,
|
||
uint8_t *left, uint8_t *right);
|
||
|
||
/* Fire-and-forget: set L and R to different volumes in one PA call. */
|
||
int zebra_set_volume_lr_noack(zebra_pulse_t *z, uint32_t sink_index,
|
||
uint8_t left, uint8_t right);
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* inline modem helpers *
|
||
* ------------------------------------------------------------------ */
|
||
|
||
static inline uint8_t zebra_bit_to_vol(int bit) {
|
||
return (uint8_t)(bit ? ZEBRA_VOL_MARK : ZEBRA_VOL_SPACE);
|
||
}
|
||
|
||
/* returns 0, 1, or -1 (exactly on threshold — ambiguous) */
|
||
static inline int zebra_vol_to_bit(uint8_t vol) {
|
||
if (vol > ZEBRA_VOL_THRESHOLD) return 1;
|
||
if (vol < ZEBRA_VOL_THRESHOLD) return 0;
|
||
return -1;
|
||
}
|