ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
src/common/session/audio.c
Go to the documentation of this file.
1
13#include "session/audio.h"
14#include <ascii-chat/common.h>
17
18#include <string.h>
19
20/* ============================================================================
21 * Session Audio Context Structure
22 * ============================================================================ */
23
27#define SESSION_AUDIO_MAX_SOURCES 32
28
37
69
70/* ============================================================================
71 * Session Audio Lifecycle Functions
72 * ============================================================================ */
73
75 // Allocate context
77
78 ctx->is_host = is_host;
79 ctx->running = false;
80
81 // Initialize underlying audio context
83 if (result != ASCIICHAT_OK) {
84 log_error("Failed to initialize audio context: %d", result);
85 SAFE_FREE(ctx);
86 return NULL;
87 }
88
89 // Initialize mixing resources for host
90 if (is_host) {
91 // Allocate mix buffer (enough for one audio buffer worth)
92 ctx->mix_buffer_size = AUDIO_BUFFER_SIZE * 4; // Extra space for safety
93 ctx->mix_buffer = SAFE_CALLOC(ctx->mix_buffer_size, sizeof(float), float *);
94
95 // Initialize source array
96 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
97 ctx->sources[i].source_id = 0;
98 ctx->sources[i].active = false;
99 ctx->sources[i].buffer = NULL;
100 }
101 ctx->source_count = 0;
102 }
103
104 ctx->initialized = true;
105 return ctx;
106}
107
109 if (!ctx) {
110 return;
111 }
112
113 // Stop audio if running
114 if (ctx->running) {
116 }
117
118 // Cleanup host-specific resources
119 if (ctx->is_host) {
120 // Destroy source buffers
121 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
122 if (ctx->sources[i].buffer) {
124 ctx->sources[i].buffer = NULL;
125 }
126 }
127
128 // Free mix buffer
129 if (ctx->mix_buffer) {
130 SAFE_FREE(ctx->mix_buffer);
131 }
132 }
133
134 // Destroy underlying audio context
136
137 ctx->initialized = false;
138 SAFE_FREE(ctx);
139}
140
141/* ============================================================================
142 * Session Audio Control Functions
143 * ============================================================================ */
144
146 if (!ctx || !ctx->initialized) {
147 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_start_capture: invalid context");
148 }
149
150 // Use full duplex for proper echo cancellation
151 return session_audio_start_duplex(ctx);
152}
153
155 if (!ctx || !ctx->initialized) {
156 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_start_playback: invalid context");
157 }
158
159 // Use full duplex for proper echo cancellation
160 return session_audio_start_duplex(ctx);
161}
162
164 if (!ctx || !ctx->initialized) {
165 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_start_duplex: invalid context");
166 }
167
168 if (ctx->running) {
169 return ASCIICHAT_OK; // Already running
170 }
171
173 if (result == ASCIICHAT_OK) {
174 ctx->running = true;
175 }
176
177 return result;
178}
179
181 if (!ctx || !ctx->initialized || !ctx->running) {
182 return;
183 }
184
185 (void)audio_stop_duplex(&ctx->audio_ctx);
186 ctx->running = false;
187}
188
190 if (!ctx || !ctx->initialized) {
191 return false;
192 }
193 return ctx->running;
194}
195
196/* ============================================================================
197 * Session Audio I/O Functions
198 * ============================================================================ */
199
200size_t session_audio_read_captured(session_audio_ctx_t *ctx, float *buffer, size_t num_samples) {
201 if (!ctx || !ctx->initialized || !buffer || num_samples == 0) {
202 return 0;
203 }
204
205 return audio_ring_buffer_read(ctx->audio_ctx.capture_buffer, buffer, num_samples);
206}
207
208asciichat_error_t session_audio_write_playback(session_audio_ctx_t *ctx, const float *buffer, size_t num_samples) {
209 if (!ctx || !ctx->initialized || !buffer) {
210 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_write_playback: invalid parameter");
211 }
212
213 return audio_write_samples(&ctx->audio_ctx, buffer, (int)num_samples);
214}
215
216/* ============================================================================
217 * Session Audio Host-Only Functions (Mixing)
218 * ============================================================================ */
219
221 if (!ctx || !ctx->initialized) {
222 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_add_source: invalid context");
223 }
224
225 if (!ctx->is_host) {
226 return SET_ERRNO(ERROR_INVALID_STATE, "session_audio_add_source: not a host context");
227 }
228
229 // Check if source already exists
230 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
231 if (ctx->sources[i].active && ctx->sources[i].source_id == source_id) {
232 return ASCIICHAT_OK; // Already registered
233 }
234 }
235
236 // Find empty slot
237 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
238 if (!ctx->sources[i].active) {
239 ctx->sources[i].source_id = source_id;
240 ctx->sources[i].active = true;
242 if (!ctx->sources[i].buffer) {
243 ctx->sources[i].active = false;
244 return SET_ERRNO(ERROR_MEMORY, "Failed to create audio buffer for source");
245 }
246 ctx->source_count++;
247 return ASCIICHAT_OK;
248 }
249 }
250
251 return SET_ERRNO(ERROR_RESOURCE_EXHAUSTED, "Maximum audio sources reached");
252}
253
255 if (!ctx || !ctx->initialized || !ctx->is_host) {
256 return;
257 }
258
259 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
260 if (ctx->sources[i].active && ctx->sources[i].source_id == source_id) {
261 if (ctx->sources[i].buffer) {
263 ctx->sources[i].buffer = NULL;
264 }
265 ctx->sources[i].active = false;
266 ctx->sources[i].source_id = 0;
267 ctx->source_count--;
268 return;
269 }
270 }
271}
272
274 size_t num_samples) {
275 if (!ctx || !ctx->initialized || !samples) {
276 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_write_source: invalid parameter");
277 }
278
279 if (!ctx->is_host) {
280 return SET_ERRNO(ERROR_INVALID_STATE, "session_audio_write_source: not a host context");
281 }
282
283 // Find the source
284 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
285 if (ctx->sources[i].active && ctx->sources[i].source_id == source_id) {
286 if (ctx->sources[i].buffer) {
287 return audio_ring_buffer_write(ctx->sources[i].buffer, samples, (int)num_samples);
288 }
289 return SET_ERRNO(ERROR_INVALID_STATE, "Source buffer not initialized");
290 }
291 }
292
293 return SET_ERRNO(ERROR_NOT_FOUND, "Audio source not found: %u", source_id);
294}
295
296size_t session_audio_mix_excluding(session_audio_ctx_t *ctx, uint32_t exclude_id, float *output, size_t num_samples) {
297 if (!ctx || !ctx->initialized || !output || num_samples == 0) {
298 return 0;
299 }
300
301 if (!ctx->is_host) {
302 return 0;
303 }
304
305 // Initialize output to silence
306 memset(output, 0, num_samples * sizeof(float));
307
308 // Ensure we have a mix buffer
309 if (!ctx->mix_buffer || num_samples > ctx->mix_buffer_size) {
310 return 0;
311 }
312
313 int sources_mixed = 0;
314
315 // Mix all active sources except the excluded one
316 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
317 if (!ctx->sources[i].active || ctx->sources[i].source_id == exclude_id) {
318 continue;
319 }
320
321 if (!ctx->sources[i].buffer) {
322 continue;
323 }
324
325 // Read samples from this source
326 size_t samples_read = audio_ring_buffer_read(ctx->sources[i].buffer, ctx->mix_buffer, num_samples);
327
328 if (samples_read == 0) {
329 continue;
330 }
331
332 // Add to output mix
333 for (size_t j = 0; j < samples_read; j++) {
334 output[j] += ctx->mix_buffer[j];
335 }
336
337 sources_mixed++;
338 }
339
340 // Apply simple clipping prevention if multiple sources were mixed
341 if (sources_mixed > 1) {
342 float scale = 1.0f / (float)sources_mixed;
343 for (size_t i = 0; i < num_samples; i++) {
344 output[i] *= scale;
345 // Soft clipping
346 if (output[i] > 1.0f) {
347 output[i] = 1.0f;
348 } else if (output[i] < -1.0f) {
349 output[i] = -1.0f;
350 }
351 }
352 }
353
354 return num_samples;
355}
356
358 if (ctx && !ctx->running)
359 ctx->audio_ctx.capture_media_source = source;
360}
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
⚙️ Common definitions, error codes, macros, and types shared throughout the application
size_t audio_ring_buffer_read(audio_ring_buffer_t *rb, float *data, size_t samples)
Read audio samples from ring buffer.
#define AUDIO_BUFFER_SIZE
Total audio buffer size (frames × channels)
asciichat_error_t audio_ring_buffer_write(audio_ring_buffer_t *rb, const float *data, int samples)
Write audio samples to ring buffer.
void audio_ring_buffer_destroy(audio_ring_buffer_t *rb)
Destroy an audio ring buffer.
asciichat_error_t audio_init(audio_context_t *ctx)
Initialize audio context and PortAudio.
audio_ring_buffer_t * audio_ring_buffer_create(void)
Create a new audio ring buffer (for playback with jitter buffering)
asciichat_error_t audio_start_duplex(audio_context_t *ctx)
Start full-duplex audio (simultaneous capture and playback)
void audio_destroy(audio_context_t *ctx)
Destroy audio context and clean up resources.
asciichat_error_t audio_stop_duplex(audio_context_t *ctx)
Stop full-duplex audio.
asciichat_error_t audio_write_samples(audio_context_t *ctx, const float *buffer, int num_samples)
Write audio samples to playback buffer.
unsigned int uint32_t
Definition common.h:58
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_INVALID_STATE
@ ERROR_RESOURCE_EXHAUSTED
@ ERROR_NOT_FOUND
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
asciichat_error_t session_audio_write_playback(session_audio_ctx_t *ctx, const float *buffer, size_t num_samples)
Write audio samples for playback.
size_t session_audio_read_captured(session_audio_ctx_t *ctx, float *buffer, size_t num_samples)
Read captured audio samples.
asciichat_error_t session_audio_start_capture(session_audio_ctx_t *ctx)
Start audio capture (microphone input)
void session_audio_destroy(session_audio_ctx_t *ctx)
Destroy session audio context and free resources.
asciichat_error_t session_audio_start_playback(session_audio_ctx_t *ctx)
Start audio playback (speaker output)
bool session_audio_is_running(session_audio_ctx_t *ctx)
Check if audio is currently running.
asciichat_error_t session_audio_write_source(session_audio_ctx_t *ctx, uint32_t source_id, const float *samples, size_t num_samples)
Write audio samples from a specific source (host only)
asciichat_error_t session_audio_start_duplex(session_audio_ctx_t *ctx)
Start full-duplex audio (simultaneous capture and playback)
void session_audio_stop(session_audio_ctx_t *ctx)
Stop all audio streams.
asciichat_error_t session_audio_add_source(session_audio_ctx_t *ctx, uint32_t source_id)
Add an audio source for mixing (host only)
void session_audio_remove_source(session_audio_ctx_t *ctx, uint32_t source_id)
Remove an audio source from mixing (host only)
session_audio_ctx_t * session_audio_create(bool is_host)
Create a new session audio context.
size_t session_audio_mix_excluding(session_audio_ctx_t *ctx, uint32_t exclude_id, float *output, size_t num_samples)
Mix all sources except one and return the result (host only)
🔊 Audio Capture and Playback Interface for ascii-chat
void session_audio_set_capture_source(session_audio_ctx_t *ctx, void *source)
#define SESSION_AUDIO_MAX_SOURCES
Maximum number of audio sources for mixing.
🔊 Session-level audio coordination wrapper
Audio context for full-duplex capture and playback.
void * capture_media_source
Borrowed local media audio; configure before starting duplex.
audio_ring_buffer_t * capture_buffer
OLD: Worker → Network (will become processed_capture_rb)
Audio ring buffer for real-time audio streaming.
Definition ringbuffer.h:205
Internal session audio context structure.
int source_count
Number of active sources (host only)
size_t mix_buffer_size
Size of mix buffer in samples.
session_audio_source_t sources[32]
Audio sources for mixing (host only)
bool initialized
Context is fully initialized.
bool is_host
True if this is a host context (has mixing capabilities)
audio_context_t audio_ctx
Underlying audio context from lib/audio.
bool running
Audio streams are currently running.
float * mix_buffer
Temporary buffer for mixing operations.
Per-source audio buffer info for mixing.