8#include <ascii-chat/audio/audio.h>
9#include <ascii-chat/audio/client_audio_pipeline.h>
10#include <ascii-chat/util/endian.h>
11#include <ascii-chat/common.h>
12#include <ascii-chat/util/endian.h>
13#include <ascii-chat/util/time.h>
14#include <ascii-chat/asciichat_errno.h>
15#include <ascii-chat/buffer_pool.h>
16#include <ascii-chat/options/options.h>
17#include <ascii-chat/platform/init.h>
18#include <ascii-chat/platform/abstraction.h>
19#include <ascii-chat/network/packet.h>
20#include <ascii-chat/log/logging.h>
21#include <ascii-chat/media/source.h>
37static unsigned int g_pa_init_refcount = 0;
38static static_mutex_t g_pa_refcount_mutex = STATIC_MUTEX_INIT;
41static int g_pa_init_count = 0;
42static int g_pa_terminate_count = 0;
53static asciichat_error_t audio_ensure_portaudio_initialized(
void) {
54 static_mutex_lock(&g_pa_refcount_mutex);
57 if (g_pa_init_refcount > 0) {
59 static_mutex_unlock(&g_pa_refcount_mutex);
68 platform_stderr_redirect_handle_t stdio_handle = platform_stdout_stderr_redirect_to_null();
74 platform_stdout_stderr_restore(stdio_handle);
77 static_mutex_unlock(&g_pa_refcount_mutex);
78 return SET_ERRNO(ERROR_AUDIO,
"Failed to initialize PortAudio: %s", Pa_GetErrorText(err));
81 g_pa_init_refcount = 1;
82 static_mutex_unlock(&g_pa_refcount_mutex);
95static void audio_release_portaudio(
void) {
96 static_mutex_lock(&g_pa_refcount_mutex);
98 if (g_pa_init_refcount > 0) {
100 log_debug(
"PortAudio refcount decremented to %u", g_pa_init_refcount);
102 log_warn(
"audio_release_portaudio() called but refcount is already 0");
105 static_mutex_unlock(&g_pa_refcount_mutex);
115 static_mutex_lock(&g_pa_refcount_mutex);
117 if (g_pa_init_refcount == 0 && g_pa_init_count > 0 && g_pa_terminate_count == 0) {
118 log_debug(
"[PORTAUDIO_TERM] Calling Pa_Terminate() to release PortAudio");
121 g_pa_terminate_count++;
123 log_debug(
"[PORTAUDIO_TERM] Pa_Terminate() returned: %s", Pa_GetErrorText(err));
126 static_mutex_unlock(&g_pa_refcount_mutex);
132#define WORKER_BATCH_FRAMES 128
133#define WORKER_BATCH_SAMPLES (WORKER_BATCH_FRAMES * AUDIO_CHANNELS)
134#define WORKER_TIMEOUT_MS 1
161static void *audio_worker_thread(
void *arg) {
162 audio_context_t *ctx = (audio_context_t *)arg;
163 log_debug(
"Audio worker thread started (batch size: %d frames = %d samples)",
WORKER_BATCH_FRAMES,
167 static int bypass_aec3_worker = -1;
168 if (bypass_aec3_worker == -1) {
170 bypass_aec3_worker = (env && (strcmp(env,
"1") == 0 || strcmp(env,
"true") == 0)) ? 1 : 0;
171 if (bypass_aec3_worker) {
172 log_warn(
"Worker thread: AEC3 BYPASSED via BYPASS_AEC3=1 (worker will skip AEC3 processing)");
177 static uint64_t loop_count = 0;
178 static uint64_t timeout_count = 0;
179 static uint64_t signal_count = 0;
180 static uint64_t process_count = 0;
183 static double total_wait_ns = 0;
184 static double total_capture_ns = 0;
185 static double total_playback_ns = 0;
186 static double max_wait_ns = 0;
187 static double max_capture_ns = 0;
188 static double max_playback_ns = 0;
192 START_TIMER(
"worker_loop_iteration");
196 bool is_output_only = ctx->output_stream && !ctx->input_stream && !ctx->duplex_stream;
199 if (!is_output_only) {
201 mutex_lock(&ctx->worker_mutex);
202 START_TIMER(
"worker_cond_wait");
203 wait_result = cond_timedwait(&ctx->worker_cond, &ctx->worker_mutex,
WORKER_TIMEOUT_MS * NS_PER_MS_INT);
204 double wait_time_ns = STOP_TIMER(
"worker_cond_wait");
205 mutex_unlock(&ctx->worker_mutex);
207 total_wait_ns += wait_time_ns;
208 if (wait_time_ns > max_wait_ns)
209 max_wait_ns = wait_time_ns;
218 if (atomic_load(&ctx->worker_should_stop)) {
219 log_debug(
"Worker thread received shutdown signal");
224 if (wait_result == 0) {
236 if (loop_count % 100 == 0) {
237 char avg_wait_str[32], max_wait_str[32];
238 char avg_capture_str[32], max_capture_str[32];
239 char avg_playback_str[32], max_playback_str[32];
242 format_duration_ns(total_capture_ns / (process_count > 0 ? process_count : 1), avg_capture_str,
243 sizeof(avg_capture_str));
245 format_duration_ns(total_playback_ns / (process_count > 0 ? process_count : 1), avg_playback_str,
246 sizeof(avg_playback_str));
249 log_info(
"Worker stats: loops=%lu, signals=%lu, timeouts=%lu, processed=%lu", loop_count, signal_count,
250 timeout_count, process_count);
251 log_info(
"Worker timing: wait avg=%s max=%s, capture avg=%s max=%s, playback avg=%s max=%s", avg_wait_str,
252 max_wait_str, avg_capture_str, max_capture_str, avg_playback_str, max_playback_str);
253 log_info(
"Worker buffers: capture=%zu, render=%zu, playback=%zu (need >= %d to process)", capture_available,
257 if (wait_result != 0 && capture_available == 0 && playback_available == 0) {
259 STOP_TIMER(
"worker_loop_iteration");
268 const size_t MIN_PROCESS_SAMPLES = 64;
269 if (capture_available >= MIN_PROCESS_SAMPLES) {
270 START_TIMER(
"worker_capture_processing");
275 size_t capture_read =
audio_ring_buffer_read(ctx->raw_capture_rb, ctx->worker_capture_batch, samples_to_process);
277 if (capture_read > 0) {
279 if (!bypass_aec3_worker && ctx->audio_pipeline && render_available >= capture_read) {
283 if (render_read > 0) {
290 ctx->worker_capture_batch, (
int)capture_read,
291 ctx->worker_capture_batch);
296 static int aec3_count = 0;
297 static long aec3_total_ns = 0;
298 static long aec3_max_ns = 0;
300 aec3_total_ns += aec3_ns;
301 if (aec3_ns > aec3_max_ns)
302 aec3_max_ns = aec3_ns;
304 if (aec3_count % 100 == 0) {
305 long avg_ns = aec3_total_ns / aec3_count;
306 log_info(
"AEC3 performance: avg=%.2fms, max=%.2fms, latest=%.2fms (samples=%zu, %d calls)",
307 avg_ns / NS_PER_MS, aec3_max_ns / NS_PER_MS, aec3_ns / NS_PER_MS, capture_read, aec3_count);
316 float mic_sensitivity = GET_OPTION(microphone_sensitivity);
317 if (mic_sensitivity != 1.0f) {
319 if (mic_sensitivity < 0.0f)
320 mic_sensitivity = 0.0f;
321 if (mic_sensitivity > 1.0f)
322 mic_sensitivity = 1.0f;
324 for (
size_t i = 0; i < capture_read; i++) {
325 ctx->worker_capture_batch[i] *= mic_sensitivity;
332 log_debug_every(NS_PER_MS_INT,
"Worker processed %zu capture samples (AEC3 %s)", capture_read,
338 double capture_time_ns = STOP_TIMER(
"worker_capture_processing");
339 total_capture_ns += capture_time_ns;
340 if (capture_time_ns > max_capture_ns)
341 max_capture_ns = capture_time_ns;
347 (void)playback_available;
350 double loop_time_ns = STOP_TIMER(
"worker_loop_iteration");
351 static double total_loop_ns = 0;
352 static double max_loop_ns = 0;
353 total_loop_ns += loop_time_ns;
354 if (loop_time_ns > max_loop_ns)
355 max_loop_ns = loop_time_ns;
357 if (loop_count % 100 == 0) {
358 char avg_loop_str[32], max_loop_str[32];
361 log_info(
"Worker loop timing: avg=%s max=%s", avg_loop_str, max_loop_str);
365 log_debug(
"Audio worker thread exiting");
383static int duplex_callback(
const void *inputBuffer,
void *outputBuffer,
unsigned long framesPerBuffer,
388 static uint64_t duplex_invoke_count = 0;
389 duplex_invoke_count++;
390 if (duplex_invoke_count == 1) {
391 log_warn(
"!!! DUPLEX_CALLBACK INVOKED FOR FIRST TIME !!!");
394 START_TIMER(
"duplex_callback");
396 static uint64_t total_callbacks = 0;
398 if (total_callbacks == 1) {
399 log_warn(
"FIRST CALLBACK RECEIVED! total=%llu frames=%lu", (
unsigned long long)total_callbacks, framesPerBuffer);
402 audio_context_t *ctx = (audio_context_t *)userData;
404 SET_ERRNO(ERROR_INVALID_PARAM,
"duplex_callback: ctx is NULL");
408 log_info_every(100 * NS_PER_MS_INT,
"CB_START: ctx=%p output=%p inputBuffer=%p", (
void *)ctx, (
void *)outputBuffer,
411 const float *input = (
const float *)inputBuffer;
412 float *output = (
float *)outputBuffer;
413 size_t num_samples = framesPerBuffer * AUDIO_CHANNELS;
415 static uint64_t audio_callback_debug_count = 0;
416 audio_callback_debug_count++;
417 if (audio_callback_debug_count <= 10 || audio_callback_debug_count % 100 == 0) {
418 log_info(
"AUDIO_CALLBACK #%lu: frames=%lu samples=%zu media_source=%p", audio_callback_debug_count, framesPerBuffer,
419 num_samples, (
void *)ctx->media_source);
423 if (atomic_load(&ctx->shutting_down)) {
425 SAFE_MEMSET(output, num_samples *
sizeof(
float), 0, num_samples *
sizeof(
float));
427 STOP_TIMER(
"duplex_callback");
432 if (statusFlags != 0) {
433 if (statusFlags & paOutputUnderflow) {
434 log_warn_every(LOG_RATE_FAST,
"PortAudio output underflow");
436 if (statusFlags & paInputOverflow) {
437 log_warn_every(LOG_RATE_FAST,
"PortAudio input overflow");
442 static uint64_t total_samples_read_local = 0;
443 static uint64_t underrun_count_local = 0;
447 size_t samples_read = 0;
451 if (ctx->media_source) {
454 static uint64_t cb_count = 0;
456 if (cb_count <= 5 || cb_count % 500 == 0) {
457 log_info(
"Callback #%lu: media_source path, read %zu samples", cb_count, samples_read);
459 }
else if (ctx->playback_buffer) {
463 static uint64_t playback_count = 0;
465 if (playback_count <= 5 || playback_count % 500 == 0) {
466 log_info(
"Callback #%lu: playback_buffer path, read %zu samples", playback_count, samples_read);
469 static uint64_t null_count = 0;
470 if (++null_count == 1) {
471 log_warn(
"Callback: BOTH media_source AND playback_buffer are NULL!");
475 total_samples_read_local += samples_read;
477 if (samples_read < num_samples) {
479 SAFE_MEMSET(output + samples_read, (num_samples - samples_read) *
sizeof(
float), 0,
480 (num_samples - samples_read) *
sizeof(
float));
481 if (ctx->media_source) {
482 log_debug_every(5 * NS_PER_MS_INT,
"Media playback: got %zu/%zu samples", samples_read, num_samples);
484 log_debug_every(NS_PER_MS_INT,
"Network playback underrun: got %zu/%zu samples", samples_read, num_samples);
485 underrun_count_local++;
490 if (samples_read > 0) {
491 float speaker_volume = GET_OPTION(speakers_volume);
493 if (speaker_volume < 0.0f) {
494 speaker_volume = 0.0f;
495 }
else if (speaker_volume > 1.0f) {
496 speaker_volume = 1.0f;
499 if (speaker_volume != 1.0f) {
500 log_debug_every(48000,
"Applying audio volume %.1f%% to %zu samples", speaker_volume * 100.0, samples_read);
501 for (
size_t i = 0; i < samples_read; i++) {
502 output[i] *= speaker_volume;
505 log_debug_every(48000,
"Audio at 100%% volume, no scaling needed");
514 if (!ctx->playback_only && input && ctx->raw_capture_rb) {
516 }
else if (ctx->playback_only && input) {
525 if (output && ctx->raw_render_rb) {
531 cond_signal(&ctx->worker_cond);
534 double callback_time_ns = STOP_TIMER(
"duplex_callback");
535 static double total_callback_ns = 0;
536 static double max_callback_ns = 0;
537 static uint64_t callback_count = 0;
540 total_callback_ns += callback_time_ns;
541 if (callback_time_ns > max_callback_ns)
542 max_callback_ns = callback_time_ns;
544 if (callback_count % 500 == 0) {
545 char avg_str[32], max_str[32];
548 log_info(
"Duplex callback timing: count=%lu, avg=%s, max=%s (budget: 2ms)", callback_count, avg_str, max_str);
549 log_info(
"Playback stats: total_samples_read=%lu, underruns=%lu, read_success_rate=%.1f%%",
550 total_samples_read_local, underrun_count_local,
551 100.0 * (
double)(callback_count - underrun_count_local) / (
double)callback_count);
554 if (output && num_samples >= 4) {
555 log_info(
"Output sample check: first4=[%.4f, %.4f, %.4f, %.4f] (verifying audio is not silent)", output[0],
556 output[1], output[2], output[3]);
574void resample_linear(
const float *src,
size_t src_samples,
float *dst,
size_t dst_samples,
double src_rate,
576 if (src_samples == 0 || dst_samples == 0) {
577 SAFE_MEMSET(dst, dst_samples *
sizeof(
float), 0, dst_samples *
sizeof(
float));
581 double ratio = src_rate / dst_rate;
583 for (
size_t i = 0; i < dst_samples; i++) {
584 double src_pos = (double)i * ratio;
585 size_t idx0 = (size_t)src_pos;
586 size_t idx1 = idx0 + 1;
587 double frac = src_pos - (double)idx0;
590 if (idx0 >= src_samples)
591 idx0 = src_samples - 1;
592 if (idx1 >= src_samples)
593 idx1 = src_samples - 1;
596 dst[i] = (float)((1.0 - frac) * src[idx0] + frac * src[idx1]);
611static int output_callback(
const void *inputBuffer,
void *outputBuffer,
unsigned long framesPerBuffer,
617 static uint64_t output_cb_invoke_count = 0;
618 output_cb_invoke_count++;
619 if (output_cb_invoke_count == 1) {
620 log_warn(
"!!! OUTPUT_CALLBACK INVOKED FOR FIRST TIME !!!");
623 audio_context_t *ctx = (audio_context_t *)userData;
624 float *output = (
float *)outputBuffer;
625 size_t num_samples = framesPerBuffer * AUDIO_CHANNELS;
627 static uint64_t output_cb_count = 0;
629 if (output_cb_count == 1) {
630 log_warn(
"FIRST OUTPUT_CALLBACK! frames=%lu ctx->media_source=%p", framesPerBuffer, (
void *)ctx->media_source);
634 if (atomic_load(&ctx->shutting_down)) {
636 SAFE_MEMSET(output, num_samples *
sizeof(
float), 0, num_samples *
sizeof(
float));
641 if (statusFlags & paOutputUnderflow) {
642 log_warn_every(LOG_RATE_FAST,
"PortAudio output underflow (separate stream)");
646 size_t samples_read = 0;
648 if (ctx->media_source) {
651 if (output_cb_count <= 3) {
652 log_warn(
"OUTPUT_CB: media_source path, read %zu samples", samples_read);
654 }
else if (ctx->processed_playback_rb) {
657 if (output_cb_count <= 3) {
658 log_warn(
"OUTPUT_CB: processed_playback_rb path, read %zu samples", samples_read);
660 }
else if (ctx->playback_buffer) {
663 if (output_cb_count <= 3) {
664 log_warn(
"OUTPUT_CB: playback_buffer path, read %zu samples", samples_read);
667 if (output_cb_count <= 3) {
668 log_warn(
"OUTPUT_CB: NO BUFFERS! media_source=%p processed_rb=%p playback_buf=%p", (
void *)ctx->media_source,
669 (
void *)ctx->processed_playback_rb, (
void *)ctx->playback_buffer);
674 if (samples_read > 0) {
675 float speaker_volume = GET_OPTION(speakers_volume);
677 if (speaker_volume < 0.0f) {
678 speaker_volume = 0.0f;
679 }
else if (speaker_volume > 1.0f) {
680 speaker_volume = 1.0f;
683 if (speaker_volume != 1.0f) {
684 log_debug_every(48000,
"OUTPUT_CALLBACK: Applying volume %.0f%% to %zu samples", speaker_volume * 100.0,
686 for (
size_t i = 0; i < samples_read; i++) {
687 output[i] *= speaker_volume;
693 if (samples_read < num_samples) {
694 SAFE_MEMSET(output + samples_read, (num_samples - samples_read) *
sizeof(
float), 0,
695 (num_samples - samples_read) *
sizeof(
float));
699 if (ctx->render_buffer && samples_read > 0) {
705 cond_signal(&ctx->worker_cond);
723static int input_callback(
const void *inputBuffer,
void *outputBuffer,
unsigned long framesPerBuffer,
728 static uint64_t input_invoke_count = 0;
729 input_invoke_count++;
730 if (input_invoke_count == 1) {
731 log_warn(
"!!! INPUT_CALLBACK INVOKED FOR FIRST TIME !!!");
734 audio_context_t *ctx = (audio_context_t *)userData;
735 const float *input = (
const float *)inputBuffer;
736 size_t num_samples = framesPerBuffer * AUDIO_CHANNELS;
739 static uint64_t callback_count = 0;
740 static uint64_t last_log_time_ns = 0;
745 if (last_log_time_ns == 0) {
746 last_log_time_ns = now_ns;
748 long elapsed_ms = (long)time_ns_to_ms(
time_elapsed_ns(last_log_time_ns, now_ns));
749 if (elapsed_ms >= 1000) {
750 log_info(
"Input callback: %lu calls/sec, %lu frames/call, %zu samples/call", callback_count, framesPerBuffer,
753 last_log_time_ns = now_ns;
758 if (atomic_load(&ctx->shutting_down)) {
762 if (statusFlags & paInputOverflow) {
763 log_warn_every(LOG_RATE_FAST,
"PortAudio input overflow (separate stream)");
768 if (!ctx->playback_only && input && ctx->raw_capture_rb) {
774 if (ctx->render_buffer && ctx->raw_render_rb) {
777 if (render_available >= num_samples) {
779 float render_temp[AUDIO_BUFFER_SIZE];
781 if (render_read > 0) {
788 cond_signal(&ctx->worker_cond);
794static audio_ring_buffer_t *audio_ring_buffer_create_internal(
bool jitter_buffer_enabled);
796static audio_ring_buffer_t *audio_ring_buffer_create_internal(
bool jitter_buffer_enabled) {
797 size_t rb_size =
sizeof(audio_ring_buffer_t);
798 audio_ring_buffer_t *rb = (audio_ring_buffer_t *)
buffer_pool_alloc(NULL, rb_size);
801 SET_ERRNO(ERROR_MEMORY,
"Failed to allocate audio ring buffer from buffer pool");
805 SAFE_MEMSET(rb->data,
sizeof(rb->data), 0,
sizeof(rb->data));
810 rb->jitter_buffer_filled = !jitter_buffer_enabled;
811 rb->crossfade_samples_remaining = 0;
812 rb->crossfade_fade_in =
false;
813 rb->last_sample = 0.0f;
814 rb->underrun_count = 0;
815 rb->jitter_buffer_enabled = jitter_buffer_enabled;
818 SET_ERRNO(ERROR_THREAD,
"Failed to initialize audio ring buffer mutex");
827 return audio_ring_buffer_create_internal(
true);
831 return audio_ring_buffer_create_internal(
false);
846 mutex_lock(&rb->mutex);
850 rb->last_sample = 0.0f;
852 SAFE_MEMSET(rb->data,
sizeof(rb->data), 0,
sizeof(rb->data));
853 mutex_unlock(&rb->mutex);
857 if (!rb || !data || samples <= 0)
858 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters: rb=%p, data=%p, samples=%d", rb, data, samples);
861 if (samples > AUDIO_RING_BUFFER_SIZE) {
862 return SET_ERRNO(ERROR_BUFFER,
"Attempted to write %d samples, but buffer size is only %d", samples,
863 AUDIO_RING_BUFFER_SIZE);
869 unsigned int write_idx = atomic_load_explicit(&rb->write_index, memory_order_relaxed);
870 unsigned int read_idx = atomic_load_explicit(&rb->read_index, memory_order_acquire);
874 if (write_idx >= read_idx) {
875 buffer_level = (int)(write_idx - read_idx);
877 buffer_level = AUDIO_RING_BUFFER_SIZE - (int)(read_idx - write_idx);
881 int available = AUDIO_RING_BUFFER_SIZE - 1 - buffer_level;
887 if (buffer_level > AUDIO_JITTER_HIGH_WATER_MARK) {
889 int target_writes = AUDIO_JITTER_TARGET_LEVEL - buffer_level;
890 if (target_writes < 0) {
894 if (samples > target_writes) {
895 int dropped = samples - target_writes;
896 log_warn_every(LOG_RATE_FAST,
897 "Audio buffer high water mark exceeded (%d > %d): dropping %d INCOMING samples "
898 "(keeping newest %d to maintain target %d)",
899 buffer_level, AUDIO_JITTER_HIGH_WATER_MARK, dropped, target_writes, AUDIO_JITTER_TARGET_LEVEL);
900 samples = target_writes;
905 int samples_to_write = samples;
906 if (samples > available) {
908 int samples_dropped = samples - available;
909 samples_to_write = available;
910 log_warn_every(LOG_RATE_FAST,
"Audio buffer overflow: dropping %d of %d incoming samples (buffer_used=%d/%d)",
911 samples_dropped, samples, AUDIO_RING_BUFFER_SIZE - available, AUDIO_RING_BUFFER_SIZE);
915 if (samples_to_write > 0) {
916 int remaining = AUDIO_RING_BUFFER_SIZE - (int)write_idx;
918 if (samples_to_write <= remaining) {
920 SAFE_MEMCPY(&rb->data[write_idx], samples_to_write *
sizeof(
float), data, samples_to_write *
sizeof(
float));
923 SAFE_MEMCPY(&rb->data[write_idx], remaining *
sizeof(
float), data, remaining *
sizeof(
float));
924 SAFE_MEMCPY(&rb->data[0], (samples_to_write - remaining) *
sizeof(
float), &data[remaining],
925 (samples_to_write - remaining) *
sizeof(
float));
930 unsigned int new_write_idx = (write_idx + (
unsigned int)samples_to_write) % AUDIO_RING_BUFFER_SIZE;
931 atomic_store_explicit(&rb->write_index, new_write_idx, memory_order_release);
940 if (!rb || !data || samples <= 0) {
941 SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters: rb=%p, data=%p, samples=%d", rb, data, samples);
948 unsigned int write_idx = atomic_load_explicit(&rb->write_index, memory_order_acquire);
949 unsigned int read_idx = atomic_load_explicit(&rb->read_index, memory_order_relaxed);
953 if (write_idx >= read_idx) {
954 available = write_idx - read_idx;
956 available = AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
960 bool jitter_filled = atomic_load_explicit(&rb->jitter_buffer_filled, memory_order_acquire);
961 int crossfade_remaining = atomic_load_explicit(&rb->crossfade_samples_remaining, memory_order_acquire);
962 bool fade_in = atomic_load_explicit(&rb->crossfade_fade_in, memory_order_acquire);
966 if (!jitter_filled && rb->jitter_buffer_enabled) {
968 if (available >= AUDIO_JITTER_BUFFER_THRESHOLD) {
969 atomic_store_explicit(&rb->jitter_buffer_filled,
true, memory_order_release);
970 atomic_store_explicit(&rb->crossfade_samples_remaining, AUDIO_CROSSFADE_SAMPLES, memory_order_release);
971 atomic_store_explicit(&rb->crossfade_fade_in,
true, memory_order_release);
972 log_info(
"Jitter buffer filled (%zu samples), starting playback with fade-in", available);
974 jitter_filled =
true;
975 crossfade_remaining = AUDIO_CROSSFADE_SAMPLES;
979 log_debug_every(NS_PER_MS_INT,
"Jitter buffer filling: %zu/%d samples (%.1f%%)", available,
980 AUDIO_JITTER_BUFFER_THRESHOLD, (100.0f * available) / AUDIO_JITTER_BUFFER_THRESHOLD);
986 unsigned int underruns = atomic_load_explicit(&rb->underrun_count, memory_order_relaxed);
987 log_dev_every(5 * NS_PER_MS_INT,
"Buffer health: %zu/%d samples (%.1f%%), underruns=%u", available,
988 AUDIO_RING_BUFFER_SIZE, (100.0f * available) / AUDIO_RING_BUFFER_SIZE, underruns);
996 if (rb->jitter_buffer_enabled && available < AUDIO_JITTER_LOW_WATER_MARK) {
997 unsigned int underrun_count = atomic_fetch_add_explicit(&rb->underrun_count, 1, memory_order_relaxed) + 1;
998 log_warn_every(LOG_RATE_FAST,
999 "Audio buffer low #%u: only %zu samples available (low water mark: %d), padding with silence",
1000 underrun_count, available, AUDIO_JITTER_LOW_WATER_MARK);
1004 size_t to_read = (samples > available) ? available : samples;
1007 size_t remaining = AUDIO_RING_BUFFER_SIZE - read_idx;
1009 if (to_read <= remaining) {
1011 SAFE_MEMCPY(data, to_read *
sizeof(
float), &rb->data[read_idx], to_read *
sizeof(
float));
1014 SAFE_MEMCPY(data, remaining *
sizeof(
float), &rb->data[read_idx], remaining *
sizeof(
float));
1015 SAFE_MEMCPY(&data[remaining], (to_read - remaining) *
sizeof(
float), &rb->data[0],
1016 (to_read - remaining) *
sizeof(
float));
1021 unsigned int new_read_idx = (read_idx + (
unsigned int)to_read) % AUDIO_RING_BUFFER_SIZE;
1022 atomic_store_explicit(&rb->read_index, new_read_idx, memory_order_release);
1025 if (fade_in && crossfade_remaining > 0) {
1026 int fade_start = AUDIO_CROSSFADE_SAMPLES - crossfade_remaining;
1027 size_t fade_samples = (to_read < (size_t)crossfade_remaining) ? to_read : (size_t)crossfade_remaining;
1029 for (
size_t i = 0; i < fade_samples; i++) {
1030 float fade_factor = (float)(fade_start + (
int)i + 1) / (
float)AUDIO_CROSSFADE_SAMPLES;
1031 data[i] *= fade_factor;
1034 int new_crossfade_remaining = crossfade_remaining - (int)fade_samples;
1035 atomic_store_explicit(&rb->crossfade_samples_remaining, new_crossfade_remaining, memory_order_release);
1036 if (new_crossfade_remaining <= 0) {
1037 atomic_store_explicit(&rb->crossfade_fade_in,
false, memory_order_release);
1038 log_debug(
"Audio fade-in complete");
1046 rb->last_sample = data[to_read - 1];
1070 if (!rb || !data || samples <= 0) {
1075 unsigned int write_idx = atomic_load_explicit(&rb->write_index, memory_order_acquire);
1076 unsigned int read_idx = atomic_load_explicit(&rb->read_index, memory_order_relaxed);
1080 if (write_idx >= read_idx) {
1081 available = write_idx - read_idx;
1083 available = AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
1086 size_t to_peek = (samples > available) ? available : samples;
1093 size_t first_chunk = (read_idx + to_peek <= AUDIO_RING_BUFFER_SIZE) ? to_peek : (AUDIO_RING_BUFFER_SIZE - read_idx);
1095 SAFE_MEMCPY(data, first_chunk *
sizeof(
float), rb->data + read_idx, first_chunk *
sizeof(
float));
1097 if (first_chunk < to_peek) {
1099 size_t second_chunk = to_peek - first_chunk;
1100 SAFE_MEMCPY(data + first_chunk, second_chunk *
sizeof(
float), rb->data, second_chunk *
sizeof(
float));
1113 unsigned int write_idx = atomic_load_explicit(&rb->write_index, memory_order_acquire);
1114 unsigned int read_idx = atomic_load_explicit(&rb->read_index, memory_order_relaxed);
1116 if (write_idx >= read_idx) {
1117 return write_idx - read_idx;
1120 return AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
1125 SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters: rb is NULL");
1133 log_debug(
"audio_init: starting, ctx=%p", (
void *)ctx);
1135 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters: ctx is NULL");
1138 SAFE_MEMSET(ctx,
sizeof(audio_context_t), 0,
sizeof(audio_context_t));
1141 return SET_ERRNO(ERROR_THREAD,
"Failed to initialize audio context mutex");
1150 if (!ctx->capture_buffer) {
1152 return SET_ERRNO(ERROR_MEMORY,
"Failed to create capture buffer");
1156 if (!ctx->playback_buffer) {
1159 return SET_ERRNO(ERROR_MEMORY,
"Failed to create playback buffer");
1164 if (!ctx->raw_capture_rb) {
1168 return SET_ERRNO(ERROR_MEMORY,
"Failed to create raw capture buffer");
1172 if (!ctx->raw_render_rb) {
1177 return SET_ERRNO(ERROR_MEMORY,
"Failed to create raw render buffer");
1181 if (!ctx->processed_playback_rb) {
1187 return SET_ERRNO(ERROR_MEMORY,
"Failed to create processed playback buffer");
1197 audio_release_portaudio();
1199 return SET_ERRNO(ERROR_THREAD,
"Failed to initialize worker mutex");
1202 if (cond_init(&ctx->worker_cond) != 0) {
1209 audio_release_portaudio();
1211 return SET_ERRNO(ERROR_THREAD,
"Failed to initialize worker condition variable");
1216 if (!ctx->worker_capture_batch) {
1217 cond_destroy(&ctx->worker_cond);
1224 audio_release_portaudio();
1226 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate worker capture batch buffer");
1230 if (!ctx->worker_render_batch) {
1231 SAFE_FREE(ctx->worker_capture_batch);
1232 cond_destroy(&ctx->worker_cond);
1239 audio_release_portaudio();
1241 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate worker render batch buffer");
1245 if (!ctx->worker_playback_batch) {
1246 SAFE_FREE(ctx->worker_render_batch);
1247 SAFE_FREE(ctx->worker_capture_batch);
1248 cond_destroy(&ctx->worker_cond);
1255 audio_release_portaudio();
1257 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate worker playback batch buffer");
1261 ctx->worker_running =
false;
1262 atomic_store(&ctx->worker_should_stop,
false);
1264 ctx->initialized =
true;
1265 atomic_store(&ctx->shutting_down,
false);
1266 log_info(
"Audio system initialized successfully (worker thread architecture enabled)");
1267 return ASCIICHAT_OK;
1279 if (ctx->initialized) {
1287 if (ctx->worker_running) {
1288 log_debug(
"Stopping worker thread during audio_destroy");
1289 atomic_store(&ctx->worker_should_stop,
true);
1290 cond_signal(&ctx->worker_cond);
1292 ctx->worker_running =
false;
1295 mutex_lock(&ctx->state_mutex);
1306 SAFE_FREE(ctx->worker_capture_batch);
1307 SAFE_FREE(ctx->worker_render_batch);
1308 SAFE_FREE(ctx->worker_playback_batch);
1311 cond_destroy(&ctx->worker_cond);
1314 ctx->initialized =
false;
1316 mutex_unlock(&ctx->state_mutex);
1319 log_debug(
"Audio system cleanup complete (all resources released)");
1325 audio_release_portaudio();
1331 ctx->audio_pipeline = pipeline;
1335 if (!ctx || !ctx->initialized) {
1339 if (ctx->playback_buffer) {
1342 if (ctx->processed_playback_rb) {
1345 if (ctx->render_buffer) {
1348 if (ctx->raw_render_rb) {
1354 if (!ctx || !ctx->initialized) {
1355 return SET_ERRNO(ERROR_INVALID_STATE,
"Audio context not initialized");
1360 asciichat_error_t pa_result = audio_ensure_portaudio_initialized();
1361 if (pa_result != ASCIICHAT_OK) {
1365 mutex_lock(&ctx->state_mutex);
1368 if (ctx->duplex_stream || ctx->input_stream || ctx->output_stream) {
1369 mutex_unlock(&ctx->state_mutex);
1370 return ASCIICHAT_OK;
1374 PaStreamParameters inputParams = {0};
1375 const PaDeviceInfo *inputInfo = NULL;
1376 bool has_input =
false;
1378 if (!ctx->playback_only) {
1379 if (GET_OPTION(microphone_index) >= 0) {
1380 inputParams.device = GET_OPTION(microphone_index);
1382 inputParams.device = Pa_GetDefaultInputDevice();
1385 if (inputParams.device == paNoDevice) {
1386 mutex_unlock(&ctx->state_mutex);
1387 return SET_ERRNO(ERROR_AUDIO,
"No input device available");
1390 inputInfo = Pa_GetDeviceInfo(inputParams.device);
1392 mutex_unlock(&ctx->state_mutex);
1393 return SET_ERRNO(ERROR_AUDIO,
"Input device info not found");
1397 inputParams.channelCount = AUDIO_CHANNELS;
1398 inputParams.sampleFormat = paFloat32;
1399 inputParams.suggestedLatency = inputInfo->defaultLowInputLatency;
1400 inputParams.hostApiSpecificStreamInfo = NULL;
1404 PaStreamParameters outputParams;
1405 const PaDeviceInfo *outputInfo = NULL;
1406 bool has_output =
false;
1408 if (GET_OPTION(speakers_index) >= 0) {
1409 outputParams.device = GET_OPTION(speakers_index);
1411 outputParams.device = Pa_GetDefaultOutputDevice();
1414 if (outputParams.device != paNoDevice) {
1415 outputInfo = Pa_GetDeviceInfo(outputParams.device);
1418 outputParams.channelCount = AUDIO_CHANNELS;
1419 outputParams.sampleFormat = paFloat32;
1420 outputParams.suggestedLatency = outputInfo->defaultLowOutputLatency;
1421 outputParams.hostApiSpecificStreamInfo = NULL;
1423 log_warn(
"Output device info not found for device %d", outputParams.device);
1428 ctx->input_device_rate = (has_input && inputInfo) ? inputInfo->defaultSampleRate : 0;
1429 ctx->output_device_rate = (has_output && outputInfo) ? outputInfo->defaultSampleRate : 0;
1431 log_debug(
"Opening audio:");
1433 log_info(
" Input: %s (%.0f Hz)", inputInfo->name, inputInfo->defaultSampleRate);
1434 }
else if (ctx->playback_only) {
1435 log_debug(
" Input: (playback-only mode - no microphone)");
1437 log_debug(
" Input: (none)");
1440 log_info(
" Output: %s (%.0f Hz)", outputInfo->name, outputInfo->defaultSampleRate);
1442 log_debug(
" Output: None (input-only mode - will send audio to server)");
1447 bool rates_differ = has_input && has_output && (inputInfo->defaultSampleRate != outputInfo->defaultSampleRate);
1448 bool try_separate = rates_differ || !has_input || !has_output;
1451 if (!try_separate) {
1453 err = Pa_OpenStream(&ctx->duplex_stream, &inputParams, &outputParams, AUDIO_SAMPLE_RATE, AUDIO_FRAMES_PER_BUFFER,
1454 paClipOff, duplex_callback, ctx);
1457 err = Pa_StartStream(ctx->duplex_stream);
1459 Pa_CloseStream(ctx->duplex_stream);
1460 ctx->duplex_stream = NULL;
1461 log_warn(
"Full-duplex stream failed to start: %s", Pa_GetErrorText(err));
1462 try_separate =
true;
1465 log_warn(
"Full-duplex stream failed to open: %s", Pa_GetErrorText(err));
1466 try_separate =
true;
1472 if (has_output && has_input) {
1473 log_info(
"Using separate input/output streams (sample rates differ: %.0f vs %.0f Hz)",
1474 inputInfo->defaultSampleRate, outputInfo->defaultSampleRate);
1475 log_info(
" Will resample: buffer at %.0f Hz → output at %.0f Hz", (
double)AUDIO_SAMPLE_RATE,
1476 outputInfo->defaultSampleRate);
1477 }
else if (has_output) {
1478 log_debug(
"Using output-only mode (playback-only for mirror/media)");
1479 }
else if (has_input) {
1480 log_info(
"Using input-only mode (no output device available)");
1484 ctx->sample_rate = AUDIO_SAMPLE_RATE;
1488 if (!ctx->render_buffer) {
1489 mutex_unlock(&ctx->state_mutex);
1490 return SET_ERRNO(ERROR_MEMORY,
"Failed to create render buffer");
1494 bool output_ok =
false;
1495 double actual_output_rate = 0;
1499 double preferred_rate = AUDIO_SAMPLE_RATE;
1500 double native_rate = outputInfo->defaultSampleRate;
1502 log_debug(
"Attempting output at %.0f Hz (preferred) vs %.0f Hz (native)", preferred_rate, native_rate);
1507 PaStreamCallback *callback = output_callback;
1510 err = Pa_OpenStream(&ctx->output_stream, NULL, &outputParams, preferred_rate, AUDIO_FRAMES_PER_BUFFER, paClipOff,
1514 actual_output_rate = preferred_rate;
1516 log_info(
"✓ Output opened at preferred rate: %.0f Hz (matches input - optimal!)", preferred_rate);
1518 log_warn(
"Failed to open output at %.0f Hz: %s, trying native rate %.0f Hz", preferred_rate,
1519 Pa_GetErrorText(err), native_rate);
1522 if (ctx->output_stream) {
1523 log_debug(
"Closing partially-opened output stream from failed preferred rate");
1524 Pa_CloseStream(ctx->output_stream);
1525 ctx->output_stream = NULL;
1529 err = Pa_OpenStream(&ctx->output_stream, NULL, &outputParams, native_rate, AUDIO_FRAMES_PER_BUFFER, paClipOff,
1533 actual_output_rate = native_rate;
1535 log_info(
"✓ Output opened at native rate: %.0f Hz (will need resampling)", native_rate);
1537 log_warn(
"Failed to open output stream at native rate: %s", Pa_GetErrorText(err));
1539 if (ctx->output_stream) {
1540 log_debug(
"Closing partially-opened output stream from failed native rate");
1541 Pa_CloseStream(ctx->output_stream);
1542 ctx->output_stream = NULL;
1549 ctx->output_device_rate = actual_output_rate;
1550 if (actual_output_rate != AUDIO_SAMPLE_RATE) {
1551 log_warn(
"⚠️ Output rate mismatch: %.0f Hz output vs %.0f Hz input - resampling will be used",
1552 actual_output_rate, (
double)AUDIO_SAMPLE_RATE);
1558 bool input_ok = !has_input;
1562 double input_stream_rate = AUDIO_SAMPLE_RATE;
1563 err = Pa_OpenStream(&ctx->input_stream, &inputParams, NULL, input_stream_rate, AUDIO_FRAMES_PER_BUFFER, paClipOff,
1564 input_callback, ctx);
1569 log_debug(
"Input failed - trying device 0 as fallback");
1572 if (ctx->input_stream) {
1573 log_debug(
"Closing partially-opened input stream from failed primary device");
1574 Pa_CloseStream(ctx->input_stream);
1575 ctx->input_stream = NULL;
1578 PaStreamParameters fallback_input_params = inputParams;
1579 fallback_input_params.device = 0;
1580 const PaDeviceInfo *device_0_info = Pa_GetDeviceInfo(0);
1581 if (device_0_info && device_0_info->maxInputChannels > 0) {
1582 err = Pa_OpenStream(&ctx->input_stream, &fallback_input_params, NULL, input_stream_rate,
1583 AUDIO_FRAMES_PER_BUFFER, paClipOff, input_callback, ctx);
1585 log_info(
"Input stream opened on device 0 (fallback from default)");
1588 log_warn(
"Fallback also failed on device 0: %s", Pa_GetErrorText(err));
1590 if (ctx->input_stream) {
1591 log_debug(
"Closing partially-opened input stream from failed fallback device");
1592 Pa_CloseStream(ctx->input_stream);
1593 ctx->input_stream = NULL;
1600 log_warn(
"Failed to open input stream: %s", Pa_GetErrorText(err));
1605 if (!input_ok && !output_ok) {
1608 ctx->render_buffer = NULL;
1609 mutex_unlock(&ctx->state_mutex);
1610 return SET_ERRNO(ERROR_AUDIO,
"Failed to open both input and output streams");
1614 if (!output_ok && input_ok) {
1615 log_info(
"Output stream unavailable - continuing with input-only (can send audio to server)");
1616 ctx->output_stream = NULL;
1619 if (!input_ok && output_ok) {
1620 log_info(
"Input stream unavailable - continuing with output-only (can receive audio from server)");
1621 ctx->input_stream = NULL;
1625 if (ctx->output_stream) {
1626 err = Pa_StartStream(ctx->output_stream);
1628 if (ctx->input_stream)
1629 Pa_CloseStream(ctx->input_stream);
1630 Pa_CloseStream(ctx->output_stream);
1631 ctx->input_stream = NULL;
1632 ctx->output_stream = NULL;
1634 ctx->render_buffer = NULL;
1635 mutex_unlock(&ctx->state_mutex);
1636 return SET_ERRNO(ERROR_AUDIO,
"Failed to start output stream: %s", Pa_GetErrorText(err));
1641 if (ctx->input_stream) {
1642 err = Pa_StartStream(ctx->input_stream);
1644 if (ctx->output_stream)
1645 Pa_StopStream(ctx->output_stream);
1646 if (ctx->input_stream)
1647 Pa_CloseStream(ctx->input_stream);
1648 if (ctx->output_stream)
1649 Pa_CloseStream(ctx->output_stream);
1650 ctx->input_stream = NULL;
1651 ctx->output_stream = NULL;
1653 ctx->render_buffer = NULL;
1654 mutex_unlock(&ctx->state_mutex);
1655 return SET_ERRNO(ERROR_AUDIO,
"Failed to start input stream: %s", Pa_GetErrorText(err));
1659 ctx->separate_streams =
true;
1660 log_debug(
"Separate streams started successfully");
1662 ctx->separate_streams =
false;
1663 log_info(
"Full-duplex stream started (single callback, perfect AEC3 timing)");
1669 if (!ctx->worker_running) {
1670 atomic_store(&ctx->worker_should_stop,
false);
1673 if (ctx->duplex_stream) {
1674 Pa_StopStream(ctx->duplex_stream);
1675 Pa_CloseStream(ctx->duplex_stream);
1676 ctx->duplex_stream = NULL;
1678 if (ctx->input_stream) {
1679 Pa_StopStream(ctx->input_stream);
1680 Pa_CloseStream(ctx->input_stream);
1681 ctx->input_stream = NULL;
1683 if (ctx->output_stream) {
1684 Pa_StopStream(ctx->output_stream);
1685 Pa_CloseStream(ctx->output_stream);
1686 ctx->output_stream = NULL;
1689 ctx->render_buffer = NULL;
1690 mutex_unlock(&ctx->state_mutex);
1691 return SET_ERRNO(ERROR_THREAD,
"Failed to create worker thread");
1693 ctx->worker_running =
true;
1694 log_debug(
"Worker thread started successfully");
1697 ctx->running =
true;
1698 ctx->sample_rate = AUDIO_SAMPLE_RATE;
1699 mutex_unlock(&ctx->state_mutex);
1700 return ASCIICHAT_OK;
1704 if (!ctx || !ctx->initialized) {
1705 return SET_ERRNO(ERROR_INVALID_STATE,
"Audio context not initialized");
1708 atomic_store(&ctx->shutting_down,
true);
1711 if (ctx->worker_running) {
1712 log_debug(
"Stopping worker thread");
1713 atomic_store(&ctx->worker_should_stop,
true);
1714 cond_signal(&ctx->worker_cond);
1716 ctx->worker_running =
false;
1717 log_debug(
"Worker thread stopped successfully");
1720 if (ctx->playback_buffer) {
1726 mutex_lock(&ctx->state_mutex);
1728 if (ctx->duplex_stream) {
1729 log_debug(
"Stopping duplex stream");
1730 PaError err = Pa_StopStream(ctx->duplex_stream);
1732 log_warn(
"Pa_StopStream failed: %s", Pa_GetErrorText(err));
1734 log_debug(
"Closing duplex stream");
1735 err = Pa_CloseStream(ctx->duplex_stream);
1737 log_warn(
"Pa_CloseStream failed: %s", Pa_GetErrorText(err));
1739 log_debug(
"Duplex stream closed successfully");
1741 ctx->duplex_stream = NULL;
1745 if (ctx->input_stream) {
1746 log_debug(
"Stopping input stream");
1747 PaError err = Pa_StopStream(ctx->input_stream);
1749 log_warn(
"Pa_StopStream input failed: %s", Pa_GetErrorText(err));
1751 log_debug(
"Closing input stream");
1752 err = Pa_CloseStream(ctx->input_stream);
1754 log_warn(
"Pa_CloseStream input failed: %s", Pa_GetErrorText(err));
1756 log_debug(
"Input stream closed successfully");
1758 ctx->input_stream = NULL;
1761 if (ctx->output_stream) {
1762 log_debug(
"Stopping output stream");
1763 PaError err = Pa_StopStream(ctx->output_stream);
1765 log_warn(
"Pa_StopStream output failed: %s", Pa_GetErrorText(err));
1767 log_debug(
"Closing output stream");
1768 err = Pa_CloseStream(ctx->output_stream);
1770 log_warn(
"Pa_CloseStream output failed: %s", Pa_GetErrorText(err));
1772 log_debug(
"Output stream closed successfully");
1774 ctx->output_stream = NULL;
1778 if (ctx->render_buffer) {
1780 ctx->render_buffer = NULL;
1783 ctx->running =
false;
1784 ctx->separate_streams =
false;
1785 mutex_unlock(&ctx->state_mutex);
1787 log_debug(
"Audio stopped");
1788 return ASCIICHAT_OK;
1792 if (!ctx || !ctx->initialized || !buffer || num_samples <= 0) {
1793 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters: ctx=%p, buffer=%p, num_samples=%d", ctx, buffer,
1799 return (samples_read >= 0) ? ASCIICHAT_OK : ERROR_AUDIO;
1803 if (!ctx || !ctx->initialized || !buffer || num_samples <= 0) {
1804 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters: ctx=%p, buffer=%p, num_samples=%d", ctx, buffer,
1809 if (atomic_load(&ctx->shutting_down)) {
1810 return ASCIICHAT_OK;
1819static asciichat_error_t audio_list_devices_internal(audio_device_info_t **out_devices,
unsigned int *out_count,
1821 if (!out_devices || !out_count) {
1822 return SET_ERRNO(ERROR_INVALID_PARAM,
"audio_list_devices: invalid parameters");
1825 *out_devices = NULL;
1829 asciichat_error_t pa_result = audio_ensure_portaudio_initialized();
1830 if (pa_result != ASCIICHAT_OK) {
1834 int num_devices = Pa_GetDeviceCount();
1835 if (num_devices < 0) {
1836 audio_release_portaudio();
1837 return SET_ERRNO(ERROR_AUDIO,
"Failed to get device count: %s", Pa_GetErrorText(num_devices));
1840 if (num_devices == 0) {
1841 audio_release_portaudio();
1842 return ASCIICHAT_OK;
1846 PaDeviceIndex default_input = Pa_GetDefaultInputDevice();
1847 PaDeviceIndex default_output = Pa_GetDefaultOutputDevice();
1850 unsigned int device_count = 0;
1851 for (
int i = 0; i < num_devices; i++) {
1852 const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
1854 bool matches = list_inputs ? (info->maxInputChannels > 0) : (info->maxOutputChannels > 0);
1861 if (device_count == 0) {
1862 audio_release_portaudio();
1863 return ASCIICHAT_OK;
1867 audio_device_info_t *devices = SAFE_CALLOC(device_count,
sizeof(audio_device_info_t), audio_device_info_t *);
1869 audio_release_portaudio();
1870 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate audio device info array");
1874 unsigned int idx = 0;
1875 for (
int i = 0; i < num_devices && idx < device_count; i++) {
1876 const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
1880 bool match = list_inputs ? (info->maxInputChannels > 0) : (info->maxOutputChannels > 0);
1884 devices[idx].index = i;
1886 SAFE_STRNCPY(devices[idx].name, info->name, AUDIO_DEVICE_NAME_MAX);
1888 SAFE_STRNCPY(devices[idx].name,
"<Unknown>", AUDIO_DEVICE_NAME_MAX);
1890 devices[idx].max_input_channels = info->maxInputChannels;
1891 devices[idx].max_output_channels = info->maxOutputChannels;
1892 devices[idx].default_sample_rate = info->defaultSampleRate;
1893 devices[idx].is_default_input = (i == default_input);
1894 devices[idx].is_default_output = (i == default_output);
1899 audio_release_portaudio();
1901 *out_devices = devices;
1903 return ASCIICHAT_OK;
1907 return audio_list_devices_internal(out_devices, out_count,
true);
1911 return audio_list_devices_internal(out_devices, out_count,
false);
1919 if (!samples_ptr || !out_samples || total_samples == 0) {
1920 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for audio dequantization");
1923 for (uint32_t i = 0; i < total_samples; i++) {
1924 uint32_t network_sample;
1926 memcpy(&network_sample, samples_ptr + i *
sizeof(uint32_t),
sizeof(uint32_t));
1927 int32_t scaled = (int32_t)NET_TO_HOST_U32(network_sample);
1928 out_samples[i] = (float)scaled / 2147483647.0f;
1931 return ASCIICHAT_OK;
1936 asciichat_error_t result = asciichat_thread_set_realtime_priority();
1937 if (result == ASCIICHAT_OK) {
1938 log_debug(
"✓ Audio thread real-time priority set successfully");
1950 return SET_ERRNO(ERROR_INVALID_PARAM,
"Audio batch header data pointer is NULL");
1954 return SET_ERRNO(ERROR_INVALID_PARAM,
"Audio batch info output pointer is NULL");
1957 if (len <
sizeof(audio_batch_packet_t)) {
1958 return SET_ERRNO(ERROR_INVALID_PARAM,
"Audio batch header too small (len=%zu, expected=%zu)", len,
1959 sizeof(audio_batch_packet_t));
1962 const audio_batch_packet_t *batch_header = (
const audio_batch_packet_t *)data;
1965 out_batch->
batch_count = ntohl(batch_header->batch_count);
1966 out_batch->
total_samples = ntohl(batch_header->total_samples);
1967 out_batch->
sample_rate = ntohl(batch_header->sample_rate);
1968 out_batch->
channels = ntohl(batch_header->channels);
1970 return ASCIICHAT_OK;
1975 return SET_ERRNO(ERROR_INVALID_PARAM,
"Audio batch info pointer is NULL");
1980 return SET_ERRNO(ERROR_INVALID_PARAM,
"Audio batch count cannot be zero");
1985 return SET_ERRNO(ERROR_INVALID_PARAM,
"Audio batch count too large (batch_count=%u, max=256)", batch->
batch_count);
1990 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid channel count (channels=%u, valid=1-8)", batch->
channels);
1995 return SET_ERRNO(ERROR_INVALID_PARAM,
"Unsupported sample rate (sample_rate=%u)", batch->
sample_rate);
2000 return SET_ERRNO(ERROR_INVALID_PARAM,
"Audio batch has zero samples");
2007 return SET_ERRNO(ERROR_INVALID_PARAM,
"Audio batch sample count suspiciously large (total_samples=%u)",
2011 return ASCIICHAT_OK;
2016 static const uint32_t supported_rates[] = {
2027 const size_t rate_count =
sizeof(supported_rates) /
sizeof(supported_rates[0]);
2028 for (
size_t i = 0; i < rate_count; i++) {
2029 if (sample_rate == supported_rates[i]) {
2039 case AUDIO_SOURCE_AUTO:
2040 return !has_media_audio;
2042 case AUDIO_SOURCE_MIC:
2045 case AUDIO_SOURCE_MEDIA:
2048 case AUDIO_SOURCE_BOTH:
2052 return !has_media_audio;
void buffer_pool_free(buffer_pool_t *pool, void *data, size_t size)
void * buffer_pool_alloc(buffer_pool_t *pool, size_t size)
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)
size_t audio_ring_buffer_read(audio_ring_buffer_t *rb, float *data, size_t samples)
#define WORKER_BATCH_FRAMES
void audio_free_device_list(audio_device_info_t *devices)
asciichat_error_t audio_ring_buffer_write(audio_ring_buffer_t *rb, const float *data, int samples)
void audio_ring_buffer_destroy(audio_ring_buffer_t *rb)
asciichat_error_t audio_init(audio_context_t *ctx)
asciichat_error_t audio_set_realtime_priority(void)
audio_ring_buffer_t * audio_ring_buffer_create(void)
bool audio_is_supported_sample_rate(uint32_t sample_rate)
size_t audio_ring_buffer_available_read(audio_ring_buffer_t *rb)
asciichat_error_t audio_start_duplex(audio_context_t *ctx)
asciichat_error_t audio_parse_batch_header(const void *data, size_t len, audio_batch_info_t *out_batch)
void audio_destroy(audio_context_t *ctx)
asciichat_error_t audio_stop_duplex(audio_context_t *ctx)
asciichat_error_t audio_write_samples(audio_context_t *ctx, const float *buffer, int num_samples)
void audio_terminate_portaudio_final(void)
Terminate PortAudio and free all device resources.
#define WORKER_BATCH_SAMPLES
void audio_flush_playback_buffers(audio_context_t *ctx)
void audio_set_pipeline(audio_context_t *ctx, void *pipeline)
audio_ring_buffer_t * audio_ring_buffer_create_for_capture(void)
#define WORKER_TIMEOUT_MS
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)
asciichat_error_t audio_list_input_devices(audio_device_info_t **out_devices, unsigned int *out_count)
asciichat_error_t audio_dequantize_samples(const uint8_t *samples_ptr, uint32_t total_samples, float *out_samples)
bool audio_should_enable_microphone(audio_source_t source, bool has_media_audio)
asciichat_error_t audio_list_output_devices(audio_device_info_t **out_devices, unsigned int *out_count)
void audio_ring_buffer_clear(audio_ring_buffer_t *rb)
asciichat_error_t audio_read_samples(audio_context_t *ctx, float *buffer, int num_samples)
void resample_linear(const float *src, size_t src_samples, float *dst, size_t dst_samples, double src_rate, double dst_rate)
size_t audio_ring_buffer_available_write(audio_ring_buffer_t *rb)
struct PaStreamCallbackTimeInfo PaStreamCallbackTimeInfo
unsigned long PaStreamCallbackFlags
size_t media_source_read_audio(media_source_t *source, float *buffer, size_t num_samples)
int mutex_init(mutex_t *mutex)
int asciichat_thread_create(asciichat_thread_t *thread, void *(*start_routine)(void *), void *arg)
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
int mutex_destroy(mutex_t *mutex)
uint64_t time_get_ns(void)
int format_duration_ns(double nanoseconds, char *buffer, size_t buffer_size)
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
const char * platform_getenv(const char *name)