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
304 lines
10 KiB
C
304 lines
10 KiB
C
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "zebra.h"
|
|
|
|
/* ------------------------------------------------------------------ *
|
|
* internal callbacks *
|
|
* ------------------------------------------------------------------ */
|
|
|
|
static void _ctx_state_cb(pa_context *ctx, void *ud) {
|
|
(void)ctx;
|
|
pa_threaded_mainloop_signal((pa_threaded_mainloop *)ud, 0);
|
|
}
|
|
|
|
static void _success_cb(pa_context *ctx, int success, void *ud) {
|
|
(void)ctx;
|
|
(void)success;
|
|
pa_threaded_mainloop_signal((pa_threaded_mainloop *)ud, 0);
|
|
}
|
|
|
|
/* result type for single-sink-input queries */
|
|
typedef struct {
|
|
pa_threaded_mainloop *ml;
|
|
uint8_t channels;
|
|
uint8_t volume_pct;
|
|
uint8_t vol_left;
|
|
uint8_t vol_right;
|
|
int found;
|
|
} _info_t;
|
|
|
|
static uint8_t _pa_vol_to_pct(pa_volume_t v) {
|
|
uint32_t pct = (uint32_t)(v * 100 / PA_VOLUME_NORM);
|
|
return (uint8_t)(pct > 100 ? 100 : pct);
|
|
}
|
|
|
|
static void _single_info_cb(pa_context *ctx,
|
|
const pa_sink_input_info *info,
|
|
int eol, void *ud) {
|
|
(void)ctx;
|
|
_info_t *r = ud;
|
|
if (!eol && info) {
|
|
r->channels = info->volume.channels;
|
|
pa_volume_t avg = pa_cvolume_avg(&info->volume);
|
|
r->volume_pct = _pa_vol_to_pct(avg);
|
|
r->vol_left = _pa_vol_to_pct(info->volume.values[0]);
|
|
r->vol_right = (info->volume.channels >= 2)
|
|
? _pa_vol_to_pct(info->volume.values[1])
|
|
: r->vol_left;
|
|
r->found = 1;
|
|
} else {
|
|
pa_threaded_mainloop_signal(r->ml, 0);
|
|
}
|
|
}
|
|
|
|
/* result type for list queries */
|
|
typedef struct {
|
|
zebra_sink_t *buf;
|
|
int max;
|
|
int count;
|
|
pa_threaded_mainloop *ml;
|
|
} _list_t;
|
|
|
|
static void _list_info_cb(pa_context *ctx,
|
|
const pa_sink_input_info *info,
|
|
int eol, void *ud) {
|
|
(void)ctx;
|
|
_list_t *r = ud;
|
|
if (!eol && info) {
|
|
if (r->count < r->max) {
|
|
zebra_sink_t *s = &r->buf[r->count++];
|
|
s->index = info->index;
|
|
s->channels = info->volume.channels;
|
|
snprintf(s->name, sizeof(s->name), "%s",
|
|
info->name ? info->name : "");
|
|
const char *app = pa_proplist_gets(info->proplist,
|
|
PA_PROP_APPLICATION_NAME);
|
|
snprintf(s->app_name, sizeof(s->app_name), "%s",
|
|
app ? app : "");
|
|
pa_volume_t avg = pa_cvolume_avg(&info->volume);
|
|
uint32_t pct = (uint32_t)(avg * 100 / PA_VOLUME_NORM);
|
|
s->volume_pct = (uint8_t)(pct > 100 ? 100 : pct);
|
|
}
|
|
} else {
|
|
pa_threaded_mainloop_signal(r->ml, 0);
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ *
|
|
* public API *
|
|
* ------------------------------------------------------------------ */
|
|
|
|
int zebra_pulse_connect(zebra_pulse_t *z, const char *app_name) {
|
|
memset(z, 0, sizeof(*z));
|
|
|
|
z->loop = pa_threaded_mainloop_new();
|
|
if (!z->loop) return -1;
|
|
|
|
pa_mainloop_api *api = pa_threaded_mainloop_get_api(z->loop);
|
|
z->ctx = pa_context_new(api, app_name ? app_name : "zebra");
|
|
if (!z->ctx) {
|
|
pa_threaded_mainloop_free(z->loop);
|
|
return -1;
|
|
}
|
|
|
|
pa_context_set_state_callback(z->ctx, _ctx_state_cb, z->loop);
|
|
|
|
pa_threaded_mainloop_lock(z->loop);
|
|
|
|
if (pa_threaded_mainloop_start(z->loop) < 0) {
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
pa_context_unref(z->ctx);
|
|
pa_threaded_mainloop_free(z->loop);
|
|
return -1;
|
|
}
|
|
|
|
pa_context_connect(z->ctx, NULL, PA_CONTEXT_NOFLAGS, NULL);
|
|
|
|
for (;;) {
|
|
pa_context_state_t st = pa_context_get_state(z->ctx);
|
|
if (st == PA_CONTEXT_READY) break;
|
|
if (!PA_CONTEXT_IS_GOOD(st)) {
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
return -1;
|
|
}
|
|
pa_threaded_mainloop_wait(z->loop);
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
return 0;
|
|
}
|
|
|
|
void zebra_pulse_disconnect(zebra_pulse_t *z) {
|
|
if (!z->loop) return;
|
|
pa_threaded_mainloop_stop(z->loop);
|
|
if (z->ctx) {
|
|
pa_context_disconnect(z->ctx);
|
|
pa_context_unref(z->ctx);
|
|
}
|
|
pa_threaded_mainloop_free(z->loop);
|
|
memset(z, 0, sizeof(*z));
|
|
}
|
|
|
|
int zebra_list_sinks(zebra_pulse_t *z, zebra_sink_t *buf, int max) {
|
|
_list_t r = { .buf = buf, .max = max, .count = 0, .ml = z->loop };
|
|
|
|
pa_threaded_mainloop_lock(z->loop);
|
|
pa_operation *op = pa_context_get_sink_input_info_list(z->ctx,
|
|
_list_info_cb, &r);
|
|
if (!op) {
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
return -1;
|
|
}
|
|
while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
|
|
pa_threaded_mainloop_wait(z->loop);
|
|
pa_operation_unref(op);
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
return r.count;
|
|
}
|
|
|
|
int zebra_find_sink(zebra_pulse_t *z, const char *match, zebra_sink_t *out) {
|
|
zebra_sink_t buf[64];
|
|
int n = zebra_list_sinks(z, buf, 64);
|
|
if (n < 0) return -1;
|
|
for (int i = 0; i < n; i++) {
|
|
if (strstr(buf[i].name, match) || strstr(buf[i].app_name, match)) {
|
|
*out = buf[i];
|
|
return 0;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int zebra_get_volume(zebra_pulse_t *z, uint32_t sink_index, uint8_t *pct) {
|
|
_info_t r = { .ml = z->loop, .found = 0 };
|
|
|
|
pa_threaded_mainloop_lock(z->loop);
|
|
pa_operation *op = pa_context_get_sink_input_info(z->ctx, sink_index,
|
|
_single_info_cb, &r);
|
|
if (!op) {
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
return -1;
|
|
}
|
|
while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
|
|
pa_threaded_mainloop_wait(z->loop);
|
|
pa_operation_unref(op);
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
|
|
if (!r.found) return -1;
|
|
*pct = r.volume_pct;
|
|
return 0;
|
|
}
|
|
|
|
int zebra_set_volume(zebra_pulse_t *z, uint32_t sink_index, uint8_t pct) {
|
|
if (pct > 100) pct = 100;
|
|
|
|
_info_t r = { .ml = z->loop, .channels = 2, .found = 0 };
|
|
|
|
pa_threaded_mainloop_lock(z->loop);
|
|
|
|
/* query channel count before setting — needed to build pa_cvolume */
|
|
pa_operation *op = pa_context_get_sink_input_info(z->ctx, sink_index,
|
|
_single_info_cb, &r);
|
|
if (op) {
|
|
while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
|
|
pa_threaded_mainloop_wait(z->loop);
|
|
pa_operation_unref(op);
|
|
}
|
|
|
|
pa_cvolume cv;
|
|
pa_volume_t vol = (pa_volume_t)((uint64_t)PA_VOLUME_NORM * pct / 100);
|
|
pa_cvolume_set(&cv, r.channels ? r.channels : 2, vol);
|
|
|
|
op = pa_context_set_sink_input_volume(z->ctx, sink_index, &cv,
|
|
_success_cb, z->loop);
|
|
if (!op) {
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
return -1;
|
|
}
|
|
while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
|
|
pa_threaded_mainloop_wait(z->loop);
|
|
pa_operation_unref(op);
|
|
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
return 0;
|
|
}
|
|
|
|
int zebra_set_volume_fast(zebra_pulse_t *z, uint32_t sink_index,
|
|
uint8_t channels, uint8_t pct) {
|
|
if (pct > 100) pct = 100;
|
|
|
|
pa_cvolume cv;
|
|
pa_volume_t vol = (pa_volume_t)((uint64_t)PA_VOLUME_NORM * pct / 100);
|
|
pa_cvolume_set(&cv, channels ? channels : 2, vol);
|
|
|
|
pa_threaded_mainloop_lock(z->loop);
|
|
pa_operation *op = pa_context_set_sink_input_volume(z->ctx, sink_index, &cv,
|
|
_success_cb, z->loop);
|
|
if (!op) {
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
return -1;
|
|
}
|
|
while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
|
|
pa_threaded_mainloop_wait(z->loop);
|
|
pa_operation_unref(op);
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
return 0;
|
|
}
|
|
|
|
int zebra_set_volume_noack(zebra_pulse_t *z, uint32_t sink_index,
|
|
uint8_t channels, uint8_t pct) {
|
|
if (pct > 100) pct = 100;
|
|
|
|
pa_cvolume cv;
|
|
pa_volume_t vol = (pa_volume_t)((uint64_t)PA_VOLUME_NORM * pct / 100);
|
|
pa_cvolume_set(&cv, channels ? channels : 2, vol);
|
|
|
|
pa_threaded_mainloop_lock(z->loop);
|
|
pa_operation *op = pa_context_set_sink_input_volume(z->ctx, sink_index, &cv,
|
|
NULL, NULL);
|
|
if (op) pa_operation_unref(op);
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ *
|
|
* Battle Toads: per-channel stereo API *
|
|
* ------------------------------------------------------------------ */
|
|
|
|
int zebra_get_volume_lr(zebra_pulse_t *z, uint32_t sink_index,
|
|
uint8_t *left, uint8_t *right) {
|
|
_info_t r = { .ml = z->loop, .found = 0 };
|
|
|
|
pa_threaded_mainloop_lock(z->loop);
|
|
pa_operation *op = pa_context_get_sink_input_info(z->ctx, sink_index,
|
|
_single_info_cb, &r);
|
|
if (!op) { pa_threaded_mainloop_unlock(z->loop); return -1; }
|
|
while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
|
|
pa_threaded_mainloop_wait(z->loop);
|
|
pa_operation_unref(op);
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
|
|
if (!r.found) return -1;
|
|
*left = r.vol_left;
|
|
*right = r.vol_right;
|
|
return 0;
|
|
}
|
|
|
|
int zebra_set_volume_lr_noack(zebra_pulse_t *z, uint32_t sink_index,
|
|
uint8_t left, uint8_t right) {
|
|
if (left > 100) left = 100;
|
|
if (right > 100) right = 100;
|
|
|
|
pa_cvolume cv;
|
|
cv.channels = 2;
|
|
cv.values[0] = (pa_volume_t)((uint64_t)PA_VOLUME_NORM * left / 100);
|
|
cv.values[1] = (pa_volume_t)((uint64_t)PA_VOLUME_NORM * right / 100);
|
|
|
|
pa_threaded_mainloop_lock(z->loop);
|
|
pa_operation *op = pa_context_set_sink_input_volume(z->ctx, sink_index, &cv,
|
|
NULL, NULL);
|
|
if (op) pa_operation_unref(op);
|
|
pa_threaded_mainloop_unlock(z->loop);
|
|
return 0;
|
|
}
|