ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
src/client/audio.c
Go to the documentation of this file.
1
74#include "audio.h"
75#include "capture.h"
77#include "main.h"
78#include "../main.h" // Global exit API
79#include "server.h"
80#include <ascii-chat/util/fps.h>
82#include <ascii-chat/util/time.h> // For timing instrumentation
84
85#include <ascii-chat/audio/audio.h> // lib/audio/audio.h for PortAudio wrapper
86#include <ascii-chat/audio/client_pipeline.h> // Unified audio processing pipeline
87#include <ascii-chat/audio/wav_writer.h> // WAV file dumping for debugging
88#include <ascii-chat/common.h>
90#include <ascii-chat/options/rcu.h> // For RCU-based options access
91#include <ascii-chat/platform/system.h> // For platform_memcpy
92
93#include <ascii-chat/atomic.h>
94#include <ascii-chat/debug/named.h> // For NAMED_REGISTER_ATOMIC macro
95#include <string.h>
96#include <math.h>
97
101
102/* ============================================================================
103 * Audio System State
104 * ============================================================================ */
105
115static audio_context_t g_audio_context = {0};
116
125static lifecycle_t g_audio_client_init_lc = LIFECYCLE_INIT;
126static lifecycle_t g_audio_stop_thread_lc = LIFECYCLE_INIT;
127
140static client_audio_pipeline_t *g_audio_pipeline = NULL;
141
142/* ============================================================================
143 * Audio Debugging - WAV File Dumpers
144 * ============================================================================ */
145
147static wav_writer_t *g_wav_capture_raw = NULL;
148
150static wav_writer_t *g_wav_capture_processed = NULL;
151
153static wav_writer_t *g_wav_playback_received = NULL;
154
155/* ============================================================================
156 * Audio Capture Thread Management
157 * ============================================================================ */
158
168static asciichat_thread_t g_audio_capture_thread;
169
178static bool g_audio_capture_thread_created = false;
179
188static atomic_t g_audio_capture_thread_exited = {0};
189
190/* ============================================================================
191 * Async Audio Packet Queue (decouples capture from network I/O)
192 * ============================================================================ */
193
200typedef struct {
201 uint8_t data[8 * 4000]; // Max 8 frames * ~500 bytes each (with safety margin)
202 size_t size;
203 uint16_t frame_sizes[8];
206
208#define AUDIO_SEND_QUEUE_SIZE 32
209static audio_send_packet_t g_audio_send_queue[AUDIO_SEND_QUEUE_SIZE];
210static int g_audio_send_queue_head = 0; // Write position
211static int g_audio_send_queue_tail = 0; // Read position
212static mutex_t g_audio_send_queue_mutex;
213static cond_t g_audio_send_queue_cond;
214static lifecycle_t g_audio_send_queue_lc = LIFECYCLE_INIT;
215
217static bool g_audio_sender_thread_created = false;
218static atomic_t g_audio_sender_should_exit = {0};
219static atomic_t g_audio_sender_exited = {0};
220
232static int audio_queue_packet(const uint8_t *opus_data, size_t opus_size, const uint16_t *frame_sizes,
233 int frame_count) {
234 if (!lifecycle_is_initialized(&g_audio_send_queue_lc) || !opus_data || opus_size == 0) {
235 return -1;
236 }
237
238 mutex_lock(&g_audio_send_queue_mutex);
239
240 // Check if queue is full
241 int next_head = (g_audio_send_queue_head + 1) % AUDIO_SEND_QUEUE_SIZE;
242 if (next_head == g_audio_send_queue_tail) {
243 mutex_unlock(&g_audio_send_queue_mutex);
244 log_warn_every(LOG_RATE_FAST, "Audio send queue full, dropping packet");
245 return -1;
246 }
247
248 // Copy packet to queue
249 audio_send_packet_t *packet = &g_audio_send_queue[g_audio_send_queue_head];
250 if (opus_size > sizeof(packet->data)) {
251 mutex_unlock(&g_audio_send_queue_mutex);
252 log_warn("Audio packet too large for queue: %zu > %zu bytes", opus_size, sizeof(packet->data));
253 return -1;
254 }
255
256 memcpy(packet->data, opus_data, opus_size);
257 packet->size = opus_size;
258 packet->frame_count = frame_count;
259 for (int i = 0; i < frame_count && i < 8; i++) {
260 packet->frame_sizes[i] = frame_sizes[i];
261 }
262 g_audio_send_queue_head = next_head;
263
264 // Signal sender thread (must be held while signaling)
265 cond_signal(&g_audio_send_queue_cond);
266 mutex_unlock(&g_audio_send_queue_mutex);
267
268 return 0;
269}
270
277static void *audio_sender_thread_func(void *arg) {
278 (void)arg;
279 log_debug("Audio sender thread started");
280
281 static int send_count = 0;
282
283 while (!atomic_load_bool(&g_audio_sender_should_exit)) {
284 mutex_lock(&g_audio_send_queue_mutex);
285
286 // Wait for packet or exit signal (cond_wait re-acquires mutex before returning)
287 while (g_audio_send_queue_head == g_audio_send_queue_tail && !atomic_load_bool(&g_audio_sender_should_exit)) {
288 cond_wait(&g_audio_send_queue_cond, &g_audio_send_queue_mutex);
289 }
290
291 // Check exit flag after waking from cond_wait
292 if (atomic_load_bool(&g_audio_sender_should_exit)) {
293 mutex_unlock(&g_audio_send_queue_mutex);
294 break;
295 }
296
297 // Safety check: ensure queue actually has data (handle spurious wakeups)
298 if (g_audio_send_queue_head == g_audio_send_queue_tail) {
299 // Spurious wakeup - queue is empty, go back to waiting
300 mutex_unlock(&g_audio_send_queue_mutex);
301 continue;
302 }
303
304 // Dequeue packet
305 audio_send_packet_t packet = g_audio_send_queue[g_audio_send_queue_tail];
306 g_audio_send_queue_tail = (g_audio_send_queue_tail + 1) % AUDIO_SEND_QUEUE_SIZE;
307
308 mutex_unlock(&g_audio_send_queue_mutex);
309
310 // Send packet (may block on network I/O - that's OK, we're not in capture thread)
311 START_TIMER("network_send_audio");
312 asciichat_error_t send_result =
313 threaded_send_audio_opus_batch(packet.data, packet.size, packet.frame_sizes, packet.frame_count);
314 double send_time_ns = STOP_TIMER("network_send_audio");
315
316 send_count++;
317 if (send_result < 0) {
318 log_debug_every(LOG_RATE_VERY_FAST, "Failed to send audio packet");
319 } else if (send_count % 50 == 0) {
320 char duration_str[32];
321 time_pretty((uint64_t)(send_time_ns), -1, duration_str, sizeof(duration_str));
322 log_debug("Audio network send #%d: %zu bytes (%d frames) in %s", send_count, packet.size, packet.frame_count,
323 duration_str);
324 }
325 }
326
327 // Clean up thread-local error context before exit
329
330 // Signal that audio sender thread has exited
331 atomic_store_bool(&g_audio_sender_exited, true);
332
333 return NULL;
334}
335
346 log_info("[AUDIO_SENDER_INIT] Starting audio sender initialization");
347
348 if (!lifecycle_init(&g_audio_send_queue_lc, "audio_queue")) {
349 log_info("[AUDIO_SENDER_INIT] Already initialized, returning early");
350 return; // Already initialized
351 }
352
353 log_info("[AUDIO_SENDER_INIT] Initializing mutex");
354 // Initialize queue structures with error checking
355 if (mutex_init(&g_audio_send_queue_mutex, "audio_send_queue_mutex") != 0) {
356 log_error("[AUDIO_SENDER_INIT] Failed to initialize audio queue mutex");
357 return;
358 }
359
360 log_info("[AUDIO_SENDER_INIT] Initializing condition variable");
361 if (cond_init(&g_audio_send_queue_cond, "audio_send_queue_cond") != 0) {
362 log_error("[AUDIO_SENDER_INIT] Failed to initialize audio queue condition variable");
363 mutex_destroy(&g_audio_send_queue_mutex);
364 return;
365 }
366
367 log_info("[AUDIO_SENDER_INIT] Setting queue state");
368 g_audio_send_queue_head = 0;
369 g_audio_send_queue_tail = 0;
370 atomic_store_bool(&g_audio_sender_should_exit, false);
371 atomic_store_bool(&g_audio_sender_exited, false);
372
373 log_info("[AUDIO_SENDER_INIT] Spawning audio sender thread");
374 // Start sender thread (after lock release to avoid blocking other threads)
375 if (thread_pool_spawn(g_client_worker_pool, audio_sender_thread_func, NULL, 5, "audio_sender") == ASCIICHAT_OK) {
376 g_audio_sender_thread_created = true;
377 log_info("[AUDIO_SENDER_INIT] โœ“ Audio sender thread created successfully");
378 } else {
379 log_error("[AUDIO_SENDER_INIT] Failed to spawn audio sender thread in worker pool");
380 LOG_ERRNO_IF_SET("Audio sender thread creation failed");
381 }
382}
383
388 // Signal thread to exit
389 if (lifecycle_is_initialized(&g_audio_send_queue_lc)) {
390 atomic_store_bool(&g_audio_sender_should_exit, true);
391 mutex_lock(&g_audio_send_queue_mutex);
392 cond_signal(&g_audio_send_queue_cond);
393 mutex_unlock(&g_audio_send_queue_mutex);
394 }
395
396 // The worker is joined by thread_pool_stop_all() in protocol_stop_connection().
397 // Keep the queue synchronization objects alive until that join completes.
398 if (THREAD_IS_CREATED(g_audio_sender_thread_created)) {
399 log_debug("Audio sender thread will be joined by thread pool");
400 }
401}
402
404 if (!lifecycle_is_initialized(&g_audio_send_queue_lc)) {
405 return;
406 }
407
408 // thread_pool_stop_all() normally joins this worker. If the sender was
409 // blocked in transport I/O when the pool timeout elapsed, wait for the
410 // shutdown signal to be observed before destroying its queue mutex.
411 for (int i = 0; i < 200 && !atomic_load_bool(&g_audio_sender_exited); i++) {
413 }
414 if (!atomic_load_bool(&g_audio_sender_exited)) {
415 log_warn("Audio sender thread did not exit before queue cleanup");
416 return;
417 }
418
419 mutex_destroy(&g_audio_send_queue_mutex);
420 cond_destroy(&g_audio_send_queue_cond);
421 lifecycle_shutdown(&g_audio_send_queue_lc);
422 g_audio_sender_thread_created = false;
423}
424
425/* ============================================================================
426 * Audio Processing Constants
427 * ============================================================================ */
428
430#define AUDIO_VOLUME_BOOST 1.0f // No boost/attenuation
431
432/* ============================================================================
433 * Audio Processing Functions
434 * ============================================================================ */
435
450void audio_process_received_samples(const float *samples, int num_samples) {
451 // Validate parameters
452 if (!samples || num_samples <= 0) {
453 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid audio samples: samples=%p, num_samples=%d", (void *)samples, num_samples);
454 return;
455 }
456
457 if (!GET_OPTION(audio_enabled)) {
458 log_warn_every(NS_PER_MS_INT, "Received audio samples but audio is disabled");
459 return;
460 }
461
462 // Allow both single packets and batched packets
463 if (num_samples > AUDIO_BATCH_SAMPLES) {
464 log_warn("Audio packet too large: %d samples (max %d)", num_samples, AUDIO_BATCH_SAMPLES);
465 return;
466 }
467
468 // Calculate RMS energy of received samples
469 float sum_squares = 0.0f;
470 for (int i = 0; i < num_samples; i++) {
471 sum_squares += samples[i] * samples[i];
472 }
473 float received_rms = sqrtf(sum_squares / num_samples);
474
475 // DUMP: Received audio from server (before playback processing)
476 if (g_wav_playback_received) {
477 wav_writer_write(g_wav_playback_received, samples, num_samples);
478 }
479
480 // Track samples for analysis
481 if (GET_OPTION(audio_analysis_enabled)) {
482 for (int i = 0; i < num_samples; i++) {
484 }
485 }
486
487 // Copy samples to playback buffer (no processing needed - mixer already handled clipping)
488 float audio_buffer[AUDIO_BATCH_SAMPLES];
489 memcpy(audio_buffer, samples, (size_t)num_samples * sizeof(float));
490
491 // DEBUG: Log what we're writing to playback buffer (with first 4 samples to verify audio integrity)
492 static int recv_count = 0;
493 recv_count++;
494 if (recv_count <= 10 || recv_count % 50 == 0) {
495 float peak = 0.0f;
496 for (int i = 0; i < num_samples; i++) {
497 float abs_val = fabsf(samples[i]);
498 if (abs_val > peak)
499 peak = abs_val;
500 }
501 log_debug("CLIENT AUDIO RECV #%d: %d samples, RMS=%.6f, Peak=%.6f, first4=[%.4f,%.4f,%.4f,%.4f]", recv_count,
502 num_samples, received_rms, peak, num_samples > 0 ? samples[0] : 0.0f, num_samples > 1 ? samples[1] : 0.0f,
503 num_samples > 2 ? samples[2] : 0.0f, num_samples > 3 ? samples[3] : 0.0f);
504 }
505
506 // Submit to playback system (goes to jitter buffer and speakers)
507 // NOTE: AEC3's AnalyzeRender is called in output_callback() when audio actually plays,
508 // NOT here. The jitter buffer adds 50-100ms delay, so calling AnalyzeRender here
509 // would give AEC3 the wrong timing and break echo cancellation.
510 audio_write_samples(&g_audio_context, audio_buffer, num_samples);
511
512 // Log latency after writing to playback buffer
513 if (g_audio_context.playback_buffer) {
514 size_t buffer_samples = audio_ring_buffer_available_read(g_audio_context.playback_buffer);
515 float buffer_latency_ms = (float)buffer_samples / 48.0f;
516 log_dev_every(500 * US_PER_MS_INT, "LATENCY: Client playback buffer after recv: %.1fms (%zu samples)",
517 buffer_latency_ms, buffer_samples);
518 }
519
520#ifdef DEBUG_AUDIO
521 log_debug("Processed %d received audio samples", num_samples);
522#endif
523}
524
525/* ============================================================================
526 * Audio Capture Thread Implementation
527 * ============================================================================ */
528
550static void *audio_capture_thread_func(void *arg) {
551 (void)arg;
552
553 log_debug("Audio capture thread started");
554
555 // FPS tracking for audio capture thread (tracking Opus frames, ~50 FPS at 20ms per frame)
556 static fps_t fps_tracker = {0};
557 static bool fps_tracker_initialized = false;
558 if (!fps_tracker_initialized) {
559 fps_init(&fps_tracker, 50, "AUDIO_TX");
560 fps_tracker_initialized = true;
561 }
562
563 // Detailed timing stats
564 static double total_loop_ns = 0;
565 static double total_read_ns = 0;
566 static double total_encode_ns = 0;
567 static double total_queue_ns = 0;
568 static double max_loop_ns = 0;
569 static double max_read_ns = 0;
570 static double max_encode_ns = 0;
571 static double max_queue_ns = 0;
572 static uint64_t timing_loop_count = 0;
573
574// Opus frame size: 960 samples = 20ms @ 48kHz (must match pipeline config)
575#define OPUS_FRAME_SAMPLES 960
576#define OPUS_MAX_PACKET_SIZE 500 // Max Opus packet size
577
578 // Read enough samples per iteration to drain faster than we fill
579 // Buffer holds multiple Opus frames worth to prevent overflow
580 // 4 frames = 3840 samples = 80ms, but we'll read what's available up to this
581#define CAPTURE_READ_SIZE (OPUS_FRAME_SAMPLES * 4)
582
583 float audio_buffer[CAPTURE_READ_SIZE];
584 static bool wav_dumpers_initialized = false;
585
586 // Initialize WAV dumpers only once (file handles persist)
587 if (!wav_dumpers_initialized && wav_dump_enabled()) {
588 char tmp[PLATFORM_MAX_PATH_LENGTH];
589 if (platform_get_temp_dir(tmp, sizeof(tmp))) {
590 char path[PLATFORM_MAX_PATH_LENGTH];
591 safe_snprintf(path, sizeof(path), "%s/audio_capture_raw.wav", tmp);
592 g_wav_capture_raw = wav_writer_open(path, AUDIO_SAMPLE_RATE, 1);
593 safe_snprintf(path, sizeof(path), "%s/audio_capture_processed.wav", tmp);
594 g_wav_capture_processed = wav_writer_open(path, AUDIO_SAMPLE_RATE, 1);
595 log_debug("Audio debugging enabled: dumping to %s/audio_capture_*.wav", tmp);
596 }
597 wav_dumpers_initialized = true;
598 }
599
600 // Accumulator for building complete Opus frames
601 float opus_frame_buffer[OPUS_FRAME_SAMPLES];
602 int opus_frame_samples_collected = 0;
603
604 // Batch buffer for multiple Opus frames - send all at once to reduce blocking
605#define MAX_BATCH_FRAMES 8
606#define BATCH_TIMEOUT_NS (40LL * NS_PER_MS_INT) // Flush batch after 40ms even if not full (2 Opus frames @ 20ms each)
607 static uint8_t batch_buffer[MAX_BATCH_FRAMES * OPUS_MAX_PACKET_SIZE];
608 static uint16_t batch_frame_sizes[MAX_BATCH_FRAMES];
609 static int batch_frame_count = 0;
610 static size_t batch_total_size = 0;
611 static uint64_t batch_start_time_ns = 0;
612 static bool batch_has_data = false;
613
614 while (!should_exit() && !server_connection_is_lost()) {
615 START_TIMER("audio_capture_loop_iteration");
616 timing_loop_count++;
617
619 STOP_TIMER("audio_capture_loop_iteration"); // Don't count sleep time
620 platform_sleep_us(100 * US_PER_MS_INT); // Wait for connection
621 continue;
622 }
623
624 // Check if pipeline is ready
625 if (!g_audio_pipeline) {
626 STOP_TIMER("audio_capture_loop_iteration"); // Don't count sleep time
628 continue;
629 }
630
631 // Check how many samples are available in the ring buffer
632 int available = audio_ring_buffer_available_read(g_audio_context.capture_buffer);
633 if (available <= 0) {
634 // Flush partial batch before sleeping (prevent starvation during idle periods)
635 if (batch_has_data && batch_frame_count > 0) {
636 uint64_t now_ns = time_get_ns();
637 uint64_t elapsed_ns = time_elapsed_ns(batch_start_time_ns, now_ns);
638
639 if (elapsed_ns >= BATCH_TIMEOUT_NS) {
640 long elapsed_ms = (long)time_ns_to_ms(elapsed_ns);
641 log_debug_every(LOG_RATE_FAST, "Idle timeout flush: %d frames (%zu bytes) after %ld ms", batch_frame_count,
642 batch_total_size, elapsed_ms);
643 (void)audio_queue_packet(batch_buffer, batch_total_size, batch_frame_sizes, batch_frame_count);
644 batch_frame_count = 0;
645 batch_total_size = 0;
646 batch_has_data = false;
647 }
648 }
649
650 // Sleep briefly to reduce CPU usage when idle
651 // 5ms polling = 200 times/sec, fast enough to catch audio promptly
652 // Note: 50ms was causing 872ms gaps in audio transmission!
653 STOP_TIMER("audio_capture_loop_iteration"); // Must stop before loop repeats
654 platform_sleep_us(5 * US_PER_MS_INT); // 5ms (was 50ms - caused huge gaps!)
655 continue;
656 }
657
658 // Read as many samples as possible (up to CAPTURE_READ_SIZE) to drain faster
659 // This prevents buffer overflow when processing is slower than capture
660 int to_read = (available < CAPTURE_READ_SIZE) ? available : CAPTURE_READ_SIZE;
661
662 START_TIMER("audio_read_samples");
663 asciichat_error_t read_result = audio_read_samples(&g_audio_context, audio_buffer, to_read);
664 double read_time_ns = STOP_TIMER("audio_read_samples");
665
666 total_read_ns += read_time_ns;
667 if (read_time_ns > max_read_ns)
668 max_read_ns = read_time_ns;
669
670 if (read_result != ASCIICHAT_OK) {
671 log_error("Failed to read audio samples from ring buffer");
672 STOP_TIMER("audio_capture_loop_iteration"); // Don't count sleep time
673 platform_sleep_us(5 * US_PER_MS_INT); // 5ms (error path - was 50ms, caused gaps!)
674 continue;
675 }
676
677 int samples_read = to_read;
678
679 // Log every 10 reads to see if we're getting samples
680 static int total_reads = 0;
681 total_reads++;
682 if (total_reads % 10 == 0) {
683 log_debug("Audio capture loop iteration #%d: available=%d, samples_read=%d", total_reads, available,
684 samples_read);
685 }
686
687 if (samples_read > 0) {
688 // Normalize input to prevent clipping: bring peak to ยฑ0.99
689 // Calculate peak level first
690 float peak = 0.0f;
691 for (int i = 0; i < samples_read; i++) {
692 float abs_val = fabsf(audio_buffer[i]);
693 if (abs_val > peak)
694 peak = abs_val;
695 }
696
697 // Apply normalization if peak exceeds 1.0
698 // Use 0.99 to leave headroom for processing
699 if (peak > 1.0f) {
700 float gain = 0.99f / peak;
701 for (int i = 0; i < samples_read; i++) {
702 audio_buffer[i] *= gain;
703 }
704 static int norm_count = 0;
705 norm_count++;
706 if (norm_count <= 5 || norm_count % 100 == 0) {
707 log_debug("Input normalization #%d: peak=%.4f, gain=%.4f", norm_count, peak, gain);
708 }
709 }
710
711 // DUMP: Raw captured audio (before any processing)
712 if (g_wav_capture_raw) {
713 wav_writer_write(g_wav_capture_raw, audio_buffer, samples_read);
714 }
715
716 // DEBUG: Log EVERY read to see what we're getting from the ring buffer
717 static int read_count = 0;
718 read_count++;
719 float sum_squares = 0.0f;
720 for (int i = 0; i < samples_read && i < 10; i++) {
721 sum_squares += audio_buffer[i] * audio_buffer[i];
722 }
723 float rms = sqrtf(sum_squares / (samples_read > 10 ? 10 : samples_read));
724 if (read_count <= 5 || read_count % 20 == 0) {
725 log_debug("Audio capture read #%d: available=%d, samples_read=%d, first=[%.6f,%.6f,%.6f], RMS=%.6f", read_count,
726 available, samples_read, samples_read > 0 ? audio_buffer[0] : 0.0f,
727 samples_read > 1 ? audio_buffer[1] : 0.0f, samples_read > 2 ? audio_buffer[2] : 0.0f, rms);
728 }
729
730 // Track sent samples for analysis
731 if (GET_OPTION(audio_analysis_enabled)) {
732 for (int i = 0; i < samples_read; i++) {
733 audio_analysis_track_sent_sample(audio_buffer[i]);
734 }
735 }
736
737 // Accumulate samples into Opus frame buffer
738 int samples_to_process = samples_read;
739 int sample_offset = 0;
740
741 while (samples_to_process > 0) {
742 // How many samples can we add to current frame?
743 int space_in_frame = OPUS_FRAME_SAMPLES - opus_frame_samples_collected;
744 int samples_to_copy = (samples_to_process < space_in_frame) ? samples_to_process : space_in_frame;
745
746 // Copy samples to frame buffer
747 memcpy(&opus_frame_buffer[opus_frame_samples_collected], &audio_buffer[sample_offset],
748 (size_t)samples_to_copy * sizeof(float));
749
750 opus_frame_samples_collected += samples_to_copy;
751 sample_offset += samples_to_copy;
752 samples_to_process -= samples_to_copy;
753
754 // Do we have a complete frame?
755 if (opus_frame_samples_collected >= OPUS_FRAME_SAMPLES) {
756 // Process through pipeline: AEC, filters, AGC, noise gate, Opus encode
757 uint8_t opus_packet[OPUS_MAX_PACKET_SIZE];
758
759 START_TIMER("opus_encode");
760 int opus_len = client_audio_pipeline_capture(g_audio_pipeline, opus_frame_buffer, OPUS_FRAME_SAMPLES,
761 opus_packet, OPUS_MAX_PACKET_SIZE);
762
763 static int encode_count = 0;
764 encode_count++;
765 double opus_elapsed_ns = STOP_TIMER("opus_encode");
766 if (encode_count % 50 == 0) {
767 if (opus_elapsed_ns >= 0.0) {
768 char _duration_str[32];
769 time_pretty((uint64_t)(opus_elapsed_ns), -1, _duration_str, sizeof(_duration_str));
770 log_dev("Opus encode #%d: %d samples -> %d bytes in %s", encode_count, OPUS_FRAME_SAMPLES, opus_len,
771 _duration_str);
772 }
773 }
774
775 double encode_time_ns = 0; // Timing already logged
776
777 total_encode_ns += encode_time_ns;
778 if (encode_time_ns > max_encode_ns)
779 max_encode_ns = encode_time_ns;
780
781 if (opus_len > 0) {
782
783 log_debug_every(LOG_RATE_VERY_FAST, "Pipeline encoded: %d samples -> %d bytes (compression: %.1fx)",
784 OPUS_FRAME_SAMPLES, opus_len,
785 (float)(OPUS_FRAME_SAMPLES * sizeof(float)) / (float)opus_len);
786
787 // Add to batch buffer
788 if (batch_frame_count < MAX_BATCH_FRAMES && batch_total_size + (size_t)opus_len <= sizeof(batch_buffer)) {
789 // Mark batch start time on first frame
790 if (batch_frame_count == 0) {
791 batch_start_time_ns = time_get_ns();
792 batch_has_data = true;
793 }
794
795 memcpy(batch_buffer + batch_total_size, opus_packet, (size_t)opus_len);
796 batch_frame_sizes[batch_frame_count] = (uint16_t)opus_len;
797 batch_total_size += (size_t)opus_len;
798 batch_frame_count++;
799
800 if (GET_OPTION(audio_analysis_enabled)) {
801 audio_analysis_track_sent_packet((size_t)opus_len);
802 }
803 }
804 } else if (opus_len == 0) {
805 // DTX frame (silence) - no data to send
806 log_debug_every(LOG_RATE_VERY_FAST, "Pipeline DTX frame (silence detected)");
807 }
808
809 // Reset frame buffer
810 opus_frame_samples_collected = 0;
811 }
812 }
813
814 // Queue batch for async sending (non-blocking - sender thread handles network I/O)
815 if (batch_frame_count > 0) {
816 static int batch_send_count = 0;
817 batch_send_count++;
818
819 START_TIMER("audio_queue_packet");
820 int queue_result = audio_queue_packet(batch_buffer, batch_total_size, batch_frame_sizes, batch_frame_count);
821 double queue_time_ns = STOP_TIMER("audio_queue_packet");
822
823 total_queue_ns += queue_time_ns;
824 if (queue_time_ns > max_queue_ns)
825 max_queue_ns = queue_time_ns;
826
827 if (queue_result < 0) {
828 log_debug_every(LOG_RATE_VERY_FAST, "Failed to queue audio batch (queue full)");
829 } else {
830 if (batch_send_count <= 10 || batch_send_count % 50 == 0) {
831 char queue_duration_str[32];
832 time_pretty((uint64_t)(queue_time_ns), -1, queue_duration_str, sizeof(queue_duration_str));
833 log_debug("CLIENT: Queued Opus batch #%d (%d frames, %zu bytes) in %s", batch_send_count, batch_frame_count,
834 batch_total_size, queue_duration_str);
835 }
836 // Track audio frame for FPS reporting
837 fps_frame_ns(&fps_tracker, time_get_ns(), "audio batch queued");
838 }
839
840 // Reset batch
841 batch_frame_count = 0;
842 batch_total_size = 0;
843 batch_has_data = false;
844 }
845
846 // Log overall loop iteration time periodically
847 double loop_time_ns = STOP_TIMER("audio_capture_loop_iteration");
848 total_loop_ns += loop_time_ns;
849 if (loop_time_ns > max_loop_ns)
850 max_loop_ns = loop_time_ns;
851
852 // Comprehensive timing report every 100 iterations (~2 seconds)
853 if (timing_loop_count % 100 == 0) {
854 char avg_loop_str[32], max_loop_str[32];
855 char avg_read_str[32], max_read_str[32];
856 char avg_encode_str[32], max_encode_str[32];
857 char avg_queue_str[32], max_queue_str[32];
858
859 time_pretty((uint64_t)(total_loop_ns / timing_loop_count), -1, avg_loop_str, sizeof(avg_loop_str));
860 time_pretty((uint64_t)(max_loop_ns), -1, max_loop_str, sizeof(max_loop_str));
861 time_pretty((uint64_t)(total_read_ns / timing_loop_count), -1, avg_read_str, sizeof(avg_read_str));
862 time_pretty((uint64_t)(max_read_ns), -1, max_read_str, sizeof(max_read_str));
863 time_pretty((uint64_t)(total_encode_ns / timing_loop_count), -1, avg_encode_str, sizeof(avg_encode_str));
864 time_pretty((uint64_t)(max_encode_ns), -1, max_encode_str, sizeof(max_encode_str));
865 time_pretty((uint64_t)(total_queue_ns / timing_loop_count), -1, avg_queue_str, sizeof(avg_queue_str));
866 time_pretty((uint64_t)(max_queue_ns), -1, max_queue_str, sizeof(max_queue_str));
867
868 log_debug("CAPTURE TIMING #%lu: loop avg=%s max=%s, read avg=%s max=%s", timing_loop_count, avg_loop_str,
869 max_loop_str, avg_read_str, max_read_str);
870 log_info(" encode avg=%s max=%s, queue avg=%s max=%s", avg_encode_str, max_encode_str, avg_queue_str,
871 max_queue_str);
872 }
873
874 // Check if we have a partial batch that's been waiting too long (time-based flush)
875 // This prevents batches from sitting indefinitely when audio capture is irregular
876 if (batch_has_data && batch_frame_count > 0) {
877 uint64_t now_ns = time_get_ns();
878 uint64_t elapsed_ns = time_elapsed_ns(batch_start_time_ns, now_ns);
879
880 if (elapsed_ns >= BATCH_TIMEOUT_NS) {
881 static int timeout_flush_count = 0;
882 timeout_flush_count++;
883
884 long elapsed_ms = (long)time_ns_to_ms(elapsed_ns);
885 log_debug_every(LOG_RATE_FAST, "Timeout flush #%d: %d frames (%zu bytes) after %ld ms", timeout_flush_count,
886 batch_frame_count, batch_total_size, elapsed_ms);
887
888 // Queue partial batch
889 int queue_result = audio_queue_packet(batch_buffer, batch_total_size, batch_frame_sizes, batch_frame_count);
890 if (queue_result == 0) {
891 // Track audio frame for FPS reporting
892 fps_frame_ns(&fps_tracker, time_get_ns(), "audio batch timeout flush");
893 }
894
895 // Reset batch
896 batch_frame_count = 0;
897 batch_total_size = 0;
898 batch_has_data = false;
899 }
900 }
901
902 // Yield to reduce CPU usage - audio arrives at ~20ms per Opus frame (960 samples @ 48kHz)
903 // Without sleep, thread spins at 90-100% CPU constantly checking for new samples
904 // Even 1ms sleep reduces CPU usage from 90% to <10% with minimal latency impact
906 } else {
907 // Track loop time even when no samples processed
908 double loop_time_ns = STOP_TIMER("audio_capture_loop_iteration");
909 total_loop_ns += loop_time_ns;
910 if (loop_time_ns > max_loop_ns)
911 max_loop_ns = loop_time_ns;
912
913 // Flush partial batch before sleeping on error path (prevent starvation)
914 if (batch_has_data && batch_frame_count > 0) {
915 uint64_t now_ns = time_get_ns();
916 uint64_t elapsed_ns = time_elapsed_ns(batch_start_time_ns, now_ns);
917
918 if (elapsed_ns >= BATCH_TIMEOUT_NS) {
919 long elapsed_ms = (long)time_ns_to_ms(elapsed_ns);
920 log_debug_every(LOG_RATE_FAST, "Error path timeout flush: %d frames (%zu bytes) after %ld ms",
921 batch_frame_count, batch_total_size, elapsed_ms);
922 (void)audio_queue_packet(batch_buffer, batch_total_size, batch_frame_sizes, batch_frame_count);
923 batch_frame_count = 0;
924 batch_total_size = 0;
925 batch_has_data = false;
926 }
927 }
928
929 platform_sleep_us(5 * US_PER_MS_INT); // 5ms (error path - was 50ms, caused gaps!)
930 }
931 }
932
933 log_debug("Audio capture thread stopped");
934 atomic_store_bool(&g_audio_capture_thread_exited, true);
935
936 // Clean up thread-local error context before exit
938
939 return NULL;
940}
941
942/* ============================================================================
943 * Public Interface Functions
944 * ============================================================================ */
945
957 if (!GET_OPTION(audio_enabled)) {
958 return 0; // Audio disabled - not an error
959 }
960
961 // Use lifecycle to ensure only one thread initializes audio at a time
962 // This prevents race conditions in worker thread creation
963 if (!lifecycle_init_once(&g_audio_client_init_lc)) {
964 // Another thread is already initializing, wait for it to complete
965 while (!lifecycle_is_initialized(&g_audio_client_init_lc)) {
966 platform_sleep_us(1000); // 1ms
967 }
968 return 0; // Already initialized by another thread
969 }
970
971 // Clean up previous audio context if it was already initialized
972 // This prevents leaks and thread creation issues on reconnection
973 if (g_audio_context.initialized) {
974 log_debug("Cleaning up previous audio context before reinitializing");
975 audio_destroy(&g_audio_context);
976 // NOTE: Do NOT reset lifecycle - keep it INITIALIZED so only ONE global
977 // audio context is ever created. Subsequent calls will return early.
978 }
979
980 // Initialize WAV dumper for received audio if debugging enabled
981 if (wav_dump_enabled()) {
982 char tmp[PLATFORM_MAX_PATH_LENGTH];
983 if (platform_get_temp_dir(tmp, sizeof(tmp))) {
984 char path[PLATFORM_MAX_PATH_LENGTH];
985 safe_snprintf(path, sizeof(path), "%s/audio_playback_received.wav", tmp);
986 g_wav_playback_received = wav_writer_open(path, AUDIO_SAMPLE_RATE, 1);
987 if (g_wav_playback_received) {
988 log_debug("Audio debugging enabled: dumping received audio to %s", path);
989 }
990 }
991 }
992
993 // Initialize PortAudio context using library function
994 log_debug("DEBUG: About to call audio_init()...");
995 if (audio_init(&g_audio_context) != ASCIICHAT_OK) {
996 log_error("Failed to initialize audio system");
997 // Clean up WAV writer if it was opened
998 if (g_wav_playback_received) {
999 wav_writer_close(g_wav_playback_received);
1000 g_wav_playback_received = NULL;
1001 }
1002 lifecycle_init_abort(&g_audio_client_init_lc);
1003 return -1;
1004 }
1005 log_debug("DEBUG: audio_init() completed successfully");
1006
1007 lifecycle_init_commit(&g_audio_client_init_lc);
1008
1009 // Create unified audio pipeline (handles AEC, AGC, noise suppression, Opus)
1011 pipeline_config.opus_bitrate = 128000; // 128 kbps AUDIO mode for music quality
1012
1013 // Enable echo cancellation, AGC, and essential processing for clear audio
1014 // Noise suppression and VAD can destroy music quality, so keep them disabled
1015 pipeline_config.flags.echo_cancel = true; // ENABLE: removes echo
1016 pipeline_config.flags.jitter_buffer = true; // ENABLE: needed for AEC sync
1017 pipeline_config.flags.noise_suppress = false; // DISABLED: destroys music quality
1018 pipeline_config.flags.agc = true; // ENABLE: boost quiet microphones (35 dB gain)
1019 pipeline_config.flags.vad = false; // DISABLED: destroys music quality
1020 pipeline_config.flags.compressor = true; // ENABLE: prevent clipping from AGC boost
1021 pipeline_config.flags.noise_gate = false; // DISABLED: would cut quiet music passages
1022 pipeline_config.flags.highpass = true; // ENABLE: remove rumble and low-frequency feedback
1023 pipeline_config.flags.lowpass = false; // DISABLED: preserve high-frequency content
1024
1025 // Set jitter buffer margin for smooth playback without excessive delay
1026 // 100ms is conservative - AEC3 will adapt to actual network delay automatically
1027 // We don't tune this; let the system adapt to its actual conditions
1028 pipeline_config.jitter_margin_ns = 100;
1029
1030 log_debug("DEBUG: About to create audio pipeline...");
1031 g_audio_pipeline = client_audio_pipeline_create(&pipeline_config);
1032 log_debug("DEBUG: client_audio_pipeline_create() returned");
1033 if (!g_audio_pipeline) {
1034 log_error("Failed to create audio pipeline");
1035 audio_destroy(&g_audio_context);
1036 // Clean up WAV writer if it was opened
1037 if (g_wav_playback_received) {
1038 wav_writer_close(g_wav_playback_received);
1039 g_wav_playback_received = NULL;
1040 }
1041 return -1;
1042 }
1043
1044 log_debug("Audio pipeline created: %d Hz sample rate, %d bps bitrate", pipeline_config.sample_rate,
1045 pipeline_config.opus_bitrate);
1046
1047 // Associate pipeline with audio context for echo cancellation
1048 // The audio output callback will feed playback samples directly to AEC3 from the speaker output,
1049 // ensuring proper timing synchronization (not from the decode path 50-100ms earlier)
1050 audio_set_pipeline(&g_audio_context, (void *)g_audio_pipeline);
1051
1052 // Audio streams start from protocol_start_connection() after the network
1053 // connection succeeds, so local media playback does not advance while dialing.
1054
1055 return 0;
1056}
1057
1069 log_debug("audio_start_thread called: audio_enabled=%d", GET_OPTION(audio_enabled));
1070
1071 // Register audio atomics for debug sync state monitoring (one-time registration)
1072 static bool audio_atomics_registered = false;
1073 if (!audio_atomics_registered) {
1074 NAMED_REGISTER_ATOMIC(&g_audio_capture_thread_exited, "audio_capture_thread_exit_confirmation", NULL);
1075 NAMED_REGISTER_ATOMIC(&g_audio_sender_should_exit, "audio_sender_exit_flag", NULL);
1076 NAMED_REGISTER_ATOMIC(&g_audio_sender_exited, "audio_sender_exited_confirmation", NULL);
1077 audio_atomics_registered = true;
1078 }
1079
1080 if (!GET_OPTION(audio_enabled)) {
1081 log_debug("Audio is disabled, skipping audio capture thread creation");
1082 return 0; // Audio disabled - not an error
1083 }
1084
1085 // Check if thread is already running (not just created flag)
1086 if (g_audio_capture_thread_created && !atomic_load_bool(&g_audio_capture_thread_exited)) {
1087 log_warn("Audio capture thread already running");
1088 return 0;
1089 }
1090
1091 // If thread exited, allow recreation
1092 if (g_audio_capture_thread_created && atomic_load_bool(&g_audio_capture_thread_exited)) {
1093 log_debug("Previous audio capture thread exited, recreating");
1094 // Use timeout to prevent indefinite blocking
1095 int join_result = asciichat_thread_join_timeout(&g_audio_capture_thread, NULL, 5000 * NS_PER_MS_INT);
1096 if (join_result != 0) {
1097 log_warn("Audio capture thread join timed out after 5s - thread may be deadlocked, "
1098 "forcing thread handle reset (stuck thread resources will not be cleaned up)");
1099 // Thread is stuck - we can't safely reuse the handle, but we can reset our tracking
1100 // This is a resource leak of the stuck thread but continuing is safer than hanging
1101 }
1102 g_audio_capture_thread_created = false;
1103 }
1104
1105 // Note: STREAM_START for audio was already sent by protocol_start_connection()
1106 // No need to send it again here - the protocol layer handles sending combined stream types
1107
1108 // Start audio capture thread
1109 atomic_store_bool(&g_audio_capture_thread_exited, false);
1110 if (thread_pool_spawn(g_client_worker_pool, audio_capture_thread_func, NULL, 4, "audio_capture") != ASCIICHAT_OK) {
1111 log_error("Failed to spawn audio capture thread in worker pool");
1112 LOG_ERRNO_IF_SET("Audio capture thread creation failed");
1113 return -1;
1114 }
1115
1116 g_audio_capture_thread_created = true;
1117
1118 return 0;
1119}
1120
1130 // If audio was never initialized (e.g., in snapshot mode), nothing to stop
1131 if (!lifecycle_is_initialized(&g_audio_client_init_lc)) {
1132 log_debug("[AUDIO_STOP] Audio never initialized, nothing to stop");
1133 return;
1134 }
1135
1136 // Ensure only one thread performs cleanup - prevent double-join and double-cleanup
1137 // First, initialize the lifecycle if it hasn't been yet (moves UNINITIALIZED โ†’ INITIALIZED)
1138 if (!lifecycle_is_initialized(&g_audio_stop_thread_lc)) {
1139 if (!lifecycle_init(&g_audio_stop_thread_lc, "audio_stop_thread")) {
1140 // Already initialized by another thread or being destroyed
1141 log_debug("[AUDIO_STOP] Lifecycle already initialized by another thread");
1142 return;
1143 }
1144 log_debug("[AUDIO_STOP] Initialized stop thread lifecycle");
1145 }
1146
1147 // Now destroy it (moves INITIALIZED โ†’ DESTROYING)
1148 if (!lifecycle_destroy_once(&g_audio_stop_thread_lc)) {
1149 log_debug("[AUDIO_STOP] lifecycle_destroy_once() returned false - already destroying/destroyed");
1150 log_debug("[AUDIO_STOP] Audio stop already in progress or completed, skipping");
1151 return;
1152 }
1153 log_debug("[AUDIO_STOP] lifecycle_destroy_once() returned true, proceeding with audio stop");
1154
1155 // Signal audio worker thread to exit first (stop processing)
1156 log_debug("[AUDIO_STOP] Signaling audio worker thread to stop");
1157 atomic_store_bool(&g_audio_context.worker_should_stop, true);
1158 // Wake up the worker thread if it's waiting on condition variable
1159 mutex_lock(&g_audio_context.worker_mutex);
1160 cond_signal(&g_audio_context.worker_cond);
1161 mutex_unlock(&g_audio_context.worker_mutex);
1162 log_debug("[AUDIO_STOP] Worker thread stop signal sent");
1163
1164 // Signal audio sender thread to exit first.
1165 // This must happen before thread_pool_stop_all() is called, otherwise the sender
1166 // thread will be stuck in cond_wait() and thread_pool_stop_all() will hang forever.
1167 // The sender thread uses a condition variable to wait for packets - we must wake it up.
1168 // NOTE: Only do this if audio_sender_init() was actually called
1169 if (lifecycle_is_initialized(&g_audio_send_queue_lc)) {
1170 log_debug("[AUDIO_STOP] Signaling audio sender thread to exit");
1171 atomic_store_bool(&g_audio_sender_should_exit, true);
1172 mutex_lock(&g_audio_send_queue_mutex);
1173 cond_signal(&g_audio_send_queue_cond);
1174 mutex_unlock(&g_audio_send_queue_mutex);
1175 log_debug("[AUDIO_STOP] Signal sent to sender thread");
1176
1177 // Wait for audio sender thread to exit gracefully
1178 // This must complete before thread_pool_stop_all() to prevent deadlock
1179 // CRITICAL: Longer timeout to ensure thread completes any in-flight send operations
1180 // Network operations can block for 10-50ms, so we need to wait longer
1181 int wait_count = 0;
1182 log_debug("[AUDIO_STOP] Waiting for sender thread to exit, max 500ms");
1183 while (wait_count < 50 && !atomic_load_bool(&g_audio_sender_exited)) {
1184 platform_sleep_us(10 * US_PER_MS_INT); // 10ms * 50 = 500ms max wait
1185 wait_count++;
1186 }
1187
1188 if (!atomic_load_bool(&g_audio_sender_exited)) {
1189 log_warn("[AUDIO_STOP] Audio sender thread not responding after 500ms - will be joined by thread pool");
1190 } else {
1191 log_debug("[AUDIO_STOP] Sender thread exited successfully");
1192 }
1193 } else {
1194 log_debug("[AUDIO_STOP] Audio sender was never initialized (connection failed), skipping sender shutdown");
1195 }
1196
1197 log_debug("[AUDIO_STOP] Checking if capture thread was created: g_audio_capture_thread_created=%d",
1198 g_audio_capture_thread_created);
1199 if (!THREAD_IS_CREATED(g_audio_capture_thread_created)) {
1200 log_debug("[AUDIO_STOP] Capture thread was not created, committing lifecycle and returning");
1201 lifecycle_destroy_commit(&g_audio_stop_thread_lc);
1202 log_debug("[AUDIO_STOP] Lifecycle committed, returning");
1203 return;
1204 }
1205 log_debug("[AUDIO_STOP] Capture thread was created, waiting for it to exit");
1206
1207 // Note: We don't call signal_exit() here because that's for global shutdown only
1208 // The audio capture thread checks server_connection_is_active() to detect connection loss
1209
1210 // Wait for thread to exit gracefully
1211 // Threads check should_exit() every 1-5ms, so timeout can be much shorter than 500ms
1212 int wait_count2 = 0;
1213 log_debug("[AUDIO_STOP] Waiting for capture thread to exit, max 50ms");
1214 while (wait_count2 < 5 && !atomic_load_bool(&g_audio_capture_thread_exited)) {
1215 platform_sleep_us(10 * US_PER_MS_INT); // 10ms * 5 = 50ms max wait (threads respond in ~1-5ms)
1216 wait_count2++;
1217 }
1218
1219 if (!atomic_load_bool(&g_audio_capture_thread_exited)) {
1220 log_warn("[AUDIO_STOP] Audio capture thread not responding - will be joined by thread pool");
1221 } else {
1222 log_debug("[AUDIO_STOP] Capture thread exited successfully");
1223 }
1224
1225 // Thread will be joined by thread_pool_stop_all() in protocol_stop_connection()
1226 g_audio_capture_thread_created = false;
1227
1228 lifecycle_destroy_commit(&g_audio_stop_thread_lc);
1229
1230 log_debug("Audio capture thread stopped");
1231}
1232
1241 return atomic_load_bool(&g_audio_capture_thread_exited);
1242}
1243
1253 if (!GET_OPTION(audio_enabled)) {
1254 return;
1255 }
1256
1257 // Stop capture thread first (stops producing packets)
1259
1260 // Stop async sender thread (drains queue and exits)
1262
1263 // Stop audio stream before destroying pipeline to prevent race condition.
1264 // PortAudio may invoke the callback one more time after we request stop.
1265 // We need to clear the pipeline pointer first so the callback can't access freed memory.
1266 if (g_audio_context.initialized) {
1267 audio_stop_duplex(&g_audio_context);
1268 }
1269
1270 // Clear the pipeline pointer from audio context BEFORE destroying pipeline
1271 // This prevents any lingering PortAudio callbacks from trying to access freed memory
1272 audio_set_pipeline(&g_audio_context, NULL);
1274
1275 // Sleep to allow CoreAudio threads to finish executing callbacks.
1276 // On macOS, CoreAudio's internal threads may continue running after Pa_StopStream() returns.
1277 // The duplex_callback may still be in-flight on other threads. Even after we set the pipeline
1278 // pointer to NULL, a CoreAudio thread may have already cached the pointer before the assignment.
1279 // This sleep ensures all in-flight callbacks have fully completed before we destroy the pipeline.
1280 // WORKAROUND: Skip sleep during shutdown (process is exiting anyway)
1281 // 500ms is sufficient on macOS for CoreAudio's internal thread pool to completely wind down.
1282 // platform_sleep_us(500 * US_PER_MS_INT); // 500ms - macOS CoreAudio needs time to shut down all threads
1283
1284 // Destroy audio pipeline (handles Opus, AEC, etc.)
1285 if (g_audio_pipeline) {
1286 client_audio_pipeline_destroy(g_audio_pipeline);
1287 g_audio_pipeline = NULL;
1288 log_debug("Audio pipeline destroyed");
1289 }
1290
1291 // Close WAV dumpers
1292 if (g_wav_capture_raw) {
1293 wav_writer_close(g_wav_capture_raw);
1294 g_wav_capture_raw = NULL;
1295 log_debug("Closed audio capture raw dump");
1296 }
1297 if (g_wav_capture_processed) {
1298 wav_writer_close(g_wav_capture_processed);
1299 g_wav_capture_processed = NULL;
1300 log_debug("Closed audio capture processed dump");
1301 }
1302 if (g_wav_playback_received) {
1303 wav_writer_close(g_wav_playback_received);
1304 g_wav_playback_received = NULL;
1305 log_debug("Closed audio playback received dump");
1306 }
1307
1308 // Finally destroy the audio context
1309 if (g_audio_context.initialized) {
1310 audio_destroy(&g_audio_context);
1311 }
1312}
1313
1321 return g_audio_pipeline;
1322}
1323
1334int audio_decode_opus(const uint8_t *opus_data, size_t opus_len, float *output, int max_samples) {
1335 if (!g_audio_pipeline || !output || max_samples <= 0) {
1336 return -1;
1337 }
1338
1339 return client_audio_pipeline_playback(g_audio_pipeline, opus_data, (int)opus_len, output, max_samples);
1340}
1341
1349 return &g_audio_context;
1350}
Platform abstraction layer umbrella header providing unified cross-platform API.
void audio_analysis_track_received_sample(float sample)
Track received audio sample.
Definition analysis.c:278
void audio_analysis_track_sent_sample(float sample)
Track sent audio sample.
Definition analysis.c:179
void audio_analysis_track_sent_packet(size_t size)
Track sent packet.
Definition analysis.c:251
Audio Analysis and Debugging Interface.
#define LOG_ERRNO_IF_SET(message)
Check if any error occurred and log it if so.
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
โš›๏ธ Atomic operations abstraction layer with debug tracking
thread_pool_t * g_client_worker_pool
Global client worker thread pool.
int client_audio_pipeline_playback(client_audio_pipeline_t *pipeline, const uint8_t *opus_in, int opus_len, float *output, int num_samples)
Decode Opus packet and process for playback.
client_audio_pipeline_t * client_audio_pipeline_create(const client_audio_pipeline_config_t *config)
Create and initialize a client audio pipeline.
client_audio_pipeline_config_t client_audio_pipeline_default_config(void)
Get default configuration.
int client_audio_pipeline_capture(client_audio_pipeline_t *pipeline, const float *input, int num_samples, uint8_t *opus_out, int max_opus_len)
Process captured audio and encode to Opus.
void client_audio_pipeline_destroy(client_audio_pipeline_t *pipeline)
Destroy a client audio pipeline.
Unified client-side audio processing pipeline.
๐Ÿ“น Unified media capture abstraction for session-based video sources
โš™๏ธ Common definitions, error codes, macros, and types shared throughout the application
asciichat_error_t thread_pool_spawn(void *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *name)
Spawn a worker thread in the pool.
Named object registry for debugging โ€” log identifiable resource names.
โฑ๏ธ FPS tracking utility for monitoring frame throughput across all threads
#define AUDIO_SAMPLE_RATE
Audio sample rate (48kHz professional quality, Opus-compatible)
asciichat_error_t audio_init(audio_context_t *ctx)
Initialize audio context and PortAudio.
size_t audio_ring_buffer_available_read(audio_ring_buffer_t *rb)
Get number of samples available for reading.
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.
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.
void audio_set_pipeline(audio_context_t *ctx, void *pipeline)
Set audio pipeline for echo cancellation.
asciichat_error_t audio_read_samples(audio_context_t *ctx, float *buffer, int num_samples)
Read captured audio samples from capture buffer.
void audio_process_received_samples(const float *samples, int num_samples)
Process received audio samples from server.
void audio_stop_thread()
Stop audio capture thread.
int audio_client_init()
Initialize audio subsystem.
client_audio_pipeline_t * audio_get_pipeline(void)
Get the audio pipeline (for advanced usage)
void audio_sender_init(void)
Initialize async audio sender queue and thread.
void audio_cleanup()
Cleanup audio subsystem.
int audio_decode_opus(const uint8_t *opus_data, size_t opus_len, float *output, int max_samples)
Decode Opus packet using the audio pipeline.
bool audio_thread_exited()
Check if audio capture thread has exited.
audio_context_t * audio_get_context(void)
Get the global audio context for use by other subsystems.
int audio_start_thread()
Start audio capture thread.
bool server_connection_is_active()
Check if server connection is currently active.
bool server_connection_is_lost()
Check if connection loss has been detected.
asciichat_error_t threaded_send_audio_opus_batch(const uint8_t *opus_data, size_t opus_size, const uint16_t *frame_sizes, int frame_count)
Thread-safe Opus audio batch packet transmission.
unsigned short uint16_t
Definition common.h:57
void fps_frame_ns(fps_t *tracker, uint64_t current_time_ns, const char *context)
Track a frame and detect lag conditions (nanosecond version - PRIMARY)
Definition fps.c:52
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
void fps_init(fps_t *tracker, int expected_fps, const char *name)
Initialize FPS tracker.
Definition fps.c:32
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
void asciichat_errno_destroy(void)
Cleanup error system resources.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
#define LOG_RATE_VERY_FAST
Log rate limit: 0.1 seconds (100,000 microseconds) - for very high frequency operations.
Definition log_rates.h:23
#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_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
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 START_TIMER(name_fmt,...)
Start a timer with formatted name.
Definition time.h:333
#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 US_PER_MS_INT
Definition time.h:160
#define STOP_TIMER(name_fmt,...)
Stop a timer with formatted name and return elapsed time.
Definition time.h:403
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
#define AUDIO_BATCH_SAMPLES
Total samples in audio batch (8192 samples)
Definition packet.h:216
int asciichat_thread_join_timeout(asciichat_thread_t *thread, void **retval, uint64_t timeout_ns)
Wait for a thread to complete with timeout.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
#define cond_wait(cond, mutex)
Wait on a condition variable (with debug tracking in debug builds)
Definition cond.h:275
#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
int cond_signal(cond_t *cond)
Signal a condition variable (wake one waiting thread)
bool platform_get_temp_dir(char *temp_dir, size_t path_size)
Get the system temporary directory path.
Definition util.c:74
int cond_init(cond_t *cond, const char *name)
Initialize a condition variable with a name.
void platform_sleep_us(unsigned int us)
High-precision sleep function with microsecond precision.
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
int cond_destroy(cond_t *cond)
Destroy a condition variable.
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
๐Ÿ”Š Audio Capture and Playback Interface for ascii-chat
โš™๏ธ Unified options parsing system for ascii-chat with builder pattern and lock-free access
Platform initialization and static synchronization helpers.
๐Ÿงต Thread lifecycle management helpers
#define THREAD_IS_CREATED(created_flag)
void * asciichat_thread_t
bool lifecycle_destroy_once(lifecycle_t *lc)
Definition lifecycle.c:193
void lifecycle_init_abort(lifecycle_t *lc)
Definition lifecycle.c:99
void lifecycle_destroy_commit(lifecycle_t *lc)
Definition lifecycle.c:236
bool lifecycle_is_initialized(const lifecycle_t *lc)
Definition lifecycle.c:155
void lifecycle_init_commit(lifecycle_t *lc)
Definition lifecycle.c:90
bool lifecycle_init(lifecycle_t *lc, const char *name)
Definition lifecycle.c:26
bool lifecycle_shutdown(lifecycle_t *lc)
Definition lifecycle.c:108
bool lifecycle_init_once(lifecycle_t *lc)
Definition lifecycle.c:52
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
#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
bool should_exit(void)
Definition misc.c:131
ascii-chat Server Mode Entry Point Header
#define OPUS_FRAME_SAMPLES
#define MAX_BATCH_FRAMES
void audio_sender_cleanup(void)
Cleanup async audio sender.
#define AUDIO_SEND_QUEUE_SIZE
#define CAPTURE_READ_SIZE
void audio_sender_finalize(void)
#define BATCH_TIMEOUT_NS
#define OPUS_MAX_PACKET_SIZE
๐Ÿ”Š Session-level audio coordination wrapper
Server-side H.265 decoding pipeline for multi-client streams.
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
Audio context for full-duplex capture and playback.
bool initialized
True if context has been initialized.
audio_ring_buffer_t * playback_buffer
OLD: Network โ†’ Worker (will become network_playback_rb)
cond_t worker_cond
Condition variable for worker signaling.
atomic_t worker_should_stop
Signal worker thread to exit.
audio_ring_buffer_t * capture_buffer
OLD: Worker โ†’ Network (will become processed_capture_rb)
mutex_t worker_mutex
Mutex protecting worker state.
Audio packet for async sending.
uint8_t data[8 *4000]
Pipeline configuration parameters.
client_audio_pipeline_flags_t flags
Client audio pipeline state.
Condition variable type (POSIX: pthread_cond_t with debug tracking)
Definition cond.h:63
FPS tracking state.
Definition fps.h:52
Mutex type (POSIX: pthread_mutex_t with debug tracking)
WAV file writer context.
Definition wav_writer.h:23
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:69
Cross-platform system functions interface for ascii-chat.
๐Ÿงต Generic thread pool abstraction for managing worker threads
โฑ๏ธ High-precision timing utilities using sokol_time.h and uthash
bool wav_dump_enabled(void)
Check if audio dumping is enabled via environment.
Definition wav_writer.c:138
wav_writer_t * wav_writer_open(const char *filepath, int sample_rate, int channels)
Open WAV file for writing.
Definition wav_writer.c:48
int wav_writer_write(wav_writer_t *writer, const float *samples, int num_samples)
Write audio samples to WAV file.
Definition wav_writer.c:94
void wav_writer_close(wav_writer_t *writer)
Close WAV file and finalize header.
Definition wav_writer.c:112
Simple WAV file writer for audio debugging.