ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
Audio Processing

🔊 Audio capture, playback, and sample processing More...

Files

file  audio.c
 ðŸ”Š Client audio management: capture thread, sample processing, and playback coordination
 
file  audio.h
 ascii-chat Client Audio Processing Management Interface
 

Functions

void audio_process_received_samples (const float *samples, int num_samples)
 Process received audio samples from server.
 
int audio_client_init (void)
 Initialize audio subsystem.
 
int audio_start_thread (void)
 Start audio capture thread.
 
void audio_stop_thread (void)
 Stop audio capture thread.
 
bool audio_thread_exited (void)
 Check if audio capture thread has exited.
 
void audio_cleanup (void)
 Cleanup audio subsystem.
 
client_audio_pipeline_t * audio_get_pipeline (void)
 Get the audio pipeline (for advanced usage)
 
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.
 
audio_context_t * audio_get_context (void)
 Get the global audio context for use by other subsystems.
 

Detailed Description

🔊 Audio capture, playback, and sample processing

Audio Processing

Overview

The client audio subsystem manages PortAudio initialization, audio capture from microphone, transmission to server, and playback of received audio samples with jitter buffering.

Implementation: src/client/audio.c, src/client/audio.h

Configuration

Audio Parameters:

  • Sample rate: 44100 Hz
  • Channels: 1 (mono)
  • Format: 32-bit float
  • Batch size: 256 samples (~5.8ms @ 44.1kHz)
  • Ring buffer: 8192 samples (~185ms jitter buffer)

Audio Capture Thread

static void *audio_capture_thread_func(void *arg)
{
(void)arg;
float capture_buffer[AUDIO_BATCH_SIZE];
// Capture audio samples from microphone
int samples_captured = audio_capture_samples(capture_buffer, AUDIO_BATCH_SIZE);
if (samples_captured > 0) {
// Send to server
send_packet_to_server(PACKET_TYPE_AUDIO, capture_buffer,
samples_captured * sizeof(float),
}
// Small sleep to avoid busy loop
platform_sleep_usec(1000); // 1ms
}
atomic_store(&g_audio_thread_exited, true);
return NULL;
}
bool should_exit(void)
Definition main.c:90
bool server_connection_is_active()
Check if server connection is currently active.
uint32_t server_connection_get_client_id()
Get client ID assigned by server.

Audio Playback

Jitter Buffering

void audio_process_received_samples(const float *samples, int num_samples)
{
// Write to ring buffer for jitter compensation
audio_ringbuffer_write(&g_playback_ringbuffer, samples, num_samples);
// PortAudio callback reads from ring buffer
}
static int audio_playback_callback(const void *input, void *output,
unsigned long frameCount,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData)
{
(void)input; (void)timeInfo; (void)statusFlags; (void)userData;
float *out = (float *)output;
// Read from ring buffer
int samples_read = audio_ringbuffer_read(&g_playback_ringbuffer, out, frameCount);
// Fill remaining with silence if underrun
if (samples_read < (int)frameCount) {
memset(&out[samples_read], 0, (frameCount - samples_read) * sizeof(float));
}
return paContinue;
}
void audio_process_received_samples(const float *samples, int num_samples)
Process received audio samples from server.
struct PaStreamCallbackTimeInfo PaStreamCallbackTimeInfo
Definition portaudio.h:16
unsigned long PaStreamCallbackFlags
Definition portaudio.h:15
See also
src/client/audio.c
src/client/audio.h
lib/audio.h
Client Overview

Function Documentation

◆ audio_cleanup()

void audio_cleanup ( )

#include <audio.c>

Cleanup audio subsystem.

Cleanup audio subsystem

Stops audio threads and cleans up PortAudio resources. Called during client shutdown.

Definition at line 1115 of file src/client/audio.c.

1115 {
1116 if (!GET_OPTION(audio_enabled)) {
1117 return;
1118 }
1119
1120 // Stop capture thread first (stops producing packets)
1122
1123 // Stop async sender thread (drains queue and exits)
1124 audio_sender_cleanup();
1125
1126 // Terminate PortAudio FIRST to properly free device resources before cleanup
1127 // This must happen before audio_stop_duplex() and audio_destroy()
1129
1130 // Stop audio stream before destroying pipeline to prevent race condition.
1131 // PortAudio may invoke the callback one more time after we request stop.
1132 // We need to clear the pipeline pointer first so the callback can't access freed memory.
1133 if (g_audio_context.initialized) {
1134 audio_stop_duplex(&g_audio_context);
1135 }
1136
1137 // Clear the pipeline pointer from audio context BEFORE destroying pipeline
1138 // This prevents any lingering PortAudio callbacks from trying to access freed memory
1139 audio_set_pipeline(&g_audio_context, NULL);
1140
1141 // Sleep to allow CoreAudio threads to finish executing callbacks.
1142 // On macOS, CoreAudio's internal threads may continue running after Pa_StopStream() returns.
1143 // The duplex_callback may still be in-flight on other threads. Even after we set the pipeline
1144 // pointer to NULL, a CoreAudio thread may have already cached the pointer before the assignment.
1145 // This sleep ensures all in-flight callbacks have fully completed before we destroy the pipeline.
1146 // 500ms is sufficient on macOS for CoreAudio's internal thread pool to completely wind down.
1147 platform_sleep_us(500 * US_PER_MS_INT); // 500ms - macOS CoreAudio needs time to shut down all threads
1148
1149 // Destroy audio pipeline (handles Opus, AEC, etc.)
1150 if (g_audio_pipeline) {
1151 client_audio_pipeline_destroy(g_audio_pipeline);
1152 g_audio_pipeline = NULL;
1153 log_debug("Audio pipeline destroyed");
1154 }
1155
1156 // Close WAV dumpers
1157 if (g_wav_capture_raw) {
1158 wav_writer_close(g_wav_capture_raw);
1159 g_wav_capture_raw = NULL;
1160 log_debug("Closed audio capture raw dump");
1161 }
1162 if (g_wav_capture_processed) {
1163 wav_writer_close(g_wav_capture_processed);
1164 g_wav_capture_processed = NULL;
1165 log_debug("Closed audio capture processed dump");
1166 }
1167 if (g_wav_playback_received) {
1168 wav_writer_close(g_wav_playback_received);
1169 g_wav_playback_received = NULL;
1170 log_debug("Closed audio playback received dump");
1171 }
1172
1173 // Finally destroy the audio context
1174 if (g_audio_context.initialized) {
1175 audio_destroy(&g_audio_context);
1176 }
1177}
void client_audio_pipeline_destroy(client_audio_pipeline_t *pipeline)
void audio_stop_thread()
Stop audio capture thread.
void audio_destroy(audio_context_t *ctx)
asciichat_error_t audio_stop_duplex(audio_context_t *ctx)
void audio_terminate_portaudio_final(void)
Terminate PortAudio and free all device resources.
void audio_set_pipeline(audio_context_t *ctx, void *pipeline)
void platform_sleep_us(unsigned int us)
void wav_writer_close(wav_writer_t *writer)
Definition wav_writer.c:113

References audio_destroy(), audio_set_pipeline(), audio_stop_duplex(), audio_stop_thread(), audio_terminate_portaudio_final(), client_audio_pipeline_destroy(), platform_sleep_us(), and wav_writer_close().

◆ audio_client_init()

int audio_client_init ( )

#include <audio.c>

Initialize audio subsystem.

Initialize audio subsystem

Sets up PortAudio context, creates the audio pipeline, and starts audio playback/capture if audio is enabled.

Returns
0 on success, negative on error
0 on success, negative on error

Definition at line 904 of file src/client/audio.c.

904 {
905 if (!GET_OPTION(audio_enabled)) {
906 return 0; // Audio disabled - not an error
907 }
908
909 // Initialize WAV dumper for received audio if debugging enabled
910 if (wav_dump_enabled()) {
911 g_wav_playback_received = wav_writer_open("/tmp/audio_playback_received.wav", AUDIO_SAMPLE_RATE, 1);
912 if (g_wav_playback_received) {
913 log_debug("Audio debugging enabled: dumping received audio to /tmp/audio_playback_received.wav");
914 }
915 }
916
917 // Initialize PortAudio context using library function
918 log_debug("DEBUG: About to call audio_init()...");
919 if (audio_init(&g_audio_context) != ASCIICHAT_OK) {
920 log_error("Failed to initialize audio system");
921 // Clean up WAV writer if it was opened
922 if (g_wav_playback_received) {
923 wav_writer_close(g_wav_playback_received);
924 g_wav_playback_received = NULL;
925 }
926 return -1;
927 }
928 log_debug("DEBUG: audio_init() completed successfully");
929
930 // Create unified audio pipeline (handles AEC, AGC, noise suppression, Opus)
931 client_audio_pipeline_config_t pipeline_config = client_audio_pipeline_default_config();
932 pipeline_config.opus_bitrate = 128000; // 128 kbps AUDIO mode for music quality
933
934 // Enable echo cancellation, AGC, and essential processing for clear audio
935 // Noise suppression and VAD can destroy music quality, so keep them disabled
936 pipeline_config.flags.echo_cancel = true; // ENABLE: removes echo
937 pipeline_config.flags.jitter_buffer = true; // ENABLE: needed for AEC sync
938 pipeline_config.flags.noise_suppress = false; // DISABLED: destroys music quality
939 pipeline_config.flags.agc = true; // ENABLE: boost quiet microphones (35 dB gain)
940 pipeline_config.flags.vad = false; // DISABLED: destroys music quality
941 pipeline_config.flags.compressor = true; // ENABLE: prevent clipping from AGC boost
942 pipeline_config.flags.noise_gate = false; // DISABLED: would cut quiet music passages
943 pipeline_config.flags.highpass = true; // ENABLE: remove rumble and low-frequency feedback
944 pipeline_config.flags.lowpass = false; // DISABLED: preserve high-frequency content
945
946 // Set jitter buffer margin for smooth playback without excessive delay
947 // 100ms is conservative - AEC3 will adapt to actual network delay automatically
948 // We don't tune this; let the system adapt to its actual conditions
949 pipeline_config.jitter_margin_ns = 100;
950
951 log_debug("DEBUG: About to create audio pipeline...");
952 g_audio_pipeline = client_audio_pipeline_create(&pipeline_config);
953 log_debug("DEBUG: client_audio_pipeline_create() returned");
954 if (!g_audio_pipeline) {
955 log_error("Failed to create audio pipeline");
956 audio_destroy(&g_audio_context);
957 // Clean up WAV writer if it was opened
958 if (g_wav_playback_received) {
959 wav_writer_close(g_wav_playback_received);
960 g_wav_playback_received = NULL;
961 }
962 return -1;
963 }
964
965 log_debug("Audio pipeline created: %d Hz sample rate, %d bps bitrate", pipeline_config.sample_rate,
966 pipeline_config.opus_bitrate);
967
968 // Associate pipeline with audio context for echo cancellation
969 // The audio output callback will feed playback samples directly to AEC3 from the speaker output,
970 // ensuring proper timing synchronization (not from the decode path 50-100ms earlier)
971 audio_set_pipeline(&g_audio_context, (void *)g_audio_pipeline);
972
973 // Start full-duplex audio (simultaneous capture + playback for perfect AEC3 timing)
974 if (audio_start_duplex(&g_audio_context) != ASCIICHAT_OK) {
975 log_error("Failed to start full-duplex audio");
976 client_audio_pipeline_destroy(g_audio_pipeline);
977 g_audio_pipeline = NULL;
978 audio_destroy(&g_audio_context);
979 // Clean up WAV writer if it was opened
980 if (g_wav_playback_received) {
981 wav_writer_close(g_wav_playback_received);
982 g_wav_playback_received = NULL;
983 }
984 return -1;
985 }
986
987 // Initialize async audio sender (decouples capture from network I/O)
988 audio_sender_init();
989
990 return 0;
991}
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)
asciichat_error_t audio_init(audio_context_t *ctx)
asciichat_error_t audio_start_duplex(audio_context_t *ctx)
bool wav_dump_enabled(void)
Definition wav_writer.c:139
wav_writer_t * wav_writer_open(const char *filepath, int sample_rate, int channels)
Definition wav_writer.c:49

References audio_destroy(), audio_init(), audio_set_pipeline(), audio_start_duplex(), client_audio_pipeline_create(), client_audio_pipeline_default_config(), client_audio_pipeline_destroy(), wav_dump_enabled(), wav_writer_close(), and wav_writer_open().

◆ audio_decode_opus()

int audio_decode_opus ( const uint8_t *  opus_data,
size_t  opus_len,
float *  output,
int  max_samples 
)

#include <audio.c>

Decode Opus packet using the audio pipeline.

Parameters
opus_dataOpus packet data
opus_lenOpus packet length
outputOutput buffer for decoded samples
max_samplesMaximum samples output buffer can hold
Returns
Number of decoded samples, or negative on error

Definition at line 1199 of file src/client/audio.c.

1199 {
1200 if (!g_audio_pipeline || !output || max_samples <= 0) {
1201 return -1;
1202 }
1203
1204 return client_audio_pipeline_playback(g_audio_pipeline, opus_data, (int)opus_len, output, max_samples);
1205}
int client_audio_pipeline_playback(client_audio_pipeline_t *pipeline, const uint8_t *opus_in, int opus_len, float *output, int num_samples)

References client_audio_pipeline_playback().

◆ audio_get_context()

audio_context_t * audio_get_context ( void  )

#include <audio.c>

Get the global audio context for use by other subsystems.

Returns
Pointer to the audio context, or NULL if not initialized
Pointer to the audio context, or NULL if not initialized

Used by capture subsystem to enable microphone fallback when file has no audio.

Definition at line 1213 of file src/client/audio.c.

1213 {
1214 return &g_audio_context;
1215}

Referenced by capture_init().

◆ audio_get_pipeline()

client_audio_pipeline_t * audio_get_pipeline ( void  )

#include <audio.c>

Get the audio pipeline (for advanced usage)

Returns
Pointer to the audio pipeline, or NULL if not initialized

Definition at line 1185 of file src/client/audio.c.

1185 {
1186 return g_audio_pipeline;
1187}

◆ audio_process_received_samples()

void audio_process_received_samples ( const float *  samples,
int  num_samples 
)

#include <audio.c>

Process received audio samples from server.

Process received audio samples from server

Uses the audio pipeline for processing:

  1. Input validation and size checking
  2. Feed samples to pipeline (applies soft clipping)
  3. Feed echo reference for AEC
  4. Submit processed samples to PortAudio playback queue
Parameters
samplesRaw audio sample data from server
num_samplesNumber of samples in the buffer
samplesAudio sample data from server
num_samplesNumber of samples in buffer

Definition at line 399 of file src/client/audio.c.

399 {
400 // Validate parameters
401 if (!samples || num_samples <= 0) {
402 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid audio samples: samples=%p, num_samples=%d", (void *)samples, num_samples);
403 return;
404 }
405
406 if (!GET_OPTION(audio_enabled)) {
407 log_warn_every(NS_PER_MS_INT, "Received audio samples but audio is disabled");
408 return;
409 }
410
411 // Allow both single packets and batched packets
412 if (num_samples > AUDIO_BATCH_SAMPLES) {
413 log_warn("Audio packet too large: %d samples (max %d)", num_samples, AUDIO_BATCH_SAMPLES);
414 return;
415 }
416
417 // Calculate RMS energy of received samples
418 float sum_squares = 0.0f;
419 for (int i = 0; i < num_samples; i++) {
420 sum_squares += samples[i] * samples[i];
421 }
422 float received_rms = sqrtf(sum_squares / num_samples);
423
424 // DUMP: Received audio from server (before playback processing)
425 if (g_wav_playback_received) {
426 wav_writer_write(g_wav_playback_received, samples, num_samples);
427 }
428
429 // Track samples for analysis
430 if (GET_OPTION(audio_analysis_enabled)) {
431 for (int i = 0; i < num_samples; i++) {
433 }
434 }
435
436 // Copy samples to playback buffer (no processing needed - mixer already handled clipping)
437 float audio_buffer[AUDIO_BATCH_SAMPLES];
438 memcpy(audio_buffer, samples, (size_t)num_samples * sizeof(float));
439
440 // DEBUG: Log what we're writing to playback buffer (with first 4 samples to verify audio integrity)
441 static int recv_count = 0;
442 recv_count++;
443 if (recv_count <= 10 || recv_count % 50 == 0) {
444 float peak = 0.0f;
445 for (int i = 0; i < num_samples; i++) {
446 float abs_val = fabsf(samples[i]);
447 if (abs_val > peak)
448 peak = abs_val;
449 }
450 log_debug("CLIENT AUDIO RECV #%d: %d samples, RMS=%.6f, Peak=%.6f, first4=[%.4f,%.4f,%.4f,%.4f]", recv_count,
451 num_samples, received_rms, peak, num_samples > 0 ? samples[0] : 0.0f, num_samples > 1 ? samples[1] : 0.0f,
452 num_samples > 2 ? samples[2] : 0.0f, num_samples > 3 ? samples[3] : 0.0f);
453 }
454
455 // Submit to playback system (goes to jitter buffer and speakers)
456 // NOTE: AEC3's AnalyzeRender is called in output_callback() when audio actually plays,
457 // NOT here. The jitter buffer adds 50-100ms delay, so calling AnalyzeRender here
458 // would give AEC3 the wrong timing and break echo cancellation.
459 audio_write_samples(&g_audio_context, audio_buffer, num_samples);
460
461 // Log latency after writing to playback buffer
462 if (g_audio_context.playback_buffer) {
463 size_t buffer_samples = audio_ring_buffer_available_read(g_audio_context.playback_buffer);
464 float buffer_latency_ms = (float)buffer_samples / 48.0f;
465 log_dev_every(500 * US_PER_MS_INT, "LATENCY: Client playback buffer after recv: %.1fms (%zu samples)",
466 buffer_latency_ms, buffer_samples);
467 }
468
469#ifdef DEBUG_AUDIO
470 log_debug("Processed %d received audio samples", num_samples);
471#endif
472}
void audio_analysis_track_received_sample(float sample)
Definition analysis.c:272
size_t audio_ring_buffer_available_read(audio_ring_buffer_t *rb)
asciichat_error_t audio_write_samples(audio_context_t *ctx, const float *buffer, int num_samples)
int wav_writer_write(wav_writer_t *writer, const float *samples, int num_samples)
Definition wav_writer.c:95

References audio_analysis_track_received_sample(), audio_ring_buffer_available_read(), audio_write_samples(), and wav_writer_write().

◆ audio_start_thread()

int audio_start_thread ( )

#include <audio.c>

Start audio capture thread.

Start audio capture thread

Creates and starts the audio capture thread. Also sends stream start notification to server.

Returns
0 on success, negative on error
0 on success, negative on error

Definition at line 1003 of file src/client/audio.c.

1003 {
1004 log_debug("audio_start_thread called: audio_enabled=%d", GET_OPTION(audio_enabled));
1005
1006 if (!GET_OPTION(audio_enabled)) {
1007 log_debug("Audio is disabled, skipping audio capture thread creation");
1008 return 0; // Audio disabled - not an error
1009 }
1010
1011 // Check if thread is already running (not just created flag)
1012 if (g_audio_capture_thread_created && !atomic_load(&g_audio_capture_thread_exited)) {
1013 log_warn("Audio capture thread already running");
1014 return 0;
1015 }
1016
1017 // If thread exited, allow recreation
1018 if (g_audio_capture_thread_created && atomic_load(&g_audio_capture_thread_exited)) {
1019 log_debug("Previous audio capture thread exited, recreating");
1020 // Use timeout to prevent indefinite blocking
1021 int join_result = asciichat_thread_join_timeout(&g_audio_capture_thread, NULL, 5000 * NS_PER_MS_INT);
1022 if (join_result != 0) {
1023 log_warn("Audio capture thread join timed out after 5s - thread may be deadlocked, "
1024 "forcing thread handle reset (stuck thread resources will not be cleaned up)");
1025 // Thread is stuck - we can't safely reuse the handle, but we can reset our tracking
1026 // This is a resource leak of the stuck thread but continuing is safer than hanging
1027 }
1028 g_audio_capture_thread_created = false;
1029 }
1030
1031 // Notify server we're starting to send audio BEFORE spawning thread
1032 // IMPORTANT: Must send STREAM_START before thread starts sending packets to avoid protocol violation
1033 if (threaded_send_stream_start_packet(STREAM_TYPE_AUDIO) < 0) {
1034 log_error("Failed to send audio stream start packet");
1035 return -1; // Don't start thread if we can't notify server
1036 }
1037
1038 // Start audio capture thread
1039 atomic_store(&g_audio_capture_thread_exited, false);
1040 if (thread_pool_spawn(g_client_worker_pool, audio_capture_thread_func, NULL, 4, "audio_capture") != ASCIICHAT_OK) {
1041 log_error("Failed to spawn audio capture thread in worker pool");
1042 LOG_ERRNO_IF_SET("Audio capture thread creation failed");
1043 return -1;
1044 }
1045
1046 g_audio_capture_thread_created = true;
1047
1048 return 0;
1049}
thread_pool_t * g_client_worker_pool
Global client worker thread pool.
asciichat_error_t threaded_send_stream_start_packet(uint32_t stream_type)
Thread-safe stream start packet transmission.
asciichat_error_t thread_pool_spawn(thread_pool_t *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *thread_name)
Definition thread_pool.c:70

References g_client_worker_pool, thread_pool_spawn(), and threaded_send_stream_start_packet().

Referenced by protocol_start_connection().

◆ audio_stop_thread()

void audio_stop_thread ( )

#include <audio.c>

Stop audio capture thread.

Stop audio capture thread

Gracefully stops the audio capture thread and cleans up resources. Safe to call multiple times.

Definition at line 1059 of file src/client/audio.c.

1059 {
1060 // Signal audio sender thread to exit first.
1061 // This must happen before thread_pool_stop_all() is called, otherwise the sender
1062 // thread will be stuck in cond_wait() and thread_pool_stop_all() will hang forever.
1063 // The sender thread uses a condition variable to wait for packets - we must wake it up.
1064 if (g_audio_send_queue_initialized) {
1065 log_debug("Signaling audio sender thread to exit");
1066 atomic_store(&g_audio_sender_should_exit, true);
1067 mutex_lock(&g_audio_send_queue_mutex);
1068 cond_signal(&g_audio_send_queue_cond);
1069 mutex_unlock(&g_audio_send_queue_mutex);
1070 }
1071
1072 if (!THREAD_IS_CREATED(g_audio_capture_thread_created)) {
1073 return;
1074 }
1075
1076 // Note: We don't call signal_exit() here because that's for global shutdown only
1077 // The audio capture thread checks server_connection_is_active() to detect connection loss
1078
1079 // Wait for thread to exit gracefully
1080 int wait_count = 0;
1081 while (wait_count < 20 && !atomic_load(&g_audio_capture_thread_exited)) {
1082 platform_sleep_us(100 * US_PER_MS_INT); // 100ms
1083 wait_count++;
1084 }
1085
1086 if (!atomic_load(&g_audio_capture_thread_exited)) {
1087 log_warn("Audio capture thread not responding - will be joined by thread pool");
1088 }
1089
1090 // Thread will be joined by thread_pool_stop_all() in protocol_stop_connection()
1091 g_audio_capture_thread_created = false;
1092
1093 log_debug("Audio capture thread stopped");
1094}

References platform_sleep_us().

Referenced by audio_cleanup(), and protocol_stop_connection().

◆ audio_thread_exited()

bool audio_thread_exited ( )

#include <audio.c>

Check if audio capture thread has exited.

Check if audio capture thread has exited

Returns
true if thread has exited, false otherwise
true if thread exited, false otherwise

Definition at line 1103 of file src/client/audio.c.

1103 {
1104 return atomic_load(&g_audio_capture_thread_exited);
1105}