ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches

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

Files

file  analysis.c
 Audio Analysis Implementation.
 
file  audio.c
 🔊 Audio capture and playback using PortAudio with buffer management
 
file  mixer.c
 🎚️ Real-time audio mixer with ducking, gain control, and multi-stream blending
 
file  opus.c
 Opus audio codec implementation.
 
file  analysis.h
 Audio Analysis and Debugging Interface.
 
file  audio.h
 🔊 Audio Capture and Playback Interface for ascii-chat
 
file  client_pipeline.h
 Unified client-side audio processing pipeline.
 
file  mixer.h
 Multi-Source Audio Mixing and Processing System.
 
file  opus.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_terminate_portaudio_final (void)
 Terminate PortAudio and free all device 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_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)
 
void audio_ring_buffer_destroy (audio_ring_buffer_t *rb)
 Destroy an audio ring buffer.
 
void audio_ring_buffer_register_atomics (audio_ring_buffer_t *rb, const char *context_name)
 Register audio ring buffer atomic fields for debug tracking.
 
void audio_ring_buffer_unregister_atomics (audio_ring_buffer_t *rb)
 Unregister audio ring buffer atomic fields.
 
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.
 
void audio_flush_playback_buffers (audio_context_t *ctx)
 Flush all audio buffers to synchronize with seek operations.
 
bool audio_should_enable_microphone (audio_capture_source_t source, bool has_media_audio)
 Determine if microphone should be enabled based on audio source setting and media state.
 
opus_codec_t * opus_codec_create_encoder (opus_application_t application, int sample_rate, int bitrate)
 Create an Opus encoder.
 
opus_codec_t * opus_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)
 
uint64_t compressor_t::attack_ns
 Attack time in nanoseconds (how fast compression kicks in)
 
uint64_t compressor_t::release_ns
 Release time in nanoseconds (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)
 
uint64_t noise_gate_t::attack_ns
 Attack time in nanoseconds (how fast gate opens)
 
uint64_t noise_gate_t::release_ns
 Release time in nanoseconds (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.
 
uint64_t ducking_t::attack_ns
 Ducking attack time in nanoseconds.
 
uint64_t ducking_t::release_ns
 Ducking release time in nanoseconds.
 
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.
 
struct mixer_listener_buffer * mixer_t::listener_buffers [32][32]
 
const char ** mixer_t::source_ids
 Array of client ID strings (one per source slot)
 
bool * mixer_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 strings)
 
const char * mixer_t::source_id_at_hash [256]
 Client ID strings 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)
 
float * mixer_t::mix_buffer
 Temporary buffer for mixing operations (pre-allocated)
 

Mixer Lifecycle Functions

mixer_t * mixer_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, const char *client_id, audio_ring_buffer_t *buffer)
 Add an audio source to the mixer.
 
void mixer_remove_source (mixer_t *mixer, const char *client_id)
 Remove an audio source from the mixer.
 
void mixer_set_source_active (mixer_t *mixer, const char *client_id, bool active)
 Set whether a source is active (receiving audio)
 
#define MIXER_HASH_INVALID   0xFF
 Invalid index sentinel value for mixer hash table.
 

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, uint64_t attack_ns, uint64_t release_ns, 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_destroy (ducking_t *duck)
 Free ducking system resources.
 
void ducking_set_params (ducking_t *duck, float threshold_dB, float leader_margin_dB, float atten_dB, uint64_t attack_ns, uint64_t release_ns)
 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, uint64_t attack_ns, uint64_t release_ns, 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.c:74
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.c:143
opus_codec_t * opus_codec_create_encoder(opus_application_t application, int sample_rate, int bitrate)
Create an Opus encoder.
Definition opus.c:19
void opus_codec_destroy(opus_codec_t *codec)
Destroy an Opus codec instance.
Definition opus.c:230
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.c:112
@ OPUS_APPLICATION_VOIP
Voice over IP (optimized for speech)
Definition opus.h:77
unsigned char uint8_t
Definition common.h:56
Opus codec context for encoding or decoding.
Definition opus.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:49
@ ASCIICHAT_OK
Definition error_codes.h:51
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
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.
Definition log/log.h:574

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

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 91 of file include/ascii-chat/audio/audio.h.

◆ AUDIO_CHANNELS

#define AUDIO_CHANNELS   1

#include <audio.h>

Number of audio channels (1 = mono)

Definition at line 89 of file include/ascii-chat/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 87 of file include/ascii-chat/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 85 of file include/ascii-chat/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_HASH_INVALID

#define MIXER_HASH_INVALID   0xFF

#include <mixer.h>

Invalid index sentinel value for mixer hash table.

Definition at line 464 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.h>

Definition at line 66 of file opus.h.

◆ OpusEncoder

typedef struct OpusEncoder OpusEncoder

#include <opus.h>

Definition at line 65 of file opus.h.

Enumeration Type Documentation

◆ opus_application_t

#include <opus.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.h.

76 {
opus_application_t
Application mode for opus encoder.
Definition opus.h:76
@ OPUS_APPLICATION_AUDIO
General audio (optimized for music)
Definition opus.h:78
@ OPUS_APPLICATION_RESTRICTED_LOWDELAY
Low-latency mode.
Definition opus.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 1186 of file mixer.c.

1186 {
1187 if (!buffer || count <= 0)
1188 return;
1189
1190 for (int i = 0; i < count; i++) {
1191 buffer[i] *= gain;
1192 }
1193}

◆ 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 2239 of file lib/audio/audio.c.

2239 {
2240 if (!samples_ptr || !out_samples || total_samples == 0) {
2241 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for audio dequantization");
2242 }
2243
2244 for (uint32_t i = 0; i < total_samples; i++) {
2245 uint32_t network_sample;
2246 // Use memcpy to safely handle potential misalignment from packet header
2247 memcpy(&network_sample, samples_ptr + i * sizeof(uint32_t), sizeof(uint32_t));
2248 int32_t scaled = (int32_t)NET_TO_HOST_U32(network_sample);
2249 out_samples[i] = (float)scaled / 2147483647.0f;
2250 }
2251
2252 return ASCIICHAT_OK;
2253}
#define NET_TO_HOST_U32(val)
Definition endian.h:81
unsigned int uint32_t
Definition common.h:58
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ 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 1553 of file lib/audio/audio.c.

1553 {
1554 if (!ctx) {
1555 return;
1556 }
1557
1558 // Always release PortAudio refcount if it was incremented
1559 // audio_init() calls Pa_Initialize() very early, and if it fails partway through,
1560 // ctx->initialized will be false. But we MUST still call audio_release_portaudio()
1561 // to properly decrement the refcount and allow Pa_Terminate() to be called.
1562 if (ctx->initialized) {
1563
1564 // Stop duplex stream if running (this also stops the worker thread)
1565 if (ctx->running) {
1566 audio_stop_duplex(ctx);
1567 }
1568
1569 // Ensure worker thread is stopped even if streams weren't running
1570 if (ctx->worker_running) {
1571 log_debug("Stopping worker thread during audio_destroy");
1573 cond_signal(&ctx->worker_cond); // Wake up worker if waiting
1575 ctx->worker_running = false;
1576 }
1577
1578 mutex_lock(&ctx->state_mutex);
1579
1580 // Destroy all ring buffers (old + new)
1586 audio_ring_buffer_destroy(ctx->render_buffer); // May be NULL, that's OK
1587
1588 // Free pre-allocated worker buffers
1592
1593 // Destroy worker synchronization primitives
1596
1597 ctx->initialized = false;
1598
1601
1602 log_debug("Audio system cleanup complete (all resources released)");
1603 } else {
1604 }
1605}
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
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 SAFE_FREE(ptr)
Definition common.h:376
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
int cond_signal(cond_t *cond)
Signal a condition variable (wake one waiting thread)
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
int cond_destroy(cond_t *cond)
Destroy a condition variable.
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
#define asciichat_thread_join(thread, timeout_ms)
bool initialized
True if context has been initialized.
audio_ring_buffer_t * raw_render_rb
Callback → Worker: Raw speaker samples (AEC3 reference)
audio_ring_buffer_t * playback_buffer
OLD: Network → Worker (will become network_playback_rb)
float * worker_render_batch
Pre-allocated buffer for batch render processing.
bool worker_running
True if worker thread is running.
cond_t worker_cond
Condition variable for worker signaling.
atomic_t worker_should_stop
Signal worker thread to exit.
audio_ring_buffer_t * capture_buffer
OLD: Worker → Network (will become processed_capture_rb)
bool running
True if duplex stream is active.
audio_ring_buffer_t * render_buffer
Ring buffer for render reference (separate streams mode)
audio_ring_buffer_t * raw_capture_rb
Callback → Worker: Raw mic samples (no processing)
float * worker_playback_batch
Pre-allocated buffer for batch playback processing.
mutex_t worker_mutex
Mutex protecting worker state.
mutex_t state_mutex
Mutex protecting context state.
asciichat_thread_t worker_thread
Worker thread for audio processing.
float * worker_capture_batch
Pre-allocated buffer for batch capture processing.
audio_ring_buffer_t * processed_playback_rb
Worker → Callback: Ready for speakers.

References asciichat_thread_join, atomic_store_bool(), audio_ring_buffer_destroy(), audio_stop_duplex(), audio_context_t::capture_buffer, cond_destroy(), cond_signal(), audio_context_t::initialized, log_debug, mutex_destroy(), mutex_lock, mutex_unlock, audio_context_t::playback_buffer, audio_context_t::processed_playback_rb, audio_context_t::raw_capture_rb, audio_context_t::raw_render_rb, audio_context_t::render_buffer, audio_context_t::running, SAFE_FREE, audio_context_t::state_mutex, audio_context_t::worker_capture_batch, audio_context_t::worker_cond, audio_context_t::worker_mutex, audio_context_t::worker_playback_batch, audio_context_t::worker_render_batch, audio_context_t::worker_running, audio_context_t::worker_should_stop, and audio_context_t::worker_thread.

Referenced by audio_cleanup(), audio_client_init(), session_audio_destroy(), and session_client_like_run().

◆ audio_flush_playback_buffers()

void audio_flush_playback_buffers ( audio_context_t *  ctx)

#include <audio.h>

Flush all audio buffers to synchronize with seek operations.

Parameters
ctxAudio context (must not be NULL)

Clears the playback buffers when seeking occurs. This prevents 5-10 seconds of old audio from playing after a seek operation. Called by keyboard handler when user presses seek arrow keys.

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

1613 {
1614 if (!ctx || !ctx->initialized) {
1615 return;
1616 }
1617
1618 if (ctx->playback_buffer) {
1620 }
1621 if (ctx->processed_playback_rb) {
1623 }
1624 if (ctx->render_buffer) {
1626 }
1627 if (ctx->raw_render_rb) {
1629 }
1630}
void audio_ring_buffer_clear(audio_ring_buffer_t *rb)
Clear all audio samples from ring buffer.

References audio_ring_buffer_clear(), audio_context_t::initialized, audio_context_t::playback_buffer, audio_context_t::processed_playback_rb, audio_context_t::raw_render_rb, and audio_context_t::render_buffer.

Referenced by session_handle_keyboard_input().

◆ 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 2235 of file lib/audio/audio.c.

2235 {
2236 SAFE_FREE(devices);
2237}

References SAFE_FREE.

◆ 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 1399 of file lib/audio/audio.c.

1399 {
1400 log_debug("audio_init: starting, ctx=%p", (void *)ctx);
1401 if (!ctx) {
1402 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx is NULL");
1403 }
1404
1405 SAFE_MEMSET(ctx, sizeof(audio_context_t), 0, sizeof(audio_context_t));
1406
1407 if (mutex_init(&ctx->state_mutex, "audio_context") != 0) {
1408 return SET_ERRNO(ERROR_THREAD, "Failed to initialize audio context mutex");
1409 }
1410
1411 // NOTE: PortAudio initialization deferred to audio_start_duplex() where streams are actually opened
1412 // This avoids Pa_Initialize() overhead for contexts that might not start duplex
1413 // and prevents premature ALSA device allocation and memory leaks
1414
1415 // Create capture buffer WITHOUT jitter buffering (PortAudio writes directly from microphone)
1417 if (!ctx->capture_buffer) {
1419 return SET_ERRNO(ERROR_MEMORY, "Failed to create capture buffer");
1420 }
1421
1423 if (!ctx->playback_buffer) {
1426 return SET_ERRNO(ERROR_MEMORY, "Failed to create playback buffer");
1427 }
1428
1429 // Create new ring buffers for worker thread architecture
1431 if (!ctx->raw_capture_rb) {
1435 return SET_ERRNO(ERROR_MEMORY, "Failed to create raw capture buffer");
1436 }
1437
1439 if (!ctx->raw_render_rb) {
1444 return SET_ERRNO(ERROR_MEMORY, "Failed to create raw render buffer");
1445 }
1446
1448 if (!ctx->processed_playback_rb) {
1454 return SET_ERRNO(ERROR_MEMORY, "Failed to create processed playback buffer");
1455 }
1456
1457 // Initialize worker thread infrastructure
1458 if (mutex_init(&ctx->worker_mutex, "audio_worker_mutex") != 0) {
1465 return SET_ERRNO(ERROR_THREAD, "Failed to initialize worker mutex");
1466 }
1467
1468 if (cond_init(&ctx->worker_cond, "audio_worker_cond") != 0) {
1476 return SET_ERRNO(ERROR_THREAD, "Failed to initialize worker condition variable");
1477 }
1478
1479 // Allocate pre-allocated worker buffers (avoid malloc in worker loop)
1480 ctx->worker_capture_batch = SAFE_MALLOC(WORKER_BATCH_SAMPLES * sizeof(float), float *);
1481 if (!ctx->worker_capture_batch) {
1490 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate worker capture batch buffer");
1491 }
1492
1493 ctx->worker_render_batch = SAFE_MALLOC(WORKER_BATCH_SAMPLES * sizeof(float), float *);
1494 if (!ctx->worker_render_batch) {
1504 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate worker render batch buffer");
1505 }
1506
1507 ctx->worker_playback_batch = SAFE_MALLOC(WORKER_BATCH_SAMPLES * sizeof(float), float *);
1508 if (!ctx->worker_playback_batch) {
1519 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate worker playback batch buffer");
1520 }
1521
1522 ctx->capture_media_source = NULL;
1523 ctx->monitor_local_media = false;
1524
1525 // Initialize worker thread state (thread will be started in audio_start_duplex)
1526 ctx->worker_running = false;
1528
1529 ctx->initialized = true;
1530 atomic_store_bool(&ctx->shutting_down, false);
1531
1532 /* Register audio context with named registry */
1533 NAMED_REGISTER(ctx, "audio_context", "audio_context_t", "0x%tx", NULL);
1534
1535 /* Register audio context's sync primitives with hierarchical naming */
1536 NAMED_REGISTER_MUTEX(&ctx->state_mutex, "state_mutex", (uintptr_t)(const void *)(ctx));
1537 NAMED_REGISTER_MUTEX(&ctx->worker_mutex, "worker_mutex", (uintptr_t)(const void *)(ctx));
1538 NAMED_REGISTER_COND(&ctx->worker_cond, "worker_cond", (uintptr_t)(const void *)(ctx));
1539 NAMED_REGISTER_ATOMIC(&ctx->worker_should_stop, "worker_should_stop", (uintptr_t)(const void *)(ctx));
1540 NAMED_REGISTER_ATOMIC(&ctx->shutting_down, "shutting_down", (uintptr_t)(const void *)(ctx));
1541
1542 /* Register ring buffers as child structures with audio context as parent */
1543 NAMED_REGISTER_AUDIO_RINGBUF(ctx->capture_buffer, "capture_buffer", (uintptr_t)(const void *)(ctx));
1544 NAMED_REGISTER_AUDIO_RINGBUF(ctx->playback_buffer, "playback_buffer", (uintptr_t)(const void *)(ctx));
1545 NAMED_REGISTER_AUDIO_RINGBUF(ctx->raw_capture_rb, "raw_capture_rb", (uintptr_t)(const void *)(ctx));
1546 NAMED_REGISTER_AUDIO_RINGBUF(ctx->raw_render_rb, "raw_render_rb", (uintptr_t)(const void *)(ctx));
1547 NAMED_REGISTER_AUDIO_RINGBUF(ctx->processed_playback_rb, "processed_playback_rb", (uintptr_t)(const void *)(ctx));
1548
1549 log_info("Audio system initialized successfully (worker thread architecture enabled)");
1550 return ASCIICHAT_OK;
1551}
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:469
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
#define NAMED_REGISTER_COND(cond, name, parent_ptr)
Register a condition variable with automatic format specifier.
#define NAMED_REGISTER(ptr, name, type, fmt, parent_ptr)
Register any pointer with base name, type, format spec, and location (auto-suffix)
#define NAMED_REGISTER_AUDIO_RINGBUF(buf, name, parent_ptr)
Register an audio ring buffer with automatic format specifier.
#define NAMED_REGISTER_MUTEX(mutex, name, parent_ptr)
Register a mutex with automatic format specifier.
@ ERROR_MEMORY
Definition error_codes.h:56
@ ERROR_THREAD
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
int cond_init(cond_t *cond, const char *name)
Initialize a condition variable with a name.
#define WORKER_BATCH_SAMPLES
atomic_t shutting_down
True when shutdown started - callback outputs silence.
bool monitor_local_media
Play local file audio in solo sessions.
void * capture_media_source
Borrowed local media audio; configure before starting duplex.

References ASCIICHAT_OK, atomic_store_bool(), audio_ring_buffer_create(), audio_ring_buffer_create_for_capture(), audio_ring_buffer_destroy(), audio_context_t::capture_buffer, audio_context_t::capture_media_source, cond_destroy(), cond_init(), ERROR_INVALID_PARAM, ERROR_MEMORY, ERROR_THREAD, audio_context_t::initialized, log_debug, log_info, audio_context_t::monitor_local_media, mutex_destroy(), mutex_init(), NAMED_REGISTER, NAMED_REGISTER_ATOMIC, NAMED_REGISTER_AUDIO_RINGBUF, NAMED_REGISTER_COND, NAMED_REGISTER_MUTEX, audio_context_t::playback_buffer, audio_context_t::processed_playback_rb, audio_context_t::raw_capture_rb, audio_context_t::raw_render_rb, SAFE_FREE, SAFE_MALLOC, SAFE_MEMSET, SET_ERRNO, audio_context_t::shutting_down, audio_context_t::state_mutex, WORKER_BATCH_SAMPLES, audio_context_t::worker_capture_batch, audio_context_t::worker_cond, audio_context_t::worker_mutex, audio_context_t::worker_playback_batch, audio_context_t::worker_render_batch, audio_context_t::worker_running, and audio_context_t::worker_should_stop.

Referenced by audio_client_init(), session_audio_create(), and session_client_like_run().

◆ 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 2335 of file lib/audio/audio.c.

2335 {
2336 // List of commonly supported audio sample rates
2337 static const uint32_t supported_rates[] = {
2338 8000, // Telephone quality
2339 16000, // Wideband telephony
2340 24000, // High quality speech
2341 32000, // Good for video
2342 44100, // CD quality (less common in VoIP)
2343 48000, // Standard professional
2344 96000, // High-end professional
2345 192000, // Ultra-high-end mastering
2346 };
2347
2348 const size_t rate_count = sizeof(supported_rates) / sizeof(supported_rates[0]);
2349 for (size_t i = 0; i < rate_count; i++) {
2350 if (sample_rate == supported_rates[i]) {
2351 return true;
2352 }
2353 }
2354
2355 return false;
2356}

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 2227 of file lib/audio/audio.c.

2227 {
2228 return audio_list_devices_internal(out_devices, out_count, true);
2229}

◆ 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 2231 of file lib/audio/audio.c.

2231 {
2232 return audio_list_devices_internal(out_devices, out_count, false);
2233}

◆ 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 2269 of file lib/audio/audio.c.

2269 {
2270 if (!data) {
2271 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch header data pointer is NULL");
2272 }
2273
2274 if (!out_batch) {
2275 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch info output pointer is NULL");
2276 }
2277
2278 if (len < sizeof(audio_batch_packet_t)) {
2279 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch header too small (len=%zu, expected=%zu)", len,
2280 sizeof(audio_batch_packet_t));
2281 }
2282
2283 const audio_batch_packet_t *batch_header = (const audio_batch_packet_t *)data;
2284
2285 // Unpack network byte order values to host byte order
2286 out_batch->batch_count = ntohl(batch_header->batch_count);
2287 out_batch->total_samples = ntohl(batch_header->total_samples);
2288 out_batch->sample_rate = ntohl(batch_header->sample_rate);
2289 out_batch->channels = ntohl(batch_header->channels);
2290
2291 return ASCIICHAT_OK;
2292}
uint32_t total_samples
Total number of samples across all frames.
Audio batch packet structure (Packet Type 28)
Definition packet.h:904
uint32_t channels
Number of audio channels (1=mono, 2=stereo)
Definition packet.h:912
uint32_t batch_count
Number of audio chunks in this batch (usually AUDIO_BATCH_COUNT = 32)
Definition packet.h:906
uint32_t sample_rate
Sample rate in Hz (e.g., 44100, 48000)
Definition packet.h:910
uint32_t total_samples
Total audio samples across all chunks (typically 8192)
Definition packet.h:908

References ASCIICHAT_OK, audio_batch_packet_t::batch_count, audio_batch_info_t::batch_count, audio_batch_packet_t::channels, audio_batch_info_t::channels, ERROR_INVALID_PARAM, audio_batch_packet_t::sample_rate, audio_batch_info_t::sample_rate, SET_ERRNO, audio_batch_packet_t::total_samples, and audio_batch_info_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 2117 of file lib/audio/audio.c.

2117 {
2118 if (!ctx || !ctx->initialized || !buffer || num_samples <= 0) {
2119 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, buffer=%p, num_samples=%d", ctx, buffer,
2120 num_samples);
2121 }
2122
2123 // audio_ring_buffer_read now returns number of samples read, not error code
2124 int samples_read = audio_ring_buffer_read(ctx->capture_buffer, buffer, num_samples);
2125 return (samples_read >= 0) ? ASCIICHAT_OK : ERROR_AUDIO;
2126}
size_t audio_ring_buffer_read(audio_ring_buffer_t *rb, float *data, size_t samples)
Read audio samples from ring buffer.
@ ERROR_AUDIO
Definition error_codes.h:67

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 1373 of file lib/audio/audio.c.

1373 {
1374 if (!rb)
1375 return 0;
1376
1377 // LOCK-FREE: Load indices with proper memory ordering
1378 // Use acquire for write_index to see writer's updates
1379 // Use relaxed for read_index (our own index)
1380 unsigned int write_idx = atomic_load_u64(&rb->write_index);
1381 unsigned int read_idx = atomic_load_u64(&rb->read_index);
1382
1383 if (write_idx >= read_idx) {
1384 return write_idx - read_idx;
1385 }
1386
1387 return AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
1388}
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
#define AUDIO_RING_BUFFER_SIZE
Audio ring buffer size in samples (192000 samples = 4 seconds @ 48kHz)
Definition ringbuffer.h:128
atomic_t read_index
Read index (consumer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:211
atomic_t write_index
Write index (producer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:209

References atomic_load_u64(), AUDIO_RING_BUFFER_SIZE, audio_ring_buffer::read_index, and audio_ring_buffer::write_index.

Referenced by audio_process_received_samples(), and audio_ring_buffer_available_write().

◆ 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 1390 of file lib/audio/audio.c.

1390 {
1391 if (!rb) {
1392 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: rb is NULL");
1393 return 0;
1394 }
1395
1397}
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 1071 of file lib/audio/audio.c.

1071 {
1072 if (!rb)
1073 return;
1074
1075 mutex_lock(&rb->mutex);
1076 // Reset buffer to empty state (no audio to play = silence at shutdown)
1081 rb->last_sample = 0.0f;
1082 // Clear the actual data to zeros to prevent any stale audio
1083 SAFE_MEMSET(rb->data, sizeof(rb->data), 0, sizeof(rb->data));
1084 mutex_unlock(&rb->mutex);
1085}
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
atomic_t discard_pending
Consumer should discard queued samples before its next read.
Definition ringbuffer.h:213
atomic_t discard_to_index
Producer position at which the stale queue ends.
Definition ringbuffer.h:215
float last_sample
Last sample value for smooth fade-out during underrun - NOT atomic (only written by reader)
Definition ringbuffer.h:223
mutex_t mutex
Mutex for SLOW PATH only (clear/destroy operations, not regular read/write)
Definition ringbuffer.h:229
float data[192000]
Audio sample data buffer.
Definition ringbuffer.h:207

References atomic_store_bool(), atomic_store_u64(), audio_ring_buffer::data, audio_ring_buffer::discard_pending, audio_ring_buffer::discard_to_index, 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_flush_playback_buffers(), audio_stop_duplex(), and media_source_seek().

◆ 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 1014 of file lib/audio/audio.c.

1014 {
1015 return audio_ring_buffer_create_internal(true); // Default: enable jitter buffering for playback
1016}

Referenced by audio_init(), and session_audio_add_source().

◆ 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 1018 of file lib/audio/audio.c.

1018 {
1019 return audio_ring_buffer_create_internal(false); // Disable jitter buffering for capture
1020}

Referenced by add_client(), add_webrtc_client(), 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 1022 of file lib/audio/audio.c.

1022 {
1023 if (!rb)
1024 return;
1025
1026 NAMED_UNREGISTER(rb);
1028
1029 mutex_destroy(&rb->mutex);
1030 buffer_pool_free(NULL, rb, sizeof(audio_ring_buffer_t));
1031}
void audio_ring_buffer_unregister_atomics(audio_ring_buffer_t *rb)
Unregister audio ring buffer atomic fields.
void buffer_pool_free(buffer_pool_t *pool, const void *data, size_t size)
Free a buffer back to the pool (lock-free)
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
Audio ring buffer for real-time audio streaming.
Definition ringbuffer.h:205

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

Referenced by add_client(), add_webrtc_client(), audio_destroy(), audio_init(), audio_start_duplex(), audio_stop_duplex(), cleanup_client_media_buffers(), session_audio_destroy(), and session_audio_remove_source().

◆ 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 1336 of file lib/audio/audio.c.

1336 {
1337 if (!rb || !data || samples <= 0) {
1338 return 0;
1339 }
1340
1341 // LOCK-FREE: Load indices with proper memory ordering
1342 unsigned int write_idx = atomic_load_u64(&rb->write_index);
1343 unsigned int read_idx = atomic_load_u64(&rb->read_index);
1344
1345 // Calculate available samples
1346 size_t available;
1347 if (write_idx >= read_idx) {
1348 available = write_idx - read_idx;
1349 } else {
1350 available = AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
1351 }
1352
1353 size_t to_peek = (samples > available) ? available : samples;
1354
1355 if (to_peek == 0) {
1356 return 0;
1357 }
1358
1359 // Copy samples in chunks (handle wraparound)
1360 size_t first_chunk = (read_idx + to_peek <= AUDIO_RING_BUFFER_SIZE) ? to_peek : (AUDIO_RING_BUFFER_SIZE - read_idx);
1361
1362 SAFE_MEMCPY(data, first_chunk * sizeof(float), rb->data + read_idx, first_chunk * sizeof(float));
1363
1364 if (first_chunk < to_peek) {
1365 // Wraparound: copy second chunk from beginning of buffer
1366 size_t second_chunk = to_peek - first_chunk;
1367 SAFE_MEMCPY(data + first_chunk, second_chunk * sizeof(float), rb->data, second_chunk * sizeof(float));
1368 }
1369
1370 return to_peek;
1371}
#define SAFE_MEMCPY(dest, dest_size, src, count)
Definition common.h:468

References atomic_load_u64(), 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 1191 of file lib/audio/audio.c.

1191 {
1192 if (!rb || !data || samples <= 0) {
1193 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: rb=%p, data=%p, samples=%d", rb, data, samples);
1194 return 0; // Return 0 samples read on error
1195 }
1196
1197 // Keep read_index consumer-owned while letting a producer request a flush.
1198 if (atomic_exchange_bool(&rb->discard_pending, false)) {
1199 unsigned int read_idx = atomic_load_u64(&rb->read_index);
1200 unsigned int discard_to = atomic_load_u64(&rb->discard_to_index);
1201 unsigned int advance = (discard_to + AUDIO_RING_BUFFER_SIZE - read_idx) % AUDIO_RING_BUFFER_SIZE;
1202 // A concurrent callback may already have consumed beyond the requested
1203 // boundary. In that case the target is behind us and must not rewind the
1204 // single-consumer index.
1205 if (advance < AUDIO_RING_BUFFER_SIZE / 2) {
1206 atomic_store_u64(&rb->read_index, discard_to);
1207 rb->last_sample = 0.0f;
1209 }
1210 }
1211
1212 // LOCK-FREE: Load indices with proper memory ordering
1213 // - Load writer's write_index with acquire (see writer's data updates)
1214 // - Load our own read_index with relaxed (no sync needed with ourselves)
1215 unsigned int write_idx = atomic_load_u64(&rb->write_index);
1216 unsigned int read_idx = atomic_load_u64(&rb->read_index);
1217
1218 // Calculate available samples
1219 size_t available;
1220 if (write_idx >= read_idx) {
1221 available = write_idx - read_idx;
1222 } else {
1223 available = AUDIO_RING_BUFFER_SIZE - read_idx + write_idx;
1224 }
1225
1226 // LOCK-FREE: Load jitter buffer state with acquire ordering
1227 bool jitter_filled = atomic_load_u64(&rb->jitter_buffer_filled);
1228 int crossfade_remaining = atomic_load_u64(&rb->crossfade_samples_remaining);
1229 bool fade_in = atomic_load_u64(&rb->crossfade_fade_in);
1230
1231 // Jitter buffer: don't read until initial fill threshold is reached
1232 // (only for playback buffers - capture buffers have jitter_buffer_enabled = false)
1233 if (!jitter_filled && rb->jitter_buffer_enabled) {
1234 // Check if we've accumulated enough samples to start playback
1235 if (available >= AUDIO_JITTER_BUFFER_THRESHOLD) {
1239 log_info("Jitter buffer filled (%zu samples), starting playback with fade-in", available);
1240 // Reload state for processing below
1241 jitter_filled = true;
1242 crossfade_remaining = AUDIO_CROSSFADE_SAMPLES;
1243 fade_in = true;
1244 } else {
1245 // Log buffer fill progress every second
1246 log_debug_every(NS_PER_MS_INT, "Jitter buffer filling: %zu/%d samples (%.1f%%)", available,
1248 return 0; // Return 0 samples - caller will pad with silence
1249 }
1250 }
1251
1252 // Periodic buffer health logging (every 5 seconds when healthy)
1253 unsigned int underruns = atomic_load_u64(&rb->underrun_count);
1254 log_dev_every(5 * NS_PER_MS_INT, "Buffer health: %zu/%d samples (%.1f%%), underruns=%u", available,
1255 AUDIO_RING_BUFFER_SIZE, (100.0f * available) / AUDIO_RING_BUFFER_SIZE, underruns);
1256
1257 // Low buffer handling: DON'T pause playback - continue reading what's available
1258 // and fill the rest with silence. Pausing causes a feedback loop where:
1259 // 1. Underrun -> pause reading -> buffer overflows from incoming samples
1260 // 2. Threshold reached -> resume reading -> drains too fast -> underrun again
1261 //
1262 // Instead: always consume samples to prevent overflow, use silence for missing data
1263 if (rb->jitter_buffer_enabled && available < AUDIO_JITTER_LOW_WATER_MARK) {
1264 unsigned int underrun_count = atomic_fetch_add_u64(&rb->underrun_count, 1) + 1;
1266 "Audio buffer low #%u: only %zu samples available (low water mark: %d), padding with silence",
1267 underrun_count, available, AUDIO_JITTER_LOW_WATER_MARK);
1268 // Don't set jitter_buffer_filled = false - keep reading to prevent overflow
1269 }
1270
1271 size_t to_read = (samples > available) ? available : samples;
1272
1273 // Optimize: copy in chunks instead of one sample at a time
1274 size_t remaining = AUDIO_RING_BUFFER_SIZE - read_idx;
1275
1276 if (to_read <= remaining) {
1277 // Can copy in one chunk
1278 SAFE_MEMCPY(data, to_read * sizeof(float), &rb->data[read_idx], to_read * sizeof(float));
1279 } else {
1280 // Need to wrap around - copy in two chunks
1281 SAFE_MEMCPY(data, remaining * sizeof(float), &rb->data[read_idx], remaining * sizeof(float));
1282 SAFE_MEMCPY(&data[remaining], (to_read - remaining) * sizeof(float), &rb->data[0],
1283 (to_read - remaining) * sizeof(float));
1284 }
1285
1286 // LOCK-FREE: Store new read_index with release ordering
1287 // This ensures all data reads above complete before the index update
1288 unsigned int new_read_idx = (read_idx + (unsigned int)to_read) % AUDIO_RING_BUFFER_SIZE;
1289 atomic_store_u64(&rb->read_index, new_read_idx);
1290
1291 // Apply fade-in if recovering from underrun
1292 if (fade_in && crossfade_remaining > 0) {
1293 int fade_start = AUDIO_CROSSFADE_SAMPLES - crossfade_remaining;
1294 size_t fade_samples = (to_read < (size_t)crossfade_remaining) ? to_read : (size_t)crossfade_remaining;
1295
1296 for (size_t i = 0; i < fade_samples; i++) {
1297 float fade_factor = (float)(fade_start + (int)i + 1) / (float)AUDIO_CROSSFADE_SAMPLES;
1298 data[i] *= fade_factor;
1299 }
1300
1301 int new_crossfade_remaining = crossfade_remaining - (int)fade_samples;
1302 atomic_store_u64(&rb->crossfade_samples_remaining, new_crossfade_remaining);
1303 if (new_crossfade_remaining <= 0) {
1305 log_debug("Audio fade-in complete");
1306 }
1307 }
1308
1309 // Save last sample for potential fade-out
1310 // Note: only update if we actually read some data
1311 // This is NOT atomic - only the reader thread writes this
1312 if (to_read > 0) {
1313 rb->last_sample = data[to_read - 1];
1314 }
1315
1316 // Return ACTUAL number of samples read, not padded count
1317 // The caller (mixer) expects truthful return values to detect underruns
1318 // and handle silence padding externally. Internal padding creates double-padding bugs.
1319 //
1320 // This function was incorrectly returning `samples` even when
1321 // it only read `to_read` samples. This broke the mixer's underrun detection.
1322 return to_read;
1323}
uint64_t atomic_fetch_add_u64(atomic_t *a, uint64_t delta)
Atomically add to a uint64_t and return the previous value.
Definition atomic.c:248
bool atomic_exchange_bool(atomic_t *a, bool new_value)
Atomically exchange a boolean and return the old value.
Definition atomic.c:303
#define LOG_RATE_FAST
Log rate limit: 1 second (1,000,000 microseconds)
Definition log_rates.h:26
#define NS_PER_MS_INT
Definition time.h:156
#define AUDIO_JITTER_BUFFER_THRESHOLD
Jitter buffer threshold - samples needed before starting playback.
Definition ringbuffer.h:143
#define AUDIO_JITTER_LOW_WATER_MARK
Low water mark - warn when available drops below this.
Definition ringbuffer.h:156
#define AUDIO_CROSSFADE_SAMPLES
Crossfade duration in samples for smooth underrun recovery.
Definition ringbuffer.h:186
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:708
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:699
atomic_t crossfade_fade_in
True if we're fading in (recovering from underrun)
Definition ringbuffer.h:221
atomic_t jitter_buffer_filled
True after initial jitter buffer fill.
Definition ringbuffer.h:217
bool jitter_buffer_enabled
Whether jitter buffering is enabled (false for capture, true for playback)
Definition ringbuffer.h:227
atomic_t underrun_count
Count of underrun events for diagnostics.
Definition ringbuffer.h:225
atomic_t crossfade_samples_remaining
Samples remaining in crossfade (0 = no crossfade active)
Definition ringbuffer.h:219

References atomic_exchange_bool(), atomic_fetch_add_u64(), atomic_load_u64(), atomic_store_u64(), 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, audio_ring_buffer::discard_pending, audio_ring_buffer::discard_to_index, 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_dev_every, log_info, LOG_RATE_FAST, log_warn_every, NS_PER_MS_INT, audio_ring_buffer::read_index, SAFE_MEMCPY, SET_ERRNO, audio_ring_buffer::underrun_count, and audio_ring_buffer::write_index.

Referenced by audio_read_samples(), mixer_process(), mixer_process_excluding_source(), session_audio_mix_excluding(), session_audio_read_captured(), and session_capture_read_audio().

◆ audio_ring_buffer_register_atomics()

void audio_ring_buffer_register_atomics ( audio_ring_buffer_t *  rb,
const char *  context_name 
)

#include <audio.h>

Register audio ring buffer atomic fields for debug tracking.

Parameters
rbAudio ring buffer (must not be NULL)
context_nameDescriptive context name for registration (e.g. client_id)

Registers all atomic fields within the audio ring buffer with the named registry for debug tracking and –sync-state output. Should be called after creation.

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

1033 {
1034 if (!rb || !context_name || !context_name[0]) {
1035 return;
1036 }
1037
1038 // Register the ring buffer itself with a unique name
1039 NAMED_REGISTER_AUDIO_RINGBUF(rb, context_name, NULL);
1040
1041 // Ring buffer indices (lock-free producer-consumer)
1042 NAMED_REGISTER_ATOMIC(&rb->write_index, "write_index_producer_position", (uintptr_t)(const void *)(rb));
1043 NAMED_REGISTER_ATOMIC(&rb->read_index, "read_index_consumer_position", (uintptr_t)(const void *)(rb));
1044 NAMED_REGISTER_ATOMIC(&rb->discard_to_index, "discard_to_index_producer_position", (uintptr_t)(const void *)(rb));
1045
1046 // Jitter buffer state management
1047 NAMED_REGISTER_ATOMIC(&rb->jitter_buffer_filled, "jitter_buffer_has_filled_threshold_flag",
1048 (uintptr_t)(const void *)(rb));
1049 NAMED_REGISTER_ATOMIC(&rb->crossfade_samples_remaining, "crossfade_samples_remaining_count",
1050 (uintptr_t)(const void *)(rb));
1051 NAMED_REGISTER_ATOMIC(&rb->crossfade_fade_in, "crossfade_fade_in_direction_flag", (uintptr_t)(const void *)(rb));
1052
1053 // Audio quality monitoring
1054 NAMED_REGISTER_ATOMIC(&rb->underrun_count, "audio_underrun_event_counter", (uintptr_t)(const void *)(rb));
1055}

References audio_ring_buffer::crossfade_fade_in, audio_ring_buffer::crossfade_samples_remaining, audio_ring_buffer::discard_to_index, audio_ring_buffer::jitter_buffer_filled, NAMED_REGISTER_ATOMIC, NAMED_REGISTER_AUDIO_RINGBUF, audio_ring_buffer::read_index, audio_ring_buffer::underrun_count, and audio_ring_buffer::write_index.

◆ audio_ring_buffer_unregister_atomics()

void audio_ring_buffer_unregister_atomics ( audio_ring_buffer_t *  rb)

◆ 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 1098 of file lib/audio/audio.c.

1098 {
1099 if (!rb || !data || samples <= 0)
1100 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: rb=%p, data=%p, samples=%d", rb, data, samples);
1101
1102 // Validate samples doesn't exceed our buffer size
1103 if (samples > AUDIO_RING_BUFFER_SIZE) {
1104 return SET_ERRNO(ERROR_BUFFER, "Attempted to write %d samples, but buffer size is only %d", samples,
1106 }
1107
1108 // LOCK-FREE: Load indices with proper memory ordering
1109 // - Load our own write_index with relaxed (no sync needed with ourselves)
1110 // - Load reader's read_index with acquire (see reader's updates to free space)
1111 unsigned int write_idx = atomic_load_u64(&rb->write_index);
1112 unsigned int read_idx = atomic_load_u64(&rb->read_index);
1113
1114 // Calculate current buffer level (how many samples are buffered)
1115 int buffer_level;
1116 if (write_idx >= read_idx) {
1117 buffer_level = (int)(write_idx - read_idx);
1118 } else {
1119 buffer_level = AUDIO_RING_BUFFER_SIZE - (int)(read_idx - write_idx);
1120 }
1121 // Reserve 1 slot to distinguish between full and empty states
1122 // (when buffer is full, write_idx will be just before read_idx, not equal to it)
1123 int available = AUDIO_RING_BUFFER_SIZE - 1 - buffer_level;
1124
1125 // If playback has fallen behind, ask its consumer to skip the stale queue.
1126 // Dropping incoming samples here preserves the old backlog, so playback
1127 // drains outdated audio and then underruns before it can return to live data.
1128 if (buffer_level > AUDIO_JITTER_HIGH_WATER_MARK) {
1129 if (rb->jitter_buffer_enabled) {
1130 // The read callback owns read_index. Let it advance to the latest write
1131 // position atomically on its next invocation, then preserve this fresh
1132 // packet so audio resumes from the current stream position.
1135 "Audio playback backlog (%d samples); requesting consumer to skip stale queued audio",
1136 buffer_level);
1137 } else {
1138 // Capture queues also have a high-water mark, but their reader may be
1139 // processing a matching microphone/render interval. Retain the existing
1140 // policy there instead of changing the reader's timeline asynchronously.
1141 int target_writes = AUDIO_JITTER_TARGET_LEVEL - buffer_level;
1142 if (target_writes < 0) {
1143 target_writes = 0;
1144 }
1145
1146 if (samples > target_writes) {
1147 int dropped = samples - target_writes;
1149 "Audio capture buffer high water mark exceeded (%d > %d): dropping %d incoming samples",
1150 buffer_level, AUDIO_JITTER_HIGH_WATER_MARK, dropped);
1151 samples = target_writes;
1152 }
1153 }
1154 }
1155
1156 // Now write the new samples - should always have enough space after above
1157 int samples_to_write = samples;
1158 if (samples > available) {
1159 // This should rarely happen after the high water mark logic above
1160 int samples_dropped = samples - available;
1161 samples_to_write = available;
1162 log_warn_every(LOG_RATE_FAST, "Audio buffer overflow: dropping %d of %d incoming samples (buffer_used=%d/%d)",
1163 samples_dropped, samples, AUDIO_RING_BUFFER_SIZE - available, AUDIO_RING_BUFFER_SIZE);
1164 }
1165
1166 // Write only the samples that fit (preserves existing data integrity)
1167 if (samples_to_write > 0) {
1168 int remaining = AUDIO_RING_BUFFER_SIZE - (int)write_idx;
1169
1170 if (samples_to_write <= remaining) {
1171 // Can copy in one chunk
1172 SAFE_MEMCPY(&rb->data[write_idx], samples_to_write * sizeof(float), data, samples_to_write * sizeof(float));
1173 } else {
1174 // Need to wrap around - copy in two chunks
1175 SAFE_MEMCPY(&rb->data[write_idx], remaining * sizeof(float), data, remaining * sizeof(float));
1176 SAFE_MEMCPY(&rb->data[0], (samples_to_write - remaining) * sizeof(float), &data[remaining],
1177 (samples_to_write - remaining) * sizeof(float));
1178 }
1179
1180 // LOCK-FREE: Store new write_index with release ordering
1181 // This ensures all data writes above are visible before the index update
1182 unsigned int new_write_idx = (write_idx + (unsigned int)samples_to_write) % AUDIO_RING_BUFFER_SIZE;
1183 atomic_store_u64(&rb->write_index, new_write_idx);
1184 }
1185
1186 // Note: jitter buffer fill check is now done in read function for better control
1187
1188 return ASCIICHAT_OK; // Success
1189}
@ ERROR_BUFFER
#define AUDIO_JITTER_HIGH_WATER_MARK
High water mark - drop OLD samples when buffer exceeds this.
Definition ringbuffer.h:169
#define AUDIO_JITTER_TARGET_LEVEL
Target buffer level after dropping old samples.
Definition ringbuffer.h:179
void audio_ring_buffer_discard_pending(audio_ring_buffer_t *rb)

References ASCIICHAT_OK, atomic_load_u64(), atomic_store_u64(), AUDIO_JITTER_HIGH_WATER_MARK, AUDIO_JITTER_TARGET_LEVEL, audio_ring_buffer_discard_pending(), AUDIO_RING_BUFFER_SIZE, audio_ring_buffer::data, ERROR_BUFFER, ERROR_INVALID_PARAM, audio_ring_buffer::jitter_buffer_enabled, 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(), handle_audio_packet(), and session_audio_write_source().

◆ 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 1607 of file lib/audio/audio.c.

1607 {
1608 if (!ctx)
1609 return;
1610 ctx->audio_pipeline = pipeline;
1611}
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 2255 of file lib/audio/audio.c.

2255 {
2256 // Delegate to platform abstraction layer
2258 if (result == ASCIICHAT_OK) {
2259 log_debug("✓ Audio thread real-time priority set successfully");
2260 }
2261 return result;
2262}
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_debug.

Referenced by audio_start_duplex().

◆ audio_should_enable_microphone()

bool audio_should_enable_microphone ( audio_capture_source_t  source,
bool  has_media_audio 
)

#include <audio.h>

Determine if microphone should be enabled based on audio source setting and media state.

Smart helper function for determining microphone input availability based on:

  • User's –audio-capture-source preference (auto, microphone, media, both, remote)
  • Whether media audio is currently being played (–file or –url)
Parameters
sourceAudio capture policy from –audio-capture-source option
has_media_audioTrue if media with audio is being played, false otherwise
Returns
true if microphone should be enabled, false otherwise

Behavior by capture source value:

  • AUDIO_CAPTURE_SOURCE_AUTO: Enable microphone only when no media audio (smart default)
  • AUDIO_CAPTURE_SOURCE_MIC: Always enable microphone (ignore media state)
  • AUDIO_CAPTURE_SOURCE_MEDIA: Never enable microphone (media-only)
  • AUDIO_CAPTURE_SOURCE_BOTH: Always enable microphone (allow simultaneous capture)
  • AUDIO_CAPTURE_SOURCE_REMOTE: Disable microphone and local media capture

Usage:

bool has_media = (media_file && strlen(media_file) > 0);
bool enable_mic = audio_should_enable_microphone(GET_OPTION(audio_capture_source), has_media);
if (enable_mic) {
audio_ctx->playback_only = false; // Allow microphone capture
} else {
audio_ctx->playback_only = true; // Disable microphone to prevent interference
}
bool audio_should_enable_microphone(audio_capture_source_t source, bool has_media_audio)
Determine if microphone should be enabled based on audio source setting and media state.
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)

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

2358 {
2359 switch (source) {
2361 return !has_media_audio;
2362
2364 return true;
2365
2367 return false;
2368
2370 return true;
2371
2373 return false;
2374
2375 default:
2376 return !has_media_audio;
2377 }
2378}

References AUDIO_CAPTURE_SOURCE_AUTO, AUDIO_CAPTURE_SOURCE_BOTH, AUDIO_CAPTURE_SOURCE_MEDIA, AUDIO_CAPTURE_SOURCE_MIC, and AUDIO_CAPTURE_SOURCE_REMOTE.

Referenced by audio_start_duplex(), and session_client_like_run().

◆ 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 1632 of file lib/audio/audio.c.

1632 {
1633 if (!ctx || !ctx->initialized) {
1634 return SET_ERRNO(ERROR_INVALID_STATE, "Audio context not initialized");
1635 }
1636
1637 // Check if already running (without holding lock during blocking operations)
1638 // Do this check first before acquiring any locks
1639 if (ctx->duplex_stream || ctx->input_stream || ctx->output_stream) {
1640 return ASCIICHAT_OK;
1641 }
1642
1643 bool media_audio = ctx->capture_media_source && media_source_has_audio(ctx->capture_media_source);
1644 if (!audio_should_enable_microphone(GET_OPTION(audio_capture_source), media_audio))
1645 ctx->playback_only = true;
1646 bool media_only = media_audio && ctx->playback_only;
1647
1648 // Media-only recording can run through the software worker without an audio
1649 // device. Initialize PortAudio opportunistically so monitoring still works
1650 // when a speaker is available.
1651 asciichat_error_t pa_result = audio_ensure_portaudio_initialized();
1652 if (pa_result != ASCIICHAT_OK && !media_only)
1653 return pa_result;
1654
1655 if (pa_result != ASCIICHAT_OK) {
1657 log_warn("PortAudio unavailable; continuing media-only audio without an audio device");
1658 goto start_worker;
1659 }
1660
1661 // Setup input parameters (skip if playback-only mode)
1662 PaStreamParameters inputParams = {0};
1663 const PaDeviceInfo *inputInfo = NULL;
1664 bool has_input = false;
1665
1666 if (!ctx->playback_only) {
1667 if (GET_OPTION(microphone_index) >= 0) {
1668 inputParams.device = GET_OPTION(microphone_index);
1669 } else {
1670 inputParams.device = Pa_GetDefaultInputDevice();
1671 }
1672
1673 if (inputParams.device == paNoDevice) {
1674 return SET_ERRNO(ERROR_AUDIO, "No input device available");
1675 }
1676
1677 inputInfo = Pa_GetDeviceInfo(inputParams.device);
1678 if (!inputInfo) {
1680 return SET_ERRNO(ERROR_AUDIO, "Input device info not found");
1681 }
1682
1683 has_input = true;
1684 inputParams.channelCount = AUDIO_CHANNELS;
1685 inputParams.sampleFormat = paFloat32;
1686 inputParams.suggestedLatency = inputInfo->defaultLowInputLatency;
1687 inputParams.hostApiSpecificStreamInfo = NULL;
1688 }
1689
1690 // Setup output parameters
1691 PaStreamParameters outputParams;
1692 const PaDeviceInfo *outputInfo = NULL;
1693 bool has_output = false;
1694
1695 if (GET_OPTION(audio_no_playback)) {
1696 outputParams.device = paNoDevice;
1697 log_info("Audio playback disabled by --no-audio-playback");
1698 } else if (GET_OPTION(speakers_index) >= 0) {
1699 outputParams.device = GET_OPTION(speakers_index);
1700 } else {
1701 outputParams.device = Pa_GetDefaultOutputDevice();
1702 }
1703
1704 if (outputParams.device != paNoDevice) {
1705 outputInfo = Pa_GetDeviceInfo(outputParams.device);
1706 if (outputInfo) {
1707 has_output = true;
1708 outputParams.channelCount = AUDIO_CHANNELS;
1709 outputParams.sampleFormat = paFloat32;
1710 outputParams.suggestedLatency = outputInfo->defaultLowOutputLatency;
1711 outputParams.hostApiSpecificStreamInfo = NULL;
1712 } else {
1713 log_warn("Output device info not found for device %d", outputParams.device);
1714 }
1715 }
1716
1717 // Store device rates for diagnostics (only access if device info was retrieved)
1718 ctx->input_device_rate = (has_input && inputInfo) ? inputInfo->defaultSampleRate : 0;
1719 ctx->output_device_rate = (has_output && outputInfo) ? outputInfo->defaultSampleRate : 0;
1720
1721 log_debug("Opening audio:");
1722 if (has_input) {
1723 log_info(" Input: %s (%.0f Hz)", inputInfo->name, inputInfo->defaultSampleRate);
1724 } else if (ctx->playback_only) {
1725 log_debug(" Input: (playback-only mode - no microphone)");
1726 } else {
1727 log_debug(" Input: (none)");
1728 }
1729 if (has_output) {
1730 log_info(" Output: %s (%.0f Hz)", outputInfo->name, outputInfo->defaultSampleRate);
1731 } else {
1732 log_debug(" Output: None (input-only mode - will send audio to server)");
1733 }
1734
1735 // Keep capture and playback synchronized in one 48 kHz duplex stream when
1736 // both devices share a native rate. PortAudio handles conversion to/from
1737 // that rate. Separate streams are needed only when the devices themselves
1738 // run at different rates or one direction is unavailable.
1739 bool rates_differ = has_input && has_output && (inputInfo->defaultSampleRate != outputInfo->defaultSampleRate);
1740 bool try_separate = rates_differ || !has_input || !has_output;
1741 PaError err = paNoError;
1742
1743 if (!try_separate) {
1744 // Try full-duplex first (preferred - perfect AEC3 timing)
1745 LOG_IO("portaudio", {
1746 err = Pa_OpenStream(&ctx->duplex_stream, &inputParams, &outputParams, AUDIO_SAMPLE_RATE, AUDIO_FRAMES_PER_BUFFER,
1747 paClipOff, duplex_callback, ctx);
1748 });
1749
1750 if (err == paNoError) {
1751 LOG_IO("portaudio", { err = Pa_StartStream(ctx->duplex_stream); });
1752 if (err != paNoError) {
1753 LOG_IO("portaudio", { Pa_CloseStream(ctx->duplex_stream); });
1754 ctx->duplex_stream = NULL;
1755 log_warn("Full-duplex stream failed to start: %s", Pa_GetErrorText(err));
1756 try_separate = true;
1757 }
1758 } else {
1759 log_warn("Full-duplex stream failed to open: %s", Pa_GetErrorText(err));
1760 try_separate = true;
1761 }
1762 }
1763
1764 if (try_separate) {
1765 // Use separate streams for non-48 kHz devices and single-direction modes.
1766 if (has_output && has_input) {
1767 log_info("Using separate native-rate input/output streams (%.0f Hz in, %.0f Hz out)",
1768 inputInfo->defaultSampleRate, outputInfo->defaultSampleRate);
1769 log_info(" Converting between device rates and the %.0f Hz audio pipeline", (double)AUDIO_SAMPLE_RATE);
1770 } else if (has_output) {
1771 log_debug("Using output-only mode (playback-only for mirror/media)");
1772 } else if (has_input) {
1773 log_info("Using input-only mode (no output device available)");
1774 }
1775
1776 // Store the internal sample rate (buffer rate)
1778
1779 // Create render buffer for AEC3 reference synchronization
1781 if (!ctx->render_buffer) {
1782 return SET_ERRNO(ERROR_MEMORY, "Failed to create render buffer");
1783 }
1784
1785 // Open output stream only if output device exists
1786 bool output_ok = false;
1787 double actual_output_rate = 0;
1788 if (has_output) {
1789 // Use the device's native rate first so conversion stays explicit and stateful.
1790 double native_rate = outputInfo->defaultSampleRate;
1791 if (native_rate <= 0.0) {
1792 native_rate = AUDIO_SAMPLE_RATE;
1793 }
1794 double preferred_rate = native_rate;
1795 double fallback_rate = (preferred_rate == AUDIO_SAMPLE_RATE) ? native_rate : AUDIO_SAMPLE_RATE;
1796
1797 log_debug("Attempting output at native rate %.0f Hz (fallback %.0f Hz)", preferred_rate, fallback_rate);
1798
1799 // Always use output_callback for output streams (both output-only and duplex modes)
1800 // PortAudio will invoke the callback whenever it needs audio data
1801 // The callback reads PCM supplied to the shared playback buffer.
1802 PaStreamCallback *callback = output_callback;
1803
1804 // Try preferred rate first
1805 LOG_IO("portaudio", {
1806 err = Pa_OpenStream(&ctx->output_stream, NULL, &outputParams, preferred_rate, AUDIO_FRAMES_PER_BUFFER,
1807 paClipOff, callback, ctx);
1808 });
1809
1810 if (err == paNoError) {
1811 actual_output_rate = preferred_rate;
1812 output_ok = true;
1813 log_info("Output stream opened at %.0f Hz", preferred_rate);
1814 } else {
1815 log_warn("Failed to open output at %.0f Hz: %s, trying %.0f Hz", preferred_rate, Pa_GetErrorText(err),
1816 fallback_rate);
1817
1818 // If first Pa_OpenStream call left a partial stream, clean it up before retrying
1819 if (ctx->output_stream) {
1820 log_debug("Closing partially-opened output stream from failed preferred rate");
1821 LOG_IO("portaudio", { Pa_CloseStream(ctx->output_stream); });
1822 ctx->output_stream = NULL;
1823 }
1824
1825 // Try the other endpoint rate if the native rate is unavailable.
1826 LOG_IO("portaudio", {
1827 err = Pa_OpenStream(&ctx->output_stream, NULL, &outputParams, fallback_rate, AUDIO_FRAMES_PER_BUFFER,
1828 paClipOff, callback, ctx);
1829 });
1830
1831 if (err == paNoError) {
1832 actual_output_rate = fallback_rate;
1833 output_ok = true;
1834 log_info("Output stream opened at fallback rate %.0f Hz", fallback_rate);
1835 } else {
1836 log_warn("Failed to open output stream at fallback rate %.0f Hz: %s", fallback_rate, Pa_GetErrorText(err));
1837 // Clean up if fallback also failed
1838 if (ctx->output_stream) {
1839 log_debug("Closing partially-opened output stream from failed native rate");
1840 LOG_IO("portaudio", { Pa_CloseStream(ctx->output_stream); });
1841 ctx->output_stream = NULL;
1842 }
1843 }
1844 }
1845
1846 // Store actual output rate for resampling
1847 if (output_ok) {
1848 ctx->output_device_rate = actual_output_rate;
1849 ctx->output_resample_phase = 0.0;
1850 ctx->output_resample_previous = 0.0f;
1851 ctx->output_resample_has_previous = false;
1852 if (actual_output_rate != AUDIO_SAMPLE_RATE) {
1853 log_warn("⚠️ Output rate mismatch: %.0f Hz output vs %.0f Hz input - resampling will be used",
1854 actual_output_rate, (double)AUDIO_SAMPLE_RATE);
1855 }
1856 }
1857 }
1858
1859 // Open input stream only if we have input (skip for playback-only mode)
1860 bool input_ok = !has_input; // If no input, mark as OK (skip)
1861 if (has_input) {
1862 // Open at the device's native rate. The separate input callback converts
1863 // to AUDIO_SAMPLE_RATE before samples enter the shared processing path.
1864 double input_stream_rate = inputInfo->defaultSampleRate;
1865 if (input_stream_rate <= 0.0) {
1866 input_stream_rate = AUDIO_SAMPLE_RATE;
1867 }
1868 LOG_IO("portaudio", {
1869 err = Pa_OpenStream(&ctx->input_stream, &inputParams, NULL, input_stream_rate, AUDIO_FRAMES_PER_BUFFER,
1870 paClipOff, input_callback, ctx);
1871 });
1872 input_ok = (err == paNoError);
1873 if (input_ok) {
1874 ctx->input_device_rate = input_stream_rate;
1875 ctx->input_resample_phase = 0.0;
1876 ctx->input_resample_previous = 0.0f;
1877 ctx->input_resample_has_previous = false;
1878 log_info("Input stream opened at native rate %.0f Hz", input_stream_rate);
1879 }
1880
1881 // If input failed, try device 0 as fallback (HDMI on BeaglePlay)
1882 if (!input_ok) {
1883 log_debug("Input failed - trying device 0 as fallback");
1884
1885 // Clean up partial stream from first attempt before retrying
1886 if (ctx->input_stream) {
1887 log_debug("Closing partially-opened input stream from failed primary device");
1888 LOG_IO("portaudio", { Pa_CloseStream(ctx->input_stream); });
1889 ctx->input_stream = NULL;
1890 }
1891
1892 PaStreamParameters fallback_input_params = inputParams;
1893 fallback_input_params.device = 0;
1894 const PaDeviceInfo *device_0_info = Pa_GetDeviceInfo(0);
1895 if (device_0_info && device_0_info->maxInputChannels > 0) {
1896 LOG_IO("portaudio", {
1897 err = Pa_OpenStream(&ctx->input_stream, &fallback_input_params, NULL, input_stream_rate,
1898 AUDIO_FRAMES_PER_BUFFER, paClipOff, input_callback, ctx);
1899 });
1900 if (err == paNoError) {
1901 log_info("Input stream opened on device 0 (fallback from default)");
1902 input_ok = true;
1903 } else {
1904 log_warn("Fallback also failed on device 0: %s", Pa_GetErrorText(err));
1905 // Clean up if fallback also failed
1906 if (ctx->input_stream) {
1907 log_debug("Closing partially-opened input stream from failed fallback device");
1908 LOG_IO("portaudio", { Pa_CloseStream(ctx->input_stream); });
1909 ctx->input_stream = NULL;
1910 }
1911 }
1912 }
1913 }
1914
1915 if (!input_ok) {
1916 log_warn("Failed to open input stream: %s", Pa_GetErrorText(err));
1917 }
1918 }
1919
1920 // Check if we got at least one stream working
1921 if (!input_ok && !output_ok && !media_only) {
1922 // Neither stream works - fail completely
1924 ctx->render_buffer = NULL;
1925 return SET_ERRNO(ERROR_AUDIO, "Failed to open both input and output streams");
1926 }
1927
1928 // If output failed but input works, we can still send audio to server
1929 if (!output_ok && input_ok) {
1930 log_info("Output stream unavailable - continuing with input-only (can send audio to server)");
1931 ctx->output_stream = NULL;
1932 }
1933 // If input failed but output works, we can still receive audio from server
1934 if (!input_ok && output_ok) {
1935 log_info("Input stream unavailable - continuing with output-only (can receive audio from server)");
1936 ctx->input_stream = NULL;
1937 }
1938
1939 // Start output stream if it's open
1940 if (ctx->output_stream) {
1941 LOG_IO("portaudio", { err = Pa_StartStream(ctx->output_stream); });
1942 if (err != paNoError) {
1943 if (ctx->input_stream) {
1944 LOG_IO("portaudio", { Pa_CloseStream(ctx->input_stream); });
1945 }
1946 LOG_IO("portaudio", { Pa_CloseStream(ctx->output_stream); });
1947 ctx->input_stream = NULL;
1948 ctx->output_stream = NULL;
1950 ctx->render_buffer = NULL;
1951 return SET_ERRNO(ERROR_AUDIO, "Failed to start output stream: %s", Pa_GetErrorText(err));
1952 }
1953 }
1954
1955 // Start input stream if it's open
1956 if (ctx->input_stream) {
1957 LOG_IO("portaudio", { err = Pa_StartStream(ctx->input_stream); });
1958 if (err != paNoError) {
1959 if (ctx->output_stream) {
1960 LOG_IO("portaudio", { Pa_StopStream(ctx->output_stream); });
1961 }
1962 if (ctx->input_stream) {
1963 LOG_IO("portaudio", { Pa_CloseStream(ctx->input_stream); });
1964 }
1965 if (ctx->output_stream) {
1966 LOG_IO("portaudio", { Pa_CloseStream(ctx->output_stream); });
1967 }
1968 ctx->input_stream = NULL;
1969 ctx->output_stream = NULL;
1971 ctx->render_buffer = NULL;
1972 return SET_ERRNO(ERROR_AUDIO, "Failed to start input stream: %s", Pa_GetErrorText(err));
1973 }
1974 }
1975
1976 ctx->separate_streams = true;
1977 log_debug("Separate streams started successfully");
1978 } else {
1979 ctx->separate_streams = false;
1980 log_info("Full-duplex stream started (single callback, perfect AEC3 timing)");
1981 }
1982start_worker:
1983
1985
1986 // Start worker thread for heavy audio processing
1987 if (!ctx->worker_running) {
1989 if (asciichat_thread_create(&ctx->worker_thread, "audio_worker", audio_worker_thread, ctx) != 0) {
1990 // Failed to create worker thread - stop streams and cleanup
1991 if (ctx->duplex_stream) {
1992 LOG_IO("portaudio", {
1993 Pa_StopStream(ctx->duplex_stream);
1994 Pa_CloseStream(ctx->duplex_stream);
1995 });
1996 ctx->duplex_stream = NULL;
1997 }
1998 if (ctx->input_stream) {
1999 LOG_IO("portaudio", {
2000 Pa_StopStream(ctx->input_stream);
2001 Pa_CloseStream(ctx->input_stream);
2002 });
2003 ctx->input_stream = NULL;
2004 }
2005 if (ctx->output_stream) {
2006 LOG_IO("portaudio", {
2007 Pa_StopStream(ctx->output_stream);
2008 Pa_CloseStream(ctx->output_stream);
2009 });
2010 ctx->output_stream = NULL;
2011 }
2013 ctx->render_buffer = NULL;
2014 return SET_ERRNO(ERROR_THREAD, "Failed to create worker thread");
2015 }
2016 ctx->worker_running = true;
2017 log_debug("Worker thread started successfully");
2018 }
2019
2020 // Lock to update final state flags atomically
2021 mutex_lock(&ctx->state_mutex);
2022 ctx->running = true;
2025 return ASCIICHAT_OK;
2026}
#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
bool media_source_has_audio(media_source_t *source)
Check if media source has audio stream.
Definition source.c:737
#define LOG_IO(prefix, block)
Capture output from a code block and log it.
Definition io.h:81
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
#define paNoError
Definition portaudio.h:19
int PaError
Definition portaudio.h:14
double input_device_rate
Native sample rate of input device.
float input_resample_previous
Last input sample used at a block boundary.
PaStream * input_stream
Separate input stream (when full-duplex unavailable)
bool input_resample_has_previous
Whether a prior input block has been processed.
bool separate_streams
True if using separate input/output streams.
float output_resample_previous
Last source sample used at an output boundary.
double output_device_rate
Native sample rate of output device.
double output_resample_phase
48 kHz source position carried between output callbacks
bool playback_only
True for playback-only mode (mirror), skip microphone capture.
PaStream * duplex_stream
PortAudio full-duplex stream (simultaneous input+output)
double input_resample_phase
Source position carried between native-rate input blocks.
PaStream * output_stream
Separate output stream (when full-duplex unavailable)
bool output_resample_has_previous
Whether a prior output block has been processed.

References ASCIICHAT_OK, asciichat_thread_create, atomic_store_bool(), AUDIO_CHANNELS, AUDIO_FRAMES_PER_BUFFER, audio_ring_buffer_create_for_capture(), audio_ring_buffer_destroy(), AUDIO_SAMPLE_RATE, audio_set_realtime_priority(), audio_should_enable_microphone(), audio_context_t::capture_media_source, audio_context_t::duplex_stream, ERROR_AUDIO, ERROR_INVALID_STATE, ERROR_MEMORY, ERROR_THREAD, GET_OPTION, audio_context_t::initialized, audio_context_t::input_device_rate, audio_context_t::input_resample_has_previous, audio_context_t::input_resample_phase, audio_context_t::input_resample_previous, audio_context_t::input_stream, log_debug, log_info, LOG_IO, log_warn, media_source_has_audio(), mutex_lock, mutex_unlock, audio_context_t::output_device_rate, audio_context_t::output_resample_has_previous, audio_context_t::output_resample_phase, audio_context_t::output_resample_previous, audio_context_t::output_stream, paNoError, audio_context_t::playback_only, audio_context_t::render_buffer, audio_context_t::running, audio_context_t::sample_rate, audio_context_t::separate_streams, SET_ERRNO, audio_context_t::state_mutex, audio_context_t::worker_running, audio_context_t::worker_should_stop, and audio_context_t::worker_thread.

Referenced by protocol_start_connection(), session_audio_start_duplex(), and session_client_like_run().

◆ 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 2028 of file lib/audio/audio.c.

2028 {
2029 if (!ctx || !ctx->initialized) {
2030 return SET_ERRNO(ERROR_INVALID_STATE, "Audio context not initialized");
2031 }
2032
2033 atomic_store_bool(&ctx->shutting_down, true);
2034
2035 // Stop worker thread before stopping streams
2036 if (ctx->worker_running) {
2037 log_debug("Stopping worker thread");
2039 cond_signal(&ctx->worker_cond); // Wake up worker if waiting
2041 ctx->worker_running = false;
2042 log_debug("Worker thread stopped successfully");
2043 }
2044
2045 if (ctx->playback_buffer) {
2047 }
2048
2049 mutex_lock(&ctx->state_mutex);
2050
2051 if (ctx->duplex_stream) {
2052 log_debug("Stopping duplex stream");
2053 PaError err;
2054 LOG_IO("portaudio", { err = Pa_StopStream(ctx->duplex_stream); });
2055 if (err != paNoError) {
2056 log_warn("Pa_StopStream failed: %s", Pa_GetErrorText(err));
2057 }
2058 log_debug("Closing duplex stream");
2059 LOG_IO("portaudio", { err = Pa_CloseStream(ctx->duplex_stream); });
2060 if (err != paNoError) {
2061 log_warn("Pa_CloseStream failed: %s", Pa_GetErrorText(err));
2062 } else {
2063 log_debug("Duplex stream closed successfully");
2064 }
2065 ctx->duplex_stream = NULL;
2066 }
2067
2068 // Stop separate streams if used
2069 if (ctx->input_stream) {
2070 log_debug("Stopping input stream");
2071 PaError err;
2072 LOG_IO("portaudio", { err = Pa_StopStream(ctx->input_stream); });
2073 if (err != paNoError) {
2074 log_warn("Pa_StopStream input failed: %s", Pa_GetErrorText(err));
2075 }
2076 log_debug("Closing input stream");
2077 LOG_IO("portaudio", { err = Pa_CloseStream(ctx->input_stream); });
2078 if (err != paNoError) {
2079 log_warn("Pa_CloseStream input failed: %s", Pa_GetErrorText(err));
2080 } else {
2081 log_debug("Input stream closed successfully");
2082 }
2083 ctx->input_stream = NULL;
2084 }
2085
2086 if (ctx->output_stream) {
2087 log_debug("Stopping output stream");
2088 PaError err;
2089 LOG_IO("portaudio", { err = Pa_StopStream(ctx->output_stream); });
2090 if (err != paNoError) {
2091 log_warn("Pa_StopStream output failed: %s", Pa_GetErrorText(err));
2092 }
2093 log_debug("Closing output stream");
2094 LOG_IO("portaudio", { err = Pa_CloseStream(ctx->output_stream); });
2095 if (err != paNoError) {
2096 log_warn("Pa_CloseStream output failed: %s", Pa_GetErrorText(err));
2097 } else {
2098 log_debug("Output stream closed successfully");
2099 }
2100 ctx->output_stream = NULL;
2101 }
2102
2103 // Cleanup render buffer
2104 if (ctx->render_buffer) {
2106 ctx->render_buffer = NULL;
2107 }
2108
2109 ctx->running = false;
2110 ctx->separate_streams = false;
2112
2113 log_debug("Audio stopped");
2114 return ASCIICHAT_OK;
2115}

References ASCIICHAT_OK, asciichat_thread_join, atomic_store_bool(), audio_ring_buffer_clear(), audio_ring_buffer_destroy(), cond_signal(), audio_context_t::duplex_stream, ERROR_INVALID_STATE, audio_context_t::initialized, audio_context_t::input_stream, log_debug, LOG_IO, log_warn, mutex_lock, mutex_unlock, audio_context_t::output_stream, paNoError, 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, audio_context_t::state_mutex, audio_context_t::worker_cond, audio_context_t::worker_running, audio_context_t::worker_should_stop, and audio_context_t::worker_thread.

Referenced by audio_cleanup(), audio_destroy(), session_audio_stop(), and session_client_like_run().

◆ audio_terminate_portaudio_final()

void audio_terminate_portaudio_final ( void  )

#include <audio.h>

Terminate PortAudio and free all device resources.

This must be called at program shutdown to free device structures allocated by ALSA/libpulse during Pa_Initialize(). Should be called AFTER all audio contexts are destroyed and other session cleanup is complete.

Note
This is only needed if you want to ensure all memory is freed at exit. In normal operation, the OS will clean up when the process terminates.

This must be called to actually free device structures allocated by ALSA/libpulse. It should be called AFTER all audio contexts are destroyed and session cleanup is complete.

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

87 {
88 if (lifecycle_shutdown(&g_pa_lc)) {
89 log_debug("[PORTAUDIO_TERM] Calling Pa_Terminate() to release PortAudio");
90
91 PaError err;
92 LOG_IO("portaudio", { err = Pa_Terminate(); });
93
94 log_debug("[PORTAUDIO_TERM] Pa_Terminate() returned: %s", Pa_GetErrorText(err));
95 }
96}
bool lifecycle_shutdown(lifecycle_t *lc)
Definition lifecycle.c:108

References lifecycle_shutdown(), log_debug, and LOG_IO.

Referenced by audio_cleanup().

◆ 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 2294 of file lib/audio/audio.c.

2294 {
2295 if (!batch) {
2296 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch info pointer is NULL");
2297 }
2298
2299 // Validate batch_count
2300 if (batch->batch_count == 0) {
2301 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch count cannot be zero");
2302 }
2303
2304 // Check for reasonable max (256 frames per batch is very generous)
2305 if (batch->batch_count > 256) {
2306 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch count too large (batch_count=%u, max=256)", batch->batch_count);
2307 }
2308
2309 // Validate channels (1=mono, 2=stereo, max 8 for multi-channel)
2310 if (batch->channels == 0 || batch->channels > 8) {
2311 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid channel count (channels=%u, valid=1-8)", batch->channels);
2312 }
2313
2314 // Validate sample rate
2316 return SET_ERRNO(ERROR_INVALID_PARAM, "Unsupported sample rate (sample_rate=%u)", batch->sample_rate);
2317 }
2318
2319 // Check for reasonable sample counts
2320 if (batch->total_samples == 0) {
2321 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch has zero samples");
2322 }
2323
2324 // Each batch typically has samples_per_frame worth of samples
2325 // For 48kHz at 20ms per frame: 48000 * 0.02 = 960 samples per frame
2326 // With max 256 frames, that's up to ~245k samples per batch
2327 if (batch->total_samples > 1000000) {
2328 return SET_ERRNO(ERROR_INVALID_PARAM, "Audio batch sample count suspiciously large (total_samples=%u)",
2329 batch->total_samples);
2330 }
2331
2332 return ASCIICHAT_OK;
2333}

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 2128 of file lib/audio/audio.c.

2128 {
2129 if (!ctx || !ctx->initialized || !buffer || num_samples <= 0) {
2130 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, buffer=%p, num_samples=%d", ctx, buffer,
2131 num_samples);
2132 }
2133
2134 // Don't accept new audio data during shutdown - this prevents garbage/beeps
2135 if (atomic_load_bool(&ctx->shutting_down)) {
2136 return ASCIICHAT_OK; // Silently discard
2137 }
2138
2141 asciichat_error_t result = audio_ring_buffer_write(ctx->playback_buffer, buffer, num_samples);
2142
2143 return result;
2144}
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
asciichat_error_t audio_ring_buffer_write(audio_ring_buffer_t *rb, const float *data, int samples)
Write audio samples to ring buffer.
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
void audio_recording_submit(audio_recording_source_t source, const float *samples, size_t count, uint64_t start_ns)
Definition recording.c:61
@ AUDIO_RECORDING_REMOTE
Definition recording.h:12
void audio_visualization_submit(audio_visualization_source_t source, const float *samples, size_t count)
@ AUDIO_VISUALIZATION_SOURCE_REMOTE

References ASCIICHAT_OK, atomic_load_bool(), AUDIO_RECORDING_REMOTE, audio_recording_submit(), audio_ring_buffer_write(), AUDIO_VISUALIZATION_SOURCE_REMOTE, audio_visualization_submit(), ERROR_INVALID_PARAM, audio_context_t::initialized, audio_context_t::playback_buffer, SET_ERRNO, audio_context_t::shutting_down, and time_get_ns().

Referenced by audio_process_received_samples(), and session_audio_write_playback().

◆ 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 1157 of file mixer.c.

1157 {
1158 if (!src || !dst || count <= 0)
1159 return;
1160 for (int i = 0; i < count; i++) {
1161 dst[i] = float_to_int16(src[i]);
1162 }
1163}
int16_t float_to_int16(float sample)
Convert float sample to int16 (WebRTC format)
Definition mixer.c:1144

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 1165 of file mixer.c.

1165 {
1166 if (!src || !dst || count <= 0)
1167 return;
1168 for (int i = 0; i < count; i++) {
1169 dst[i] = int16_to_float(src[i]);
1170 }
1171}
float int16_to_float(int16_t sample)
Convert int16 sample to float.
Definition mixer.c:1153

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 1173 of file mixer.c.

1173 {
1174 if (!buffer || count <= 0)
1175 return 0.0f;
1176
1177 float peak = 0.0f;
1178 for (int i = 0; i < count; i++) {
1179 float abs_sample = fabsf(buffer[i]);
1180 if (abs_sample > peak)
1181 peak = abs_sample;
1182 }
1183 return peak;
1184}

◆ 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 75 of file mixer.c.

75 {
76 if (value < min) {
77 return min;
78 }
79 if (value > max) {
80 return max;
81 }
82 return value;
83}

◆ 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 86 of file mixer.c.

86 {
87 comp->sample_rate = sample_rate;
88 comp->envelope = 0.0f;
89 comp->gain_lin = 1.0f;
90
91 // Set default parameters with 0dB makeup gain (unity)
92 // Soft clip at 0.7 provides 3dB headroom to prevent hard clipping
93 // Ducking and crowd scaling naturally reduce volume, no compensation needed
94 compressor_set_params(comp, -10.0f, 4.0f, 10 * NS_PER_MS_INT, 100 * NS_PER_MS_INT, 0.0f);
95}
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, uint64_t attack_ns, uint64_t release_ns, float makeup_dB)
Set compressor parameters.
Definition mixer.c:97
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, NS_PER_MS_INT, 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 131 of file mixer.c.

131 {
132 float x = fabsf(sidechain);
133
134 // Update envelope with attack/release
135 if (x > comp->envelope)
136 comp->envelope = comp->attack_coeff * comp->envelope + (1.0f - comp->attack_coeff) * x;
137 else
138 comp->envelope = comp->release_coeff * comp->envelope + (1.0f - comp->release_coeff) * x;
139
140 // Calculate gain reduction
141 float level_dB = linear_to_db(comp->envelope);
142 float gr_dB = compressor_gain_reduction_db(comp, level_dB);
143 float target_lin = db_to_linear(gr_dB + comp->makeup_dB);
144
145 // Smooth gain changes
146 if (target_lin < comp->gain_lin)
147 comp->gain_lin = comp->attack_coeff * comp->gain_lin + (1.0f - comp->attack_coeff) * target_lin;
148 else
149 comp->gain_lin = comp->release_coeff * comp->gain_lin + (1.0f - comp->release_coeff) * target_lin;
150
151 return comp->gain_lin;
152}
float db_to_linear(float db)
Convert decibels to linear gain.
Definition mixer.c:67
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:71
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,
uint64_t  attack_ns,
uint64_t  release_ns,
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 97 of file mixer.c.

98 {
99 comp->threshold_dB = threshold_dB;
100 comp->ratio = ratio;
101 comp->attack_ns = attack_ns;
102 comp->release_ns = release_ns;
103 comp->makeup_dB = makeup_dB;
104 comp->knee_dB = 2.0f; // Fixed soft knee
105
106 // Calculate time constants
107 float attack_tau = (float)attack_ns / NS_PER_SEC;
108 float release_tau = (float)release_ns / NS_PER_SEC;
109 comp->attack_coeff = expf(-1.0f / (attack_tau * comp->sample_rate + 1e-12f));
110 comp->release_coeff = expf(-1.0f / (release_tau * comp->sample_rate + 1e-12f));
111}
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 threshold_dB
Compression threshold in dB (e.g., -10.0)
Definition mixer.h:139
uint64_t attack_ns
Attack time in nanoseconds (how fast compression kicks in)
Definition mixer.h:145
uint64_t release_ns
Release time in nanoseconds (how fast compression releases)
Definition mixer.h:147
#define NS_PER_SEC
Definition time.h:148

References compressor_t::attack_coeff, compressor_t::attack_ns, compressor_t::knee_dB, compressor_t::makeup_dB, NS_PER_SEC, compressor_t::ratio, compressor_t::release_coeff, compressor_t::release_ns, 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 1218 of file mixer.c.

1218 {
1219 if (!src || !dst || count <= 0)
1220 return;
1221
1222 for (int i = 0; i < count; i++) {
1223 dst[i] = src[i] * gain;
1224 }
1225}

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 67 of file mixer.c.

67 {
68 return powf(10.0f, db / 20.0f);
69}

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

◆ ducking_destroy()

void ducking_destroy ( ducking_t *  duck)

#include <mixer.h>

Free ducking system resources.

Parameters
duckDucking structure

Frees per-source arrays allocated by ducking_init().

Definition at line 210 of file mixer.c.

210 {
211 if (duck->envelope) {
212 SAFE_FREE(duck->envelope);
213 }
214 if (duck->gain) {
215 SAFE_FREE(duck->gain);
216 }
217}
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_destroy() to prevent memory leaks.
Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 155 of file mixer.c.

155 {
156 if (!duck) {
157 return SET_ERRNO(ERROR_INVALID_PARAM, "ducking_init: duck is NULL");
158 }
159
160 // Set default parameters
161 // threshold_dB: sources below this aren't considered "speaking"
162 // leader_margin_dB: sources within this margin of loudest are all "leaders"
163 // atten_dB: how much to attenuate non-leaders (was -12dB, too aggressive)
164 duck->threshold_dB = -45.0f; // More lenient threshold
165 duck->leader_margin_dB = 6.0f; // Wider margin = more sources treated as leaders
166 duck->atten_dB = -6.0f; // Only -6dB attenuation (was -12dB)
167 duck->attack_ns = 10 * NS_PER_MS_INT; // Slower attack (10ms)
168 duck->release_ns = 200 * NS_PER_MS_INT; // Slower release (200ms)
169 duck->envelope = NULL;
170 duck->gain = NULL;
171
172 // Calculate time constants
173 float attack_tau = (float)duck->attack_ns / NS_PER_SEC;
174 float release_tau = (float)duck->release_ns / NS_PER_SEC;
175 duck->attack_coeff = expf(-1.0f / (attack_tau * sample_rate + 1e-12f));
176 duck->release_coeff = expf(-1.0f / (release_tau * sample_rate + 1e-12f));
177
178 // Allocate arrays with overflow checking
179 size_t envelope_size = 0;
180 if (checked_size_mul((size_t)num_sources, sizeof(float), &envelope_size) != ASCIICHAT_OK) {
181 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Ducking envelope array size overflow: %d sources", num_sources);
182 }
183 duck->envelope = SAFE_MALLOC(envelope_size, float *);
184 if (!duck->envelope) {
185 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate ducking envelope array");
186 }
187
188 size_t gain_size = 0;
189 if (checked_size_mul((size_t)num_sources, sizeof(float), &gain_size) != ASCIICHAT_OK) {
190 SAFE_FREE(duck->envelope);
191 duck->envelope = NULL;
192 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Ducking gain array size overflow: %d sources", num_sources);
193 }
194 duck->gain = SAFE_MALLOC(gain_size, float *);
195 if (!duck->gain) {
196 SAFE_FREE(duck->envelope);
197 duck->envelope = NULL;
198 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate ducking gain array");
199 }
200
201 // Initialize
202 SAFE_MEMSET(duck->envelope, (size_t)num_sources * sizeof(float), 0, (size_t)num_sources * sizeof(float));
203 for (int i = 0; i < num_sources; i++) {
204 duck->gain[i] = 1.0f;
205 }
206
207 return ASCIICHAT_OK;
208}
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
uint64_t release_ns
Ducking release time in nanoseconds.
Definition mixer.h:281
float threshold_dB
Speaking threshold in dB (sources below this are not "speaking")
Definition mixer.h:273
uint64_t attack_ns
Ducking attack time in nanoseconds.
Definition mixer.h:279
@ ERROR_BUFFER_OVERFLOW

References ASCIICHAT_OK, ducking_t::attack_coeff, ducking_t::attack_ns, ducking_t::atten_dB, ducking_t::envelope, ERROR_BUFFER_OVERFLOW, ERROR_INVALID_PARAM, ERROR_MEMORY, ducking_t::gain, ducking_t::leader_margin_dB, NS_PER_MS_INT, NS_PER_SEC, ducking_t::release_coeff, ducking_t::release_ns, 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 228 of file mixer.c.

228 {
229 // Find the loudest source
230 float max_dB = -120.0f;
231 float env_dB[MIXER_MAX_SOURCES];
232
233 for (int i = 0; i < num_sources; i++) {
234 env_dB[i] = linear_to_db(envelopes[i]);
235 if (env_dB[i] > max_dB)
236 max_dB = env_dB[i];
237 }
238
239 // Calculate ducking gain for each source
240 float leader_cut = db_to_linear(duck->atten_dB);
241
242 for (int i = 0; i < num_sources; i++) {
243 bool is_speaking = env_dB[i] > duck->threshold_dB;
244 bool is_leader = is_speaking && (env_dB[i] >= max_dB - duck->leader_margin_dB);
245
246 float target;
247 if (is_speaking && !is_leader) {
248 target = leader_cut;
249 } else {
250 target = 1.0f;
251 }
252
253 // Smooth gain transitions
254 if (target < gains[i])
255 gains[i] = duck->attack_coeff * gains[i] + (1.0f - duck->attack_coeff) * target;
256 else
257 gains[i] = duck->release_coeff * gains[i] + (1.0f - duck->release_coeff) * target;
258 }
259}
#define MIXER_MAX_SOURCES
Maximum number of simultaneous audio sources.
Definition mixer.h:104

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,
uint64_t  attack_ns,
uint64_t  release_ns 
)

#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 219 of file mixer.c.

220 {
221 duck->threshold_dB = threshold_dB;
222 duck->leader_margin_dB = leader_margin_dB;
223 duck->atten_dB = atten_dB;
224 duck->attack_ns = attack_ns;
225 duck->release_ns = release_ns;
226}

References ducking_t::attack_ns, ducking_t::atten_dB, ducking_t::leader_margin_dB, ducking_t::release_ns, 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 1195 of file mixer.c.

1195 {
1196 if (!buffer || count <= 0)
1197 return;
1198
1199 float step = (end_gain - start_gain) / (float)count;
1200 float gain = start_gain;
1201 for (int i = 0; i < count; i++) {
1202 buffer[i] *= gain;
1203 gain += step;
1204 }
1205}

◆ 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 1207 of file mixer.c.

1207 {
1208 if (!buffer || count <= 0)
1209 return;
1210
1211 for (int i = 0; i < count; i++) {
1212 float t = (float)i / (float)(count - 1);
1213 float gain = smoothstep(fade_in ? t : (1.0f - t));
1214 buffer[i] *= gain;
1215 }
1216}
float smoothstep(float t)
Compute smoothstep interpolation.
Definition mixer.c:1136

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 1144 of file mixer.c.

1144 {
1145 // Clamp to [-1, 1] then scale to int16 range
1146 if (sample > 1.0f)
1147 sample = 1.0f;
1148 if (sample < -1.0f)
1149 sample = -1.0f;
1150 return (int16_t)(sample * 32767.0f);
1151}

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 1010 of file mixer.c.

1010 {
1011 if (!filter)
1012 return;
1013
1014 filter->cutoff_hz = cutoff_hz;
1015 filter->sample_rate = sample_rate;
1016
1017 // Calculate filter coefficient
1018 // alpha = 1 / (1 + 2*pi*fc/fs)
1019 filter->alpha = 1.0f / (1.0f + 2.0f * M_PI * cutoff_hz / sample_rate);
1020
1021 highpass_filter_reset(filter);
1022}
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:1024
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:63

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 1046 of file mixer.c.

1046 {
1047 if (!filter || !buffer || num_samples <= 0)
1048 return;
1049
1050 for (int i = 0; i < num_samples; i++) {
1051 buffer[i] = highpass_filter_process_sample(filter, buffer[i]);
1052 }
1053}
float highpass_filter_process_sample(highpass_filter_t *filter, float input)
Process a single sample through high-pass filter.
Definition mixer.c:1032

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 1032 of file mixer.c.

1032 {
1033 if (!filter)
1034 return input;
1035
1036 // First-order high-pass filter
1037 // y[n] = alpha * (y[n-1] + x[n] - x[n-1])
1038 float output = filter->alpha * (filter->prev_output + input - filter->prev_input);
1039
1040 filter->prev_input = input;
1041 filter->prev_output = output;
1042
1043 return output;
1044}
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 1024 of file mixer.c.

1024 {
1025 if (!filter)
1026 return;
1027
1028 filter->prev_input = 0.0f;
1029 filter->prev_output = 0.0f;
1030}

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 1153 of file mixer.c.

1153 {
1154 return (float)sample / 32768.0f;
1155}

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 71 of file mixer.c.

71 {
72 return 20.0f * log10f(fmaxf(linear, 1e-12f));
73}

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 1060 of file mixer.c.

1060 {
1061 if (!filter)
1062 return;
1063
1064 filter->cutoff_hz = cutoff_hz;
1065 filter->sample_rate = sample_rate;
1066
1067 // Calculate filter coefficient using RC time constant formula
1068 // alpha = dt / (RC + dt) where RC = 1 / (2 * pi * fc)
1069 float dt = 1.0f / sample_rate;
1070 float rc = 1.0f / (2.0f * (float)M_PI * cutoff_hz);
1071 filter->alpha = dt / (rc + dt);
1072
1073 lowpass_filter_reset(filter);
1074}
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:1076

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 1095 of file mixer.c.

1095 {
1096 if (!filter || !buffer || num_samples <= 0)
1097 return;
1098
1099 for (int i = 0; i < num_samples; i++) {
1100 buffer[i] = lowpass_filter_process_sample(filter, buffer[i]);
1101 }
1102}
float lowpass_filter_process_sample(lowpass_filter_t *filter, float input)
Process a single sample through low-pass filter.
Definition mixer.c:1083

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 1083 of file mixer.c.

1083 {
1084 if (!filter)
1085 return input;
1086
1087 // First-order IIR low-pass filter: y[n] = alpha * x[n] + (1 - alpha) * y[n-1]
1088 float output = filter->alpha * input + (1.0f - filter->alpha) * filter->prev_output;
1089
1090 filter->prev_output = output;
1091
1092 return output;
1093}
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 1076 of file mixer.c.

1076 {
1077 if (!filter)
1078 return;
1079
1080 filter->prev_output = 0.0f;
1081}

References lowpass_filter_t::prev_output.

Referenced by lowpass_filter_init().

◆ mixer_add_source()

int mixer_add_source ( mixer_t *  mixer,
const char *  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 424 of file mixer.c.

424 {
425 if (!mixer || !buffer)
426 return -1;
427
428 // OPTIMIZATION 2: Acquire write lock for source modification
429 rwlock_wrlock(&mixer->source_lock);
430
431 // Find an empty slot
432 int slot = -1;
433 for (int i = 0; i < mixer->max_sources; i++) {
434 if (mixer->source_ids[i] == NULL) {
435 slot = i;
436 break;
437 }
438 }
439
440 if (slot == -1) {
442 log_warn("Mixer: No available slots for client %s", client_id);
443 return -1;
444 }
445
446 mixer->source_buffers[slot] = buffer;
447 // Duplicate the client_id string - we own this memory now
448 size_t client_id_len = strlen(client_id) + 1;
449 mixer->source_ids[slot] = SAFE_MALLOC(client_id_len, char *);
450 if (!mixer->source_ids[slot]) {
451 log_warn("Mixer: Failed to allocate memory for client_id %s", client_id);
452 mixer->source_buffers[slot] = NULL;
454 return -1;
455 }
456 asciichat_error_t strcpy_result = SAFE_STRCPY((char *)mixer->source_ids[slot], client_id_len, client_id);
457 if (strcpy_result != ASCIICHAT_OK) {
458 log_error("Mixer: Failed to copy client_id: %s", asciichat_error_string(strcpy_result));
459 SAFE_FREE(mixer->source_ids[slot]);
460 mixer->source_buffers[slot] = NULL;
462 return -1;
463 }
464 mixer->source_active[slot] = true;
465 mixer->num_sources++;
466
467 // OPTIMIZATION 1: Update bitset optimization structures
468 mixer->active_sources_mask |= (1ULL << slot); // Set bit for this slot
469 // Hash table optimization disabled for string-based client_ids (linear search is acceptable for small mixer sizes)
470
472
473 log_info("Mixer: Added source for client %s at slot %d", client_id, slot);
474 return slot;
475}
const char ** source_ids
Array of client ID strings (one per source slot)
Definition mixer.h:338
bool * source_active
Array of active flags (true if source is active)
Definition mixer.h:340
rwlock_t source_lock
Reader-writer lock protecting source arrays and bitset.
Definition mixer.h:350
int num_sources
Current number of active audio sources.
Definition mixer.h:327
uint64_t active_sources_mask
Bitset of active sources (bit i = source i is active, O(1) iteration)
Definition mixer.h:343
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 SAFE_STRCPY(dest, dest_size, src)
Definition common.h:471
#define rwlock_wrunlock(lock)
Release a write lock (with debug tracking in debug builds)
Definition rwlock.h:349
#define rwlock_wrlock(lock)
Acquire a write lock (with debug tracking in debug builds)
Definition rwlock.h:313

References mixer_t::active_sources_mask, ASCIICHAT_OK, log_error, log_info, log_warn, mixer_t::max_sources, mixer_t::num_sources, rwlock_wrlock, rwlock_wrunlock, SAFE_FREE, SAFE_MALLOC, SAFE_STRCPY, mixer_t::source_active, mixer_t::source_buffers, mixer_t::source_ids, and mixer_t::source_lock.

Referenced by add_client(), and add_webrtc_client().

◆ 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 262 of file mixer.c.

262 {
263 // Validate parameters
264 if (max_sources <= 0 || max_sources > MIXER_MAX_SOURCES) {
265 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid max_sources: %d (must be 1-%d)", max_sources, MIXER_MAX_SOURCES);
266 return NULL;
267 }
268
269 if (sample_rate <= 0 || sample_rate > 192000) {
270 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid sample_rate: %d (must be 1-192000)", sample_rate);
271 return NULL;
272 }
273
274 mixer_t *mixer;
275 mixer = SAFE_MALLOC(sizeof(mixer_t), mixer_t *);
276 if (!mixer) {
277 SET_ERRNO(ERROR_MEMORY, "Failed to allocate mixer structure");
278 return NULL;
279 }
280
281 mixer->num_sources = 0;
282 mixer->max_sources = max_sources;
283 mixer->sample_rate = sample_rate;
284 memset(mixer->listener_buffers, 0, sizeof(mixer->listener_buffers));
285
286 // Allocate source management arrays with overflow checking
287 size_t buffers_size = 0;
288 if (checked_size_mul((size_t)max_sources, sizeof(audio_ring_buffer_t *), &buffers_size) != ASCIICHAT_OK) {
289 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Mixer source buffers array overflow: %d sources", max_sources);
290 SAFE_FREE(mixer);
291 return NULL;
292 }
293 mixer->source_buffers = SAFE_MALLOC(buffers_size, audio_ring_buffer_t **);
294 if (!mixer->source_buffers) {
295 SAFE_FREE(mixer);
296 return NULL;
297 }
298
299 size_t ids_size = 0;
300 if (checked_size_mul((size_t)max_sources, sizeof(const char *), &ids_size) != ASCIICHAT_OK) {
301 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Mixer source IDs array overflow: %d sources", max_sources);
303 SAFE_FREE(mixer);
304 return NULL;
305 }
306 mixer->source_ids = SAFE_MALLOC(ids_size, const char **);
307 if (!mixer->source_ids) {
309 SAFE_FREE(mixer);
310 return NULL;
311 }
312
313 size_t active_size = 0;
314 if (checked_size_mul((size_t)max_sources, sizeof(bool), &active_size) != ASCIICHAT_OK) {
315 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Mixer source active array overflow: %d sources", max_sources);
317 SAFE_FREE(mixer->source_ids);
318 SAFE_FREE(mixer);
319 return NULL;
320 }
321 mixer->source_active = SAFE_MALLOC(active_size, bool *);
322 if (!mixer->source_active) {
324 SAFE_FREE(mixer->source_ids);
325 SAFE_FREE(mixer);
326 return NULL;
327 }
328
329 // Initialize arrays
330 SAFE_MEMSET((void *)mixer->source_buffers, buffers_size, 0, buffers_size);
331 SAFE_MEMSET(mixer->source_ids, ids_size, 0, ids_size);
332 SAFE_MEMSET(mixer->source_active, active_size, 0, active_size);
333
334 // OPTIMIZATION 1: Initialize bitset optimization structures
335 mixer->active_sources_mask = 0ULL; // No sources active initially
336 for (int i = 0; i < 256; i++) {
338 }
339
340 // Allocate mix buffer BEFORE rwlock_init so cleanup path is correct
341 size_t mix_buffer_size = 0;
342 if (checked_size_mul(MIXER_FRAME_SIZE, sizeof(float), &mix_buffer_size) != ASCIICHAT_OK) {
343 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Mixer mix buffer size overflow: %zu samples", (size_t)MIXER_FRAME_SIZE);
345 SAFE_FREE(mixer->source_ids);
346 SAFE_FREE(mixer->source_active);
347 SAFE_FREE(mixer);
348 return NULL;
349 }
350 mixer->mix_buffer = SAFE_MALLOC(mix_buffer_size, float *);
351 if (!mixer->mix_buffer) {
352 SET_ERRNO(ERROR_MEMORY, "Failed to allocate mix buffer");
354 SAFE_FREE(mixer->source_ids);
355 SAFE_FREE(mixer->source_active);
356 SAFE_FREE(mixer);
357 return NULL;
358 }
359
360 // OPTIMIZATION 2: Initialize reader-writer lock
361 if (rwlock_init(&mixer->source_lock, "audio_mixer_sources") != 0) {
362 SET_ERRNO(ERROR_THREAD, "Failed to initialize mixer source lock");
364 SAFE_FREE(mixer->source_ids);
365 SAFE_FREE(mixer->source_active);
366 SAFE_FREE(mixer->mix_buffer);
367 SAFE_FREE(mixer);
368 return NULL;
369 }
370
371 // Set crowd scaling parameters
372 mixer->crowd_alpha = 0.5f; // Square root scaling
373 mixer->base_gain = 1.0f; // Unity gain - let compressor handle loudness, avoid clipping
374
375 // Initialize processing
376 if (ducking_init(&mixer->ducking, max_sources, (float)sample_rate) != ASCIICHAT_OK) {
379 SAFE_FREE(mixer->source_ids);
380 SAFE_FREE(mixer->source_active);
381 SAFE_FREE(mixer->mix_buffer);
382 SAFE_FREE(mixer);
383 return NULL;
384 }
385 compressor_init(&mixer->compressor, (float)sample_rate);
386
387 log_debug("Audio mixer created: max_sources=%d, sample_rate=%d", max_sources, sample_rate);
388
389 // Register mixer and its sync primitives for debugging
390 NAMED_REGISTER_MIXER(mixer, "audio_mixer", NULL);
391 NAMED_REGISTER_RWLOCK(&mixer->source_lock, "source_lock", (uintptr_t)(const void *)(mixer));
392
393 return mixer;
394}
uint8_t source_id_to_index[256]
Hash table mapping client_id → mixer source index (uses hash function for strings)
Definition mixer.h:345
float base_gain
Base gain before crowd scaling is applied.
Definition mixer.h:355
float crowd_alpha
Crowd scaling exponent (typically 0.5 for sqrt scaling)
Definition mixer.h:353
ducking_t ducking
Ducking system (active speaker detection and attenuation)
Definition mixer.h:358
void compressor_init(compressor_t *comp, float sample_rate)
Initialize a compressor.
Definition mixer.c:86
int ducking_init(ducking_t *duck, int num_sources, float sample_rate)
Initialize ducking system.
Definition mixer.c:155
#define MIXER_FRAME_SIZE
Number of samples processed per audio frame.
Definition mixer.h:114
float * mix_buffer
Temporary buffer for mixing operations (pre-allocated)
Definition mixer.h:363
compressor_t compressor
Compressor (dynamic range compression)
Definition mixer.h:360
#define MIXER_HASH_INVALID
Invalid index sentinel value for mixer hash table.
Definition mixer.h:464
int sample_rate
Sample rate in Hz (e.g., 44100)
Definition mixer.h:331
struct mixer_listener_buffer * listener_buffers[32][32]
Definition mixer.h:336
#define NAMED_REGISTER_RWLOCK(lock, name, parent_ptr)
Register an rwlock with automatic format specifier.
#define NAMED_REGISTER_MIXER(mixer, name, parent_ptr)
Register an audio mixer with automatic format specifier.
int rwlock_init(rwlock_t *rwlock, const char *name)
Initialize a read-write lock with a name.
Definition threading.c:65
int rwlock_destroy(rwlock_t *lock)
Destroy a read-write lock.
Main mixer structure for multi-source audio processing.
Definition mixer.h:325

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, mixer_t::listener_buffers, log_debug, mixer_t::max_sources, mixer_t::mix_buffer, MIXER_FRAME_SIZE, MIXER_HASH_INVALID, MIXER_MAX_SOURCES, NAMED_REGISTER_MIXER, NAMED_REGISTER_RWLOCK, 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.

◆ 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 396 of file mixer.c.

396 {
397 if (!mixer)
398 return;
399
400 NAMED_UNREGISTER(mixer);
401
402 // OPTIMIZATION 2: Destroy reader-writer lock
404
405 ducking_destroy(&mixer->ducking);
406
407 // Free all allocated client_id strings before freeing the array itself
408 for (int i = 0; i < mixer->max_sources; i++) {
409 for (int j = 0; j < mixer->max_sources; j++)
410 SAFE_FREE(mixer->listener_buffers[i][j]);
411 if (mixer->source_ids[i] != NULL) {
412 SAFE_FREE(mixer->source_ids[i]);
413 }
414 }
415
417 SAFE_FREE(mixer->source_ids);
418 SAFE_FREE(mixer->source_active);
419 SAFE_FREE(mixer->mix_buffer);
420 SAFE_FREE(mixer);
421 log_debug("Audio mixer destroyed");
422}
void ducking_destroy(ducking_t *duck)
Free ducking system resources.
Definition mixer.c:210

References mixer_t::ducking, ducking_destroy(), mixer_t::listener_buffers, log_debug, mixer_t::max_sources, mixer_t::mix_buffer, NAMED_UNREGISTER, rwlock_destroy(), SAFE_FREE, mixer_t::source_active, mixer_t::source_buffers, mixer_t::source_ids, and mixer_t::source_lock.

◆ 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 542 of file mixer.c.

542 {
543 if (!mixer || !output || num_samples <= 0)
544 return -1;
545
546 // THREAD SAFETY: Acquire read lock to protect against concurrent source add/remove
547 // This prevents race conditions where source_buffers[i] could be set to NULL while we read it
548 rwlock_rdlock(&mixer->source_lock);
549
550 // Clear output buffer
551 SAFE_MEMSET(output, num_samples * sizeof(float), 0, num_samples * sizeof(float));
552
553 // Count active sources
554 int active_count = 0;
555 for (int i = 0; i < mixer->max_sources; i++) {
556 if (mixer->source_ids[i] != 0 && mixer->source_active[i] && mixer->source_buffers[i]) {
557 active_count++;
558 }
559 }
560
561 if (active_count == 0) {
562 // No active sources, output silence
563 return 0;
564 }
565
566 // Process in frames for efficiency
567 for (int frame_start = 0; frame_start < num_samples; frame_start += MIXER_FRAME_SIZE) {
568 int frame_size = (frame_start + MIXER_FRAME_SIZE > num_samples) ? (num_samples - frame_start) : MIXER_FRAME_SIZE;
569
570 // Clear mix buffer
571 SAFE_MEMSET(mixer->mix_buffer, frame_size * sizeof(float), 0, frame_size * sizeof(float));
572
573 // Temporary buffers for source audio
574 float source_samples[MIXER_MAX_SOURCES][MIXER_FRAME_SIZE];
575 int source_count = 0;
576 int source_map[MIXER_MAX_SOURCES]; // Maps source index to slot
577
578 // Read from each active source
579 for (int i = 0; i < mixer->max_sources; i++) {
580 if (mixer->source_ids[i] != 0 && mixer->source_active[i] && mixer->source_buffers[i]) {
581 // Read samples from this source's ring buffer
582 size_t samples_read_size =
583 audio_ring_buffer_read(mixer->source_buffers[i], source_samples[source_count], frame_size);
584 int samples_read = (int)samples_read_size;
585
586 // Accept partial frames - pad with silence if needed
587 // This prevents audio dropouts when ring buffers are temporarily under-filled
588 if (samples_read > 0) {
589 // Pad remaining samples with silence if we got a partial frame
590 if (samples_read < frame_size) {
591 SAFE_MEMSET(&source_samples[source_count][samples_read], (frame_size - samples_read) * sizeof(float), 0,
592 (frame_size - samples_read) * sizeof(float));
593 }
594
595 source_map[source_count] = i;
596 source_count++;
597
598 if (source_count >= MIXER_MAX_SOURCES)
599 break;
600 }
601 }
602 }
603
604 // OPTIMIZATION: Batch envelope calculation per-frame instead of per-sample
605 // Calculate peak amplitude for each source over the entire frame
606 int speaking_count = 0;
607
608 for (int i = 0; i < source_count; i++) {
609 int slot = source_map[i];
610 float peak = 0.0f;
611
612 // Find peak amplitude in frame (much faster than per-sample envelope)
613 for (int s = 0; s < frame_size; s++) {
614 float abs_sample = fabsf(source_samples[i][s]);
615 if (abs_sample > peak)
616 peak = abs_sample;
617 }
618
619 // Update envelope using frame peak (one update per frame instead of per-sample)
620 if (peak > mixer->ducking.envelope[slot]) {
621 mixer->ducking.envelope[slot] =
622 mixer->ducking.attack_coeff * mixer->ducking.envelope[slot] + (1.0f - mixer->ducking.attack_coeff) * peak;
623 } else {
624 mixer->ducking.envelope[slot] =
625 mixer->ducking.release_coeff * mixer->ducking.envelope[slot] + (1.0f - mixer->ducking.release_coeff) * peak;
626 }
627
628 // Count speaking sources
629 if (mixer->ducking.envelope[slot] > db_to_linear(-60.0f))
630 speaking_count++;
631 }
632
633 // Apply ducking ONCE per frame (not per-sample)
634 ducking_process_frame(&mixer->ducking, mixer->ducking.envelope, mixer->ducking.gain, mixer->max_sources);
635
636 // Calculate crowd scaling ONCE per frame
637 float crowd_gain = (speaking_count > 0) ? (1.0f / powf((float)speaking_count, mixer->crowd_alpha)) : 1.0f;
638 float pre_bus = mixer->base_gain * crowd_gain;
639
640 // Pre-calculate combined gains for each source (ducking * pre_bus)
641 float combined_gains[MIXER_MAX_SOURCES];
642 for (int i = 0; i < source_count; i++) {
643 int slot = source_map[i];
644 combined_gains[i] = mixer->ducking.gain[slot] * pre_bus;
645 }
646
647 // OPTIMIZATION: Fast mixing loop - simple multiply-add with pre-calculated gains
648 // NO per-sample compressor to avoid expensive log/pow calls (480x per iteration)
649 for (int s = 0; s < frame_size; s++) {
650 float mix = 0.0f;
651 for (int i = 0; i < source_count; i++) {
652 mix += source_samples[i][s] * combined_gains[i];
653 }
654
655 // Store in mix buffer for frame-level compression below
656 mixer->mix_buffer[s] = mix;
657 }
658
659 // OPTIMIZATION: Apply compression ONCE per frame instead of per-sample
660 // This reduces expensive log10f/powf calls from 480x to 1x per iteration
661 // Calculate frame peak for compressor sidechain
662 float frame_peak = 0.0f;
663 for (int s = 0; s < frame_size; s++) {
664 float abs_val = fabsf(mixer->mix_buffer[s]);
665 if (abs_val > frame_peak)
666 frame_peak = abs_val;
667 }
668
669 // Process compressor with frame peak (1 call instead of 480)
670 float comp_gain = compressor_process_sample(&mixer->compressor, frame_peak);
671
672 // Apply compression gain and soft clipping to all samples
673 for (int s = 0; s < frame_size; s++) {
674 float mix = mixer->mix_buffer[s] * comp_gain;
675
676 // Soft clip at 0.7 to provide 3dB headroom and prevent hard clipping
677 // With +6dB makeup gain, peaks can exceed 1.0, so we need headroom
678 // threshold=0.7 maps asymptotically toward 1.0, preventing harsh distortion
679 output[frame_start + s] = soft_clip(mix, 0.7f, 3.0f);
680 }
681 }
682
684 return num_samples;
685}
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:228
float soft_clip(float sample, float threshold, float steepness)
Apply soft clipping to a sample.
Definition mixer.c:1109
float compressor_process_sample(compressor_t *comp, float sidechain)
Process a single sample through compressor.
Definition mixer.c:131
#define rwlock_rdlock(lock)
Acquire a read lock (with debug tracking in debug builds)
Definition rwlock.h:294
#define rwlock_rdunlock(lock)
Release a read lock (with debug tracking in debug builds)
Definition rwlock.h:331

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 687 of file mixer.c.

687 {
688 if (!mixer || !output || num_samples <= 0) {
689 return -1;
690 }
691
692 // Only use timing in debug builds - snprintf + hashtable ops are expensive in hot path
693#ifndef NDEBUG
694 START_TIMER("mixer_total");
695#endif
696
697 // Serialize sample distribution and mutable ducking/compressor state.
698 rwlock_wrlock(&mixer->source_lock);
699
700 // Clear output buffer
701 SAFE_MEMSET(output, num_samples * sizeof(float), 0, num_samples * sizeof(float));
702
703 // Source IDs are strings; match the complete ID hash used by the caller.
704 uint8_t exclude_index = MIXER_HASH_INVALID;
705 for (int i = 0; i < mixer->max_sources; i++) {
706 if (mixer->source_ids[i] && fnv1a_hash_string(mixer->source_ids[i]) == exclude_client_id) {
707 exclude_index = (uint8_t)i;
708 break;
709 }
710 }
711 if (exclude_index != MIXER_HASH_INVALID)
712 mixer_distribute_samples(mixer);
713 uint64_t active_mask = mixer->active_sources_mask;
714
715 // Validate exclude_index before using in bitshift
716 // - exclude_index == MIXER_HASH_INVALID means "not found" (sentinel value from initialization)
717 // - exclude_index >= MIXER_MAX_SOURCES would cause undefined behavior in bitshift
718 bool valid_exclude = (exclude_index < MIXER_MAX_SOURCES && exclude_index != MIXER_HASH_INVALID);
719
720 if (valid_exclude) {
721 // Apply exclusion to prevent echo feedback
722 uint64_t mask_without_excluded = active_mask & ~(1ULL << exclude_index);
723 active_mask = mask_without_excluded;
724
725 // DIAGNOSTIC: Log which client was excluded and which remain active
727 4500 * US_PER_MS_INT,
728 "MIXER EXCLUSION: exclude_client=%u, exclude_index=%u, active_mask_before=0x%llx, active_mask_after=0x%llx",
729 exclude_client_id, exclude_index, (unsigned long long)(active_mask | (1ULL << exclude_index)),
730 (unsigned long long)active_mask);
731 } else {
732 // DIAGNOSTIC: Failed to exclude - log why
734 "MIXER EXCLUSION FAILED: exclude_client=%u, exclude_index=%u (valid=%d), lookup_id=%u",
735 exclude_client_id, exclude_index, valid_exclude,
736 (exclude_index < MIXER_MAX_SOURCES && exclude_index != 0xFF) ? mixer->source_ids[exclude_index] : 0);
737 }
738
739 // Fast check: any sources to mix?
740 if (active_mask == 0) {
742#ifndef NDEBUG
743 STOP_TIMER("mixer_total");
744#endif
745 return 0; // No sources to mix (excluding the specified client), output silence
746 }
747
748 // Process in frames for efficiency
749 for (int frame_start = 0; frame_start < num_samples; frame_start += MIXER_FRAME_SIZE) {
750 int frame_size = (frame_start + MIXER_FRAME_SIZE > num_samples) ? (num_samples - frame_start) : MIXER_FRAME_SIZE;
751
752 uint64_t read_start_ns = time_get_ns();
753
754 // Clear mix buffer
755 SAFE_MEMSET(mixer->mix_buffer, frame_size * sizeof(float), 0, frame_size * sizeof(float));
756
757 // Temporary buffers for source audio
758 float source_samples[MIXER_MAX_SOURCES][MIXER_FRAME_SIZE];
759 int source_count = 0;
760 int source_map[MIXER_MAX_SOURCES]; // Maps source index to slot
761
762 // OPTIMIZATION 1: Iterate only over active sources using bitset
763 uint64_t current_mask = active_mask;
764 while (current_mask && source_count < MIXER_MAX_SOURCES) {
765 int i = find_first_set_bit(current_mask); // Portable: find first set bit
766 current_mask &= current_mask - 1; // Clear lowest set bit
767
768 // Verify source is valid (defensive programming)
769 if (i < mixer->max_sources && mixer->source_ids[i] != 0 && mixer->source_buffers[i]) {
770 // Read samples from this source's ring buffer
771 size_t samples_read_size = 0;
772 if (valid_exclude) {
773 mixer_listener_buffer_t *queue = mixer->listener_buffers[exclude_index][i];
774 if (queue && queue->count >= (size_t)frame_size) {
775 samples_read_size = (size_t)frame_size;
776 for (int s = 0; s < frame_size; s++)
777 source_samples[source_count][s] = queue->samples[(queue->read + s) % MIXER_LISTENER_SAMPLES];
778 queue->read = (queue->read + samples_read_size) % MIXER_LISTENER_SAMPLES;
779 queue->count -= samples_read_size;
780 }
781 } else {
782 samples_read_size =
783 audio_ring_buffer_read(mixer->source_buffers[i], source_samples[source_count], frame_size);
784 }
785 int samples_read = (int)samples_read_size;
786
787 // Accept partial frames - pad with silence if needed
788 // This prevents audio dropouts when ring buffers are temporarily under-filled
789 if (samples_read > 0) {
790 // Pad remaining samples with silence if we got a partial frame
791 if (samples_read < frame_size) {
792 SAFE_MEMSET(&source_samples[source_count][samples_read], (frame_size - samples_read) * sizeof(float), 0,
793 (frame_size - samples_read) * sizeof(float));
794 }
795
796 // DIAGNOSTIC: Calculate RMS of this source's audio
797 float source_rms = 0.0f;
798 if (frame_start == 0) { // Only log for first frame to reduce spam
799 float sum_squares = 0.0f;
800 for (int s = 0; s < samples_read; s++) {
801 sum_squares += source_samples[source_count][s] * source_samples[source_count][s];
802 }
803 source_rms = sqrtf(sum_squares / (float)samples_read);
804 log_dev_every(NS_PER_MS_INT, "MIXER SOURCE READ: client_id=%u, slot=%d, samples_read=%d, RMS=%.6f",
805 mixer->source_ids[i], i, samples_read, source_rms);
806 }
807
808 source_map[source_count] = i;
809 source_count++;
810 }
811 }
812 }
813
814 uint64_t read_end_ns = time_get_ns();
815 uint64_t read_time_ns = time_elapsed_ns(read_start_ns, read_end_ns);
816
817 if (read_time_ns > 10 * NS_PER_MS_INT) {
818 char read_time_str[32];
819 time_pretty(read_time_ns, -1, read_time_str, sizeof(read_time_str));
820 log_warn_every(LOG_RATE_DEFAULT, "Mixer: Slow source reading took %s for %d sources", read_time_str,
821 source_count);
822 }
823
824 if (GET_OPTION(no_audio_mixer)) {
825 for (int sample = 0; sample < frame_size; sample++) {
826 for (int source = 0; source < source_count; source++)
827 output[frame_start + sample] += source_samples[source][sample];
828 }
829 continue;
830 }
831
832 // OPTIMIZATION: Batch envelope calculation per-frame instead of per-sample
833 // Calculate peak amplitude for each source over the entire frame
834 int speaking_count = 0;
835
836 for (int i = 0; i < source_count; i++) {
837 int slot = source_map[i];
838 float peak = 0.0f;
839
840 // Find peak amplitude in frame (much faster than per-sample envelope)
841 for (int s = 0; s < frame_size; s++) {
842 float abs_sample = fabsf(source_samples[i][s]);
843 if (abs_sample > peak)
844 peak = abs_sample;
845 }
846
847 // Update envelope using frame peak (one update per frame instead of per-sample)
848 if (peak > mixer->ducking.envelope[slot]) {
849 mixer->ducking.envelope[slot] =
850 mixer->ducking.attack_coeff * mixer->ducking.envelope[slot] + (1.0f - mixer->ducking.attack_coeff) * peak;
851 } else {
852 mixer->ducking.envelope[slot] =
853 mixer->ducking.release_coeff * mixer->ducking.envelope[slot] + (1.0f - mixer->ducking.release_coeff) * peak;
854 }
855
856 // Count speaking sources
857 if (mixer->ducking.envelope[slot] > db_to_linear(-60.0f))
858 speaking_count++;
859 }
860
861 // Apply ducking ONCE per frame (not per-sample)
862 ducking_process_frame(&mixer->ducking, mixer->ducking.envelope, mixer->ducking.gain, mixer->max_sources);
863
864 // Calculate crowd scaling ONCE per frame
865 float crowd_gain = (speaking_count > 0) ? (1.0f / powf((float)speaking_count, mixer->crowd_alpha)) : 1.0f;
866 float pre_bus = mixer->base_gain * crowd_gain;
867
868 // Pre-calculate combined gains for each source (ducking * pre_bus)
869 float combined_gains[MIXER_MAX_SOURCES];
870 for (int i = 0; i < source_count; i++) {
871 int slot = source_map[i];
872 combined_gains[i] = mixer->ducking.gain[slot] * pre_bus;
873 }
874
875 // OPTIMIZATION: Fast mixing loop - simple multiply-add with pre-calculated gains
876 // NO per-sample compressor to avoid expensive log/pow calls (480x per iteration)
877 for (int s = 0; s < frame_size; s++) {
878 float mix = 0.0f;
879 for (int i = 0; i < source_count; i++) {
880 mix += source_samples[i][s] * combined_gains[i];
881 }
882
883 // Store in mix buffer for frame-level compression below
884 mixer->mix_buffer[s] = mix;
885 }
886
887 // OPTIMIZATION: Apply compression ONCE per frame instead of per-sample
888 // This reduces expensive log10f/powf calls from 480x to 1x per iteration
889 // Calculate frame peak for compressor sidechain
890 float frame_peak = 0.0f;
891 for (int s = 0; s < frame_size; s++) {
892 float abs_val = fabsf(mixer->mix_buffer[s]);
893 if (abs_val > frame_peak)
894 frame_peak = abs_val;
895 }
896
897 // Process compressor with frame peak (1 call instead of 480)
898 float comp_gain = compressor_process_sample(&mixer->compressor, frame_peak);
899
900 // Apply compression gain and soft clipping to all samples
901 for (int s = 0; s < frame_size; s++) {
902 float mix = mixer->mix_buffer[s] * comp_gain;
903
904 // Soft clip at 0.7 to provide 3dB headroom and prevent hard clipping
905 // With +6dB makeup gain, peaks can exceed 1.0, so we need headroom
906 // threshold=0.7 maps asymptotically toward 1.0, preventing harsh distortion
907 output[frame_start + s] = soft_clip(mix, 0.7f, 3.0f);
908 }
909 }
910
912
913#ifndef NDEBUG
914 STOP_TIMER_AND_LOG_EVERY(warn, NS_PER_SEC_INT, 2 * NS_PER_MS_INT, "mixer_total", "Mixer took");
915#endif
916
917 return num_samples;
918}
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 STOP_TIMER_AND_LOG_EVERY(log_level, interval_ns, threshold_ns, timer_name, msg_fmt,...)
Stop a timer and log the result with rate limiting.
Definition time.h:647
#define NS_PER_SEC_INT
Definition time.h:157
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424
#define START_TIMER(name_fmt,...)
Start a timer with formatted name.
Definition time.h:333
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition util/time.c:150
#define US_PER_MS_INT
Definition time.h:160
#define STOP_TIMER(name_fmt,...)
Stop a timer with formatted name and return elapsed time.
Definition time.h:403
#define MIXER_LISTENER_SAMPLES
Definition mixer.c:21
float samples[8192]
Definition mixer.c:23

References mixer_t::active_sources_mask, ducking_t::attack_coeff, audio_ring_buffer_read(), mixer_t::base_gain, mixer_t::compressor, compressor_process_sample(), mixer_listener_buffer::count, mixer_t::crowd_alpha, db_to_linear(), mixer_t::ducking, ducking_process_frame(), ducking_t::envelope, ducking_t::gain, GET_OPTION, mixer_t::listener_buffers, log_dev_every, LOG_RATE_DEFAULT, log_warn_every, mixer_t::max_sources, mixer_t::mix_buffer, MIXER_FRAME_SIZE, MIXER_HASH_INVALID, MIXER_LISTENER_SAMPLES, MIXER_MAX_SOURCES, NS_PER_MS_INT, NS_PER_SEC_INT, mixer_listener_buffer::read, ducking_t::release_coeff, rwlock_wrlock, rwlock_wrunlock, SAFE_MEMSET, mixer_listener_buffer::samples, soft_clip(), mixer_t::source_buffers, mixer_t::source_ids, mixer_t::source_lock, START_TIMER, STOP_TIMER, STOP_TIMER_AND_LOG_EVERY, time_elapsed_ns(), time_get_ns(), time_pretty(), and US_PER_MS_INT.

Referenced by client_audio_render_thread().

◆ mixer_remove_source()

void mixer_remove_source ( mixer_t *  mixer,
const char *  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 477 of file mixer.c.

477 {
478 if (!mixer)
479 return;
480
481 // OPTIMIZATION 2: Acquire write lock for source modification
482 rwlock_wrlock(&mixer->source_lock);
483
484 for (int i = 0; i < mixer->max_sources; i++) {
485 if (mixer->source_ids[i] != NULL && strcmp(mixer->source_ids[i], client_id) == 0) {
486 for (int j = 0; j < mixer->max_sources; j++) {
487 SAFE_FREE(mixer->listener_buffers[i][j]);
488 if (j != i)
489 SAFE_FREE(mixer->listener_buffers[j][i]);
490 }
491 mixer->source_buffers[i] = NULL;
492 // Free the allocated client_id string
493 SAFE_FREE(mixer->source_ids[i]);
494 mixer->source_active[i] = false;
495 mixer->num_sources--;
496
497 // OPTIMIZATION 1: Update bitset optimization structures
498 mixer->active_sources_mask &= ~(1ULL << i); // Clear bit for this slot
499 // Hash table optimization disabled for string-based client_ids
500
501 // Reset ducking state for this source
502 mixer->ducking.envelope[i] = 0.0f;
503 mixer->ducking.gain[i] = 1.0f;
504
506
507 log_info("Mixer: Removed source for client %s from slot %d", client_id, i);
508 return;
509 }
510 }
511
513}

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

Referenced by remove_client().

◆ mixer_set_source_active()

void mixer_set_source_active ( mixer_t *  mixer,
const char *  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 515 of file mixer.c.

515 {
516 if (!mixer)
517 return;
518
519 // OPTIMIZATION 2: Acquire write lock for source modification
520 rwlock_wrlock(&mixer->source_lock);
521
522 for (int i = 0; i < mixer->max_sources; i++) {
523 if (mixer->source_ids[i] != NULL && strcmp(mixer->source_ids[i], client_id) == 0) {
524 mixer->source_active[i] = active;
525
526 // OPTIMIZATION 1: Update bitset for active state change
527 if (active) {
528 mixer->active_sources_mask |= (1ULL << i); // Set bit
529 } else {
530 mixer->active_sources_mask &= ~(1ULL << i); // Clear bit
531 }
532
534 log_debug("Mixer: Set source %s active=%d", client_id, active);
535 return;
536 }
537 }
538
540}

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 925 of file mixer.c.

925 {
926 if (!gate)
927 return;
928
929 gate->sample_rate = sample_rate;
930 gate->envelope = 0.0f;
931 gate->gate_open = false;
932
933 // Default parameters
934 // Slower attack (10ms) prevents clicking artifacts during gate transitions
935 // threshold=0.01, attack=10ms, release=50ms, hysteresis=0.9
936 noise_gate_set_params(gate, 0.01f, 10 * NS_PER_MS_INT, 50 * NS_PER_MS_INT, 0.9f);
937}
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, uint64_t attack_ns, uint64_t release_ns, float hysteresis)
Set noise gate parameters.
Definition mixer.c:939

References noise_gate_t::envelope, noise_gate_t::gate_open, noise_gate_set_params(), NS_PER_MS_INT, 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 1001 of file mixer.c.

1001 {
1002 return gate ? gate->gate_open : false;
1003}

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 982 of file mixer.c.

982 {
983 if (!gate || !buffer || num_samples <= 0)
984 return;
985
986 // First pass: find peak amplitude
987 float peak = 0.0f;
988 for (int i = 0; i < num_samples; i++) {
989 float abs_sample = fabsf(buffer[i]);
990 if (abs_sample > peak) {
991 peak = abs_sample;
992 }
993 }
994
995 // Second pass: apply gate
996 for (int i = 0; i < num_samples; i++) {
997 buffer[i] = noise_gate_process_sample(gate, buffer[i], peak);
998 }
999}
float noise_gate_process_sample(noise_gate_t *gate, float input, float peak_amplitude)
Process a single sample through noise gate.
Definition mixer.c:957

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 957 of file mixer.c.

957 {
958 if (!gate)
959 return input;
960
961 // Determine target state with hysteresis
962 float target;
963 if (gate->gate_open) {
964 // Gate is open - use lower threshold (hysteresis) to close
965 target = (peak_amplitude > gate->threshold * gate->hysteresis) ? 1.0f : 0.0f;
966 } else {
967 // Gate is closed - use normal threshold to open
968 target = (peak_amplitude > gate->threshold) ? 1.0f : 0.0f;
969 }
970
971 // Update gate state
972 gate->gate_open = (target > 0.5f);
973
974 // Update envelope with appropriate coefficient
975 float coeff = (target > gate->envelope) ? gate->attack_coeff : gate->release_coeff;
976 gate->envelope += coeff * (target - gate->envelope);
977
978 // Apply gate
979 return input * gate->envelope;
980}
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,
uint64_t  attack_ns,
uint64_t  release_ns,
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 939 of file mixer.c.

940 {
941 if (!gate)
942 return;
943
944 gate->threshold = threshold;
945 gate->attack_ns = attack_ns;
946 gate->release_ns = release_ns;
947 gate->hysteresis = hysteresis;
948
949 // Calculate coefficients for envelope follower
950 // Using exponential moving average: coeff = 1 - exp(-1 / (time_s * sample_rate))
951 float attack_s = (float)attack_ns / NS_PER_SEC;
952 float release_s = (float)release_ns / NS_PER_SEC;
953 gate->attack_coeff = 1.0f - expf(-1.0f / (attack_s * gate->sample_rate + 1e-12f));
954 gate->release_coeff = 1.0f - expf(-1.0f / (release_s * gate->sample_rate + 1e-12f));
955}
float release_coeff
Release coefficient (converted from release_ms)
Definition mixer.h:197
uint64_t attack_ns
Attack time in nanoseconds (how fast gate opens)
Definition mixer.h:184
uint64_t release_ns
Release time in nanoseconds (how fast gate closes)
Definition mixer.h:186

References noise_gate_t::attack_coeff, noise_gate_t::attack_ns, noise_gate_t::hysteresis, NS_PER_SEC, noise_gate_t::release_coeff, noise_gate_t::release_ns, 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.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 74 of file opus.c.

74 {
75 if (sample_rate <= 0) {
76 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid sample rate: %d", sample_rate);
77 return NULL;
78 }
79
81 if (!codec) {
82 SET_ERRNO(ERROR_MEMORY, "Failed to allocate opus codec context");
83 return NULL;
84 }
85
86 // Create decoder
87 int error = OPUS_OK;
88 codec->decoder = opus_decoder_create(sample_rate, 1, &error);
89 if (error != OPUS_OK || !codec->decoder) {
90 SET_ERRNO(ERROR_AUDIO, "Failed to create Opus decoder: %s", opus_strerror(error));
91 SAFE_FREE(codec);
92 return NULL;
93 }
94
95 codec->encoder = NULL;
96 codec->sample_rate = sample_rate;
97 codec->bitrate = 0; // N/A for decoder
98 codec->tmp_buffer = NULL;
99
100 log_debug("Opus decoder created: sample_rate=%d", sample_rate);
101
102 // Register Opus decoder for debugging
103 NAMED_REGISTER_CONTEXT(codec, "opus_decoder", "opus_codec", NULL);
104
105 return codec;
106}
#define NAMED_REGISTER_CONTEXT(context, context_type, name, parent_ptr)
Register a generic context with automatic format specifier.
uint8_t * tmp_buffer
Temporary buffer for internal use.
Definition opus.h:100
int bitrate
Bitrate in bits per second (encoder only)
Definition opus.h:99
int sample_rate
Sample rate in Hz (e.g., 44100)
Definition opus.h:98
OpusEncoder * encoder
Encoder state (NULL if decoder)
Definition opus.h:96
OpusDecoder * decoder
Decoder state (NULL if encoder)
Definition opus.h:97

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

Referenced by handle_stream_start_packet(), and session_host_start_render().

◆ opus_codec_create_encoder()

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

#include <opus.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 19 of file opus.c.

19 {
20 if (sample_rate <= 0 || bitrate <= 0) {
21 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid codec parameters: sample_rate=%d, bitrate=%d", sample_rate, bitrate);
22 return NULL;
23 }
24
26 if (!codec) {
27 SET_ERRNO(ERROR_MEMORY, "Failed to allocate opus codec context");
28 return NULL;
29 }
30
31 // Create encoder
32 int error = OPUS_OK;
33 codec->encoder = opus_encoder_create(sample_rate, 1, (int)application, &error);
34 if (error != OPUS_OK || !codec->encoder) {
35 SET_ERRNO(ERROR_AUDIO, "Failed to create Opus encoder: %s", opus_strerror(error));
36 SAFE_FREE(codec);
37 return NULL;
38 }
39
40 // Set bitrate
41 error = opus_encoder_ctl(codec->encoder, OPUS_SET_BITRATE(bitrate));
42 if (error != OPUS_OK) {
43 SET_ERRNO(ERROR_AUDIO, "Failed to set Opus bitrate: %s", opus_strerror(error));
44 opus_encoder_destroy(codec->encoder);
45 SAFE_FREE(codec);
46 return NULL;
47 }
48
49 // Enable DTX (Discontinuous Transmission) for efficient silence frame encoding
50 // DTX allows the encoder to produce very small packets (3-4 bytes) for silence,
51 // reducing bandwidth usage while maintaining audio quality for voice/music
52 error = opus_encoder_ctl(codec->encoder, OPUS_SET_DTX(1));
53 if (error != OPUS_OK) {
54 log_warn("Failed to enable Opus DTX: %s (continuing without DTX)", opus_strerror(error));
55 }
56
57 codec->decoder = NULL;
58 codec->sample_rate = sample_rate;
59 codec->bitrate = bitrate;
60 codec->tmp_buffer = NULL;
61
62 log_debug("Opus encoder created: sample_rate=%d, bitrate=%d bps, DTX enabled", sample_rate, bitrate);
63
64 // Register Opus encoder for debugging
65 NAMED_REGISTER_CONTEXT(codec, "opus_encoder", "opus_codec", NULL);
66
67 return codec;
68}

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

Referenced by client_audio_render_thread(), session_host_start_render(), and session_participant_start_audio_capture().

◆ 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.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 143 of file opus.c.

144 {
145 if (!codec || !codec->decoder || !out_samples || out_num_samples <= 0) {
147 "Invalid decode parameters: codec=%p, decoder=%p, out_samples=%p, out_num_samples=%d", (void *)codec,
148 (void *)(codec ? codec->decoder : NULL), (void *)out_samples, out_num_samples);
149 return -1;
150 }
151
152 // If data is NULL, use PLC (Packet Loss Concealment)
153 if (!data || data_len == 0) {
154 log_debug_every(LOG_RATE_VERY_FAST, "Opus PLC (Packet Loss Concealment)");
155 int samples = opus_decode_float(codec->decoder, NULL, 0, out_samples, out_num_samples, 0);
156 if (samples < 0) {
157 SET_ERRNO(ERROR_AUDIO, "Opus PLC failed: %s", opus_strerror(samples));
158 return -1;
159 }
160 return samples;
161 }
162
163 // Decode frame
164 int samples = opus_decode_float(codec->decoder, data, (opus_int32)data_len, out_samples, out_num_samples, 0);
165
166 if (samples < 0) {
167 SET_ERRNO(ERROR_AUDIO, "Opus decoding failed: %s", opus_strerror(samples));
168 return -1;
169 }
170
171 return samples;
172}
#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.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 230 of file opus.c.

230 {
231 if (!codec) {
232 return;
233 }
234
235 NAMED_UNREGISTER(codec);
236
237 if (codec->encoder) {
238 opus_encoder_destroy(codec->encoder);
239 codec->encoder = NULL;
240 }
241
242 if (codec->decoder) {
243 opus_decoder_destroy(codec->decoder);
244 codec->decoder = NULL;
245 }
246
247 if (codec->tmp_buffer) {
248 SAFE_FREE(codec->tmp_buffer);
249 codec->tmp_buffer = NULL;
250 }
251
252 SAFE_FREE(codec);
253}

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

Referenced by cleanup_client_media_buffers(), client_audio_render_thread(), session_host_destroy(), session_host_start_render(), session_host_stop_render(), and session_participant_destroy().

◆ 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.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 112 of file opus.c.

113 {
114 if (!codec || !codec->encoder || !samples || num_samples <= 0 || !out_data || out_size == 0) {
116 "Invalid encode parameters: codec=%p, encoder=%p, samples=%p, num_samples=%d, out_data=%p, "
117 "out_size=%zu",
118 (void *)codec, (void *)(codec ? codec->encoder : NULL), (const void *)samples, num_samples,
119 (void *)out_data, out_size);
120 return 0;
121 }
122
123 // Encode frame
124 opus_int32 encoded_bytes = opus_encode_float(codec->encoder, samples, num_samples, out_data, (opus_int32)out_size);
125
126 if (encoded_bytes < 0) {
127 SET_ERRNO(ERROR_AUDIO, "Opus encoding failed: %s", opus_strerror((int)encoded_bytes));
128 return 0;
129 }
130
131 if (encoded_bytes == 0) {
132 // DTX frame (silence) - encoder produced zero bytes
133 log_debug_every(LOG_RATE_VERY_FAST, "Opus DTX frame (silence detected)");
134 }
135
136 return (size_t)encoded_bytes;
137}

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

Get current encoder bitrate.

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

Definition at line 195 of file opus.c.

195 {
196 if (!codec || !codec->encoder) {
197 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid codec for bitrate query");
198 return -1;
199 }
200
201 opus_int32 bitrate = 0;
202 int error = opus_encoder_ctl(codec->encoder, OPUS_GET_BITRATE(&bitrate));
203 if (error != OPUS_OK) {
204 SET_ERRNO(ERROR_AUDIO, "Failed to get Opus bitrate: %s", opus_strerror(error));
205 return -1;
206 }
207
208 return (int)bitrate;
209}

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.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 178 of file opus.c.

178 {
179 if (!codec || !codec->encoder || bitrate <= 0) {
180 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid bitrate parameters: codec=%p, encoder=%p, bitrate=%d", (void *)codec,
181 (void *)(codec ? codec->encoder : NULL), bitrate);
182 }
183
184 int error = opus_encoder_ctl(codec->encoder, OPUS_SET_BITRATE(bitrate));
185 if (error != OPUS_OK) {
186 return SET_ERRNO(ERROR_AUDIO, "Failed to set Opus bitrate: %s", opus_strerror(error));
187 }
188
189 codec->bitrate = bitrate;
190 log_debug("Opus bitrate changed to %d bps", bitrate);
191
192 return ASCIICHAT_OK;
193}

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.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 211 of file opus.c.

211 {
212 if (!codec || !codec->encoder) {
213 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid codec for DTX configuration");
214 }
215
216 int error = opus_encoder_ctl(codec->encoder, OPUS_SET_DTX(enable ? 1 : 0));
217 if (error != OPUS_OK) {
218 return SET_ERRNO(ERROR_AUDIO, "Failed to set Opus DTX: %s", opus_strerror(error));
219 }
220
221 log_debug("Opus DTX %s", enable ? "enabled" : "disabled");
222
223 return ASCIICHAT_OK;
224}

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 629 of file lib/audio/audio.c.

630 {
631 if (src_samples == 0 || dst_samples == 0) {
632 SAFE_MEMSET(dst, dst_samples * sizeof(float), 0, dst_samples * sizeof(float));
633 return;
634 }
635
636 double ratio = src_rate / dst_rate;
637
638 for (size_t i = 0; i < dst_samples; i++) {
639 double src_pos = (double)i * ratio;
640 size_t idx0 = (size_t)src_pos;
641 size_t idx1 = idx0 + 1;
642 double frac = src_pos - (double)idx0;
643
644 // Clamp indices to valid range
645 if (idx0 >= src_samples)
646 idx0 = src_samples - 1;
647 if (idx1 >= src_samples)
648 idx1 = src_samples - 1;
649
650 // Linear interpolation
651 dst[i] = (float)((1.0 - frac) * src[idx0] + frac * src[idx1]);
652 }
653}

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 1136 of file mixer.c.

1136 {
1137 if (t <= 0.0f)
1138 return 0.0f;
1139 if (t >= 1.0f)
1140 return 1.0f;
1141 return t * t * (3.0f - 2.0f * t);
1142}

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 1109 of file mixer.c.

1109 {
1110 if (sample > threshold) {
1111 // Soft clip positive values using tanh curve
1112 // Maps samples above threshold asymptotically toward 1.0
1113 return threshold + (1.0f - threshold) * tanhf((sample - threshold) * steepness);
1114 }
1115 if (sample < -threshold) {
1116 // Soft clip negative values symmetrically
1117 return -threshold + (-1.0f + threshold) * tanhf((sample + threshold) * steepness);
1118 }
1119 return sample;
1120}

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 1122 of file mixer.c.

1122 {
1123 if (!buffer || num_samples <= 0)
1124 return;
1125
1126 for (int i = 0; i < num_samples; i++) {
1127 buffer[i] = soft_clip(buffer[i], threshold, steepness);
1128 }
1129}

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 343 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_ns [1/3]

uint64_t compressor_t::attack_ns

Attack time in nanoseconds (how fast compression kicks in)

Definition at line 145 of file mixer.h.

Referenced by compressor_set_params().

◆ attack_ns [2/3]

uint64_t noise_gate_t::attack_ns

Attack time in nanoseconds (how fast gate opens)

Definition at line 184 of file mixer.h.

Referenced by noise_gate_set_params().

◆ attack_ns [3/3]

uint64_t ducking_t::attack_ns

Ducking attack time in nanoseconds.

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 355 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 360 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 353 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 358 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_destroy(), 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_destroy(), 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().

◆ listener_buffers

struct mixer_listener_buffer* mixer_t::listener_buffers[32][32]

◆ 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 mixer_add_source(), mixer_create(), mixer_destroy(), 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 363 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_ns [1/3]

uint64_t compressor_t::release_ns

Release time in nanoseconds (how fast compression releases)

Definition at line 147 of file mixer.h.

Referenced by compressor_set_params().

◆ release_ns [2/3]

uint64_t noise_gate_t::release_ns

Release time in nanoseconds (how fast gate closes)

Definition at line 186 of file mixer.h.

Referenced by noise_gate_set_params().

◆ release_ns [3/3]

uint64_t ducking_t::release_ns

Ducking release time in nanoseconds.

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 340 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 mixer_add_source(), mixer_create(), mixer_destroy(), mixer_process(), mixer_process_excluding_source(), and mixer_remove_source().

◆ source_id_at_hash

const char* mixer_t::source_id_at_hash[256]

Client ID strings stored at each hash index for collision detection.

Definition at line 347 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 strings)

Definition at line 345 of file mixer.h.

Referenced by mixer_create().

◆ source_ids

const char** mixer_t::source_ids

Array of client ID strings (one per source slot)

Definition at line 338 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().

◆ source_lock

rwlock_t mixer_t::source_lock

Reader-writer lock protecting source arrays and bitset.

Definition at line 350 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().