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.h File Reference

Unified client-side audio processing pipeline. More...

Go to the source code of this file.

Data Structures

struct  client_audio_pipeline_flags_t
 Component enable/disable flags. More...
struct  client_audio_pipeline_config_t
 Pipeline configuration parameters. More...
struct  client_audio_pipeline_t
 Client audio pipeline state. More...

Macros

#define CLIENT_AUDIO_PIPELINE_FLAGS_ALL
 Component enable/disable flags.
#define CLIENT_AUDIO_PIPELINE_FLAGS_MINIMAL
 Minimal flags for testing (only codec, no processing).

Typedefs

typedef struct SpeexPreprocessState_ SpeexPreprocessState
typedef struct JitterBuffer_ JitterBuffer
typedef struct webrtc_EchoCanceller3 webrtc_EchoCanceller3
typedef struct OpusEncoder OpusEncoder
typedef struct OpusDecoder OpusDecoder

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)
Lifecycle Functions

When code needs to work with client audio pipeline, it keeps flags, echo_canceller, processor, preprocess here instead of scattering it across globals. Initialization establishes the handles and sizes; teardown must release any allocation, lock, thread, or callback context recorded in those fields.

client_audio_pipeline_t * client_audio_pipeline_create (const client_audio_pipeline_config_t *config)
 Create a new client audio pipeline.
void client_audio_pipeline_destroy (client_audio_pipeline_t *pipeline)
 Destroy a client audio pipeline.
Flag Management
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.
Capture Path (Microphone → Network)
int client_audio_pipeline_capture (client_audio_pipeline_t *pipeline, const float *input, int num_samples, uint8_t *opus_out, int opus_out_size)
 Process captured audio and encode to Opus.
Playback Path (Network → Speakers)
int client_audio_pipeline_playback (client_audio_pipeline_t *pipeline, const uint8_t *opus_in, int opus_len, float *output, int max_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.
Full-Duplex AEC3 Processing
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.
Status and Diagnostics
int client_audio_pipeline_jitter_margin (client_audio_pipeline_t *pipeline)
 Get jitter buffer margin (buffered time in ms).
bool client_audio_pipeline_voice_detected (client_audio_pipeline_t *pipeline)
 Check if VAD detected voice activity in last capture.
void client_audio_pipeline_reset (client_audio_pipeline_t *pipeline)
 Reset pipeline state.

Detailed Description

Unified client-side audio processing pipeline.

This header provides a complete audio processing pipeline for ascii-chat clients, integrating WebRTC AEC3 (production-grade echo cancellation), SpeexDSP (noise suppression, AGC, VAD), Speex jitter buffer, Opus codec, and mixer components (compression, noise gate, filters).

PIPELINE ARCHITECTURE:

CAPTURE PATH (microphone → network): Mic Input (float32, 48kHz) ↓ Echo Cancellation (WebRTC AEC3 - automatic network delay estimation + adaptive filtering) ↓ Preprocessor (Noise/AGC/VAD) ↓ High-Pass Filter (remove rumble) ↓ Low-Pass Filter (remove hiss) ↓ Noise Gate (silence detection) ↓ Compressor (dynamic range control) ↓ Opus Encode → Network

PLAYBACK PATH (network → speakers): Network ↓ Speex Jitter Buffer ↓ Opus Decode ↓ Register with AEC (WebRTC AEC3 reference signal for echo learning) ↓ Soft Clipping → Speakers

COMPONENT FLAGS:

Each processing stage can be individually enabled/disabled via flags. This allows testing and debugging specific components.

THREAD SAFETY:

  • Pipeline has internal mutex for state protection
  • Capture and playback paths can run concurrently
  • Echo reference feeding is thread-safe
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
December 2025

Definition in file client_pipeline.h.

Macro Definition Documentation

◆ CLIENT_AUDIO_PIPELINE_FLAGS_ALL

#define CLIENT_AUDIO_PIPELINE_FLAGS_ALL
Value:
.echo_cancel = true, \
.noise_suppress = true, \
.agc = true, \
.vad = true, \
.jitter_buffer = true, \
.compressor = true, \
.noise_gate = true, \
.highpass = true, \
.lowpass = true, \
})
Component enable/disable flags.

Component enable/disable flags.

Default flags with all processing enabled

Definition at line 114 of file client_pipeline.h.

114#define CLIENT_AUDIO_PIPELINE_FLAGS_ALL \
115 ((client_audio_pipeline_flags_t){ \
116 .echo_cancel = true, \
117 .noise_suppress = true, \
118 .agc = true, \
119 .vad = true, \
120 .jitter_buffer = true, \
121 .compressor = true, \
122 .noise_gate = true, \
123 .highpass = true, \
124 .lowpass = true, \
125 })

Referenced by client_audio_pipeline_default_config().

◆ CLIENT_AUDIO_PIPELINE_FLAGS_MINIMAL

#define CLIENT_AUDIO_PIPELINE_FLAGS_MINIMAL
Value:
.echo_cancel = false, \
.noise_suppress = false, \
.agc = false, \
.vad = false, \
.jitter_buffer = false, \
.compressor = false, \
.noise_gate = false, \
.highpass = false, \
.lowpass = false, \
})

Minimal flags for testing (only codec, no processing).

Definition at line 130 of file client_pipeline.h.

130#define CLIENT_AUDIO_PIPELINE_FLAGS_MINIMAL \
131 ((client_audio_pipeline_flags_t){ \
132 .echo_cancel = false, \
133 .noise_suppress = false, \
134 .agc = false, \
135 .vad = false, \
136 .jitter_buffer = false, \
137 .compressor = false, \
138 .noise_gate = false, \
139 .highpass = false, \
140 .lowpass = false, \
141 })

Referenced by client_audio_pipeline_get_flags().

Typedef Documentation

◆ JitterBuffer

typedef struct JitterBuffer_ JitterBuffer

Definition at line 70 of file client_pipeline.h.

◆ SpeexPreprocessState

typedef struct SpeexPreprocessState_ SpeexPreprocessState

Definition at line 69 of file client_pipeline.h.

◆ webrtc_EchoCanceller3

Definition at line 73 of file client_pipeline.h.

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.

Parameters
pipelinePipeline instance
inputInput samples (float32, -1.0 to 1.0)
num_samplesNumber of input samples (should match frame_size)
opus_outOutput buffer for Opus packet
opus_out_sizeSize of opus_out buffer (should be >= MAX_OPUS_PACKET)
Returns
Number of bytes written to opus_out, or negative on error

Processing pipeline (when flags enabled):

  1. Convert float → int16
  2. Echo cancellation (subtract speaker output from mic input)
  3. Speex preprocessor (noise suppression, AGC, VAD)
  4. Convert int16 → float
  5. High-pass filter
  6. Low-pass filter
  7. Noise gate
  8. Compressor
  9. Opus encode

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 a new client audio pipeline.

Parameters
configPipeline configuration (NULL for defaults)
Returns
New pipeline instance, or NULL on failure

Allocates and initializes all audio processing components. Must be destroyed with client_audio_pipeline_destroy().

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.

Parameters
pipelinePipeline instance
outputOutput buffer for decoded samples (float32)
num_samplesNumber of samples to retrieve
Returns
Number of samples written, or negative on error

This is called from the audio output callback to get the next frame of audio. It pulls from the jitter buffer and decodes.

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).

Parameters
pipelinePipeline instance
Returns
Current jitter buffer margin in milliseconds

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.

Parameters
pipelinePipeline instance
opus_inOpus packet data (can be NULL for packet loss concealment)
opus_lenOpus packet length (0 if opus_in is NULL)
outputOutput buffer for decoded samples (float32)
max_samplesMaximum samples to write to output
Returns
Number of samples written to output, or negative on error

Processing pipeline (when flags enabled):

  1. Put Opus packet into jitter buffer
  2. Get packet from jitter buffer (handles reordering, loss)
  3. Opus decode
  4. Soft clipping

Note: Output should be fed to speakers AND to echo reference via client_audio_pipeline_feed_echo_ref().

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.

Parameters
pipelinePipeline instance
render_samplesAudio samples being played to speakers RIGHT NOW
render_countNumber of render samples
capture_samplesAudio samples from microphone RIGHT NOW
capture_countNumber of capture samples
processed_outputOutput buffer for processed capture (must be capture_count size)

Called from PortAudio's single full-duplex callback where render and capture happen at the EXACT same instant. No ring buffers, no timing mismatch.

Processing:

  1. AEC3 AnalyzeRender on render_samples
  2. AEC3 AnalyzeCapture + ProcessCapture on capture_samples
  3. Apply highpass, lowpass, noise gate, compressor

REAL-TIME SAFE: No mutexes, no allocations, no blocking.

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.

Parameters
pipelinePipeline instance

Resets echo canceller, jitter buffer, and filter states. Call when starting a new audio session.

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.

◆ client_audio_pipeline_voice_detected()

bool client_audio_pipeline_voice_detected ( client_audio_pipeline_t * pipeline)

Check if VAD detected voice activity in last capture.

Parameters
pipelinePipeline instance
Returns
true if voice was detected, false otherwise