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

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