ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
audio.c File Reference

🔊 Audio capture and playback using PortAudio with buffer management More...

Go to the source code of this file.

Macros

#define WORKER_BATCH_FRAMES   128
 
#define WORKER_BATCH_SAMPLES   (WORKER_BATCH_FRAMES * AUDIO_CHANNELS)
 
#define WORKER_TIMEOUT_MS   1
 

Functions

void audio_terminate_portaudio_final (void)
 Terminate PortAudio and free all device resources.
 
void resample_linear (const float *src, size_t src_samples, float *dst, size_t dst_samples, double src_rate, double dst_rate)
 
audio_ring_buffer_t * audio_ring_buffer_create (void)
 
audio_ring_buffer_t * audio_ring_buffer_create_for_capture (void)
 
void audio_ring_buffer_destroy (audio_ring_buffer_t *rb)
 
void audio_ring_buffer_clear (audio_ring_buffer_t *rb)
 
asciichat_error_t audio_ring_buffer_write (audio_ring_buffer_t *rb, const float *data, int samples)
 
size_t audio_ring_buffer_read (audio_ring_buffer_t *rb, float *data, size_t samples)
 
size_t audio_ring_buffer_peek (audio_ring_buffer_t *rb, float *data, size_t samples)
 Peek at available samples without consuming them (for AEC3 render signal)
 
size_t audio_ring_buffer_available_read (audio_ring_buffer_t *rb)
 
size_t audio_ring_buffer_available_write (audio_ring_buffer_t *rb)
 
asciichat_error_t audio_init (audio_context_t *ctx)
 
void audio_destroy (audio_context_t *ctx)
 
void audio_set_pipeline (audio_context_t *ctx, void *pipeline)
 
void audio_flush_playback_buffers (audio_context_t *ctx)
 
asciichat_error_t audio_start_duplex (audio_context_t *ctx)
 
asciichat_error_t audio_stop_duplex (audio_context_t *ctx)
 
asciichat_error_t audio_read_samples (audio_context_t *ctx, float *buffer, int num_samples)
 
asciichat_error_t audio_write_samples (audio_context_t *ctx, const float *buffer, int num_samples)
 
asciichat_error_t audio_list_input_devices (audio_device_info_t **out_devices, unsigned int *out_count)
 
asciichat_error_t audio_list_output_devices (audio_device_info_t **out_devices, unsigned int *out_count)
 
void audio_free_device_list (audio_device_info_t *devices)
 
asciichat_error_t audio_dequantize_samples (const uint8_t *samples_ptr, uint32_t total_samples, float *out_samples)
 
asciichat_error_t audio_set_realtime_priority (void)
 
asciichat_error_t audio_parse_batch_header (const void *data, size_t len, audio_batch_info_t *out_batch)
 
asciichat_error_t audio_validate_batch_params (const audio_batch_info_t *batch)
 
bool audio_is_supported_sample_rate (uint32_t sample_rate)
 
bool audio_should_enable_microphone (audio_source_t source, bool has_media_audio)
 

Detailed Description

🔊 Audio capture and playback using PortAudio with buffer management

Definition in file lib/audio/audio.c.

Macro Definition Documentation

◆ WORKER_BATCH_FRAMES

#define WORKER_BATCH_FRAMES   128

Definition at line 132 of file lib/audio/audio.c.

◆ WORKER_BATCH_SAMPLES

#define WORKER_BATCH_SAMPLES   (WORKER_BATCH_FRAMES * AUDIO_CHANNELS)

Definition at line 133 of file lib/audio/audio.c.

◆ WORKER_TIMEOUT_MS

#define WORKER_TIMEOUT_MS   1

Definition at line 134 of file lib/audio/audio.c.

Function Documentation

◆ audio_dequantize_samples()

asciichat_error_t audio_dequantize_samples ( const uint8_t *  samples_ptr,
uint32_t  total_samples,
float *  out_samples 
)

Definition at line 1918 of file lib/audio/audio.c.

1918 {
1919 if (!samples_ptr || !out_samples || total_samples == 0) {
1920 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for audio dequantization");
1921 }
1922
1923 for (uint32_t i = 0; i < total_samples; i++) {
1924 uint32_t network_sample;
1925 // Use memcpy to safely handle potential misalignment from packet header
1926 memcpy(&network_sample, samples_ptr + i * sizeof(uint32_t), sizeof(uint32_t));
1927 int32_t scaled = (int32_t)NET_TO_HOST_U32(network_sample);
1928 out_samples[i] = (float)scaled / 2147483647.0f;
1929 }
1930
1931 return ASCIICHAT_OK;
1932}

Referenced by handle_audio_batch_packet().

◆ audio_destroy()

void audio_destroy ( audio_context_t *  ctx)

Definition at line 1270 of file lib/audio/audio.c.

1270 {
1271 if (!ctx) {
1272 return;
1273 }
1274
1275 // Always release PortAudio refcount if it was incremented
1276 // audio_init() calls Pa_Initialize() very early, and if it fails partway through,
1277 // ctx->initialized will be false. But we MUST still call audio_release_portaudio()
1278 // to properly decrement the refcount and allow Pa_Terminate() to be called.
1279 if (ctx->initialized) {
1280
1281 // Stop duplex stream if running (this also stops the worker thread)
1282 if (ctx->running) {
1283 audio_stop_duplex(ctx);
1284 }
1285
1286 // Ensure worker thread is stopped even if streams weren't running
1287 if (ctx->worker_running) {
1288 log_debug("Stopping worker thread during audio_destroy");
1289 atomic_store(&ctx->worker_should_stop, true);
1290 cond_signal(&ctx->worker_cond); // Wake up worker if waiting
1291 asciichat_thread_join(&ctx->worker_thread, NULL);
1292 ctx->worker_running = false;
1293 }
1294
1295 mutex_lock(&ctx->state_mutex);
1296
1297 // Destroy all ring buffers (old + new)
1298 audio_ring_buffer_destroy(ctx->capture_buffer);
1299 audio_ring_buffer_destroy(ctx->playback_buffer);
1300 audio_ring_buffer_destroy(ctx->raw_capture_rb);
1301 audio_ring_buffer_destroy(ctx->raw_render_rb);
1302 audio_ring_buffer_destroy(ctx->processed_playback_rb);
1303 audio_ring_buffer_destroy(ctx->render_buffer); // May be NULL, that's OK
1304
1305 // Free pre-allocated worker buffers
1306 SAFE_FREE(ctx->worker_capture_batch);
1307 SAFE_FREE(ctx->worker_render_batch);
1308 SAFE_FREE(ctx->worker_playback_batch);
1309
1310 // Destroy worker synchronization primitives
1311 cond_destroy(&ctx->worker_cond);
1312 mutex_destroy(&ctx->worker_mutex);
1313
1314 ctx->initialized = false;
1315
1316 mutex_unlock(&ctx->state_mutex);
1317 mutex_destroy(&ctx->state_mutex);
1318
1319 log_debug("Audio system cleanup complete (all resources released)");
1320 } else {
1321 }
1322
1323 // MUST happen for both initialized and non-initialized contexts
1324 // If audio_init() called Pa_Initialize() but failed partway, refcount must be decremented
1325 audio_release_portaudio();
1326}
void audio_ring_buffer_destroy(audio_ring_buffer_t *rb)
asciichat_error_t audio_stop_duplex(audio_context_t *ctx)
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
Definition threading.c:46
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21

References asciichat_thread_join(), audio_ring_buffer_destroy(), audio_stop_duplex(), and mutex_destroy().

Referenced by audio_cleanup(), audio_client_init(), session_audio_destroy(), and session_client_like_run().

◆ audio_flush_playback_buffers()

void audio_flush_playback_buffers ( audio_context_t *  ctx)

Definition at line 1334 of file lib/audio/audio.c.

1334 {
1335 if (!ctx || !ctx->initialized) {
1336 return;
1337 }
1338
1339 if (ctx->playback_buffer) {
1340 audio_ring_buffer_clear(ctx->playback_buffer);
1341 }
1342 if (ctx->processed_playback_rb) {
1343 audio_ring_buffer_clear(ctx->processed_playback_rb);
1344 }
1345 if (ctx->render_buffer) {
1346 audio_ring_buffer_clear(ctx->render_buffer);
1347 }
1348 if (ctx->raw_render_rb) {
1349 audio_ring_buffer_clear(ctx->raw_render_rb);
1350 }
1351}
void audio_ring_buffer_clear(audio_ring_buffer_t *rb)

References audio_ring_buffer_clear().

Referenced by session_handle_keyboard_input().

◆ audio_free_device_list()

void audio_free_device_list ( audio_device_info_t *  devices)

Definition at line 1914 of file lib/audio/audio.c.

1914 {
1915 SAFE_FREE(devices);
1916}

◆ audio_init()

asciichat_error_t audio_init ( audio_context_t *  ctx)

Definition at line 1132 of file lib/audio/audio.c.

1132 {
1133 log_debug("audio_init: starting, ctx=%p", (void *)ctx);
1134 if (!ctx) {
1135 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx is NULL");
1136 }
1137
1138 SAFE_MEMSET(ctx, sizeof(audio_context_t), 0, sizeof(audio_context_t));
1139
1140 if (mutex_init(&ctx->state_mutex) != 0) {
1141 return SET_ERRNO(ERROR_THREAD, "Failed to initialize audio context mutex");
1142 }
1143
1144 // NOTE: PortAudio initialization deferred to audio_start_duplex() where streams are actually opened
1145 // This avoids Pa_Initialize() overhead for contexts that might not start duplex
1146 // and prevents premature ALSA device allocation and memory leaks
1147
1148 // Create capture buffer WITHOUT jitter buffering (PortAudio writes directly from microphone)
1149 ctx->capture_buffer = audio_ring_buffer_create_for_capture();
1150 if (!ctx->capture_buffer) {
1151 mutex_destroy(&ctx->state_mutex);
1152 return SET_ERRNO(ERROR_MEMORY, "Failed to create capture buffer");
1153 }
1154
1155 ctx->playback_buffer = audio_ring_buffer_create();
1156 if (!ctx->playback_buffer) {
1157 audio_ring_buffer_destroy(ctx->capture_buffer);
1158 mutex_destroy(&ctx->state_mutex);
1159 return SET_ERRNO(ERROR_MEMORY, "Failed to create playback buffer");
1160 }
1161
1162 // Create new ring buffers for worker thread architecture
1163 ctx->raw_capture_rb = audio_ring_buffer_create_for_capture();
1164 if (!ctx->raw_capture_rb) {
1165 audio_ring_buffer_destroy(ctx->playback_buffer);
1166 audio_ring_buffer_destroy(ctx->capture_buffer);
1167 mutex_destroy(&ctx->state_mutex);
1168 return SET_ERRNO(ERROR_MEMORY, "Failed to create raw capture buffer");
1169 }
1170
1171 ctx->raw_render_rb = audio_ring_buffer_create_for_capture();
1172 if (!ctx->raw_render_rb) {
1173 audio_ring_buffer_destroy(ctx->raw_capture_rb);
1174 audio_ring_buffer_destroy(ctx->playback_buffer);
1175 audio_ring_buffer_destroy(ctx->capture_buffer);
1176 mutex_destroy(&ctx->state_mutex);
1177 return SET_ERRNO(ERROR_MEMORY, "Failed to create raw render buffer");
1178 }
1179
1180 ctx->processed_playback_rb = audio_ring_buffer_create();
1181 if (!ctx->processed_playback_rb) {
1182 audio_ring_buffer_destroy(ctx->raw_render_rb);
1183 audio_ring_buffer_destroy(ctx->raw_capture_rb);
1184 audio_ring_buffer_destroy(ctx->playback_buffer);
1185 audio_ring_buffer_destroy(ctx->capture_buffer);
1186 mutex_destroy(&ctx->state_mutex);
1187 return SET_ERRNO(ERROR_MEMORY, "Failed to create processed playback buffer");
1188 }
1189
1190 // Initialize worker thread infrastructure
1191 if (mutex_init(&ctx->worker_mutex) != 0) {
1192 audio_ring_buffer_destroy(ctx->processed_playback_rb);
1193 audio_ring_buffer_destroy(ctx->raw_render_rb);
1194 audio_ring_buffer_destroy(ctx->raw_capture_rb);
1195 audio_ring_buffer_destroy(ctx->playback_buffer);
1196 audio_ring_buffer_destroy(ctx->capture_buffer);
1197 audio_release_portaudio();
1198 mutex_destroy(&ctx->state_mutex);
1199 return SET_ERRNO(ERROR_THREAD, "Failed to initialize worker mutex");
1200 }
1201
1202 if (cond_init(&ctx->worker_cond) != 0) {
1203 mutex_destroy(&ctx->worker_mutex);
1204 audio_ring_buffer_destroy(ctx->processed_playback_rb);
1205 audio_ring_buffer_destroy(ctx->raw_render_rb);
1206 audio_ring_buffer_destroy(ctx->raw_capture_rb);
1207 audio_ring_buffer_destroy(ctx->playback_buffer);
1208 audio_ring_buffer_destroy(ctx->capture_buffer);
1209 audio_release_portaudio();
1210 mutex_destroy(&ctx->state_mutex);
1211 return SET_ERRNO(ERROR_THREAD, "Failed to initialize worker condition variable");
1212 }
1213
1214 // Allocate pre-allocated worker buffers (avoid malloc in worker loop)
1215 ctx->worker_capture_batch = SAFE_MALLOC(WORKER_BATCH_SAMPLES * sizeof(float), float *);
1216 if (!ctx->worker_capture_batch) {
1217 cond_destroy(&ctx->worker_cond);
1218 mutex_destroy(&ctx->worker_mutex);
1219 audio_ring_buffer_destroy(ctx->processed_playback_rb);
1220 audio_ring_buffer_destroy(ctx->raw_render_rb);
1221 audio_ring_buffer_destroy(ctx->raw_capture_rb);
1222 audio_ring_buffer_destroy(ctx->playback_buffer);
1223 audio_ring_buffer_destroy(ctx->capture_buffer);
1224 audio_release_portaudio();
1225 mutex_destroy(&ctx->state_mutex);
1226 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate worker capture batch buffer");
1227 }
1228
1229 ctx->worker_render_batch = SAFE_MALLOC(WORKER_BATCH_SAMPLES * sizeof(float), float *);
1230 if (!ctx->worker_render_batch) {
1231 SAFE_FREE(ctx->worker_capture_batch);
1232 cond_destroy(&ctx->worker_cond);
1233 mutex_destroy(&ctx->worker_mutex);
1234 audio_ring_buffer_destroy(ctx->processed_playback_rb);
1235 audio_ring_buffer_destroy(ctx->raw_render_rb);
1236 audio_ring_buffer_destroy(ctx->raw_capture_rb);
1237 audio_ring_buffer_destroy(ctx->playback_buffer);
1238 audio_ring_buffer_destroy(ctx->capture_buffer);
1239 audio_release_portaudio();
1240 mutex_destroy(&ctx->state_mutex);
1241 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate worker render batch buffer");
1242 }
1243
1244 ctx->worker_playback_batch = SAFE_MALLOC(WORKER_BATCH_SAMPLES * sizeof(float), float *);
1245 if (!ctx->worker_playback_batch) {
1246 SAFE_FREE(ctx->worker_render_batch);
1247 SAFE_FREE(ctx->worker_capture_batch);
1248 cond_destroy(&ctx->worker_cond);
1249 mutex_destroy(&ctx->worker_mutex);
1250 audio_ring_buffer_destroy(ctx->processed_playback_rb);
1251 audio_ring_buffer_destroy(ctx->raw_render_rb);
1252 audio_ring_buffer_destroy(ctx->raw_capture_rb);
1253 audio_ring_buffer_destroy(ctx->playback_buffer);
1254 audio_ring_buffer_destroy(ctx->capture_buffer);
1255 audio_release_portaudio();
1256 mutex_destroy(&ctx->state_mutex);
1257 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate worker playback batch buffer");
1258 }
1259
1260 // Initialize worker thread state (thread will be started in audio_start_duplex)
1261 ctx->worker_running = false;
1262 atomic_store(&ctx->worker_should_stop, false);
1263
1264 ctx->initialized = true;
1265 atomic_store(&ctx->shutting_down, false);
1266 log_info("Audio system initialized successfully (worker thread architecture enabled)");
1267 return ASCIICHAT_OK;
1268}
audio_ring_buffer_t * audio_ring_buffer_create(void)
#define WORKER_BATCH_SAMPLES
audio_ring_buffer_t * audio_ring_buffer_create_for_capture(void)
int mutex_init(mutex_t *mutex)
Definition threading.c:16

References audio_ring_buffer_create(), audio_ring_buffer_create_for_capture(), audio_ring_buffer_destroy(), mutex_destroy(), mutex_init(), and WORKER_BATCH_SAMPLES.

Referenced by audio_client_init(), session_audio_create(), and session_client_like_run().

◆ audio_is_supported_sample_rate()

bool audio_is_supported_sample_rate ( uint32_t  sample_rate)

Definition at line 2014 of file lib/audio/audio.c.

2014 {
2015 // List of commonly supported audio sample rates
2016 static const uint32_t supported_rates[] = {
2017 8000, // Telephone quality
2018 16000, // Wideband telephony
2019 24000, // High quality speech
2020 32000, // Good for video
2021 44100, // CD quality (less common in VoIP)
2022 48000, // Standard professional
2023 96000, // High-end professional
2024 192000, // Ultra-high-end mastering
2025 };
2026
2027 const size_t rate_count = sizeof(supported_rates) / sizeof(supported_rates[0]);
2028 for (size_t i = 0; i < rate_count; i++) {
2029 if (sample_rate == supported_rates[i]) {
2030 return true;
2031 }
2032 }
2033
2034 return false;
2035}

Referenced by audio_validate_batch_params().

◆ audio_list_input_devices()

asciichat_error_t audio_list_input_devices ( audio_device_info_t **  out_devices,
unsigned int *  out_count 
)

Definition at line 1906 of file lib/audio/audio.c.

1906 {
1907 return audio_list_devices_internal(out_devices, out_count, true);
1908}

◆ audio_list_output_devices()

asciichat_error_t audio_list_output_devices ( audio_device_info_t **  out_devices,
unsigned int *  out_count 
)

Definition at line 1910 of file lib/audio/audio.c.

1910 {
1911 return audio_list_devices_internal(out_devices, out_count, false);
1912}

◆ audio_parse_batch_header()

asciichat_error_t audio_parse_batch_header ( const void *  data,
size_t  len,
audio_batch_info_t out_batch 
)

Definition at line 1948 of file lib/audio/audio.c.

1948 {
1949 if (!data) {
1950 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch header data pointer is NULL");
1951 }
1952
1953 if (!out_batch) {
1954 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch info output pointer is NULL");
1955 }
1956
1957 if (len < sizeof(audio_batch_packet_t)) {
1958 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch header too small (len=%zu, expected=%zu)", len,
1959 sizeof(audio_batch_packet_t));
1960 }
1961
1962 const audio_batch_packet_t *batch_header = (const audio_batch_packet_t *)data;
1963
1964 // Unpack network byte order values to host byte order
1965 out_batch->batch_count = ntohl(batch_header->batch_count);
1966 out_batch->total_samples = ntohl(batch_header->total_samples);
1967 out_batch->sample_rate = ntohl(batch_header->sample_rate);
1968 out_batch->channels = ntohl(batch_header->channels);
1969
1970 return ASCIICHAT_OK;
1971}

References audio_batch_info_t::batch_count, audio_batch_info_t::channels, audio_batch_info_t::sample_rate, and audio_batch_info_t::total_samples.

Referenced by handle_audio_batch_packet().

◆ audio_read_samples()

asciichat_error_t audio_read_samples ( audio_context_t *  ctx,
float *  buffer,
int  num_samples 
)

Definition at line 1791 of file lib/audio/audio.c.

1791 {
1792 if (!ctx || !ctx->initialized || !buffer || num_samples <= 0) {
1793 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, buffer=%p, num_samples=%d", ctx, buffer,
1794 num_samples);
1795 }
1796
1797 // audio_ring_buffer_read now returns number of samples read, not error code
1798 int samples_read = audio_ring_buffer_read(ctx->capture_buffer, buffer, num_samples);
1799 return (samples_read >= 0) ? ASCIICHAT_OK : ERROR_AUDIO;
1800}
size_t audio_ring_buffer_read(audio_ring_buffer_t *rb, float *data, size_t samples)

References audio_ring_buffer_read().

Referenced by session_audio_read_captured().

◆ audio_ring_buffer_available_read()

size_t audio_ring_buffer_available_read ( audio_ring_buffer_t *  rb)

Definition at line 1106 of file lib/audio/audio.c.

1106 {
1107 if (!rb)
1108 return 0;
1109
1110 // LOCK-FREE: Load indices with proper memory ordering
1111 // Use acquire for write_index to see writer's updates
1112 // Use relaxed for read_index (our own index)
1113 unsigned int write_idx = atomic_load_explicit(&rb->write_index, memory_order_acquire);
1114 unsigned int read_idx = atomic_load_explicit(&rb->read_index, memory_order_relaxed);
1115
1116 if (write_idx >= read_idx) {
1117 return write_idx - read_idx;
1118 }
1119
1120 return AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
1121}

Referenced by audio_process_received_samples(), audio_ring_buffer_available_write(), and client_audio_render_thread().

◆ audio_ring_buffer_available_write()

size_t audio_ring_buffer_available_write ( audio_ring_buffer_t *  rb)

Definition at line 1123 of file lib/audio/audio.c.

1123 {
1124 if (!rb) {
1125 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: rb is NULL");
1126 return 0;
1127 }
1128
1129 return AUDIO_RING_BUFFER_SIZE - audio_ring_buffer_available_read(rb) - 1;
1130}
size_t audio_ring_buffer_available_read(audio_ring_buffer_t *rb)

References audio_ring_buffer_available_read().

◆ audio_ring_buffer_clear()

void audio_ring_buffer_clear ( audio_ring_buffer_t *  rb)

Definition at line 842 of file lib/audio/audio.c.

842 {
843 if (!rb)
844 return;
845
846 mutex_lock(&rb->mutex);
847 // Reset buffer to empty state (no audio to play = silence at shutdown)
848 rb->write_index = 0;
849 rb->read_index = 0;
850 rb->last_sample = 0.0f;
851 // Clear the actual data to zeros to prevent any stale audio
852 SAFE_MEMSET(rb->data, sizeof(rb->data), 0, sizeof(rb->data));
853 mutex_unlock(&rb->mutex);
854}

Referenced by audio_flush_playback_buffers(), audio_stop_duplex(), and media_source_seek().

◆ audio_ring_buffer_create()

audio_ring_buffer_t * audio_ring_buffer_create ( void  )

Definition at line 826 of file lib/audio/audio.c.

826 {
827 return audio_ring_buffer_create_internal(true); // Default: enable jitter buffering for playback
828}

Referenced by audio_init(), and session_audio_add_source().

◆ audio_ring_buffer_create_for_capture()

audio_ring_buffer_t * audio_ring_buffer_create_for_capture ( void  )

Definition at line 830 of file lib/audio/audio.c.

830 {
831 return audio_ring_buffer_create_internal(false); // Disable jitter buffering for capture
832}

Referenced by add_client(), add_webrtc_client(), audio_init(), and audio_start_duplex().

◆ audio_ring_buffer_destroy()

void audio_ring_buffer_destroy ( audio_ring_buffer_t *  rb)

Definition at line 834 of file lib/audio/audio.c.

834 {
835 if (!rb)
836 return;
837
838 mutex_destroy(&rb->mutex);
839 buffer_pool_free(NULL, rb, sizeof(audio_ring_buffer_t));
840}
void buffer_pool_free(buffer_pool_t *pool, void *data, size_t size)

References buffer_pool_free(), and mutex_destroy().

Referenced by add_client(), add_webrtc_client(), audio_destroy(), audio_init(), audio_start_duplex(), audio_stop_duplex(), cleanup_client_media_buffers(), session_audio_destroy(), and session_audio_remove_source().

◆ audio_ring_buffer_peek()

size_t audio_ring_buffer_peek ( audio_ring_buffer_t *  rb,
float *  data,
size_t  samples 
)

Peek at available samples without consuming them (for AEC3 render signal)

This function reads samples from the jitter buffer WITHOUT advancing the read_index. Used to feed audio to AEC3 for echo cancellation even during jitter buffer fill period.

Parameters
rbRing buffer to peek from
dataOutput buffer for samples
samplesNumber of samples to peek
Returns
Number of samples actually peeked (may be less than requested)

Definition at line 1069 of file lib/audio/audio.c.

1069 {
1070 if (!rb || !data || samples <= 0) {
1071 return 0;
1072 }
1073
1074 // LOCK-FREE: Load indices with proper memory ordering
1075 unsigned int write_idx = atomic_load_explicit(&rb->write_index, memory_order_acquire);
1076 unsigned int read_idx = atomic_load_explicit(&rb->read_index, memory_order_relaxed);
1077
1078 // Calculate available samples
1079 size_t available;
1080 if (write_idx >= read_idx) {
1081 available = write_idx - read_idx;
1082 } else {
1083 available = AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
1084 }
1085
1086 size_t to_peek = (samples > available) ? available : samples;
1087
1088 if (to_peek == 0) {
1089 return 0;
1090 }
1091
1092 // Copy samples in chunks (handle wraparound)
1093 size_t first_chunk = (read_idx + to_peek <= AUDIO_RING_BUFFER_SIZE) ? to_peek : (AUDIO_RING_BUFFER_SIZE - read_idx);
1094
1095 SAFE_MEMCPY(data, first_chunk * sizeof(float), rb->data + read_idx, first_chunk * sizeof(float));
1096
1097 if (first_chunk < to_peek) {
1098 // Wraparound: copy second chunk from beginning of buffer
1099 size_t second_chunk = to_peek - first_chunk;
1100 SAFE_MEMCPY(data + first_chunk, second_chunk * sizeof(float), rb->data, second_chunk * sizeof(float));
1101 }
1102
1103 return to_peek;
1104}

◆ audio_ring_buffer_read()

size_t audio_ring_buffer_read ( audio_ring_buffer_t *  rb,
float *  data,
size_t  samples 
)

Definition at line 939 of file lib/audio/audio.c.

939 {
940 if (!rb || !data || samples <= 0) {
941 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: rb=%p, data=%p, samples=%d", rb, data, samples);
942 return 0; // Return 0 samples read on error
943 }
944
945 // LOCK-FREE: Load indices with proper memory ordering
946 // - Load writer's write_index with acquire (see writer's data updates)
947 // - Load our own read_index with relaxed (no sync needed with ourselves)
948 unsigned int write_idx = atomic_load_explicit(&rb->write_index, memory_order_acquire);
949 unsigned int read_idx = atomic_load_explicit(&rb->read_index, memory_order_relaxed);
950
951 // Calculate available samples
952 size_t available;
953 if (write_idx >= read_idx) {
954 available = write_idx - read_idx;
955 } else {
956 available = AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
957 }
958
959 // LOCK-FREE: Load jitter buffer state with acquire ordering
960 bool jitter_filled = atomic_load_explicit(&rb->jitter_buffer_filled, memory_order_acquire);
961 int crossfade_remaining = atomic_load_explicit(&rb->crossfade_samples_remaining, memory_order_acquire);
962 bool fade_in = atomic_load_explicit(&rb->crossfade_fade_in, memory_order_acquire);
963
964 // Jitter buffer: don't read until initial fill threshold is reached
965 // (only for playback buffers - capture buffers have jitter_buffer_enabled = false)
966 if (!jitter_filled && rb->jitter_buffer_enabled) {
967 // Check if we've accumulated enough samples to start playback
968 if (available >= AUDIO_JITTER_BUFFER_THRESHOLD) {
969 atomic_store_explicit(&rb->jitter_buffer_filled, true, memory_order_release);
970 atomic_store_explicit(&rb->crossfade_samples_remaining, AUDIO_CROSSFADE_SAMPLES, memory_order_release);
971 atomic_store_explicit(&rb->crossfade_fade_in, true, memory_order_release);
972 log_info("Jitter buffer filled (%zu samples), starting playback with fade-in", available);
973 // Reload state for processing below
974 jitter_filled = true;
975 crossfade_remaining = AUDIO_CROSSFADE_SAMPLES;
976 fade_in = true;
977 } else {
978 // Log buffer fill progress every second
979 log_debug_every(NS_PER_MS_INT, "Jitter buffer filling: %zu/%d samples (%.1f%%)", available,
980 AUDIO_JITTER_BUFFER_THRESHOLD, (100.0f * available) / AUDIO_JITTER_BUFFER_THRESHOLD);
981 return 0; // Return 0 samples - caller will pad with silence
982 }
983 }
984
985 // Periodic buffer health logging (every 5 seconds when healthy)
986 unsigned int underruns = atomic_load_explicit(&rb->underrun_count, memory_order_relaxed);
987 log_dev_every(5 * NS_PER_MS_INT, "Buffer health: %zu/%d samples (%.1f%%), underruns=%u", available,
988 AUDIO_RING_BUFFER_SIZE, (100.0f * available) / AUDIO_RING_BUFFER_SIZE, underruns);
989
990 // Low buffer handling: DON'T pause playback - continue reading what's available
991 // and fill the rest with silence. Pausing causes a feedback loop where:
992 // 1. Underrun -> pause reading -> buffer overflows from incoming samples
993 // 2. Threshold reached -> resume reading -> drains too fast -> underrun again
994 //
995 // Instead: always consume samples to prevent overflow, use silence for missing data
996 if (rb->jitter_buffer_enabled && available < AUDIO_JITTER_LOW_WATER_MARK) {
997 unsigned int underrun_count = atomic_fetch_add_explicit(&rb->underrun_count, 1, memory_order_relaxed) + 1;
998 log_warn_every(LOG_RATE_FAST,
999 "Audio buffer low #%u: only %zu samples available (low water mark: %d), padding with silence",
1000 underrun_count, available, AUDIO_JITTER_LOW_WATER_MARK);
1001 // Don't set jitter_buffer_filled = false - keep reading to prevent overflow
1002 }
1003
1004 size_t to_read = (samples > available) ? available : samples;
1005
1006 // Optimize: copy in chunks instead of one sample at a time
1007 size_t remaining = AUDIO_RING_BUFFER_SIZE - read_idx;
1008
1009 if (to_read <= remaining) {
1010 // Can copy in one chunk
1011 SAFE_MEMCPY(data, to_read * sizeof(float), &rb->data[read_idx], to_read * sizeof(float));
1012 } else {
1013 // Need to wrap around - copy in two chunks
1014 SAFE_MEMCPY(data, remaining * sizeof(float), &rb->data[read_idx], remaining * sizeof(float));
1015 SAFE_MEMCPY(&data[remaining], (to_read - remaining) * sizeof(float), &rb->data[0],
1016 (to_read - remaining) * sizeof(float));
1017 }
1018
1019 // LOCK-FREE: Store new read_index with release ordering
1020 // This ensures all data reads above complete before the index update
1021 unsigned int new_read_idx = (read_idx + (unsigned int)to_read) % AUDIO_RING_BUFFER_SIZE;
1022 atomic_store_explicit(&rb->read_index, new_read_idx, memory_order_release);
1023
1024 // Apply fade-in if recovering from underrun
1025 if (fade_in && crossfade_remaining > 0) {
1026 int fade_start = AUDIO_CROSSFADE_SAMPLES - crossfade_remaining;
1027 size_t fade_samples = (to_read < (size_t)crossfade_remaining) ? to_read : (size_t)crossfade_remaining;
1028
1029 for (size_t i = 0; i < fade_samples; i++) {
1030 float fade_factor = (float)(fade_start + (int)i + 1) / (float)AUDIO_CROSSFADE_SAMPLES;
1031 data[i] *= fade_factor;
1032 }
1033
1034 int new_crossfade_remaining = crossfade_remaining - (int)fade_samples;
1035 atomic_store_explicit(&rb->crossfade_samples_remaining, new_crossfade_remaining, memory_order_release);
1036 if (new_crossfade_remaining <= 0) {
1037 atomic_store_explicit(&rb->crossfade_fade_in, false, memory_order_release);
1038 log_debug("Audio fade-in complete");
1039 }
1040 }
1041
1042 // Save last sample for potential fade-out
1043 // Note: only update if we actually read some data
1044 // This is NOT atomic - only the reader thread writes this
1045 if (to_read > 0) {
1046 rb->last_sample = data[to_read - 1];
1047 }
1048
1049 // Return ACTUAL number of samples read, not padded count
1050 // The caller (mixer) expects truthful return values to detect underruns
1051 // and handle silence padding externally. Internal padding creates double-padding bugs.
1052 //
1053 // This function was incorrectly returning `samples` even when
1054 // it only read `to_read` samples. This broke the mixer's underrun detection.
1055 return to_read;
1056}

Referenced by audio_read_samples(), client_audio_render_thread(), mixer_process(), mixer_process_excluding_source(), session_audio_mix_excluding(), and session_capture_read_audio().

◆ audio_ring_buffer_write()

asciichat_error_t audio_ring_buffer_write ( audio_ring_buffer_t *  rb,
const float *  data,
int  samples 
)

Definition at line 856 of file lib/audio/audio.c.

856 {
857 if (!rb || !data || samples <= 0)
858 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: rb=%p, data=%p, samples=%d", rb, data, samples);
859
860 // Validate samples doesn't exceed our buffer size
861 if (samples > AUDIO_RING_BUFFER_SIZE) {
862 return SET_ERRNO(ERROR_BUFFER, "Attempted to write %d samples, but buffer size is only %d", samples,
863 AUDIO_RING_BUFFER_SIZE);
864 }
865
866 // LOCK-FREE: Load indices with proper memory ordering
867 // - Load our own write_index with relaxed (no sync needed with ourselves)
868 // - Load reader's read_index with acquire (see reader's updates to free space)
869 unsigned int write_idx = atomic_load_explicit(&rb->write_index, memory_order_relaxed);
870 unsigned int read_idx = atomic_load_explicit(&rb->read_index, memory_order_acquire);
871
872 // Calculate current buffer level (how many samples are buffered)
873 int buffer_level;
874 if (write_idx >= read_idx) {
875 buffer_level = (int)(write_idx - read_idx);
876 } else {
877 buffer_level = AUDIO_RING_BUFFER_SIZE - (int)(read_idx - write_idx);
878 }
879 // Reserve 1 slot to distinguish between full and empty states
880 // (when buffer is full, write_idx will be just before read_idx, not equal to it)
881 int available = AUDIO_RING_BUFFER_SIZE - 1 - buffer_level;
882
883 // HIGH WATER MARK: Drop INCOMING samples to prevent latency accumulation
884 // Writer must not modify read_index (race condition with reader).
885 // Instead, we drop incoming samples to keep buffer bounded.
886 // This sacrifices newest data to prevent unbounded latency growth.
887 if (buffer_level > AUDIO_JITTER_HIGH_WATER_MARK) {
888 // Buffer is already too full - drop incoming samples to maintain target level
889 int target_writes = AUDIO_JITTER_TARGET_LEVEL - buffer_level;
890 if (target_writes < 0) {
891 target_writes = 0; // Buffer is way over - drop everything
892 }
893
894 if (samples > target_writes) {
895 int dropped = samples - target_writes;
896 log_warn_every(LOG_RATE_FAST,
897 "Audio buffer high water mark exceeded (%d > %d): dropping %d INCOMING samples "
898 "(keeping newest %d to maintain target %d)",
899 buffer_level, AUDIO_JITTER_HIGH_WATER_MARK, dropped, target_writes, AUDIO_JITTER_TARGET_LEVEL);
900 samples = target_writes; // Only write what fits within target level
901 }
902 }
903
904 // Now write the new samples - should always have enough space after above
905 int samples_to_write = samples;
906 if (samples > available) {
907 // This should rarely happen after the high water mark logic above
908 int samples_dropped = samples - available;
909 samples_to_write = available;
910 log_warn_every(LOG_RATE_FAST, "Audio buffer overflow: dropping %d of %d incoming samples (buffer_used=%d/%d)",
911 samples_dropped, samples, AUDIO_RING_BUFFER_SIZE - available, AUDIO_RING_BUFFER_SIZE);
912 }
913
914 // Write only the samples that fit (preserves existing data integrity)
915 if (samples_to_write > 0) {
916 int remaining = AUDIO_RING_BUFFER_SIZE - (int)write_idx;
917
918 if (samples_to_write <= remaining) {
919 // Can copy in one chunk
920 SAFE_MEMCPY(&rb->data[write_idx], samples_to_write * sizeof(float), data, samples_to_write * sizeof(float));
921 } else {
922 // Need to wrap around - copy in two chunks
923 SAFE_MEMCPY(&rb->data[write_idx], remaining * sizeof(float), data, remaining * sizeof(float));
924 SAFE_MEMCPY(&rb->data[0], (samples_to_write - remaining) * sizeof(float), &data[remaining],
925 (samples_to_write - remaining) * sizeof(float));
926 }
927
928 // LOCK-FREE: Store new write_index with release ordering
929 // This ensures all data writes above are visible before the index update
930 unsigned int new_write_idx = (write_idx + (unsigned int)samples_to_write) % AUDIO_RING_BUFFER_SIZE;
931 atomic_store_explicit(&rb->write_index, new_write_idx, memory_order_release);
932 }
933
934 // Note: jitter buffer fill check is now done in read function for better control
935
936 return ASCIICHAT_OK; // Success
937}

Referenced by audio_write_samples(), handle_audio_batch_packet(), handle_audio_opus_batch_packet(), handle_audio_opus_packet(), handle_audio_packet(), and session_audio_write_source().

◆ audio_set_pipeline()

void audio_set_pipeline ( audio_context_t *  ctx,
void *  pipeline 
)

Definition at line 1328 of file lib/audio/audio.c.

1328 {
1329 if (!ctx)
1330 return;
1331 ctx->audio_pipeline = pipeline;
1332}

Referenced by audio_cleanup(), and audio_client_init().

◆ audio_set_realtime_priority()

asciichat_error_t audio_set_realtime_priority ( void  )

Definition at line 1934 of file lib/audio/audio.c.

1934 {
1935 // Delegate to platform abstraction layer
1936 asciichat_error_t result = asciichat_thread_set_realtime_priority();
1937 if (result == ASCIICHAT_OK) {
1938 log_debug("✓ Audio thread real-time priority set successfully");
1939 }
1940 return result;
1941}

Referenced by audio_start_duplex().

◆ audio_should_enable_microphone()

bool audio_should_enable_microphone ( audio_source_t  source,
bool  has_media_audio 
)

Definition at line 2037 of file lib/audio/audio.c.

2037 {
2038 switch (source) {
2039 case AUDIO_SOURCE_AUTO:
2040 return !has_media_audio;
2041
2042 case AUDIO_SOURCE_MIC:
2043 return true;
2044
2045 case AUDIO_SOURCE_MEDIA:
2046 return false;
2047
2048 case AUDIO_SOURCE_BOTH:
2049 return true;
2050
2051 default:
2052 return !has_media_audio;
2053 }
2054}

Referenced by session_client_like_run().

◆ audio_start_duplex()

asciichat_error_t audio_start_duplex ( audio_context_t *  ctx)

Definition at line 1353 of file lib/audio/audio.c.

1353 {
1354 if (!ctx || !ctx->initialized) {
1355 return SET_ERRNO(ERROR_INVALID_STATE, "Audio context not initialized");
1356 }
1357
1358 // Initialize PortAudio here, when we actually need to open streams
1359 // This defers Pa_Initialize() until necessary, avoiding premature ALSA allocation
1360 asciichat_error_t pa_result = audio_ensure_portaudio_initialized();
1361 if (pa_result != ASCIICHAT_OK) {
1362 return pa_result;
1363 }
1364
1365 mutex_lock(&ctx->state_mutex);
1366
1367 // Already running?
1368 if (ctx->duplex_stream || ctx->input_stream || ctx->output_stream) {
1369 mutex_unlock(&ctx->state_mutex);
1370 return ASCIICHAT_OK;
1371 }
1372
1373 // Setup input parameters (skip if playback-only mode)
1374 PaStreamParameters inputParams = {0};
1375 const PaDeviceInfo *inputInfo = NULL;
1376 bool has_input = false;
1377
1378 if (!ctx->playback_only) {
1379 if (GET_OPTION(microphone_index) >= 0) {
1380 inputParams.device = GET_OPTION(microphone_index);
1381 } else {
1382 inputParams.device = Pa_GetDefaultInputDevice();
1383 }
1384
1385 if (inputParams.device == paNoDevice) {
1386 mutex_unlock(&ctx->state_mutex);
1387 return SET_ERRNO(ERROR_AUDIO, "No input device available");
1388 }
1389
1390 inputInfo = Pa_GetDeviceInfo(inputParams.device);
1391 if (!inputInfo) {
1392 mutex_unlock(&ctx->state_mutex);
1393 return SET_ERRNO(ERROR_AUDIO, "Input device info not found");
1394 }
1395
1396 has_input = true;
1397 inputParams.channelCount = AUDIO_CHANNELS;
1398 inputParams.sampleFormat = paFloat32;
1399 inputParams.suggestedLatency = inputInfo->defaultLowInputLatency;
1400 inputParams.hostApiSpecificStreamInfo = NULL;
1401 }
1402
1403 // Setup output parameters
1404 PaStreamParameters outputParams;
1405 const PaDeviceInfo *outputInfo = NULL;
1406 bool has_output = false;
1407
1408 if (GET_OPTION(speakers_index) >= 0) {
1409 outputParams.device = GET_OPTION(speakers_index);
1410 } else {
1411 outputParams.device = Pa_GetDefaultOutputDevice();
1412 }
1413
1414 if (outputParams.device != paNoDevice) {
1415 outputInfo = Pa_GetDeviceInfo(outputParams.device);
1416 if (outputInfo) {
1417 has_output = true;
1418 outputParams.channelCount = AUDIO_CHANNELS;
1419 outputParams.sampleFormat = paFloat32;
1420 outputParams.suggestedLatency = outputInfo->defaultLowOutputLatency;
1421 outputParams.hostApiSpecificStreamInfo = NULL;
1422 } else {
1423 log_warn("Output device info not found for device %d", outputParams.device);
1424 }
1425 }
1426
1427 // Store device rates for diagnostics (only access if device info was retrieved)
1428 ctx->input_device_rate = (has_input && inputInfo) ? inputInfo->defaultSampleRate : 0;
1429 ctx->output_device_rate = (has_output && outputInfo) ? outputInfo->defaultSampleRate : 0;
1430
1431 log_debug("Opening audio:");
1432 if (has_input) {
1433 log_info(" Input: %s (%.0f Hz)", inputInfo->name, inputInfo->defaultSampleRate);
1434 } else if (ctx->playback_only) {
1435 log_debug(" Input: (playback-only mode - no microphone)");
1436 } else {
1437 log_debug(" Input: (none)");
1438 }
1439 if (has_output) {
1440 log_info(" Output: %s (%.0f Hz)", outputInfo->name, outputInfo->defaultSampleRate);
1441 } else {
1442 log_debug(" Output: None (input-only mode - will send audio to server)");
1443 }
1444
1445 // Check if sample rates differ - ALSA full-duplex doesn't handle this well
1446 // If no input or no output, always use separate streams
1447 bool rates_differ = has_input && has_output && (inputInfo->defaultSampleRate != outputInfo->defaultSampleRate);
1448 bool try_separate = rates_differ || !has_input || !has_output;
1449 PaError err = paNoError;
1450
1451 if (!try_separate) {
1452 // Try full-duplex first (preferred - perfect AEC3 timing)
1453 err = Pa_OpenStream(&ctx->duplex_stream, &inputParams, &outputParams, AUDIO_SAMPLE_RATE, AUDIO_FRAMES_PER_BUFFER,
1454 paClipOff, duplex_callback, ctx);
1455
1456 if (err == paNoError) {
1457 err = Pa_StartStream(ctx->duplex_stream);
1458 if (err != paNoError) {
1459 Pa_CloseStream(ctx->duplex_stream);
1460 ctx->duplex_stream = NULL;
1461 log_warn("Full-duplex stream failed to start: %s", Pa_GetErrorText(err));
1462 try_separate = true;
1463 }
1464 } else {
1465 log_warn("Full-duplex stream failed to open: %s", Pa_GetErrorText(err));
1466 try_separate = true;
1467 }
1468 }
1469
1470 if (try_separate) {
1471 // Fall back to separate streams (needed when sample rates differ or input-only/playback-only mode)
1472 if (has_output && has_input) {
1473 log_info("Using separate input/output streams (sample rates differ: %.0f vs %.0f Hz)",
1474 inputInfo->defaultSampleRate, outputInfo->defaultSampleRate);
1475 log_info(" Will resample: buffer at %.0f Hz → output at %.0f Hz", (double)AUDIO_SAMPLE_RATE,
1476 outputInfo->defaultSampleRate);
1477 } else if (has_output) {
1478 log_debug("Using output-only mode (playback-only for mirror/media)");
1479 } else if (has_input) {
1480 log_info("Using input-only mode (no output device available)");
1481 }
1482
1483 // Store the internal sample rate (buffer rate)
1484 ctx->sample_rate = AUDIO_SAMPLE_RATE;
1485
1486 // Create render buffer for AEC3 reference synchronization
1487 ctx->render_buffer = audio_ring_buffer_create_for_capture();
1488 if (!ctx->render_buffer) {
1489 mutex_unlock(&ctx->state_mutex);
1490 return SET_ERRNO(ERROR_MEMORY, "Failed to create render buffer");
1491 }
1492
1493 // Open output stream only if output device exists
1494 bool output_ok = false;
1495 double actual_output_rate = 0;
1496 if (has_output) {
1497 // Try to use AUDIO_SAMPLE_RATE (48kHz) first for best quality and duplex compatibility
1498 // Fall back to native rate if 48kHz not supported
1499 double preferred_rate = AUDIO_SAMPLE_RATE;
1500 double native_rate = outputInfo->defaultSampleRate;
1501
1502 log_debug("Attempting output at %.0f Hz (preferred) vs %.0f Hz (native)", preferred_rate, native_rate);
1503
1504 // Always use output_callback for output streams (both output-only and duplex modes)
1505 // PortAudio will invoke the callback whenever it needs audio data
1506 // The callback reads from media_source (mirror mode) or playback buffers (network mode)
1507 PaStreamCallback *callback = output_callback;
1508
1509 // Try preferred rate first
1510 err = Pa_OpenStream(&ctx->output_stream, NULL, &outputParams, preferred_rate, AUDIO_FRAMES_PER_BUFFER, paClipOff,
1511 callback, ctx);
1512
1513 if (err == paNoError) {
1514 actual_output_rate = preferred_rate;
1515 output_ok = true;
1516 log_info("✓ Output opened at preferred rate: %.0f Hz (matches input - optimal!)", preferred_rate);
1517 } else {
1518 log_warn("Failed to open output at %.0f Hz: %s, trying native rate %.0f Hz", preferred_rate,
1519 Pa_GetErrorText(err), native_rate);
1520
1521 // If first Pa_OpenStream call left a partial stream, clean it up before retrying
1522 if (ctx->output_stream) {
1523 log_debug("Closing partially-opened output stream from failed preferred rate");
1524 Pa_CloseStream(ctx->output_stream);
1525 ctx->output_stream = NULL;
1526 }
1527
1528 // Fall back to native rate (still using blocking mode for output-only)
1529 err = Pa_OpenStream(&ctx->output_stream, NULL, &outputParams, native_rate, AUDIO_FRAMES_PER_BUFFER, paClipOff,
1530 callback, ctx);
1531
1532 if (err == paNoError) {
1533 actual_output_rate = native_rate;
1534 output_ok = true;
1535 log_info("✓ Output opened at native rate: %.0f Hz (will need resampling)", native_rate);
1536 } else {
1537 log_warn("Failed to open output stream at native rate: %s", Pa_GetErrorText(err));
1538 // Clean up if fallback also failed
1539 if (ctx->output_stream) {
1540 log_debug("Closing partially-opened output stream from failed native rate");
1541 Pa_CloseStream(ctx->output_stream);
1542 ctx->output_stream = NULL;
1543 }
1544 }
1545 }
1546
1547 // Store actual output rate for resampling
1548 if (output_ok) {
1549 ctx->output_device_rate = actual_output_rate;
1550 if (actual_output_rate != AUDIO_SAMPLE_RATE) {
1551 log_warn("⚠️ Output rate mismatch: %.0f Hz output vs %.0f Hz input - resampling will be used",
1552 actual_output_rate, (double)AUDIO_SAMPLE_RATE);
1553 }
1554 }
1555 }
1556
1557 // Open input stream only if we have input (skip for playback-only mode)
1558 bool input_ok = !has_input; // If no input, mark as OK (skip)
1559 if (has_input) {
1560 // Use pipeline sample rate (AUDIO_SAMPLE_RATE)
1561 // In input-only mode, we don't need to match output device rate
1562 double input_stream_rate = AUDIO_SAMPLE_RATE;
1563 err = Pa_OpenStream(&ctx->input_stream, &inputParams, NULL, input_stream_rate, AUDIO_FRAMES_PER_BUFFER, paClipOff,
1564 input_callback, ctx);
1565 input_ok = (err == paNoError);
1566
1567 // If input failed, try device 0 as fallback (HDMI on BeaglePlay)
1568 if (!input_ok) {
1569 log_debug("Input failed - trying device 0 as fallback");
1570
1571 // Clean up partial stream from first attempt before retrying
1572 if (ctx->input_stream) {
1573 log_debug("Closing partially-opened input stream from failed primary device");
1574 Pa_CloseStream(ctx->input_stream);
1575 ctx->input_stream = NULL;
1576 }
1577
1578 PaStreamParameters fallback_input_params = inputParams;
1579 fallback_input_params.device = 0;
1580 const PaDeviceInfo *device_0_info = Pa_GetDeviceInfo(0);
1581 if (device_0_info && device_0_info->maxInputChannels > 0) {
1582 err = Pa_OpenStream(&ctx->input_stream, &fallback_input_params, NULL, input_stream_rate,
1583 AUDIO_FRAMES_PER_BUFFER, paClipOff, input_callback, ctx);
1584 if (err == paNoError) {
1585 log_info("Input stream opened on device 0 (fallback from default)");
1586 input_ok = true;
1587 } else {
1588 log_warn("Fallback also failed on device 0: %s", Pa_GetErrorText(err));
1589 // Clean up if fallback also failed
1590 if (ctx->input_stream) {
1591 log_debug("Closing partially-opened input stream from failed fallback device");
1592 Pa_CloseStream(ctx->input_stream);
1593 ctx->input_stream = NULL;
1594 }
1595 }
1596 }
1597 }
1598
1599 if (!input_ok) {
1600 log_warn("Failed to open input stream: %s", Pa_GetErrorText(err));
1601 }
1602 }
1603
1604 // Check if we got at least one stream working
1605 if (!input_ok && !output_ok) {
1606 // Neither stream works - fail completely
1607 audio_ring_buffer_destroy(ctx->render_buffer);
1608 ctx->render_buffer = NULL;
1609 mutex_unlock(&ctx->state_mutex);
1610 return SET_ERRNO(ERROR_AUDIO, "Failed to open both input and output streams");
1611 }
1612
1613 // If output failed but input works, we can still send audio to server
1614 if (!output_ok && input_ok) {
1615 log_info("Output stream unavailable - continuing with input-only (can send audio to server)");
1616 ctx->output_stream = NULL;
1617 }
1618 // If input failed but output works, we can still receive audio from server
1619 if (!input_ok && output_ok) {
1620 log_info("Input stream unavailable - continuing with output-only (can receive audio from server)");
1621 ctx->input_stream = NULL;
1622 }
1623
1624 // Start output stream if it's open
1625 if (ctx->output_stream) {
1626 err = Pa_StartStream(ctx->output_stream);
1627 if (err != paNoError) {
1628 if (ctx->input_stream)
1629 Pa_CloseStream(ctx->input_stream);
1630 Pa_CloseStream(ctx->output_stream);
1631 ctx->input_stream = NULL;
1632 ctx->output_stream = NULL;
1633 audio_ring_buffer_destroy(ctx->render_buffer);
1634 ctx->render_buffer = NULL;
1635 mutex_unlock(&ctx->state_mutex);
1636 return SET_ERRNO(ERROR_AUDIO, "Failed to start output stream: %s", Pa_GetErrorText(err));
1637 }
1638 }
1639
1640 // Start input stream if it's open
1641 if (ctx->input_stream) {
1642 err = Pa_StartStream(ctx->input_stream);
1643 if (err != paNoError) {
1644 if (ctx->output_stream)
1645 Pa_StopStream(ctx->output_stream);
1646 if (ctx->input_stream)
1647 Pa_CloseStream(ctx->input_stream);
1648 if (ctx->output_stream)
1649 Pa_CloseStream(ctx->output_stream);
1650 ctx->input_stream = NULL;
1651 ctx->output_stream = NULL;
1652 audio_ring_buffer_destroy(ctx->render_buffer);
1653 ctx->render_buffer = NULL;
1654 mutex_unlock(&ctx->state_mutex);
1655 return SET_ERRNO(ERROR_AUDIO, "Failed to start input stream: %s", Pa_GetErrorText(err));
1656 }
1657 }
1658
1659 ctx->separate_streams = true;
1660 log_debug("Separate streams started successfully");
1661 } else {
1662 ctx->separate_streams = false;
1663 log_info("Full-duplex stream started (single callback, perfect AEC3 timing)");
1664 }
1665
1667
1668 // Start worker thread for heavy audio processing
1669 if (!ctx->worker_running) {
1670 atomic_store(&ctx->worker_should_stop, false);
1671 if (asciichat_thread_create(&ctx->worker_thread, audio_worker_thread, ctx) != 0) {
1672 // Failed to create worker thread - stop streams and cleanup
1673 if (ctx->duplex_stream) {
1674 Pa_StopStream(ctx->duplex_stream);
1675 Pa_CloseStream(ctx->duplex_stream);
1676 ctx->duplex_stream = NULL;
1677 }
1678 if (ctx->input_stream) {
1679 Pa_StopStream(ctx->input_stream);
1680 Pa_CloseStream(ctx->input_stream);
1681 ctx->input_stream = NULL;
1682 }
1683 if (ctx->output_stream) {
1684 Pa_StopStream(ctx->output_stream);
1685 Pa_CloseStream(ctx->output_stream);
1686 ctx->output_stream = NULL;
1687 }
1688 audio_ring_buffer_destroy(ctx->render_buffer);
1689 ctx->render_buffer = NULL;
1690 mutex_unlock(&ctx->state_mutex);
1691 return SET_ERRNO(ERROR_THREAD, "Failed to create worker thread");
1692 }
1693 ctx->worker_running = true;
1694 log_debug("Worker thread started successfully");
1695 }
1696
1697 ctx->running = true;
1698 ctx->sample_rate = AUDIO_SAMPLE_RATE;
1699 mutex_unlock(&ctx->state_mutex);
1700 return ASCIICHAT_OK;
1701}
asciichat_error_t audio_set_realtime_priority(void)
#define paNoError
Definition portaudio.h:19
int PaError
Definition portaudio.h:14
int asciichat_thread_create(asciichat_thread_t *thread, void *(*start_routine)(void *), void *arg)
Definition threading.c:42

References asciichat_thread_create(), audio_ring_buffer_create_for_capture(), audio_ring_buffer_destroy(), audio_set_realtime_priority(), and paNoError.

Referenced by audio_client_init(), session_audio_start_duplex(), and session_client_like_run().

◆ audio_stop_duplex()

asciichat_error_t audio_stop_duplex ( audio_context_t *  ctx)

Definition at line 1703 of file lib/audio/audio.c.

1703 {
1704 if (!ctx || !ctx->initialized) {
1705 return SET_ERRNO(ERROR_INVALID_STATE, "Audio context not initialized");
1706 }
1707
1708 atomic_store(&ctx->shutting_down, true);
1709
1710 // Stop worker thread before stopping streams
1711 if (ctx->worker_running) {
1712 log_debug("Stopping worker thread");
1713 atomic_store(&ctx->worker_should_stop, true);
1714 cond_signal(&ctx->worker_cond); // Wake up worker if waiting
1715 asciichat_thread_join(&ctx->worker_thread, NULL);
1716 ctx->worker_running = false;
1717 log_debug("Worker thread stopped successfully");
1718 }
1719
1720 if (ctx->playback_buffer) {
1721 audio_ring_buffer_clear(ctx->playback_buffer);
1722 }
1723
1724 Pa_Sleep(50); // Let callbacks drain
1725
1726 mutex_lock(&ctx->state_mutex);
1727
1728 if (ctx->duplex_stream) {
1729 log_debug("Stopping duplex stream");
1730 PaError err = Pa_StopStream(ctx->duplex_stream);
1731 if (err != paNoError) {
1732 log_warn("Pa_StopStream failed: %s", Pa_GetErrorText(err));
1733 }
1734 log_debug("Closing duplex stream");
1735 err = Pa_CloseStream(ctx->duplex_stream);
1736 if (err != paNoError) {
1737 log_warn("Pa_CloseStream failed: %s", Pa_GetErrorText(err));
1738 } else {
1739 log_debug("Duplex stream closed successfully");
1740 }
1741 ctx->duplex_stream = NULL;
1742 }
1743
1744 // Stop separate streams if used
1745 if (ctx->input_stream) {
1746 log_debug("Stopping input stream");
1747 PaError err = Pa_StopStream(ctx->input_stream);
1748 if (err != paNoError) {
1749 log_warn("Pa_StopStream input failed: %s", Pa_GetErrorText(err));
1750 }
1751 log_debug("Closing input stream");
1752 err = Pa_CloseStream(ctx->input_stream);
1753 if (err != paNoError) {
1754 log_warn("Pa_CloseStream input failed: %s", Pa_GetErrorText(err));
1755 } else {
1756 log_debug("Input stream closed successfully");
1757 }
1758 ctx->input_stream = NULL;
1759 }
1760
1761 if (ctx->output_stream) {
1762 log_debug("Stopping output stream");
1763 PaError err = Pa_StopStream(ctx->output_stream);
1764 if (err != paNoError) {
1765 log_warn("Pa_StopStream output failed: %s", Pa_GetErrorText(err));
1766 }
1767 log_debug("Closing output stream");
1768 err = Pa_CloseStream(ctx->output_stream);
1769 if (err != paNoError) {
1770 log_warn("Pa_CloseStream output failed: %s", Pa_GetErrorText(err));
1771 } else {
1772 log_debug("Output stream closed successfully");
1773 }
1774 ctx->output_stream = NULL;
1775 }
1776
1777 // Cleanup render buffer
1778 if (ctx->render_buffer) {
1779 audio_ring_buffer_destroy(ctx->render_buffer);
1780 ctx->render_buffer = NULL;
1781 }
1782
1783 ctx->running = false;
1784 ctx->separate_streams = false;
1785 mutex_unlock(&ctx->state_mutex);
1786
1787 log_debug("Audio stopped");
1788 return ASCIICHAT_OK;
1789}

References asciichat_thread_join(), audio_ring_buffer_clear(), audio_ring_buffer_destroy(), and paNoError.

Referenced by audio_cleanup(), audio_destroy(), session_audio_stop(), and session_client_like_run().

◆ audio_terminate_portaudio_final()

void audio_terminate_portaudio_final ( void  )

Terminate PortAudio and free all device resources.

This must be called to actually free device structures allocated by ALSA/libpulse. It should be called AFTER all audio contexts are destroyed and session cleanup is complete.

Definition at line 114 of file lib/audio/audio.c.

114 {
115 static_mutex_lock(&g_pa_refcount_mutex);
116
117 if (g_pa_init_refcount == 0 && g_pa_init_count > 0 && g_pa_terminate_count == 0) {
118 log_debug("[PORTAUDIO_TERM] Calling Pa_Terminate() to release PortAudio");
119
120 PaError err = Pa_Terminate();
121 g_pa_terminate_count++;
122
123 log_debug("[PORTAUDIO_TERM] Pa_Terminate() returned: %s", Pa_GetErrorText(err));
124 }
125
126 static_mutex_unlock(&g_pa_refcount_mutex);
127}

Referenced by audio_cleanup(), and session_client_like_run().

◆ audio_validate_batch_params()

asciichat_error_t audio_validate_batch_params ( const audio_batch_info_t batch)

Definition at line 1973 of file lib/audio/audio.c.

1973 {
1974 if (!batch) {
1975 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch info pointer is NULL");
1976 }
1977
1978 // Validate batch_count
1979 if (batch->batch_count == 0) {
1980 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch count cannot be zero");
1981 }
1982
1983 // Check for reasonable max (256 frames per batch is very generous)
1984 if (batch->batch_count > 256) {
1985 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch count too large (batch_count=%u, max=256)", batch->batch_count);
1986 }
1987
1988 // Validate channels (1=mono, 2=stereo, max 8 for multi-channel)
1989 if (batch->channels == 0 || batch->channels > 8) {
1990 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid channel count (channels=%u, valid=1-8)", batch->channels);
1991 }
1992
1993 // Validate sample rate
1995 return SET_ERRNO(ERROR_INVALID_PARAM, "Unsupported sample rate (sample_rate=%u)", batch->sample_rate);
1996 }
1997
1998 // Check for reasonable sample counts
1999 if (batch->total_samples == 0) {
2000 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch has zero samples");
2001 }
2002
2003 // Each batch typically has samples_per_frame worth of samples
2004 // For 48kHz at 20ms per frame: 48000 * 0.02 = 960 samples per frame
2005 // With max 256 frames, that's up to ~245k samples per batch
2006 if (batch->total_samples > 1000000) {
2007 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch sample count suspiciously large (total_samples=%u)",
2008 batch->total_samples);
2009 }
2010
2011 return ASCIICHAT_OK;
2012}
bool audio_is_supported_sample_rate(uint32_t sample_rate)

References audio_is_supported_sample_rate(), audio_batch_info_t::batch_count, audio_batch_info_t::channels, audio_batch_info_t::sample_rate, and audio_batch_info_t::total_samples.

◆ audio_write_samples()

asciichat_error_t audio_write_samples ( audio_context_t *  ctx,
const float *  buffer,
int  num_samples 
)

Definition at line 1802 of file lib/audio/audio.c.

1802 {
1803 if (!ctx || !ctx->initialized || !buffer || num_samples <= 0) {
1804 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, buffer=%p, num_samples=%d", ctx, buffer,
1805 num_samples);
1806 }
1807
1808 // Don't accept new audio data during shutdown - this prevents garbage/beeps
1809 if (atomic_load(&ctx->shutting_down)) {
1810 return ASCIICHAT_OK; // Silently discard
1811 }
1812
1813 asciichat_error_t result = audio_ring_buffer_write(ctx->playback_buffer, buffer, num_samples);
1814
1815 return result;
1816}
asciichat_error_t audio_ring_buffer_write(audio_ring_buffer_t *rb, const float *data, int samples)

References audio_ring_buffer_write().

Referenced by audio_process_received_samples(), and session_audio_write_playback().

◆ resample_linear()

void resample_linear ( const float *  src,
size_t  src_samples,
float *  dst,
size_t  dst_samples,
double  src_rate,
double  dst_rate 
)

Simple linear interpolation resampler. Resamples from src_rate to dst_rate using linear interpolation.

Parameters
srcSource samples at src_rate
src_samplesNumber of source samples
dstDestination buffer at dst_rate
dst_samplesNumber of destination samples to produce
src_rateSource sample rate (e.g., 48000)
dst_rateDestination sample rate (e.g., 44100)

Definition at line 574 of file lib/audio/audio.c.

575 {
576 if (src_samples == 0 || dst_samples == 0) {
577 SAFE_MEMSET(dst, dst_samples * sizeof(float), 0, dst_samples * sizeof(float));
578 return;
579 }
580
581 double ratio = src_rate / dst_rate;
582
583 for (size_t i = 0; i < dst_samples; i++) {
584 double src_pos = (double)i * ratio;
585 size_t idx0 = (size_t)src_pos;
586 size_t idx1 = idx0 + 1;
587 double frac = src_pos - (double)idx0;
588
589 // Clamp indices to valid range
590 if (idx0 >= src_samples)
591 idx0 = src_samples - 1;
592 if (idx1 >= src_samples)
593 idx1 = src_samples - 1;
594
595 // Linear interpolation
596 dst[i] = (float)((1.0 - frac) * src[idx0] + frac * src[idx1]);
597 }
598}