ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
client_pipeline.cpp File Reference

Unified client-side audio processing pipeline with WebRTC AEC3. More...

Go to the source code of this file.

Data Structures

struct  WebRTCAec3Wrapper
 C++ wrapper for WebRTC AEC3 (opaque to C code). More...

Macros

#define WEBRTC_APM_DEBUG_DUMP   0
#define WEBRTC_MODULE_AUDIO_PROCESSING   1
#define __STDC_NO_ATOMICS__   1

Functions

client_audio_pipeline_config_t client_audio_pipeline_default_config (void)
 Audio-pipeline setup for channel conversion, Opus encoding, echo handling, and preprocessing. The channel counts, sample rate, frame duration, bitrate, and processor settings are copied into the pipeline during initialization; the config itself can then be stack-owned.
client_audio_pipeline_config_t client_audio_pipeline_config_for_source (bool has_media_audio)
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_destroy (client_audio_pipeline_t *pipeline)
 Destroy a client audio pipeline.
void client_audio_pipeline_set_flags (client_audio_pipeline_t *pipeline, client_audio_pipeline_flags_t flags)
 Set component enable flags.
client_audio_pipeline_flags_t client_audio_pipeline_get_flags (client_audio_pipeline_t *pipeline)
 Get current 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.
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.
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.
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.
int client_audio_pipeline_jitter_margin (client_audio_pipeline_t *pipeline)
 Get jitter buffer margin (buffered time in ms).
void client_audio_pipeline_reset (client_audio_pipeline_t *pipeline)
 Reset pipeline state.

Detailed Description

Unified client-side audio processing pipeline with WebRTC AEC3.

Implements production-grade echo cancellation using WebRTC AEC3 (Acoustic Echo Cancellation v3) with automatic network delay estimation, adaptive filtering, and residual echo suppression.

Uses WebRTC directly via C++ API - no wrapper layer.

Definition in file client_pipeline.cpp.

Macro Definition Documentation

◆ __STDC_NO_ATOMICS__

#define __STDC_NO_ATOMICS__   1

Definition at line 77 of file client_pipeline.cpp.

◆ WEBRTC_APM_DEBUG_DUMP

#define WEBRTC_APM_DEBUG_DUMP   0

Definition at line 22 of file client_pipeline.cpp.

◆ WEBRTC_MODULE_AUDIO_PROCESSING

#define WEBRTC_MODULE_AUDIO_PROCESSING   1

Definition at line 23 of file client_pipeline.cpp.

Function Documentation

◆ client_audio_pipeline_capture()

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.

Encode already-processed audio to Opus.

In full-duplex mode, AEC3 and DSP processing are done in process_duplex(). This function just does Opus encoding.

Definition at line 431 of file client_pipeline.cpp.

432 {
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.
440 const float *pcm = input;
441 const int frames = num_samples / pipeline->config.channels;
442 if (pipeline->config.encoder_channels != pipeline->config.channels) {
443 if (frames > (int)(sizeof(converted) / sizeof(converted[0])) / pipeline->config.encoder_channels)
444 return -1;
445 if (audio_convert_channels(input, (size_t)frames, pipeline->config.channels, converted,
447 return -1;
448 pcm = converted;
449 }
450 int opus_len = opus_encode_float(pipeline->encoder, pcm, frames, opus_out, max_opus_len);
451
452 if (opus_len < 0) {
453 log_error_every(US_PER_SEC_INT, "Opus encoding failed: %d", opus_len);
454 return -1;
455 }
456
457 return opus_len;
458}
asciichat_error_t audio_convert_channels(const float *input, size_t frames, unsigned input_channels, float *output, unsigned output_channels)
#define AUDIO_OPUS_MAX_DECODE_SAMPLES
#define AUDIO_CHANNELS
Definition audio/limits.h:5
@ ASCIICHAT_OK
Definition error_codes.h:46
#define US_PER_SEC_INT
Definition time.h:173
#define log_error_every(interval_us, fmt,...)
Rate-limited ERROR logging.
Definition log/log.h:758
int encoder_channels
Transmitted channels, independent of playback.
int channels
PCM channels (1 or 2).
client_audio_pipeline_config_t config

References ASCIICHAT_OK, AUDIO_CHANNELS, audio_convert_channels(), AUDIO_OPUS_MAX_DECODE_SAMPLES, client_audio_pipeline_config_t::channels, client_audio_pipeline_t::config, client_audio_pipeline_t::encoder, client_audio_pipeline_config_t::encoder_channels, client_audio_pipeline_t::frame_size, log_error_every, and US_PER_SEC_INT.

◆ client_audio_pipeline_config_for_source()

client_audio_pipeline_config_t client_audio_pipeline_config_for_source ( bool has_media_audio)

Select the codec profile using current capture options and actual media audio availability.

Definition at line 132 of file client_pipeline.cpp.

132 {
134 config.channels = AUDIO_CHANNELS;
135 const options_t *options = options_get();
136 if (options) {
137 config.processor.spectral_enabled = options->audio_spectral && !options->test_audio;
138 config.processor.spectral.fft_size = options->audio_fft_size;
144 bool media = options->test_audio || (has_media_audio && options->audio_capture_source != AUDIO_CAPTURE_SOURCE_MIC &&
146 bool stereo_microphone = options->microphone_channels == 2;
147 config.opus_application = media || stereo_microphone ? OPUS_APPLICATION_AUDIO : OPUS_APPLICATION_VOIP;
148 config.opus_bitrate = media || stereo_microphone ? 128000 : 32000;
149 }
150 return config;
151}
client_audio_pipeline_config_t client_audio_pipeline_default_config(void)
Audio-pipeline setup for channel conversion, Opus encoding, echo handling, and preprocessing....
@ OPUS_APPLICATION_AUDIO
General audio (optimized for music).
Definition opus.h:86
@ OPUS_APPLICATION_VOIP
Voice over IP (optimized for speech).
Definition opus.h:85
struct options_state options_t
Immutable, RCU-published snapshot of the application's runtime configuration.
const options_t * options_get(void)
Get pointer to current options struct (lock-free, thread-safe).
Definition rcu.c:511
@ AUDIO_CAPTURE_SOURCE_REMOTE
Definition options.h:295
@ AUDIO_CAPTURE_SOURCE_MIC
Definition options.h:292
spectral_config_t spectral
Definition processor.h:29
Pipeline configuration parameters.
audio_processor_config_t processor
bool audio_spectral_agc
Definition options.h:1113
audio_capture_source_t audio_capture_source
Local capture/media selection policy.
Definition options.h:1122
bool audio_spectral_gate
Definition options.h:1116
bool audio_adaptive_eq
Definition options.h:1118
bool test_audio
Replace microphone capture with a repeating audible test signal.
Definition options.h:1112
int microphone_channels
Selected microphone channels: 1 (speech) or 2 (stereo).
Definition options.h:1126
bool audio_pitch_correct
Definition options.h:1111
bool audio_multiband
Definition options.h:1117
int audio_fft_size
Definition options.h:1119
bool audio_spectral
Definition options.h:1114

References spectral_config_t::adaptive_eq, options_state::audio_adaptive_eq, options_state::audio_capture_source, AUDIO_CAPTURE_SOURCE_MIC, AUDIO_CAPTURE_SOURCE_REMOTE, AUDIO_CHANNELS, options_state::audio_fft_size, options_state::audio_multiband, options_state::audio_pitch_correct, options_state::audio_spectral, options_state::audio_spectral_agc, options_state::audio_spectral_gate, spectral_config_t::auto_gain, client_audio_pipeline_config_t::channels, client_audio_pipeline_default_config(), spectral_config_t::compressor, spectral_config_t::fft_size, options_state::microphone_channels, spectral_config_t::noise_gate, options_get(), client_audio_pipeline_config_t::opus_application, OPUS_APPLICATION_AUDIO, OPUS_APPLICATION_VOIP, client_audio_pipeline_config_t::opus_bitrate, spectral_config_t::pitch_correct, client_audio_pipeline_config_t::processor, audio_processor_config_t::spectral, audio_processor_config_t::spectral_enabled, and options_state::test_audio.

Referenced by audio_connection_init(), session_audio_start_duplex(), and session_participant_start_audio().

◆ client_audio_pipeline_create()

client_audio_pipeline_t * client_audio_pipeline_create ( const client_audio_pipeline_config_t * config)

Create and initialize a client audio pipeline.

Create a new client audio pipeline.

This function:

  • Allocates the pipeline structure
  • Initializes Opus encoder/decoder
  • Sets up WebRTC AEC3 echo cancellation
  • Configures all audio processing parameters

Definition at line 166 of file client_pipeline.cpp.

166 {
168 if (!p) {
169 log_error("Failed to allocate client audio pipeline");
170 return NULL;
171 }
172
173 // Use default config if none provided
174 if (config) {
175 p->config = *config;
176 } else {
178 }
179
180 if (p->config.channels == 0)
182 if (p->config.encoder_channels == 0)
184 if ((p->config.channels != 1 && p->config.channels != 2) ||
185 (p->config.encoder_channels != 1 && p->config.encoder_channels != 2)) {
186 SAFE_FREE(p);
187 return NULL;
188 }
189 p->flags = p->config.flags;
191
192 // No mutex needed - full-duplex means single callback thread handles all AEC3
193
194 // Initialize Opus encoder/decoder first (no exceptions)
195 int opus_error = 0;
196 p->encoder =
197 opus_encoder_create(p->config.sample_rate, p->config.encoder_channels, p->config.opus_application, &opus_error);
198 if (!p->encoder || opus_error != OPUS_OK) {
199 log_error("Failed to create Opus encoder: %d", opus_error);
200 goto error;
201 }
203 goto error;
204
205 // Create Opus decoder
206 p->decoder = opus_decoder_create(p->config.sample_rate, p->config.channels, &opus_error);
207 if (!p->decoder || opus_error != OPUS_OK) {
208 log_error("Failed to create Opus decoder: %d", opus_error);
209 goto error;
210 }
211
212 // Create WebRTC AEC3 Echo Cancellation
213 // AEC3 provides production-grade acoustic echo cancellation with:
214 // - Automatic network delay estimation (0-500ms)
215 // - Adaptive filtering to actual echo path
216 // - Residual echo suppression via spectral subtraction
217 // - Jitter buffer handling via side information
218 if (p->flags.echo_cancel) {
219 // Configure AEC3 for better low-frequency (bass) echo cancellation
220 webrtc::EchoCanceller3Config aec3_config;
221
222 // Increase filter length for bass frequencies (default 13 blocks = ~17ms)
223 // Bass at 80Hz has 12.5ms period, so we need at least 50+ blocks (~67ms)
224 // to properly model the echo path for low frequencies
225 aec3_config.filter.main.length_blocks = 50; // ~67ms (was 13)
226 aec3_config.filter.shadow.length_blocks = 50; // ~67ms (was 13)
227 aec3_config.filter.main_initial.length_blocks = 25; // ~33ms (was 12)
228 aec3_config.filter.shadow_initial.length_blocks = 25; // ~33ms (was 12)
229
230 // More aggressive low-frequency suppression thresholds
231 // Lower values = more aggressive echo suppression
232 aec3_config.echo_audibility.audibility_threshold_lf = 5; // (was 10)
233
234 // Create AEC3 using the factory
235 auto factory = webrtc::EchoCanceller3Factory(aec3_config);
236
237 std::unique_ptr<webrtc::EchoControl> echo_control = factory.Create(static_cast<int>(p->config.sample_rate), // 48kHz
238 p->config.channels, // speaker channels
239 p->config.channels // microphone channels
240 );
241
242 if (!echo_control) {
243 log_warn("Failed to create WebRTC AEC3 instance - echo cancellation unavailable");
244 p->echo_canceller = NULL;
245 } else {
246 // Successfully created AEC3 - wrap in our C++ wrapper for C compatibility
247 auto wrapper = new WebRTCAec3Wrapper();
248 wrapper->aec3 = std::move(echo_control);
249 wrapper->config = aec3_config;
250 p->echo_canceller = wrapper;
251
252 log_info("✓ WebRTC AEC3 initialized (67ms filter for bass, adaptive delay)");
253
254 // Create persistent AudioBuffer instances for AEC3
256 new webrtc::AudioBuffer(48000, p->config.channels, 48000, p->config.channels, 48000, p->config.channels);
258 new webrtc::AudioBuffer(48000, p->config.channels, 48000, p->config.channels, 48000, p->config.channels);
259
260 auto *render_buf = static_cast<webrtc::AudioBuffer *>(p->aec3_render_buffer);
261 auto *capture_buf = static_cast<webrtc::AudioBuffer *>(p->aec3_capture_buffer);
262
263 // Zero-initialize channel data
264 float *const *render_ch = render_buf->channels();
265 float *const *capture_ch = capture_buf->channels();
266 for (int channel = 0; channel < p->config.channels; channel++) {
267 if (render_ch && render_ch[channel])
268 memset(render_ch[channel], 0, 480 * sizeof(float));
269 if (capture_ch && capture_ch[channel])
270 memset(capture_ch[channel], 0, 480 * sizeof(float));
271 }
272
273 // Prime filterbank state with dummy processing cycle
274 render_buf->SplitIntoFrequencyBands();
275 render_buf->MergeFrequencyBands();
276 capture_buf->SplitIntoFrequencyBands();
277 capture_buf->MergeFrequencyBands();
278
279 log_debug(" - AudioBuffer filterbank state initialized");
280
281 // Warm up AEC3 with 10 silent frames to initialize internal state
282 for (int warmup = 0; warmup < 10; warmup++) {
283 for (int channel = 0; channel < p->config.channels; channel++) {
284 memset(render_ch[channel], 0, 480 * sizeof(float));
285 memset(capture_ch[channel], 0, 480 * sizeof(float));
286 }
287
288 render_buf->SplitIntoFrequencyBands();
289 wrapper->aec3->AnalyzeRender(render_buf);
290 render_buf->MergeFrequencyBands();
291
292 wrapper->aec3->AnalyzeCapture(capture_buf);
293 capture_buf->SplitIntoFrequencyBands();
294 wrapper->aec3->SetAudioBufferDelay(0);
295 wrapper->aec3->ProcessCapture(capture_buf, false);
296 capture_buf->MergeFrequencyBands();
297 }
298 log_debug(" - AEC3 warmed up with 10 silent frames");
299 log_debug(" - Persistent AudioBuffer instances created");
300 }
301 }
302
303 // Initialize debug WAV writers for AEC3 analysis (if echo_cancel enabled)
304 p->debug_wav_aec3_in = NULL;
305 p->debug_wav_aec3_out = NULL;
306 if (p->flags.echo_cancel && wav_dump_enabled()) {
307 // Record microphone diagnostics only when WAV dumping was requested.
308 char tmp[PLATFORM_MAX_PATH_LENGTH];
309 if (platform_get_temp_dir(tmp, sizeof(tmp))) {
310 char path[PLATFORM_MAX_PATH_LENGTH];
311 safe_snprintf(path, sizeof(path), "%s/aec3_input.wav", tmp);
312 p->debug_wav_aec3_in = wav_writer_open(path, 48000, p->config.channels);
313 safe_snprintf(path, sizeof(path), "%s/aec3_output.wav", tmp);
315 }
316 if (p->debug_wav_aec3_in) {
317 log_info("Debug: Recording AEC3 input WAV to temp dir");
318 }
319 if (p->debug_wav_aec3_out) {
320 log_info("Debug: Recording AEC3 output WAV to temp dir");
321 }
322
323 log_info("✓ AEC3 echo cancellation enabled (full-duplex mode, no ring buffer delay)");
324 }
325
330 p->flags.lowpass};
332 goto error;
333 p->initialized = true;
334
335 // Initialize startup fade-in to prevent initial microphone click
336 // 200ms at 48kHz = 9600 samples - gradual ramp from silence to full volume
337 // Longer fade-in (200ms vs 50ms) gives much smoother transition without audible pop
338 p->capture_fadein_remaining = (p->config.sample_rate * 200) / 1000 * p->config.channels; // 200ms worth of samples
339 log_debug("✓ Capture fade-in: %d samples (200ms)", p->capture_fadein_remaining);
340
341 log_info("Audio pipeline created: %dHz, %llums frames, %dkbps Opus", p->config.sample_rate,
342 (unsigned long long)p->config.frame_duration_ms, p->config.opus_bitrate / 1000);
343
344 (void)NAMED_REGISTER_CLIENT_AUDIO_PIPELINE(p, "audio_pipeline", NULL);
345
346 return p;
347
348error:
350 return NULL;
351}
void client_audio_pipeline_destroy(client_audio_pipeline_t *pipeline)
Destroy a client audio pipeline.
asciichat_error_t audio_opus_configure_encoder(OpusEncoder *encoder, int bitrate)
Definition opus.c:21
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:95
#define NAMED_REGISTER_CLIENT_AUDIO_PIPELINE(pipeline, name, parent_ptr)
Register a client audio pipeline with automatic format specifier.
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
asciichat_error_t audio_processor_create(const audio_processor_config_t *c, audio_processor_t **out)
Definition processor.c:71
C++ wrapper for WebRTC AEC3 (opaque to C code).
int channels
Interleaved PCM channels; zero retains the mono API default.
Definition processor.h:25
audio_processor_flags_t flags
Definition processor.h:27
client_audio_pipeline_flags_t flags
Client audio pipeline state.
audio_processor_t * processor
client_audio_pipeline_flags_t flags
bool wav_dump_enabled(void)
Check if audio dumping is enabled via environment.
Definition wav_writer.c:142
wav_writer_t * wav_writer_open(const char *filepath, int sample_rate, int channels)
Open WAV file for writing.
Definition wav_writer.c:52

References client_audio_pipeline_t::aec3_capture_buffer, client_audio_pipeline_t::aec3_render_buffer, client_audio_pipeline_flags_t::agc, ASCIICHAT_OK, AUDIO_CHANNELS, audio_opus_configure_encoder(), audio_processor_create(), client_audio_pipeline_t::capture_fadein_remaining, audio_processor_config_t::channels, client_audio_pipeline_config_t::channels, client_audio_pipeline_default_config(), client_audio_pipeline_destroy(), client_audio_pipeline_flags_t::compressor, client_audio_pipeline_t::config, client_audio_pipeline_t::debug_wav_aec3_in, client_audio_pipeline_t::debug_wav_aec3_out, client_audio_pipeline_t::decoder, client_audio_pipeline_flags_t::echo_cancel, client_audio_pipeline_t::echo_canceller, client_audio_pipeline_t::encoder, client_audio_pipeline_config_t::encoder_channels, audio_processor_config_t::flags, client_audio_pipeline_config_t::flags, client_audio_pipeline_t::flags, client_audio_pipeline_config_t::frame_duration_ms, client_audio_pipeline_t::frame_size, client_audio_pipeline_flags_t::highpass, client_audio_pipeline_t::initialized, log_debug, log_error, log_info, log_warn, client_audio_pipeline_flags_t::lowpass, NAMED_REGISTER_CLIENT_AUDIO_PIPELINE, client_audio_pipeline_flags_t::noise_gate, client_audio_pipeline_config_t::opus_application, client_audio_pipeline_config_t::opus_bitrate, PLATFORM_MAX_PATH_LENGTH, client_audio_pipeline_config_t::processor, client_audio_pipeline_t::processor, SAFE_CALLOC, SAFE_FREE, safe_snprintf(), audio_processor_config_t::sample_rate, client_audio_pipeline_config_t::sample_rate, spectral_config_t::sample_rate, audio_processor_config_t::spectral, wav_dump_enabled(), and wav_writer_open().

Referenced by audio_connection_init(), and session_audio_start_duplex().

◆ client_audio_pipeline_default_config()

client_audio_pipeline_config_t client_audio_pipeline_default_config ( void )

Audio-pipeline setup for channel conversion, Opus encoding, echo handling, and preprocessing. The channel counts, sample rate, frame duration, bitrate, and processor settings are copied into the pipeline during initialization; the config itself can then be stack-owned.

Get default configuration

Returns
Configuration with sensible defaults for voice chat

Definition at line 114 of file client_pipeline.cpp.

114 {
116 .channels = AUDIO_CHANNELS,
117 .encoder_channels = AUDIO_CHANNELS,
118 .sample_rate = AUDIO_SAMPLE_RATE,
119 .frame_duration_ms = CLIENT_AUDIO_PIPELINE_FRAME_MS,
120 .opus_bitrate = 32000,
121 .opus_application = OPUS_APPLICATION_VOIP,
122
123 .echo_filter_ms = 250,
124
126 .jitter_margin_ms = 60,
127
129 };
130}
#define CLIENT_AUDIO_PIPELINE_FRAME_MS
#define AUDIO_SAMPLE_RATE
Definition audio/limits.h:4
#define CLIENT_AUDIO_PIPELINE_FLAGS_ALL
Component enable/disable flags.
audio_processor_config_t audio_processor_default_config(int sample_rate)
Definition processor.c:29

References AUDIO_CHANNELS, audio_processor_default_config(), AUDIO_SAMPLE_RATE, CLIENT_AUDIO_PIPELINE_FLAGS_ALL, CLIENT_AUDIO_PIPELINE_FRAME_MS, audio_processor_config_t::flags, and OPUS_APPLICATION_VOIP.

Referenced by client_audio_pipeline_config_for_source(), and client_audio_pipeline_create().

◆ client_audio_pipeline_destroy()

void client_audio_pipeline_destroy ( client_audio_pipeline_t * pipeline)

Destroy a client audio pipeline.

Parameters
pipelinePipeline to destroy (can be NULL)

Frees all resources including SpeexDSP states, Opus codec, and all work buffers.

Definition at line 353 of file client_pipeline.cpp.

353 {
354 if (!pipeline)
355 return;
356
358 pipeline->processor = NULL;
359
360 // Clean up WebRTC AEC3 AudioBuffer instances
361 if (pipeline->aec3_render_buffer) {
362 delete static_cast<webrtc::AudioBuffer *>(pipeline->aec3_render_buffer);
363 pipeline->aec3_render_buffer = NULL;
364 }
365 if (pipeline->aec3_capture_buffer) {
366 delete static_cast<webrtc::AudioBuffer *>(pipeline->aec3_capture_buffer);
367 pipeline->aec3_capture_buffer = NULL;
368 }
369
370 // Clean up WebRTC AEC3
371 if (pipeline->echo_canceller) {
372 delete static_cast<WebRTCAec3Wrapper *>(pipeline->echo_canceller);
373 pipeline->echo_canceller = NULL;
374 }
375
376 // Clean up Opus
377 if (pipeline->encoder) {
378 opus_encoder_destroy(pipeline->encoder);
379 pipeline->encoder = NULL;
380 }
381 if (pipeline->decoder) {
382 opus_decoder_destroy(pipeline->decoder);
383 pipeline->decoder = NULL;
384 }
385
386 // Clean up debug WAV writers
387 if (pipeline->debug_wav_aec3_in) {
389 pipeline->debug_wav_aec3_in = NULL;
390 }
391 if (pipeline->debug_wav_aec3_out) {
393 pipeline->debug_wav_aec3_out = NULL;
394 }
395
396 NAMED_UNREGISTER(pipeline);
397
398 SAFE_FREE(pipeline);
399}
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
void audio_processor_destroy(audio_processor_t *p)
Definition processor.c:229
File handle and sample counters needed to finalize a WAV recording.
Definition wav_writer.h:28
void wav_writer_close(wav_writer_t *writer)
Close WAV file and finalize header.
Definition wav_writer.c:116

References client_audio_pipeline_t::aec3_capture_buffer, client_audio_pipeline_t::aec3_render_buffer, audio_processor_destroy(), client_audio_pipeline_t::debug_wav_aec3_in, client_audio_pipeline_t::debug_wav_aec3_out, client_audio_pipeline_t::decoder, client_audio_pipeline_t::echo_canceller, client_audio_pipeline_t::encoder, NAMED_UNREGISTER, client_audio_pipeline_t::processor, SAFE_FREE, and wav_writer_close().

Referenced by client_audio_pipeline_create(), session_audio_destroy(), and session_audio_set_capture_source().

◆ client_audio_pipeline_get_flags()

client_audio_pipeline_flags_t client_audio_pipeline_get_flags ( client_audio_pipeline_t * pipeline)

Get current component enable flags.

Parameters
pipelinePipeline instance
Returns
Current flags

Definition at line 414 of file client_pipeline.cpp.

414 {
415 if (!pipeline)
417 // No mutex needed - flags are only written from main thread during setup
418 return pipeline->flags;
419}
#define CLIENT_AUDIO_PIPELINE_FLAGS_MINIMAL
Minimal flags for testing (only codec, no processing).

References CLIENT_AUDIO_PIPELINE_FLAGS_MINIMAL, and client_audio_pipeline_t::flags.

◆ client_audio_pipeline_get_playback_frame()

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.

Get a processed playback frame (currently just returns decoded frame)

Definition at line 491 of file client_pipeline.cpp.

491 {
492 if (!pipeline || !output) {
493 return -1;
494 }
495
496 // No mutex needed - this is a placeholder
497 memset(output, 0, num_samples * sizeof(float));
498 return num_samples;
499}

◆ client_audio_pipeline_jitter_margin()

int client_audio_pipeline_jitter_margin ( client_audio_pipeline_t * pipeline)

Get jitter buffer margin (buffered time in ms).

Get jitter buffer margin

Definition at line 648 of file client_pipeline.cpp.

648 {
649 if (!pipeline)
650 return 0;
651 return pipeline->config.jitter_margin_ms;
652}

References client_audio_pipeline_t::config, and client_audio_pipeline_config_t::jitter_margin_ms.

◆ client_audio_pipeline_playback()

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.

Process network playback (decode and register with echo canceller as reference)

Definition at line 463 of file client_pipeline.cpp.

464 {
465 if (!pipeline || !output || num_samples <= 0 || opus_len < 0 || (!opus_in && opus_len != 0)) {
466 return -1;
467 }
468
469 // No mutex needed - Opus decoder is only used from this thread
470
471 // Decode Opus
472 int decoded_samples =
473 opus_decode_float(pipeline->decoder, opus_in, opus_len, output, num_samples / pipeline->config.channels, 0);
474
475 if (decoded_samples < 0) {
476 log_error_every(US_PER_SEC_INT, "Opus decoding failed: %d", decoded_samples);
477 return -1;
478 }
479
480 // Received streams can contain quiet media; microphone gating belongs at capture.
481
482 // NOTE: Render signal is queued to AEC3 in output_callback() when audio plays,
483 // not here. The capture thread drains the queue and processes AEC3.
484
485 return decoded_samples * pipeline->config.channels;
486}

References client_audio_pipeline_config_t::channels, client_audio_pipeline_t::config, client_audio_pipeline_t::decoder, log_error_every, and US_PER_SEC_INT.

Referenced by audio_decode_opus().

◆ client_audio_pipeline_process_duplex()

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.

Process AEC3 inline in full-duplex callback (REAL-TIME SAFE).

This is the PROFESSIONAL approach to AEC3 timing:

  • Called from a single PortAudio full-duplex callback
  • render_samples = what is being played to speakers RIGHT NOW
  • capture_samples = what microphone captured RIGHT NOW
  • Perfect synchronization - no timing mismatch possible

This function does ALL AEC3 processing inline:

  1. AnalyzeRender on render samples (speaker output)
  2. AnalyzeCapture + ProcessCapture on capture samples
  3. Apply filters, noise gate, compressor

Returns processed capture samples in processed_output. Opus encoding is done separately by the encoding thread.

Definition at line 518 of file client_pipeline.cpp.

520 {
521 if (!pipeline || !processed_output || capture_count < 0 || render_count < 0 ||
522 capture_count % pipeline->config.channels || render_count % pipeline->config.channels)
523 return;
524
525 // Copy capture samples to output buffer for processing
526 if (capture_samples && capture_count > 0) {
527 memcpy(processed_output, capture_samples, capture_count * sizeof(float));
528 } else {
529 memset(processed_output, 0, capture_count * sizeof(float));
530 return;
531 }
532
533 // Check for AEC3 bypass
534 static int bypass_aec3 = -1;
535 if (bypass_aec3 == -1) {
536 const char *env = platform_getenv("BYPASS_AEC3");
537 bypass_aec3 = (env && (strcmp(env, "1") == 0 || strcmp(env, "true") == 0)) ? 1 : 0;
538 if (bypass_aec3) {
539 log_warn("AEC3 BYPASSED (full-duplex mode) via BYPASS_AEC3=1");
540 }
541 }
542
543 // Debug WAV recording
544 if (pipeline->debug_wav_aec3_in) {
545 wav_writer_write((wav_writer_t *)pipeline->debug_wav_aec3_in, capture_samples, capture_count);
546 }
547
548 // Apply startup fade-in using smoothstep curve
549 if (pipeline->capture_fadein_remaining > 0) {
550 const int total_fadein_samples = (pipeline->config.sample_rate * 200) / 1000 * pipeline->config.channels;
551 for (int i = 0; i < capture_count && pipeline->capture_fadein_remaining > 0; i += pipeline->config.channels) {
552 float progress = 1.0f - ((float)pipeline->capture_fadein_remaining / (float)total_fadein_samples);
553 float gain = smoothstep(progress);
554 for (int channel = 0; channel < pipeline->config.channels; channel++)
555 processed_output[i + channel] *= gain;
556 pipeline->capture_fadein_remaining -= pipeline->config.channels;
557 }
558 }
559
560 // WebRTC AEC3 processing - INLINE, no ring buffer, no mutex
561 if (!bypass_aec3 && pipeline->flags.echo_cancel && pipeline->echo_canceller) {
562 auto wrapper = static_cast<WebRTCAec3Wrapper *>(pipeline->echo_canceller);
563 if (wrapper && wrapper->aec3) {
564 const int channels = pipeline->config.channels;
565 const int webrtc_frame_size = 480 * channels; // 10ms of interleaved PCM
566
567 auto *render_buf = static_cast<webrtc::AudioBuffer *>(pipeline->aec3_render_buffer);
568 auto *capture_buf = static_cast<webrtc::AudioBuffer *>(pipeline->aec3_capture_buffer);
569
570 if (render_buf && capture_buf) {
571 float *const *render_channels = render_buf->channels();
572 float *const *capture_channels = capture_buf->channels();
573
574 if (render_channels && render_channels[0] && capture_channels && capture_channels[0]) {
575 // Verify render_samples is valid before accessing
576 if (!render_samples && render_count > 0) {
577 log_warn_every(1000000, "AEC3: render_samples is NULL but render_count=%d", render_count);
578 return;
579 }
580
581 // Process in 10ms chunks (AEC3 requirement)
582 int render_offset = 0;
583 int capture_offset = 0;
584
585 // Process only complete 480-sample frames. The worker thread guarantees
586 // render_count == capture_count and both are multiples of 480, so no
587 // partial frames should occur. If they do (defensive), the tail samples
588 // pass through unprocessed rather than corrupting AEC3 with padded data.
589 while (render_offset + webrtc_frame_size <= render_count &&
590 capture_offset + webrtc_frame_size <= capture_count) {
591
592 // STEP 1: Feed render signal (what's playing to speakers)
593 for (int channel = 0; channel < channels; channel++)
594 for (int frame = 0; frame < 480; frame++)
595 render_channels[channel][frame] = render_samples[render_offset + frame * channels + channel] * 32768.0f;
596 render_buf->SplitIntoFrequencyBands();
597 wrapper->aec3->AnalyzeRender(render_buf);
598 render_buf->MergeFrequencyBands();
599 g_render_frames_fed.fetch_add(1, std::memory_order_relaxed);
600 render_offset += webrtc_frame_size;
601
602 // STEP 2: Process capture (microphone input)
603 for (int channel = 0; channel < channels; channel++)
604 for (int frame = 0; frame < 480; frame++)
605 capture_channels[channel][frame] =
606 processed_output[capture_offset + frame * channels + channel] * 32768.0f;
607 wrapper->aec3->AnalyzeCapture(capture_buf);
608 capture_buf->SplitIntoFrequencyBands();
609 wrapper->aec3->ProcessCapture(capture_buf, false);
610 capture_buf->MergeFrequencyBands();
611
612 // Scale back to float range and soft clip
613 for (int j = 0; j < webrtc_frame_size; j++) {
614 float sample = capture_channels[j % channels][j / channels] / 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_debug("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 capture_offset += webrtc_frame_size;
628 }
629 }
630 }
631 }
632 }
633
634 // Debug WAV recording (after AEC3)
635 if (pipeline->debug_wav_aec3_out) {
636 wav_writer_write((wav_writer_t *)pipeline->debug_wav_aec3_out, processed_output, capture_count);
637 }
638
639 asciichat_error_t result =
640 audio_processor_process(pipeline->processor, processed_output, processed_output, (size_t)capture_count);
641 if (result != ASCIICHAT_OK)
642 log_error("Audio enhancement failed: %d", result);
643}
void audio_analysis_set_aec3_metrics(double echo_return_loss, double echo_return_loss_enhancement, uint64_t delay_ns)
Set AEC3 echo cancellation metrics.
Definition analysis.c:517
float soft_clip(float sample, float threshold, float steepness)
Apply soft clipping to a sample.
Definition effects.c:286
float smoothstep(float t)
Compute smoothstep interpolation.
Definition effects.c:313
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
const char * platform_getenv(const char *name)
Get an environment variable value.
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
asciichat_error_t audio_processor_process(audio_processor_t *p, const float *input, float *output, size_t count)
Definition processor.c:125
int wav_writer_write(wav_writer_t *writer, const float *samples, int num_samples)
Write audio samples to WAV file.
Definition wav_writer.c:98

References client_audio_pipeline_t::aec3_capture_buffer, client_audio_pipeline_t::aec3_render_buffer, ASCIICHAT_OK, audio_analysis_set_aec3_metrics(), audio_processor_process(), client_audio_pipeline_t::capture_fadein_remaining, client_audio_pipeline_config_t::channels, client_audio_pipeline_t::config, client_audio_pipeline_t::debug_wav_aec3_in, client_audio_pipeline_t::debug_wav_aec3_out, client_audio_pipeline_flags_t::echo_cancel, client_audio_pipeline_t::echo_canceller, client_audio_pipeline_t::flags, log_debug, log_error, log_warn, log_warn_every, platform_getenv(), client_audio_pipeline_t::processor, client_audio_pipeline_config_t::sample_rate, smoothstep(), soft_clip(), and wav_writer_write().

◆ client_audio_pipeline_reset()

void client_audio_pipeline_reset ( client_audio_pipeline_t * pipeline)

Reset pipeline state.

Reset pipeline state

Definition at line 657 of file client_pipeline.cpp.

657 {
658 if (!pipeline)
659 return;
660
662
663 // Reset global counters
664 g_render_frames_fed.store(0, std::memory_order_relaxed);
665 g_max_render_rms.store(0.0f, std::memory_order_relaxed);
666
667 log_info("Pipeline state reset");
668}
void audio_processor_reset(audio_processor_t *p)
Definition processor.c:51

References audio_processor_reset(), log_info, and client_audio_pipeline_t::processor.

◆ client_audio_pipeline_set_flags()

void client_audio_pipeline_set_flags ( client_audio_pipeline_t * pipeline,
client_audio_pipeline_flags_t flags )

Set component enable flags.

Parameters
pipelinePipeline instance
flagsNew flags to set

Thread-safe. Changes take effect on next capture/playback call.

Definition at line 405 of file client_pipeline.cpp.

405 {
406 if (!pipeline)
407 return;
408 // No mutex needed - flags are only read by capture thread
409 pipeline->flags = flags;
411 {flags.agc, flags.compressor, flags.noise_gate, flags.highpass, flags.lowpass});
412}
void audio_processor_set_flags(audio_processor_t *p, audio_processor_flags_t flags)
Definition processor.c:120

References audio_processor_set_flags(), client_audio_pipeline_t::flags, and client_audio_pipeline_t::processor.