ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
lib/audio/audio.c
Go to the documentation of this file.
1
13#include <ascii-chat/common.h>
14#include <ascii-chat/log/io.h>
16#include <ascii-chat/util/time.h> // For monotonic timing
17#include <ascii-chat/util/lifecycle.h> // For lifecycle_t
18#include <ascii-chat/asciichat_errno.h> // For asciichat_errno system
21#include <ascii-chat/platform/init.h> // For static_mutex_t
22#include <ascii-chat/platform/abstraction.h> // For platform_sleep_us
23#include <ascii-chat/network/packet/packet.h> // For audio_batch_packet_t
24#include <ascii-chat/log/log.h> // For log_* macros
26#include <ascii-chat/media/source.h> // For media_source_read_audio()
27#include <stdlib.h>
28#include <string.h>
29#include <math.h>
30#include <ascii-chat/atomic.h>
31
32#ifdef _WIN32
33#include <malloc.h> // For _alloca on Windows
34#define alloca _alloca
35#else
36#include <unistd.h> // For dup, dup2, close, STDERR_FILENO
37#include <fcntl.h> // For O_WRONLY
38#endif
39
40// PortAudio initialization lifecycle (one-time init/shutdown, no refcounting)
41// Uses lifecycle_t for thread-safe, lock-free initialization tracking
42static lifecycle_t g_pa_lc = LIFECYCLE_INIT;
43
57static asciichat_error_t audio_ensure_portaudio_initialized(void) {
58 // Check if already initialized
59 if (lifecycle_is_initialized(&g_pa_lc)) {
60 return ASCIICHAT_OK;
61 }
62
63 // Try to win the initialization race
64 if (!lifecycle_init(&g_pa_lc, "portaudio")) {
65 return ASCIICHAT_OK; // Already initialized by another thread
66 }
67
68 // First initialization - call Pa_Initialize() exactly once
69 // Suppress PortAudio backend probe errors (ALSA/JACK/OSS warnings)
70 // These are harmless - PortAudio tries multiple backends until one works
71 PaError err;
72 LOG_IO("portaudio", { err = Pa_Initialize(); });
73
74 if (err != paNoError) {
75 return SET_ERRNO(ERROR_AUDIO, "Failed to initialize PortAudio: %s", Pa_GetErrorText(err));
76 }
77
78 return ASCIICHAT_OK;
79}
80
88 if (lifecycle_shutdown(&g_pa_lc)) {
89 log_debug("[PORTAUDIO_TERM] Calling Pa_Terminate() to release PortAudio");
90
91 PaError err;
92 LOG_IO("portaudio", { err = Pa_Terminate(); });
93
94 log_debug("[PORTAUDIO_TERM] Pa_Terminate() returned: %s", Pa_GetErrorText(err));
95 }
96}
97
98// AEC processes complete 480-sample blocks; the worker buffers must hold at least one block.
99#define WORKER_BATCH_FRAMES 480
100#define WORKER_BATCH_SAMPLES (WORKER_BATCH_FRAMES * AUDIO_CHANNELS)
101#define WORKER_TIMEOUT_MS 1 // Wake up every 1ms to keep up with 48kHz playback (was 3ms)
102
103static void audio_publish_local_capture(audio_context_t *ctx, float *samples, size_t count, bool use_media) {
104 uint64_t now = time_get_ns();
105 uint64_t duration = count * 1000000000ULL / 48000;
106 uint64_t block_start = now > duration ? now - duration : 0;
107 audio_recording_submit(AUDIO_RECORDING_MIC, samples, count, block_start);
109 if (use_media) {
110 float media[WORKER_BATCH_SAMPLES];
111 size_t read = media_source_read_audio(ctx->capture_media_source, media, count);
112 audio_recording_submit(AUDIO_RECORDING_MEDIA, media, read, block_start);
114 for (size_t i = 0; i < read; i++)
115 samples[i] = fmaxf(-1.0f, fminf(1.0f, samples[i] + media[i]));
116 if (ctx->monitor_local_media)
117 audio_ring_buffer_write(ctx->playback_buffer, media, (int)read);
118 }
119 audio_ring_buffer_write(ctx->capture_buffer, samples, (int)count);
120}
121
147static void *audio_worker_thread(void *arg) {
148 audio_context_t *ctx = (audio_context_t *)arg;
149 log_debug("Audio worker thread started (batch size: %d frames = %d samples)", WORKER_BATCH_FRAMES,
151
152 // Check for AEC3 bypass (static, checked once)
153 static int bypass_aec3_worker = -1;
154 if (bypass_aec3_worker == -1) {
155 const char *env = platform_getenv("BYPASS_AEC3");
156 bypass_aec3_worker = (env && (strcmp(env, "1") == 0 || strcmp(env, "true") == 0)) ? 1 : 0;
157 if (bypass_aec3_worker) {
158 log_warn("Worker thread: AEC3 BYPASSED via BYPASS_AEC3=1 (worker will skip AEC3 processing)");
159 }
160 }
161
162 // Timing instrumentation for debugging
163 static uint64_t loop_count = 0;
164 static uint64_t timeout_count = 0;
165 static uint64_t signal_count = 0;
166 static uint64_t process_count = 0;
167
168 // Detailed timing stats
169 static double total_wait_ns = 0;
170 static double total_capture_ns = 0;
171 static double total_playback_ns = 0;
172 static double max_wait_ns = 0;
173 static double max_capture_ns = 0;
174 static double max_playback_ns = 0;
175
176 uint64_t media_start_ns = time_get_ns();
177 uint64_t media_samples = 0;
178 while (true) {
179 loop_count++;
180 uint64_t loop_start_ns = time_get_ns();
181
182 // For output-only mode, don't wait for signal - just write continuously
183 // For duplex/input modes, wait for signal from callback
184 bool is_output_only = ctx->output_stream && !ctx->input_stream && !ctx->duplex_stream;
185
186 int wait_result = 1; // Default: timeout (will trigger processing for output-only)
187 if (!is_output_only) {
188 // Wait for signal from callback or timeout
190 uint64_t wait_start_ns = time_get_ns();
192 double wait_time_ns = (double)time_elapsed_ns(wait_start_ns, time_get_ns());
194
195 total_wait_ns += wait_time_ns;
196 if (wait_time_ns > max_wait_ns)
197 max_wait_ns = wait_time_ns;
198 } else {
199 // Output-only mode: no wait needed, callback handles audio delivery
200 // Just sleep briefly to avoid busy-looping
201 Pa_Sleep(5);
202 wait_result = 1;
203 }
204
205 // Check shutdown flag
207 log_debug("Worker thread received shutdown signal");
208 break;
209 }
210
211 // Count wake-ups
212 if (wait_result == 0) {
213 signal_count++;
214 } else {
215 timeout_count++;
216 }
217
218 // Skip processing if we timed out and no data available
219 size_t capture_available = audio_ring_buffer_available_read(ctx->raw_capture_rb);
220 size_t render_available = audio_ring_buffer_available_read(ctx->raw_render_rb);
221 size_t playback_available = audio_ring_buffer_available_read(ctx->playback_buffer);
222
223 // Log worker loop state every 100 iterations with detailed timing
224 if (loop_count % 100 == 0) {
225 char avg_wait_str[32], max_wait_str[32];
226 char avg_capture_str[32], max_capture_str[32];
227 char avg_playback_str[32], max_playback_str[32];
228 time_pretty((uint64_t)(total_wait_ns / loop_count), -1, avg_wait_str, sizeof(avg_wait_str));
229 time_pretty(max_wait_ns, -1, max_wait_str, sizeof(max_wait_str));
230 time_pretty((uint64_t)(total_capture_ns / (process_count > 0 ? process_count : 1)), -1, avg_capture_str,
231 sizeof(avg_capture_str));
232 time_pretty(max_capture_ns, -1, max_capture_str, sizeof(max_capture_str));
233 time_pretty((uint64_t)(total_playback_ns / (process_count > 0 ? process_count : 1)), -1, avg_playback_str,
234 sizeof(avg_playback_str));
235 time_pretty(max_playback_ns, -1, max_playback_str, sizeof(max_playback_str));
236
237 log_info("Worker stats: loops=%lu, signals=%lu, timeouts=%lu, processed=%lu", loop_count, signal_count,
238 timeout_count, process_count);
239 log_info("Worker timing: wait avg=%s max=%s, capture avg=%s max=%s, playback avg=%s max=%s", avg_wait_str,
240 max_wait_str, avg_capture_str, max_capture_str, avg_playback_str, max_playback_str);
241 log_info("Worker buffers: capture=%zu, render=%zu, playback=%zu (need >= %d to process)", capture_available,
242 render_available, playback_available, WORKER_BATCH_SAMPLES);
243 }
244
245 if (wait_result != 0 && capture_available == 0 && playback_available == 0 && !ctx->capture_media_source) {
246 // Timeout with no data - continue waiting
247 continue;
248 }
249
250 process_count++;
251
253 audio_capture_source_t selection = GET_OPTION(audio_capture_source);
254 bool use_media = has_media && (selection == AUDIO_CAPTURE_SOURCE_AUTO ||
255 selection == AUDIO_CAPTURE_SOURCE_MEDIA ||
256 selection == AUDIO_CAPTURE_SOURCE_BOTH);
257 bool use_mic = audio_should_enable_microphone(selection, has_media);
258 if (use_media && !use_mic) {
259 uint64_t now = time_get_ns();
260 uint64_t due = (now - media_start_ns) / 1000000ULL * 48;
261 float media[960];
262 while (media_samples < due) {
263 size_t count = due - media_samples > 960 ? 960 : (size_t)(due - media_samples);
264 size_t read = media_source_read_audio(ctx->capture_media_source, media, count);
265 if (read < count)
266 memset(media + read, 0, (count - read) * sizeof(float));
268 media_start_ns + media_samples * 1000000000ULL / 48000);
270 audio_ring_buffer_write(ctx->capture_buffer, media, (int)count);
271 if (ctx->monitor_local_media)
272 audio_ring_buffer_write(ctx->playback_buffer, media, (int)count);
273 media_samples += count;
274 }
275 }
276 if (!use_mic && capture_available > 0) {
277 size_t drain = capture_available > WORKER_BATCH_SAMPLES ? WORKER_BATCH_SAMPLES : capture_available;
279 capture_available = 0;
280 }
281
282 // STEP 1: Process capture path (mic → AEC3 → encoder)
283 // Process capture samples if available (don't wait for full batch - reduces latency)
284 // Minimum: 64 samples (1.3ms @ 48kHz) to avoid excessive overhead
285 // AEC3 requires 480-sample frames (10ms @ 48kHz). Process only complete
286 // frames where both render and capture are available. Remainders stay in
287 // the ring buffers and get processed next iteration - no zero-padding,
288 // no data corruption, no skipped echo cancellation.
289 const size_t AEC3_FRAME_SIZE = 480;
290 bool want_aec3 = !bypass_aec3_worker && ctx->audio_pipeline && (ctx->duplex_stream || ctx->output_stream);
291
292 if (want_aec3) {
293 // Process matched render and capture where a playback reference is
294 // available. Microphone capture must still progress when playback is
295 // silent or has not produced a reference yet.
296 size_t matched = (capture_available < render_available) ? capture_available : render_available;
297 size_t aligned = (matched / AEC3_FRAME_SIZE) * AEC3_FRAME_SIZE;
298 if (aligned > WORKER_BATCH_SAMPLES)
299 aligned = (WORKER_BATCH_SAMPLES / AEC3_FRAME_SIZE) * AEC3_FRAME_SIZE;
300
301 bool has_render_reference = aligned > 0;
302 if (aligned == 0 && capture_available >= AEC3_FRAME_SIZE) {
303 // AEC3 consumes paired 10 ms frames. With no complete render frame,
304 // use a zero reference so an empty playback buffer cannot stall the
305 // microphone ring buffer until it overflows.
306 aligned = (capture_available / AEC3_FRAME_SIZE) * AEC3_FRAME_SIZE;
307 if (aligned > WORKER_BATCH_SAMPLES)
308 aligned = (WORKER_BATCH_SAMPLES / AEC3_FRAME_SIZE) * AEC3_FRAME_SIZE;
309 if (render_available > 0) {
310 size_t discard = render_available > WORKER_BATCH_SAMPLES ? WORKER_BATCH_SAMPLES : render_available;
312 }
313 }
314
315 if (aligned > 0) {
316 uint64_t capture_start_ns = time_get_ns();
317
318 size_t capture_read = audio_ring_buffer_read(ctx->raw_capture_rb, ctx->worker_capture_batch, aligned);
319 size_t render_read = 0;
320 if (has_render_reference) {
321 render_read = audio_ring_buffer_read(ctx->raw_render_rb, ctx->worker_render_batch, aligned);
322 } else {
323 memset(ctx->worker_render_batch, 0, capture_read * sizeof(float));
324 render_read = capture_read;
325 }
326
327 if (capture_read > 0 && render_read > 0) {
328 uint64_t aec3_start_ns = time_get_ns();
329
330 // Both buffers have identical, frame-aligned sample counts. A zero
331 // render reference leaves near-end microphone audio available when
332 // there is no speaker signal to cancel.
334 ctx->worker_capture_batch, (int)capture_read, ctx->worker_capture_batch);
335
336 long aec3_ns = (long)time_elapsed_ns(aec3_start_ns, time_get_ns());
337
338 static int aec3_count = 0;
339 static long aec3_total_ns = 0;
340 static long aec3_max_ns = 0;
341 aec3_count++;
342 aec3_total_ns += aec3_ns;
343 if (aec3_ns > aec3_max_ns)
344 aec3_max_ns = aec3_ns;
345
346 if (aec3_count % 100 == 0) {
347 long avg_ns = aec3_total_ns / aec3_count;
348 char avg_str[32], max_str[32], latest_str[32];
349 time_pretty((uint64_t)avg_ns, -1, avg_str, sizeof(avg_str));
350 time_pretty((uint64_t)aec3_max_ns, -1, max_str, sizeof(max_str));
351 time_pretty((uint64_t)aec3_ns, -1, latest_str, sizeof(latest_str));
352 log_info("AEC3 performance: avg=%s, max=%s, latest=%s (samples=%zu, %d calls)", avg_str, max_str,
353 latest_str, capture_read, aec3_count);
354 }
355 }
356
357 // Apply microphone sensitivity
358 float mic_sensitivity = GET_OPTION(microphone_sensitivity);
359 if (mic_sensitivity != 1.0f) {
360 if (mic_sensitivity < 0.0f)
361 mic_sensitivity = 0.0f;
362 if (mic_sensitivity > 1.0f)
363 mic_sensitivity = 1.0f;
364 for (size_t i = 0; i < capture_read; i++) {
365 ctx->worker_capture_batch[i] *= mic_sensitivity;
366 }
367 }
368
369 audio_publish_local_capture(ctx, ctx->worker_capture_batch, capture_read, use_media);
370
371 log_debug_every(NS_PER_MS_INT, "Worker processed %zu samples (AEC3 applied, render=%zu)", capture_read,
372 render_read);
373
374 double capture_time_ns = (double)time_elapsed_ns(capture_start_ns, time_get_ns());
375 total_capture_ns += capture_time_ns;
376 if (capture_time_ns > max_capture_ns)
377 max_capture_ns = capture_time_ns;
378 }
379 } else if (capture_available >= 64) {
380 // No AEC3 (bypassed or no pipeline): process capture directly
381 uint64_t capture_start_ns = time_get_ns();
382
383 size_t samples_to_process = (capture_available > WORKER_BATCH_SAMPLES) ? WORKER_BATCH_SAMPLES : capture_available;
384 size_t capture_read = audio_ring_buffer_read(ctx->raw_capture_rb, ctx->worker_capture_batch, samples_to_process);
385
386 if (capture_read > 0) {
387 // Drain render buffer to keep it from growing unbounded
388 if (render_available > 0) {
389 size_t drain = (render_available > WORKER_BATCH_SAMPLES) ? WORKER_BATCH_SAMPLES : render_available;
391 }
392
393 // Capture samples are normalized to AUDIO_SAMPLE_RATE by the input callback.
394
395 // Apply microphone sensitivity (volume control)
396 float mic_sensitivity = GET_OPTION(microphone_sensitivity);
397 if (mic_sensitivity != 1.0f) {
398 // Clamp to valid range [0.0, 1.0]
399 if (mic_sensitivity < 0.0f)
400 mic_sensitivity = 0.0f;
401 if (mic_sensitivity > 1.0f)
402 mic_sensitivity = 1.0f;
403
404 for (size_t i = 0; i < capture_read; i++) {
405 ctx->worker_capture_batch[i] *= mic_sensitivity;
406 }
407 }
408
409 // Write processed capture to encoder buffer
410 audio_publish_local_capture(ctx, ctx->worker_capture_batch, capture_read, use_media);
411
412 log_debug_every(NS_PER_MS_INT, "Worker processed %zu capture samples (AEC3 %s)", capture_read,
413 bypass_aec3_worker ? "BYPASSED" : "applied");
414 }
415
416 double capture_time_ns = (double)time_elapsed_ns(capture_start_ns, time_get_ns());
417 total_capture_ns += capture_time_ns;
418 if (capture_time_ns > max_capture_ns)
419 max_capture_ns = capture_time_ns;
420 }
421
422 // STEP 2: Output/Playback handling
423 // With output_callback registered, PortAudio handles calling it automatically
424 // No need for worker thread to manually write data via Pa_WriteStream
425 (void)playback_available; // Suppress unused variable warning if not used in this build
426
427 // Log overall loop iteration time
428 double loop_time_ns = (double)time_elapsed_ns(loop_start_ns, time_get_ns());
429 static double total_loop_ns = 0;
430 static double max_loop_ns = 0;
431 total_loop_ns += loop_time_ns;
432 if (loop_time_ns > max_loop_ns)
433 max_loop_ns = loop_time_ns;
434
435 if (loop_count % 100 == 0) {
436 char avg_loop_str[32], max_loop_str[32];
437 time_pretty((uint64_t)(total_loop_ns / loop_count), -1, avg_loop_str, sizeof(avg_loop_str));
438 time_pretty(max_loop_ns, -1, max_loop_str, sizeof(max_loop_str));
439 log_info("Worker loop timing: avg=%s max=%s", avg_loop_str, max_loop_str);
440 }
441 }
442
443 log_debug("Audio worker thread exiting");
444 return NULL;
445}
446
461static int duplex_callback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer,
462 const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags,
463 void *userData) {
464 (void)timeInfo;
465
466 static uint64_t duplex_invoke_count = 0;
467 duplex_invoke_count++;
468 if (duplex_invoke_count == 1) {
469 log_warn("!!! DUPLEX_CALLBACK INVOKED FOR FIRST TIME !!!");
470 }
471
472 uint64_t callback_start_ns = time_get_ns();
473
474 static uint64_t total_callbacks = 0;
475 total_callbacks++;
476 if (total_callbacks == 1) {
477 log_warn("FIRST CALLBACK RECEIVED! total=%llu frames=%lu", (unsigned long long)total_callbacks, framesPerBuffer);
478 }
479
480 audio_context_t *ctx = (audio_context_t *)userData;
481 if (!ctx) {
482 SET_ERRNO(ERROR_INVALID_PARAM, "duplex_callback: ctx is NULL");
483 return paAbort;
484 }
485
486 log_info_every(100 * NS_PER_MS_INT, "CB_START: ctx=%p output=%p inputBuffer=%p", (void *)ctx, (void *)outputBuffer,
487 inputBuffer);
488
489 const float *input = (const float *)inputBuffer;
490 float *output = (float *)outputBuffer;
491 size_t num_samples = framesPerBuffer * AUDIO_CHANNELS;
492
493 // Silence on shutdown
494 if (atomic_load_bool(&ctx->shutting_down)) {
495 if (output) {
496 SAFE_MEMSET(output, num_samples * sizeof(float), 0, num_samples * sizeof(float));
497 }
498 return paContinue;
499 }
500
501 // Log status flags (rate-limited to avoid spam)
502 if (statusFlags != 0) {
503 if (statusFlags & paOutputUnderflow) {
504 log_warn_every(LOG_RATE_FAST, "PortAudio output underflow");
505 }
506 if (statusFlags & paInputOverflow) {
507 log_warn_every(LOG_RATE_FAST, "PortAudio input overflow");
508 }
509 }
510
511 // Static counters for playback tracking (used in logging below)
512 static uint64_t total_samples_read_local = 0;
513 static uint64_t underrun_count_local = 0;
514
515 // The worker supplies local media and the receive path supplies remote audio.
516 if (output) {
517 size_t samples_read = 0;
518
519 if (ctx->playback_buffer) {
520 // Network mode: read from playback buffer with jitter buffering logic
521 samples_read = audio_ring_buffer_read(ctx->playback_buffer, output, num_samples);
522
523 static uint64_t playback_count = 0;
524 playback_count++;
525 if (playback_count <= 5 || playback_count % 500 == 0) {
526 log_info("Callback #%lu: playback_buffer path, read %zu samples", playback_count, samples_read);
527 }
528 } else {
529 static uint64_t null_count = 0;
530 if (++null_count == 1) {
531 log_warn("Callback: playback_buffer is NULL!");
532 }
533 }
534
535 total_samples_read_local += samples_read;
536
537 if (samples_read < num_samples) {
538 // Fill remaining with silence if underrun
539 SAFE_MEMSET(output + samples_read, (num_samples - samples_read) * sizeof(float), 0,
540 (num_samples - samples_read) * sizeof(float));
541 log_debug_every(NS_PER_MS_INT, "Audio playback underrun: got %zu/%zu samples", samples_read, num_samples);
542 }
543
544 // Apply speaker volume control
545 if (samples_read > 0) {
546 float speaker_volume = GET_OPTION(speakers_volume);
547 // Clamp to valid range [0.0, 1.0]
548 if (speaker_volume < 0.0f) {
549 speaker_volume = 0.0f;
550 } else if (speaker_volume > 1.0f) {
551 speaker_volume = 1.0f;
552 }
553 // Apply volume scaling if not at 100%
554 if (speaker_volume != 1.0f) {
555 log_debug_every(48000, "Applying audio volume %.1f%% to %zu samples", speaker_volume * 100.0, samples_read);
556 for (size_t i = 0; i < samples_read; i++) {
557 output[i] *= speaker_volume;
558 }
559 } else {
560 log_debug_every(48000, "Audio at 100%% volume, no scaling needed");
561 }
562 }
563 }
564
565 // STEP 2: Copy raw mic samples → worker for AEC3 processing (~0.5ms)
566 // Skip microphone capture in playback-only mode (mirror with file/URL audio)
567 // When files (--file) or URLs (--url) are being played, microphone input is completely disabled
568 // to prevent feedback loops and interference with playback audio
569 if (!ctx->playback_only && input && ctx->raw_capture_rb) {
570 audio_ring_buffer_write(ctx->raw_capture_rb, input, (int)num_samples);
571 } else if (ctx->playback_only && input) {
572 // Explicitly discard microphone input when in playback-only mode
573 // This ensures complete isolation between microphone and media playback
574 (void)input; // Suppress unused parameter warning
575 }
576
577 // STEP 3: Copy raw speaker samples → worker for AEC3 reference (~0.5ms)
578 // This is CRITICAL for AEC3 - worker needs exact render signal at same time as capture
579 // In playback-only mode, we still write the render reference for consistency
580 if (output && ctx->raw_render_rb) {
581 audio_ring_buffer_write(ctx->raw_render_rb, output, (int)num_samples);
582 }
583
584 // STEP 4: Signal worker thread (non-blocking, ~0.1ms)
585 // Worker wakes up, processes batch, writes back to processed buffers
586 cond_signal(&ctx->worker_cond);
587
588 // Log callback timing and playback stats periodically
589 double callback_time_ns = (double)time_elapsed_ns(callback_start_ns, time_get_ns());
590 static double total_callback_ns = 0;
591 static double max_callback_ns = 0;
592 static uint64_t callback_count = 0;
593
594 callback_count++;
595 total_callback_ns += callback_time_ns;
596 if (callback_time_ns > max_callback_ns)
597 max_callback_ns = callback_time_ns;
598
599 if (callback_count % 500 == 0) { // Log every ~10 seconds @ 48 FPS
600 char avg_str[32], max_str[32];
601 time_pretty((uint64_t)(total_callback_ns / callback_count), -1, avg_str, sizeof(avg_str));
602 time_pretty(max_callback_ns, -1, max_str, sizeof(max_str));
603 log_info("Duplex callback timing: count=%lu, avg=%s, max=%s (budget: 2ms)", callback_count, avg_str, max_str);
604 log_info("Playback stats: total_samples_read=%lu, underruns=%lu, read_success_rate=%.1f%%",
605 total_samples_read_local, underrun_count_local,
606 100.0 * (double)(callback_count - underrun_count_local) / (double)callback_count);
607
608 // DEBUG: Log first few output samples to verify they're not zero
609 if (output && num_samples >= 4) {
610 log_info("Output sample check: first4=[%.4f, %.4f, %.4f, %.4f] (verifying audio is not silent)", output[0],
611 output[1], output[2], output[3]);
612 }
613 }
614
615 return paContinue;
616}
617
629void resample_linear(const float *src, size_t src_samples, float *dst, size_t dst_samples, double src_rate,
630 double dst_rate) {
631 if (src_samples == 0 || dst_samples == 0) {
632 SAFE_MEMSET(dst, dst_samples * sizeof(float), 0, dst_samples * sizeof(float));
633 return;
634 }
635
636 double ratio = src_rate / dst_rate;
637
638 for (size_t i = 0; i < dst_samples; i++) {
639 double src_pos = (double)i * ratio;
640 size_t idx0 = (size_t)src_pos;
641 size_t idx1 = idx0 + 1;
642 double frac = src_pos - (double)idx0;
643
644 // Clamp indices to valid range
645 if (idx0 >= src_samples)
646 idx0 = src_samples - 1;
647 if (idx1 >= src_samples)
648 idx1 = src_samples - 1;
649
650 // Linear interpolation
651 dst[i] = (float)((1.0 - frac) * src[idx0] + frac * src[idx1]);
652 }
653}
654
667static int output_callback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer,
668 const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags,
669 void *userData) {
670 (void)inputBuffer;
671 (void)timeInfo;
672
673 static uint64_t output_cb_invoke_count = 0;
674 output_cb_invoke_count++;
675 if (output_cb_invoke_count == 1) {
676 log_warn("!!! OUTPUT_CALLBACK INVOKED FOR FIRST TIME !!!");
677 }
678
679 audio_context_t *ctx = (audio_context_t *)userData;
680 float *output = (float *)outputBuffer;
681 size_t num_samples = framesPerBuffer * AUDIO_CHANNELS;
682
683 static uint64_t output_cb_count = 0;
684 output_cb_count++;
685 if (output_cb_count == 1) {
686 log_debug("First output callback: frames=%lu", framesPerBuffer);
687 }
688
689 // Silence on shutdown
690 if (atomic_load_bool(&ctx->shutting_down)) {
691 if (output) {
692 SAFE_MEMSET(output, num_samples * sizeof(float), 0, num_samples * sizeof(float));
693 }
694 return paContinue;
695 }
696
697 if (statusFlags & paOutputUnderflow) {
698 log_warn_every(LOG_RATE_FAST, "PortAudio output underflow (separate stream)");
699 }
700
701 // STEP 1: Read audio source
702 size_t samples_read = 0;
703 if (output) {
704 const bool resample_output = ctx->output_device_rate > 0.0 && ctx->output_device_rate != AUDIO_SAMPLE_RATE;
705 float source[AUDIO_FRAMES_PER_BUFFER * 8];
706 float *source_output = resample_output ? source : output;
707 size_t source_capacity = resample_output ? sizeof(source) / sizeof(source[0]) : num_samples;
708
709 if (resample_output && framesPerBuffer > AUDIO_FRAMES_PER_BUFFER) {
710 log_warn_every(LOG_RATE_FAST, "Output callback block too large to resample safely (%lu frames)", framesPerBuffer);
711 SAFE_MEMSET(output, num_samples * sizeof(float), 0, num_samples * sizeof(float));
713 return paContinue;
714 }
715
716 size_t source_request = num_samples;
717 if (resample_output) {
718 double step = (double)AUDIO_SAMPLE_RATE / ctx->output_device_rate;
719 double last_position = ctx->output_resample_phase + (double)(num_samples - 1) * step;
720 source_request = (last_position < 0.0) ? 1 : (size_t)floor(last_position) + 2;
721 if (source_request > source_capacity) {
722 log_warn_every(LOG_RATE_FAST, "Output resample request exceeds callback buffer (%zu samples)", source_request);
723 SAFE_MEMSET(output, num_samples * sizeof(float), 0, num_samples * sizeof(float));
725 return paContinue;
726 }
727 }
728
729 if (ctx->playback_buffer) {
730 samples_read = audio_ring_buffer_read(ctx->playback_buffer, source_output, source_request);
731 if (output_cb_count <= 3) {
732 log_warn("OUTPUT_CB: playback_buffer path, read %zu samples", samples_read);
733 }
734 } else {
735 if (output_cb_count <= 3) {
736 log_warn("Output callback has no playback buffer");
737 }
738 }
739
740 if (resample_output) {
741 float render_volume = GET_OPTION(speakers_volume);
742 if (render_volume < 0.0f) {
743 render_volume = 0.0f;
744 } else if (render_volume > 1.0f) {
745 render_volume = 1.0f;
746 }
747 if (render_volume != 1.0f) {
748 for (size_t i = 0; i < samples_read; i++) {
749 source[i] *= render_volume;
750 }
751 }
752
753 // Keep the AEC render reference in the pipeline's 48 kHz domain.
754 if (ctx->render_buffer && samples_read > 0) {
755 audio_ring_buffer_write(ctx->render_buffer, source, (int)samples_read);
756 }
757
758 double step = (double)AUDIO_SAMPLE_RATE / ctx->output_device_rate;
759 size_t generated = 0;
760 for (; generated < num_samples; generated++) {
761 double position = ctx->output_resample_phase + (double)generated * step;
762 float first;
763 float second;
764 double fraction;
765 if (position < 0.0) {
766 if (!ctx->output_resample_has_previous || samples_read == 0) {
767 break;
768 }
769 first = ctx->output_resample_previous;
770 second = source[0];
771 fraction = position + 1.0;
772 } else {
773 size_t index = (size_t)position;
774 if (index + 1 >= samples_read) {
775 break;
776 }
777 first = source[index];
778 second = source[index + 1];
779 fraction = position - (double)index;
780 }
781 output[generated] = (float)((1.0 - fraction) * first + fraction * second);
782 }
783
784 if (samples_read > 0) {
785 ctx->output_resample_phase += (double)generated * step - (double)samples_read;
786 ctx->output_resample_previous = source[samples_read - 1];
788 }
789 samples_read = generated;
790 }
791
792 // Apply speaker volume control
793 if (samples_read > 0 && !resample_output) {
794 float speaker_volume = GET_OPTION(speakers_volume);
795 // Clamp to valid range [0.0, 1.0]
796 if (speaker_volume < 0.0f) {
797 speaker_volume = 0.0f;
798 } else if (speaker_volume > 1.0f) {
799 speaker_volume = 1.0f;
800 }
801 // Apply volume scaling if not at 100%
802 if (speaker_volume != 1.0f) {
803 log_debug_every(48000, "OUTPUT_CALLBACK: Applying volume %.0f%% to %zu samples", speaker_volume * 100.0,
804 samples_read);
805 for (size_t i = 0; i < samples_read; i++) {
806 output[i] *= speaker_volume;
807 }
808 }
809 }
810
811 // Fill remaining with silence if underrun
812 if (samples_read < num_samples) {
813 SAFE_MEMSET(output + samples_read, (num_samples - samples_read) * sizeof(float), 0,
814 (num_samples - samples_read) * sizeof(float));
815 }
816
817 // STEP 2: Copy to render buffer for input callback (AEC3 reference)
818 if (!resample_output && ctx->render_buffer && samples_read > 0) {
819 audio_ring_buffer_write(ctx->render_buffer, output, (int)samples_read);
820 }
821 }
822
823 // STEP 3: Signal worker
825
826 return paContinue;
827}
828
842static size_t resample_capture_block(audio_context_t *ctx, const float *input, size_t input_samples, float *output,
843 size_t output_capacity) {
844 if (!ctx || !input || !output || input_samples == 0 || ctx->input_device_rate <= 0.0) {
845 return 0;
846 }
847
848 const double step = ctx->input_device_rate / AUDIO_SAMPLE_RATE;
849 double position = ctx->input_resample_phase;
850 size_t produced = 0;
851
852 // The previous block's last sample is the sample immediately before input[0].
853 // Retaining position and that sample avoids restarting interpolation at each
854 // PortAudio callback boundary and preserves the long-term sample-rate ratio.
855 while (position < (double)input_samples) {
856 float first;
857 float second;
858 double fraction;
859 if (position < 0.0) {
860 if (!ctx->input_resample_has_previous) {
861 position += step;
862 continue;
863 }
864 first = ctx->input_resample_previous;
865 second = input[0];
866 fraction = position + 1.0;
867 } else {
868 size_t index = (size_t)position;
869 if (index + 1 >= input_samples) {
870 break; // Keep the endpoint for interpolation with the next callback.
871 }
872 first = input[index];
873 second = input[index + 1];
874 fraction = position - (double)index;
875 }
876
877 if (produced == output_capacity) {
878 return 0;
879 }
880 output[produced++] = (float)((1.0 - fraction) * first + fraction * second);
881 position += step;
882 }
883
884 ctx->input_resample_phase = position - (double)input_samples;
885 ctx->input_resample_previous = input[input_samples - 1];
886 ctx->input_resample_has_previous = true;
887 return produced;
888}
889
890static int input_callback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer,
891 const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData) {
892 (void)outputBuffer;
893 (void)timeInfo;
894
895 static uint64_t input_invoke_count = 0;
896 input_invoke_count++;
897 if (input_invoke_count == 1) {
898 log_warn("!!! INPUT_CALLBACK INVOKED FOR FIRST TIME !!!");
899 }
900
901 audio_context_t *ctx = (audio_context_t *)userData;
902 const float *input = (const float *)inputBuffer;
903 size_t num_samples = framesPerBuffer * AUDIO_CHANNELS;
904
905 // Track callback frequency
906 static uint64_t callback_count = 0;
907 static uint64_t last_log_time_ns = 0;
908 callback_count++;
909
910 uint64_t now_ns = time_get_ns();
911
912 if (last_log_time_ns == 0) {
913 last_log_time_ns = now_ns;
914 } else {
915 long elapsed_ms = (long)time_ns_to_ms(time_elapsed_ns(last_log_time_ns, now_ns));
916 if (elapsed_ms >= 1000) {
917 log_info("Input callback: %lu calls/sec, %lu frames/call, %zu samples/call", callback_count, framesPerBuffer,
918 num_samples);
919 callback_count = 0;
920 last_log_time_ns = now_ns;
921 }
922 }
923
924 // Silence on shutdown
925 if (atomic_load_bool(&ctx->shutting_down)) {
926 return paContinue;
927 }
928
929 if (statusFlags & paInputOverflow) {
930 log_warn_every(LOG_RATE_FAST, "PortAudio input overflow (separate stream)");
931 }
932
933 // STEP 1: Copy raw mic samples → worker for AEC3 processing
934 // Skip microphone capture in playback-only mode (mirror)
935 if (!ctx->playback_only && input && ctx->raw_capture_rb) {
936 const float *capture = input;
937 size_t capture_samples = num_samples;
938 float normalized[AUDIO_FRAMES_PER_BUFFER * 8];
939 if (ctx->input_device_rate > 0.0 && ctx->input_device_rate != AUDIO_SAMPLE_RATE) {
940 // PortAudio delivers frames at the native stream rate. Convert them to
941 // the pipeline's fixed 48 kHz clock before AEC3/Opus.
942 size_t output_samples = resample_capture_block(ctx, input, num_samples, normalized,
943 sizeof(normalized) / sizeof(normalized[0]));
944 if (output_samples == 0) {
945 log_warn_every(LOG_RATE_FAST, "Input callback block too large to resample safely (%zu samples)", num_samples);
947 return paContinue;
948 }
949 capture = normalized;
950 capture_samples = output_samples;
951 }
952 audio_ring_buffer_write(ctx->raw_capture_rb, capture, (int)capture_samples);
953
954 // Keep the render reference in the same 48 kHz sample domain as capture.
955 num_samples = capture_samples;
956 }
957
958 // STEP 2: Read render reference from render_buffer and copy to worker
959 // (render_buffer is written by output_callback, read here for synchronization)
960 if (ctx->render_buffer && ctx->raw_render_rb) {
961 // Worker will handle the AEC3 processing using this render reference
962 size_t render_available = audio_ring_buffer_available_read(ctx->render_buffer);
963 if (render_available >= num_samples) {
964 // Read exactly what we need
965 float render_temp[AUDIO_FRAMES_PER_BUFFER * 8]; // Allows the converted 48 kHz callback block.
966 size_t render_read = audio_ring_buffer_read(ctx->render_buffer, render_temp, num_samples);
967 if (render_read > 0) {
968 audio_ring_buffer_write(ctx->raw_render_rb, render_temp, (int)render_read);
969 }
970 }
971 }
972
973 // STEP 3: Signal worker thread
975
976 return paContinue;
977}
978
979// Forward declaration for internal helper function
980static audio_ring_buffer_t *audio_ring_buffer_create_internal(bool jitter_buffer_enabled);
981
982static audio_ring_buffer_t *audio_ring_buffer_create_internal(bool jitter_buffer_enabled) {
983 size_t rb_size = sizeof(audio_ring_buffer_t);
985
986 if (!rb) {
987 SET_ERRNO(ERROR_MEMORY, "Failed to allocate audio ring buffer from buffer pool");
988 return NULL;
989 }
990
991 SAFE_MEMSET(rb->data, sizeof(rb->data), 0, sizeof(rb->data));
996 // For capture buffers (jitter_buffer_enabled=false), mark as already filled to bypass jitter logic
997 // For playback buffers (jitter_buffer_enabled=true), start unfilled to wait for threshold
998 atomic_store_bool(&rb->jitter_buffer_filled, !jitter_buffer_enabled);
1001 rb->last_sample = 0.0f;
1003 rb->jitter_buffer_enabled = jitter_buffer_enabled;
1004
1005 if (mutex_init(&rb->mutex, "audio_ring_buffer") != 0) {
1006 SET_ERRNO(ERROR_THREAD, "Failed to initialize audio ring buffer mutex");
1007 buffer_pool_free(NULL, rb, sizeof(audio_ring_buffer_t));
1008 return NULL;
1009 }
1010
1011 return rb;
1012}
1013
1015 return audio_ring_buffer_create_internal(true); // Default: enable jitter buffering for playback
1016}
1017
1019 return audio_ring_buffer_create_internal(false); // Disable jitter buffering for capture
1020}
1021
1023 if (!rb)
1024 return;
1025
1026 NAMED_UNREGISTER(rb);
1028
1029 mutex_destroy(&rb->mutex);
1030 buffer_pool_free(NULL, rb, sizeof(audio_ring_buffer_t));
1031}
1032
1033void audio_ring_buffer_register_atomics(audio_ring_buffer_t *rb, const char *context_name) {
1034 if (!rb || !context_name || !context_name[0]) {
1035 return;
1036 }
1037
1038 // Register the ring buffer itself with a unique name
1039 NAMED_REGISTER_AUDIO_RINGBUF(rb, context_name, NULL);
1040
1041 // Ring buffer indices (lock-free producer-consumer)
1042 NAMED_REGISTER_ATOMIC(&rb->write_index, "write_index_producer_position", (uintptr_t)(const void *)(rb));
1043 NAMED_REGISTER_ATOMIC(&rb->read_index, "read_index_consumer_position", (uintptr_t)(const void *)(rb));
1044 NAMED_REGISTER_ATOMIC(&rb->discard_to_index, "discard_to_index_producer_position", (uintptr_t)(const void *)(rb));
1045
1046 // Jitter buffer state management
1047 NAMED_REGISTER_ATOMIC(&rb->jitter_buffer_filled, "jitter_buffer_has_filled_threshold_flag",
1048 (uintptr_t)(const void *)(rb));
1049 NAMED_REGISTER_ATOMIC(&rb->crossfade_samples_remaining, "crossfade_samples_remaining_count",
1050 (uintptr_t)(const void *)(rb));
1051 NAMED_REGISTER_ATOMIC(&rb->crossfade_fade_in, "crossfade_fade_in_direction_flag", (uintptr_t)(const void *)(rb));
1052
1053 // Audio quality monitoring
1054 NAMED_REGISTER_ATOMIC(&rb->underrun_count, "audio_underrun_event_counter", (uintptr_t)(const void *)(rb));
1055}
1056
1070
1072 if (!rb)
1073 return;
1074
1075 mutex_lock(&rb->mutex);
1076 // Reset buffer to empty state (no audio to play = silence at shutdown)
1081 rb->last_sample = 0.0f;
1082 // Clear the actual data to zeros to prevent any stale audio
1083 SAFE_MEMSET(rb->data, sizeof(rb->data), 0, sizeof(rb->data));
1084 mutex_unlock(&rb->mutex);
1085}
1086
1088 if (rb) {
1089 // Capture the current producer position before the caller writes its new
1090 // samples. The consumer can drop everything through this point while
1091 // preserving samples appended afterward.
1092 unsigned int write_idx = atomic_load_u64(&rb->write_index);
1093 atomic_store_u64(&rb->discard_to_index, write_idx);
1095 }
1096}
1097
1099 if (!rb || !data || samples <= 0)
1100 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: rb=%p, data=%p, samples=%d", rb, data, samples);
1101
1102 // Validate samples doesn't exceed our buffer size
1103 if (samples > AUDIO_RING_BUFFER_SIZE) {
1104 return SET_ERRNO(ERROR_BUFFER, "Attempted to write %d samples, but buffer size is only %d", samples,
1106 }
1107
1108 // LOCK-FREE: Load indices with proper memory ordering
1109 // - Load our own write_index with relaxed (no sync needed with ourselves)
1110 // - Load reader's read_index with acquire (see reader's updates to free space)
1111 unsigned int write_idx = atomic_load_u64(&rb->write_index);
1112 unsigned int read_idx = atomic_load_u64(&rb->read_index);
1113
1114 // Calculate current buffer level (how many samples are buffered)
1115 int buffer_level;
1116 if (write_idx >= read_idx) {
1117 buffer_level = (int)(write_idx - read_idx);
1118 } else {
1119 buffer_level = AUDIO_RING_BUFFER_SIZE - (int)(read_idx - write_idx);
1120 }
1121 // Reserve 1 slot to distinguish between full and empty states
1122 // (when buffer is full, write_idx will be just before read_idx, not equal to it)
1123 int available = AUDIO_RING_BUFFER_SIZE - 1 - buffer_level;
1124
1125 // If playback has fallen behind, ask its consumer to skip the stale queue.
1126 // Dropping incoming samples here preserves the old backlog, so playback
1127 // drains outdated audio and then underruns before it can return to live data.
1128 if (buffer_level > AUDIO_JITTER_HIGH_WATER_MARK) {
1129 if (rb->jitter_buffer_enabled) {
1130 // The read callback owns read_index. Let it advance to the latest write
1131 // position atomically on its next invocation, then preserve this fresh
1132 // packet so audio resumes from the current stream position.
1135 "Audio playback backlog (%d samples); requesting consumer to skip stale queued audio",
1136 buffer_level);
1137 } else {
1138 // Capture queues also have a high-water mark, but their reader may be
1139 // processing a matching microphone/render interval. Retain the existing
1140 // policy there instead of changing the reader's timeline asynchronously.
1141 int target_writes = AUDIO_JITTER_TARGET_LEVEL - buffer_level;
1142 if (target_writes < 0) {
1143 target_writes = 0;
1144 }
1145
1146 if (samples > target_writes) {
1147 int dropped = samples - target_writes;
1149 "Audio capture buffer high water mark exceeded (%d > %d): dropping %d incoming samples",
1150 buffer_level, AUDIO_JITTER_HIGH_WATER_MARK, dropped);
1151 samples = target_writes;
1152 }
1153 }
1154 }
1155
1156 // Now write the new samples - should always have enough space after above
1157 int samples_to_write = samples;
1158 if (samples > available) {
1159 // This should rarely happen after the high water mark logic above
1160 int samples_dropped = samples - available;
1161 samples_to_write = available;
1162 log_warn_every(LOG_RATE_FAST, "Audio buffer overflow: dropping %d of %d incoming samples (buffer_used=%d/%d)",
1163 samples_dropped, samples, AUDIO_RING_BUFFER_SIZE - available, AUDIO_RING_BUFFER_SIZE);
1164 }
1165
1166 // Write only the samples that fit (preserves existing data integrity)
1167 if (samples_to_write > 0) {
1168 int remaining = AUDIO_RING_BUFFER_SIZE - (int)write_idx;
1169
1170 if (samples_to_write <= remaining) {
1171 // Can copy in one chunk
1172 SAFE_MEMCPY(&rb->data[write_idx], samples_to_write * sizeof(float), data, samples_to_write * sizeof(float));
1173 } else {
1174 // Need to wrap around - copy in two chunks
1175 SAFE_MEMCPY(&rb->data[write_idx], remaining * sizeof(float), data, remaining * sizeof(float));
1176 SAFE_MEMCPY(&rb->data[0], (samples_to_write - remaining) * sizeof(float), &data[remaining],
1177 (samples_to_write - remaining) * sizeof(float));
1178 }
1179
1180 // LOCK-FREE: Store new write_index with release ordering
1181 // This ensures all data writes above are visible before the index update
1182 unsigned int new_write_idx = (write_idx + (unsigned int)samples_to_write) % AUDIO_RING_BUFFER_SIZE;
1183 atomic_store_u64(&rb->write_index, new_write_idx);
1184 }
1185
1186 // Note: jitter buffer fill check is now done in read function for better control
1187
1188 return ASCIICHAT_OK; // Success
1189}
1190
1191size_t audio_ring_buffer_read(audio_ring_buffer_t *rb, float *data, size_t samples) {
1192 if (!rb || !data || samples <= 0) {
1193 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: rb=%p, data=%p, samples=%d", rb, data, samples);
1194 return 0; // Return 0 samples read on error
1195 }
1196
1197 // Keep read_index consumer-owned while letting a producer request a flush.
1198 if (atomic_exchange_bool(&rb->discard_pending, false)) {
1199 unsigned int read_idx = atomic_load_u64(&rb->read_index);
1200 unsigned int discard_to = atomic_load_u64(&rb->discard_to_index);
1201 unsigned int advance = (discard_to + AUDIO_RING_BUFFER_SIZE - read_idx) % AUDIO_RING_BUFFER_SIZE;
1202 // A concurrent callback may already have consumed beyond the requested
1203 // boundary. In that case the target is behind us and must not rewind the
1204 // single-consumer index.
1205 if (advance < AUDIO_RING_BUFFER_SIZE / 2) {
1206 atomic_store_u64(&rb->read_index, discard_to);
1207 rb->last_sample = 0.0f;
1209 }
1210 }
1211
1212 // LOCK-FREE: Load indices with proper memory ordering
1213 // - Load writer's write_index with acquire (see writer's data updates)
1214 // - Load our own read_index with relaxed (no sync needed with ourselves)
1215 unsigned int write_idx = atomic_load_u64(&rb->write_index);
1216 unsigned int read_idx = atomic_load_u64(&rb->read_index);
1217
1218 // Calculate available samples
1219 size_t available;
1220 if (write_idx >= read_idx) {
1221 available = write_idx - read_idx;
1222 } else {
1223 available = AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
1224 }
1225
1226 // LOCK-FREE: Load jitter buffer state with acquire ordering
1227 bool jitter_filled = atomic_load_u64(&rb->jitter_buffer_filled);
1228 int crossfade_remaining = atomic_load_u64(&rb->crossfade_samples_remaining);
1229 bool fade_in = atomic_load_u64(&rb->crossfade_fade_in);
1230
1231 // Jitter buffer: don't read until initial fill threshold is reached
1232 // (only for playback buffers - capture buffers have jitter_buffer_enabled = false)
1233 if (!jitter_filled && rb->jitter_buffer_enabled) {
1234 // Check if we've accumulated enough samples to start playback
1235 if (available >= AUDIO_JITTER_BUFFER_THRESHOLD) {
1239 log_info("Jitter buffer filled (%zu samples), starting playback with fade-in", available);
1240 // Reload state for processing below
1241 jitter_filled = true;
1242 crossfade_remaining = AUDIO_CROSSFADE_SAMPLES;
1243 fade_in = true;
1244 } else {
1245 // Log buffer fill progress every second
1246 log_debug_every(NS_PER_MS_INT, "Jitter buffer filling: %zu/%d samples (%.1f%%)", available,
1248 return 0; // Return 0 samples - caller will pad with silence
1249 }
1250 }
1251
1252 // Periodic buffer health logging (every 5 seconds when healthy)
1253 unsigned int underruns = atomic_load_u64(&rb->underrun_count);
1254 log_dev_every(5 * NS_PER_MS_INT, "Buffer health: %zu/%d samples (%.1f%%), underruns=%u", available,
1255 AUDIO_RING_BUFFER_SIZE, (100.0f * available) / AUDIO_RING_BUFFER_SIZE, underruns);
1256
1257 // Low buffer handling: DON'T pause playback - continue reading what's available
1258 // and fill the rest with silence. Pausing causes a feedback loop where:
1259 // 1. Underrun -> pause reading -> buffer overflows from incoming samples
1260 // 2. Threshold reached -> resume reading -> drains too fast -> underrun again
1261 //
1262 // Instead: always consume samples to prevent overflow, use silence for missing data
1263 if (rb->jitter_buffer_enabled && available < AUDIO_JITTER_LOW_WATER_MARK) {
1264 unsigned int underrun_count = atomic_fetch_add_u64(&rb->underrun_count, 1) + 1;
1266 "Audio buffer low #%u: only %zu samples available (low water mark: %d), padding with silence",
1267 underrun_count, available, AUDIO_JITTER_LOW_WATER_MARK);
1268 // Don't set jitter_buffer_filled = false - keep reading to prevent overflow
1269 }
1270
1271 size_t to_read = (samples > available) ? available : samples;
1272
1273 // Optimize: copy in chunks instead of one sample at a time
1274 size_t remaining = AUDIO_RING_BUFFER_SIZE - read_idx;
1275
1276 if (to_read <= remaining) {
1277 // Can copy in one chunk
1278 SAFE_MEMCPY(data, to_read * sizeof(float), &rb->data[read_idx], to_read * sizeof(float));
1279 } else {
1280 // Need to wrap around - copy in two chunks
1281 SAFE_MEMCPY(data, remaining * sizeof(float), &rb->data[read_idx], remaining * sizeof(float));
1282 SAFE_MEMCPY(&data[remaining], (to_read - remaining) * sizeof(float), &rb->data[0],
1283 (to_read - remaining) * sizeof(float));
1284 }
1285
1286 // LOCK-FREE: Store new read_index with release ordering
1287 // This ensures all data reads above complete before the index update
1288 unsigned int new_read_idx = (read_idx + (unsigned int)to_read) % AUDIO_RING_BUFFER_SIZE;
1289 atomic_store_u64(&rb->read_index, new_read_idx);
1290
1291 // Apply fade-in if recovering from underrun
1292 if (fade_in && crossfade_remaining > 0) {
1293 int fade_start = AUDIO_CROSSFADE_SAMPLES - crossfade_remaining;
1294 size_t fade_samples = (to_read < (size_t)crossfade_remaining) ? to_read : (size_t)crossfade_remaining;
1295
1296 for (size_t i = 0; i < fade_samples; i++) {
1297 float fade_factor = (float)(fade_start + (int)i + 1) / (float)AUDIO_CROSSFADE_SAMPLES;
1298 data[i] *= fade_factor;
1299 }
1300
1301 int new_crossfade_remaining = crossfade_remaining - (int)fade_samples;
1302 atomic_store_u64(&rb->crossfade_samples_remaining, new_crossfade_remaining);
1303 if (new_crossfade_remaining <= 0) {
1305 log_debug("Audio fade-in complete");
1306 }
1307 }
1308
1309 // Save last sample for potential fade-out
1310 // Note: only update if we actually read some data
1311 // This is NOT atomic - only the reader thread writes this
1312 if (to_read > 0) {
1313 rb->last_sample = data[to_read - 1];
1314 }
1315
1316 // Return ACTUAL number of samples read, not padded count
1317 // The caller (mixer) expects truthful return values to detect underruns
1318 // and handle silence padding externally. Internal padding creates double-padding bugs.
1319 //
1320 // This function was incorrectly returning `samples` even when
1321 // it only read `to_read` samples. This broke the mixer's underrun detection.
1322 return to_read;
1323}
1324
1336size_t audio_ring_buffer_peek(audio_ring_buffer_t *rb, float *data, size_t samples) {
1337 if (!rb || !data || samples <= 0) {
1338 return 0;
1339 }
1340
1341 // LOCK-FREE: Load indices with proper memory ordering
1342 unsigned int write_idx = atomic_load_u64(&rb->write_index);
1343 unsigned int read_idx = atomic_load_u64(&rb->read_index);
1344
1345 // Calculate available samples
1346 size_t available;
1347 if (write_idx >= read_idx) {
1348 available = write_idx - read_idx;
1349 } else {
1350 available = AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
1351 }
1352
1353 size_t to_peek = (samples > available) ? available : samples;
1354
1355 if (to_peek == 0) {
1356 return 0;
1357 }
1358
1359 // Copy samples in chunks (handle wraparound)
1360 size_t first_chunk = (read_idx + to_peek <= AUDIO_RING_BUFFER_SIZE) ? to_peek : (AUDIO_RING_BUFFER_SIZE - read_idx);
1361
1362 SAFE_MEMCPY(data, first_chunk * sizeof(float), rb->data + read_idx, first_chunk * sizeof(float));
1363
1364 if (first_chunk < to_peek) {
1365 // Wraparound: copy second chunk from beginning of buffer
1366 size_t second_chunk = to_peek - first_chunk;
1367 SAFE_MEMCPY(data + first_chunk, second_chunk * sizeof(float), rb->data, second_chunk * sizeof(float));
1368 }
1369
1370 return to_peek;
1371}
1372
1374 if (!rb)
1375 return 0;
1376
1377 // LOCK-FREE: Load indices with proper memory ordering
1378 // Use acquire for write_index to see writer's updates
1379 // Use relaxed for read_index (our own index)
1380 unsigned int write_idx = atomic_load_u64(&rb->write_index);
1381 unsigned int read_idx = atomic_load_u64(&rb->read_index);
1382
1383 if (write_idx >= read_idx) {
1384 return write_idx - read_idx;
1385 }
1386
1387 return AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
1388}
1389
1391 if (!rb) {
1392 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: rb is NULL");
1393 return 0;
1394 }
1395
1397}
1398
1400 log_debug("audio_init: starting, ctx=%p", (void *)ctx);
1401 if (!ctx) {
1402 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx is NULL");
1403 }
1404
1405 SAFE_MEMSET(ctx, sizeof(audio_context_t), 0, sizeof(audio_context_t));
1406
1407 if (mutex_init(&ctx->state_mutex, "audio_context") != 0) {
1408 return SET_ERRNO(ERROR_THREAD, "Failed to initialize audio context mutex");
1409 }
1410
1411 // NOTE: PortAudio initialization deferred to audio_start_duplex() where streams are actually opened
1412 // This avoids Pa_Initialize() overhead for contexts that might not start duplex
1413 // and prevents premature ALSA device allocation and memory leaks
1414
1415 // Create capture buffer WITHOUT jitter buffering (PortAudio writes directly from microphone)
1417 if (!ctx->capture_buffer) {
1419 return SET_ERRNO(ERROR_MEMORY, "Failed to create capture buffer");
1420 }
1421
1423 if (!ctx->playback_buffer) {
1426 return SET_ERRNO(ERROR_MEMORY, "Failed to create playback buffer");
1427 }
1428
1429 // Create new ring buffers for worker thread architecture
1431 if (!ctx->raw_capture_rb) {
1435 return SET_ERRNO(ERROR_MEMORY, "Failed to create raw capture buffer");
1436 }
1437
1439 if (!ctx->raw_render_rb) {
1444 return SET_ERRNO(ERROR_MEMORY, "Failed to create raw render buffer");
1445 }
1446
1448 if (!ctx->processed_playback_rb) {
1454 return SET_ERRNO(ERROR_MEMORY, "Failed to create processed playback buffer");
1455 }
1456
1457 // Initialize worker thread infrastructure
1458 if (mutex_init(&ctx->worker_mutex, "audio_worker_mutex") != 0) {
1465 return SET_ERRNO(ERROR_THREAD, "Failed to initialize worker mutex");
1466 }
1467
1468 if (cond_init(&ctx->worker_cond, "audio_worker_cond") != 0) {
1476 return SET_ERRNO(ERROR_THREAD, "Failed to initialize worker condition variable");
1477 }
1478
1479 // Allocate pre-allocated worker buffers (avoid malloc in worker loop)
1480 ctx->worker_capture_batch = SAFE_MALLOC(WORKER_BATCH_SAMPLES * sizeof(float), float *);
1481 if (!ctx->worker_capture_batch) {
1490 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate worker capture batch buffer");
1491 }
1492
1493 ctx->worker_render_batch = SAFE_MALLOC(WORKER_BATCH_SAMPLES * sizeof(float), float *);
1494 if (!ctx->worker_render_batch) {
1504 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate worker render batch buffer");
1505 }
1506
1507 ctx->worker_playback_batch = SAFE_MALLOC(WORKER_BATCH_SAMPLES * sizeof(float), float *);
1508 if (!ctx->worker_playback_batch) {
1519 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate worker playback batch buffer");
1520 }
1521
1522 ctx->capture_media_source = NULL;
1523 ctx->monitor_local_media = false;
1524
1525 // Initialize worker thread state (thread will be started in audio_start_duplex)
1526 ctx->worker_running = false;
1528
1529 ctx->initialized = true;
1530 atomic_store_bool(&ctx->shutting_down, false);
1531
1532 /* Register audio context with named registry */
1533 NAMED_REGISTER(ctx, "audio_context", "audio_context_t", "0x%tx", NULL);
1534
1535 /* Register audio context's sync primitives with hierarchical naming */
1536 NAMED_REGISTER_MUTEX(&ctx->state_mutex, "state_mutex", (uintptr_t)(const void *)(ctx));
1537 NAMED_REGISTER_MUTEX(&ctx->worker_mutex, "worker_mutex", (uintptr_t)(const void *)(ctx));
1538 NAMED_REGISTER_COND(&ctx->worker_cond, "worker_cond", (uintptr_t)(const void *)(ctx));
1539 NAMED_REGISTER_ATOMIC(&ctx->worker_should_stop, "worker_should_stop", (uintptr_t)(const void *)(ctx));
1540 NAMED_REGISTER_ATOMIC(&ctx->shutting_down, "shutting_down", (uintptr_t)(const void *)(ctx));
1541
1542 /* Register ring buffers as child structures with audio context as parent */
1543 NAMED_REGISTER_AUDIO_RINGBUF(ctx->capture_buffer, "capture_buffer", (uintptr_t)(const void *)(ctx));
1544 NAMED_REGISTER_AUDIO_RINGBUF(ctx->playback_buffer, "playback_buffer", (uintptr_t)(const void *)(ctx));
1545 NAMED_REGISTER_AUDIO_RINGBUF(ctx->raw_capture_rb, "raw_capture_rb", (uintptr_t)(const void *)(ctx));
1546 NAMED_REGISTER_AUDIO_RINGBUF(ctx->raw_render_rb, "raw_render_rb", (uintptr_t)(const void *)(ctx));
1547 NAMED_REGISTER_AUDIO_RINGBUF(ctx->processed_playback_rb, "processed_playback_rb", (uintptr_t)(const void *)(ctx));
1548
1549 log_info("Audio system initialized successfully (worker thread architecture enabled)");
1550 return ASCIICHAT_OK;
1551}
1552
1554 if (!ctx) {
1555 return;
1556 }
1557
1558 // Always release PortAudio refcount if it was incremented
1559 // audio_init() calls Pa_Initialize() very early, and if it fails partway through,
1560 // ctx->initialized will be false. But we MUST still call audio_release_portaudio()
1561 // to properly decrement the refcount and allow Pa_Terminate() to be called.
1562 if (ctx->initialized) {
1563
1564 // Stop duplex stream if running (this also stops the worker thread)
1565 if (ctx->running) {
1566 audio_stop_duplex(ctx);
1567 }
1568
1569 // Ensure worker thread is stopped even if streams weren't running
1570 if (ctx->worker_running) {
1571 log_debug("Stopping worker thread during audio_destroy");
1573 cond_signal(&ctx->worker_cond); // Wake up worker if waiting
1575 ctx->worker_running = false;
1576 }
1577
1578 mutex_lock(&ctx->state_mutex);
1579
1580 // Destroy all ring buffers (old + new)
1586 audio_ring_buffer_destroy(ctx->render_buffer); // May be NULL, that's OK
1587
1588 // Free pre-allocated worker buffers
1592
1593 // Destroy worker synchronization primitives
1596
1597 ctx->initialized = false;
1598
1601
1602 log_debug("Audio system cleanup complete (all resources released)");
1603 } else {
1604 }
1605}
1606
1607void audio_set_pipeline(audio_context_t *ctx, void *pipeline) {
1608 if (!ctx)
1609 return;
1610 ctx->audio_pipeline = pipeline;
1611}
1612
1614 if (!ctx || !ctx->initialized) {
1615 return;
1616 }
1617
1618 if (ctx->playback_buffer) {
1620 }
1621 if (ctx->processed_playback_rb) {
1623 }
1624 if (ctx->render_buffer) {
1626 }
1627 if (ctx->raw_render_rb) {
1629 }
1630}
1631
1633 if (!ctx || !ctx->initialized) {
1634 return SET_ERRNO(ERROR_INVALID_STATE, "Audio context not initialized");
1635 }
1636
1637 // Check if already running (without holding lock during blocking operations)
1638 // Do this check first before acquiring any locks
1639 if (ctx->duplex_stream || ctx->input_stream || ctx->output_stream) {
1640 return ASCIICHAT_OK;
1641 }
1642
1643 bool media_audio = ctx->capture_media_source && media_source_has_audio(ctx->capture_media_source);
1644 if (!audio_should_enable_microphone(GET_OPTION(audio_capture_source), media_audio))
1645 ctx->playback_only = true;
1646 bool media_only = media_audio && ctx->playback_only;
1647
1648 // Media-only recording can run through the software worker without an audio
1649 // device. Initialize PortAudio opportunistically so monitoring still works
1650 // when a speaker is available.
1651 asciichat_error_t pa_result = audio_ensure_portaudio_initialized();
1652 if (pa_result != ASCIICHAT_OK && !media_only)
1653 return pa_result;
1654
1655 if (pa_result != ASCIICHAT_OK) {
1657 log_warn("PortAudio unavailable; continuing media-only audio without an audio device");
1658 goto start_worker;
1659 }
1660
1661 // Setup input parameters (skip if playback-only mode)
1662 PaStreamParameters inputParams = {0};
1663 const PaDeviceInfo *inputInfo = NULL;
1664 bool has_input = false;
1665
1666 if (!ctx->playback_only) {
1667 if (GET_OPTION(microphone_index) >= 0) {
1668 inputParams.device = GET_OPTION(microphone_index);
1669 } else {
1670 inputParams.device = Pa_GetDefaultInputDevice();
1671 }
1672
1673 if (inputParams.device == paNoDevice) {
1674 return SET_ERRNO(ERROR_AUDIO, "No input device available");
1675 }
1676
1677 inputInfo = Pa_GetDeviceInfo(inputParams.device);
1678 if (!inputInfo) {
1680 return SET_ERRNO(ERROR_AUDIO, "Input device info not found");
1681 }
1682
1683 has_input = true;
1684 inputParams.channelCount = AUDIO_CHANNELS;
1685 inputParams.sampleFormat = paFloat32;
1686 inputParams.suggestedLatency = inputInfo->defaultLowInputLatency;
1687 inputParams.hostApiSpecificStreamInfo = NULL;
1688 }
1689
1690 // Setup output parameters
1691 PaStreamParameters outputParams;
1692 const PaDeviceInfo *outputInfo = NULL;
1693 bool has_output = false;
1694
1695 if (GET_OPTION(audio_no_playback)) {
1696 outputParams.device = paNoDevice;
1697 log_info("Audio playback disabled by --no-audio-playback");
1698 } else if (GET_OPTION(speakers_index) >= 0) {
1699 outputParams.device = GET_OPTION(speakers_index);
1700 } else {
1701 outputParams.device = Pa_GetDefaultOutputDevice();
1702 }
1703
1704 if (outputParams.device != paNoDevice) {
1705 outputInfo = Pa_GetDeviceInfo(outputParams.device);
1706 if (outputInfo) {
1707 has_output = true;
1708 outputParams.channelCount = AUDIO_CHANNELS;
1709 outputParams.sampleFormat = paFloat32;
1710 outputParams.suggestedLatency = outputInfo->defaultLowOutputLatency;
1711 outputParams.hostApiSpecificStreamInfo = NULL;
1712 } else {
1713 log_warn("Output device info not found for device %d", outputParams.device);
1714 }
1715 }
1716
1717 // Store device rates for diagnostics (only access if device info was retrieved)
1718 ctx->input_device_rate = (has_input && inputInfo) ? inputInfo->defaultSampleRate : 0;
1719 ctx->output_device_rate = (has_output && outputInfo) ? outputInfo->defaultSampleRate : 0;
1720
1721 log_debug("Opening audio:");
1722 if (has_input) {
1723 log_info(" Input: %s (%.0f Hz)", inputInfo->name, inputInfo->defaultSampleRate);
1724 } else if (ctx->playback_only) {
1725 log_debug(" Input: (playback-only mode - no microphone)");
1726 } else {
1727 log_debug(" Input: (none)");
1728 }
1729 if (has_output) {
1730 log_info(" Output: %s (%.0f Hz)", outputInfo->name, outputInfo->defaultSampleRate);
1731 } else {
1732 log_debug(" Output: None (input-only mode - will send audio to server)");
1733 }
1734
1735 // Keep capture and playback synchronized in one 48 kHz duplex stream when
1736 // both devices share a native rate. PortAudio handles conversion to/from
1737 // that rate. Separate streams are needed only when the devices themselves
1738 // run at different rates or one direction is unavailable.
1739 bool rates_differ = has_input && has_output && (inputInfo->defaultSampleRate != outputInfo->defaultSampleRate);
1740 bool try_separate = rates_differ || !has_input || !has_output;
1741 PaError err = paNoError;
1742
1743 if (!try_separate) {
1744 // Try full-duplex first (preferred - perfect AEC3 timing)
1745 LOG_IO("portaudio", {
1746 err = Pa_OpenStream(&ctx->duplex_stream, &inputParams, &outputParams, AUDIO_SAMPLE_RATE, AUDIO_FRAMES_PER_BUFFER,
1747 paClipOff, duplex_callback, ctx);
1748 });
1749
1750 if (err == paNoError) {
1751 LOG_IO("portaudio", { err = Pa_StartStream(ctx->duplex_stream); });
1752 if (err != paNoError) {
1753 LOG_IO("portaudio", { Pa_CloseStream(ctx->duplex_stream); });
1754 ctx->duplex_stream = NULL;
1755 log_warn("Full-duplex stream failed to start: %s", Pa_GetErrorText(err));
1756 try_separate = true;
1757 }
1758 } else {
1759 log_warn("Full-duplex stream failed to open: %s", Pa_GetErrorText(err));
1760 try_separate = true;
1761 }
1762 }
1763
1764 if (try_separate) {
1765 // Use separate streams for non-48 kHz devices and single-direction modes.
1766 if (has_output && has_input) {
1767 log_info("Using separate native-rate input/output streams (%.0f Hz in, %.0f Hz out)",
1768 inputInfo->defaultSampleRate, outputInfo->defaultSampleRate);
1769 log_info(" Converting between device rates and the %.0f Hz audio pipeline", (double)AUDIO_SAMPLE_RATE);
1770 } else if (has_output) {
1771 log_debug("Using output-only mode (playback-only for mirror/media)");
1772 } else if (has_input) {
1773 log_info("Using input-only mode (no output device available)");
1774 }
1775
1776 // Store the internal sample rate (buffer rate)
1778
1779 // Create render buffer for AEC3 reference synchronization
1781 if (!ctx->render_buffer) {
1782 return SET_ERRNO(ERROR_MEMORY, "Failed to create render buffer");
1783 }
1784
1785 // Open output stream only if output device exists
1786 bool output_ok = false;
1787 double actual_output_rate = 0;
1788 if (has_output) {
1789 // Use the device's native rate first so conversion stays explicit and stateful.
1790 double native_rate = outputInfo->defaultSampleRate;
1791 if (native_rate <= 0.0) {
1792 native_rate = AUDIO_SAMPLE_RATE;
1793 }
1794 double preferred_rate = native_rate;
1795 double fallback_rate = (preferred_rate == AUDIO_SAMPLE_RATE) ? native_rate : AUDIO_SAMPLE_RATE;
1796
1797 log_debug("Attempting output at native rate %.0f Hz (fallback %.0f Hz)", preferred_rate, fallback_rate);
1798
1799 // Always use output_callback for output streams (both output-only and duplex modes)
1800 // PortAudio will invoke the callback whenever it needs audio data
1801 // The callback reads PCM supplied to the shared playback buffer.
1802 PaStreamCallback *callback = output_callback;
1803
1804 // Try preferred rate first
1805 LOG_IO("portaudio", {
1806 err = Pa_OpenStream(&ctx->output_stream, NULL, &outputParams, preferred_rate, AUDIO_FRAMES_PER_BUFFER,
1807 paClipOff, callback, ctx);
1808 });
1809
1810 if (err == paNoError) {
1811 actual_output_rate = preferred_rate;
1812 output_ok = true;
1813 log_info("Output stream opened at %.0f Hz", preferred_rate);
1814 } else {
1815 log_warn("Failed to open output at %.0f Hz: %s, trying %.0f Hz", preferred_rate, Pa_GetErrorText(err),
1816 fallback_rate);
1817
1818 // If first Pa_OpenStream call left a partial stream, clean it up before retrying
1819 if (ctx->output_stream) {
1820 log_debug("Closing partially-opened output stream from failed preferred rate");
1821 LOG_IO("portaudio", { Pa_CloseStream(ctx->output_stream); });
1822 ctx->output_stream = NULL;
1823 }
1824
1825 // Try the other endpoint rate if the native rate is unavailable.
1826 LOG_IO("portaudio", {
1827 err = Pa_OpenStream(&ctx->output_stream, NULL, &outputParams, fallback_rate, AUDIO_FRAMES_PER_BUFFER,
1828 paClipOff, callback, ctx);
1829 });
1830
1831 if (err == paNoError) {
1832 actual_output_rate = fallback_rate;
1833 output_ok = true;
1834 log_info("Output stream opened at fallback rate %.0f Hz", fallback_rate);
1835 } else {
1836 log_warn("Failed to open output stream at fallback rate %.0f Hz: %s", fallback_rate, Pa_GetErrorText(err));
1837 // Clean up if fallback also failed
1838 if (ctx->output_stream) {
1839 log_debug("Closing partially-opened output stream from failed native rate");
1840 LOG_IO("portaudio", { Pa_CloseStream(ctx->output_stream); });
1841 ctx->output_stream = NULL;
1842 }
1843 }
1844 }
1845
1846 // Store actual output rate for resampling
1847 if (output_ok) {
1848 ctx->output_device_rate = actual_output_rate;
1849 ctx->output_resample_phase = 0.0;
1850 ctx->output_resample_previous = 0.0f;
1851 ctx->output_resample_has_previous = false;
1852 if (actual_output_rate != AUDIO_SAMPLE_RATE) {
1853 log_warn("⚠️ Output rate mismatch: %.0f Hz output vs %.0f Hz input - resampling will be used",
1854 actual_output_rate, (double)AUDIO_SAMPLE_RATE);
1855 }
1856 }
1857 }
1858
1859 // Open input stream only if we have input (skip for playback-only mode)
1860 bool input_ok = !has_input; // If no input, mark as OK (skip)
1861 if (has_input) {
1862 // Open at the device's native rate. The separate input callback converts
1863 // to AUDIO_SAMPLE_RATE before samples enter the shared processing path.
1864 double input_stream_rate = inputInfo->defaultSampleRate;
1865 if (input_stream_rate <= 0.0) {
1866 input_stream_rate = AUDIO_SAMPLE_RATE;
1867 }
1868 LOG_IO("portaudio", {
1869 err = Pa_OpenStream(&ctx->input_stream, &inputParams, NULL, input_stream_rate, AUDIO_FRAMES_PER_BUFFER,
1870 paClipOff, input_callback, ctx);
1871 });
1872 input_ok = (err == paNoError);
1873 if (input_ok) {
1874 ctx->input_device_rate = input_stream_rate;
1875 ctx->input_resample_phase = 0.0;
1876 ctx->input_resample_previous = 0.0f;
1877 ctx->input_resample_has_previous = false;
1878 log_info("Input stream opened at native rate %.0f Hz", input_stream_rate);
1879 }
1880
1881 // If input failed, try device 0 as fallback (HDMI on BeaglePlay)
1882 if (!input_ok) {
1883 log_debug("Input failed - trying device 0 as fallback");
1884
1885 // Clean up partial stream from first attempt before retrying
1886 if (ctx->input_stream) {
1887 log_debug("Closing partially-opened input stream from failed primary device");
1888 LOG_IO("portaudio", { Pa_CloseStream(ctx->input_stream); });
1889 ctx->input_stream = NULL;
1890 }
1891
1892 PaStreamParameters fallback_input_params = inputParams;
1893 fallback_input_params.device = 0;
1894 const PaDeviceInfo *device_0_info = Pa_GetDeviceInfo(0);
1895 if (device_0_info && device_0_info->maxInputChannels > 0) {
1896 LOG_IO("portaudio", {
1897 err = Pa_OpenStream(&ctx->input_stream, &fallback_input_params, NULL, input_stream_rate,
1898 AUDIO_FRAMES_PER_BUFFER, paClipOff, input_callback, ctx);
1899 });
1900 if (err == paNoError) {
1901 log_info("Input stream opened on device 0 (fallback from default)");
1902 input_ok = true;
1903 } else {
1904 log_warn("Fallback also failed on device 0: %s", Pa_GetErrorText(err));
1905 // Clean up if fallback also failed
1906 if (ctx->input_stream) {
1907 log_debug("Closing partially-opened input stream from failed fallback device");
1908 LOG_IO("portaudio", { Pa_CloseStream(ctx->input_stream); });
1909 ctx->input_stream = NULL;
1910 }
1911 }
1912 }
1913 }
1914
1915 if (!input_ok) {
1916 log_warn("Failed to open input stream: %s", Pa_GetErrorText(err));
1917 }
1918 }
1919
1920 // Check if we got at least one stream working
1921 if (!input_ok && !output_ok && !media_only) {
1922 // Neither stream works - fail completely
1924 ctx->render_buffer = NULL;
1925 return SET_ERRNO(ERROR_AUDIO, "Failed to open both input and output streams");
1926 }
1927
1928 // If output failed but input works, we can still send audio to server
1929 if (!output_ok && input_ok) {
1930 log_info("Output stream unavailable - continuing with input-only (can send audio to server)");
1931 ctx->output_stream = NULL;
1932 }
1933 // If input failed but output works, we can still receive audio from server
1934 if (!input_ok && output_ok) {
1935 log_info("Input stream unavailable - continuing with output-only (can receive audio from server)");
1936 ctx->input_stream = NULL;
1937 }
1938
1939 // Start output stream if it's open
1940 if (ctx->output_stream) {
1941 LOG_IO("portaudio", { err = Pa_StartStream(ctx->output_stream); });
1942 if (err != paNoError) {
1943 if (ctx->input_stream) {
1944 LOG_IO("portaudio", { Pa_CloseStream(ctx->input_stream); });
1945 }
1946 LOG_IO("portaudio", { Pa_CloseStream(ctx->output_stream); });
1947 ctx->input_stream = NULL;
1948 ctx->output_stream = NULL;
1950 ctx->render_buffer = NULL;
1951 return SET_ERRNO(ERROR_AUDIO, "Failed to start output stream: %s", Pa_GetErrorText(err));
1952 }
1953 }
1954
1955 // Start input stream if it's open
1956 if (ctx->input_stream) {
1957 LOG_IO("portaudio", { err = Pa_StartStream(ctx->input_stream); });
1958 if (err != paNoError) {
1959 if (ctx->output_stream) {
1960 LOG_IO("portaudio", { Pa_StopStream(ctx->output_stream); });
1961 }
1962 if (ctx->input_stream) {
1963 LOG_IO("portaudio", { Pa_CloseStream(ctx->input_stream); });
1964 }
1965 if (ctx->output_stream) {
1966 LOG_IO("portaudio", { Pa_CloseStream(ctx->output_stream); });
1967 }
1968 ctx->input_stream = NULL;
1969 ctx->output_stream = NULL;
1971 ctx->render_buffer = NULL;
1972 return SET_ERRNO(ERROR_AUDIO, "Failed to start input stream: %s", Pa_GetErrorText(err));
1973 }
1974 }
1975
1976 ctx->separate_streams = true;
1977 log_debug("Separate streams started successfully");
1978 } else {
1979 ctx->separate_streams = false;
1980 log_info("Full-duplex stream started (single callback, perfect AEC3 timing)");
1981 }
1982start_worker:
1983
1985
1986 // Start worker thread for heavy audio processing
1987 if (!ctx->worker_running) {
1989 if (asciichat_thread_create(&ctx->worker_thread, "audio_worker", audio_worker_thread, ctx) != 0) {
1990 // Failed to create worker thread - stop streams and cleanup
1991 if (ctx->duplex_stream) {
1992 LOG_IO("portaudio", {
1993 Pa_StopStream(ctx->duplex_stream);
1994 Pa_CloseStream(ctx->duplex_stream);
1995 });
1996 ctx->duplex_stream = NULL;
1997 }
1998 if (ctx->input_stream) {
1999 LOG_IO("portaudio", {
2000 Pa_StopStream(ctx->input_stream);
2001 Pa_CloseStream(ctx->input_stream);
2002 });
2003 ctx->input_stream = NULL;
2004 }
2005 if (ctx->output_stream) {
2006 LOG_IO("portaudio", {
2007 Pa_StopStream(ctx->output_stream);
2008 Pa_CloseStream(ctx->output_stream);
2009 });
2010 ctx->output_stream = NULL;
2011 }
2013 ctx->render_buffer = NULL;
2014 return SET_ERRNO(ERROR_THREAD, "Failed to create worker thread");
2015 }
2016 ctx->worker_running = true;
2017 log_debug("Worker thread started successfully");
2018 }
2019
2020 // Lock to update final state flags atomically
2021 mutex_lock(&ctx->state_mutex);
2022 ctx->running = true;
2025 return ASCIICHAT_OK;
2026}
2027
2029 if (!ctx || !ctx->initialized) {
2030 return SET_ERRNO(ERROR_INVALID_STATE, "Audio context not initialized");
2031 }
2032
2033 atomic_store_bool(&ctx->shutting_down, true);
2034
2035 // Stop worker thread before stopping streams
2036 if (ctx->worker_running) {
2037 log_debug("Stopping worker thread");
2039 cond_signal(&ctx->worker_cond); // Wake up worker if waiting
2041 ctx->worker_running = false;
2042 log_debug("Worker thread stopped successfully");
2043 }
2044
2045 if (ctx->playback_buffer) {
2047 }
2048
2049 mutex_lock(&ctx->state_mutex);
2050
2051 if (ctx->duplex_stream) {
2052 log_debug("Stopping duplex stream");
2053 PaError err;
2054 LOG_IO("portaudio", { err = Pa_StopStream(ctx->duplex_stream); });
2055 if (err != paNoError) {
2056 log_warn("Pa_StopStream failed: %s", Pa_GetErrorText(err));
2057 }
2058 log_debug("Closing duplex stream");
2059 LOG_IO("portaudio", { err = Pa_CloseStream(ctx->duplex_stream); });
2060 if (err != paNoError) {
2061 log_warn("Pa_CloseStream failed: %s", Pa_GetErrorText(err));
2062 } else {
2063 log_debug("Duplex stream closed successfully");
2064 }
2065 ctx->duplex_stream = NULL;
2066 }
2067
2068 // Stop separate streams if used
2069 if (ctx->input_stream) {
2070 log_debug("Stopping input stream");
2071 PaError err;
2072 LOG_IO("portaudio", { err = Pa_StopStream(ctx->input_stream); });
2073 if (err != paNoError) {
2074 log_warn("Pa_StopStream input failed: %s", Pa_GetErrorText(err));
2075 }
2076 log_debug("Closing input stream");
2077 LOG_IO("portaudio", { err = Pa_CloseStream(ctx->input_stream); });
2078 if (err != paNoError) {
2079 log_warn("Pa_CloseStream input failed: %s", Pa_GetErrorText(err));
2080 } else {
2081 log_debug("Input stream closed successfully");
2082 }
2083 ctx->input_stream = NULL;
2084 }
2085
2086 if (ctx->output_stream) {
2087 log_debug("Stopping output stream");
2088 PaError err;
2089 LOG_IO("portaudio", { err = Pa_StopStream(ctx->output_stream); });
2090 if (err != paNoError) {
2091 log_warn("Pa_StopStream output failed: %s", Pa_GetErrorText(err));
2092 }
2093 log_debug("Closing output stream");
2094 LOG_IO("portaudio", { err = Pa_CloseStream(ctx->output_stream); });
2095 if (err != paNoError) {
2096 log_warn("Pa_CloseStream output failed: %s", Pa_GetErrorText(err));
2097 } else {
2098 log_debug("Output stream closed successfully");
2099 }
2100 ctx->output_stream = NULL;
2101 }
2102
2103 // Cleanup render buffer
2104 if (ctx->render_buffer) {
2106 ctx->render_buffer = NULL;
2107 }
2108
2109 ctx->running = false;
2110 ctx->separate_streams = false;
2112
2113 log_debug("Audio stopped");
2114 return ASCIICHAT_OK;
2115}
2116
2117asciichat_error_t audio_read_samples(audio_context_t *ctx, float *buffer, int num_samples) {
2118 if (!ctx || !ctx->initialized || !buffer || num_samples <= 0) {
2119 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, buffer=%p, num_samples=%d", ctx, buffer,
2120 num_samples);
2121 }
2122
2123 // audio_ring_buffer_read now returns number of samples read, not error code
2124 int samples_read = audio_ring_buffer_read(ctx->capture_buffer, buffer, num_samples);
2125 return (samples_read >= 0) ? ASCIICHAT_OK : ERROR_AUDIO;
2126}
2127
2128asciichat_error_t audio_write_samples(audio_context_t *ctx, const float *buffer, int num_samples) {
2129 if (!ctx || !ctx->initialized || !buffer || num_samples <= 0) {
2130 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, buffer=%p, num_samples=%d", ctx, buffer,
2131 num_samples);
2132 }
2133
2134 // Don't accept new audio data during shutdown - this prevents garbage/beeps
2135 if (atomic_load_bool(&ctx->shutting_down)) {
2136 return ASCIICHAT_OK; // Silently discard
2137 }
2138
2141 asciichat_error_t result = audio_ring_buffer_write(ctx->playback_buffer, buffer, num_samples);
2142
2143 return result;
2144}
2145
2146// Internal helper to list audio devices (input or output)
2147static asciichat_error_t audio_list_devices_internal(audio_device_info_t **out_devices, unsigned int *out_count,
2148 bool list_inputs) {
2149 if (!out_devices || !out_count) {
2150 return SET_ERRNO(ERROR_INVALID_PARAM, "audio_list_devices: invalid parameters");
2151 }
2152
2153 *out_devices = NULL;
2154 *out_count = 0;
2155
2156 // Ensure PortAudio is initialized (centralized initialization)
2157 asciichat_error_t pa_result = audio_ensure_portaudio_initialized();
2158 if (pa_result != ASCIICHAT_OK) {
2159 return pa_result;
2160 }
2161
2162 int num_devices = Pa_GetDeviceCount();
2163 if (num_devices < 0) {
2164 return SET_ERRNO(ERROR_AUDIO, "Failed to get device count: %s", Pa_GetErrorText(num_devices));
2165 }
2166
2167 if (num_devices == 0) {
2168 return ASCIICHAT_OK; // No devices found
2169 }
2170
2171 // Get default device indices
2172 PaDeviceIndex default_input = Pa_GetDefaultInputDevice();
2173 PaDeviceIndex default_output = Pa_GetDefaultOutputDevice();
2174
2175 // First pass: count matching devices
2176 unsigned int device_count = 0;
2177 for (int i = 0; i < num_devices; i++) {
2178 const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
2179 if (info) {
2180 bool matches = list_inputs ? (info->maxInputChannels > 0) : (info->maxOutputChannels > 0);
2181 if (matches) {
2182 device_count++;
2183 }
2184 }
2185 }
2186
2187 if (device_count == 0) {
2188 return ASCIICHAT_OK; // No matching devices
2189 }
2190
2191 // Allocate device array
2192 audio_device_info_t *devices = SAFE_CALLOC(device_count, sizeof(audio_device_info_t), audio_device_info_t *);
2193 if (!devices) {
2194 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate audio device info array");
2195 }
2196
2197 // Second pass: populate device info
2198 unsigned int idx = 0;
2199 for (int i = 0; i < num_devices && idx < device_count; i++) {
2200 const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
2201 if (!info)
2202 continue;
2203
2204 bool match = list_inputs ? (info->maxInputChannels > 0) : (info->maxOutputChannels > 0);
2205 if (!match)
2206 continue;
2207
2208 devices[idx].index = i;
2209 if (info->name) {
2210 SAFE_STRNCPY(devices[idx].name, info->name, AUDIO_DEVICE_NAME_MAX);
2211 } else {
2212 SAFE_STRNCPY(devices[idx].name, "<Unknown>", AUDIO_DEVICE_NAME_MAX);
2213 }
2214 devices[idx].max_input_channels = info->maxInputChannels;
2215 devices[idx].max_output_channels = info->maxOutputChannels;
2216 devices[idx].default_sample_rate = info->defaultSampleRate;
2217 devices[idx].is_default_input = (i == default_input);
2218 devices[idx].is_default_output = (i == default_output);
2219 idx++;
2220 }
2221
2222 *out_devices = devices;
2223 *out_count = idx;
2224 return ASCIICHAT_OK;
2225}
2226
2227asciichat_error_t audio_list_input_devices(audio_device_info_t **out_devices, unsigned int *out_count) {
2228 return audio_list_devices_internal(out_devices, out_count, true);
2229}
2230
2231asciichat_error_t audio_list_output_devices(audio_device_info_t **out_devices, unsigned int *out_count) {
2232 return audio_list_devices_internal(out_devices, out_count, false);
2233}
2234
2236 SAFE_FREE(devices);
2237}
2238
2239asciichat_error_t audio_dequantize_samples(const uint8_t *samples_ptr, uint32_t total_samples, float *out_samples) {
2240 if (!samples_ptr || !out_samples || total_samples == 0) {
2241 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for audio dequantization");
2242 }
2243
2244 for (uint32_t i = 0; i < total_samples; i++) {
2245 uint32_t network_sample;
2246 // Use memcpy to safely handle potential misalignment from packet header
2247 memcpy(&network_sample, samples_ptr + i * sizeof(uint32_t), sizeof(uint32_t));
2248 int32_t scaled = (int32_t)NET_TO_HOST_U32(network_sample);
2249 out_samples[i] = (float)scaled / 2147483647.0f;
2250 }
2251
2252 return ASCIICHAT_OK;
2253}
2254
2256 // Delegate to platform abstraction layer
2258 if (result == ASCIICHAT_OK) {
2259 log_debug("✓ Audio thread real-time priority set successfully");
2260 }
2261 return result;
2262}
2263
2264/* ============================================================================
2265 * Audio Batch Packet Parsing
2266 * ============================================================================
2267 */
2268
2269asciichat_error_t audio_parse_batch_header(const void *data, size_t len, audio_batch_info_t *out_batch) {
2270 if (!data) {
2271 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch header data pointer is NULL");
2272 }
2273
2274 if (!out_batch) {
2275 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch info output pointer is NULL");
2276 }
2277
2278 if (len < sizeof(audio_batch_packet_t)) {
2279 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch header too small (len=%zu, expected=%zu)", len,
2280 sizeof(audio_batch_packet_t));
2281 }
2282
2283 const audio_batch_packet_t *batch_header = (const audio_batch_packet_t *)data;
2284
2285 // Unpack network byte order values to host byte order
2286 out_batch->batch_count = ntohl(batch_header->batch_count);
2287 out_batch->total_samples = ntohl(batch_header->total_samples);
2288 out_batch->sample_rate = ntohl(batch_header->sample_rate);
2289 out_batch->channels = ntohl(batch_header->channels);
2290
2291 return ASCIICHAT_OK;
2292}
2293
2295 if (!batch) {
2296 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch info pointer is NULL");
2297 }
2298
2299 // Validate batch_count
2300 if (batch->batch_count == 0) {
2301 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch count cannot be zero");
2302 }
2303
2304 // Check for reasonable max (256 frames per batch is very generous)
2305 if (batch->batch_count > 256) {
2306 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch count too large (batch_count=%u, max=256)", batch->batch_count);
2307 }
2308
2309 // Validate channels (1=mono, 2=stereo, max 8 for multi-channel)
2310 if (batch->channels == 0 || batch->channels > 8) {
2311 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid channel count (channels=%u, valid=1-8)", batch->channels);
2312 }
2313
2314 // Validate sample rate
2316 return SET_ERRNO(ERROR_INVALID_PARAM, "Unsupported sample rate (sample_rate=%u)", batch->sample_rate);
2317 }
2318
2319 // Check for reasonable sample counts
2320 if (batch->total_samples == 0) {
2321 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch has zero samples");
2322 }
2323
2324 // Each batch typically has samples_per_frame worth of samples
2325 // For 48kHz at 20ms per frame: 48000 * 0.02 = 960 samples per frame
2326 // With max 256 frames, that's up to ~245k samples per batch
2327 if (batch->total_samples > 1000000) {
2328 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch sample count suspiciously large (total_samples=%u)",
2329 batch->total_samples);
2330 }
2331
2332 return ASCIICHAT_OK;
2333}
2334
2336 // List of commonly supported audio sample rates
2337 static const uint32_t supported_rates[] = {
2338 8000, // Telephone quality
2339 16000, // Wideband telephony
2340 24000, // High quality speech
2341 32000, // Good for video
2342 44100, // CD quality (less common in VoIP)
2343 48000, // Standard professional
2344 96000, // High-end professional
2345 192000, // Ultra-high-end mastering
2346 };
2347
2348 const size_t rate_count = sizeof(supported_rates) / sizeof(supported_rates[0]);
2349 for (size_t i = 0; i < rate_count; i++) {
2350 if (sample_rate == supported_rates[i]) {
2351 return true;
2352 }
2353 }
2354
2355 return false;
2356}
2357
2358bool audio_should_enable_microphone(audio_capture_source_t source, bool has_media_audio) {
2359 switch (source) {
2361 return !has_media_audio;
2362
2364 return true;
2365
2367 return false;
2368
2370 return true;
2371
2373 return false;
2374
2375 default:
2376 return !has_media_audio;
2377 }
2378}
Platform abstraction layer umbrella header providing unified cross-platform API.
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
uint64_t atomic_fetch_add_u64(atomic_t *a, uint64_t delta)
Atomically add to a uint64_t and return the previous value.
Definition atomic.c:248
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
bool atomic_exchange_bool(atomic_t *a, bool new_value)
Atomically exchange a boolean and return the old value.
Definition atomic.c:303
⚛️ Atomic operations abstraction layer with debug tracking
🗃️ Lock-Free Unified Memory Buffer Pool with Lazy Allocation
void client_audio_pipeline_process_duplex(client_audio_pipeline_t *pipeline, const float *render_samples, int render_count, const float *capture_samples, int capture_count, float *processed_output)
Process AEC3 inline in full-duplex callback.
Unified client-side audio processing pipeline.
⚙️ Common definitions, error codes, macros, and types shared throughout the application
struct audio_ring_buffer audio_ring_buffer_t
Named object registry for debugging — log identifiable resource names.
🔄 Network byte order conversion helpers
#define NET_TO_HOST_U32(val)
Definition endian.h:81
bool audio_should_enable_microphone(audio_capture_source_t source, bool has_media_audio)
Determine if microphone should be enabled based on audio source setting and media state.
size_t audio_ring_buffer_read(audio_ring_buffer_t *rb, float *data, size_t samples)
Read audio samples from ring buffer.
void audio_free_device_list(audio_device_info_t *devices)
Free device list allocated by audio_list_input_devices/audio_list_output_devices.
asciichat_error_t audio_ring_buffer_write(audio_ring_buffer_t *rb, const float *data, int samples)
Write audio samples to ring buffer.
#define AUDIO_SAMPLE_RATE
Audio sample rate (48kHz professional quality, Opus-compatible)
void audio_ring_buffer_destroy(audio_ring_buffer_t *rb)
Destroy an audio ring buffer.
asciichat_error_t audio_init(audio_context_t *ctx)
Initialize audio context and PortAudio.
asciichat_error_t audio_set_realtime_priority(void)
Request real-time priority for current thread.
audio_ring_buffer_t * audio_ring_buffer_create(void)
Create a new audio ring buffer (for playback with jitter buffering)
bool audio_is_supported_sample_rate(uint32_t sample_rate)
Check if a sample rate is a standard/supported rate.
#define AUDIO_FRAMES_PER_BUFFER
Audio frames per buffer (480 = 10ms at 48kHz, matches WebRTC AEC3 frame size)
size_t audio_ring_buffer_available_read(audio_ring_buffer_t *rb)
Get number of samples available for reading.
asciichat_error_t audio_start_duplex(audio_context_t *ctx)
Start full-duplex audio (simultaneous capture and playback)
void audio_ring_buffer_register_atomics(audio_ring_buffer_t *rb, const char *context_name)
Register audio ring buffer atomic fields for debug tracking.
asciichat_error_t audio_parse_batch_header(const void *data, size_t len, audio_batch_info_t *out_batch)
Parse an audio batch packet header from raw packet data.
void audio_destroy(audio_context_t *ctx)
Destroy audio context and clean up resources.
asciichat_error_t audio_stop_duplex(audio_context_t *ctx)
Stop full-duplex audio.
void audio_ring_buffer_unregister_atomics(audio_ring_buffer_t *rb)
Unregister audio ring buffer atomic fields.
asciichat_error_t audio_write_samples(audio_context_t *ctx, const float *buffer, int num_samples)
Write audio samples to playback buffer.
void audio_terminate_portaudio_final(void)
Terminate PortAudio and free all device resources.
#define AUDIO_CHANNELS
Number of audio channels (1 = mono)
void audio_flush_playback_buffers(audio_context_t *ctx)
Flush all audio buffers to synchronize with seek operations.
void audio_set_pipeline(audio_context_t *ctx, void *pipeline)
Set audio pipeline for echo cancellation.
audio_ring_buffer_t * audio_ring_buffer_create_for_capture(void)
Create a new audio ring buffer for capture (without jitter buffering)
size_t audio_ring_buffer_peek(audio_ring_buffer_t *rb, float *data, size_t samples)
Peek at available samples without consuming them (for AEC3 render signal)
asciichat_error_t audio_validate_batch_params(const audio_batch_info_t *batch)
Validate audio batch parameters for sanity.
asciichat_error_t audio_list_input_devices(audio_device_info_t **out_devices, unsigned int *out_count)
List available audio input devices (microphones)
asciichat_error_t audio_dequantize_samples(const uint8_t *samples_ptr, uint32_t total_samples, float *out_samples)
Dequantize network audio samples from int32 to float.
asciichat_error_t audio_list_output_devices(audio_device_info_t **out_devices, unsigned int *out_count)
List available audio output devices (speakers)
void audio_ring_buffer_clear(audio_ring_buffer_t *rb)
Clear all audio samples from ring buffer.
asciichat_error_t audio_read_samples(audio_context_t *ctx, float *buffer, int num_samples)
Read captured audio samples from capture buffer.
void resample_linear(const float *src, size_t src_samples, float *dst, size_t dst_samples, double src_rate, double dst_rate)
Resample audio using linear interpolation.
size_t audio_ring_buffer_available_write(audio_ring_buffer_t *rb)
Get number of sample slots available for writing.
void buffer_pool_free(buffer_pool_t *pool, const void *data, size_t size)
Free a buffer back to the pool (lock-free)
void * buffer_pool_alloc(buffer_pool_t *pool, size_t size)
Allocate a buffer from the pool (lock-free fast path)
unsigned int uint32_t
Definition common.h:58
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MEMSET(dest, dest_size, ch, count)
Definition common.h:469
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define SAFE_MEMCPY(dest, dest_size, src, count)
Definition common.h:468
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
#define NAMED_REGISTER_COND(cond, name, parent_ptr)
Register a condition variable with automatic format specifier.
#define NAMED_REGISTER(ptr, name, type, fmt, parent_ptr)
Register any pointer with base name, type, format spec, and location (auto-suffix)
#define NAMED_REGISTER_AUDIO_RINGBUF(buf, name, parent_ptr)
Register an audio ring buffer with automatic format specifier.
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
#define NAMED_REGISTER_MUTEX(mutex, name, parent_ptr)
Register a mutex with automatic format specifier.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_INVALID_STATE
@ ERROR_AUDIO
Definition error_codes.h:67
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
@ ERROR_BUFFER
@ ERROR_THREAD
#define LOG_RATE_FAST
Log rate limit: 1 second (1,000,000 microseconds)
Definition log_rates.h:26
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
size_t media_source_read_audio(media_source_t *source, float *buffer, size_t num_samples)
Read audio samples from media source.
Definition source.c:652
bool media_source_has_audio(media_source_t *source)
Check if media source has audio stream.
Definition source.c:737
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424
#define NS_PER_MS_INT
Definition time.h:156
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition util/time.c:150
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
asciichat_error_t asciichat_thread_set_realtime_priority(void)
Set the current thread to real-time priority.
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
const char * platform_getenv(const char *name)
Get an environment variable value.
Definition wasm/system.c:39
int cond_signal(cond_t *cond)
Signal a condition variable (wake one waiting thread)
int cond_init(cond_t *cond, const char *name)
Initialize a condition variable with a name.
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
#define cond_timedwait(cond, mutex, timeout_ns)
Wait on a condition variable with timeout (with debug tracking in debug builds)
Definition cond.h:300
int cond_destroy(cond_t *cond)
Destroy a condition variable.
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
#define AUDIO_JITTER_HIGH_WATER_MARK
High water mark - drop OLD samples when buffer exceeds this.
Definition ringbuffer.h:169
#define AUDIO_JITTER_BUFFER_THRESHOLD
Jitter buffer threshold - samples needed before starting playback.
Definition ringbuffer.h:143
#define AUDIO_RING_BUFFER_SIZE
Audio ring buffer size in samples (192000 samples = 4 seconds @ 48kHz)
Definition ringbuffer.h:128
#define AUDIO_JITTER_LOW_WATER_MARK
Low water mark - warn when available drops below this.
Definition ringbuffer.h:156
#define AUDIO_JITTER_TARGET_LEVEL
Target buffer level after dropping old samples.
Definition ringbuffer.h:179
#define AUDIO_CROSSFADE_SAMPLES
Crossfade duration in samples for smooth underrun recovery.
Definition ringbuffer.h:186
🔊 Audio Capture and Playback Interface for ascii-chat
#define AUDIO_DEVICE_NAME_MAX
Maximum length of audio device name.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
Platform initialization and static synchronization helpers.
Capture stdout/stderr and redirect to logging system.
#define LOG_IO(prefix, block)
Capture output from a code block and log it.
Definition io.h:81
#define WORKER_BATCH_FRAMES
void audio_ring_buffer_discard_pending(audio_ring_buffer_t *rb)
#define WORKER_BATCH_SAMPLES
#define WORKER_TIMEOUT_MS
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
#define asciichat_thread_join(thread, timeout_ms)
bool lifecycle_is_initialized(const lifecycle_t *lc)
Definition lifecycle.c:155
bool lifecycle_init(lifecycle_t *lc, const char *name)
Definition lifecycle.c:26
bool lifecycle_shutdown(lifecycle_t *lc)
Definition lifecycle.c:108
Lock-free module lifecycle state machine using pure stdatomic.
#define LIFECYCLE_INIT
Static initializer for module-global lifecycle variables (no sync primitive)
Definition lifecycle.h:68
📝 Logging API with multiple log levels and terminal output control
#define log_info_every(interval_us, fmt,...)
Rate-limited INFO logging.
Definition log/log.h:705
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:708
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:699
🔢 Mathematical Utility Functions
struct PaStreamCallbackTimeInfo PaStreamCallbackTimeInfo
Definition portaudio.h:16
#define paNoError
Definition portaudio.h:19
int PaError
Definition portaudio.h:14
unsigned long PaStreamCallbackFlags
Definition portaudio.h:15
void audio_recording_submit(audio_recording_source_t source, const float *samples, size_t count, uint64_t start_ns)
Definition recording.c:61
@ AUDIO_RECORDING_MIC
Definition recording.h:10
@ AUDIO_RECORDING_REMOTE
Definition recording.h:12
@ AUDIO_RECORDING_MEDIA
Definition recording.h:11
🎬 Unified media source abstraction for webcam, files, and stdin
Parsed audio batch packet header information.
uint32_t batch_count
Number of audio frames in this batch.
uint32_t channels
Number of channels (1=mono, 2=stereo)
uint32_t sample_rate
Sample rate in Hz (e.g., 48000)
uint32_t total_samples
Total number of samples across all frames.
Audio batch packet structure (Packet Type 28)
Definition packet.h:904
uint32_t channels
Number of audio channels (1=mono, 2=stereo)
Definition packet.h:912
uint32_t batch_count
Number of audio chunks in this batch (usually AUDIO_BATCH_COUNT = 32)
Definition packet.h:906
uint32_t sample_rate
Sample rate in Hz (e.g., 44100, 48000)
Definition packet.h:910
uint32_t total_samples
Total audio samples across all chunks (typically 8192)
Definition packet.h:908
Audio context for full-duplex capture and playback.
bool initialized
True if context has been initialized.
audio_ring_buffer_t * raw_render_rb
Callback → Worker: Raw speaker samples (AEC3 reference)
double input_device_rate
Native sample rate of input device.
audio_ring_buffer_t * playback_buffer
OLD: Network → Worker (will become network_playback_rb)
float * worker_render_batch
Pre-allocated buffer for batch render processing.
float input_resample_previous
Last input sample used at a block boundary.
PaStream * input_stream
Separate input stream (when full-duplex unavailable)
bool worker_running
True if worker thread is running.
atomic_t shutting_down
True when shutdown started - callback outputs silence.
bool monitor_local_media
Play local file audio in solo sessions.
cond_t worker_cond
Condition variable for worker signaling.
atomic_t worker_should_stop
Signal worker thread to exit.
bool input_resample_has_previous
Whether a prior input block has been processed.
void * capture_media_source
Borrowed local media audio; configure before starting duplex.
bool separate_streams
True if using separate input/output streams.
double sample_rate
Actual sample rate of streams (48kHz)
audio_ring_buffer_t * capture_buffer
OLD: Worker → Network (will become processed_capture_rb)
void * audio_pipeline
Client audio pipeline for AEC3 echo cancellation (opaque pointer)
bool running
True if duplex stream is active.
float output_resample_previous
Last source sample used at an output boundary.
audio_ring_buffer_t * render_buffer
Ring buffer for render reference (separate streams mode)
audio_ring_buffer_t * raw_capture_rb
Callback → Worker: Raw mic samples (no processing)
double output_device_rate
Native sample rate of output device.
float * worker_playback_batch
Pre-allocated buffer for batch playback processing.
double output_resample_phase
48 kHz source position carried between output callbacks
mutex_t worker_mutex
Mutex protecting worker state.
mutex_t state_mutex
Mutex protecting context state.
asciichat_thread_t worker_thread
Worker thread for audio processing.
float * worker_capture_batch
Pre-allocated buffer for batch capture processing.
bool playback_only
True for playback-only mode (mirror), skip microphone capture.
audio_ring_buffer_t * processed_playback_rb
Worker → Callback: Ready for speakers.
PaStream * duplex_stream
PortAudio full-duplex stream (simultaneous input+output)
double input_resample_phase
Source position carried between native-rate input blocks.
PaStream * output_stream
Separate output stream (when full-duplex unavailable)
bool output_resample_has_previous
Whether a prior output block has been processed.
Audio device information structure.
int max_output_channels
Maximum output channels (0 if input only)
int max_input_channels
Maximum input channels (0 if output only)
int index
PortAudio device index.
double default_sample_rate
Default sample rate in Hz.
bool is_default_output
True if this is the default output device.
bool is_default_input
True if this is the default input device.
Audio ring buffer for real-time audio streaming.
Definition ringbuffer.h:205
atomic_t discard_pending
Consumer should discard queued samples before its next read.
Definition ringbuffer.h:213
atomic_t discard_to_index
Producer position at which the stale queue ends.
Definition ringbuffer.h:215
float last_sample
Last sample value for smooth fade-out during underrun - NOT atomic (only written by reader)
Definition ringbuffer.h:223
atomic_t crossfade_fade_in
True if we're fading in (recovering from underrun)
Definition ringbuffer.h:221
atomic_t jitter_buffer_filled
True after initial jitter buffer fill.
Definition ringbuffer.h:217
atomic_t read_index
Read index (consumer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:211
mutex_t mutex
Mutex for SLOW PATH only (clear/destroy operations, not regular read/write)
Definition ringbuffer.h:229
bool jitter_buffer_enabled
Whether jitter buffering is enabled (false for capture, true for playback)
Definition ringbuffer.h:227
atomic_t underrun_count
Count of underrun events for diagnostics.
Definition ringbuffer.h:225
atomic_t write_index
Write index (producer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:209
atomic_t crossfade_samples_remaining
Samples remaining in crossfade (0 = no crossfade active)
Definition ringbuffer.h:219
float data[192000]
Audio sample data buffer.
Definition ringbuffer.h:207
⏱️ High-precision timing utilities using sokol_time.h and uthash
void audio_visualization_submit(audio_visualization_source_t source, const float *samples, size_t count)
@ AUDIO_VISUALIZATION_SOURCE_MEDIA
@ AUDIO_VISUALIZATION_SOURCE_MIC
@ AUDIO_VISUALIZATION_SOURCE_REMOTE