ascii-chat 0.11.33
Video chat in your terminal
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.
 
void audio_sender_init (void)
 Initialize audio sender thread.
 

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 server_connection_is_active()
Check if server connection is currently active.
uint32_t server_connection_get_client_id()
Get client ID assigned by server.
bool should_exit(void)
Definition misc.c:131

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.

Closes PortAudio stream and releases audio resources. Must be called after audio_stop_thread() and before process exit.

The cleanup process:

  1. Stops PortAudio stream (interrupts playback)
  2. Closes audio device
  3. Destroys Opus encoder/decoder
  4. Frees ring buffer
  5. Terminates PortAudio

Must be called in proper order during shutdown to avoid resource leaks.

See also
audio_client_init "Initialize audio subsystem"

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

1252 {
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}
void client_audio_pipeline_destroy(client_audio_pipeline_t *pipeline)
Destroy a client audio pipeline.
void audio_destroy(audio_context_t *ctx)
Destroy audio context and clean up resources.
asciichat_error_t audio_stop_duplex(audio_context_t *ctx)
Stop full-duplex audio.
void audio_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.
void audio_stop_thread()
Stop audio capture thread.
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
void audio_sender_cleanup(void)
Cleanup async audio sender.
bool initialized
True if context has been initialized.
void wav_writer_close(wav_writer_t *writer)
Close WAV file and finalize header.
Definition wav_writer.c:112

References audio_destroy(), audio_sender_cleanup(), audio_set_pipeline(), audio_stop_duplex(), audio_stop_thread(), audio_terminate_portaudio_final(), client_audio_pipeline_destroy(), GET_OPTION, audio_context_t::initialized, log_debug, 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

Sets up PortAudio, selects audio device, and initializes Opus codec. Must be called before spawning audio threads.

Initialization steps:

  1. Initialize PortAudio library
  2. Enumerate audio devices (for –audio-device selection)
  3. Open audio device for input (microphone) and output (speakers)
  4. Create Opus encoder for capture
  5. Create Opus decoder for playback
  6. Initialize ring buffer for jitter buffering
  7. Start PortAudio stream

Options affecting initialization:

  • --audio-device <index>: Which audio device to use (default: system default)
  • --audio flag must be set (audio disabled by default)
Returns
0 on success, negative on error:
  • ERROR_AUDIO: Device not found or PortAudio error
  • ERROR_MEMORY: Insufficient memory for buffers
Note
Optional - only called if –audio flag is set. Client works without audio (defaults to disabled).
See also
audio_cleanup "Cleanup audio resources"

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

956 {
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}
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.
#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.
@ ASCIICHAT_OK
Definition error_codes.h:51
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
bool platform_get_temp_dir(char *temp_dir, size_t path_size)
Get the system temporary directory path.
Definition util.c:74
void platform_sleep_us(unsigned int us)
High-precision sleep function with microsecond precision.
void lifecycle_init_abort(lifecycle_t *lc)
Definition lifecycle.c:99
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_once(lifecycle_t *lc)
Definition lifecycle.c:52
Pipeline configuration parameters.
client_audio_pipeline_flags_t flags
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:69
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

References client_audio_pipeline_flags_t::agc, ASCIICHAT_OK, audio_destroy(), audio_init(), AUDIO_SAMPLE_RATE, audio_set_pipeline(), client_audio_pipeline_create(), client_audio_pipeline_default_config(), client_audio_pipeline_flags_t::compressor, client_audio_pipeline_flags_t::echo_cancel, client_audio_pipeline_config_t::flags, GET_OPTION, client_audio_pipeline_flags_t::highpass, audio_context_t::initialized, client_audio_pipeline_flags_t::jitter_buffer, client_audio_pipeline_config_t::jitter_margin_ns, lifecycle_init_abort(), lifecycle_init_commit(), lifecycle_init_once(), lifecycle_is_initialized(), log_debug, log_error, client_audio_pipeline_flags_t::lowpass, client_audio_pipeline_flags_t::noise_gate, client_audio_pipeline_flags_t::noise_suppress, client_audio_pipeline_config_t::opus_bitrate, platform_get_temp_dir(), PLATFORM_MAX_PATH_LENGTH, platform_sleep_us(), safe_snprintf(), client_audio_pipeline_config_t::sample_rate, client_audio_pipeline_flags_t::vad, 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

Decodes a single Opus-encoded audio packet to PCM samples. Can be used independently of the audio pipeline for testing or external audio processing.

Parameters
opus_dataOpus packet data (variable-length frame)
opus_lenOpus packet length in bytes
outputOutput buffer for decoded samples (float array)
max_samplesMaximum samples output buffer can hold
Returns
Number of decoded samples, or negative on error (invalid Opus data)
Note
The pipeline must be initialized (audio_client_init) for this to work.

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

1334 {
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}
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.

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

Returns the audio context for access to low-level audio state. Used by capture subsystem to enable microphone fallback when media file has no audio.

Returns
Pointer to the audio context, or NULL if not initialized
Note
Most code should use the higher-level interface functions rather than direct context access.

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

1348 {
1349 return &g_audio_context;
1350}

Referenced by capture_init(), and protocol_start_connection().

◆ 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

Returns the internal audio pipeline structure for direct access to encoder/decoder or buffer manipulation (advanced usage only).

Returns
Pointer to the audio pipeline, or NULL if not initialized
Note
Most code should use audio_process_received_samples() and the capture thread instead of direct pipeline access.

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

1320 {
1321 return g_audio_pipeline;
1322}

◆ 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

Queues received audio samples for playback. Handles jitter buffering to smooth out network timing variations. Called by protocol thread when processing PACKET_TYPE_AUDIO_BATCH from server.

The samples are queued in a ring buffer that smooths out timing variations. The playback thread drains this buffer at a constant rate.

Parameters
samplesAudio sample data from server (32-bit float, mono)
num_samplesNumber of samples in buffer
Note
This is called by the protocol thread - must be thread-safe. Ring buffer handles synchronization internally.

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

450 {
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}
void audio_analysis_track_received_sample(float sample)
Track received audio sample.
Definition analysis.c:278
size_t audio_ring_buffer_available_read(audio_ring_buffer_t *rb)
Get number of samples available for reading.
asciichat_error_t audio_write_samples(audio_context_t *ctx, const float *buffer, int num_samples)
Write audio samples to playback buffer.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define NS_PER_MS_INT
Definition time.h:156
#define US_PER_MS_INT
Definition time.h:160
#define AUDIO_BATCH_SAMPLES
Total samples in audio batch (8192 samples)
Definition packet.h:216
#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
audio_ring_buffer_t * playback_buffer
OLD: Network → Worker (will become network_playback_rb)
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

References audio_analysis_track_received_sample(), AUDIO_BATCH_SAMPLES, audio_ring_buffer_available_read(), audio_write_samples(), ERROR_INVALID_PARAM, GET_OPTION, log_debug, log_dev_every, log_warn, log_warn_every, NS_PER_MS_INT, audio_context_t::playback_buffer, SET_ERRNO, US_PER_MS_INT, and wav_writer_write().

◆ audio_sender_init()

void audio_sender_init ( void  )

#include <audio.h>

Initialize audio sender thread.

Spawns the audio sender thread in the worker pool. This thread pulls packets from the send queue (populated by the capture thread) and sends them to the server.

Must be called after audio_client_init() and BEFORE audio_start_thread() during protocol_start_connection() after server connection succeeds.

Note
This is called automatically by protocol_start_connection() and should not be called directly in most code.
See also
audio_start_thread "Start audio capture thread"

Initialize audio sender thread.

Uses mutex protection to prevent TOCTOU race conditions where multiple threads might attempt initialization simultaneously.

Called from protocol_start_connection() after server connection succeeds, to prevent deadlock if connection fails before capture thread starts.

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

345 {
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}
#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
thread_pool_t * g_client_worker_pool
Global client worker thread pool.
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.
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
int cond_init(cond_t *cond, const char *name)
Initialize a condition variable with a name.
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
bool lifecycle_init(lifecycle_t *lc, const char *name)
Definition lifecycle.c:26

References ASCIICHAT_OK, atomic_store_bool(), cond_init(), g_client_worker_pool, lifecycle_init(), LOG_ERRNO_IF_SET, log_error, log_info, mutex_destroy(), mutex_init(), and thread_pool_spawn().

Referenced by protocol_start_connection().

◆ 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

Spawns audio capture/transmission thread in global worker pool. Thread continuously captures audio from microphone, encodes with Opus, and sends to server.

The capture loop:

  1. Captures samples from PortAudio input stream
  2. Encodes with Opus encoder (variable bitrate)
  3. Sends PACKET_TYPE_AUDIO_OPUS packet to server
  4. Checks exit signals
  5. Repeats until exit signal or connection loss

Must be called after audio_client_init() and server connection established.

Returns
0 on success, negative on error (thread pool full)
Note
Audio playback (from ring buffer) happens continuously in PortAudio callback, not in a separate thread.
See also
audio_stop_thread "Stop audio capture thread"

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

1068 {
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}
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
int asciichat_thread_join_timeout(asciichat_thread_t *thread, void **retval, uint64_t timeout_ns)
Wait for a thread to complete with timeout.

References ASCIICHAT_OK, asciichat_thread_join_timeout(), atomic_load_bool(), atomic_store_bool(), g_client_worker_pool, GET_OPTION, log_debug, LOG_ERRNO_IF_SET, log_error, log_warn, NAMED_REGISTER_ATOMIC, NS_PER_MS_INT, and thread_pool_spawn().

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.

Signals audio capture thread to exit and waits for it to join. Must be called before reconnection or shutdown.

The function:

  1. Sets exit flag
  2. Polls audio_thread_exited() until thread exits
  3. Drains remaining samples in ring buffer

Playback continues until ring buffer is empty (graceful fade-out).

Note
Audio playback thread is internal to PortAudio callback and continues until PortAudio stream is closed in audio_cleanup().
See also
audio_start_thread "Start audio capture thread"

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

1129 {
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}
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
int cond_signal(cond_t *cond)
Signal a condition variable (wake one waiting thread)
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
#define THREAD_IS_CREATED(created_flag)
bool lifecycle_destroy_once(lifecycle_t *lc)
Definition lifecycle.c:193
void lifecycle_destroy_commit(lifecycle_t *lc)
Definition lifecycle.c:236
cond_t worker_cond
Condition variable for worker signaling.
atomic_t worker_should_stop
Signal worker thread to exit.
mutex_t worker_mutex
Mutex protecting worker state.

References atomic_load_bool(), atomic_store_bool(), cond_signal(), lifecycle_destroy_commit(), lifecycle_destroy_once(), lifecycle_init(), lifecycle_is_initialized(), log_debug, log_warn, mutex_lock, mutex_unlock, platform_sleep_us(), THREAD_IS_CREATED, US_PER_MS_INT, audio_context_t::worker_cond, audio_context_t::worker_mutex, and audio_context_t::worker_should_stop.

Referenced by audio_cleanup(), protocol_stop_connection(), and session_client_like_run().

◆ 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

Atomically checks whether the audio capture thread has finished. Main thread uses this to wait for clean shutdown before reconnection.

Returns
true if thread exited, false otherwise
Note
Playback thread (PortAudio callback) exits when PortAudio stream is closed, not when capture thread exits.

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

1240 {
1241 return atomic_load_bool(&g_audio_capture_thread_exited);
1242}

References atomic_load_bool().