ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches

🔊 Audio system for real-time audio capture and playback More...

Files

file  analysis.c
 Audio Analysis Implementation.
 
file  analysis.h
 Audio Analysis and Debugging Interface.
 
file  audio.c
 🔊 Audio capture and playback using PortAudio with buffer management
 
file  audio.h
 🔊 Audio Capture and Playback Interface for ascii-chat
 
file  client_audio_pipeline.h
 Unified client-side audio processing pipeline.
 
file  mixer.c
 🎚️ Real-time audio mixer with ducking, gain control, and multi-stream blending
 
file  mixer.h
 Multi-Source Audio Mixing and Processing System.
 
file  opus_codec.c
 Opus audio codec implementation.
 
file  opus_codec.h
 Opus audio codec wrapper for real-time encoding/decoding.
 

Data Structures

struct  audio_context_t
 Audio context for full-duplex capture and playback. More...
 
struct  audio_device_info_t
 Audio device information structure. More...
 
struct  audio_batch_info_t
 Parsed audio batch packet header information. More...
 
struct  compressor_t
 Dynamic range compressor settings and state. More...
 
struct  noise_gate_t
 Noise gate settings and state. More...
 
struct  highpass_filter_t
 High-pass filter settings and state. More...
 
struct  lowpass_filter_t
 Low-pass filter state. More...
 
struct  ducking_t
 Ducking system settings and state. More...
 
struct  mixer_t
 Main mixer structure for multi-source audio processing. More...
 
struct  opus_codec_t
 Opus codec context for encoding or decoding. More...
 

Macros

#define AUDIO_SAMPLE_RATE   48000
 Audio sample rate (48kHz professional quality, Opus-compatible)
 
#define AUDIO_FRAMES_PER_BUFFER   480
 Audio frames per buffer (480 = 10ms at 48kHz, matches WebRTC AEC3 frame size)
 
#define AUDIO_CHANNELS   1
 Number of audio channels (1 = mono)
 
#define AUDIO_BUFFER_SIZE   (AUDIO_FRAMES_PER_BUFFER * AUDIO_CHANNELS)
 Total audio buffer size (frames × channels)
 

Typedefs

typedef struct OpusEncoder OpusEncoder
 
typedef struct OpusDecoder OpusDecoder
 

Enumerations

enum  opus_application_t { OPUS_APPLICATION_VOIP = 2048 , OPUS_APPLICATION_AUDIO = 2049 , OPUS_APPLICATION_RESTRICTED_LOWDELAY = 2051 }
 Application mode for opus encoder. More...
 

Functions

asciichat_error_t audio_init (audio_context_t *ctx)
 Initialize audio context and PortAudio.
 
void audio_destroy (audio_context_t *ctx)
 Destroy audio context and clean up resources.
 
void audio_set_pipeline (audio_context_t *ctx, void *pipeline)
 Set audio pipeline for echo cancellation.
 
asciichat_error_t audio_start_duplex (audio_context_t *ctx)
 Start full-duplex audio (simultaneous capture and playback)
 
asciichat_error_t audio_stop_duplex (audio_context_t *ctx)
 Stop full-duplex audio.
 
asciichat_error_t audio_read_samples (audio_context_t *ctx, float *buffer, int num_samples)
 Read captured audio samples from capture buffer.
 
asciichat_error_t audio_write_samples (audio_context_t *ctx, const float *buffer, int num_samples)
 Write audio samples to playback buffer.
 
asciichat_error_t audio_set_realtime_priority (void)
 Request real-time priority for current thread.
 
asciichat_error_t audio_list_input_devices (audio_device_info_t **out_devices, unsigned int *out_count)
 List available audio input devices (microphones)
 
asciichat_error_t audio_list_output_devices (audio_device_info_t **out_devices, unsigned int *out_count)
 List available audio output devices (speakers)
 
void audio_free_device_list (audio_device_info_t *devices)
 Free device list allocated by audio_list_input_devices/audio_list_output_devices.
 
asciichat_error_t audio_dequantize_samples (const uint8_t *samples_ptr, uint32_t total_samples, float *out_samples)
 Dequantize network audio samples from int32 to float.
 
audio_ring_buffer_taudio_ring_buffer_create (void)
 Create a new audio ring buffer (for playback with jitter buffering)
 
audio_ring_buffer_taudio_ring_buffer_create_for_capture (void)
 Create a new audio ring buffer for capture (without jitter buffering)
 
void audio_ring_buffer_destroy (audio_ring_buffer_t *rb)
 Destroy an audio ring buffer.
 
void audio_ring_buffer_clear (audio_ring_buffer_t *rb)
 Clear all audio samples from ring buffer.
 
asciichat_error_t audio_ring_buffer_write (audio_ring_buffer_t *rb, const float *data, int samples)
 Write audio samples to ring buffer.
 
size_t audio_ring_buffer_read (audio_ring_buffer_t *rb, float *data, size_t samples)
 Read audio samples from ring buffer.
 
size_t audio_ring_buffer_peek (audio_ring_buffer_t *rb, float *data, size_t samples)
 Peek at available samples without consuming them (for AEC3 render signal)
 
size_t audio_ring_buffer_available_read (audio_ring_buffer_t *rb)
 Get number of samples available for reading.
 
size_t audio_ring_buffer_available_write (audio_ring_buffer_t *rb)
 Get number of sample slots available for writing.
 
void resample_linear (const float *src, size_t src_samples, float *dst, size_t dst_samples, double src_rate, double dst_rate)
 Resample audio using linear interpolation.
 
asciichat_error_t audio_parse_batch_header (const void *data, size_t len, audio_batch_info_t *out_batch)
 Parse an audio batch packet header from raw packet data.
 
asciichat_error_t audio_validate_batch_params (const audio_batch_info_t *batch)
 Validate audio batch parameters for sanity.
 
bool audio_is_supported_sample_rate (uint32_t sample_rate)
 Check if a sample rate is a standard/supported rate.
 
opus_codec_topus_codec_create_encoder (opus_application_t application, int sample_rate, int bitrate)
 Create an Opus encoder.
 
opus_codec_topus_codec_create_decoder (int sample_rate)
 Create an Opus decoder.
 
size_t opus_codec_encode (opus_codec_t *codec, const float *samples, int num_samples, uint8_t *out_data, size_t out_size)
 Encode audio frame with Opus.
 
int opus_codec_decode (opus_codec_t *codec, const uint8_t *data, size_t data_len, float *out_samples, int out_num_samples)
 Decode Opus audio frame.
 
asciichat_error_t opus_codec_set_bitrate (opus_codec_t *codec, int bitrate)
 Set encoder bitrate.
 
int opus_codec_get_bitrate (opus_codec_t *codec)
 Get current encoder bitrate.
 
asciichat_error_t opus_codec_set_dtx (opus_codec_t *codec, int enable)
 Enable/disable DTX (Discontinuous Transmission)
 
void opus_codec_destroy (opus_codec_t *codec)
 Destroy an Opus codec instance.
 

Variables

float compressor_t::threshold_dB
 Compression threshold in dB (e.g., -10.0)
 
float compressor_t::knee_dB
 Knee width in dB for soft knee (e.g., 2.0)
 
float compressor_t::ratio
 Compression ratio (e.g., 4.0 for 4:1 compression)
 
float compressor_t::attack_ms
 Attack time in milliseconds (how fast compression kicks in)
 
float compressor_t::release_ms
 Release time in milliseconds (how fast compression releases)
 
float compressor_t::makeup_dB
 Makeup gain in dB (compensates for gain reduction)
 
float compressor_t::sample_rate
 Sample rate in Hz (set during initialization)
 
float compressor_t::envelope
 Current envelope follower state (linear, 0-1)
 
float compressor_t::gain_lin
 Current gain multiplier (linear, calculated from envelope)
 
float compressor_t::attack_coeff
 Attack coefficient (converted from attack_ms)
 
float compressor_t::release_coeff
 Release coefficient (converted from release_ms)
 
float noise_gate_t::threshold
 Gate threshold in linear units (e.g., 0.01f for -40dB)
 
float noise_gate_t::attack_ms
 Attack time in milliseconds (how fast gate opens)
 
float noise_gate_t::release_ms
 Release time in milliseconds (how fast gate closes)
 
float noise_gate_t::hysteresis
 Hysteresis factor (0-1, prevents gate chatter)
 
float noise_gate_t::sample_rate
 Sample rate in Hz (set during initialization)
 
float noise_gate_t::envelope
 Current envelope follower state (linear, 0-1)
 
float noise_gate_t::attack_coeff
 Attack coefficient (converted from attack_ms)
 
float noise_gate_t::release_coeff
 Release coefficient (converted from release_ms)
 
bool noise_gate_t::gate_open
 True if gate is currently open (allowing audio through)
 
float highpass_filter_t::cutoff_hz
 Cutoff frequency in Hz (frequencies below this are attenuated)
 
float highpass_filter_t::sample_rate
 Sample rate in Hz (set during initialization)
 
float highpass_filter_t::alpha
 Filter coefficient alpha (calculated from cutoff_hz)
 
float highpass_filter_t::prev_input
 Previous input sample (filter state)
 
float highpass_filter_t::prev_output
 Previous output sample (filter state)
 
float lowpass_filter_t::cutoff_hz
 Cutoff frequency in Hz (frequencies above this are attenuated)
 
float lowpass_filter_t::sample_rate
 Sample rate in Hz (set during initialization)
 
float lowpass_filter_t::alpha
 Filter coefficient alpha (calculated from cutoff_hz)
 
float lowpass_filter_t::prev_output
 Previous output sample (filter state)
 
float ducking_t::threshold_dB
 Speaking threshold in dB (sources below this are not "speaking")
 
float ducking_t::leader_margin_dB
 Leader margin in dB (sources within this of loudest are leaders)
 
float ducking_t::atten_dB
 Attenuation in dB for non-leader sources.
 
float ducking_t::attack_ms
 Ducking attack time in milliseconds.
 
float ducking_t::release_ms
 Ducking release time in milliseconds.
 
float ducking_t::attack_coeff
 Attack coefficient (converted from attack_ms)
 
float ducking_t::release_coeff
 Release coefficient (converted from release_ms)
 
float * ducking_t::envelope
 Per-source envelope follower state (linear, allocated per source)
 
float * ducking_t::gain
 Per-source ducking gain (linear, calculated from envelope)
 
int mixer_t::num_sources
 Current number of active audio sources.
 
int mixer_t::max_sources
 Maximum number of sources (allocated array sizes)
 
int mixer_t::sample_rate
 Sample rate in Hz (e.g., 44100)
 
audio_ring_buffer_t ** mixer_t::source_buffers
 Array of pointers to client audio ring buffers.
 
uint32_tmixer_t::source_ids
 Array of client IDs (one per source slot)
 
boolmixer_t::source_active
 Array of active flags (true if source is active)
 
uint64_t mixer_t::active_sources_mask
 Bitset of active sources (bit i = source i is active, O(1) iteration)
 
uint8_t mixer_t::source_id_to_index [256]
 Hash table mapping client_id → mixer source index (uses hash function for 32-bit IDs)
 
uint32_t mixer_t::source_id_at_hash [256]
 Client IDs stored at each hash index for collision detection.
 
rwlock_t mixer_t::source_lock
 Reader-writer lock protecting source arrays and bitset.
 
float mixer_t::crowd_alpha
 Crowd scaling exponent (typically 0.5 for sqrt scaling)
 
float mixer_t::base_gain
 Base gain before crowd scaling is applied.
 
ducking_t mixer_t::ducking
 Ducking system (active speaker detection and attenuation)
 
compressor_t mixer_t::compressor
 Compressor (dynamic range compression)
 
floatmixer_t::mix_buffer
 Temporary buffer for mixing operations (pre-allocated)
 

Mixer Lifecycle Functions

mixer_tmixer_create (int max_sources, int sample_rate)
 Create a new audio mixer.
 
void mixer_destroy (mixer_t *mixer)
 Destroy a mixer and free all resources.
 

Source Management Functions

int mixer_add_source (mixer_t *mixer, uint32_t client_id, audio_ring_buffer_t *buffer)
 Add an audio source to the mixer.
 
void mixer_remove_source (mixer_t *mixer, uint32_t client_id)
 Remove an audio source from the mixer.
 
void mixer_set_source_active (mixer_t *mixer, uint32_t client_id, bool active)
 Set whether a source is active (receiving audio)
 

Audio Processing Functions

int mixer_process (mixer_t *mixer, float *output, int num_samples)
 Process audio from all active sources.
 
int mixer_process_excluding_source (mixer_t *mixer, float *output, int num_samples, uint32_t exclude_client_id)
 Process audio from all sources except one (for per-client output)
 

Utility Functions

float db_to_linear (float db)
 Convert decibels to linear gain.
 
float linear_to_db (float linear)
 Convert linear gain to decibels.
 
float clamp_float (float value, float min, float max)
 Clamp a float value to a range.
 

Compressor Functions

void compressor_init (compressor_t *comp, float sample_rate)
 Initialize a compressor.
 
void compressor_set_params (compressor_t *comp, float threshold_dB, float ratio, float attack_ms, float release_ms, float makeup_dB)
 Set compressor parameters.
 
float compressor_process_sample (compressor_t *comp, float sidechain)
 Process a single sample through compressor.
 

Ducking Functions

int ducking_init (ducking_t *duck, int num_sources, float sample_rate)
 Initialize ducking system.
 
void ducking_free (ducking_t *duck)
 Free ducking system resources.
 
void ducking_set_params (ducking_t *duck, float threshold_dB, float leader_margin_dB, float atten_dB, float attack_ms, float release_ms)
 Set ducking parameters.
 
void ducking_process_frame (ducking_t *duck, float *envelopes, float *gains, int num_sources)
 Process a frame of audio through ducking system.
 

Noise Gate Functions

void noise_gate_init (noise_gate_t *gate, float sample_rate)
 Initialize a noise gate.
 
void noise_gate_set_params (noise_gate_t *gate, float threshold, float attack_ms, float release_ms, float hysteresis)
 Set noise gate parameters.
 
float noise_gate_process_sample (noise_gate_t *gate, float input, float peak_amplitude)
 Process a single sample through noise gate.
 
void noise_gate_process_buffer (noise_gate_t *gate, float *buffer, int num_samples)
 Process a buffer of samples through noise gate.
 
bool noise_gate_is_open (const noise_gate_t *gate)
 Check if noise gate is currently open.
 

High-Pass Filter Functions

void highpass_filter_init (highpass_filter_t *filter, float cutoff_hz, float sample_rate)
 Initialize a high-pass filter.
 
void highpass_filter_reset (highpass_filter_t *filter)
 Reset high-pass filter state.
 
float highpass_filter_process_sample (highpass_filter_t *filter, float input)
 Process a single sample through high-pass filter.
 
void highpass_filter_process_buffer (highpass_filter_t *filter, float *buffer, int num_samples)
 Process a buffer of samples through high-pass filter.
 

Low-Pass Filter Functions

void lowpass_filter_init (lowpass_filter_t *filter, float cutoff_hz, float sample_rate)
 Initialize a low-pass filter.
 
void lowpass_filter_reset (lowpass_filter_t *filter)
 Reset low-pass filter state.
 
float lowpass_filter_process_sample (lowpass_filter_t *filter, float input)
 Process a single sample through low-pass filter.
 
void lowpass_filter_process_buffer (lowpass_filter_t *filter, float *buffer, int num_samples)
 Process a buffer of samples through low-pass filter.
 

Buffer Utility Functions

float smoothstep (float t)
 Compute smoothstep interpolation.
 
int16_t float_to_int16 (float sample)
 Convert float sample to int16 (WebRTC format)
 
float int16_to_float (int16_t sample)
 Convert int16 sample to float.
 
void buffer_float_to_int16 (const float *src, int16_t *dst, int count)
 Convert float buffer to int16 buffer.
 
void buffer_int16_to_float (const int16_t *src, float *dst, int count)
 Convert int16 buffer to float buffer.
 
float buffer_peak (const float *buffer, int count)
 Find peak absolute value in buffer.
 
void apply_gain_buffer (float *buffer, int count, float gain)
 Apply gain to buffer in-place.
 
void fade_buffer (float *buffer, int count, float start_gain, float end_gain)
 Apply linear fade to buffer in-place.
 
void fade_buffer_smooth (float *buffer, int count, bool fade_in)
 Apply smoothstep fade to buffer in-place.
 
void copy_buffer_with_gain (const float *src, float *dst, int count, float gain)
 Copy buffer with gain scaling.
 

Soft Clipping Functions

float soft_clip (float sample, float threshold, float steepness)
 Apply soft clipping to a sample.
 
void soft_clip_buffer (float *buffer, int num_samples, float threshold, float steepness)
 Apply soft clipping to a buffer.
 

Audio Mixing Configuration

#define MIXER_MAX_SOURCES   32
 Maximum number of simultaneous audio sources.
 
#define MIXER_FRAME_SIZE   256
 Number of samples processed per audio frame.
 

Detailed Description

🔊 Audio system for real-time audio capture and playback

This header provides audio capture and playback functionality using PortAudio, enabling real-time audio streaming for ascii-chat video chat sessions.

CORE FEATURES:

AUDIO ARCHITECTURE:

The audio system uses PortAudio for cross-platform audio I/O:

AUDIO PARAMETERS:

THREAD SAFETY:

PLATFORM SUPPORT:

Note
Audio capture and playback can be started/stopped independently.
The audio system uses ring buffers to smooth out timing variations between capture/playback threads and network I/O.
Real-time priority is automatically requested on supported platforms to minimize audio glitches and latency.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This header provides professional-quality audio mixing for ascii-chat's multi-client audio chat functionality. The mixer combines audio from multiple clients with advanced processing including compression, ducking, noise gating, and high-pass filtering.

CORE FEATURES:

AUDIO PROCESSING PIPELINE:

The mixer processes audio through the following stages:

  1. Source Reading: Reads samples from client audio ring buffers
  2. Ducking: Identifies active speaker and attenuates background sources
  3. Mixing: Combines all active sources with crowd scaling
  4. Compression: Applies dynamic range compression to prevent clipping
  5. Noise Gate: Suppresses background noise below threshold
  6. High-Pass Filter: Removes low-frequency noise and rumble
  7. Soft Clipping: Prevents hard clipping artifacts

DUCKING SYSTEM:

The ducking system automatically:

COMPRESSION:

The compressor provides:

OPTIMIZATIONS:

THREAD SAFETY:

Note
The mixer processes audio in fixed-size frames (256 samples) for consistent latency and processing behavior.
Ducking parameters should be tuned based on room acoustics and expected participant count for optimal performance.
Crowd scaling automatically reduces per-source volume as more participants join, preventing audio overload.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

Provides high-level interface to libopus for encoding and decoding audio frames with configurable bitrate and frame sizes.

OPUS CODEC FEATURES:

USAGE EXAMPLE:

// Create encoder for voice at 24 kbps
// Encode audio (882 samples = 20ms at 44.1kHz)
float samples[882];
uint8_t compressed[250];
size_t encoded_bytes = opus_codec_encode(encoder, samples, 882, compressed, 250);
// Send over network...
// Create decoder
// Decode received audio
float decoded[882];
int decoded_samples = opus_codec_decode(decoder, compressed, encoded_bytes, decoded, 882);
// Cleanup
opus_codec_t * opus_codec_create_decoder(int sample_rate)
Create an Opus decoder.
Definition opus_codec.c:62
int opus_codec_decode(opus_codec_t *codec, const uint8_t *data, size_t data_len, float *out_samples, int out_num_samples)
Decode Opus audio frame.
Definition opus_codec.c:128
opus_codec_t * opus_codec_create_encoder(opus_application_t application, int sample_rate, int bitrate)
Create an Opus encoder.
Definition opus_codec.c:18
void opus_codec_destroy(opus_codec_t *codec)
Destroy an Opus codec instance.
Definition opus_codec.c:215
size_t opus_codec_encode(opus_codec_t *codec, const float *samples, int num_samples, uint8_t *out_data, size_t out_size)
Encode audio frame with Opus.
Definition opus_codec.c:97
@ OPUS_APPLICATION_VOIP
Voice over IP (optimized for speech)
Definition opus_codec.h:77
unsigned char uint8_t
Definition common.h:56
Opus codec context for encoding or decoding.
Definition opus_codec.h:95
Note
Thread-safety: Each codec instance must not be accessed from multiple threads simultaneously. Create separate encoder and decoder instances per thread if needed.
Frame sizes: Opus works with fixed frame sizes. Recommended:
  • 20ms (882 samples @ 44.1kHz) for voice
  • 40ms (1764 samples @ 44.1kHz) for low-latency music
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
December 2025

Audio README

Overview

Welcome to the Audio System! This is where all the audio magic happens—capturing your voice from the microphone, playing back audio from other participants, and making sure everything runs smoothly in real-time. We use PortAudio for cross-platform audio I/O, which means everything works the same way on Linux, macOS, and Windows.

What does the audio system do?

The Audio System provides real-time audio capture and playback functionality for ascii-chat video chat sessions. Here's what it gives you:

  • Real-time audio capture from microphone/input devices (so your voice is captured as you speak)
  • Real-time audio playback to speakers/output devices (so you hear others in real-time)
  • Thread-safe ring buffers for audio data (so capture and playback can run in parallel without conflicts)
  • Low-latency audio processing (so there's minimal delay between speaking and hearing)
  • Platform-specific real-time priority scheduling (so audio threads get priority and don't get interrupted by other tasks)
  • Configurable audio parameters (sample rate, buffer size) so you can tune for your needs
  • Automatic device enumeration and selection (so it just works with your audio devices)

Implementation: lib/audio.h

Architecture

The audio system is built around a few key concepts: PortAudio for cross-platform audio I/O, ring buffers for efficient data transfer, and thread-safe operations so everything can run in parallel. Let's walk through how everything fits together.

How does PortAudio work?

We use PortAudio for cross-platform audio I/O because it handles all the platform-specific details for us—we write the same code and it works on Linux, macOS, and Windows. PortAudio provides:

Audio Streams:

  • Separate input and output streams for full-duplex audio (you can capture and play back at the same time)
  • Independent capture and playback threads (so capture doesn't block playback and vice versa)
  • Automatic stream management and lifecycle (PortAudio handles starting, stopping, and cleaning up streams)

What about ring buffers?

Ring Buffers:

  • Efficient producer-consumer audio data transfer: Ring buffers let one thread (the producer) write data while another thread (the consumer) reads data, without blocking each other
  • Lock-free or mutex-protected buffers depending on platform (we use the most efficient approach for each platform)
  • Jitter buffering to smooth out network timing variations (network latency can vary, so we buffer a bit to smooth it out)
  • Configurable buffer sizes for latency/quality trade-offs (bigger buffers = smoother playback but higher latency, smaller buffers = lower latency but might stutter)

How do we handle threading?

Thread Safety:

  • Audio context state protected by mutex: When multiple threads access the audio context, they're protected by a mutex so there are no race conditions
  • Ring buffers provide thread-safe audio data transfer: The ring buffers themselves are thread-safe, so capture and playback can run in parallel
  • Real-time priority scheduling on supported platforms: Audio threads get real-time priority, so they don't get interrupted by other tasks (critical for smooth audio playback)

Audio Parameters

Audio parameters control the quality and latency of audio. The defaults are tuned for good quality with low latency, but you can adjust them for your needs.

What are the defaults?

Default Configuration:

  • Sample Rate: 44.1kHz (CD quality)—this gives you excellent audio quality while keeping bandwidth reasonable. If you need even higher quality, you can go up to 48kHz or even 192kHz
  • Channels: Mono (1 channel)—this keeps bandwidth low. If you want stereo (2 channels), you can enable it, but it doubles the bandwidth
  • Buffer Size: 256 frames per buffer (low latency)—this gives you low latency (~5.8ms at 44.1kHz). If you have audio stuttering, you might want to increase this
  • Format: 32-bit floating point samples—this gives you the best quality and is what PortAudio recommends

What can I configure?

Configurable Options:

  • Custom sample rates: You can use any sample rate from 8kHz (for low bandwidth) up to 192kHz (for high quality), but 44.1kHz and 48kHz are the most common
  • Stereo support: You can enable stereo (2 channels) if you want spatial audio, but it doubles the bandwidth
  • Variable buffer sizes: You can adjust buffer sizes from 128 frames (ultra-low latency but might stutter) to 1024 frames (smooth but higher latency)
  • Device selection: You can choose which input/output device to use if you have multiple audio devices

Operations

Initialization

Create Audio Context:

audio_context_t audio_ctx;
asciichat_error_t err = audio_init(&audio_ctx);
if (err != ASCIICHAT_OK) {
log_error("Failed to initialize audio: %d", err);
return err;
}
asciichat_error_t audio_init(audio_context_t *ctx)
Initialize audio context and PortAudio.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ASCIICHAT_OK
Definition error_codes.h:48
#define log_error(...)
Log an ERROR message.
Audio context for full-duplex capture and playback.

Configure Audio Parameters:

// Set custom sample rate and buffer size
audio_ctx.sample_rate = 48000;
audio_ctx.buffer_size = 512;
audio_ctx.channels = 1; // Mono
double sample_rate
Actual sample rate of streams (48kHz)

Audio Capture

Start Capture:

err = audio_start_capture(&audio_ctx);
if (err != ASCIICHAT_OK) {
log_error("Failed to start audio capture");
return err;
}

Read Captured Samples:

float samples[256];
asciichat_error_t err = audio_read_samples(&audio_ctx, samples, 256);
if (err == ASCIICHAT_OK) {
// Send samples to network
send_audio_packet(sockfd, samples, 256);
}
asciichat_error_t audio_read_samples(audio_context_t *ctx, float *buffer, int num_samples)
Read captured audio samples from capture buffer.

Audio Playback

Start Playback:

err = audio_start_playback(&audio_ctx);
if (err != ASCIICHAT_OK) {
log_error("Failed to start audio playback");
return err;
}

Write Playback Samples:

float samples[256];
// Receive samples from network
receive_audio_packet(sockfd, samples, 256);
// Write to playback buffer
asciichat_error_t err = audio_write_samples(&audio_ctx, samples, 256);
if (err != ASCIICHAT_OK) {
log_warn("Failed to write audio samples");
}
asciichat_error_t audio_write_samples(audio_context_t *ctx, const float *buffer, int num_samples)
Write audio samples to playback buffer.
#define log_warn(...)
Log a WARN message.

Cleanup

Stop Audio:

audio_stop_capture(&audio_ctx);
audio_stop_playback(&audio_ctx);

Destroy Audio Context:

audio_destroy(&audio_ctx);
void audio_destroy(audio_context_t *ctx)
Destroy audio context and clean up resources.

Platform Support

Windows:

  • DirectSound backend (legacy)
  • WASAPI backend (modern, recommended)
  • ASIO backend (low-latency professional audio)

Linux:

  • ALSA backend (standard Linux audio)
  • JACK backend (professional audio, low latency)
  • PulseAudio support via ALSA

macOS:

  • CoreAudio backend (native macOS audio)
  • Automatic device selection
  • Low-latency support

Performance

Audio performance is all about balancing latency, CPU usage, and bandwidth. We've tuned the defaults for good performance across all three dimensions, but let's look at what you can expect:

How much latency do we have?

Latency:

  • Buffer size: 256 frames @ 44.1kHz = ~5.8ms latency (this is the latency from the audio buffer itself)
  • Network jitter buffering: +46ms (8 packets) (this is extra buffering to smooth out network timing variations)
  • Total end-to-end latency: ~50-60ms (this is the total time from when someone speaks to when you hear it)

50-60ms is quite good for networked audio—it's comparable to phone calls and way better than most video conferencing software. The jitter buffering helps smooth out network hiccups, so you get smooth audio even when the network is a bit flaky.

How much CPU does audio use?

CPU Usage:

  • Audio capture: ~1-2% CPU (single thread) (capturing audio is pretty lightweight)
  • Audio playback: ~1-2% CPU (single thread) (playback is also lightweight)
  • Total audio overhead: ~2-4% CPU (so audio adds about 2-4% CPU overhead total)

Audio is pretty lightweight—you probably won't even notice the CPU usage unless you're monitoring it closely.

How much bandwidth does audio use?

Bandwidth:

  • 44.1kHz mono: ~176 KB/s (about 176 kilobytes per second)
  • 48kHz mono: ~192 KB/s (about 192 kilobytes per second)
  • 48kHz stereo: ~384 KB/s (about 384 kilobytes per second, double because stereo is two channels)

Audio bandwidth is pretty reasonable—even stereo at 48kHz is only about 384 KB/s, which is much less than video. You could stream audio over a decent cellular connection without any problems.

Ring Buffers

The audio system uses ring buffers for efficient producer-consumer audio transfer:

Capture Ring Buffer:

  • Producer: PortAudio capture callback
  • Consumer: Network send thread
  • Size: 8192 samples (~186ms @ 44.1kHz)
  • Thread-safe: Mutex-protected on all platforms

Playback Ring Buffer:

  • Producer: Network receive thread
  • Consumer: PortAudio playback callback
  • Size: 8192 samples (~186ms @ 44.1kHz)
  • Jitter buffer threshold: 2048 samples (~46ms)
  • Thread-safe: Mutex-protected on all platforms

Threading

Audio Threads:

  • Capture thread: PortAudio callback (real-time priority)
  • Playback thread: PortAudio callback (real-time priority)
  • Network threads: User threads (normal priority)

Priority Scheduling:

  • Windows: THREAD_PRIORITY_TIME_CRITICAL for audio threads
  • Linux: SCHED_FIFO with priority 50 (requires capabilities)
  • macOS: Real-time thread priority

Integration

Network Integration:

  • Audio samples sent via PACKET_TYPE_AUDIO_BATCH
  • Compression enabled for large batches
  • Encryption via crypto context (if enabled)

Ring Buffer Integration:

  • Uses specialized audio ring buffer (audio_ring_buffer_t)
  • Jitter buffering for network latency compensation
  • Thread-safe operations with mutex protection
See also
audio.h
ringbuffer.h
network/av.h

Mixer README

Overview

Welcome to the audio mixer—where all the magic happens when multiple people are talking at once!

Picture yourself in a group video call. Person A is talking, Person B laughs, Person C asks a question—all happening simultaneously. Your speakers don't have three separate outputs (well, most don't). So how does your computer play all three audio streams at once? That's where the mixer comes in!

The mixer takes multiple audio streams (one from each client) and combines them into a single output stream that gets sent to everyone. It's like a real mixing board at a concert—each microphone is a separate input, and the mixer blends them into one cohesive sound that goes to the speakers.

But here's the cool part: when lots of people are talking at once, the mixer automatically applies "ducking" (volume reduction) so the combined audio doesn't clip or distort. It's like how a good sound engineer knows to turn down each microphone a bit when everyone's singing together—the mix stays clear and balanced.

Implementation: lib/mixer.h

What makes the mixer special?

  • Real-time mixing: Combines multiple audio streams on the fly
  • Dynamic source management: Sources can join or leave without disrupting the mix
  • Active speaker detection: Automatically identifies who's talking loudest
  • Automatic ducking: Attenuates background sources when someone is speaking
  • Dynamic range compression: Prevents clipping with professional compressor
  • Noise gate: Suppresses background noise below threshold with hysteresis
  • High-pass filtering: Removes low-frequency rumble and noise
  • Soft clipping: Prevents harsh digital clipping artifacts
  • Crowd scaling: Automatically adjusts volume based on participant count
  • Thread-safe: Reader-writer locks for concurrent access
  • Low latency: Fixed 256-sample frame processing
  • O(1) source exclusion: Bitset-based tracking for echo cancellation

Architecture

Mixer Design:

  • Single mixer instance per server
  • Per-client audio input buffers
  • Shared output buffer for mixed audio
  • Thread-safe operation with mutex protection

Audio Flow:

Client 1 Audio → Input Buffer 1 ──┐
Client 2 Audio → Input Buffer 2 ──┤
Client 3 Audio → Input Buffer 3 ──┼→ Mixer → Mixed Output → All Clients
... ─┘

Operations

Initialization

Create Mixer:

// mixer_create returns a pointer to a new mixer (NULL on failure)
if (!mixer) {
log_error("Failed to create mixer");
return ASCIICHAT_ERROR_MEMORY;
}
#define MIXER_MAX_SOURCES
Maximum number of simultaneous audio sources.
Definition mixer.h:104
mixer_t * mixer_create(int max_sources, int sample_rate)
Create a new audio mixer.
Definition mixer.c:218
Main mixer structure for multi-source audio processing.
Definition mixer.h:325

Source Management

Add Audio Source (client with audio ring buffer):

uint32_t client_id = 12345;
audio_ring_buffer_t *client_audio_buffer = ...; // Client's audio ring buffer
int result = mixer_add_source(mixer, client_id, client_audio_buffer);
if (result < 0) {
log_error("Failed to add client %u to mixer", client_id);
return ASCIICHAT_ERROR_FULL;
}
log_info("Client %u added to mixer at index %d", client_id, result);
int mixer_add_source(mixer_t *mixer, uint32_t client_id, audio_ring_buffer_t *buffer)
Add an audio source to the mixer.
Definition mixer.c:363
unsigned int uint32_t
Definition common.h:58
#define log_info(...)
Log an INFO message.
Audio ring buffer for real-time audio streaming.
Definition ringbuffer.h:208

Remove Audio Source:

mixer_remove_source(mixer, client_id);
log_info("Client %u removed from mixer", client_id);
void mixer_remove_source(mixer_t *mixer, uint32_t client_id)
Remove an audio source from the mixer.
Definition mixer.c:400

Audio Processing

The mixer reads audio directly from each client's audio ring buffer. Clients write their audio samples to their ring buffer, and the mixer reads and mixes them during processing.

Mix Audio (reads from all source ring buffers):

float mixed_output[MIXER_FRAME_SIZE];
int samples_mixed = mixer_process(mixer, mixed_output, MIXER_FRAME_SIZE);
if (samples_mixed > 0) {
// Send mixed audio to all clients
send_audio_to_all_clients(mixed_output, samples_mixed);
}
#define MIXER_FRAME_SIZE
Number of samples processed per audio frame.
Definition mixer.h:114
int mixer_process(mixer_t *mixer, float *output, int num_samples)
Process audio from all active sources.
Definition mixer.c:459

Mix Audio Excluding a Source (for echo cancellation):

// Mix all sources except the client we're sending to (prevents echo)
float output_for_client[MIXER_FRAME_SIZE];
int samples = mixer_process_excluding_source(mixer, output_for_client,
MIXER_FRAME_SIZE, client_id);
send_audio_to_client(client_id, output_for_client, samples);
int mixer_process_excluding_source(mixer_t *mixer, float *output, int num_samples, uint32_t exclude_client_id)
Process audio from all sources except one (for per-client output)
Definition mixer.c:604

Cleanup

Destroy Mixer:

mixer_destroy(mixer); // Pass pointer, not address-of
void mixer_destroy(mixer_t *mixer)
Destroy a mixer and free all resources.
Definition mixer.c:346

Active Speaker Detection & Ducking

The ducking system automatically identifies who's speaking and attenuates background sources to improve clarity. This is more sophisticated than simple volume scaling.

How It Works:

  • Leader Detection: The loudest source(s) above threshold_dB are identified
  • Margin Tracking: Sources within leader_margin_dB of the loudest are also "leaders"
  • Attenuation: Non-leader sources are attenuated by atten_dB
  • Smooth Transitions: Attack/release curves prevent jarring volume changes

The ducking uses dB-based audio analysis:

// Ducking parameters (in dB and milliseconds)
typedef struct {
float threshold_dB; // Speaking threshold (-40dB typical)
float leader_margin_dB; // Margin to be considered a leader (6dB typical)
float atten_dB; // Attenuation for non-leaders (-12dB typical)
float attack_ms; // How fast ducking engages (10ms typical)
float release_ms; // How fast ducking releases (100ms typical)
Ducking system settings and state.
Definition mixer.h:271

Practical Example:

  • Person A speaks at -20dB (loud)
  • Person B speaks at -25dB (within 6dB margin of A)
  • Person C has background noise at -50dB (below threshold)
  • Result: A and B are heard at full volume, C is attenuated by 12dB

This allows multiple people to have a natural conversation while suppressing background noise from inactive participants.

Thread Safety

Reader-Writer Lock Protection:

  • Source array protected by reader-writer locks (rwlock)
  • Multiple readers can process audio concurrently
  • Writers (add/remove source) get exclusive access
  • Bitset operations are atomic for source exclusion

Thread Model:

// Network receive thread - writes to client's ring buffer
void* client_receive_thread(void *arg) {
client_t *client = (client_t *)arg;
while (running) {
float samples[256];
receive_audio_packet(client->id, samples, 256);
// Write directly to client's audio ring buffer
audio_ring_buffer_write(client->audio_buffer, samples, 256);
}
return NULL;
}
// Audio processing thread - reads from all ring buffers via mixer
void* audio_mix_thread(void *arg) {
mixer_t *mixer = (mixer_t *)arg;
while (running) {
float mixed[MIXER_FRAME_SIZE];
int samples = mixer_process(mixer, mixed, MIXER_FRAME_SIZE);
if (samples > 0) {
send_to_all_clients(mixed, samples);
}
}
return NULL;
}
asciichat_error_t audio_ring_buffer_write(audio_ring_buffer_t *rb, const float *data, int samples)
Write audio samples to ring buffer.
void * client_receive_thread(void *arg)

Performance

Mixing Algorithm:

  • Simple additive mixing with ducking
  • SIMD optimization where available
  • Minimal memory allocations
  • Cache-friendly data layout

CPU Usage:

  • 2 clients: ~1% CPU
  • 4 clients: ~2% CPU
  • 8 clients: ~3% CPU
  • 16 clients: ~5% CPU

Latency:

  • Mixing latency: <1ms
  • Total audio latency: ~50-60ms (includes network)

Buffer Management

Per-Client Buffers:

  • Fixed-size circular buffers
  • Automatic overflow handling
  • Underrun detection and handling

Buffer Configuration:

mixer.buffer_size = 8192; // Samples per client buffer
mixer.min_frames = 256; // Minimum frames for mixing

Overflow Handling:

  • Drop oldest frames when buffer full
  • Log warning message
  • Continue operation without crash

Underrun Handling:

  • Output silence when insufficient data
  • Log debug message
  • Wait for more data

Integration Example

Complete Server Integration:

// Initialize mixer
mixer_t mixer;
mixer_init(&mixer, MAX_CLIENTS);
// When client connects
void on_client_connect(uint32_t client_id) {
mixer_add_client(&mixer, client_id);
log_info("Client %u added to audio mixer", client_id);
}
// When client disconnects
void on_client_disconnect(uint32_t client_id) {
mixer_remove_client(&mixer, client_id);
log_info("Client %u removed from audio mixer", client_id);
}
// When audio packet arrives
void on_audio_packet(uint32_t client_id, float *samples, size_t num_frames) {
mixer_submit_audio(&mixer, client_id, samples, num_frames);
}
// Audio mixing thread
void* audio_thread(void *arg) {
float mixed[256];
while (running) {
size_t frames = mixer_process(&mixer, mixed, 256);
if (frames > 0) {
broadcast_audio_to_all_clients(mixed, frames);
}
usleep(5000); // ~5ms sleep for 44.1kHz
}
return NULL;
}
// Cleanup
mixer_destroy(&mixer);
#define MAX_CLIENTS
Maximum possible clients (static array size) - actual runtime limit set by –max-clients (1-32)
Definition limits.h:23

Best Practices

DO:

  • Enable ducking for 4+ clients
  • Monitor buffer overflows/underruns
  • Use consistent sample rates across clients
  • Remove clients from mixer on disconnect
  • Use dedicated audio mixing thread

DON'T:

  • Don't mix audio on network thread
  • Don't forget to remove disconnected clients
  • Don't use different sample rates per client
  • Don't disable ducking with many clients
  • Don't mix audio without mutex protection
See also
mixer.h
audio.h
ringbuffer.h

Macro Definition Documentation

◆ AUDIO_BUFFER_SIZE

#define AUDIO_BUFFER_SIZE   (AUDIO_FRAMES_PER_BUFFER * AUDIO_CHANNELS)

#include <audio.h>

Total audio buffer size (frames × channels)

Definition at line 93 of file lib/audio/audio.h.

◆ AUDIO_CHANNELS

#define AUDIO_CHANNELS   1

#include <audio.h>

Number of audio channels (1 = mono)

Definition at line 91 of file lib/audio/audio.h.

◆ AUDIO_FRAMES_PER_BUFFER

#define AUDIO_FRAMES_PER_BUFFER   480

#include <audio.h>

Audio frames per buffer (480 = 10ms at 48kHz, matches WebRTC AEC3 frame size)

Definition at line 89 of file lib/audio/audio.h.

◆ AUDIO_SAMPLE_RATE

#define AUDIO_SAMPLE_RATE   48000

#include <audio.h>

Audio sample rate (48kHz professional quality, Opus-compatible)

Definition at line 87 of file lib/audio/audio.h.

◆ MIXER_FRAME_SIZE

#define MIXER_FRAME_SIZE   256

#include <mixer.h>

Number of samples processed per audio frame.

Fixed frame size for consistent latency and processing behavior. 256 samples at 48kHz = ~5.3ms per frame.

Definition at line 114 of file mixer.h.

◆ MIXER_MAX_SOURCES

#define MIXER_MAX_SOURCES   32

#include <mixer.h>

Maximum number of simultaneous audio sources.

Limits the maximum number of clients that can provide audio simultaneously. Each client requires one source slot in the mixer.

Definition at line 104 of file mixer.h.

Typedef Documentation

◆ OpusDecoder

typedef struct OpusDecoder OpusDecoder

#include <opus_codec.h>

Definition at line 66 of file opus_codec.h.

◆ OpusEncoder

typedef struct OpusEncoder OpusEncoder

#include <opus_codec.h>

Definition at line 65 of file opus_codec.h.

Enumeration Type Documentation

◆ opus_application_t

#include <opus_codec.h>

Application mode for opus encoder.

Enumerator
OPUS_APPLICATION_VOIP 

Voice over IP (optimized for speech)

OPUS_APPLICATION_AUDIO 

General audio (optimized for music)

OPUS_APPLICATION_RESTRICTED_LOWDELAY 

Low-latency mode.

Definition at line 76 of file opus_codec.h.

76 {
opus_application_t
Application mode for opus encoder.
Definition opus_codec.h:76
@ OPUS_APPLICATION_AUDIO
General audio (optimized for music)
Definition opus_codec.h:78
@ OPUS_APPLICATION_RESTRICTED_LOWDELAY
Low-latency mode.
Definition opus_codec.h:79

Function Documentation

◆ apply_gain_buffer()

void apply_gain_buffer ( float *  buffer,
int  count,
float  gain 
)

#include <mixer.h>

Apply gain to buffer in-place.

Parameters
bufferAudio buffer (modified in-place)
countNumber of samples
gainGain multiplier to apply

Multiplies all samples by gain factor.

Definition at line 1096 of file mixer.c.

1096 {
1097 if (!buffer || count <= 0)
1098 return;
1099
1100 for (int i = 0; i < count; i++) {
1101 buffer[i] *= gain;
1102 }
1103}

◆ audio_dequantize_samples()

asciichat_error_t audio_dequantize_samples ( const uint8_t samples_ptr,
uint32_t  total_samples,
float *  out_samples 
)

#include <audio.h>

Dequantize network audio samples from int32 to float.

Parameters
samples_ptrPointer to quantized samples (network byte order uint32_t values)
total_samplesNumber of samples to convert
out_samplesOutput buffer for float samples (must be pre-allocated)
Returns
ASCIICHAT_OK on success, error code on failure

Converts quantized audio samples from network byte order (uint32_t) to floating point format. Performs the following transformation:

  • Read uint32_t in network byte order
  • Convert to int32_t via ntohl()
  • Scale to float range [-1.0, 1.0] by dividing by 2147483647.0

This helper eliminates code duplication between server and client handlers.

Note
out_samples must be pre-allocated by caller with space for total_samples floats
samples_ptr should point to total_samples * sizeof(uint32_t) bytes

Definition at line 1359 of file lib/audio/audio.c.

1359 {
1360 if (!samples_ptr || !out_samples || total_samples == 0) {
1361 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for audio dequantization");
1362 }
1363
1364 for (uint32_t i = 0; i < total_samples; i++) {
1365 uint32_t network_sample;
1366 // Use memcpy to safely handle potential misalignment from packet header
1367 memcpy(&network_sample, samples_ptr + i * sizeof(uint32_t), sizeof(uint32_t));
1368 int32_t scaled = (int32_t)NET_TO_HOST_U32(network_sample);
1369 out_samples[i] = (float)scaled / 2147483647.0f;
1370 }
1371
1372 return ASCIICHAT_OK;
1373}
#define NET_TO_HOST_U32(val)
Definition endian.h:86
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
@ ERROR_INVALID_PARAM

References ASCIICHAT_OK, ERROR_INVALID_PARAM, NET_TO_HOST_U32, and SET_ERRNO.

Referenced by handle_audio_batch_packet().

◆ audio_destroy()

void audio_destroy ( audio_context_t ctx)

#include <audio.h>

Destroy audio context and clean up resources.

Parameters
ctxAudio context to destroy (can be NULL)

Stops all audio streams, destroys ring buffers, and cleans up PortAudio resources. Safe to call multiple times or with NULL pointer.

Note
Automatically stops capture and playback if active.
Ring buffers are destroyed and all data is lost.
PortAudio is terminated after last context is destroyed.

Definition at line 894 of file lib/audio/audio.c.

894 {
895 if (!ctx || !ctx->initialized) {
896 return;
897 }
898
899 // Stop duplex stream if running
900 if (ctx->running) {
902 }
903
904 mutex_lock(&ctx->state_mutex);
905
908
909 // Terminate PortAudio only when last context is destroyed
910 static_mutex_lock(&g_pa_refcount_mutex);
911 if (g_pa_init_refcount > 0) {
912 g_pa_init_refcount--;
913 if (g_pa_init_refcount == 0) {
914 Pa_Terminate();
915 }
916 }
917 static_mutex_unlock(&g_pa_refcount_mutex);
918
919 ctx->initialized = false;
920
923
924 log_info("Audio system destroyed");
925}
void audio_ring_buffer_destroy(audio_ring_buffer_t *rb)
Destroy an audio ring buffer.
asciichat_error_t audio_stop_duplex(audio_context_t *ctx)
Stop full-duplex audio.
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
Definition mutex.h:140
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
Definition mutex.h:175
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
bool initialized
True if context has been initialized.
audio_ring_buffer_t * playback_buffer
Ring buffer for decoded audio from network.
audio_ring_buffer_t * capture_buffer
Ring buffer for processed capture (after AEC3) for encoder thread.
bool running
True if duplex stream is active.
mutex_t state_mutex
Mutex protecting context state.

References audio_ring_buffer_destroy(), audio_stop_duplex(), audio_context_t::capture_buffer, audio_context_t::initialized, log_info, mutex_destroy(), mutex_lock, mutex_unlock, audio_context_t::playback_buffer, audio_context_t::running, and audio_context_t::state_mutex.

Referenced by audio_cleanup(), and audio_client_init().

◆ audio_free_device_list()

void audio_free_device_list ( audio_device_info_t devices)

#include <audio.h>

Free device list allocated by audio_list_input_devices/audio_list_output_devices.

Parameters
devicesDevice array to free (can be NULL)

Frees the device array allocated by audio_list_input_devices() or audio_list_output_devices(). Safe to call with NULL.

Definition at line 1355 of file lib/audio/audio.c.

1355 {
1356 SAFE_FREE(devices);
1357}
#define SAFE_FREE(ptr)
Definition common.h:320

References SAFE_FREE.

Referenced by action_list_microphones(), and action_list_speakers().

◆ audio_init()

asciichat_error_t audio_init ( audio_context_t ctx)

#include <audio.h>

Initialize audio context and PortAudio.

Parameters
ctxAudio context to initialize (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Initializes the audio context and PortAudio library. Creates ring buffers for capture and playback but does not start streams. Must be called before any other audio functions.

Note
PortAudio initialization is idempotent and thread-safe.
Ring buffers are created but empty after initialization.
Use audio_start_duplex() to begin full-duplex audio I/O.
Warning
Must call audio_destroy() to clean up resources when done.

Definition at line 773 of file lib/audio/audio.c.

773 {
774 if (!ctx) {
775 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx is NULL");
776 }
777
778 SAFE_MEMSET(ctx, sizeof(audio_context_t), 0, sizeof(audio_context_t));
779
780 if (mutex_init(&ctx->state_mutex) != 0) {
781 return SET_ERRNO(ERROR_THREAD, "Failed to initialize audio context mutex");
782 }
783
784 // Initialize PortAudio with reference counting
785 static_mutex_lock(&g_pa_refcount_mutex);
786 if (g_pa_init_refcount == 0) {
787 // Suppress PortAudio backend probe errors (ALSA/JACK/OSS warnings)
788 // These are harmless - PortAudio tries multiple backends until one works
789 int stderr_fd_backup = -1;
790 int devnull_fd = -1;
791#ifndef _WIN32
792 stderr_fd_backup = dup(STDERR_FILENO);
793 devnull_fd = platform_open("/dev/null", O_WRONLY, 0);
794 if (stderr_fd_backup >= 0 && devnull_fd >= 0) {
795 dup2(devnull_fd, STDERR_FILENO);
796 }
797#endif
798
799 PaError err = Pa_Initialize();
800
801 // Restore stderr IMMEDIATELY so real errors are visible
802#ifndef _WIN32
803 if (stderr_fd_backup >= 0) {
804 dup2(stderr_fd_backup, STDERR_FILENO);
805 close(stderr_fd_backup);
806 }
807 if (devnull_fd >= 0) {
808 close(devnull_fd);
809 }
810#endif
811
812 if (err != paNoError) {
813 static_mutex_unlock(&g_pa_refcount_mutex);
815 // stderr is restored, so this error will be visible
816 return SET_ERRNO(ERROR_AUDIO, "Failed to initialize PortAudio: %s", Pa_GetErrorText(err));
817 }
818
819 log_debug("PortAudio initialized successfully (probe warnings suppressed)");
820 }
821 g_pa_init_refcount++;
822 static_mutex_unlock(&g_pa_refcount_mutex);
823
824 // Enumerate all audio devices for debugging
825 int numDevices = Pa_GetDeviceCount();
826 const size_t max_device_info_size = 4096; // Limit total device info size
827 char device_names[max_device_info_size];
828 int offset = 0;
829 for (int i = 0; i < numDevices && offset < (int)sizeof(device_names) - 256; i++) {
830 const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo(i);
831 if (deviceInfo && deviceInfo->name) {
832 int remaining = sizeof(device_names) - offset;
833 if (remaining < 256)
834 break;
835
836 int len = snprintf(&device_names[offset], remaining,
837 "\n Device %d: %s (inputs=%d, outputs=%d, sample_rate=%.0f Hz)%s%s", i, deviceInfo->name,
838 deviceInfo->maxInputChannels, deviceInfo->maxOutputChannels, deviceInfo->defaultSampleRate,
839 (i == Pa_GetDefaultInputDevice()) ? " [DEFAULT INPUT]" : "",
840 (i == Pa_GetDefaultOutputDevice()) ? " [DEFAULT OUTPUT]" : "");
841 if (len > 0 && len < remaining) {
842 offset += len;
843 } else {
844 // Buffer full or error - stop here
845 break;
846 }
847 }
848 }
849 device_names[offset] = '\0';
850 if (offset > 0) {
851 log_debug("PortAudio found %d audio devices:%s", numDevices, device_names);
852 } else {
853 log_warn("PortAudio found no audio devices");
854 }
855
856 // Create capture buffer WITHOUT jitter buffering (PortAudio writes directly from microphone)
858 if (!ctx->capture_buffer) {
859 // Decrement refcount and terminate if this was the only context
860 static_mutex_lock(&g_pa_refcount_mutex);
861 if (g_pa_init_refcount > 0) {
862 g_pa_init_refcount--;
863 if (g_pa_init_refcount == 0) {
864 Pa_Terminate();
865 }
866 }
867 static_mutex_unlock(&g_pa_refcount_mutex);
869 return SET_ERRNO(ERROR_MEMORY, "Failed to create capture buffer");
870 }
871
873 if (!ctx->playback_buffer) {
875 // Decrement refcount and terminate if this was the only context
876 static_mutex_lock(&g_pa_refcount_mutex);
877 if (g_pa_init_refcount > 0) {
878 g_pa_init_refcount--;
879 if (g_pa_init_refcount == 0) {
880 Pa_Terminate();
881 }
882 }
883 static_mutex_unlock(&g_pa_refcount_mutex);
885 return SET_ERRNO(ERROR_MEMORY, "Failed to create playback buffer");
886 }
887
888 ctx->initialized = true;
889 atomic_store(&ctx->shutting_down, false);
890 log_info("Audio system initialized successfully");
891 return ASCIICHAT_OK;
892}
audio_ring_buffer_t * audio_ring_buffer_create(void)
Create a new audio ring buffer (for playback with jitter buffering)
audio_ring_buffer_t * audio_ring_buffer_create_for_capture(void)
Create a new audio ring buffer for capture (without jitter buffering)
#define SAFE_MEMSET(dest, dest_size, ch, count)
Definition common.h:389
@ ERROR_AUDIO
Definition error_codes.h:64
@ ERROR_MEMORY
Definition error_codes.h:53
@ ERROR_THREAD
Definition error_codes.h:95
#define log_debug(...)
Log a DEBUG message.
int mutex_init(mutex_t *mutex)
Initialize a mutex.
int platform_open(const char *pathname, int flags,...)
Safe file open (open replacement)
_Atomic bool shutting_down
True when shutdown started - callback outputs silence.

References ASCIICHAT_OK, audio_ring_buffer_create(), audio_ring_buffer_create_for_capture(), audio_ring_buffer_destroy(), audio_context_t::capture_buffer, ERROR_AUDIO, ERROR_INVALID_PARAM, ERROR_MEMORY, ERROR_THREAD, audio_context_t::initialized, log_debug, log_info, log_warn, mutex_destroy(), mutex_init(), platform_open(), audio_context_t::playback_buffer, SAFE_MEMSET, SET_ERRNO, audio_context_t::shutting_down, and audio_context_t::state_mutex.

Referenced by audio_client_init().

◆ audio_is_supported_sample_rate()

bool audio_is_supported_sample_rate ( uint32_t  sample_rate)

#include <audio.h>

Check if a sample rate is a standard/supported rate.

Supported rates: 8000, 16000, 24000, 32000, 44100, 48000, 96000, 192000

Parameters
sample_rateSample rate in Hz
Returns
true if the sample rate is supported, false otherwise

Usage:

if (!audio_is_supported_sample_rate(batch.sample_rate)) {
log_error("Unsupported sample rate: %u", batch.sample_rate);
return;
}
bool audio_is_supported_sample_rate(uint32_t sample_rate)
Check if a sample rate is a standard/supported rate.

Definition at line 1455 of file lib/audio/audio.c.

1455 {
1456 // List of commonly supported audio sample rates
1457 static const uint32_t supported_rates[] = {
1458 8000, // Telephone quality
1459 16000, // Wideband telephony
1460 24000, // High quality speech
1461 32000, // Good for video
1462 44100, // CD quality (less common in VoIP)
1463 48000, // Standard professional
1464 96000, // High-end professional
1465 192000, // Ultra-high-end mastering
1466 };
1467
1468 const size_t rate_count = sizeof(supported_rates) / sizeof(supported_rates[0]);
1469 for (size_t i = 0; i < rate_count; i++) {
1470 if (sample_rate == supported_rates[i]) {
1471 return true;
1472 }
1473 }
1474
1475 return false;
1476}

Referenced by audio_validate_batch_params().

◆ audio_list_input_devices()

asciichat_error_t audio_list_input_devices ( audio_device_info_t **  out_devices,
unsigned int *  out_count 
)

#include <audio.h>

List available audio input devices (microphones)

Parameters
out_devicesPointer to store allocated array of devices (must not be NULL)
out_countPointer to store device count (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Enumerates all audio input devices (microphones) available on the system. The caller is responsible for freeing the returned array with audio_free_device_list().

Note
This function initializes PortAudio temporarily if not already initialized.
Devices with maxInputChannels > 0 are included.
Warning
Must call audio_free_device_list() to free the returned array.

Definition at line 1347 of file lib/audio/audio.c.

1347 {
1348 return audio_list_devices_internal(out_devices, out_count, true);
1349}

Referenced by action_list_microphones().

◆ audio_list_output_devices()

asciichat_error_t audio_list_output_devices ( audio_device_info_t **  out_devices,
unsigned int *  out_count 
)

#include <audio.h>

List available audio output devices (speakers)

Parameters
out_devicesPointer to store allocated array of devices (must not be NULL)
out_countPointer to store device count (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Enumerates all audio output devices (speakers/headphones) available on the system. The caller is responsible for freeing the returned array with audio_free_device_list().

Note
This function initializes PortAudio temporarily if not already initialized.
Devices with maxOutputChannels > 0 are included.
Warning
Must call audio_free_device_list() to free the returned array.

Definition at line 1351 of file lib/audio/audio.c.

1351 {
1352 return audio_list_devices_internal(out_devices, out_count, false);
1353}

Referenced by action_list_speakers().

◆ audio_parse_batch_header()

asciichat_error_t audio_parse_batch_header ( const void *  data,
size_t  len,
audio_batch_info_t out_batch 
)

#include <audio.h>

Parse an audio batch packet header from raw packet data.

Performs the following validations:

  1. Checks that the data pointer is not NULL
  2. Checks that len is at least sizeof(audio_batch_header_t)
  3. Verifies that batch_count and channels are within reasonable bounds
  4. Unpacks all 32-bit values from network byte order to host byte order
Parameters
dataPointer to packet data (must not be NULL)
lenLength of packet data in bytes
[out]out_batchPointer to audio_batch_info_t struct to fill with parsed data
Returns
ASCIICHAT_OK if parsing succeeded
ERROR_INVALID_PARAM if data is NULL or out_batch is NULL
ERROR_INVALID_PARAM if len is too small for the header

Usage:

asciichat_error_t result = audio_parse_batch_header(packet_data, packet_len, &batch);
if (result != ASCIICHAT_OK) {
log_error("Failed to parse audio batch: %d", result);
return;
}
// Now batch.batch_count, batch.sample_rate, etc. are ready to use
log_debug("Audio batch: %u frames at %u Hz, %u channels",
batch.batch_count, batch.sample_rate, batch.channels);
asciichat_error_t audio_parse_batch_header(const void *data, size_t len, audio_batch_info_t *out_batch)
Parse an audio batch packet header from raw packet data.
Parsed audio batch packet header information.
uint32_t batch_count
Number of audio frames in this batch.
uint32_t channels
Number of channels (1=mono, 2=stereo)
uint32_t sample_rate
Sample rate in Hz (e.g., 48000)

Definition at line 1389 of file lib/audio/audio.c.

1389 {
1390 if (!data) {
1391 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch header data pointer is NULL");
1392 }
1393
1394 if (!out_batch) {
1395 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch info output pointer is NULL");
1396 }
1397
1398 if (len < sizeof(audio_batch_packet_t)) {
1399 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch header too small (len=%zu, expected=%zu)", len,
1400 sizeof(audio_batch_packet_t));
1401 }
1402
1403 const audio_batch_packet_t *batch_header = (const audio_batch_packet_t *)data;
1404
1405 // Unpack network byte order values to host byte order
1406 out_batch->batch_count = ntohl(batch_header->batch_count);
1407 out_batch->total_samples = ntohl(batch_header->total_samples);
1408 out_batch->sample_rate = ntohl(batch_header->sample_rate);
1409 out_batch->channels = ntohl(batch_header->channels);
1410
1411 return ASCIICHAT_OK;
1412}
uint32_t channels
Number of audio channels (1=mono, 2=stereo)
Definition packet.h:804
uint32_t batch_count
Number of audio chunks in this batch (usually AUDIO_BATCH_COUNT = 32)
Definition packet.h:798
uint32_t sample_rate
Sample rate in Hz (e.g., 44100, 48000)
Definition packet.h:802
uint32_t total_samples
Total audio samples across all chunks (typically 8192)
Definition packet.h:800
uint32_t total_samples
Total number of samples across all frames.
Audio batch packet structure (Packet Type 28)
Definition packet.h:796

References ASCIICHAT_OK, audio_batch_info_t::batch_count, audio_batch_packet_t::batch_count, audio_batch_info_t::channels, audio_batch_packet_t::channels, ERROR_INVALID_PARAM, audio_batch_info_t::sample_rate, audio_batch_packet_t::sample_rate, SET_ERRNO, audio_batch_info_t::total_samples, and audio_batch_packet_t::total_samples.

Referenced by handle_audio_batch_packet().

◆ audio_read_samples()

asciichat_error_t audio_read_samples ( audio_context_t ctx,
float *  buffer,
int  num_samples 
)

#include <audio.h>

Read captured audio samples from capture buffer.

Parameters
ctxAudio context (must not be NULL)
bufferOutput buffer for audio samples (must not be NULL)
num_samplesNumber of samples to read (must be > 0)
Returns
ASCIICHAT_OK on success, error code on failure

Reads audio samples from the capture ring buffer. Samples are in floating point format (-1.0 to 1.0 range). This function is non-blocking and returns whatever samples are available up to num_samples.

Note
Actual number of samples read may be less than num_samples if buffer doesn't contain enough samples.
Samples are removed from the buffer after reading.
Thread-safe: Can be called from any thread.
Warning
Buffer must be large enough to hold num_samples float values.

Definition at line 1156 of file lib/audio/audio.c.

1156 {
1157 if (!ctx || !ctx->initialized || !buffer || num_samples <= 0) {
1158 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, buffer=%p, num_samples=%d", ctx, buffer,
1159 num_samples);
1160 }
1161
1162 // audio_ring_buffer_read now returns number of samples read, not error code
1163 int samples_read = audio_ring_buffer_read(ctx->capture_buffer, buffer, num_samples);
1164 return (samples_read >= 0) ? ASCIICHAT_OK : ERROR_AUDIO;
1165}
size_t audio_ring_buffer_read(audio_ring_buffer_t *rb, float *data, size_t samples)
Read audio samples from ring buffer.

References ASCIICHAT_OK, audio_ring_buffer_read(), audio_context_t::capture_buffer, ERROR_AUDIO, ERROR_INVALID_PARAM, audio_context_t::initialized, and SET_ERRNO.

◆ audio_ring_buffer_available_read()

size_t audio_ring_buffer_available_read ( audio_ring_buffer_t rb)

#include <audio.h>

Get number of samples available for reading.

Parameters
rbAudio ring buffer (must not be NULL)
Returns
Number of samples available to read

Returns the current number of samples available in the ring buffer for reading. Useful for determining if enough samples are available before calling audio_ring_buffer_read().

Note
Thread-safe: Can be called from any thread.
Value may change immediately after return due to concurrent writes.

Definition at line 747 of file lib/audio/audio.c.

747 {
748 if (!rb)
749 return 0;
750
751 // LOCK-FREE: Load indices with proper memory ordering
752 // Use acquire for write_index to see writer's updates
753 // Use relaxed for read_index (our own index)
754 unsigned int write_idx = atomic_load_explicit(&rb->write_index, memory_order_acquire);
755 unsigned int read_idx = atomic_load_explicit(&rb->read_index, memory_order_relaxed);
756
757 if (write_idx >= read_idx) {
758 return write_idx - read_idx;
759 }
760
761 return AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
762}
#define AUDIO_RING_BUFFER_SIZE
Audio ring buffer size in samples (192000 samples = 4 seconds @ 48kHz)
Definition ringbuffer.h:135
atomic_uint read_index
Read index (consumer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:214
atomic_uint write_index
Write index (producer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:212

References AUDIO_RING_BUFFER_SIZE, audio_ring_buffer::read_index, and audio_ring_buffer::write_index.

Referenced by audio_process_received_samples(), audio_ring_buffer_available_write(), client_audio_render_thread(), and mixer_process_excluding_source().

◆ audio_ring_buffer_available_write()

size_t audio_ring_buffer_available_write ( audio_ring_buffer_t rb)

#include <audio.h>

Get number of sample slots available for writing.

Parameters
rbAudio ring buffer (must not be NULL)
Returns
Number of sample slots available for writing

Returns the current number of sample slots available in the ring buffer for writing. Useful for determining if enough space is available before calling audio_ring_buffer_write().

Note
Thread-safe: Can be called from any thread.
Value may change immediately after return due to concurrent reads.

Definition at line 764 of file lib/audio/audio.c.

764 {
765 if (!rb) {
766 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: rb is NULL");
767 return 0;
768 }
769
771}
size_t audio_ring_buffer_available_read(audio_ring_buffer_t *rb)
Get number of samples available for reading.

References audio_ring_buffer_available_read(), AUDIO_RING_BUFFER_SIZE, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ audio_ring_buffer_clear()

void audio_ring_buffer_clear ( audio_ring_buffer_t rb)

#include <audio.h>

Clear all audio samples from ring buffer.

Parameters
rbAudio ring buffer (must not be NULL)

Resets the ring buffer to empty state, clearing all samples. Used during shutdown to prevent stale audio from playing.

Definition at line 447 of file lib/audio/audio.c.

447 {
448 if (!rb)
449 return;
450
451 mutex_lock(&rb->mutex);
452 // Reset buffer to empty state (no audio to play = silence at shutdown)
453 rb->write_index = 0;
454 rb->read_index = 0;
455 rb->last_sample = 0.0f;
456 // Clear the actual data to zeros to prevent any stale audio
457 SAFE_MEMSET(rb->data, sizeof(rb->data), 0, sizeof(rb->data));
458 mutex_unlock(&rb->mutex);
459}
float last_sample
Last sample value for smooth fade-out during underrun - NOT atomic (only written by reader)
Definition ringbuffer.h:222
mutex_t mutex
Mutex for SLOW PATH only (clear/destroy operations, not regular read/write)
Definition ringbuffer.h:228
float data[192000]
Audio sample data buffer.
Definition ringbuffer.h:210

References audio_ring_buffer::data, audio_ring_buffer::last_sample, audio_ring_buffer::mutex, mutex_lock, mutex_unlock, audio_ring_buffer::read_index, SAFE_MEMSET, and audio_ring_buffer::write_index.

Referenced by audio_stop_duplex().

◆ audio_ring_buffer_create()

audio_ring_buffer_t * audio_ring_buffer_create ( void  )

#include <audio.h>

Create a new audio ring buffer (for playback with jitter buffering)

Returns
Pointer to newly created audio ring buffer, or NULL on failure

Creates a new audio ring buffer for storing audio samples. The buffer has a fixed size defined by AUDIO_RING_BUFFER_SIZE and is optimized for real-time audio streaming with jitter buffering support (enabled by default).

Note
The ring buffer is thread-safe and can be used concurrently by capture and playback threads.
Ring buffer includes jitter buffer threshold for network latency compensation.
Use audio_ring_buffer_create_for_capture() for capture buffers.
Warning
Must call audio_ring_buffer_destroy() to free resources.

Definition at line 431 of file lib/audio/audio.c.

431 {
432 return audio_ring_buffer_create_internal(true); // Default: enable jitter buffering for playback
433}

Referenced by audio_init().

◆ audio_ring_buffer_create_for_capture()

audio_ring_buffer_t * audio_ring_buffer_create_for_capture ( void  )

#include <audio.h>

Create a new audio ring buffer for capture (without jitter buffering)

Returns
Pointer to newly created audio ring buffer, or NULL on failure

Creates an audio ring buffer optimized for direct microphone input from PortAudio. Unlike playback buffers, capture buffers disable jitter buffering since PortAudio writes directly from the microphone with no network latency.

Note
The ring buffer is thread-safe and can be used concurrently by capture and playback threads.
Jitter buffering is disabled for capture buffers.
Warning
Must call audio_ring_buffer_destroy() to free resources.

Definition at line 435 of file lib/audio/audio.c.

435 {
436 return audio_ring_buffer_create_internal(false); // Disable jitter buffering for capture
437}

Referenced by audio_init(), and audio_start_duplex().

◆ audio_ring_buffer_destroy()

void audio_ring_buffer_destroy ( audio_ring_buffer_t rb)

#include <audio.h>

Destroy an audio ring buffer.

Parameters
rbAudio ring buffer to destroy (can be NULL)

Destroys an audio ring buffer and frees all associated resources. Safe to call multiple times or with NULL pointer.

Note
All samples in the buffer are discarded.

Definition at line 439 of file lib/audio/audio.c.

439 {
440 if (!rb)
441 return;
442
443 mutex_destroy(&rb->mutex);
444 buffer_pool_free(NULL, rb, sizeof(audio_ring_buffer_t));
445}
void buffer_pool_free(buffer_pool_t *pool, void *data, size_t size)
Free a buffer back to the pool (lock-free)

References buffer_pool_free(), audio_ring_buffer::mutex, and mutex_destroy().

Referenced by audio_destroy(), audio_init(), audio_start_duplex(), audio_stop_duplex(), and cleanup_client_media_buffers().

◆ audio_ring_buffer_peek()

size_t audio_ring_buffer_peek ( audio_ring_buffer_t rb,
float *  data,
size_t  samples 
)

#include <audio.h>

Peek at available samples without consuming them (for AEC3 render signal)

This function reads samples from the jitter buffer WITHOUT advancing the read_index. Used to feed audio to AEC3 for echo cancellation even during jitter buffer fill period.

Parameters
rbRing buffer to peek from
dataOutput buffer for samples
samplesNumber of samples to peek
Returns
Number of samples actually peeked (may be less than requested)

Definition at line 710 of file lib/audio/audio.c.

710 {
711 if (!rb || !data || samples <= 0) {
712 return 0;
713 }
714
715 // LOCK-FREE: Load indices with proper memory ordering
716 unsigned int write_idx = atomic_load_explicit(&rb->write_index, memory_order_acquire);
717 unsigned int read_idx = atomic_load_explicit(&rb->read_index, memory_order_relaxed);
718
719 // Calculate available samples
720 size_t available;
721 if (write_idx >= read_idx) {
722 available = write_idx - read_idx;
723 } else {
724 available = AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
725 }
726
727 size_t to_peek = (samples > available) ? available : samples;
728
729 if (to_peek == 0) {
730 return 0;
731 }
732
733 // Copy samples in chunks (handle wraparound)
734 size_t first_chunk = (read_idx + to_peek <= AUDIO_RING_BUFFER_SIZE) ? to_peek : (AUDIO_RING_BUFFER_SIZE - read_idx);
735
736 SAFE_MEMCPY(data, first_chunk * sizeof(float), rb->data + read_idx, first_chunk * sizeof(float));
737
738 if (first_chunk < to_peek) {
739 // Wraparound: copy second chunk from beginning of buffer
740 size_t second_chunk = to_peek - first_chunk;
741 SAFE_MEMCPY(data + first_chunk, second_chunk * sizeof(float), rb->data, second_chunk * sizeof(float));
742 }
743
744 return to_peek;
745}
#define SAFE_MEMCPY(dest, dest_size, src, count)
Definition common.h:388

References AUDIO_RING_BUFFER_SIZE, audio_ring_buffer::data, audio_ring_buffer::read_index, SAFE_MEMCPY, and audio_ring_buffer::write_index.

◆ audio_ring_buffer_read()

size_t audio_ring_buffer_read ( audio_ring_buffer_t rb,
float *  data,
size_t  samples 
)

#include <audio.h>

Read audio samples from ring buffer.

Parameters
rbAudio ring buffer (must not be NULL)
dataOutput buffer for audio samples (must not be NULL)
samplesMaximum number of samples to read
Returns
Number of samples actually read (may be less than requested)

Reads audio samples from the ring buffer. Samples are in floating point format (-1.0 to 1.0 range). This function is non-blocking and returns whatever samples are available up to the requested count.

Note
Actual number of samples read may be less than samples if buffer doesn't contain enough samples.
Samples are removed from the buffer after reading.
Thread-safe: Can be called from multiple threads simultaneously.
Warning
Data buffer must be large enough to hold samples float values.

Definition at line 549 of file lib/audio/audio.c.

549 {
550 if (!rb || !data || samples <= 0) {
551 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: rb=%p, data=%p, samples=%d", rb, data, samples);
552 return 0; // Return 0 samples read on error
553 }
554
555 // LOCK-FREE: Load indices with proper memory ordering
556 // - Load writer's write_index with acquire (see writer's data updates)
557 // - Load our own read_index with relaxed (no sync needed with ourselves)
558 unsigned int write_idx = atomic_load_explicit(&rb->write_index, memory_order_acquire);
559 unsigned int read_idx = atomic_load_explicit(&rb->read_index, memory_order_relaxed);
560
561 // Calculate available samples
562 size_t available;
563 if (write_idx >= read_idx) {
564 available = write_idx - read_idx;
565 } else {
566 available = AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
567 }
568
569 // LOCK-FREE: Load jitter buffer state with acquire ordering
570 bool jitter_filled = atomic_load_explicit(&rb->jitter_buffer_filled, memory_order_acquire);
571 int crossfade_remaining = atomic_load_explicit(&rb->crossfade_samples_remaining, memory_order_acquire);
572 bool fade_in = atomic_load_explicit(&rb->crossfade_fade_in, memory_order_acquire);
573
574 // Jitter buffer: don't read until initial fill threshold is reached
575 // (only for playback buffers - capture buffers have jitter_buffer_enabled = false)
576 if (!jitter_filled && rb->jitter_buffer_enabled) {
577 // First, check if we're in the middle of a fade-out that needs to continue
578 // This happens when fade-out spans multiple buffer reads
579 if (!fade_in && crossfade_remaining > 0) {
580 // Continue fade-out from where we left off
581 int fade_start = AUDIO_CROSSFADE_SAMPLES - crossfade_remaining;
582 size_t fade_samples = (samples < (size_t)crossfade_remaining) ? samples : (size_t)crossfade_remaining;
583 float last = rb->last_sample; // NOT atomic - only written by reader
584 for (size_t i = 0; i < fade_samples; i++) {
585 float fade_factor = 1.0f - ((float)(fade_start + (int)i) / (float)AUDIO_CROSSFADE_SAMPLES);
586 data[i] = last * fade_factor;
587 }
588 // Fill rest with silence
589 for (size_t i = fade_samples; i < samples; i++) {
590 data[i] = 0.0f;
591 }
592 // Update crossfade state atomically
593 atomic_store_explicit(&rb->crossfade_samples_remaining, crossfade_remaining - (int)fade_samples,
594 memory_order_release);
595 if (crossfade_remaining - (int)fade_samples <= 0) {
596 rb->last_sample = 0.0f;
597 }
598
599 return samples; // Return full buffer (with continued fade-out)
600 }
601
602 // Check if we've accumulated enough samples to start playback
603 if (available >= AUDIO_JITTER_BUFFER_THRESHOLD) {
604 atomic_store_explicit(&rb->jitter_buffer_filled, true, memory_order_release);
605 atomic_store_explicit(&rb->crossfade_samples_remaining, AUDIO_CROSSFADE_SAMPLES, memory_order_release);
606 atomic_store_explicit(&rb->crossfade_fade_in, true, memory_order_release);
607 log_info("Jitter buffer filled (%zu samples), starting playback with fade-in", available);
608 // Reload state for processing below
609 jitter_filled = true;
610 crossfade_remaining = AUDIO_CROSSFADE_SAMPLES;
611 fade_in = true;
612 } else {
613 // Log buffer fill progress every second
614 log_debug_every(1000000, "Jitter buffer filling: %zu/%d samples (%.1f%%)", available,
616 return 0; // Return silence until buffer is filled
617 }
618 }
619
620 // Periodic buffer health logging (every 5 seconds when healthy)
621 static unsigned int health_log_counter = 0;
622 if (++health_log_counter % 250 == 0) { // ~5 seconds at 50Hz callback rate
623 unsigned int underruns = atomic_load_explicit(&rb->underrun_count, memory_order_relaxed);
624 log_debug("Buffer health: %zu/%d samples (%.1f%%), underruns=%u", available, AUDIO_RING_BUFFER_SIZE,
625 (100.0f * available) / AUDIO_RING_BUFFER_SIZE, underruns);
626 }
627
628 // Low buffer handling: DON'T pause playback - continue reading what's available
629 // and fill the rest with silence. Pausing causes a feedback loop where:
630 // 1. Underrun -> pause reading -> buffer overflows from incoming samples
631 // 2. Threshold reached -> resume reading -> drains too fast -> underrun again
632 //
633 // Instead: always consume samples to prevent overflow, use silence for missing data
634 if (rb->jitter_buffer_enabled && available < AUDIO_JITTER_LOW_WATER_MARK) {
635 unsigned int underrun_count = atomic_fetch_add_explicit(&rb->underrun_count, 1, memory_order_relaxed) + 1;
637 "Audio buffer low #%u: only %zu samples available (low water mark: %d), padding with silence",
638 underrun_count, available, AUDIO_JITTER_LOW_WATER_MARK);
639 // Don't set jitter_buffer_filled = false - keep reading to prevent overflow
640 }
641
642 size_t to_read = (samples > available) ? available : samples;
643
644 // Optimize: copy in chunks instead of one sample at a time
645 size_t remaining = AUDIO_RING_BUFFER_SIZE - read_idx;
646
647 if (to_read <= remaining) {
648 // Can copy in one chunk
649 SAFE_MEMCPY(data, to_read * sizeof(float), &rb->data[read_idx], to_read * sizeof(float));
650 } else {
651 // Need to wrap around - copy in two chunks
652 SAFE_MEMCPY(data, remaining * sizeof(float), &rb->data[read_idx], remaining * sizeof(float));
653 SAFE_MEMCPY(&data[remaining], (to_read - remaining) * sizeof(float), &rb->data[0],
654 (to_read - remaining) * sizeof(float));
655 }
656
657 // LOCK-FREE: Store new read_index with release ordering
658 // This ensures all data reads above complete before the index update
659 unsigned int new_read_idx = (read_idx + (unsigned int)to_read) % AUDIO_RING_BUFFER_SIZE;
660 atomic_store_explicit(&rb->read_index, new_read_idx, memory_order_release);
661
662 // Apply fade-in if recovering from underrun
663 if (fade_in && crossfade_remaining > 0) {
664 int fade_start = AUDIO_CROSSFADE_SAMPLES - crossfade_remaining;
665 size_t fade_samples = (to_read < (size_t)crossfade_remaining) ? to_read : (size_t)crossfade_remaining;
666
667 for (size_t i = 0; i < fade_samples; i++) {
668 float fade_factor = (float)(fade_start + (int)i + 1) / (float)AUDIO_CROSSFADE_SAMPLES;
669 data[i] *= fade_factor;
670 }
671
672 int new_crossfade_remaining = crossfade_remaining - (int)fade_samples;
673 atomic_store_explicit(&rb->crossfade_samples_remaining, new_crossfade_remaining, memory_order_release);
674 if (new_crossfade_remaining <= 0) {
675 atomic_store_explicit(&rb->crossfade_fade_in, false, memory_order_release);
676 log_debug("Audio fade-in complete");
677 }
678 }
679
680 // Save last sample for potential fade-out
681 // Note: only update if we actually read some data
682 // This is NOT atomic - only the reader thread writes this
683 if (to_read > 0) {
684 rb->last_sample = data[to_read - 1];
685 }
686
687 // Fill any remaining samples with pure silence if we couldn't read enough
688 // NOTE: Previous code applied fade-out from last sample, but this created
689 // audible "little extra sounds in the gaps" during frequent underruns.
690 // Pure silence is less disruptive than artificial fade artifacts.
691 if (to_read < samples) {
692 size_t silence_samples = samples - to_read;
693 SAFE_MEMSET(data + to_read, silence_samples * sizeof(float), 0, silence_samples * sizeof(float));
694 }
695
696 return samples; // Always return full buffer (with silence padding if needed)
697}
#define LOG_RATE_FAST
Log rate limit: 1 second (1,000,000 microseconds)
Definition log_rates.h:26
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
#define AUDIO_JITTER_BUFFER_THRESHOLD
Jitter buffer threshold - samples needed before starting playback.
Definition ringbuffer.h:148
#define AUDIO_JITTER_LOW_WATER_MARK
Low water mark - warn when available drops below this.
Definition ringbuffer.h:159
#define AUDIO_CROSSFADE_SAMPLES
Crossfade duration in samples for smooth underrun recovery.
Definition ringbuffer.h:189
atomic_bool jitter_buffer_filled
True after initial jitter buffer fill.
Definition ringbuffer.h:216
atomic_uint underrun_count
Count of underrun events for diagnostics.
Definition ringbuffer.h:224
atomic_int crossfade_samples_remaining
Samples remaining in crossfade (0 = no crossfade active)
Definition ringbuffer.h:218
bool jitter_buffer_enabled
Whether jitter buffering is enabled (false for capture, true for playback)
Definition ringbuffer.h:226
atomic_bool crossfade_fade_in
True if we're fading in (recovering from underrun)
Definition ringbuffer.h:220

References AUDIO_CROSSFADE_SAMPLES, AUDIO_JITTER_BUFFER_THRESHOLD, AUDIO_JITTER_LOW_WATER_MARK, AUDIO_RING_BUFFER_SIZE, audio_ring_buffer::crossfade_fade_in, audio_ring_buffer::crossfade_samples_remaining, audio_ring_buffer::data, ERROR_INVALID_PARAM, audio_ring_buffer::jitter_buffer_enabled, audio_ring_buffer::jitter_buffer_filled, audio_ring_buffer::last_sample, log_debug, log_debug_every, log_info, LOG_RATE_FAST, log_warn_every, audio_ring_buffer::read_index, SAFE_MEMCPY, SAFE_MEMSET, SET_ERRNO, audio_ring_buffer::underrun_count, and audio_ring_buffer::write_index.

Referenced by audio_read_samples(), client_audio_render_thread(), mixer_process(), and mixer_process_excluding_source().

◆ audio_ring_buffer_write()

asciichat_error_t audio_ring_buffer_write ( audio_ring_buffer_t rb,
const float *  data,
int  samples 
)

#include <audio.h>

Write audio samples to ring buffer.

Parameters
rbAudio ring buffer (must not be NULL)
dataAudio samples to write (must not be NULL)
samplesNumber of samples to write
Returns
ASCIICHAT_OK on success, error code on failure

Writes audio samples to the ring buffer. Samples should be in floating point format (-1.0 to 1.0 range). This function is non-blocking and writes as many samples as buffer space allows.

Note
Actual number of samples written may be less than requested if buffer is full or nearly full.
Thread-safe: Can be called from multiple threads simultaneously.
Warning
Data must contain at least samples float values.

Definition at line 461 of file lib/audio/audio.c.

461 {
462 if (!rb || !data || samples <= 0)
463 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: rb=%p, data=%p, samples=%d", rb, data, samples);
464
465 // Validate samples doesn't exceed our buffer size
466 if (samples > AUDIO_RING_BUFFER_SIZE) {
467 return SET_ERRNO(ERROR_BUFFER, "Attempted to write %d samples, but buffer size is only %d", samples,
469 }
470
471 // LOCK-FREE: Load indices with proper memory ordering
472 // - Load our own write_index with relaxed (no sync needed with ourselves)
473 // - Load reader's read_index with acquire (see reader's updates to free space)
474 unsigned int write_idx = atomic_load_explicit(&rb->write_index, memory_order_relaxed);
475 unsigned int read_idx = atomic_load_explicit(&rb->read_index, memory_order_acquire);
476
477 // Calculate current buffer level (how many samples are buffered)
478 int buffer_level;
479 if (write_idx >= read_idx) {
480 buffer_level = (int)(write_idx - read_idx);
481 } else {
482 buffer_level = AUDIO_RING_BUFFER_SIZE - (int)(read_idx - write_idx);
483 }
484 int available = AUDIO_RING_BUFFER_SIZE - buffer_level;
485
486 // HIGH WATER MARK: Drop OLD samples to prevent latency accumulation
487 // This is critical for real-time audio - we always want the NEWEST data
488 // ALWAYS apply high-water-mark on write, regardless of jitter_buffer_enabled
489 // jitter_buffer_enabled only controls READ side (whether to wait for threshold)
490 // On WRITE side, we ALWAYS want to drop old samples to bound latency
491 if (buffer_level + samples > AUDIO_JITTER_HIGH_WATER_MARK) {
492 // Calculate how many old samples to drop to bring buffer to target level
493 int excess = (buffer_level + samples) - AUDIO_JITTER_TARGET_LEVEL;
494 if (excess > 0) {
495 // Advance read_index to drop old samples
496 // Note: This is safe because the reader checks for underrun and handles it gracefully
497 unsigned int new_read_idx = (read_idx + (unsigned int)excess) % AUDIO_RING_BUFFER_SIZE;
498 atomic_store_explicit(&rb->read_index, new_read_idx, memory_order_release);
499
501 "Audio buffer high water mark exceeded: dropping %d OLD samples to reduce latency "
502 "(buffer was %d, target %d)",
503 excess, buffer_level, AUDIO_JITTER_TARGET_LEVEL);
504
505 // Recalculate available space after dropping old samples
506 read_idx = new_read_idx;
507 buffer_level = AUDIO_JITTER_TARGET_LEVEL - samples;
508 if (buffer_level < 0)
509 buffer_level = 0;
510 available = AUDIO_RING_BUFFER_SIZE - buffer_level;
511 }
512 }
513
514 // Now write the new samples - should always have enough space after above
515 int samples_to_write = samples;
516 if (samples > available) {
517 // This should rarely happen after the high water mark logic above
518 int samples_dropped = samples - available;
519 samples_to_write = available;
520 log_warn_every(LOG_RATE_FAST, "Audio buffer overflow: dropping %d of %d incoming samples (buffer_used=%d/%d)",
521 samples_dropped, samples, AUDIO_RING_BUFFER_SIZE - available, AUDIO_RING_BUFFER_SIZE);
522 }
523
524 // Write only the samples that fit (preserves existing data integrity)
525 if (samples_to_write > 0) {
526 int remaining = AUDIO_RING_BUFFER_SIZE - (int)write_idx;
527
528 if (samples_to_write <= remaining) {
529 // Can copy in one chunk
530 SAFE_MEMCPY(&rb->data[write_idx], samples_to_write * sizeof(float), data, samples_to_write * sizeof(float));
531 } else {
532 // Need to wrap around - copy in two chunks
533 SAFE_MEMCPY(&rb->data[write_idx], remaining * sizeof(float), data, remaining * sizeof(float));
534 SAFE_MEMCPY(&rb->data[0], (samples_to_write - remaining) * sizeof(float), &data[remaining],
535 (samples_to_write - remaining) * sizeof(float));
536 }
537
538 // LOCK-FREE: Store new write_index with release ordering
539 // This ensures all data writes above are visible before the index update
540 unsigned int new_write_idx = (write_idx + (unsigned int)samples_to_write) % AUDIO_RING_BUFFER_SIZE;
541 atomic_store_explicit(&rb->write_index, new_write_idx, memory_order_release);
542 }
543
544 // Note: jitter buffer fill check is now done in read function for better control
545
546 return ASCIICHAT_OK; // Success
547}
@ ERROR_BUFFER
Definition error_codes.h:96
#define AUDIO_JITTER_HIGH_WATER_MARK
High water mark - drop OLD samples when buffer exceeds this.
Definition ringbuffer.h:172
#define AUDIO_JITTER_TARGET_LEVEL
Target buffer level after dropping old samples.
Definition ringbuffer.h:182

References ASCIICHAT_OK, AUDIO_JITTER_HIGH_WATER_MARK, AUDIO_JITTER_TARGET_LEVEL, AUDIO_RING_BUFFER_SIZE, audio_ring_buffer::data, ERROR_BUFFER, ERROR_INVALID_PARAM, LOG_RATE_FAST, log_warn_every, audio_ring_buffer::read_index, SAFE_MEMCPY, SET_ERRNO, and audio_ring_buffer::write_index.

Referenced by audio_write_samples(), handle_audio_batch_packet(), handle_audio_opus_batch_packet(), handle_audio_opus_packet(), and handle_audio_packet().

◆ audio_set_pipeline()

void audio_set_pipeline ( audio_context_t ctx,
void *  pipeline 
)

#include <audio.h>

Set audio pipeline for echo cancellation.

Parameters
ctxAudio context (must not be NULL)
pipelineClient audio pipeline pointer (opaque, can be NULL)

Associates an audio pipeline with the context for echo cancellation. The pipeline is fed playback samples from the output callback, allowing AEC3 to properly synchronize render and capture signals.

Note
Safe to call at any time, including before/after audio streams are started.
Pass NULL to disable pipeline integration.

Definition at line 927 of file lib/audio/audio.c.

927 {
928 if (!ctx)
929 return;
930 ctx->audio_pipeline = pipeline;
931}
void * audio_pipeline
Client audio pipeline for AEC3 echo cancellation (opaque pointer)

References audio_context_t::audio_pipeline.

Referenced by audio_cleanup(), and audio_client_init().

◆ audio_set_realtime_priority()

asciichat_error_t audio_set_realtime_priority ( void  )

#include <audio.h>

Request real-time priority for current thread.

Returns
ASCIICHAT_OK on success, error code on failure

Requests real-time priority scheduling for the current thread. This reduces audio latency and glitches by ensuring audio threads get scheduled promptly. Platform-specific implementation (SCHED_FIFO on Linux, thread priority on Windows/macOS).

Note
This function should be called from audio capture/playback threads.
On some platforms, real-time priority requires appropriate permissions.
If real-time priority cannot be obtained, the function may succeed but use a lower priority level.
Warning
Real-time priority can cause system instability if misused. Only use in audio threads that process data quickly.
LIMITATION: This function only affects the calling thread. When used with PortAudio, the audio callbacks run in PortAudio's internal threads, not the thread that calls audio_start_duplex. Some PortAudio backends (WASAPI, CoreAudio) automatically use real-time priority for their callback threads. For other backends, this function has limited effect when called from the main thread.

Definition at line 1375 of file lib/audio/audio.c.

1375 {
1376 // Delegate to platform abstraction layer
1378 if (result == ASCIICHAT_OK) {
1379 log_info("✓ Audio thread real-time priority set successfully");
1380 }
1381 return result;
1382}
asciichat_error_t asciichat_thread_set_realtime_priority(void)
Set the current thread to real-time priority.

References ASCIICHAT_OK, asciichat_thread_set_realtime_priority(), and log_info.

Referenced by audio_start_duplex().

◆ audio_start_duplex()

asciichat_error_t audio_start_duplex ( audio_context_t ctx)

#include <audio.h>

Start full-duplex audio (simultaneous capture and playback)

Parameters
ctxAudio context (must not be NULL, must be initialized)
Returns
ASCIICHAT_OK on success, error code on failure

Opens a single PortAudio stream that handles BOTH input (microphone) and output (speakers) simultaneously. This is CRITICAL for proper AEC3 echo cancellation because:

  • Single callback receives both render and capture samples at the EXACT same instant
  • No timing mismatch between input/output callbacks
  • No ring buffer delay for AEC3 reference signal
  • Perfect synchronization for echo cancellation

The duplex callback:

  1. Reads from playback_buffer → outputs to speakers
  2. Processes capture through AEC3 (using render as reference)
  3. Writes processed capture to capture_buffer for encoder thread
Note
Real-time priority is automatically requested for callback thread.
If already running, this function has no effect.
Warning
Both input and output devices must be available.

Definition at line 933 of file lib/audio/audio.c.

933 {
934 if (!ctx || !ctx->initialized) {
935 return SET_ERRNO(ERROR_INVALID_STATE, "Audio context not initialized");
936 }
937
938 mutex_lock(&ctx->state_mutex);
939
940 // Already running?
941 if (ctx->duplex_stream || ctx->input_stream || ctx->output_stream) {
943 return ASCIICHAT_OK;
944 }
945
946 // Setup input parameters
947 PaStreamParameters inputParams;
948 if (GET_OPTION(microphone_index) >= 0) {
949 inputParams.device = GET_OPTION(microphone_index);
950 } else {
951 inputParams.device = Pa_GetDefaultInputDevice();
952 }
953
954 if (inputParams.device == paNoDevice) {
956 return SET_ERRNO(ERROR_AUDIO, "No input device available");
957 }
958
959 const PaDeviceInfo *inputInfo = Pa_GetDeviceInfo(inputParams.device);
960 if (!inputInfo) {
962 return SET_ERRNO(ERROR_AUDIO, "Input device info not found");
963 }
964
965 inputParams.channelCount = AUDIO_CHANNELS;
966 inputParams.sampleFormat = paFloat32;
967 inputParams.suggestedLatency = inputInfo->defaultLowInputLatency;
968 inputParams.hostApiSpecificStreamInfo = NULL;
969
970 // Setup output parameters
971 PaStreamParameters outputParams;
972 if (GET_OPTION(speakers_index) >= 0) {
973 outputParams.device = GET_OPTION(speakers_index);
974 } else {
975 outputParams.device = Pa_GetDefaultOutputDevice();
976 }
977
978 if (outputParams.device == paNoDevice) {
980 return SET_ERRNO(ERROR_AUDIO, "No output device available");
981 }
982
983 const PaDeviceInfo *outputInfo = Pa_GetDeviceInfo(outputParams.device);
984 if (!outputInfo) {
986 return SET_ERRNO(ERROR_AUDIO, "Output device info not found");
987 }
988
989 outputParams.channelCount = AUDIO_CHANNELS;
990 outputParams.sampleFormat = paFloat32;
991 outputParams.suggestedLatency = outputInfo->defaultLowOutputLatency;
992 outputParams.hostApiSpecificStreamInfo = NULL;
993
994 // Store device rates for diagnostics
995 ctx->input_device_rate = inputInfo->defaultSampleRate;
996 ctx->output_device_rate = outputInfo->defaultSampleRate;
997
998 log_info("Opening audio:");
999 log_info(" Input: %s (%.0f Hz)", inputInfo->name, inputInfo->defaultSampleRate);
1000 log_info(" Output: %s (%.0f Hz)", outputInfo->name, outputInfo->defaultSampleRate);
1001
1002 // Check if sample rates differ - ALSA full-duplex doesn't handle this well
1003 bool rates_differ = (inputInfo->defaultSampleRate != outputInfo->defaultSampleRate);
1004 bool try_separate = rates_differ;
1005 PaError err = paNoError;
1006
1007 if (!try_separate) {
1008 // Try full-duplex first (preferred - perfect AEC3 timing)
1009 err = Pa_OpenStream(&ctx->duplex_stream, &inputParams, &outputParams, AUDIO_SAMPLE_RATE, AUDIO_FRAMES_PER_BUFFER,
1010 paClipOff, duplex_callback, ctx);
1011
1012 if (err == paNoError) {
1013 err = Pa_StartStream(ctx->duplex_stream);
1014 if (err != paNoError) {
1015 Pa_CloseStream(ctx->duplex_stream);
1016 ctx->duplex_stream = NULL;
1017 log_warn("Full-duplex stream failed to start: %s", Pa_GetErrorText(err));
1018 try_separate = true;
1019 }
1020 } else {
1021 log_warn("Full-duplex stream failed to open: %s", Pa_GetErrorText(err));
1022 try_separate = true;
1023 }
1024 }
1025
1026 if (try_separate) {
1027 // Fall back to separate streams (needed when sample rates differ)
1028 log_info("Using separate input/output streams (sample rates differ: %.0f vs %.0f Hz)", inputInfo->defaultSampleRate,
1029 outputInfo->defaultSampleRate);
1030 log_info(" Will resample: buffer at %.0f Hz → output at %.0f Hz", (double)AUDIO_SAMPLE_RATE,
1031 outputInfo->defaultSampleRate);
1032
1033 // Store the internal sample rate (buffer rate)
1035
1036 // Create render buffer for AEC3 reference synchronization
1038 if (!ctx->render_buffer) {
1040 return SET_ERRNO(ERROR_MEMORY, "Failed to create render buffer");
1041 }
1042
1043 // Open output stream at NATIVE device rate - we'll resample from 48kHz buffer in callback
1044 err = Pa_OpenStream(&ctx->output_stream, NULL, &outputParams, outputInfo->defaultSampleRate,
1045 AUDIO_FRAMES_PER_BUFFER, paClipOff, output_callback, ctx);
1046 if (err != paNoError) {
1048 ctx->render_buffer = NULL;
1050 return SET_ERRNO(ERROR_AUDIO, "Failed to open output stream: %s", Pa_GetErrorText(err));
1051 }
1052
1053 // Open input stream at PIPELINE rate (48kHz) - let PortAudio resample from device if needed
1054 // This ensures input matches sample_rate for AEC3, avoiding resampling in our callback
1055 err = Pa_OpenStream(&ctx->input_stream, &inputParams, NULL, AUDIO_SAMPLE_RATE, AUDIO_FRAMES_PER_BUFFER, paClipOff,
1056 input_callback, ctx);
1057 if (err != paNoError) {
1058 Pa_CloseStream(ctx->output_stream);
1059 ctx->output_stream = NULL;
1061 ctx->render_buffer = NULL;
1063 return SET_ERRNO(ERROR_AUDIO, "Failed to open input stream: %s", Pa_GetErrorText(err));
1064 }
1065
1066 // Start both streams
1067 err = Pa_StartStream(ctx->output_stream);
1068 if (err != paNoError) {
1069 Pa_CloseStream(ctx->input_stream);
1070 Pa_CloseStream(ctx->output_stream);
1071 ctx->input_stream = NULL;
1072 ctx->output_stream = NULL;
1074 ctx->render_buffer = NULL;
1076 return SET_ERRNO(ERROR_AUDIO, "Failed to start output stream: %s", Pa_GetErrorText(err));
1077 }
1078
1079 err = Pa_StartStream(ctx->input_stream);
1080 if (err != paNoError) {
1081 Pa_StopStream(ctx->output_stream);
1082 Pa_CloseStream(ctx->input_stream);
1083 Pa_CloseStream(ctx->output_stream);
1084 ctx->input_stream = NULL;
1085 ctx->output_stream = NULL;
1087 ctx->render_buffer = NULL;
1089 return SET_ERRNO(ERROR_AUDIO, "Failed to start input stream: %s", Pa_GetErrorText(err));
1090 }
1091
1092 ctx->separate_streams = true;
1093 log_info("Separate streams started successfully");
1094 } else {
1095 ctx->separate_streams = false;
1096 log_info("Full-duplex stream started (single callback, perfect AEC3 timing)");
1097 }
1098
1100
1101 ctx->running = true;
1104
1105 return ASCIICHAT_OK;
1106}
#define AUDIO_SAMPLE_RATE
Audio sample rate (48kHz professional quality, Opus-compatible)
asciichat_error_t audio_set_realtime_priority(void)
Request real-time priority for current thread.
#define AUDIO_FRAMES_PER_BUFFER
Audio frames per buffer (480 = 10ms at 48kHz, matches WebRTC AEC3 frame size)
#define AUDIO_CHANNELS
Number of audio channels (1 = mono)
@ ERROR_INVALID_STATE
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
Definition options.h:644
double input_device_rate
Native sample rate of input device.
PaStream * input_stream
Separate input stream (when full-duplex unavailable)
bool separate_streams
True if using separate input/output streams.
audio_ring_buffer_t * render_buffer
Ring buffer for render reference (separate streams mode)
double output_device_rate
Native sample rate of output device.
PaStream * duplex_stream
PortAudio full-duplex stream (simultaneous input+output)
PaStream * output_stream
Separate output stream (when full-duplex unavailable)

References ASCIICHAT_OK, AUDIO_CHANNELS, AUDIO_FRAMES_PER_BUFFER, audio_ring_buffer_create_for_capture(), audio_ring_buffer_destroy(), AUDIO_SAMPLE_RATE, audio_set_realtime_priority(), audio_context_t::duplex_stream, ERROR_AUDIO, ERROR_INVALID_STATE, ERROR_MEMORY, GET_OPTION, audio_context_t::initialized, audio_context_t::input_device_rate, audio_context_t::input_stream, log_info, log_warn, mutex_lock, mutex_unlock, audio_context_t::output_device_rate, audio_context_t::output_stream, audio_context_t::render_buffer, audio_context_t::running, audio_context_t::sample_rate, audio_context_t::separate_streams, SET_ERRNO, and audio_context_t::state_mutex.

Referenced by audio_client_init().

◆ audio_stop_duplex()

asciichat_error_t audio_stop_duplex ( audio_context_t ctx)

#include <audio.h>

Stop full-duplex audio.

Parameters
ctxAudio context (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Stops the full-duplex audio stream and closes the stream.

Note
If not running, this function has no effect.
Buffers are not cleared by this function.

Definition at line 1108 of file lib/audio/audio.c.

1108 {
1109 if (!ctx || !ctx->initialized) {
1110 return SET_ERRNO(ERROR_INVALID_STATE, "Audio context not initialized");
1111 }
1112
1113 atomic_store(&ctx->shutting_down, true);
1114
1115 if (ctx->playback_buffer) {
1117 }
1118
1119 Pa_Sleep(50); // Let callbacks drain
1120
1121 mutex_lock(&ctx->state_mutex);
1122
1123 if (ctx->duplex_stream) {
1124 Pa_StopStream(ctx->duplex_stream);
1125 Pa_CloseStream(ctx->duplex_stream);
1126 ctx->duplex_stream = NULL;
1127 }
1128
1129 // Stop separate streams if used
1130 if (ctx->input_stream) {
1131 Pa_StopStream(ctx->input_stream);
1132 Pa_CloseStream(ctx->input_stream);
1133 ctx->input_stream = NULL;
1134 }
1135
1136 if (ctx->output_stream) {
1137 Pa_StopStream(ctx->output_stream);
1138 Pa_CloseStream(ctx->output_stream);
1139 ctx->output_stream = NULL;
1140 }
1141
1142 // Cleanup render buffer
1143 if (ctx->render_buffer) {
1145 ctx->render_buffer = NULL;
1146 }
1147
1148 ctx->running = false;
1149 ctx->separate_streams = false;
1151
1152 log_info("Audio stopped");
1153 return ASCIICHAT_OK;
1154}
void audio_ring_buffer_clear(audio_ring_buffer_t *rb)
Clear all audio samples from ring buffer.

References ASCIICHAT_OK, audio_ring_buffer_clear(), audio_ring_buffer_destroy(), audio_context_t::duplex_stream, ERROR_INVALID_STATE, audio_context_t::initialized, audio_context_t::input_stream, log_info, mutex_lock, mutex_unlock, audio_context_t::output_stream, audio_context_t::playback_buffer, audio_context_t::render_buffer, audio_context_t::running, audio_context_t::separate_streams, SET_ERRNO, audio_context_t::shutting_down, and audio_context_t::state_mutex.

Referenced by audio_cleanup(), and audio_destroy().

◆ audio_validate_batch_params()

asciichat_error_t audio_validate_batch_params ( const audio_batch_info_t batch)

#include <audio.h>

Validate audio batch parameters for sanity.

Checks that parsed batch parameters are within acceptable ranges. Performs these checks:

  • batch_count > 0 and <= MAX_BATCH_SIZE
  • sample_rate is a standard rate (8000, 16000, 24000, 48000, etc.)
  • channels is 1 (mono) or 2 (stereo)
  • total_samples is consistent with batch_count
Parameters
batchPointer to audio_batch_info_t struct to validate
Returns
ASCIICHAT_OK if batch parameters are valid
ERROR_INVALID_PARAM if any parameter is out of range

Usage:

audio_parse_batch_header(data, len, &batch);
log_error("Audio batch parameters invalid");
return;
}
asciichat_error_t audio_validate_batch_params(const audio_batch_info_t *batch)
Validate audio batch parameters for sanity.

Definition at line 1414 of file lib/audio/audio.c.

1414 {
1415 if (!batch) {
1416 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch info pointer is NULL");
1417 }
1418
1419 // Validate batch_count
1420 if (batch->batch_count == 0) {
1421 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch count cannot be zero");
1422 }
1423
1424 // Check for reasonable max (256 frames per batch is very generous)
1425 if (batch->batch_count > 256) {
1426 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch count too large (batch_count=%u, max=256)", batch->batch_count);
1427 }
1428
1429 // Validate channels (1=mono, 2=stereo, max 8 for multi-channel)
1430 if (batch->channels == 0 || batch->channels > 8) {
1431 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid channel count (channels=%u, valid=1-8)", batch->channels);
1432 }
1433
1434 // Validate sample rate
1436 return SET_ERRNO(ERROR_INVALID_PARAM, "Unsupported sample rate (sample_rate=%u)", batch->sample_rate);
1437 }
1438
1439 // Check for reasonable sample counts
1440 if (batch->total_samples == 0) {
1441 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch has zero samples");
1442 }
1443
1444 // Each batch typically has samples_per_frame worth of samples
1445 // For 48kHz at 20ms per frame: 48000 * 0.02 = 960 samples per frame
1446 // With max 256 frames, that's up to ~245k samples per batch
1447 if (batch->total_samples > 1000000) {
1448 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch sample count suspiciously large (total_samples=%u)",
1449 batch->total_samples);
1450 }
1451
1452 return ASCIICHAT_OK;
1453}

References ASCIICHAT_OK, audio_is_supported_sample_rate(), audio_batch_info_t::batch_count, audio_batch_info_t::channels, ERROR_INVALID_PARAM, audio_batch_info_t::sample_rate, SET_ERRNO, and audio_batch_info_t::total_samples.

◆ audio_write_samples()

asciichat_error_t audio_write_samples ( audio_context_t ctx,
const float *  buffer,
int  num_samples 
)

#include <audio.h>

Write audio samples to playback buffer.

Parameters
ctxAudio context (must not be NULL)
bufferInput buffer of audio samples (must not be NULL)
num_samplesNumber of samples to write (must be > 0)
Returns
ASCIICHAT_OK on success, error code on failure

Writes audio samples to the playback ring buffer for playback. Samples should be in floating point format (-1.0 to 1.0 range). This function is non-blocking and writes as many samples as buffer space allows.

Note
Actual number of samples written may be less than num_samples if buffer doesn't have enough space.
Samples are queued for playback by the playback thread.
Thread-safe: Can be called from any thread.
Warning
Buffer must contain at least num_samples float values.

Definition at line 1167 of file lib/audio/audio.c.

1167 {
1168 if (!ctx || !ctx->initialized || !buffer || num_samples <= 0) {
1169 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, buffer=%p, num_samples=%d", ctx, buffer,
1170 num_samples);
1171 }
1172
1173 // Don't accept new audio data during shutdown - this prevents garbage/beeps
1174 if (atomic_load(&ctx->shutting_down)) {
1175 return ASCIICHAT_OK; // Silently discard
1176 }
1177
1178 asciichat_error_t result = audio_ring_buffer_write(ctx->playback_buffer, buffer, num_samples);
1179
1180 return result;
1181}

References ASCIICHAT_OK, audio_ring_buffer_write(), ERROR_INVALID_PARAM, audio_context_t::initialized, audio_context_t::playback_buffer, SET_ERRNO, and audio_context_t::shutting_down.

Referenced by audio_process_received_samples().

◆ buffer_float_to_int16()

void buffer_float_to_int16 ( const float *  src,
int16_t *  dst,
int  count 
)

#include <mixer.h>

Convert float buffer to int16 buffer.

Parameters
srcSource float buffer
dstDestination int16 buffer
countNumber of samples to convert

Batch converts float samples to int16 format for WebRTC.

Definition at line 1067 of file mixer.c.

1067 {
1068 if (!src || !dst || count <= 0)
1069 return;
1070 for (int i = 0; i < count; i++) {
1071 dst[i] = float_to_int16(src[i]);
1072 }
1073}
int16_t float_to_int16(float sample)
Convert float sample to int16 (WebRTC format)
Definition mixer.c:1054

References float_to_int16().

◆ buffer_int16_to_float()

void buffer_int16_to_float ( const int16_t *  src,
float *  dst,
int  count 
)

#include <mixer.h>

Convert int16 buffer to float buffer.

Parameters
srcSource int16 buffer
dstDestination float buffer
countNumber of samples to convert

Batch converts int16 samples to float format.

Definition at line 1075 of file mixer.c.

1075 {
1076 if (!src || !dst || count <= 0)
1077 return;
1078 for (int i = 0; i < count; i++) {
1079 dst[i] = int16_to_float(src[i]);
1080 }
1081}
float int16_to_float(int16_t sample)
Convert int16 sample to float.
Definition mixer.c:1063

References int16_to_float().

◆ buffer_peak()

float buffer_peak ( const float *  buffer,
int  count 
)

#include <mixer.h>

Find peak absolute value in buffer.

Parameters
bufferAudio buffer
countNumber of samples
Returns
Maximum absolute sample value (0.0 to 1.0+)

Scans buffer for the sample with highest absolute value. Useful for level metering and gain staging.

Definition at line 1083 of file mixer.c.

1083 {
1084 if (!buffer || count <= 0)
1085 return 0.0f;
1086
1087 float peak = 0.0f;
1088 for (int i = 0; i < count; i++) {
1089 float abs_sample = fabsf(buffer[i]);
1090 if (abs_sample > peak)
1091 peak = abs_sample;
1092 }
1093 return peak;
1094}

◆ clamp_float()

float clamp_float ( float  value,
float  min,
float  max 
)

#include <mixer.h>

Clamp a float value to a range.

Parameters
valueValue to clamp
minMinimum value
maxMaximum value
Returns
Clamped value (min <= result <= max)

Clamps a float value to the specified range. Useful for preventing audio overflow and ensuring parameters stay within valid ranges.

Definition at line 31 of file mixer.c.

31 {
32 if (value < min) {
33 return min;
34 }
35 if (value > max) {
36 return max;
37 }
38 return value;
39}

◆ compressor_init()

void compressor_init ( compressor_t comp,
float  sample_rate 
)

#include <mixer.h>

Initialize a compressor.

Parameters
compCompressor structure
sample_rateSample rate in Hz

Initializes compressor state and converts time-based parameters (attack_ms, release_ms) to coefficients for sample-by-sample processing.

Definition at line 42 of file mixer.c.

42 {
43 comp->sample_rate = sample_rate;
44 comp->envelope = 0.0f;
45 comp->gain_lin = 1.0f;
46
47 // Set default parameters with +6dB makeup gain
48 // The client playback path now has proper soft clipping to handle any peaks
49 // Server ducking (-6dB) + crowd scaling (-3dB) needs compensation
50 compressor_set_params(comp, -10.0f, 4.0f, 10.0f, 100.0f, 6.0f);
51}
float envelope
Current envelope follower state (linear, 0-1)
Definition mixer.h:154
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
float sample_rate
Sample rate in Hz (set during initialization)
Definition mixer.h:152
float gain_lin
Current gain multiplier (linear, calculated from envelope)
Definition mixer.h:156

References compressor_set_params(), compressor_t::envelope, compressor_t::gain_lin, and compressor_t::sample_rate.

Referenced by client_audio_pipeline_create(), and mixer_create().

◆ compressor_process_sample()

float compressor_process_sample ( compressor_t comp,
float  sidechain 
)

#include <mixer.h>

Process a single sample through compressor.

Parameters
compCompressor structure
sidechainInput level for gain reduction calculation
Returns
Compressed output level

Processes sidechain input through compressor to calculate gain reduction. Returns compressed output level (not actual audio sample - use for sidechain).

Note
This calculates gain reduction, not actual audio compression. Apply the gain to audio samples separately.

Definition at line 87 of file mixer.c.

87 {
88 float x = fabsf(sidechain);
89
90 // Update envelope with attack/release
91 if (x > comp->envelope)
92 comp->envelope = comp->attack_coeff * comp->envelope + (1.0f - comp->attack_coeff) * x;
93 else
94 comp->envelope = comp->release_coeff * comp->envelope + (1.0f - comp->release_coeff) * x;
95
96 // Calculate gain reduction
97 float level_dB = linear_to_db(comp->envelope);
98 float gr_dB = compressor_gain_reduction_db(comp, level_dB);
99 float target_lin = db_to_linear(gr_dB + comp->makeup_dB);
100
101 // Smooth gain changes
102 if (target_lin < comp->gain_lin)
103 comp->gain_lin = comp->attack_coeff * comp->gain_lin + (1.0f - comp->attack_coeff) * target_lin;
104 else
105 comp->gain_lin = comp->release_coeff * comp->gain_lin + (1.0f - comp->release_coeff) * target_lin;
106
107 return comp->gain_lin;
108}
float db_to_linear(float db)
Convert decibels to linear gain.
Definition mixer.c:23
float makeup_dB
Makeup gain in dB (compensates for gain reduction)
Definition mixer.h:149
float linear_to_db(float linear)
Convert linear gain to decibels.
Definition mixer.c:27
float attack_coeff
Attack coefficient (converted from attack_ms)
Definition mixer.h:158
float release_coeff
Release coefficient (converted from release_ms)
Definition mixer.h:160

References compressor_t::attack_coeff, db_to_linear(), compressor_t::envelope, compressor_t::gain_lin, linear_to_db(), compressor_t::makeup_dB, and compressor_t::release_coeff.

Referenced by client_audio_pipeline_process_duplex(), mixer_process(), and mixer_process_excluding_source().

◆ compressor_set_params()

void compressor_set_params ( compressor_t comp,
float  threshold_dB,
float  ratio,
float  attack_ms,
float  release_ms,
float  makeup_dB 
)

#include <mixer.h>

Set compressor parameters.

Parameters
compCompressor structure
threshold_dBCompression threshold in dB
ratioCompression ratio (e.g., 4.0 for 4:1)
attack_msAttack time in milliseconds
release_msRelease time in milliseconds
makeup_dBMakeup gain in dB

Updates compressor parameters. Time-based parameters are converted to coefficients internally.

Definition at line 53 of file mixer.c.

54 {
55 comp->threshold_dB = threshold_dB;
56 comp->ratio = ratio;
57 comp->attack_ms = attack_ms;
58 comp->release_ms = release_ms;
59 comp->makeup_dB = makeup_dB;
60 comp->knee_dB = 2.0f; // Fixed soft knee
61
62 // Calculate time constants
63 float attack_tau = attack_ms / 1000.0f;
64 float release_tau = release_ms / 1000.0f;
65 comp->attack_coeff = expf(-1.0f / (attack_tau * comp->sample_rate + 1e-12f));
66 comp->release_coeff = expf(-1.0f / (release_tau * comp->sample_rate + 1e-12f));
67}
float ratio
Compression ratio (e.g., 4.0 for 4:1 compression)
Definition mixer.h:143
float knee_dB
Knee width in dB for soft knee (e.g., 2.0)
Definition mixer.h:141
float attack_ms
Attack time in milliseconds (how fast compression kicks in)
Definition mixer.h:145
float threshold_dB
Compression threshold in dB (e.g., -10.0)
Definition mixer.h:139
float release_ms
Release time in milliseconds (how fast compression releases)
Definition mixer.h:147

References compressor_t::attack_coeff, compressor_t::attack_ms, compressor_t::knee_dB, compressor_t::makeup_dB, compressor_t::ratio, compressor_t::release_coeff, compressor_t::release_ms, compressor_t::sample_rate, and compressor_t::threshold_dB.

Referenced by client_audio_pipeline_create(), and compressor_init().

◆ copy_buffer_with_gain()

void copy_buffer_with_gain ( const float *  src,
float *  dst,
int  count,
float  gain 
)

#include <mixer.h>

Copy buffer with gain scaling.

Parameters
srcSource buffer
dstDestination buffer
countNumber of samples
gainGain multiplier to apply during copy

Copies samples from src to dst while applying gain. Useful for format conversion (e.g., scale by 32768 for WebRTC).

Definition at line 1128 of file mixer.c.

1128 {
1129 if (!src || !dst || count <= 0)
1130 return;
1131
1132 for (int i = 0; i < count; i++) {
1133 dst[i] = src[i] * gain;
1134 }
1135}

Referenced by client_audio_pipeline_process_duplex().

◆ db_to_linear()

float db_to_linear ( float  db)

#include <mixer.h>

Convert decibels to linear gain.

Parameters
dbDecibel value
Returns
Linear gain multiplier (10^(db/20))

Converts decibel value to linear gain multiplier for audio processing.

Definition at line 23 of file mixer.c.

23 {
24 return powf(10.0f, db / 20.0f);
25}

Referenced by compressor_process_sample(), ducking_process_frame(), mixer_process(), and mixer_process_excluding_source().

◆ ducking_free()

void ducking_free ( ducking_t duck)

#include <mixer.h>

Free ducking system resources.

Parameters
duckDucking structure

Frees per-source arrays allocated by ducking_init().

Definition at line 166 of file mixer.c.

166 {
167 if (duck->envelope) {
168 SAFE_FREE(duck->envelope);
169 }
170 if (duck->gain) {
171 SAFE_FREE(duck->gain);
172 }
173}
float * gain
Per-source ducking gain (linear, calculated from envelope)
Definition mixer.h:290
float * envelope
Per-source envelope follower state (linear, allocated per source)
Definition mixer.h:288

References ducking_t::envelope, ducking_t::gain, and SAFE_FREE.

Referenced by mixer_destroy().

◆ ducking_init()

int ducking_init ( ducking_t duck,
int  num_sources,
float  sample_rate 
)

#include <mixer.h>

Initialize ducking system.

Parameters
duckDucking structure
num_sourcesNumber of sources to support
sample_rateSample rate in Hz

Allocates per-source arrays (envelope, gain) for ducking system. Time-based parameters are converted to coefficients.

Warning
Must be paired with ducking_free() to prevent memory leaks.
Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 111 of file mixer.c.

111 {
112 if (!duck) {
113 return SET_ERRNO(ERROR_INVALID_PARAM, "ducking_init: duck is NULL");
114 }
115
116 // Set default parameters
117 // threshold_dB: sources below this aren't considered "speaking"
118 // leader_margin_dB: sources within this margin of loudest are all "leaders"
119 // atten_dB: how much to attenuate non-leaders (was -12dB, too aggressive)
120 duck->threshold_dB = -45.0f; // More lenient threshold
121 duck->leader_margin_dB = 6.0f; // Wider margin = more sources treated as leaders
122 duck->atten_dB = -6.0f; // Only -6dB attenuation (was -12dB)
123 duck->attack_ms = 10.0f; // Slower attack (was 5ms)
124 duck->release_ms = 200.0f; // Slower release (was 100ms)
125 duck->envelope = NULL;
126 duck->gain = NULL;
127
128 // Calculate time constants
129 float attack_tau = duck->attack_ms / 1000.0f;
130 float release_tau = duck->release_ms / 1000.0f;
131 duck->attack_coeff = expf(-1.0f / (attack_tau * sample_rate + 1e-12f));
132 duck->release_coeff = expf(-1.0f / (release_tau * sample_rate + 1e-12f));
133
134 // Allocate arrays with overflow checking
135 size_t envelope_size = 0;
136 if (checked_size_mul((size_t)num_sources, sizeof(float), &envelope_size) != ASCIICHAT_OK) {
137 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Ducking envelope array size overflow: %d sources", num_sources);
138 }
139 duck->envelope = SAFE_MALLOC(envelope_size, float *);
140 if (!duck->envelope) {
141 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate ducking envelope array");
142 }
143
144 size_t gain_size = 0;
145 if (checked_size_mul((size_t)num_sources, sizeof(float), &gain_size) != ASCIICHAT_OK) {
146 SAFE_FREE(duck->envelope);
147 duck->envelope = NULL;
148 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Ducking gain array size overflow: %d sources", num_sources);
149 }
150 duck->gain = SAFE_MALLOC(gain_size, float *);
151 if (!duck->gain) {
152 SAFE_FREE(duck->envelope);
153 duck->envelope = NULL;
154 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate ducking gain array");
155 }
156
157 // Initialize
158 SAFE_MEMSET(duck->envelope, (size_t)num_sources * sizeof(float), 0, (size_t)num_sources * sizeof(float));
159 for (int i = 0; i < num_sources; i++) {
160 duck->gain[i] = 1.0f;
161 }
162
163 return ASCIICHAT_OK;
164}
float leader_margin_dB
Leader margin in dB (sources within this of loudest are leaders)
Definition mixer.h:275
float release_coeff
Release coefficient (converted from release_ms)
Definition mixer.h:286
float atten_dB
Attenuation in dB for non-leader sources.
Definition mixer.h:277
float attack_coeff
Attack coefficient (converted from attack_ms)
Definition mixer.h:284
float threshold_dB
Speaking threshold in dB (sources below this are not "speaking")
Definition mixer.h:273
float attack_ms
Ducking attack time in milliseconds.
Definition mixer.h:279
float release_ms
Ducking release time in milliseconds.
Definition mixer.h:281
#define SAFE_MALLOC(size, cast)
Definition common.h:208
@ ERROR_BUFFER_OVERFLOW
Definition error_codes.h:98

References ASCIICHAT_OK, ducking_t::attack_coeff, ducking_t::attack_ms, ducking_t::atten_dB, ducking_t::envelope, ERROR_BUFFER_OVERFLOW, ERROR_INVALID_PARAM, ERROR_MEMORY, ducking_t::gain, ducking_t::leader_margin_dB, ducking_t::release_coeff, ducking_t::release_ms, SAFE_FREE, SAFE_MALLOC, SAFE_MEMSET, SET_ERRNO, and ducking_t::threshold_dB.

Referenced by mixer_create().

◆ ducking_process_frame()

void ducking_process_frame ( ducking_t duck,
float *  envelopes,
float *  gains,
int  num_sources 
)

#include <mixer.h>

Process a frame of audio through ducking system.

Parameters
duckDucking structure
envelopesArray of per-source envelope levels (input)
gainsArray of per-source ducking gains (output)
num_sourcesNumber of sources to process

Calculates ducking gains for each source based on envelope levels. Outputs per-source gain multipliers to apply to audio samples.

Note
Gains are calculated from envelope levels, identifying leaders and applying attenuation to non-leaders.

Definition at line 184 of file mixer.c.

184 {
185 // Find the loudest source
186 float max_dB = -120.0f;
187 float env_dB[MIXER_MAX_SOURCES];
188
189 for (int i = 0; i < num_sources; i++) {
190 env_dB[i] = linear_to_db(envelopes[i]);
191 if (env_dB[i] > max_dB)
192 max_dB = env_dB[i];
193 }
194
195 // Calculate ducking gain for each source
196 float leader_cut = db_to_linear(duck->atten_dB);
197
198 for (int i = 0; i < num_sources; i++) {
199 bool is_speaking = env_dB[i] > duck->threshold_dB;
200 bool is_leader = is_speaking && (env_dB[i] >= max_dB - duck->leader_margin_dB);
201
202 float target;
203 if (is_speaking && !is_leader) {
204 target = leader_cut;
205 } else {
206 target = 1.0f;
207 }
208
209 // Smooth gain transitions
210 if (target < gains[i])
211 gains[i] = duck->attack_coeff * gains[i] + (1.0f - duck->attack_coeff) * target;
212 else
213 gains[i] = duck->release_coeff * gains[i] + (1.0f - duck->release_coeff) * target;
214 }
215}

References ducking_t::attack_coeff, ducking_t::atten_dB, db_to_linear(), ducking_t::leader_margin_dB, linear_to_db(), MIXER_MAX_SOURCES, ducking_t::release_coeff, and ducking_t::threshold_dB.

Referenced by mixer_process(), and mixer_process_excluding_source().

◆ ducking_set_params()

void ducking_set_params ( ducking_t duck,
float  threshold_dB,
float  leader_margin_dB,
float  atten_dB,
float  attack_ms,
float  release_ms 
)

#include <mixer.h>

Set ducking parameters.

Parameters
duckDucking structure
threshold_dBSpeaking threshold in dB
leader_margin_dBLeader margin in dB
atten_dBAttenuation in dB for non-leaders
attack_msAttack time in milliseconds
release_msRelease time in milliseconds

Updates ducking parameters. Time-based parameters are converted to coefficients internally.

Definition at line 175 of file mixer.c.

176 {
177 duck->threshold_dB = threshold_dB;
178 duck->leader_margin_dB = leader_margin_dB;
179 duck->atten_dB = atten_dB;
180 duck->attack_ms = attack_ms;
181 duck->release_ms = release_ms;
182}

References ducking_t::attack_ms, ducking_t::atten_dB, ducking_t::leader_margin_dB, ducking_t::release_ms, and ducking_t::threshold_dB.

◆ fade_buffer()

void fade_buffer ( float *  buffer,
int  count,
float  start_gain,
float  end_gain 
)

#include <mixer.h>

Apply linear fade to buffer in-place.

Parameters
bufferAudio buffer (modified in-place)
countNumber of samples
start_gainGain at start of buffer
end_gainGain at end of buffer

Applies linear interpolation from start_gain to end_gain across buffer. Use start_gain=0, end_gain=1 for fade-in; start_gain=1, end_gain=0 for fade-out.

Definition at line 1105 of file mixer.c.

1105 {
1106 if (!buffer || count <= 0)
1107 return;
1108
1109 float step = (end_gain - start_gain) / (float)count;
1110 float gain = start_gain;
1111 for (int i = 0; i < count; i++) {
1112 buffer[i] *= gain;
1113 gain += step;
1114 }
1115}

◆ fade_buffer_smooth()

void fade_buffer_smooth ( float *  buffer,
int  count,
bool  fade_in 
)

#include <mixer.h>

Apply smoothstep fade to buffer in-place.

Parameters
bufferAudio buffer (modified in-place)
countNumber of samples
fade_inIf true, fade from 0 to 1; if false, fade from 1 to 0

Applies smoothstep curve for more natural-sounding fades.

Definition at line 1117 of file mixer.c.

1117 {
1118 if (!buffer || count <= 0)
1119 return;
1120
1121 for (int i = 0; i < count; i++) {
1122 float t = (float)i / (float)(count - 1);
1123 float gain = smoothstep(fade_in ? t : (1.0f - t));
1124 buffer[i] *= gain;
1125 }
1126}
float smoothstep(float t)
Compute smoothstep interpolation.
Definition mixer.c:1046

References smoothstep().

◆ float_to_int16()

int16_t float_to_int16 ( float  sample)

#include <mixer.h>

Convert float sample to int16 (WebRTC format)

Parameters
sampleFloat sample in [-1.0, 1.0] range
Returns
Scaled int16 sample in [-32768, 32767] range

Scales float audio sample to 16-bit integer format used by WebRTC. Values outside [-1.0, 1.0] are clamped before scaling.

Definition at line 1054 of file mixer.c.

1054 {
1055 // Clamp to [-1, 1] then scale to int16 range
1056 if (sample > 1.0f)
1057 sample = 1.0f;
1058 if (sample < -1.0f)
1059 sample = -1.0f;
1060 return (int16_t)(sample * 32767.0f);
1061}

Referenced by buffer_float_to_int16().

◆ highpass_filter_init()

void highpass_filter_init ( highpass_filter_t filter,
float  cutoff_hz,
float  sample_rate 
)

#include <mixer.h>

Initialize a high-pass filter.

Parameters
filterHigh-pass filter structure
cutoff_hzCutoff frequency in Hz
sample_rateSample rate in Hz

Initializes filter state and calculates filter coefficient alpha from cutoff frequency.

Definition at line 920 of file mixer.c.

920 {
921 if (!filter)
922 return;
923
924 filter->cutoff_hz = cutoff_hz;
925 filter->sample_rate = sample_rate;
926
927 // Calculate filter coefficient
928 // alpha = 1 / (1 + 2*pi*fc/fs)
929 filter->alpha = 1.0f / (1.0f + 2.0f * M_PI * cutoff_hz / sample_rate);
930
931 highpass_filter_reset(filter);
932}
float alpha
Filter coefficient alpha (calculated from cutoff_hz)
Definition mixer.h:225
void highpass_filter_reset(highpass_filter_t *filter)
Reset high-pass filter state.
Definition mixer.c:934
float sample_rate
Sample rate in Hz (set during initialization)
Definition mixer.h:222
float cutoff_hz
Cutoff frequency in Hz (frequencies below this are attenuated)
Definition mixer.h:220
#define M_PI
Definition mixer.c:19

References highpass_filter_t::alpha, highpass_filter_t::cutoff_hz, highpass_filter_reset(), M_PI, and highpass_filter_t::sample_rate.

Referenced by client_audio_pipeline_create().

◆ highpass_filter_process_buffer()

void highpass_filter_process_buffer ( highpass_filter_t filter,
float *  buffer,
int  num_samples 
)

#include <mixer.h>

Process a buffer of samples through high-pass filter.

Parameters
filterHigh-pass filter structure
bufferAudio buffer (modified in-place)
num_samplesNumber of samples to process

Processes entire buffer through high-pass filter. Buffer is modified in-place.

Definition at line 956 of file mixer.c.

956 {
957 if (!filter || !buffer || num_samples <= 0)
958 return;
959
960 for (int i = 0; i < num_samples; i++) {
961 buffer[i] = highpass_filter_process_sample(filter, buffer[i]);
962 }
963}
float highpass_filter_process_sample(highpass_filter_t *filter, float input)
Process a single sample through high-pass filter.
Definition mixer.c:942

References highpass_filter_process_sample().

Referenced by client_audio_pipeline_process_duplex().

◆ highpass_filter_process_sample()

float highpass_filter_process_sample ( highpass_filter_t filter,
float  input 
)

#include <mixer.h>

Process a single sample through high-pass filter.

Parameters
filterHigh-pass filter structure
inputInput sample value
Returns
Filtered output sample

Processes input sample through first-order IIR high-pass filter. Filter state is updated for next sample.

Definition at line 942 of file mixer.c.

942 {
943 if (!filter)
944 return input;
945
946 // First-order high-pass filter
947 // y[n] = alpha * (y[n-1] + x[n] - x[n-1])
948 float output = filter->alpha * (filter->prev_output + input - filter->prev_input);
949
950 filter->prev_input = input;
951 filter->prev_output = output;
952
953 return output;
954}
float prev_input
Previous input sample (filter state)
Definition mixer.h:227
float prev_output
Previous output sample (filter state)
Definition mixer.h:229

References highpass_filter_t::alpha, highpass_filter_t::prev_input, and highpass_filter_t::prev_output.

Referenced by highpass_filter_process_buffer().

◆ highpass_filter_reset()

void highpass_filter_reset ( highpass_filter_t filter)

#include <mixer.h>

Reset high-pass filter state.

Parameters
filterHigh-pass filter structure

Resets filter state (prev_input, prev_output) to zero. Useful for starting fresh processing or removing DC offset.

Definition at line 934 of file mixer.c.

934 {
935 if (!filter)
936 return;
937
938 filter->prev_input = 0.0f;
939 filter->prev_output = 0.0f;
940}

References highpass_filter_t::prev_input, and highpass_filter_t::prev_output.

Referenced by highpass_filter_init().

◆ int16_to_float()

float int16_to_float ( int16_t  sample)

#include <mixer.h>

Convert int16 sample to float.

Parameters
sampleInt16 sample in [-32768, 32767] range
Returns
Float sample in [-1.0, 1.0] range

Scales 16-bit integer audio sample to float format.

Definition at line 1063 of file mixer.c.

1063 {
1064 return (float)sample / 32768.0f;
1065}

Referenced by buffer_int16_to_float().

◆ linear_to_db()

float linear_to_db ( float  linear)

#include <mixer.h>

Convert linear gain to decibels.

Parameters
linearLinear gain multiplier
Returns
Decibel value (20*log10(linear))

Converts linear gain multiplier to decibel value for display/logging.

Definition at line 27 of file mixer.c.

27 {
28 return 20.0f * log10f(fmaxf(linear, 1e-12f));
29}

Referenced by compressor_process_sample(), and ducking_process_frame().

◆ lowpass_filter_init()

void lowpass_filter_init ( lowpass_filter_t filter,
float  cutoff_hz,
float  sample_rate 
)

#include <mixer.h>

Initialize a low-pass filter.

Parameters
filterLow-pass filter structure
cutoff_hzCutoff frequency in Hz
sample_rateSample rate in Hz

Initializes filter state and calculates filter coefficient alpha from cutoff frequency. Frequencies above cutoff are attenuated.

Definition at line 970 of file mixer.c.

970 {
971 if (!filter)
972 return;
973
974 filter->cutoff_hz = cutoff_hz;
975 filter->sample_rate = sample_rate;
976
977 // Calculate filter coefficient using RC time constant formula
978 // alpha = dt / (RC + dt) where RC = 1 / (2 * pi * fc)
979 float dt = 1.0f / sample_rate;
980 float rc = 1.0f / (2.0f * (float)M_PI * cutoff_hz);
981 filter->alpha = dt / (rc + dt);
982
983 lowpass_filter_reset(filter);
984}
float cutoff_hz
Cutoff frequency in Hz (frequencies above this are attenuated)
Definition mixer.h:243
float alpha
Filter coefficient alpha (calculated from cutoff_hz)
Definition mixer.h:248
float sample_rate
Sample rate in Hz (set during initialization)
Definition mixer.h:245
void lowpass_filter_reset(lowpass_filter_t *filter)
Reset low-pass filter state.
Definition mixer.c:986

References lowpass_filter_t::alpha, lowpass_filter_t::cutoff_hz, lowpass_filter_reset(), M_PI, and lowpass_filter_t::sample_rate.

Referenced by client_audio_pipeline_create().

◆ lowpass_filter_process_buffer()

void lowpass_filter_process_buffer ( lowpass_filter_t filter,
float *  buffer,
int  num_samples 
)

#include <mixer.h>

Process a buffer of samples through low-pass filter.

Parameters
filterLow-pass filter structure
bufferAudio buffer (modified in-place)
num_samplesNumber of samples to process

Processes entire buffer through low-pass filter. Buffer is modified in-place.

Definition at line 1005 of file mixer.c.

1005 {
1006 if (!filter || !buffer || num_samples <= 0)
1007 return;
1008
1009 for (int i = 0; i < num_samples; i++) {
1010 buffer[i] = lowpass_filter_process_sample(filter, buffer[i]);
1011 }
1012}
float lowpass_filter_process_sample(lowpass_filter_t *filter, float input)
Process a single sample through low-pass filter.
Definition mixer.c:993

References lowpass_filter_process_sample().

Referenced by client_audio_pipeline_process_duplex().

◆ lowpass_filter_process_sample()

float lowpass_filter_process_sample ( lowpass_filter_t filter,
float  input 
)

#include <mixer.h>

Process a single sample through low-pass filter.

Parameters
filterLow-pass filter structure
inputInput sample value
Returns
Filtered output sample

Processes input sample through first-order IIR low-pass filter. Filter state is updated for next sample.

Definition at line 993 of file mixer.c.

993 {
994 if (!filter)
995 return input;
996
997 // First-order IIR low-pass filter: y[n] = alpha * x[n] + (1 - alpha) * y[n-1]
998 float output = filter->alpha * input + (1.0f - filter->alpha) * filter->prev_output;
999
1000 filter->prev_output = output;
1001
1002 return output;
1003}
float prev_output
Previous output sample (filter state)
Definition mixer.h:250

References lowpass_filter_t::alpha, and lowpass_filter_t::prev_output.

Referenced by lowpass_filter_process_buffer().

◆ lowpass_filter_reset()

void lowpass_filter_reset ( lowpass_filter_t filter)

#include <mixer.h>

Reset low-pass filter state.

Parameters
filterLow-pass filter structure

Resets filter state (prev_output) to zero.

Definition at line 986 of file mixer.c.

986 {
987 if (!filter)
988 return;
989
990 filter->prev_output = 0.0f;
991}

References lowpass_filter_t::prev_output.

Referenced by lowpass_filter_init().

◆ mixer_add_source()

int mixer_add_source ( mixer_t mixer,
uint32_t  client_id,
audio_ring_buffer_t buffer 
)

#include <mixer.h>

Add an audio source to the mixer.

Parameters
mixerAudio mixer
client_idClient ID for this source
bufferPointer to client's audio ring buffer
Returns
Mixer source index on success, -1 on failure (max sources reached)

Adds a new audio source to the mixer. Client ID is mapped to a mixer source index for efficient lookup during processing.

Note
Thread-safe (acquires write lock internally).
If max_sources is reached, addition fails and -1 is returned.

Definition at line 363 of file mixer.c.

363 {
364 if (!mixer || !buffer)
365 return -1;
366
367 // OPTIMIZATION 2: Acquire write lock for source modification
368 rwlock_wrlock(&mixer->source_lock);
369
370 // Find an empty slot
371 int slot = -1;
372 for (int i = 0; i < mixer->max_sources; i++) {
373 if (mixer->source_ids[i] == 0) {
374 slot = i;
375 break;
376 }
377 }
378
379 if (slot == -1) {
381 log_warn("Mixer: No available slots for client %u", client_id);
382 return -1;
383 }
384
385 mixer->source_buffers[slot] = buffer;
386 mixer->source_ids[slot] = client_id;
387 mixer->source_active[slot] = true;
388 mixer->num_sources++;
389
390 // OPTIMIZATION 1: Update bitset optimization structures
391 mixer->active_sources_mask |= (1ULL << slot); // Set bit for this slot
392 mixer->source_id_to_index[client_id & 0xFF] = (uint8_t)slot; // Hash table: client_id → slot
393
395
396 log_info("Mixer: Added source for client %u at slot %d", client_id, slot);
397 return slot;
398}
uint8_t source_id_to_index[256]
Hash table mapping client_id → mixer source index (uses hash function for 32-bit IDs)
Definition mixer.h:343
bool * source_active
Array of active flags (true if source is active)
Definition mixer.h:338
rwlock_t source_lock
Reader-writer lock protecting source arrays and bitset.
Definition mixer.h:348
int num_sources
Current number of active audio sources.
Definition mixer.h:327
uint32_t * source_ids
Array of client IDs (one per source slot)
Definition mixer.h:336
uint64_t active_sources_mask
Bitset of active sources (bit i = source i is active, O(1) iteration)
Definition mixer.h:341
int max_sources
Maximum number of sources (allocated array sizes)
Definition mixer.h:329
audio_ring_buffer_t ** source_buffers
Array of pointers to client audio ring buffers.
Definition mixer.h:334
#define rwlock_wrunlock(lock)
Release a write lock (with debug tracking in debug builds)
Definition rwlock.h:249
#define rwlock_wrlock(lock)
Acquire a write lock (with debug tracking in debug builds)
Definition rwlock.h:213

References mixer_t::active_sources_mask, log_info, log_warn, mixer_t::max_sources, mixer_t::num_sources, rwlock_wrlock, rwlock_wrunlock, mixer_t::source_active, mixer_t::source_buffers, mixer_t::source_id_to_index, mixer_t::source_ids, and mixer_t::source_lock.

◆ mixer_create()

mixer_t * mixer_create ( int  max_sources,
int  sample_rate 
)

#include <mixer.h>

Create a new audio mixer.

Parameters
max_sourcesMaximum number of audio sources to support
sample_rateSample rate in Hz (e.g., 44100)
Returns
Pointer to new mixer, or NULL on failure

Creates a new mixer with pre-allocated source arrays and processing buffers. All audio processing components are initialized.

Note
max_sources should not exceed MIXER_MAX_SOURCES.

Definition at line 218 of file mixer.c.

218 {
219 // Validate parameters
220 if (max_sources <= 0 || max_sources > MIXER_MAX_SOURCES) {
221 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid max_sources: %d (must be 1-%d)", max_sources, MIXER_MAX_SOURCES);
222 return NULL;
223 }
224
225 if (sample_rate <= 0 || sample_rate > 192000) {
226 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid sample_rate: %d (must be 1-192000)", sample_rate);
227 return NULL;
228 }
229
230 mixer_t *mixer;
231 mixer = SAFE_MALLOC(sizeof(mixer_t), mixer_t *);
232 if (!mixer) {
233 SET_ERRNO(ERROR_MEMORY, "Failed to allocate mixer structure");
234 return NULL;
235 }
236
237 mixer->num_sources = 0;
238 mixer->max_sources = max_sources;
239 mixer->sample_rate = sample_rate;
240
241 // Allocate source management arrays with overflow checking
242 size_t buffers_size = 0;
243 if (checked_size_mul((size_t)max_sources, sizeof(audio_ring_buffer_t *), &buffers_size) != ASCIICHAT_OK) {
244 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Mixer source buffers array overflow: %d sources", max_sources);
245 SAFE_FREE(mixer);
246 return NULL;
247 }
248 mixer->source_buffers = SAFE_MALLOC(buffers_size, audio_ring_buffer_t **);
249 if (!mixer->source_buffers) {
250 SAFE_FREE(mixer);
251 return NULL;
252 }
253
254 size_t ids_size = 0;
255 if (checked_size_mul((size_t)max_sources, sizeof(uint32_t), &ids_size) != ASCIICHAT_OK) {
256 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Mixer source IDs array overflow: %d sources", max_sources);
258 SAFE_FREE(mixer);
259 return NULL;
260 }
261 mixer->source_ids = SAFE_MALLOC(ids_size, uint32_t *);
262 if (!mixer->source_ids) {
264 SAFE_FREE(mixer);
265 return NULL;
266 }
267
268 size_t active_size = 0;
269 if (checked_size_mul((size_t)max_sources, sizeof(bool), &active_size) != ASCIICHAT_OK) {
270 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Mixer source active array overflow: %d sources", max_sources);
272 SAFE_FREE(mixer->source_ids);
273 SAFE_FREE(mixer);
274 return NULL;
275 }
276 mixer->source_active = SAFE_MALLOC(active_size, bool *);
277 if (!mixer->source_active) {
279 SAFE_FREE(mixer->source_ids);
280 SAFE_FREE(mixer);
281 return NULL;
282 }
283
284 // Initialize arrays
285 SAFE_MEMSET((void *)mixer->source_buffers, buffers_size, 0, buffers_size);
286 SAFE_MEMSET(mixer->source_ids, ids_size, 0, ids_size);
287 SAFE_MEMSET(mixer->source_active, active_size, 0, active_size);
288
289 // OPTIMIZATION 1: Initialize bitset optimization structures
290 mixer->active_sources_mask = 0ULL; // No sources active initially
291 SAFE_MEMSET(mixer->source_id_to_index, sizeof(mixer->source_id_to_index), 0xFF,
292 sizeof(mixer->source_id_to_index)); // 0xFF = invalid index
293
294 // Allocate mix buffer BEFORE rwlock_init so cleanup path is correct
295 size_t mix_buffer_size = 0;
296 if (checked_size_mul(MIXER_FRAME_SIZE, sizeof(float), &mix_buffer_size) != ASCIICHAT_OK) {
297 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Mixer mix buffer size overflow: %zu samples", (size_t)MIXER_FRAME_SIZE);
299 SAFE_FREE(mixer->source_ids);
300 SAFE_FREE(mixer->source_active);
301 SAFE_FREE(mixer);
302 return NULL;
303 }
304 mixer->mix_buffer = SAFE_MALLOC(mix_buffer_size, float *);
305 if (!mixer->mix_buffer) {
306 SET_ERRNO(ERROR_MEMORY, "Failed to allocate mix buffer");
308 SAFE_FREE(mixer->source_ids);
309 SAFE_FREE(mixer->source_active);
310 SAFE_FREE(mixer);
311 return NULL;
312 }
313
314 // OPTIMIZATION 2: Initialize reader-writer lock
315 if (rwlock_init(&mixer->source_lock) != 0) {
316 SET_ERRNO(ERROR_THREAD, "Failed to initialize mixer source lock");
318 SAFE_FREE(mixer->source_ids);
319 SAFE_FREE(mixer->source_active);
320 SAFE_FREE(mixer->mix_buffer);
321 SAFE_FREE(mixer);
322 return NULL;
323 }
324
325 // Set crowd scaling parameters
326 mixer->crowd_alpha = 0.5f; // Square root scaling
327 mixer->base_gain = 1.0f; // Unity gain - let compressor handle loudness, avoid clipping
328
329 // Initialize processing
330 if (ducking_init(&mixer->ducking, max_sources, (float)sample_rate) != ASCIICHAT_OK) {
333 SAFE_FREE(mixer->source_ids);
334 SAFE_FREE(mixer->source_active);
335 SAFE_FREE(mixer->mix_buffer);
336 SAFE_FREE(mixer);
337 return NULL;
338 }
339 compressor_init(&mixer->compressor, (float)sample_rate);
340
341 log_info("Audio mixer created: max_sources=%d, sample_rate=%d", max_sources, sample_rate);
342
343 return mixer;
344}
float base_gain
Base gain before crowd scaling is applied.
Definition mixer.h:353
float crowd_alpha
Crowd scaling exponent (typically 0.5 for sqrt scaling)
Definition mixer.h:351
ducking_t ducking
Ducking system (active speaker detection and attenuation)
Definition mixer.h:356
void compressor_init(compressor_t *comp, float sample_rate)
Initialize a compressor.
Definition mixer.c:42
int ducking_init(ducking_t *duck, int num_sources, float sample_rate)
Initialize ducking system.
Definition mixer.c:111
float * mix_buffer
Temporary buffer for mixing operations (pre-allocated)
Definition mixer.h:361
compressor_t compressor
Compressor (dynamic range compression)
Definition mixer.h:358
int sample_rate
Sample rate in Hz (e.g., 44100)
Definition mixer.h:331
int rwlock_destroy(rwlock_t *lock)
Destroy a read-write lock.
int rwlock_init(rwlock_t *lock)
Initialize a read-write lock.

References mixer_t::active_sources_mask, ASCIICHAT_OK, mixer_t::base_gain, mixer_t::compressor, compressor_init(), mixer_t::crowd_alpha, mixer_t::ducking, ducking_init(), ERROR_BUFFER_OVERFLOW, ERROR_INVALID_PARAM, ERROR_MEMORY, ERROR_THREAD, log_info, mixer_t::max_sources, mixer_t::mix_buffer, MIXER_FRAME_SIZE, MIXER_MAX_SOURCES, mixer_t::num_sources, rwlock_destroy(), rwlock_init(), SAFE_FREE, SAFE_MALLOC, SAFE_MEMSET, mixer_t::sample_rate, SET_ERRNO, mixer_t::source_active, mixer_t::source_buffers, mixer_t::source_id_to_index, mixer_t::source_ids, and mixer_t::source_lock.

Referenced by server_main().

◆ mixer_destroy()

void mixer_destroy ( mixer_t mixer)

#include <mixer.h>

Destroy a mixer and free all resources.

Parameters
mixerMixer to destroy (can be NULL)

Frees all allocated memory including source arrays, processing buffers, and ducking system allocations.

Warning
Mixer must not be in use by any threads when destroyed.

Definition at line 346 of file mixer.c.

346 {
347 if (!mixer)
348 return;
349
350 // OPTIMIZATION 2: Destroy reader-writer lock
352
353 ducking_free(&mixer->ducking);
354
356 SAFE_FREE(mixer->source_ids);
357 SAFE_FREE(mixer->source_active);
358 SAFE_FREE(mixer->mix_buffer);
359 SAFE_FREE(mixer);
360 log_info("Audio mixer destroyed");
361}
void ducking_free(ducking_t *duck)
Free ducking system resources.
Definition mixer.c:166

References mixer_t::ducking, ducking_free(), log_info, mixer_t::mix_buffer, rwlock_destroy(), SAFE_FREE, mixer_t::source_active, mixer_t::source_buffers, mixer_t::source_ids, and mixer_t::source_lock.

Referenced by server_main().

◆ mixer_process()

int mixer_process ( mixer_t mixer,
float *  output,
int  num_samples 
)

#include <mixer.h>

Process audio from all active sources.

Parameters
mixerAudio mixer
outputOutput buffer for mixed audio (must have num_samples elements)
num_samplesNumber of samples to process
Returns
Number of samples processed on success, negative on error

Reads audio from all active sources, applies processing pipeline (ducking, mixing, compression, noise gate, high-pass filter), and writes mixed output to output buffer.

Note
Processing includes all stages: ducking, mixing, compression, noise gating, high-pass filtering, and soft clipping.
Thread-safe (acquires read lock for source access).
Warning
Output buffer must have at least num_samples float elements.

Definition at line 459 of file mixer.c.

459 {
460 if (!mixer || !output || num_samples <= 0)
461 return -1;
462
463 // THREAD SAFETY: Acquire read lock to protect against concurrent source add/remove
464 // This prevents race conditions where source_buffers[i] could be set to NULL while we read it
465 rwlock_rdlock(&mixer->source_lock);
466
467 // Clear output buffer
468 SAFE_MEMSET(output, num_samples * sizeof(float), 0, num_samples * sizeof(float));
469
470 // Count active sources
471 int active_count = 0;
472 for (int i = 0; i < mixer->max_sources; i++) {
473 if (mixer->source_ids[i] != 0 && mixer->source_active[i] && mixer->source_buffers[i]) {
474 active_count++;
475 }
476 }
477
478 if (active_count == 0) {
479 // No active sources, output silence
480 return 0;
481 }
482
483 // Process in frames for efficiency
484 for (int frame_start = 0; frame_start < num_samples; frame_start += MIXER_FRAME_SIZE) {
485 int frame_size = (frame_start + MIXER_FRAME_SIZE > num_samples) ? (num_samples - frame_start) : MIXER_FRAME_SIZE;
486
487 // Clear mix buffer
488 SAFE_MEMSET(mixer->mix_buffer, frame_size * sizeof(float), 0, frame_size * sizeof(float));
489
490 // Temporary buffers for source audio
491 float source_samples[MIXER_MAX_SOURCES][MIXER_FRAME_SIZE];
492 int source_count = 0;
493 int source_map[MIXER_MAX_SOURCES]; // Maps source index to slot
494
495 // Read from each active source
496 for (int i = 0; i < mixer->max_sources; i++) {
497 if (mixer->source_ids[i] != 0 && mixer->source_active[i] && mixer->source_buffers[i]) {
498 // Read samples from this source's ring buffer
499 size_t samples_read_size =
500 audio_ring_buffer_read(mixer->source_buffers[i], source_samples[source_count], frame_size);
501 int samples_read = (int)samples_read_size;
502
503 // Accept partial frames - pad with silence if needed
504 // This prevents audio dropouts when ring buffers are temporarily under-filled
505 if (samples_read > 0) {
506 // Pad remaining samples with silence if we got a partial frame
507 if (samples_read < frame_size) {
508 SAFE_MEMSET(&source_samples[source_count][samples_read], (frame_size - samples_read) * sizeof(float), 0,
509 (frame_size - samples_read) * sizeof(float));
510 }
511
512 source_map[source_count] = i;
513 source_count++;
514
515 if (source_count >= MIXER_MAX_SOURCES)
516 break;
517 }
518 }
519 }
520
521 // OPTIMIZATION: Batch envelope calculation per-frame instead of per-sample
522 // Calculate peak amplitude for each source over the entire frame
523 int speaking_count = 0;
524
525 for (int i = 0; i < source_count; i++) {
526 int slot = source_map[i];
527 float peak = 0.0f;
528
529 // Find peak amplitude in frame (much faster than per-sample envelope)
530 for (int s = 0; s < frame_size; s++) {
531 float abs_sample = fabsf(source_samples[i][s]);
532 if (abs_sample > peak)
533 peak = abs_sample;
534 }
535
536 // Update envelope using frame peak (one update per frame instead of per-sample)
537 if (peak > mixer->ducking.envelope[slot]) {
538 mixer->ducking.envelope[slot] =
539 mixer->ducking.attack_coeff * mixer->ducking.envelope[slot] + (1.0f - mixer->ducking.attack_coeff) * peak;
540 } else {
541 mixer->ducking.envelope[slot] =
542 mixer->ducking.release_coeff * mixer->ducking.envelope[slot] + (1.0f - mixer->ducking.release_coeff) * peak;
543 }
544
545 // Count speaking sources
546 if (mixer->ducking.envelope[slot] > db_to_linear(-60.0f))
547 speaking_count++;
548 }
549
550 // Apply ducking ONCE per frame (not per-sample)
551 ducking_process_frame(&mixer->ducking, mixer->ducking.envelope, mixer->ducking.gain, mixer->max_sources);
552
553 // Calculate crowd scaling ONCE per frame
554 float crowd_gain = (speaking_count > 0) ? (1.0f / powf((float)speaking_count, mixer->crowd_alpha)) : 1.0f;
555 float pre_bus = mixer->base_gain * crowd_gain;
556
557 // Pre-calculate combined gains for each source (ducking * pre_bus)
558 float combined_gains[MIXER_MAX_SOURCES];
559 for (int i = 0; i < source_count; i++) {
560 int slot = source_map[i];
561 combined_gains[i] = mixer->ducking.gain[slot] * pre_bus;
562 }
563
564 // OPTIMIZATION: Fast mixing loop - simple multiply-add with pre-calculated gains
565 // NO per-sample compressor to avoid expensive log/pow calls (480x per iteration)
566 for (int s = 0; s < frame_size; s++) {
567 float mix = 0.0f;
568 for (int i = 0; i < source_count; i++) {
569 mix += source_samples[i][s] * combined_gains[i];
570 }
571
572 // Store in mix buffer for frame-level compression below
573 mixer->mix_buffer[s] = mix;
574 }
575
576 // OPTIMIZATION: Apply compression ONCE per frame instead of per-sample
577 // This reduces expensive log10f/powf calls from 480x to 1x per iteration
578 // Calculate frame peak for compressor sidechain
579 float frame_peak = 0.0f;
580 for (int s = 0; s < frame_size; s++) {
581 float abs_val = fabsf(mixer->mix_buffer[s]);
582 if (abs_val > frame_peak)
583 frame_peak = abs_val;
584 }
585
586 // Process compressor with frame peak (1 call instead of 480)
587 float comp_gain = compressor_process_sample(&mixer->compressor, frame_peak);
588
589 // Apply compression gain and soft clipping to all samples
590 for (int s = 0; s < frame_size; s++) {
591 float mix = mixer->mix_buffer[s] * comp_gain;
592
593 // Compressor provides +6dB makeup gain for better audibility
594 // Soft clip threshold 1.0f allows full range without premature clipping
595 // Steepness 3.0 for smoother clipping behavior
596 output[frame_start + s] = soft_clip(mix, 1.0f, 3.0f);
597 }
598 }
599
601 return num_samples;
602}
void ducking_process_frame(ducking_t *duck, float *envelopes, float *gains, int num_sources)
Process a frame of audio through ducking system.
Definition mixer.c:184
float soft_clip(float sample, float threshold, float steepness)
Apply soft clipping to a sample.
Definition mixer.c:1019
float compressor_process_sample(compressor_t *comp, float sidechain)
Process a single sample through compressor.
Definition mixer.c:87
#define rwlock_rdlock(lock)
Acquire a read lock (with debug tracking in debug builds)
Definition rwlock.h:194
#define rwlock_rdunlock(lock)
Release a read lock (with debug tracking in debug builds)
Definition rwlock.h:231

References ducking_t::attack_coeff, audio_ring_buffer_read(), mixer_t::base_gain, mixer_t::compressor, compressor_process_sample(), mixer_t::crowd_alpha, db_to_linear(), mixer_t::ducking, ducking_process_frame(), ducking_t::envelope, ducking_t::gain, mixer_t::max_sources, mixer_t::mix_buffer, MIXER_FRAME_SIZE, MIXER_MAX_SOURCES, ducking_t::release_coeff, rwlock_rdlock, rwlock_rdunlock, SAFE_MEMSET, soft_clip(), mixer_t::source_active, mixer_t::source_buffers, mixer_t::source_ids, and mixer_t::source_lock.

◆ mixer_process_excluding_source()

int mixer_process_excluding_source ( mixer_t mixer,
float *  output,
int  num_samples,
uint32_t  exclude_client_id 
)

#include <mixer.h>

Process audio from all sources except one (for per-client output)

Parameters
mixerAudio mixer
outputOutput buffer for mixed audio
num_samplesNumber of samples to process
exclude_client_idClient ID to exclude from mixing
Returns
Number of samples processed on success, negative on error

Same as mixer_process() but excludes one client's audio from the mix. Used to generate per-client output that doesn't include their own audio (prevents echo and feedback).

Note
Processing pipeline is identical to mixer_process().
Thread-safe (acquires read lock for source access).

Definition at line 604 of file mixer.c.

604 {
605 if (!mixer || !output || num_samples <= 0)
606 return -1;
607
608 // Only use timing in debug builds - snprintf + hashtable ops are expensive in hot path
609#ifndef NDEBUG
610 START_TIMER("mixer_total");
611#endif
612
613 // THREAD SAFETY: Acquire read lock to protect against concurrent source add/remove
614 // This prevents race conditions where source_buffers[i] could be set to NULL while we read it
615 rwlock_rdlock(&mixer->source_lock);
616
617 // Clear output buffer
618 SAFE_MEMSET(output, num_samples * sizeof(float), 0, num_samples * sizeof(float));
619
620 // OPTIMIZATION 1: O(1) exclusion using bitset and hash table
621 uint8_t exclude_index = mixer->source_id_to_index[exclude_client_id & 0xFF];
622 uint64_t active_mask = mixer->active_sources_mask;
623
624 // Validate exclude_index before using in bitshift
625 // - exclude_index == 0xFF means "not found" (sentinel value from initialization)
626 // - exclude_index >= MIXER_MAX_SOURCES would cause undefined behavior in bitshift
627 // - Also verify hash table lookup actually matched the client_id (collision detection)
628 bool valid_exclude = (exclude_index < MIXER_MAX_SOURCES && exclude_index != 0xFF &&
629 mixer->source_ids[exclude_index] == exclude_client_id);
630
631 if (valid_exclude) {
632 // Check if this is the ONLY active source - if so, drain it to prevent buffer overflow
633 // With N>1 clients, other threads drain this buffer. With N=1, no one does.
634 uint64_t mask_without_excluded = active_mask & ~(1ULL << exclude_index);
635 if (mask_without_excluded == 0 && mixer->source_buffers[exclude_index]) {
636 // Solo client: drain their buffer to prevent overflow (discard samples)
637 // LOCK-FREE: Skip samples directly using atomic operations
638 audio_ring_buffer_t *rb = mixer->source_buffers[exclude_index];
639 if (rb) {
640 size_t available = audio_ring_buffer_available_read(rb);
641 size_t to_skip = ((size_t)num_samples < available) ? (size_t)num_samples : available;
642
643 // LOCK-FREE: Atomically advance read_index with release ordering
644 unsigned int old_read_idx = atomic_load_explicit(&rb->read_index, memory_order_relaxed);
645 unsigned int new_read_idx = (old_read_idx + to_skip) % AUDIO_RING_BUFFER_SIZE;
646 atomic_store_explicit(&rb->read_index, new_read_idx, memory_order_release);
647
648 log_debug_every(LOG_RATE_DEFAULT, "Mixer: Drained %zu samples for solo client %u (lock-free skip)", to_skip,
649 exclude_client_id);
650 }
651 }
652 active_mask = mask_without_excluded;
653
654 // DIAGNOSTIC: Log which client was excluded and which remain active
656 1000000,
657 "MIXER EXCLUSION: exclude_client=%u, exclude_index=%u, active_mask_before=0x%llx, active_mask_after=0x%llx",
658 exclude_client_id, exclude_index, (unsigned long long)(active_mask | (1ULL << exclude_index)),
659 (unsigned long long)active_mask);
660 } else {
661 // DIAGNOSTIC: Failed to exclude - log why
662 log_warn_every(1000000, "MIXER EXCLUSION FAILED: exclude_client=%u, exclude_index=%u (valid=%d), lookup_id=%u",
663 exclude_client_id, exclude_index, valid_exclude,
664 (exclude_index < MIXER_MAX_SOURCES && exclude_index != 0xFF) ? mixer->source_ids[exclude_index] : 0);
665 }
666
667 // Fast check: any sources to mix?
668 if (active_mask == 0) {
670#ifndef NDEBUG
671 STOP_TIMER("mixer_total");
672#endif
673 return 0; // No sources to mix (excluding the specified client), output silence
674 }
675
676 // Process in frames for efficiency
677 for (int frame_start = 0; frame_start < num_samples; frame_start += MIXER_FRAME_SIZE) {
678 int frame_size = (frame_start + MIXER_FRAME_SIZE > num_samples) ? (num_samples - frame_start) : MIXER_FRAME_SIZE;
679
680 struct timespec read_start, read_end;
681 (void)clock_gettime(CLOCK_MONOTONIC, &read_start);
682
683 // Clear mix buffer
684 SAFE_MEMSET(mixer->mix_buffer, frame_size * sizeof(float), 0, frame_size * sizeof(float));
685
686 // Temporary buffers for source audio
687 float source_samples[MIXER_MAX_SOURCES][MIXER_FRAME_SIZE];
688 int source_count = 0;
689 int source_map[MIXER_MAX_SOURCES]; // Maps source index to slot
690
691 // OPTIMIZATION 1: Iterate only over active sources using bitset
692 uint64_t current_mask = active_mask;
693 while (current_mask && source_count < MIXER_MAX_SOURCES) {
694 int i = find_first_set_bit(current_mask); // Portable: find first set bit
695 current_mask &= current_mask - 1; // Clear lowest set bit
696
697 // Verify source is valid (defensive programming)
698 if (i < mixer->max_sources && mixer->source_ids[i] != 0 && mixer->source_buffers[i]) {
699 // Read samples from this source's ring buffer
700 size_t samples_read_size =
701 audio_ring_buffer_read(mixer->source_buffers[i], source_samples[source_count], frame_size);
702 int samples_read = (int)samples_read_size;
703
704 // Accept partial frames - pad with silence if needed
705 // This prevents audio dropouts when ring buffers are temporarily under-filled
706 if (samples_read > 0) {
707 // Pad remaining samples with silence if we got a partial frame
708 if (samples_read < frame_size) {
709 SAFE_MEMSET(&source_samples[source_count][samples_read], (frame_size - samples_read) * sizeof(float), 0,
710 (frame_size - samples_read) * sizeof(float));
711 }
712
713 // DIAGNOSTIC: Calculate RMS of this source's audio
714 float source_rms = 0.0f;
715 if (frame_start == 0) { // Only log for first frame to reduce spam
716 float sum_squares = 0.0f;
717 for (int s = 0; s < samples_read; s++) {
718 sum_squares += source_samples[source_count][s] * source_samples[source_count][s];
719 }
720 source_rms = sqrtf(sum_squares / (float)samples_read);
721 log_info_every(1000000, "MIXER SOURCE READ: client_id=%u, slot=%d, samples_read=%d, RMS=%.6f",
722 mixer->source_ids[i], i, samples_read, source_rms);
723 }
724
725 source_map[source_count] = i;
726 source_count++;
727 }
728 }
729 }
730
731 (void)clock_gettime(CLOCK_MONOTONIC, &read_end);
732 uint64_t read_time_us = ((uint64_t)read_end.tv_sec * 1000000 + (uint64_t)read_end.tv_nsec / 1000) -
733 ((uint64_t)read_start.tv_sec * 1000000 + (uint64_t)read_start.tv_nsec / 1000);
734
735 if (read_time_us > 10000) { // Log if reading sources takes > 10ms
736 log_warn_every(LOG_RATE_DEFAULT, "Mixer: Slow source reading took %lluus (%.2fms) for %d sources", read_time_us,
737 (float)read_time_us / 1000.0f, source_count);
738 }
739
740 // OPTIMIZATION: Batch envelope calculation per-frame instead of per-sample
741 // Calculate peak amplitude for each source over the entire frame
742 int speaking_count = 0;
743
744 for (int i = 0; i < source_count; i++) {
745 int slot = source_map[i];
746 float peak = 0.0f;
747
748 // Find peak amplitude in frame (much faster than per-sample envelope)
749 for (int s = 0; s < frame_size; s++) {
750 float abs_sample = fabsf(source_samples[i][s]);
751 if (abs_sample > peak)
752 peak = abs_sample;
753 }
754
755 // Update envelope using frame peak (one update per frame instead of per-sample)
756 if (peak > mixer->ducking.envelope[slot]) {
757 mixer->ducking.envelope[slot] =
758 mixer->ducking.attack_coeff * mixer->ducking.envelope[slot] + (1.0f - mixer->ducking.attack_coeff) * peak;
759 } else {
760 mixer->ducking.envelope[slot] =
761 mixer->ducking.release_coeff * mixer->ducking.envelope[slot] + (1.0f - mixer->ducking.release_coeff) * peak;
762 }
763
764 // Count speaking sources
765 if (mixer->ducking.envelope[slot] > db_to_linear(-60.0f))
766 speaking_count++;
767 }
768
769 // Apply ducking ONCE per frame (not per-sample)
770 ducking_process_frame(&mixer->ducking, mixer->ducking.envelope, mixer->ducking.gain, mixer->max_sources);
771
772 // Calculate crowd scaling ONCE per frame
773 float crowd_gain = (speaking_count > 0) ? (1.0f / powf((float)speaking_count, mixer->crowd_alpha)) : 1.0f;
774 float pre_bus = mixer->base_gain * crowd_gain;
775
776 // Pre-calculate combined gains for each source (ducking * pre_bus)
777 float combined_gains[MIXER_MAX_SOURCES];
778 for (int i = 0; i < source_count; i++) {
779 int slot = source_map[i];
780 combined_gains[i] = mixer->ducking.gain[slot] * pre_bus;
781 }
782
783 // OPTIMIZATION: Fast mixing loop - simple multiply-add with pre-calculated gains
784 // NO per-sample compressor to avoid expensive log/pow calls (480x per iteration)
785 for (int s = 0; s < frame_size; s++) {
786 float mix = 0.0f;
787 for (int i = 0; i < source_count; i++) {
788 mix += source_samples[i][s] * combined_gains[i];
789 }
790
791 // Store in mix buffer for frame-level compression below
792 mixer->mix_buffer[s] = mix;
793 }
794
795 // OPTIMIZATION: Apply compression ONCE per frame instead of per-sample
796 // This reduces expensive log10f/powf calls from 480x to 1x per iteration
797 // Calculate frame peak for compressor sidechain
798 float frame_peak = 0.0f;
799 for (int s = 0; s < frame_size; s++) {
800 float abs_val = fabsf(mixer->mix_buffer[s]);
801 if (abs_val > frame_peak)
802 frame_peak = abs_val;
803 }
804
805 // Process compressor with frame peak (1 call instead of 480)
806 float comp_gain = compressor_process_sample(&mixer->compressor, frame_peak);
807
808 // Apply compression gain and soft clipping to all samples
809 for (int s = 0; s < frame_size; s++) {
810 float mix = mixer->mix_buffer[s] * comp_gain;
811
812 // Compressor provides +6dB makeup gain for better audibility
813 // Soft clip threshold 1.0f allows full range without premature clipping
814 // Steepness 3.0 for smoother clipping behavior
815 output[frame_start + s] = soft_clip(mix, 1.0f, 3.0f);
816 }
817 }
818
820
821#ifndef NDEBUG
822 double total_ns = STOP_TIMER("mixer_total");
823 if (total_ns > 2000000) { // > 2ms
824 char duration_str[32];
825 format_duration_ns(total_ns, duration_str, sizeof(duration_str));
826 log_warn("Slow mixer: total=%s, num_samples=%d", duration_str, num_samples);
827 }
828#endif
829
830 return num_samples;
831}
unsigned long long uint64_t
Definition common.h:59
#define LOG_RATE_DEFAULT
Log rate limit: 5 seconds (5,000,000 microseconds) - default for audio/video packets.
Definition log_rates.h:32
#define log_info_every(interval_us, fmt,...)
Rate-limited INFO logging.
int format_duration_ns(double nanoseconds, char *buffer, size_t buffer_size)
Format nanoseconds as human-readable duration string.
Definition time.c:187
#define START_TIMER(name_fmt,...)
Start a timer with formatted name.
Definition time.h:141
#define STOP_TIMER(name_fmt,...)
Stop a timer with formatted name and return elapsed time.
Definition time.h:165

References mixer_t::active_sources_mask, ducking_t::attack_coeff, audio_ring_buffer_available_read(), audio_ring_buffer_read(), AUDIO_RING_BUFFER_SIZE, mixer_t::base_gain, mixer_t::compressor, compressor_process_sample(), mixer_t::crowd_alpha, db_to_linear(), mixer_t::ducking, ducking_process_frame(), ducking_t::envelope, format_duration_ns(), ducking_t::gain, log_debug_every, log_info_every, LOG_RATE_DEFAULT, log_warn, log_warn_every, mixer_t::max_sources, mixer_t::mix_buffer, MIXER_FRAME_SIZE, MIXER_MAX_SOURCES, audio_ring_buffer::read_index, ducking_t::release_coeff, rwlock_rdlock, rwlock_rdunlock, SAFE_MEMSET, soft_clip(), mixer_t::source_buffers, mixer_t::source_id_to_index, mixer_t::source_ids, mixer_t::source_lock, START_TIMER, and STOP_TIMER.

Referenced by client_audio_render_thread().

◆ mixer_remove_source()

void mixer_remove_source ( mixer_t mixer,
uint32_t  client_id 
)

#include <mixer.h>

Remove an audio source from the mixer.

Parameters
mixerAudio mixer
client_idClient ID to remove

Removes the audio source associated with the given client ID. Source slot is freed for reuse by another client.

Note
Thread-safe (acquires write lock internally).
If client_id is not found, operation has no effect.

Definition at line 400 of file mixer.c.

400 {
401 if (!mixer)
402 return;
403
404 // OPTIMIZATION 2: Acquire write lock for source modification
405 rwlock_wrlock(&mixer->source_lock);
406
407 for (int i = 0; i < mixer->max_sources; i++) {
408 if (mixer->source_ids[i] == client_id) {
409 mixer->source_buffers[i] = NULL;
410 mixer->source_ids[i] = 0;
411 mixer->source_active[i] = false;
412 mixer->num_sources--;
413
414 // OPTIMIZATION 1: Update bitset optimization structures
415 mixer->active_sources_mask &= ~(1ULL << i); // Clear bit for this slot
416 mixer->source_id_to_index[client_id & 0xFF] = 0xFF; // Mark as invalid in hash table
417
418 // Reset ducking state for this source
419 mixer->ducking.envelope[i] = 0.0f;
420 mixer->ducking.gain[i] = 1.0f;
421
423
424 log_info("Mixer: Removed source for client %u from slot %d", client_id, i);
425 return;
426 }
427 }
428
430}

References mixer_t::active_sources_mask, mixer_t::ducking, ducking_t::envelope, ducking_t::gain, log_info, mixer_t::max_sources, mixer_t::num_sources, rwlock_wrlock, rwlock_wrunlock, mixer_t::source_active, mixer_t::source_buffers, mixer_t::source_id_to_index, mixer_t::source_ids, and mixer_t::source_lock.

◆ mixer_set_source_active()

void mixer_set_source_active ( mixer_t mixer,
uint32_t  client_id,
bool  active 
)

#include <mixer.h>

Set whether a source is active (receiving audio)

Parameters
mixerAudio mixer
client_idClient ID
activetrue to mark source as active, false to mark inactive

Updates the active status of a source. Inactive sources are excluded from mixing operations for efficiency.

Note
Thread-safe (acquires write lock internally).
Source must exist (be added) before calling this function.

Definition at line 432 of file mixer.c.

432 {
433 if (!mixer)
434 return;
435
436 // OPTIMIZATION 2: Acquire write lock for source modification
437 rwlock_wrlock(&mixer->source_lock);
438
439 for (int i = 0; i < mixer->max_sources; i++) {
440 if (mixer->source_ids[i] == client_id) {
441 mixer->source_active[i] = active;
442
443 // OPTIMIZATION 1: Update bitset for active state change
444 if (active) {
445 mixer->active_sources_mask |= (1ULL << i); // Set bit
446 } else {
447 mixer->active_sources_mask &= ~(1ULL << i); // Clear bit
448 }
449
451 log_debug("Mixer: Set source %u active=%d", client_id, active);
452 return;
453 }
454 }
455
457}

References mixer_t::active_sources_mask, log_debug, mixer_t::max_sources, rwlock_wrlock, rwlock_wrunlock, mixer_t::source_active, mixer_t::source_ids, and mixer_t::source_lock.

◆ noise_gate_init()

void noise_gate_init ( noise_gate_t gate,
float  sample_rate 
)

#include <mixer.h>

Initialize a noise gate.

Parameters
gateNoise gate structure
sample_rateSample rate in Hz

Initializes noise gate state and converts time-based parameters to coefficients.

Definition at line 838 of file mixer.c.

838 {
839 if (!gate)
840 return;
841
842 gate->sample_rate = sample_rate;
843 gate->envelope = 0.0f;
844 gate->gate_open = false;
845
846 // Default parameters
847 // Slower attack (10ms) prevents clicking artifacts during gate transitions
848 // threshold=0.01, attack=10ms, release=50ms, hysteresis=0.9
849 noise_gate_set_params(gate, 0.01f, 10.0f, 50.0f, 0.9f);
850}
float sample_rate
Sample rate in Hz (set during initialization)
Definition mixer.h:191
float envelope
Current envelope follower state (linear, 0-1)
Definition mixer.h:193
bool gate_open
True if gate is currently open (allowing audio through)
Definition mixer.h:199
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

References noise_gate_t::envelope, noise_gate_t::gate_open, noise_gate_set_params(), and noise_gate_t::sample_rate.

Referenced by client_audio_pipeline_create().

◆ noise_gate_is_open()

bool noise_gate_is_open ( const noise_gate_t gate)

#include <mixer.h>

Check if noise gate is currently open.

Parameters
gateNoise gate structure
Returns
true if gate is open, false if closed

Returns current gate state. Useful for debugging and monitoring.

Definition at line 911 of file mixer.c.

911 {
912 return gate ? gate->gate_open : false;
913}

References noise_gate_t::gate_open.

◆ noise_gate_process_buffer()

void noise_gate_process_buffer ( noise_gate_t gate,
float *  buffer,
int  num_samples 
)

#include <mixer.h>

Process a buffer of samples through noise gate.

Parameters
gateNoise gate structure
bufferAudio buffer (modified in-place)
num_samplesNumber of samples to process

Processes entire buffer through noise gate. Buffer is modified in-place.

Note
Peak amplitude is calculated per sample for gate decision.

Definition at line 892 of file mixer.c.

892 {
893 if (!gate || !buffer || num_samples <= 0)
894 return;
895
896 // First pass: find peak amplitude
897 float peak = 0.0f;
898 for (int i = 0; i < num_samples; i++) {
899 float abs_sample = fabsf(buffer[i]);
900 if (abs_sample > peak) {
901 peak = abs_sample;
902 }
903 }
904
905 // Second pass: apply gate
906 for (int i = 0; i < num_samples; i++) {
907 buffer[i] = noise_gate_process_sample(gate, buffer[i], peak);
908 }
909}
float noise_gate_process_sample(noise_gate_t *gate, float input, float peak_amplitude)
Process a single sample through noise gate.
Definition mixer.c:867

References noise_gate_process_sample().

Referenced by client_audio_pipeline_playback(), and client_audio_pipeline_process_duplex().

◆ noise_gate_process_sample()

float noise_gate_process_sample ( noise_gate_t gate,
float  input,
float  peak_amplitude 
)

#include <mixer.h>

Process a single sample through noise gate.

Parameters
gateNoise gate structure
inputInput sample value
peak_amplitudePeak amplitude for gate decision
Returns
Gated output sample (input if gate open, 0 if closed)

Processes input sample through noise gate. Gate opens/closes based on peak_amplitude compared to threshold (with hysteresis).

Note
peak_amplitude should be envelope follower output, not raw sample.

Definition at line 867 of file mixer.c.

867 {
868 if (!gate)
869 return input;
870
871 // Determine target state with hysteresis
872 float target;
873 if (gate->gate_open) {
874 // Gate is open - use lower threshold (hysteresis) to close
875 target = (peak_amplitude > gate->threshold * gate->hysteresis) ? 1.0f : 0.0f;
876 } else {
877 // Gate is closed - use normal threshold to open
878 target = (peak_amplitude > gate->threshold) ? 1.0f : 0.0f;
879 }
880
881 // Update gate state
882 gate->gate_open = (target > 0.5f);
883
884 // Update envelope with appropriate coefficient
885 float coeff = (target > gate->envelope) ? gate->attack_coeff : gate->release_coeff;
886 gate->envelope += coeff * (target - gate->envelope);
887
888 // Apply gate
889 return input * gate->envelope;
890}
float hysteresis
Hysteresis factor (0-1, prevents gate chatter)
Definition mixer.h:188
float threshold
Gate threshold in linear units (e.g., 0.01f for -40dB)
Definition mixer.h:182
float attack_coeff
Attack coefficient (converted from attack_ms)
Definition mixer.h:195

References noise_gate_t::attack_coeff, noise_gate_t::envelope, noise_gate_t::gate_open, noise_gate_t::hysteresis, noise_gate_t::release_coeff, and noise_gate_t::threshold.

Referenced by noise_gate_process_buffer().

◆ noise_gate_set_params()

void noise_gate_set_params ( noise_gate_t gate,
float  threshold,
float  attack_ms,
float  release_ms,
float  hysteresis 
)

#include <mixer.h>

Set noise gate parameters.

Parameters
gateNoise gate structure
thresholdGate threshold in linear units
attack_msAttack time in milliseconds
release_msRelease time in milliseconds
hysteresisHysteresis factor (0-1) to prevent chatter

Updates noise gate parameters. Time-based parameters are converted to coefficients internally.

Definition at line 852 of file mixer.c.

852 {
853 if (!gate)
854 return;
855
856 gate->threshold = threshold;
857 gate->attack_ms = attack_ms;
858 gate->release_ms = release_ms;
859 gate->hysteresis = hysteresis;
860
861 // Calculate coefficients for envelope follower
862 // Using exponential moving average: coeff = 1 - exp(-1 / (time_ms * sample_rate / 1000))
863 gate->attack_coeff = 1.0f - expf(-1.0f / (attack_ms * gate->sample_rate / 1000.0f));
864 gate->release_coeff = 1.0f - expf(-1.0f / (release_ms * gate->sample_rate / 1000.0f));
865}
float release_coeff
Release coefficient (converted from release_ms)
Definition mixer.h:197
float release_ms
Release time in milliseconds (how fast gate closes)
Definition mixer.h:186
float attack_ms
Attack time in milliseconds (how fast gate opens)
Definition mixer.h:184

References noise_gate_t::attack_coeff, noise_gate_t::attack_ms, noise_gate_t::hysteresis, noise_gate_t::release_coeff, noise_gate_t::release_ms, noise_gate_t::sample_rate, and noise_gate_t::threshold.

Referenced by client_audio_pipeline_create(), and noise_gate_init().

◆ opus_codec_create_decoder()

opus_codec_t * opus_codec_create_decoder ( int  sample_rate)

#include <opus_codec.h>

Create an Opus decoder.

Parameters
sample_rateSample rate in Hz (must match encoder)
Returns
Pointer to new decoder, NULL on error

Creates a new Opus decoder instance for decompressing audio.

Warning
Returned pointer must be freed with opus_codec_destroy()

Definition at line 62 of file opus_codec.c.

62 {
63 if (sample_rate <= 0) {
64 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid sample rate: %d", sample_rate);
65 return NULL;
66 }
67
69 if (!codec) {
70 SET_ERRNO(ERROR_MEMORY, "Failed to allocate opus codec context");
71 return NULL;
72 }
73
74 // Create decoder
75 int error = OPUS_OK;
76 codec->decoder = opus_decoder_create(sample_rate, 1, &error);
77 if (error != OPUS_OK || !codec->decoder) {
78 SET_ERRNO(ERROR_AUDIO, "Failed to create Opus decoder: %s", opus_strerror(error));
79 SAFE_FREE(codec);
80 return NULL;
81 }
82
83 codec->encoder = NULL;
84 codec->sample_rate = sample_rate;
85 codec->bitrate = 0; // N/A for decoder
86 codec->tmp_buffer = NULL;
87
88 log_debug("Opus decoder created: sample_rate=%d", sample_rate);
89
90 return codec;
91}
uint8_t * tmp_buffer
Temporary buffer for internal use.
Definition opus_codec.h:100
int bitrate
Bitrate in bits per second (encoder only)
Definition opus_codec.h:99
int sample_rate
Sample rate in Hz (e.g., 44100)
Definition opus_codec.h:98
OpusEncoder * encoder
Encoder state (NULL if decoder)
Definition opus_codec.h:96
OpusDecoder * decoder
Decoder state (NULL if encoder)
Definition opus_codec.h:97

References opus_codec_t::bitrate, opus_codec_t::decoder, opus_codec_t::encoder, ERROR_AUDIO, ERROR_INVALID_PARAM, ERROR_MEMORY, log_debug, SAFE_FREE, SAFE_MALLOC, opus_codec_t::sample_rate, SET_ERRNO, and opus_codec_t::tmp_buffer.

Referenced by handle_stream_start_packet().

◆ opus_codec_create_encoder()

opus_codec_t * opus_codec_create_encoder ( opus_application_t  application,
int  sample_rate,
int  bitrate 
)

#include <opus_codec.h>

Create an Opus encoder.

Parameters
applicationApplication mode (OPUS_APPLICATION_VOIP for voice)
sample_rateSample rate in Hz (8000, 12000, 16000, 24000, or 48000)
bitrateTarget bitrate in bits per second (6000-128000 typical)
Returns
Pointer to new encoder, NULL on error

Creates a new Opus encoder instance for compressing audio.

Note
Common bitrates:
  • 16 kbps: Good quality voice
  • 24 kbps: Excellent quality voice
  • 64 kbps: High quality audio
Warning
Returned pointer must be freed with opus_codec_destroy()

Definition at line 18 of file opus_codec.c.

18 {
19 if (sample_rate <= 0 || bitrate <= 0) {
20 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid codec parameters: sample_rate=%d, bitrate=%d", sample_rate, bitrate);
21 return NULL;
22 }
23
25 if (!codec) {
26 SET_ERRNO(ERROR_MEMORY, "Failed to allocate opus codec context");
27 return NULL;
28 }
29
30 // Create encoder
31 int error = OPUS_OK;
32 codec->encoder = opus_encoder_create(sample_rate, 1, (int)application, &error);
33 if (error != OPUS_OK || !codec->encoder) {
34 SET_ERRNO(ERROR_AUDIO, "Failed to create Opus encoder: %s", opus_strerror(error));
35 SAFE_FREE(codec);
36 return NULL;
37 }
38
39 // Set bitrate
40 error = opus_encoder_ctl(codec->encoder, OPUS_SET_BITRATE(bitrate));
41 if (error != OPUS_OK) {
42 SET_ERRNO(ERROR_AUDIO, "Failed to set Opus bitrate: %s", opus_strerror(error));
43 opus_encoder_destroy(codec->encoder);
44 SAFE_FREE(codec);
45 return NULL;
46 }
47
48 codec->decoder = NULL;
49 codec->sample_rate = sample_rate;
50 codec->bitrate = bitrate;
51 codec->tmp_buffer = NULL;
52
53 log_debug("Opus encoder created: sample_rate=%d, bitrate=%d bps", sample_rate, bitrate);
54
55 return codec;
56}

References opus_codec_t::bitrate, opus_codec_t::decoder, opus_codec_t::encoder, ERROR_AUDIO, ERROR_INVALID_PARAM, ERROR_MEMORY, log_debug, SAFE_FREE, SAFE_MALLOC, opus_codec_t::sample_rate, SET_ERRNO, and opus_codec_t::tmp_buffer.

Referenced by client_audio_render_thread().

◆ opus_codec_decode()

int opus_codec_decode ( opus_codec_t codec,
const uint8_t data,
size_t  data_len,
float *  out_samples,
int  out_num_samples 
)

#include <opus_codec.h>

Decode Opus audio frame.

Parameters
codecOpus decoder (created with opus_codec_create_decoder)
dataCompressed audio data (or NULL for PLC)
data_lenLength of compressed data in bytes
out_samplesOutput buffer for decoded samples (float)
out_num_samplesMaximum number of output samples
Returns
Number of samples decoded, negative on error

Decodes a compressed Opus frame back to PCM audio samples.

Note
To handle packet loss, pass NULL for data to enable PLC (Packet Loss Concealment)
Output will be exactly the frame size (typically 882 samples)
This function is NOT thread-safe for the same codec instance

Definition at line 128 of file opus_codec.c.

129 {
130 if (!codec || !codec->decoder || !out_samples || out_num_samples <= 0) {
132 "Invalid decode parameters: codec=%p, decoder=%p, out_samples=%p, out_num_samples=%d", (void *)codec,
133 (void *)(codec ? codec->decoder : NULL), (void *)out_samples, out_num_samples);
134 return -1;
135 }
136
137 // If data is NULL, use PLC (Packet Loss Concealment)
138 if (!data || data_len == 0) {
139 log_debug_every(LOG_RATE_VERY_FAST, "Opus PLC (Packet Loss Concealment)");
140 int samples = opus_decode_float(codec->decoder, NULL, 0, out_samples, out_num_samples, 0);
141 if (samples < 0) {
142 SET_ERRNO(ERROR_AUDIO, "Opus PLC failed: %s", opus_strerror(samples));
143 return -1;
144 }
145 return samples;
146 }
147
148 // Decode frame
149 int samples = opus_decode_float(codec->decoder, data, (opus_int32)data_len, out_samples, out_num_samples, 0);
150
151 if (samples < 0) {
152 SET_ERRNO(ERROR_AUDIO, "Opus decoding failed: %s", opus_strerror(samples));
153 return -1;
154 }
155
156 return samples;
157}
#define LOG_RATE_VERY_FAST
Log rate limit: 0.1 seconds (100,000 microseconds) - for very high frequency operations.
Definition log_rates.h:23

References opus_codec_t::decoder, ERROR_AUDIO, ERROR_INVALID_PARAM, log_debug_every, LOG_RATE_VERY_FAST, and SET_ERRNO.

Referenced by handle_audio_opus_batch_packet(), and handle_audio_opus_packet().

◆ opus_codec_destroy()

void opus_codec_destroy ( opus_codec_t codec)

#include <opus_codec.h>

Destroy an Opus codec instance.

Parameters
codecCodec to destroy (can be NULL)

Frees all resources associated with the codec. Safe to call multiple times or with NULL pointer.

Definition at line 215 of file opus_codec.c.

215 {
216 if (!codec) {
217 return;
218 }
219
220 if (codec->encoder) {
221 opus_encoder_destroy(codec->encoder);
222 codec->encoder = NULL;
223 }
224
225 if (codec->decoder) {
226 opus_decoder_destroy(codec->decoder);
227 codec->decoder = NULL;
228 }
229
230 if (codec->tmp_buffer) {
231 SAFE_FREE(codec->tmp_buffer);
232 codec->tmp_buffer = NULL;
233 }
234
235 SAFE_FREE(codec);
236}

References opus_codec_t::decoder, opus_codec_t::encoder, SAFE_FREE, and opus_codec_t::tmp_buffer.

Referenced by cleanup_client_media_buffers(), and client_audio_render_thread().

◆ opus_codec_encode()

size_t opus_codec_encode ( opus_codec_t codec,
const float *  samples,
int  num_samples,
uint8_t out_data,
size_t  out_size 
)

#include <opus_codec.h>

Encode audio frame with Opus.

Parameters
codecOpus encoder (created with opus_codec_create_encoder)
samplesInput audio samples (float, -1.0 to 1.0 range)
num_samplesNumber of input samples (882 for 20ms @ 44.1kHz)
out_dataOutput buffer for compressed audio
out_sizeMaximum output buffer size in bytes
Returns
Number of bytes written to out_data, negative on error

Encodes a frame of audio samples using Opus compression.

Note
Input must be exactly the correct frame size (typically 882 samples)
Output is typically 30-100 bytes depending on bitrate and content
This function is NOT thread-safe for the same codec instance

Definition at line 97 of file opus_codec.c.

98 {
99 if (!codec || !codec->encoder || !samples || num_samples <= 0 || !out_data || out_size == 0) {
101 "Invalid encode parameters: codec=%p, encoder=%p, samples=%p, num_samples=%d, out_data=%p, "
102 "out_size=%zu",
103 (void *)codec, (void *)(codec ? codec->encoder : NULL), (const void *)samples, num_samples,
104 (void *)out_data, out_size);
105 return 0;
106 }
107
108 // Encode frame
109 opus_int32 encoded_bytes = opus_encode_float(codec->encoder, samples, num_samples, out_data, (opus_int32)out_size);
110
111 if (encoded_bytes < 0) {
112 SET_ERRNO(ERROR_AUDIO, "Opus encoding failed: %s", opus_strerror((int)encoded_bytes));
113 return 0;
114 }
115
116 if (encoded_bytes == 0) {
117 // DTX frame (silence) - encoder produced zero bytes
118 log_debug_every(LOG_RATE_VERY_FAST, "Opus DTX frame (silence detected)");
119 }
120
121 return (size_t)encoded_bytes;
122}

References opus_codec_t::encoder, ERROR_AUDIO, ERROR_INVALID_PARAM, log_debug_every, LOG_RATE_VERY_FAST, and SET_ERRNO.

Referenced by client_audio_render_thread().

◆ opus_codec_get_bitrate()

int opus_codec_get_bitrate ( opus_codec_t codec)

#include <opus_codec.h>

Get current encoder bitrate.

Parameters
codecOpus encoder
Returns
Bitrate in bits per second, negative on error

Definition at line 180 of file opus_codec.c.

180 {
181 if (!codec || !codec->encoder) {
182 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid codec for bitrate query");
183 return -1;
184 }
185
186 opus_int32 bitrate = 0;
187 int error = opus_encoder_ctl(codec->encoder, OPUS_GET_BITRATE(&bitrate));
188 if (error != OPUS_OK) {
189 SET_ERRNO(ERROR_AUDIO, "Failed to get Opus bitrate: %s", opus_strerror(error));
190 return -1;
191 }
192
193 return (int)bitrate;
194}

References opus_codec_t::encoder, ERROR_AUDIO, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ opus_codec_set_bitrate()

asciichat_error_t opus_codec_set_bitrate ( opus_codec_t codec,
int  bitrate 
)

#include <opus_codec.h>

Set encoder bitrate.

Parameters
codecOpus encoder (created with opus_codec_create_encoder)
bitrateNew bitrate in bits per second
Returns
0 on success, negative on error

Changes the bitrate of an active encoder. This can be used to dynamically adjust quality based on network conditions.

Definition at line 163 of file opus_codec.c.

163 {
164 if (!codec || !codec->encoder || bitrate <= 0) {
165 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid bitrate parameters: codec=%p, encoder=%p, bitrate=%d", (void *)codec,
166 (void *)(codec ? codec->encoder : NULL), bitrate);
167 }
168
169 int error = opus_encoder_ctl(codec->encoder, OPUS_SET_BITRATE(bitrate));
170 if (error != OPUS_OK) {
171 return SET_ERRNO(ERROR_AUDIO, "Failed to set Opus bitrate: %s", opus_strerror(error));
172 }
173
174 codec->bitrate = bitrate;
175 log_debug("Opus bitrate changed to %d bps", bitrate);
176
177 return ASCIICHAT_OK;
178}

References ASCIICHAT_OK, opus_codec_t::bitrate, opus_codec_t::encoder, ERROR_AUDIO, ERROR_INVALID_PARAM, log_debug, and SET_ERRNO.

◆ opus_codec_set_dtx()

asciichat_error_t opus_codec_set_dtx ( opus_codec_t codec,
int  enable 
)

#include <opus_codec.h>

Enable/disable DTX (Discontinuous Transmission)

Parameters
codecOpus encoder
enable1 to enable DTX, 0 to disable
Returns
0 on success, negative on error

DTX allows the encoder to produce zero-byte frames during silence, reducing bandwidth usage significantly for voice communication.

Definition at line 196 of file opus_codec.c.

196 {
197 if (!codec || !codec->encoder) {
198 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid codec for DTX configuration");
199 }
200
201 int error = opus_encoder_ctl(codec->encoder, OPUS_SET_DTX(enable ? 1 : 0));
202 if (error != OPUS_OK) {
203 return SET_ERRNO(ERROR_AUDIO, "Failed to set Opus DTX: %s", opus_strerror(error));
204 }
205
206 log_debug("Opus DTX %s", enable ? "enabled" : "disabled");
207
208 return ASCIICHAT_OK;
209}

References ASCIICHAT_OK, opus_codec_t::encoder, ERROR_AUDIO, ERROR_INVALID_PARAM, log_debug, and SET_ERRNO.

◆ resample_linear()

void resample_linear ( const float *  src,
size_t  src_samples,
float *  dst,
size_t  dst_samples,
double  src_rate,
double  dst_rate 
)

#include <audio.h>

Resample audio using linear interpolation.

Parameters
srcSource samples at src_rate
src_samplesNumber of source samples
dstDestination buffer at dst_rate
dst_samplesNumber of destination samples to produce
src_rateSource sample rate (e.g., 48000)
dst_rateDestination sample rate (e.g., 44100)

Performs simple linear interpolation resampling from one sample rate to another. Suitable for real-time audio where computational efficiency is more important than perfect audio quality. For higher quality, consider a polyphase resampler.

Example usage:

// Resample from 48kHz buffer to 44.1kHz output
size_t in_samples = 960; // 20ms at 48kHz
size_t out_samples = 882; // 20ms at 44.1kHz
resample_linear(input, in_samples, output, out_samples, 48000, 44100);
void resample_linear(const float *src, size_t src_samples, float *dst, size_t dst_samples, double src_rate, double dst_rate)
Resample audio using linear interpolation.
Note
If src_samples or dst_samples is 0, dst is filled with silence.
Thread-safe: No shared state.

Simple linear interpolation resampler. Resamples from src_rate to dst_rate using linear interpolation.

Parameters
srcSource samples at src_rate
src_samplesNumber of source samples
dstDestination buffer at dst_rate
dst_samplesNumber of destination samples to produce
src_rateSource sample rate (e.g., 48000)
dst_rateDestination sample rate (e.g., 44100)

Definition at line 165 of file lib/audio/audio.c.

166 {
167 if (src_samples == 0 || dst_samples == 0) {
168 SAFE_MEMSET(dst, dst_samples * sizeof(float), 0, dst_samples * sizeof(float));
169 return;
170 }
171
172 double ratio = src_rate / dst_rate;
173
174 for (size_t i = 0; i < dst_samples; i++) {
175 double src_pos = (double)i * ratio;
176 size_t idx0 = (size_t)src_pos;
177 size_t idx1 = idx0 + 1;
178 double frac = src_pos - (double)idx0;
179
180 // Clamp indices to valid range
181 if (idx0 >= src_samples)
182 idx0 = src_samples - 1;
183 if (idx1 >= src_samples)
184 idx1 = src_samples - 1;
185
186 // Linear interpolation
187 dst[i] = (float)((1.0 - frac) * src[idx0] + frac * src[idx1]);
188 }
189}

References SAFE_MEMSET.

◆ smoothstep()

float smoothstep ( float  t)

#include <mixer.h>

Compute smoothstep interpolation.

Parameters
tInput value (typically 0.0 to 1.0)
Returns
Smoothly interpolated value using 3t² - 2t³ curve

Standard smoothstep function providing smooth fade-in/fade-out curves. Output is clamped to [0, 1] range.

Definition at line 1046 of file mixer.c.

1046 {
1047 if (t <= 0.0f)
1048 return 0.0f;
1049 if (t >= 1.0f)
1050 return 1.0f;
1051 return t * t * (3.0f - 2.0f * t);
1052}

Referenced by client_audio_pipeline_process_duplex(), and fade_buffer_smooth().

◆ soft_clip()

float soft_clip ( float  sample,
float  threshold,
float  steepness 
)

#include <mixer.h>

Apply soft clipping to a sample.

Parameters
sampleInput sample value
thresholdClipping threshold (e.g., 0.7 for 3dB headroom)
steepnessHow aggressively the curve bends (1.0 = gentle, 10.0 = sharp)
Returns
Soft-clipped output sample (always in [-1.0, 1.0] range)

Applies soft clipping using tanh curve. The formula is: output = threshold + (1.0 - threshold) * tanh((sample - threshold) * steepness)

This maps samples above threshold asymptotically toward 1.0, preventing hard clipping artifacts while maintaining perceptual loudness.

Common parameter combinations:

  • threshold=0.7, steepness=3.0: Gentle limiting, 3dB headroom
  • threshold=0.9, steepness=6.0: Moderate limiting, 1dB headroom
  • threshold=0.95, steepness=10.0: Aggressive limiting, minimal headroom

Definition at line 1019 of file mixer.c.

1019 {
1020 if (sample > threshold) {
1021 // Soft clip positive values using tanh curve
1022 // Maps samples above threshold asymptotically toward 1.0
1023 return threshold + (1.0f - threshold) * tanhf((sample - threshold) * steepness);
1024 }
1025 if (sample < -threshold) {
1026 // Soft clip negative values symmetrically
1027 return -threshold + (-1.0f + threshold) * tanhf((sample + threshold) * steepness);
1028 }
1029 return sample;
1030}

Referenced by client_audio_pipeline_process_duplex(), mixer_process(), mixer_process_excluding_source(), and soft_clip_buffer().

◆ soft_clip_buffer()

void soft_clip_buffer ( float *  buffer,
int  num_samples,
float  threshold,
float  steepness 
)

#include <mixer.h>

Apply soft clipping to a buffer.

Parameters
bufferAudio buffer (modified in-place)
num_samplesNumber of samples to process
thresholdClipping threshold (e.g., 0.7 for 3dB headroom)
steepnessHow aggressively the curve bends

Processes entire buffer through soft_clip(). Buffer is modified in-place.

Definition at line 1032 of file mixer.c.

1032 {
1033 if (!buffer || num_samples <= 0)
1034 return;
1035
1036 for (int i = 0; i < num_samples; i++) {
1037 buffer[i] = soft_clip(buffer[i], threshold, steepness);
1038 }
1039}

References soft_clip().

Referenced by client_audio_pipeline_process_duplex().

Variable Documentation

◆ active_sources_mask

uint64_t mixer_t::active_sources_mask

Bitset of active sources (bit i = source i is active, O(1) iteration)

Definition at line 341 of file mixer.h.

Referenced by mixer_add_source(), mixer_create(), mixer_process_excluding_source(), mixer_remove_source(), and mixer_set_source_active().

◆ alpha [1/2]

float highpass_filter_t::alpha

Filter coefficient alpha (calculated from cutoff_hz)

Definition at line 225 of file mixer.h.

Referenced by highpass_filter_init(), and highpass_filter_process_sample().

◆ alpha [2/2]

float lowpass_filter_t::alpha

Filter coefficient alpha (calculated from cutoff_hz)

Definition at line 248 of file mixer.h.

Referenced by lowpass_filter_init(), and lowpass_filter_process_sample().

◆ attack_coeff [1/3]

float compressor_t::attack_coeff

Attack coefficient (converted from attack_ms)

Definition at line 158 of file mixer.h.

Referenced by compressor_process_sample(), and compressor_set_params().

◆ attack_coeff [2/3]

float noise_gate_t::attack_coeff

Attack coefficient (converted from attack_ms)

Definition at line 195 of file mixer.h.

Referenced by noise_gate_process_sample(), and noise_gate_set_params().

◆ attack_coeff [3/3]

float ducking_t::attack_coeff

Attack coefficient (converted from attack_ms)

Definition at line 284 of file mixer.h.

Referenced by ducking_init(), ducking_process_frame(), mixer_process(), and mixer_process_excluding_source().

◆ attack_ms [1/3]

float compressor_t::attack_ms

Attack time in milliseconds (how fast compression kicks in)

Definition at line 145 of file mixer.h.

Referenced by compressor_set_params().

◆ attack_ms [2/3]

float noise_gate_t::attack_ms

Attack time in milliseconds (how fast gate opens)

Definition at line 184 of file mixer.h.

Referenced by noise_gate_set_params().

◆ attack_ms [3/3]

float ducking_t::attack_ms

Ducking attack time in milliseconds.

Definition at line 279 of file mixer.h.

Referenced by ducking_init(), and ducking_set_params().

◆ atten_dB

float ducking_t::atten_dB

Attenuation in dB for non-leader sources.

Definition at line 277 of file mixer.h.

Referenced by ducking_init(), ducking_process_frame(), and ducking_set_params().

◆ base_gain

float mixer_t::base_gain

Base gain before crowd scaling is applied.

Definition at line 353 of file mixer.h.

Referenced by mixer_create(), mixer_process(), and mixer_process_excluding_source().

◆ compressor

compressor_t mixer_t::compressor

Compressor (dynamic range compression)

Definition at line 358 of file mixer.h.

Referenced by mixer_create(), mixer_process(), and mixer_process_excluding_source().

◆ crowd_alpha

float mixer_t::crowd_alpha

Crowd scaling exponent (typically 0.5 for sqrt scaling)

Definition at line 351 of file mixer.h.

Referenced by mixer_create(), mixer_process(), and mixer_process_excluding_source().

◆ cutoff_hz [1/2]

float highpass_filter_t::cutoff_hz

Cutoff frequency in Hz (frequencies below this are attenuated)

Definition at line 220 of file mixer.h.

Referenced by highpass_filter_init().

◆ cutoff_hz [2/2]

float lowpass_filter_t::cutoff_hz

Cutoff frequency in Hz (frequencies above this are attenuated)

Definition at line 243 of file mixer.h.

Referenced by lowpass_filter_init().

◆ ducking

ducking_t mixer_t::ducking

Ducking system (active speaker detection and attenuation)

Definition at line 356 of file mixer.h.

Referenced by mixer_create(), mixer_destroy(), mixer_process(), mixer_process_excluding_source(), and mixer_remove_source().

◆ envelope [1/3]

float compressor_t::envelope

Current envelope follower state (linear, 0-1)

Definition at line 154 of file mixer.h.

Referenced by compressor_init(), and compressor_process_sample().

◆ envelope [2/3]

float noise_gate_t::envelope

Current envelope follower state (linear, 0-1)

Definition at line 193 of file mixer.h.

Referenced by noise_gate_init(), and noise_gate_process_sample().

◆ envelope [3/3]

float* ducking_t::envelope

Per-source envelope follower state (linear, allocated per source)

Definition at line 288 of file mixer.h.

Referenced by ducking_free(), ducking_init(), mixer_process(), mixer_process_excluding_source(), and mixer_remove_source().

◆ gain

float* ducking_t::gain

Per-source ducking gain (linear, calculated from envelope)

Definition at line 290 of file mixer.h.

Referenced by ducking_free(), ducking_init(), mixer_process(), mixer_process_excluding_source(), and mixer_remove_source().

◆ gain_lin

float compressor_t::gain_lin

Current gain multiplier (linear, calculated from envelope)

Definition at line 156 of file mixer.h.

Referenced by compressor_init(), and compressor_process_sample().

◆ gate_open

bool noise_gate_t::gate_open

True if gate is currently open (allowing audio through)

Definition at line 199 of file mixer.h.

Referenced by noise_gate_init(), noise_gate_is_open(), and noise_gate_process_sample().

◆ hysteresis

float noise_gate_t::hysteresis

Hysteresis factor (0-1, prevents gate chatter)

Definition at line 188 of file mixer.h.

Referenced by noise_gate_process_sample(), and noise_gate_set_params().

◆ knee_dB

float compressor_t::knee_dB

Knee width in dB for soft knee (e.g., 2.0)

Definition at line 141 of file mixer.h.

Referenced by compressor_set_params().

◆ leader_margin_dB

float ducking_t::leader_margin_dB

Leader margin in dB (sources within this of loudest are leaders)

Definition at line 275 of file mixer.h.

Referenced by ducking_init(), ducking_process_frame(), and ducking_set_params().

◆ makeup_dB

float compressor_t::makeup_dB

Makeup gain in dB (compensates for gain reduction)

Definition at line 149 of file mixer.h.

Referenced by compressor_process_sample(), and compressor_set_params().

◆ max_sources

int mixer_t::max_sources

Maximum number of sources (allocated array sizes)

Definition at line 329 of file mixer.h.

Referenced by client_audio_render_thread(), mixer_add_source(), mixer_create(), mixer_process(), mixer_process_excluding_source(), mixer_remove_source(), and mixer_set_source_active().

◆ mix_buffer

float* mixer_t::mix_buffer

Temporary buffer for mixing operations (pre-allocated)

Definition at line 361 of file mixer.h.

Referenced by mixer_create(), mixer_destroy(), mixer_process(), and mixer_process_excluding_source().

◆ num_sources

int mixer_t::num_sources

Current number of active audio sources.

Definition at line 327 of file mixer.h.

Referenced by mixer_add_source(), mixer_create(), and mixer_remove_source().

◆ prev_input

float highpass_filter_t::prev_input

Previous input sample (filter state)

Definition at line 227 of file mixer.h.

Referenced by highpass_filter_process_sample(), and highpass_filter_reset().

◆ prev_output [1/2]

float highpass_filter_t::prev_output

Previous output sample (filter state)

Definition at line 229 of file mixer.h.

Referenced by highpass_filter_process_sample(), and highpass_filter_reset().

◆ prev_output [2/2]

float lowpass_filter_t::prev_output

Previous output sample (filter state)

Definition at line 250 of file mixer.h.

Referenced by lowpass_filter_process_sample(), and lowpass_filter_reset().

◆ ratio

float compressor_t::ratio

Compression ratio (e.g., 4.0 for 4:1 compression)

Definition at line 143 of file mixer.h.

Referenced by compressor_set_params().

◆ release_coeff [1/3]

float compressor_t::release_coeff

Release coefficient (converted from release_ms)

Definition at line 160 of file mixer.h.

Referenced by compressor_process_sample(), and compressor_set_params().

◆ release_coeff [2/3]

float noise_gate_t::release_coeff

Release coefficient (converted from release_ms)

Definition at line 197 of file mixer.h.

Referenced by noise_gate_process_sample(), and noise_gate_set_params().

◆ release_coeff [3/3]

float ducking_t::release_coeff

Release coefficient (converted from release_ms)

Definition at line 286 of file mixer.h.

Referenced by ducking_init(), ducking_process_frame(), mixer_process(), and mixer_process_excluding_source().

◆ release_ms [1/3]

float compressor_t::release_ms

Release time in milliseconds (how fast compression releases)

Definition at line 147 of file mixer.h.

Referenced by compressor_set_params().

◆ release_ms [2/3]

float noise_gate_t::release_ms

Release time in milliseconds (how fast gate closes)

Definition at line 186 of file mixer.h.

Referenced by noise_gate_set_params().

◆ release_ms [3/3]

float ducking_t::release_ms

Ducking release time in milliseconds.

Definition at line 281 of file mixer.h.

Referenced by ducking_init(), and ducking_set_params().

◆ sample_rate [1/5]

float compressor_t::sample_rate

Sample rate in Hz (set during initialization)

Definition at line 152 of file mixer.h.

Referenced by compressor_init(), and compressor_set_params().

◆ sample_rate [2/5]

float noise_gate_t::sample_rate

Sample rate in Hz (set during initialization)

Definition at line 191 of file mixer.h.

Referenced by noise_gate_init(), and noise_gate_set_params().

◆ sample_rate [3/5]

float highpass_filter_t::sample_rate

Sample rate in Hz (set during initialization)

Definition at line 222 of file mixer.h.

Referenced by highpass_filter_init().

◆ sample_rate [4/5]

float lowpass_filter_t::sample_rate

Sample rate in Hz (set during initialization)

Definition at line 245 of file mixer.h.

Referenced by lowpass_filter_init().

◆ sample_rate [5/5]

int mixer_t::sample_rate

Sample rate in Hz (e.g., 44100)

Definition at line 331 of file mixer.h.

Referenced by mixer_create().

◆ source_active

bool* mixer_t::source_active

Array of active flags (true if source is active)

Definition at line 338 of file mixer.h.

Referenced by mixer_add_source(), mixer_create(), mixer_destroy(), mixer_process(), mixer_remove_source(), and mixer_set_source_active().

◆ source_buffers

audio_ring_buffer_t** mixer_t::source_buffers

Array of pointers to client audio ring buffers.

Definition at line 334 of file mixer.h.

Referenced by client_audio_render_thread(), mixer_add_source(), mixer_create(), mixer_destroy(), mixer_process(), mixer_process_excluding_source(), and mixer_remove_source().

◆ source_id_at_hash

uint32_t mixer_t::source_id_at_hash[256]

Client IDs stored at each hash index for collision detection.

Definition at line 345 of file mixer.h.

◆ source_id_to_index

uint8_t mixer_t::source_id_to_index[256]

Hash table mapping client_id → mixer source index (uses hash function for 32-bit IDs)

Definition at line 343 of file mixer.h.

Referenced by mixer_add_source(), mixer_create(), mixer_process_excluding_source(), and mixer_remove_source().

◆ source_ids

uint32_t* mixer_t::source_ids

◆ source_lock

rwlock_t mixer_t::source_lock

Reader-writer lock protecting source arrays and bitset.

Definition at line 348 of file mixer.h.

Referenced by mixer_add_source(), mixer_create(), mixer_destroy(), mixer_process(), mixer_process_excluding_source(), mixer_remove_source(), and mixer_set_source_active().

◆ threshold

float noise_gate_t::threshold

Gate threshold in linear units (e.g., 0.01f for -40dB)

Definition at line 182 of file mixer.h.

Referenced by noise_gate_process_sample(), and noise_gate_set_params().

◆ threshold_dB [1/2]

float compressor_t::threshold_dB

Compression threshold in dB (e.g., -10.0)

Definition at line 139 of file mixer.h.

Referenced by compressor_set_params().

◆ threshold_dB [2/2]

float ducking_t::threshold_dB

Speaking threshold in dB (sources below this are not "speaking")

Definition at line 273 of file mixer.h.

Referenced by ducking_init(), ducking_process_frame(), and ducking_set_params().