ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
lib/session/audio.c
Go to the documentation of this file.
1
13#include <ascii-chat/session/audio.h>
14#include <ascii-chat/common.h>
15#include <ascii-chat/audio/audio.h>
16#include <ascii-chat/asciichat_errno.h>
17
18#include <string.h>
19
20/* ============================================================================
21 * Session Audio Context Structure
22 * ============================================================================ */
23
27#define SESSION_AUDIO_MAX_SOURCES 32
28
32typedef struct {
33 uint32_t source_id;
34 bool active;
35 audio_ring_buffer_t *buffer;
37
69
70/* ============================================================================
71 * Session Audio Lifecycle Functions
72 * ============================================================================ */
73
74session_audio_ctx_t *session_audio_create(bool is_host) {
75 // Allocate context
76 session_audio_ctx_t *ctx = SAFE_CALLOC(1, sizeof(session_audio_ctx_t), session_audio_ctx_t *);
77
78 ctx->is_host = is_host;
79 ctx->running = false;
80
81 // Initialize underlying audio context
82 asciichat_error_t result = audio_init(&ctx->audio_ctx);
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
108void session_audio_destroy(session_audio_ctx_t *ctx) {
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) {
123 audio_ring_buffer_destroy(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
135 audio_destroy(&ctx->audio_ctx);
136
137 ctx->initialized = false;
138 SAFE_FREE(ctx);
139}
140
141/* ============================================================================
142 * Session Audio Control Functions
143 * ============================================================================ */
144
145asciichat_error_t session_audio_start_capture(session_audio_ctx_t *ctx) {
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
154asciichat_error_t session_audio_start_playback(session_audio_ctx_t *ctx) {
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
163asciichat_error_t session_audio_start_duplex(session_audio_ctx_t *ctx) {
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
172 asciichat_error_t result = audio_start_duplex(&ctx->audio_ctx);
173 if (result == ASCIICHAT_OK) {
174 ctx->running = true;
175 }
176
177 return result;
178}
179
180void session_audio_stop(session_audio_ctx_t *ctx) {
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
189bool session_audio_is_running(session_audio_ctx_t *ctx) {
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 asciichat_error_t result = audio_read_samples(&ctx->audio_ctx, buffer, (int)num_samples);
206 if (result != ASCIICHAT_OK) {
207 return 0;
208 }
209
210 return num_samples;
211}
212
213asciichat_error_t session_audio_write_playback(session_audio_ctx_t *ctx, const float *buffer, size_t num_samples) {
214 if (!ctx || !ctx->initialized || !buffer) {
215 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_write_playback: invalid parameter");
216 }
217
218 return audio_write_samples(&ctx->audio_ctx, buffer, (int)num_samples);
219}
220
221/* ============================================================================
222 * Session Audio Host-Only Functions (Mixing)
223 * ============================================================================ */
224
225asciichat_error_t session_audio_add_source(session_audio_ctx_t *ctx, uint32_t source_id) {
226 if (!ctx || !ctx->initialized) {
227 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_add_source: invalid context");
228 }
229
230 if (!ctx->is_host) {
231 return SET_ERRNO(ERROR_INVALID_STATE, "session_audio_add_source: not a host context");
232 }
233
234 // Check if source already exists
235 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
236 if (ctx->sources[i].active && ctx->sources[i].source_id == source_id) {
237 return ASCIICHAT_OK; // Already registered
238 }
239 }
240
241 // Find empty slot
242 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
243 if (!ctx->sources[i].active) {
244 ctx->sources[i].source_id = source_id;
245 ctx->sources[i].active = true;
246 ctx->sources[i].buffer = audio_ring_buffer_create();
247 if (!ctx->sources[i].buffer) {
248 ctx->sources[i].active = false;
249 return SET_ERRNO(ERROR_MEMORY, "Failed to create audio buffer for source");
250 }
251 ctx->source_count++;
252 return ASCIICHAT_OK;
253 }
254 }
255
256 return SET_ERRNO(ERROR_RESOURCE_EXHAUSTED, "Maximum audio sources reached");
257}
258
259void session_audio_remove_source(session_audio_ctx_t *ctx, uint32_t source_id) {
260 if (!ctx || !ctx->initialized || !ctx->is_host) {
261 return;
262 }
263
264 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
265 if (ctx->sources[i].active && ctx->sources[i].source_id == source_id) {
266 if (ctx->sources[i].buffer) {
267 audio_ring_buffer_destroy(ctx->sources[i].buffer);
268 ctx->sources[i].buffer = NULL;
269 }
270 ctx->sources[i].active = false;
271 ctx->sources[i].source_id = 0;
272 ctx->source_count--;
273 return;
274 }
275 }
276}
277
278asciichat_error_t session_audio_write_source(session_audio_ctx_t *ctx, uint32_t source_id, const float *samples,
279 size_t num_samples) {
280 if (!ctx || !ctx->initialized || !samples) {
281 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_write_source: invalid parameter");
282 }
283
284 if (!ctx->is_host) {
285 return SET_ERRNO(ERROR_INVALID_STATE, "session_audio_write_source: not a host context");
286 }
287
288 // Find the source
289 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
290 if (ctx->sources[i].active && ctx->sources[i].source_id == source_id) {
291 if (ctx->sources[i].buffer) {
292 return audio_ring_buffer_write(ctx->sources[i].buffer, samples, (int)num_samples);
293 }
294 return SET_ERRNO(ERROR_INVALID_STATE, "Source buffer not initialized");
295 }
296 }
297
298 return SET_ERRNO(ERROR_NOT_FOUND, "Audio source not found: %u", source_id);
299}
300
301size_t session_audio_mix_excluding(session_audio_ctx_t *ctx, uint32_t exclude_id, float *output, size_t num_samples) {
302 if (!ctx || !ctx->initialized || !output || num_samples == 0) {
303 return 0;
304 }
305
306 if (!ctx->is_host) {
307 return 0;
308 }
309
310 // Initialize output to silence
311 memset(output, 0, num_samples * sizeof(float));
312
313 // Ensure we have a mix buffer
314 if (!ctx->mix_buffer || num_samples > ctx->mix_buffer_size) {
315 return 0;
316 }
317
318 int sources_mixed = 0;
319
320 // Mix all active sources except the excluded one
321 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
322 if (!ctx->sources[i].active || ctx->sources[i].source_id == exclude_id) {
323 continue;
324 }
325
326 if (!ctx->sources[i].buffer) {
327 continue;
328 }
329
330 // Read samples from this source
331 size_t samples_read = audio_ring_buffer_read(ctx->sources[i].buffer, ctx->mix_buffer, num_samples);
332
333 if (samples_read == 0) {
334 continue;
335 }
336
337 // Add to output mix
338 for (size_t j = 0; j < samples_read; j++) {
339 output[j] += ctx->mix_buffer[j];
340 }
341
342 sources_mixed++;
343 }
344
345 // Apply simple clipping prevention if multiple sources were mixed
346 if (sources_mixed > 1) {
347 float scale = 1.0f / (float)sources_mixed;
348 for (size_t i = 0; i < num_samples; i++) {
349 output[i] *= scale;
350 // Soft clipping
351 if (output[i] > 1.0f) {
352 output[i] = 1.0f;
353 } else if (output[i] < -1.0f) {
354 output[i] = -1.0f;
355 }
356 }
357 }
358
359 return num_samples;
360}
size_t audio_ring_buffer_read(audio_ring_buffer_t *rb, float *data, size_t samples)
asciichat_error_t audio_ring_buffer_write(audio_ring_buffer_t *rb, const float *data, int samples)
void audio_ring_buffer_destroy(audio_ring_buffer_t *rb)
asciichat_error_t audio_init(audio_context_t *ctx)
audio_ring_buffer_t * audio_ring_buffer_create(void)
asciichat_error_t audio_start_duplex(audio_context_t *ctx)
void audio_destroy(audio_context_t *ctx)
asciichat_error_t audio_stop_duplex(audio_context_t *ctx)
asciichat_error_t audio_write_samples(audio_context_t *ctx, const float *buffer, int num_samples)
asciichat_error_t audio_read_samples(audio_context_t *ctx, float *buffer, int num_samples)
asciichat_error_t session_audio_write_playback(session_audio_ctx_t *ctx, const float *buffer, size_t num_samples)
size_t session_audio_read_captured(session_audio_ctx_t *ctx, float *buffer, size_t num_samples)
asciichat_error_t session_audio_start_capture(session_audio_ctx_t *ctx)
void session_audio_destroy(session_audio_ctx_t *ctx)
asciichat_error_t session_audio_start_playback(session_audio_ctx_t *ctx)
bool session_audio_is_running(session_audio_ctx_t *ctx)
asciichat_error_t session_audio_write_source(session_audio_ctx_t *ctx, uint32_t source_id, const float *samples, size_t num_samples)
asciichat_error_t session_audio_start_duplex(session_audio_ctx_t *ctx)
void session_audio_stop(session_audio_ctx_t *ctx)
asciichat_error_t session_audio_add_source(session_audio_ctx_t *ctx, uint32_t source_id)
void session_audio_remove_source(session_audio_ctx_t *ctx, uint32_t source_id)
session_audio_ctx_t * session_audio_create(bool is_host)
#define SESSION_AUDIO_MAX_SOURCES
Maximum number of audio sources for mixing.
size_t session_audio_mix_excluding(session_audio_ctx_t *ctx, uint32_t exclude_id, float *output, size_t num_samples)
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.
audio_ring_buffer_t * buffer