zebra-report/include/zebra.h
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

124 lines
5.5 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.

#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 (0100%) 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: 0100 */
/* ------------------------------------------------------------------ *
* 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; /* 0100 */
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 (0100); 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;
}