ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
client_audio_pipeline.cpp
Go to the documentation of this file.
1
12// C++ headers must come FIRST before any C headers that include stdatomic.h
13#include <memory>
14#include <cstring>
15#include <math.h>
16#include <atomic>
17
18// WebRTC headers for AEC3 MUST come before ascii-chat headers to avoid macro conflicts
19// Define required WebRTC macros before including headers
20#define WEBRTC_APM_DEBUG_DUMP 0
21#define WEBRTC_MODULE_AUDIO_PROCESSING 1
22// WEBRTC_POSIX should only be defined on POSIX systems (Unix/macOS), not Windows
23#if defined(__unix__) || defined(__APPLE__)
24#define WEBRTC_POSIX 1
25#endif
26
27// Suppress WebRTC/Abseil warnings about deprecated builtins and unused parameters
28// These are third-party code issues, not our code
29#pragma clang diagnostic push
30#pragma clang diagnostic ignored "-Wdeprecated-builtins"
31#pragma clang diagnostic ignored "-Wunused-parameter"
32
33// WebRTC AEC3 extracted repository has different include paths
34// than full WebRTC (no api/audio/ subdirectory)
35#include "api/echo_canceller3_factory.h"
36#include "api/echo_control.h"
37// Note: extracted AEC3 doesn't have environment API - using direct classes
38#include "audio_processing/audio_buffer.h"
39
40#pragma clang diagnostic pop
41
42// WebRTC defines FATAL() with no parameters, but ascii-chat defines
43// FATAL(code, ...) with parameters. Undefine the WebRTC version before
44// including ascii-chat headers so ascii-chat's version takes precedence.
45#ifdef FATAL
46#undef FATAL
47#endif
48
49// Now include ascii-chat headers after WebRTC to avoid macro conflicts
51#include "audio/wav_writer.h"
52#include "common.h"
53#include "log/logging.h"
55
56// Include mixer.h for compressor, noise gate, and filter functions
57#include "audio/mixer.h"
58
59// For AEC3 metrics reporting
60#include "audio/analysis.h"
61
62#include <opus/opus.h>
63#include <string.h>
64
65// Prevent stdatomic.h from defining conflicting macros in C++ context
66#define __STDC_NO_ATOMICS__ 1
67
68// ============================================================================
69// WebRTC AEC3 C++ Wrapper (hidden from C code)
70// ============================================================================
71
79 std::unique_ptr<webrtc::EchoControl> aec3;
80 webrtc::EchoCanceller3Config config;
81
82 WebRTCAec3Wrapper() = default;
83 ~WebRTCAec3Wrapper() = default;
84};
85
86// Global tracking of max render RMS for AEC3 diagnostics (accessible from both threads)
87static std::atomic<float> g_max_render_rms{0.0f};
88
89// Global counter for render frames fed to AEC3 (for warmup tracking)
90static std::atomic<int> g_render_frames_fed{0};
91
92#ifdef __cplusplus
93extern "C" {
94#endif
95
96// ============================================================================
97// Default Configuration
98// ============================================================================
99
103 .frame_size_ms = CLIENT_AUDIO_PIPELINE_FRAME_MS,
104 .opus_bitrate = 24000,
105
106 .echo_filter_ms = 250,
107
108 .noise_suppress_db = -25,
109 .agc_level = 8000,
110 .agc_max_gain = 30,
111
112 // Jitter margin: wait this long before starting playback
113 // Lower = less latency but more risk of underruns
114 // CRITICAL: Must match AUDIO_JITTER_BUFFER_THRESHOLD in ringbuffer.h!
115 .jitter_margin_ms = 20, // 20ms = 1 Opus packet (optimized for LAN)
116
117 // Higher cutoff to cut low-frequency rumble and feedback
118 .highpass_hz = 150.0f, // Was 80Hz, increased to break rumble feedback loop
119 .lowpass_hz = 8000.0f,
120
121 // Compressor: only compress loud peaks, minimal makeup to avoid clipping
122 // User reported clipping with +6dB makeup gain
123 .comp_threshold_db = -6.0f, // Only compress peaks above -6dB
124 .comp_ratio = 3.0f, // Gentler 3:1 ratio
125 .comp_attack_ms = 5.0f, // Fast attack for peaks
126 .comp_release_ms = 150.0f, // Slower release
127 .comp_makeup_db = 2.0f, // Reduced from 6dB to prevent clipping
128
129 // Noise gate: VERY aggressive to cut quiet background audio completely
130 // User feedback: "don't amplify or play quiet background audio at all"
131 .gate_threshold = 0.08f, // -22dB threshold (was 0.02/-34dB) - cuts quiet audio hard
132 .gate_attack_ms = 0.5f, // Very fast attack
133 .gate_release_ms = 30.0f, // Fast release (was 50ms)
134 .gate_hysteresis = 0.3f, // Tighter hysteresis = stays closed longer
135
137 };
138}
139
140// ============================================================================
141// Lifecycle Functions
142// ============================================================================
143
155 if (!p) {
156 log_error("Failed to allocate client audio pipeline");
157 return NULL;
158 }
159
160 // Use default config if none provided
161 if (config) {
162 p->config = *config;
163 } else {
165 }
166
167 p->flags = p->config.flags;
169
170 // No mutex needed - full-duplex means single callback thread handles all AEC3
171
172 // Initialize Opus encoder/decoder first (no exceptions)
173 int opus_error = 0;
174 p->encoder = opus_encoder_create(p->config.sample_rate, 1, OPUS_APPLICATION_VOIP, &opus_error);
175 if (!p->encoder || opus_error != OPUS_OK) {
176 log_error("Failed to create Opus encoder: %d", opus_error);
177 goto error;
178 }
179 opus_encoder_ctl(p->encoder, OPUS_SET_BITRATE(p->config.opus_bitrate));
180
181 // CRITICAL: Disable DTX (Discontinuous Transmission) to prevent "beeps"
182 // DTX stops sending frames during silence, causing audible clicks/beeps when audio resumes
183 opus_encoder_ctl(p->encoder, OPUS_SET_DTX(0));
184
185 // Create Opus decoder
186 p->decoder = opus_decoder_create(p->config.sample_rate, 1, &opus_error);
187 if (!p->decoder || opus_error != OPUS_OK) {
188 log_error("Failed to create Opus decoder: %d", opus_error);
189 goto error;
190 }
191
192 // Create WebRTC AEC3 Echo Cancellation
193 // AEC3 provides production-grade acoustic echo cancellation with:
194 // - Automatic network delay estimation (0-500ms)
195 // - Adaptive filtering to actual echo path
196 // - Residual echo suppression via spectral subtraction
197 // - Jitter buffer handling via side information
198 if (p->flags.echo_cancel) {
199 // Configure AEC3 for better low-frequency (bass) echo cancellation
200 webrtc::EchoCanceller3Config aec3_config;
201
202 // Increase filter length for bass frequencies (default 13 blocks = ~17ms)
203 // Bass at 80Hz has 12.5ms period, so we need at least 50+ blocks (~67ms)
204 // to properly model the echo path for low frequencies
205 aec3_config.filter.main.length_blocks = 50; // ~67ms (was 13)
206 aec3_config.filter.shadow.length_blocks = 50; // ~67ms (was 13)
207 aec3_config.filter.main_initial.length_blocks = 25; // ~33ms (was 12)
208 aec3_config.filter.shadow_initial.length_blocks = 25; // ~33ms (was 12)
209
210 // More aggressive low-frequency suppression thresholds
211 // Lower values = more aggressive echo suppression
212 aec3_config.echo_audibility.audibility_threshold_lf = 5; // (was 10)
213
214 // Create AEC3 using the factory
215 auto factory = webrtc::EchoCanceller3Factory(aec3_config);
216
217 std::unique_ptr<webrtc::EchoControl> echo_control = factory.Create(static_cast<int>(p->config.sample_rate), // 48kHz
218 1, // num_render_channels (speaker output)
219 1 // num_capture_channels (microphone input)
220 );
221
222 if (!echo_control) {
223 log_warn("Failed to create WebRTC AEC3 instance - echo cancellation unavailable");
224 p->echo_canceller = NULL;
225 } else {
226 // Successfully created AEC3 - wrap in our C++ wrapper for C compatibility
227 auto wrapper = new WebRTCAec3Wrapper();
228 wrapper->aec3 = std::move(echo_control);
229 wrapper->config = aec3_config;
230 p->echo_canceller = wrapper;
231
232 log_info("✓ WebRTC AEC3 initialized (67ms filter for bass, adaptive delay)");
233
234 // Create persistent AudioBuffer instances for AEC3
235 p->aec3_render_buffer = new webrtc::AudioBuffer(48000, 1, 48000, 1, 48000, 1);
236 p->aec3_capture_buffer = new webrtc::AudioBuffer(48000, 1, 48000, 1, 48000, 1);
237
238 auto *render_buf = static_cast<webrtc::AudioBuffer *>(p->aec3_render_buffer);
239 auto *capture_buf = static_cast<webrtc::AudioBuffer *>(p->aec3_capture_buffer);
240
241 // Zero-initialize channel data
242 float *const *render_ch = render_buf->channels();
243 float *const *capture_ch = capture_buf->channels();
244 if (render_ch && render_ch[0]) {
245 memset(render_ch[0], 0, 480 * sizeof(float)); // 10ms at 48kHz
246 }
247 if (capture_ch && capture_ch[0]) {
248 memset(capture_ch[0], 0, 480 * sizeof(float));
249 }
250
251 // Prime filterbank state with dummy processing cycle
252 render_buf->SplitIntoFrequencyBands();
253 render_buf->MergeFrequencyBands();
254 capture_buf->SplitIntoFrequencyBands();
255 capture_buf->MergeFrequencyBands();
256
257 log_info(" - AudioBuffer filterbank state initialized");
258
259 // Warm up AEC3 with 10 silent frames to initialize internal state
260 for (int warmup = 0; warmup < 10; warmup++) {
261 memset(render_ch[0], 0, 480 * sizeof(float));
262 memset(capture_ch[0], 0, 480 * sizeof(float));
263
264 render_buf->SplitIntoFrequencyBands();
265 wrapper->aec3->AnalyzeRender(render_buf);
266 render_buf->MergeFrequencyBands();
267
268 wrapper->aec3->AnalyzeCapture(capture_buf);
269 capture_buf->SplitIntoFrequencyBands();
270 wrapper->aec3->SetAudioBufferDelay(0);
271 wrapper->aec3->ProcessCapture(capture_buf, false);
272 capture_buf->MergeFrequencyBands();
273 }
274 log_info(" - AEC3 warmed up with 10 silent frames");
275 log_info(" - Persistent AudioBuffer instances created");
276 }
277 }
278
279 // Initialize debug WAV writers for AEC3 analysis (if echo_cancel enabled)
280 p->debug_wav_aec3_in = NULL;
281 p->debug_wav_aec3_out = NULL;
282 if (p->flags.echo_cancel) {
283 // Open WAV files to capture AEC3 input and output
284 p->debug_wav_aec3_in = wav_writer_open("/tmp/aec3_input.wav", 48000, 1);
285 p->debug_wav_aec3_out = wav_writer_open("/tmp/aec3_output.wav", 48000, 1);
286 if (p->debug_wav_aec3_in) {
287 log_info("Debug: Recording AEC3 input to /tmp/aec3_input.wav");
288 }
289 if (p->debug_wav_aec3_out) {
290 log_info("Debug: Recording AEC3 output to /tmp/aec3_output.wav");
291 }
292
293 log_info("✓ AEC3 echo cancellation enabled (full-duplex mode, no ring buffer delay)");
294 }
295
296 // Initialize audio processing components (compressor, noise gate, filters)
297 // These are applied in the capture path after AEC3 and before Opus encoding
298 {
299 float sample_rate = (float)p->config.sample_rate;
300
301 // Initialize compressor with config values
302 compressor_init(&p->compressor, sample_rate);
305 log_info("✓ Capture compressor: threshold=%.1fdB, ratio=%.1f:1, makeup=+%.1fdB", p->config.comp_threshold_db,
307
308 // Initialize noise gate with config values
309 noise_gate_init(&p->noise_gate, sample_rate);
312 log_info("✓ Capture noise gate: threshold=%.4f (%.1fdB)", p->config.gate_threshold,
313 20.0f * log10f(p->config.gate_threshold + 1e-10f));
314
315 // Initialize PLAYBACK noise gate - cuts quiet received audio before speakers
316 // Very low threshold - only cut actual silence, not quiet voice audio
317 // The server sends audio with RMS=0.01-0.02, so threshold must be below that
318 noise_gate_init(&p->playback_noise_gate, sample_rate);
320 0.002f, // -54dB threshold - only cut near-silence
321 1.0f, // 1ms attack - fast open
322 50.0f, // 50ms release - smooth close
323 0.4f); // Hysteresis
324 log_info("✓ Playback noise gate: threshold=0.002 (-54dB)");
325
326 // Initialize highpass filter (removes low-frequency rumble)
327 highpass_filter_init(&p->highpass, p->config.highpass_hz, sample_rate);
328 log_info("✓ Capture highpass filter: %.1f Hz", p->config.highpass_hz);
329
330 // Initialize lowpass filter (removes high-frequency hiss)
331 lowpass_filter_init(&p->lowpass, p->config.lowpass_hz, sample_rate);
332 log_info("✓ Capture lowpass filter: %.1f Hz", p->config.lowpass_hz);
333 }
334
335 p->initialized = true;
336
337 // Initialize startup fade-in to prevent initial microphone click
338 // 200ms at 48kHz = 9600 samples - gradual ramp from silence to full volume
339 // Longer fade-in (200ms vs 50ms) gives much smoother transition without audible pop
340 p->capture_fadein_remaining = (p->config.sample_rate * 200) / 1000; // 200ms worth of samples
341 log_info("✓ Capture fade-in: %d samples (200ms)", p->capture_fadein_remaining);
342
343 log_info("Audio pipeline created: %dHz, %dms frames, %dkbps Opus", p->config.sample_rate, p->config.frame_size_ms,
344 p->config.opus_bitrate / 1000);
345
346 return p;
347
348error:
349 if (p->encoder)
350 opus_encoder_destroy(p->encoder);
351 if (p->decoder)
352 opus_decoder_destroy(p->decoder);
353 if (p->echo_canceller) {
354 delete static_cast<WebRTCAec3Wrapper *>(p->echo_canceller);
355 }
356 SAFE_FREE(p);
357 return NULL;
358}
359
361 if (!pipeline)
362 return;
363
364 // Clean up WebRTC AEC3 AudioBuffer instances
365 if (pipeline->aec3_render_buffer) {
366 delete static_cast<webrtc::AudioBuffer *>(pipeline->aec3_render_buffer);
367 pipeline->aec3_render_buffer = NULL;
368 }
369 if (pipeline->aec3_capture_buffer) {
370 delete static_cast<webrtc::AudioBuffer *>(pipeline->aec3_capture_buffer);
371 pipeline->aec3_capture_buffer = NULL;
372 }
373
374 // Clean up WebRTC AEC3
375 if (pipeline->echo_canceller) {
376 delete static_cast<WebRTCAec3Wrapper *>(pipeline->echo_canceller);
377 pipeline->echo_canceller = NULL;
378 }
379
380 // Clean up Opus
381 if (pipeline->encoder) {
382 opus_encoder_destroy(pipeline->encoder);
383 pipeline->encoder = NULL;
384 }
385 if (pipeline->decoder) {
386 opus_decoder_destroy(pipeline->decoder);
387 pipeline->decoder = NULL;
388 }
389
390 // Clean up debug WAV writers
391 if (pipeline->debug_wav_aec3_in) {
393 pipeline->debug_wav_aec3_in = NULL;
394 }
395 if (pipeline->debug_wav_aec3_out) {
397 pipeline->debug_wav_aec3_out = NULL;
398 }
399
400 SAFE_FREE(pipeline);
401}
402
403// ============================================================================
404// Configuration Functions
405// ============================================================================
406
408 if (!pipeline)
409 return;
410 // No mutex needed - flags are only read by capture thread
411 pipeline->flags = flags;
412}
413
415 if (!pipeline)
417 // No mutex needed - flags are only written from main thread during setup
418 return pipeline->flags;
419}
420
421// ============================================================================
422// Audio Processing Functions
423// ============================================================================
424
431int client_audio_pipeline_capture(client_audio_pipeline_t *pipeline, const float *input, int num_samples,
432 uint8_t *opus_out, int max_opus_len) {
433 if (!pipeline || !input || !opus_out || num_samples != pipeline->frame_size) {
434 return -1;
435 }
436
437 // Input is already processed by process_duplex() in full-duplex mode.
438 // Just encode with Opus.
439 int opus_len = opus_encode_float(pipeline->encoder, input, num_samples, opus_out, max_opus_len);
440
441 if (opus_len < 0) {
442 log_error("Opus encoding failed: %d", opus_len);
443 return -1;
444 }
445
446 return opus_len;
447}
448
452int client_audio_pipeline_playback(client_audio_pipeline_t *pipeline, const uint8_t *opus_in, int opus_len,
453 float *output, int num_samples) {
454 if (!pipeline || !opus_in || !output) {
455 return -1;
456 }
457
458 // No mutex needed - Opus decoder is only used from this thread
459
460 // Decode Opus
461 int decoded_samples = opus_decode_float(pipeline->decoder, opus_in, opus_len, output, num_samples, 0);
462
463 if (decoded_samples < 0) {
464 log_error("Opus decoding failed: %d", decoded_samples);
465 return -1;
466 }
467
468 // Apply playback noise gate - cut quiet background audio before it reaches speakers
469 if (decoded_samples > 0) {
470 noise_gate_process_buffer(&pipeline->playback_noise_gate, output, decoded_samples);
471 }
472
473 // NOTE: Render signal is queued to AEC3 in output_callback() when audio plays,
474 // not here. The capture thread drains the queue and processes AEC3.
475
476 return decoded_samples;
477}
478
482int client_audio_pipeline_get_playback_frame(client_audio_pipeline_t *pipeline, float *output, int num_samples) {
483 if (!pipeline || !output) {
484 return -1;
485 }
486
487 // No mutex needed - this is a placeholder
488 memset(output, 0, num_samples * sizeof(float));
489 return num_samples;
490}
491
509void client_audio_pipeline_process_duplex(client_audio_pipeline_t *pipeline, const float *render_samples,
510 int render_count, const float *capture_samples, int capture_count,
511 float *processed_output) {
512 if (!pipeline || !processed_output)
513 return;
514
515 // Copy capture samples to output buffer for processing
516 if (capture_samples && capture_count > 0) {
517 memcpy(processed_output, capture_samples, capture_count * sizeof(float));
518 } else {
519 memset(processed_output, 0, capture_count * sizeof(float));
520 return;
521 }
522
523 // Check for AEC3 bypass
524 static int bypass_aec3 = -1;
525 if (bypass_aec3 == -1) {
526 const char *env = platform_getenv("BYPASS_AEC3");
527 bypass_aec3 = (env && (strcmp(env, "1") == 0 || strcmp(env, "true") == 0)) ? 1 : 0;
528 if (bypass_aec3) {
529 log_warn("AEC3 BYPASSED (full-duplex mode) via BYPASS_AEC3=1");
530 }
531 }
532
533 // Debug WAV recording
534 if (pipeline->debug_wav_aec3_in) {
535 wav_writer_write((wav_writer_t *)pipeline->debug_wav_aec3_in, capture_samples, capture_count);
536 }
537
538 // Apply startup fade-in using smoothstep curve
539 if (pipeline->capture_fadein_remaining > 0) {
540 const int total_fadein_samples = (pipeline->config.sample_rate * 200) / 1000;
541 for (int i = 0; i < capture_count && pipeline->capture_fadein_remaining > 0; i++) {
542 float progress = 1.0f - ((float)pipeline->capture_fadein_remaining / (float)total_fadein_samples);
543 float gain = smoothstep(progress);
544 processed_output[i] *= gain;
545 pipeline->capture_fadein_remaining--;
546 }
547 }
548
549 // WebRTC AEC3 processing - INLINE, no ring buffer, no mutex
550 if (!bypass_aec3 && pipeline->flags.echo_cancel && pipeline->echo_canceller) {
551 auto wrapper = static_cast<WebRTCAec3Wrapper *>(pipeline->echo_canceller);
552 if (wrapper && wrapper->aec3) {
553 const int webrtc_frame_size = 480; // 10ms at 48kHz
554
555 auto *render_buf = static_cast<webrtc::AudioBuffer *>(pipeline->aec3_render_buffer);
556 auto *capture_buf = static_cast<webrtc::AudioBuffer *>(pipeline->aec3_capture_buffer);
557
558 if (render_buf && capture_buf) {
559 float *const *render_channels = render_buf->channels();
560 float *const *capture_channels = capture_buf->channels();
561
562 if (render_channels && render_channels[0] && capture_channels && capture_channels[0]) {
563 // Verify render_samples is valid before accessing
564 if (!render_samples && render_count > 0) {
565 log_warn_every(1000000, "AEC3: render_samples is NULL but render_count=%d", render_count);
566 return;
567 }
568
569 // Process in 10ms chunks (AEC3 requirement)
570 int render_offset = 0;
571 int capture_offset = 0;
572
573 while (capture_offset < capture_count || render_offset < render_count) {
574 // STEP 1: Feed render signal (what's playing to speakers)
575 // In full-duplex, this is THE EXACT audio being played RIGHT NOW
576 if (render_samples && render_offset < render_count) {
577 int render_chunk = (render_offset + webrtc_frame_size <= render_count) ? webrtc_frame_size
578 : (render_count - render_offset);
579 if (render_chunk == webrtc_frame_size) {
580 // Scale float [-1,1] to WebRTC int16-range [-32768, 32767]
581 copy_buffer_with_gain(&render_samples[render_offset], render_channels[0], webrtc_frame_size, 32768.0f);
582 render_buf->SplitIntoFrequencyBands();
583 wrapper->aec3->AnalyzeRender(render_buf);
584 render_buf->MergeFrequencyBands();
585 g_render_frames_fed.fetch_add(1, std::memory_order_relaxed);
586 }
587 render_offset += render_chunk;
588 }
589
590 // STEP 2: Process capture (microphone input)
591 if (capture_offset < capture_count) {
592 int capture_chunk = (capture_offset + webrtc_frame_size <= capture_count)
593 ? webrtc_frame_size
594 : (capture_count - capture_offset);
595 if (capture_chunk == webrtc_frame_size) {
596 // Scale float [-1,1] to WebRTC int16-range [-32768, 32767]
597 copy_buffer_with_gain(&processed_output[capture_offset], capture_channels[0], webrtc_frame_size,
598 32768.0f);
599
600 // AEC3 sequence: AnalyzeCapture, split, ProcessCapture, merge
601 wrapper->aec3->AnalyzeCapture(capture_buf);
602 capture_buf->SplitIntoFrequencyBands();
603
604 // NOTE: SetAudioBufferDelay() is just an initial hint when use_external_delay_estimator=false
605 // (default). AEC3's internal delay estimator will find the actual delay (~144ms in practice). We don't
606 // call it here - let AEC3 estimate delay automatically.
607
608 wrapper->aec3->ProcessCapture(capture_buf, false);
609 capture_buf->MergeFrequencyBands();
610
611 // Scale back to float range and apply soft clip to prevent distortion
612 // Use gentle soft_clip (threshold=0.6, steepness=2.5) to leave headroom for compressor
613 for (int j = 0; j < webrtc_frame_size; j++) {
614 float sample = capture_channels[0][j] / 32768.0f;
615 processed_output[capture_offset + j] = soft_clip(sample, 0.6f, 2.5f);
616 }
617
618 // Log AEC3 metrics periodically
619 static int duplex_log_count = 0;
620 if (++duplex_log_count % 100 == 1) {
621 webrtc::EchoControl::Metrics metrics = wrapper->aec3->GetMetrics();
622 log_info("AEC3 DUPLEX: ERL=%.1f ERLE=%.1f delay=%dms", metrics.echo_return_loss,
623 metrics.echo_return_loss_enhancement, metrics.delay_ms);
624 audio_analysis_set_aec3_metrics(metrics.echo_return_loss, metrics.echo_return_loss_enhancement,
625 metrics.delay_ms);
626 }
627 }
628 capture_offset += capture_chunk;
629 }
630 }
631 }
632 }
633 }
634 }
635
636 // Debug WAV recording (after AEC3)
637 if (pipeline->debug_wav_aec3_out) {
638 wav_writer_write((wav_writer_t *)pipeline->debug_wav_aec3_out, processed_output, capture_count);
639 }
640
641 // Apply capture processing chain: filters, noise gate, compressor
642 if (pipeline->flags.highpass) {
643 highpass_filter_process_buffer(&pipeline->highpass, processed_output, capture_count);
644 }
645 if (pipeline->flags.lowpass) {
646 lowpass_filter_process_buffer(&pipeline->lowpass, processed_output, capture_count);
647 }
648 if (pipeline->flags.noise_gate) {
649 noise_gate_process_buffer(&pipeline->noise_gate, processed_output, capture_count);
650 }
651 if (pipeline->flags.compressor) {
652 for (int i = 0; i < capture_count; i++) {
653 float gain = compressor_process_sample(&pipeline->compressor, processed_output[i]);
654 processed_output[i] *= gain;
655 }
656 // Apply soft clipping after compressor - threshold=0.7 gives 3dB headroom
657 soft_clip_buffer(processed_output, capture_count, 0.7f, 3.0f);
658 }
659}
660
665 if (!pipeline)
666 return 0;
667 return pipeline->config.jitter_margin_ms;
668}
669
674 if (!pipeline)
675 return;
676
677 // Reset global counters
678 g_render_frames_fed.store(0, std::memory_order_relaxed);
679 g_max_render_rms.store(0.0f, std::memory_order_relaxed);
680
681 log_info("Pipeline state reset");
682}
683
684#ifdef __cplusplus
685}
686#endif
🔌 Cross-platform abstraction layer umbrella header for ascii-chat
void audio_analysis_set_aec3_metrics(double echo_return_loss, double echo_return_loss_enhancement, int delay_ms)
Set AEC3 echo cancellation metrics.
Definition analysis.c:508
Audio Analysis and Debugging Interface.
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.
client_audio_pipeline_t * client_audio_pipeline_create(const client_audio_pipeline_config_t *config)
Create and initialize a client audio pipeline.
void client_audio_pipeline_process_duplex(client_audio_pipeline_t *pipeline, const float *render_samples, int render_count, const float *capture_samples, int capture_count, float *processed_output)
Process AEC3 inline in full-duplex callback.
client_audio_pipeline_flags_t client_audio_pipeline_get_flags(client_audio_pipeline_t *pipeline)
Get current component enable flags.
int client_audio_pipeline_jitter_margin(client_audio_pipeline_t *pipeline)
Get jitter buffer margin (buffered time in ms)
int client_audio_pipeline_get_playback_frame(client_audio_pipeline_t *pipeline, float *output, int num_samples)
Get audio frame from jitter buffer for playback callback.
client_audio_pipeline_config_t client_audio_pipeline_default_config(void)
Get default configuration.
void client_audio_pipeline_set_flags(client_audio_pipeline_t *pipeline, client_audio_pipeline_flags_t flags)
Set component enable flags.
int client_audio_pipeline_capture(client_audio_pipeline_t *pipeline, const float *input, int num_samples, uint8_t *opus_out, int max_opus_len)
Process captured audio and encode to Opus.
void client_audio_pipeline_destroy(client_audio_pipeline_t *pipeline)
Destroy a client audio pipeline.
void client_audio_pipeline_reset(client_audio_pipeline_t *pipeline)
Reset pipeline state.
Unified client-side audio processing pipeline.
#define CLIENT_AUDIO_PIPELINE_FRAME_MS
#define CLIENT_AUDIO_PIPELINE_FLAGS_MINIMAL
Minimal flags for testing (only codec, no processing)
#define CLIENT_AUDIO_PIPELINE_FLAGS_ALL
Default flags with all processing enabled.
#define CLIENT_AUDIO_PIPELINE_SAMPLE_RATE
void copy_buffer_with_gain(const float *src, float *dst, int count, float gain)
Copy buffer with gain scaling.
Definition mixer.c:1128
void noise_gate_process_buffer(noise_gate_t *gate, float *buffer, int num_samples)
Process a buffer of samples through noise gate.
Definition mixer.c:892
void soft_clip_buffer(float *buffer, int num_samples, float threshold, float steepness)
Apply soft clipping to a buffer.
Definition mixer.c:1032
void noise_gate_init(noise_gate_t *gate, float sample_rate)
Initialize a noise gate.
Definition mixer.c:838
float soft_clip(float sample, float threshold, float steepness)
Apply soft clipping to a sample.
Definition mixer.c:1019
void compressor_set_params(compressor_t *comp, float threshold_dB, float ratio, float attack_ms, float release_ms, float makeup_dB)
Set compressor parameters.
Definition mixer.c:53
void compressor_init(compressor_t *comp, float sample_rate)
Initialize a compressor.
Definition mixer.c:42
void highpass_filter_init(highpass_filter_t *filter, float cutoff_hz, float sample_rate)
Initialize a high-pass filter.
Definition mixer.c:920
float smoothstep(float t)
Compute smoothstep interpolation.
Definition mixer.c:1046
void lowpass_filter_init(lowpass_filter_t *filter, float cutoff_hz, float sample_rate)
Initialize a low-pass filter.
Definition mixer.c:970
void lowpass_filter_process_buffer(lowpass_filter_t *filter, float *buffer, int num_samples)
Process a buffer of samples through low-pass filter.
Definition mixer.c:1005
void highpass_filter_process_buffer(highpass_filter_t *filter, float *buffer, int num_samples)
Process a buffer of samples through high-pass filter.
Definition mixer.c:956
void noise_gate_set_params(noise_gate_t *gate, float threshold, float attack_ms, float release_ms, float hysteresis)
Set noise gate parameters.
Definition mixer.c:852
float compressor_process_sample(compressor_t *comp, float sidechain)
Process a single sample through compressor.
Definition mixer.c:87
@ OPUS_APPLICATION_VOIP
Voice over IP (optimized for speech)
Definition opus_codec.h:77
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_CALLOC(count, size, cast)
Definition common.h:218
unsigned char uint8_t
Definition common.h:56
#define log_warn(...)
Log a WARN message.
#define log_error(...)
Log an ERROR message.
#define log_info(...)
Log an INFO message.
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
const char * platform_getenv(const char *name)
Get an environment variable value.
📝 Logging API with multiple log levels and terminal output control
🔢 Mathematical Utility Functions
Multi-Source Audio Mixing and Processing System.
C++ wrapper for WebRTC AEC3 (opaque to C code)
~WebRTCAec3Wrapper()=default
webrtc::EchoCanceller3Config config
WebRTCAec3Wrapper()=default
std::unique_ptr< webrtc::EchoControl > aec3
Pipeline configuration parameters.
client_audio_pipeline_flags_t flags
Component enable/disable flags.
Client audio pipeline state.
client_audio_pipeline_config_t config
client_audio_pipeline_flags_t flags
WAV file writer context.
Definition wav_writer.h:23
wav_writer_t * wav_writer_open(const char *filepath, int sample_rate, int channels)
Open WAV file for writing.
Definition wav_writer.c:39
int wav_writer_write(wav_writer_t *writer, const float *samples, int num_samples)
Write audio samples to WAV file.
Definition wav_writer.c:85
void wav_writer_close(wav_writer_t *writer)
Close WAV file and finalize header.
Definition wav_writer.c:99
Simple WAV file writer for audio debugging.