Documentation
¶
Overview ¶
Package audio implements the server-side audio mixing engine.
The Mixer decodes AAC audio from each source, applies per-channel processing, mixes to a stereo master bus, and re-encodes to AAC for the program output. A clock-driven output ticker produces one AAC frame per ~21ms tick (1024 samples at 48kHz), reading from per-channel ring buffers populated by the ingest path. This decouples output cadence from source arrival timing.
Per-channel processing pipeline (in order):
- Trim (-20 to +20 dB input gain)
- EQ: 3-band parametric equalizer (RBJ biquad filters)
- Compressor: Single-band dynamics with envelope follower
- Fader (channel level)
- Mix (sum to stereo master)
- Master fader
- Limiter: Brickwall limiter at -1 dBFS
- Encode (AAC output)
Key types:
- Mixer: Main mixer with per-channel decode/mix/encode
- EQ: 3-band parametric equalizer (Direct Form II Transposed)
- Compressor: Single-band compressor with makeup gain
- Limiter: Brickwall limiter preventing clipping
Index ¶
- Variables
- func BalanceGains(balance float64) (gainL, gainR float32)
- func DBToLinear(db float64) float64
- func EqualPowerCrossfade(oldPCM, newPCM []float32) []float32
- func EqualPowerCrossfadeInto(dst, oldPCM, newPCM []float32) []float32
- func EqualPowerCrossfadeRanged(dst, oldPCM, newPCM []float32, channels int, posStart, posEnd float64) []float32
- func EqualPowerCrossfadeStereo(oldPCM, newPCM []float32, channels int) []float32
- func EqualPowerCrossfadeStereoInto(dst, oldPCM, newPCM []float32, channels int) []float32
- func LinearToDBFS(linear float64) float64
- func PeakLevel(pcm []float32, channels int) (peakL, peakR float64)
- type BiquadFilter
- type Channel
- type Compressor
- func (c *Compressor) GainReduction() float64
- func (c *Compressor) GetParams() (threshold, ratio, attackMs, releaseMs, makeupGain float64)
- func (c *Compressor) IsBypassed() bool
- func (c *Compressor) Process(samples []float32) []float32
- func (c *Compressor) Reset()
- func (c *Compressor) SetParams(threshold, ratio, attackMs, releaseMs, makeupGain float64) error
- type CompressorState
- type Decoder
- type DecoderFactory
- type DelayBuffer
- type EQ
- type EQBandSettings
- type Encoder
- type EncoderFactory
- type FDKDecoder
- type FDKEncoder
- type Gate
- func (g *Gate) GainReduction() float64
- func (g *Gate) IsBypassed() bool
- func (g *Gate) Process(pcm []float32)
- func (g *Gate) Reset()
- func (g *Gate) SetParams(threshold, rangeDB, attackMs, releaseMs, holdMs float64, enabled bool) error
- func (g *Gate) Settings() (threshold, rangeDB, attack, release, hold float64, enabled bool)
- type GateState
- type HPF
- type Limiter
- func (l *Limiter) CeilingDB() float64
- func (l *Limiter) GainReduction() float64
- func (l *Limiter) Process(samples []float32) float64
- func (l *Limiter) Reset()
- func (l *Limiter) SetCeiling(ceilingDB float64)
- func (l *Limiter) SetThreshold(thresholdDB float64)
- func (l *Limiter) ThresholdDB() float64
- func (l *Limiter) TruePeakDBTP() float64
- type LoudnessMeter
- type Mixer
- func (m *Mixer) AddAudioForward(srcKey, dstKey string)
- func (m *Mixer) AddChannel(sourceKey string)
- func (m *Mixer) AdvanceCrossfade()
- func (m *Mixer) AudioDelayMs(sourceKey string) int
- func (m *Mixer) ChannelStates() map[string]internal.AudioChannel
- func (m *Mixer) Close() error
- func (m *Mixer) DebugSnapshot() map[string]any
- func (m *Mixer) ExpectVideoSeed()
- func (m *Mixer) GainReduction() float64
- func (m *Mixer) GetCompressor(sourceKey string) (CompressorState, error)
- func (m *Mixer) GetEQ(sourceKey string) ([3]EQBandSettings, error)
- func (m *Mixer) GetGate(sourceKey string) (GateState, error)
- func (m *Mixer) GetHPF(sourceKey string) (frequency float64, enabled bool)
- func (m *Mixer) IngestFrame(sourceKey string, frame *media.AudioFrame)
- func (m *Mixer) IngestPCM(sourceKey string, pcm []float32, pts int64, channels int)
- func (m *Mixer) IntegratedLUFS() float64
- func (m *Mixer) IsChannelActive(sourceKey string) bool
- func (m *Mixer) IsInTransitionCrossfade() bool
- func (m *Mixer) IsProgramMuted() bool
- func (m *Mixer) LimiterSettings() (thresholdDB, ceilingDB, gainReduction, truePeakDBTP float64)
- func (m *Mixer) MasterLevel() float64
- func (m *Mixer) MomentaryLUFS() float64
- func (m *Mixer) OnCut(oldSource, newSource string)
- func (m *Mixer) OnProgramChange(newProgramSource string)
- func (m *Mixer) OnTransitionAbort(token uint64)
- func (m *Mixer) OnTransitionComplete()
- func (m *Mixer) OnTransitionPosition(position float64)
- func (m *Mixer) OnTransitionStart(oldSource, newSource string, mode TransitionMode, durationMs int) uint64
- func (m *Mixer) PerfSample() PerfMixerSample
- func (m *Mixer) ProduceSamples(sampleCount int) []float32
- func (m *Mixer) ProgramPeak() [2]float64
- func (m *Mixer) RemoveAudioForward(srcKey string)
- func (m *Mixer) RemoveChannel(sourceKey string)
- func (m *Mixer) ResetLoudness()
- func (m *Mixer) ResetMaxInterFrameGap()
- func (m *Mixer) RingBufferLatency90k() int64
- func (m *Mixer) SeedPTSFromVideo(videoPTS int64)
- func (m *Mixer) SetAFV(sourceKey string, afv bool) error
- func (m *Mixer) SetActive(sourceKey string, active bool)
- func (m *Mixer) SetAudioDelay(sourceKey string, delayMs int) error
- func (m *Mixer) SetBalance(sourceKey string, balance float64) error
- func (m *Mixer) SetCompressor(sourceKey string, threshold, ratio, attack, release, makeupGain float64) error
- func (m *Mixer) SetEQ(sourceKey string, band int, frequency, gain, q float64, enabled bool) error
- func (m *Mixer) SetGate(sourceKey string, threshold, rangeDB, attack, release, hold float64, ...) error
- func (m *Mixer) SetHPF(sourceKey string, frequency float64, enabled bool) error
- func (m *Mixer) SetLevel(sourceKey string, levelDB float64) error
- func (m *Mixer) SetLimiterCeiling(ceilingDB float64) error
- func (m *Mixer) SetLimiterThreshold(thresholdDB float64) error
- func (m *Mixer) SetMasterLevel(levelDB float64) error
- func (m *Mixer) SetMetrics(pm *metrics.Metrics)
- func (m *Mixer) SetMuted(sourceKey string, muted bool) error
- func (m *Mixer) SetOutputAudioCallback(fn func(*media.AudioFrame))
- func (m *Mixer) SetPhaseInvert(sourceKey string, invert bool) error
- func (m *Mixer) SetProgramMute(muted bool)
- func (m *Mixer) SetRawAudioSink(sink RawAudioSink)
- func (m *Mixer) SetStingerAudio(audio []float32, sampleRate, channels int)
- func (m *Mixer) SetTrim(sourceKey string, trimDB float64) error
- func (m *Mixer) ShortTermLUFS() float64
- func (m *Mixer) TransitionGains() (oldGain, newGain float64)
- func (m *Mixer) TransitionPosition() float64
- type MixerConfig
- type PCMRingBuffer
- type PerfMixerSample
- type RawAudioSink
- type Resampler
- type TransitionMode
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrInvalidThreshold = errors.New("audio: threshold must be between -40 and 0 dBFS") ErrInvalidRatio = errors.New("audio: ratio must be between 1.0 and 20.0") ErrInvalidAttack = errors.New("audio: attack must be between 0.1 and 100 ms") ErrInvalidRelease = errors.New("audio: release must be between 10 and 1000 ms") ErrInvalidMakeupGain = errors.New("audio: makeup gain must be between 0 and 24 dB") )
Compressor validation errors
var ( ErrInvalidBand = errors.New("audio: band index must be 0, 1, or 2") ErrInvalidFrequency = errors.New("audio: frequency out of range for band") ErrInvalidGain = errors.New("audio: gain must be between -12 and +12 dB") ErrInvalidQ = errors.New("audio: q must be between 0.5 and 4.0") )
EQ validation errors
var ( ErrInvalidGateThreshold = errors.New("audio: gate threshold must be between -60 and 0 dBFS") ErrInvalidGateRange = errors.New("audio: gate range must be between -80 and -20 dB") ErrInvalidGateAttack = errors.New("audio: gate attack must be between 0.01 and 10 ms") ErrInvalidGateRelease = errors.New("audio: gate release must be between 10 and 500 ms") ErrInvalidGateHold = errors.New("audio: gate hold must be between 0 and 500 ms") )
Gate validation errors.
var ( ErrChannelNotFound = errors.New("audio: channel not found") ErrInvalidTrim = errors.New("audio: trim must be between -20 and +20 dB") )
Sentinel errors for the audio mixer.
Functions ¶
func BalanceGains ¶
BalanceGains computes L/R gains for a stereo balance control. balance: -1.0 (hard left) to +1.0 (hard right), 0.0 = center. At center both gains are 1.0 (transparent, zero CPU). Uses cos curve on the attenuated side for smooth rolloff.
func DBToLinear ¶
DBToLinear converts decibels to a linear gain multiplier.
func EqualPowerCrossfade ¶
EqualPowerCrossfade applies an equal-power crossfade between oldPCM and newPCM. Assumes mono (1 channel). For stereo/multi-channel, use EqualPowerCrossfadeStereo.
Example ¶
package main
import (
"fmt"
"github.com/zsiec/switchframe/server/audio"
)
func main() {
// Crossfade from silence to a constant signal over 4 samples.
old := []float32{0, 0, 0, 0}
new := []float32{1, 1, 1, 1}
result := audio.EqualPowerCrossfade(old, new)
// The new source fades in following a sin curve: sin(t * pi/2).
// At t=0 the new source is silent; at t=1 it reaches full volume.
// With 4 samples and 3 intervals, positions are t=0, 1/3, 2/3, 1.
for i, v := range result {
fmt.Printf("sample[%d] = %.4f\n", i, v)
}
}
Output: sample[0] = 0.0000 sample[1] = 0.5000 sample[2] = 0.8660 sample[3] = 1.0000
func EqualPowerCrossfadeInto ¶
EqualPowerCrossfadeInto is like EqualPowerCrossfade but writes into dst. If dst has insufficient capacity, it is grown. Returns the result slice.
func EqualPowerCrossfadeRanged ¶
func EqualPowerCrossfadeRanged(dst, oldPCM, newPCM []float32, channels int, posStart, posEnd float64) []float32
EqualPowerCrossfadeRanged applies an equal-power crossfade over a sub-range of the full [0,1] position. posStart and posEnd define the range within the full crossfade curve. For example, (0.0, 0.5) applies the first half, and (0.5, 1.0) applies the second half. This enables multi-frame crossfades where the ramp is distributed across multiple audio frames.
func EqualPowerCrossfadeStereo ¶
EqualPowerCrossfadeStereo applies an equal-power crossfade between oldPCM and newPCM, using cos/sin curves so total power remains constant through the transition:
cos²(t·π/2) + sin²(t·π/2) = 1 for all t ∈ [0,1]
At t=0 the result is purely old; at t=1 the result is purely new. The output length is max(len(oldPCM), len(newPCM)); the shorter buffer is zero-padded.
channels specifies the interleaved channel count. The crossfade position advances per sample-pair (not per individual sample) so all channels at the same time instant receive identical gain, preventing L/R phase skew.
func EqualPowerCrossfadeStereoInto ¶
EqualPowerCrossfadeStereoInto is like EqualPowerCrossfadeStereo but writes into dst. If dst has insufficient capacity, it is grown. Returns the result slice.
Uses SIMD-accelerated vec.MulAddFloat32 kernel for the multiply-add loop, with pre-expanded gain arrays from the lookup tables.
func LinearToDBFS ¶
LinearToDBFS converts a linear amplitude (0..1) to dBFS. Returns -96 for silence (linear <= 0). Clamped to avoid -Inf which is not JSON-serializable.
Example ¶
package main
import (
"fmt"
"github.com/zsiec/switchframe/server/audio"
)
func main() {
fmt.Printf("%.1f dBFS\n", audio.LinearToDBFS(1.0)) // full scale
fmt.Printf("%.1f dBFS\n", audio.LinearToDBFS(0.5)) // half amplitude
fmt.Printf("%.1f dBFS\n", audio.LinearToDBFS(0.0)) // silence
}
Output: 0.0 dBFS -6.0 dBFS -96.0 dBFS
func PeakLevel ¶
PeakLevel computes the peak absolute amplitude for each channel from interleaved float32 PCM samples. Returns linear values in [0, 1+]. For stereo (channels=2), even indices are left, odd are right.
For mono, delegates directly to vec.PeakAbsFloat32 (SIMD-accelerated). For stereo, deinterleaves into stack buffers and calls PeakAbsFloat32 twice.
Example ¶
package main
import (
"fmt"
"github.com/zsiec/switchframe/server/audio"
)
func main() {
// Stereo interleaved PCM: [L0, R0, L1, R1]
pcm := []float32{0.5, -0.3, 0.8, -0.9}
peakL, peakR := audio.PeakLevel(pcm, 2)
fmt.Printf("L=%.1f R=%.1f\n", peakL, peakR)
}
Output: L=0.8 R=0.9
Types ¶
type BiquadFilter ¶
type BiquadFilter struct {
// contains filtered or unexported fields
}
BiquadFilter implements a Direct Form II Transposed biquad filter with coefficients from the RBJ Audio EQ Cookbook peakingEQ formula.
func (*BiquadFilter) Process ¶
func (f *BiquadFilter) Process(x float64) float64
Process applies the biquad filter to a single sample. A tiny DC offset is injected and removed to prevent filter state from decaying into denormal territory during silence (10-100x CPU spike on x86).
type Channel ¶
type Channel struct {
// contains filtered or unexported fields
}
Channel tracks per-source audio state.
type Compressor ¶
type Compressor struct {
// contains filtered or unexported fields
}
Compressor is a single-band dynamics compressor with envelope follower. Uses an exponential envelope detector with configurable threshold, ratio, attack, release, and makeup gain.
Parameters are stored in an immutable compressorParams snapshot swapped atomically. Envelope state is only written by Process() (single-writer from the mixer's processing goroutine). GainReduction is an atomic float64 for lock-free metering reads.
func NewCompressor ¶
func NewCompressor(sampleRate, channels int) *Compressor
NewCompressor creates a new compressor with default parameters (bypassed: ratio 1:1). channels specifies the interleaved channel count for linked stereo envelope tracking.
func (*Compressor) GainReduction ¶
func (c *Compressor) GainReduction() float64
GainReduction returns the current gain reduction in dB. 0 means no compression is active. Positive values indicate dB of reduction. Lock-free: reads the atomic float64.
func (*Compressor) GetParams ¶
func (c *Compressor) GetParams() (threshold, ratio, attackMs, releaseMs, makeupGain float64)
GetParams returns the current compressor parameters.
func (*Compressor) IsBypassed ¶
func (c *Compressor) IsBypassed() bool
IsBypassed returns true when the compressor has no audible effect: ratio <= 1.0 (no compression) AND no makeup gain applied. Lock-free: reads the atomic params snapshot.
func (*Compressor) Process ¶
func (c *Compressor) Process(samples []float32) []float32
Process applies compression to the samples in-place and returns the result. Lock-free: reads params atomically, writes envelope state (single-writer).
func (*Compressor) Reset ¶
func (c *Compressor) Reset()
Reset clears the envelope and gain reduction state. Called when the program bus transitions to mute (FTB) so that stale envelope state does not briefly suppress audio on unmute.
func (*Compressor) SetParams ¶
func (c *Compressor) SetParams(threshold, ratio, attackMs, releaseMs, makeupGain float64) error
SetParams sets all compressor parameters at once. Validates all parameters and returns an error if any are out of range.
type CompressorState ¶
type CompressorState struct {
Threshold float64
Ratio float64
Attack float64
Release float64
MakeupGain float64
GainReduction float64
}
CompressorState holds the current state of a channel's compressor.
type Decoder ¶
type Decoder interface {
// Decode decodes an AAC frame into interleaved float32 PCM.
// Returns 1024 samples per channel for AAC-LC at 48kHz.
Decode(aacFrame []byte) ([]float32, error)
// Channels returns the interleaved channel count of the most recent
// successful Decode. The mixer uses this to detect surround sources
// that must be downmixed before reaching the per-channel DSP chain.
// Returns 0 when no successful decode has happened yet.
Channels() int
Close() error
}
Decoder decodes AAC frames to interleaved float32 PCM. Implementations: FDKDecoder (cgo), mockDecoder (tests).
type DecoderFactory ¶
DecoderFactory creates Decoders. Allows tests to inject mock factories.
type DelayBuffer ¶
type DelayBuffer struct {
// contains filtered or unexported fields
}
DelayBuffer delays audio frames by a configurable number of milliseconds. At 0ms delay, frames pass through immediately with no allocation. Uses a fixed-size circular buffer to avoid unbounded slice growth.
func NewDelayBuffer ¶
func NewDelayBuffer(delayMs int) *DelayBuffer
NewDelayBuffer creates a new DelayBuffer with the given delay in ms. Delay is clamped to [0, 500].
func (*DelayBuffer) DelayMs ¶
func (b *DelayBuffer) DelayMs() int
DelayMs returns the current delay in milliseconds.
func (*DelayBuffer) Ingest ¶
func (b *DelayBuffer) Ingest(frame *media.AudioFrame) *media.AudioFrame
Ingest stores a frame and returns the oldest frame that has aged past the delay. At 0ms delay, the input frame is returned immediately with no buffering.
func (*DelayBuffer) SetDelayMs ¶
func (b *DelayBuffer) SetDelayMs(ms int)
SetDelayMs updates the delay. Values are clamped to [0, 500].
type EQ ¶
type EQ struct {
// contains filtered or unexported fields
}
EQ is a 3-band parametric equalizer using biquad filters. Each band uses an RBJ peakingEQ formula with configurable frequency, gain, and Q. Per-channel filter state prevents stereo crosstalk in interleaved audio.
Parameters and coefficients are stored in an immutable eqParams snapshot swapped atomically. Filter state (s1, s2) is only accessed by Process() which is single-threaded (called from the mixer's processing goroutine). On coefficient change, filter state is NOT reset — the biquad naturally converges to the new response within ~10 samples, avoiding the step discontinuity (click) that zeroing s1/s2 would cause.
func NewEQ ¶
NewEQ creates a new 3-band parametric EQ with flat (0dB) defaults. channels specifies the number of interleaved audio channels (typically 2 for stereo).
func (*EQ) GetBands ¶
func (eq *EQ) GetBands() [3]EQBandSettings
GetBands returns a snapshot of the current EQ band settings.
func (*EQ) IsBypassed ¶
IsBypassed returns true when all bands are either at 0dB gain or disabled. Lock-free: reads the atomic params snapshot.
func (*EQ) Process ¶
Process applies the enabled EQ bands in series to the input samples. samples must be interleaved with the given number of channels. Returns the processed samples (modifies and returns the input slice).
Lock-free: reads coefficients from the atomic params snapshot. Filter states are only written here (single-writer from mixer goroutine).
type EQBandSettings ¶
EQBandSettings holds the parameters for a single EQ band.
type Encoder ¶
type Encoder interface {
// Encode encodes interleaved float32 PCM into an AAC frame.
Encode(pcm []float32) ([]byte, error)
Close() error
}
Encoder encodes interleaved float32 PCM to AAC frames. Implementations: FDKEncoder (cgo), mockEncoder (tests).
type EncoderFactory ¶
EncoderFactory creates Encoders. Allows tests to inject mock factories.
type FDKDecoder ¶
type FDKDecoder struct {
// contains filtered or unexported fields
}
FDKDecoder wraps the FDK AAC decoder (ADTS mode) and implements Decoder. It decodes ADTS-framed AAC data to interleaved float32 PCM.
The decoder reuses internal buffers across calls. Callers must copy or consume the returned []float32 before the next Decode() call.
func NewFDKDecoder ¶
func NewFDKDecoder(sampleRate, channels int) (*FDKDecoder, error)
NewFDKDecoder creates a new FDK AAC decoder for the given sample rate and channel count.
func (*FDKDecoder) Channels ¶
func (d *FDKDecoder) Channels() int
Channels returns the interleaved channel count of the most recent successful Decode. Returns 0 before the first non-empty decode.
func (*FDKDecoder) Close ¶
func (d *FDKDecoder) Close() error
Close releases the decoder resources. Safe to call multiple times and concurrently from multiple goroutines.
type FDKEncoder ¶
type FDKEncoder struct {
// contains filtered or unexported fields
}
FDKEncoder wraps the FDK AAC encoder (ADTS output) and implements Encoder. It encodes interleaved float32 PCM to ADTS-framed AAC data.
func NewFDKEncoder ¶
func NewFDKEncoder(sampleRate, channels int) (*FDKEncoder, error)
NewFDKEncoder creates a new FDK AAC-LC encoder for the given sample rate and channel count. Bitrate is auto-selected: 128kbps for stereo, 64kbps for mono.
func (*FDKEncoder) Close ¶
func (e *FDKEncoder) Close() error
Close releases the encoder resources. Safe to call multiple times and concurrently from multiple goroutines.
type Gate ¶
type Gate struct {
// contains filtered or unexported fields
}
Gate is a downward noise gate with hold time. Uses range (depth in dB) to control how much attenuation is applied when closed.
Parameters are stored in an immutable gateParams snapshot swapped atomically. Envelope state is only written by Process() (single-writer from the mixer's processing goroutine). GainReduction is an atomic float64 for lock-free metering reads.
func NewGate ¶
NewGate creates a new noise gate, starting bypassed (disabled). channels specifies the interleaved channel count for linked stereo envelope tracking.
func (*Gate) GainReduction ¶
GainReduction returns the current gain reduction in dB. 0 means the gate is fully open. Positive values indicate dB of attenuation. Lock-free: reads the atomic float64.
func (*Gate) IsBypassed ¶
IsBypassed returns true when the gate is disabled or has no params. Lock-free: reads the atomic params snapshot.
func (*Gate) Process ¶
Process applies the gate to interleaved PCM in-place. Lock-free: reads params atomically, writes envelope state (single-writer).
func (*Gate) Reset ¶
func (g *Gate) Reset()
Reset clears the envelope state, restoring the gate to fully open. Called when the program bus transitions to mute (FTB) so that stale envelope state does not briefly gate audio on unmute.
type GateState ¶
type GateState struct {
Threshold float64
Range float64
Attack float64
Release float64
Hold float64
Enabled bool
GainReduction float64
}
GateState holds the current state of a channel's noise gate.
type HPF ¶
type HPF struct {
// contains filtered or unexported fields
}
HPF is a 2nd-order Butterworth high-pass filter (12 dB/octave). Variable frequency 20-500 Hz. Uses RBJ Audio EQ Cookbook high-pass coefficients computed for the BiquadFilter struct.
Parameters are stored in an immutable hpfParams snapshot swapped atomically. Filter state (s1, s2) is only accessed by Process() which is single-threaded (called from the mixer's processing goroutine).
func (*HPF) IsBypassed ¶
IsBypassed returns true when the HPF is disabled or has no params. Lock-free: reads the atomic params snapshot.
func (*HPF) Process ¶
Process applies the HPF to interleaved PCM in-place. Uses per-channel biquad filters to prevent stereo crosstalk.
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter is a true-peak limiter for the program output bus. It uses a peak-following envelope with fast attack and slow release driven by an ITU-R BS.1770-4 Annex 2 4× polyphase FIR true-peak detector, so inter-sample peaks are caught before they reach a downstream DAC. Always active on the program bus.
Ceiling guarantee — steady state only. This is a feed-forward design with zero lookahead and a finite attack coefficient (0.1ms). The dBTP ceiling is only guaranteed once the envelope has settled: on a fast transient ONSET the envelope needs several samples to rise, so the first samples are under-attenuated and the reconstructed true-peak can briefly exceed the configured ceiling (measured ~1 dB of overshoot on a worst-case silence→inter-sample-peak step; see TestLimiter_TransientOnsetOvershoot). The sample-domain hard clamp below clamps the SAMPLE value to ±target, not the reconstructed true-peak, so it does not close this gap. Making the ceiling truly brickwall on transients requires a short (1-2ms) lookahead delay so gain reduction is applied before the peak it must attenuate; that is a follow-up because it adds output latency and changes the audio path.
Process() is single-threaded (called from the mixer's processing goroutine). GainReduction and TruePeak are stored as atomic float64 for lock-free metering reads. Reset() sets an atomic flag; Process() performs the actual state clearing.
func NewLimiter ¶
NewLimiter creates a brickwall true-peak limiter at -1 dBTP (EBU R128 s1). Attack time: 0.1 ms (fast enough to catch transients). Release time: 50 ms (slow enough to avoid pumping). channels specifies the interleaved channel count for linked stereo envelope tracking.
Threshold and ceiling values are in dBTP (decibels relative to full scale, true-peak estimate) rather than sample-peak dBFS. For broadcast compliance (ATSC A/85) operators should tighten the ceiling to -2 dBTP via SetCeiling.
func (*Limiter) GainReduction ¶
GainReduction returns the current gain reduction in dB. 0 means no limiting is active. Positive values indicate how many dB the signal is being reduced. Lock-free: reads the atomic float64.
func (*Limiter) Process ¶
Process applies true-peak limiting to the samples in-place and returns the current gain reduction in dB (positive = gain being reduced).
The peak detector runs an ITU-R BS.1770-4 Annex 2 4× polyphase FIR per channel to estimate the reconstructed (analog) true-peak. The envelope follower tracks that true-peak (linked across channels) and applies a single gain to all channels together. The audio path itself is unchanged — the oversampled reconstruction is used only for detection.
Lock-free: envelope / TP / FIR state is single-writer (mixer goroutine). GR and last-block TP are stored atomically for concurrent metering reads.
func (*Limiter) Reset ¶
func (l *Limiter) Reset()
Reset clears the envelope, gain reduction, true-peak, and FIR state. Called when the program bus transitions to mute (FTB) so that stale envelope state does not briefly suppress audio on unmute.
func (*Limiter) SetCeiling ¶
SetCeiling changes the output ceiling. ceilingDB: -3 to 0 dBTP (true-peak). EBU R128 s1 specifies -1 dBTP for streaming delivery; ATSC A/85 §6.5 specifies -2 dBTP for broadcast.
func (*Limiter) SetThreshold ¶
SetThreshold changes the limiter threshold. thresholdDB: -12 to 0 dBTP (interpreted against the reconstructed true-peak, not sample-peak, since commit that introduced BS.1770-4 true-peak detection).
func (*Limiter) ThresholdDB ¶
ThresholdDB returns the current threshold in dBTP.
func (*Limiter) TruePeakDBTP ¶
TruePeakDBTP returns the maximum ITU-R BS.1770-4 Annex 2 true-peak measured over the most recent Process() call, in dBTP. Returns -Inf when no block has been processed since the last Reset. Lock-free.
type LoudnessMeter ¶
type LoudnessMeter struct {
// contains filtered or unexported fields
}
LoudnessMeter implements BS.1770-4 compliant loudness metering with K-weighted filtering and gated integration. It provides three measurement windows: momentary (400ms), short-term (3s), and integrated (gated).
Process() is single-threaded (called from the mixer's processing goroutine). LUFS readouts are cached as atomic float64s, updated at the end of each block emission for lock-free reads by metering consumers.
func NewLoudnessMeter ¶
func NewLoudnessMeter(sampleRate, channels int) *LoudnessMeter
NewLoudnessMeter creates a new BS.1770-4 loudness meter.
func (*LoudnessMeter) IntegratedLUFS ¶
func (m *LoudnessMeter) IntegratedLUFS() float64
IntegratedLUFS returns the integrated loudness with BS.1770-4 gating. Lock-free: reads the cached atomic value.
func (*LoudnessMeter) MomentaryLUFS ¶
func (m *LoudnessMeter) MomentaryLUFS() float64
MomentaryLUFS returns the momentary loudness (400ms window). Lock-free: reads the cached atomic value.
func (*LoudnessMeter) Process ¶
func (m *LoudnessMeter) Process(samples []float32)
Process applies K-weighting and accumulates samples for loudness measurement. samples must be interleaved PCM (e.g., [L0, R0, L1, R1, ...]).
Lock-free on the hot path: filter state and accumulators are single-writer. Cached LUFS values updated atomically on block boundaries.
func (*LoudnessMeter) Reset ¶
func (m *LoudnessMeter) Reset()
Reset clears all measurement state including integrated blocks and filter state. Sets an atomic flag; the actual state clearing is performed by Process() on the next call to maintain single-writer invariant on filter state. The atomic LUFS readouts are cleared immediately for responsive metering.
func (*LoudnessMeter) ShortTermLUFS ¶
func (m *LoudnessMeter) ShortTermLUFS() float64
ShortTermLUFS returns the short-term loudness (3s window). Lock-free: reads the cached atomic value.
type Mixer ¶
type Mixer struct {
// contains filtered or unexported fields
}
Mixer mixes audio from multiple sources.
func (*Mixer) AddAudioForward ¶
AddAudioForward sets up forwarding: audio ingested for srcKey will also be ingested for dstKey. Used by playout to route live source audio through the playout mixer channel during live playlist items.
func (*Mixer) AddChannel ¶
AddChannel registers a source with the mixer.
func (*Mixer) AdvanceCrossfade ¶
func (m *Mixer) AdvanceCrossfade()
AdvanceCrossfade advances the cut crossfade state machine by one tick. Called by the synchronous processing loop after ProduceSamples.
func (*Mixer) AudioDelayMs ¶
AudioDelayMs returns the current audio delay in milliseconds for a source channel.
func (*Mixer) ChannelStates ¶
func (m *Mixer) ChannelStates() map[string]internal.AudioChannel
ChannelStates returns a snapshot of all channel states for state broadcast.
func (*Mixer) Close ¶
Close releases all codec resources and stops the background ticker. It is safe to call multiple times.
Returns a non-nil error if the background ticker goroutine did not exit within the shutdown timeout. In that pathological case Close still proceeds to free the codecs (leaking them forever would be worse), but it first sets m.closed so a still-running tick()/ingest cannot recreate or reuse the codecs it is about to release — see ensureEncoder/initChannelDecoder.
func (*Mixer) DebugSnapshot ¶
DebugSnapshot implements debug.SnapshotProvider.
func (*Mixer) ExpectVideoSeed ¶
func (m *Mixer) ExpectVideoSeed()
ExpectVideoSeed tells the mixer to wait for SeedPTSFromVideo before self-seeding from audio input. Without this, the mixer seeds from the first IngestPCM call which may use a different PTS timeline. No-op when ClockDriven is true (PTS comes from the external clock).
func (*Mixer) GainReduction ¶
GainReduction returns the current limiter gain reduction in dB. 0 means no limiting; positive values indicate dB of reduction applied.
func (*Mixer) GetCompressor ¶
func (m *Mixer) GetCompressor(sourceKey string) (CompressorState, error)
GetCompressor returns the current compressor settings and gain reduction for a channel.
func (*Mixer) GetEQ ¶
func (m *Mixer) GetEQ(sourceKey string) ([3]EQBandSettings, error)
GetEQ returns the current EQ settings for a channel.
func (*Mixer) GetGate ¶
GetGate returns the current noise gate settings and gain reduction for a channel.
func (*Mixer) IngestFrame ¶
func (m *Mixer) IngestFrame(sourceKey string, frame *media.AudioFrame)
IngestFrame processes an AAC audio frame from a source. Decodes to PCM, applies the per-channel processing chain (trim -> EQ -> compressor), and pushes the processed result into the channel's ring buffer for clock-driven output. No immediate output is produced -- the outputTicker reads from ring buffers at a fixed cadence.
func (*Mixer) IngestPCM ¶
IngestPCM processes raw interleaved float32 PCM from a source (e.g. MXL). Unlike IngestFrame, this skips ADTS parsing and AAC decoding -- the PCM is already in float32 format. Applies the per-channel processing chain (trim -> EQ -> compressor) and pushes into the channel's ring buffer.
PCM input is interleaved float32 (e.g. 1024 samples * 2 channels = 2048 values for stereo). The pts parameter is the presentation timestamp in 90 kHz clock units. The channels parameter is the source's actual channel count (1=mono, 2=stereo). If channels < mixer's numChannels, mono samples are upmixed to stereo.
func (*Mixer) IntegratedLUFS ¶
IntegratedLUFS returns the BS.1770-4 integrated loudness (gated, since last reset).
func (*Mixer) IsChannelActive ¶
IsChannelActive returns whether a channel is currently active.
func (*Mixer) IsInTransitionCrossfade ¶
IsInTransitionCrossfade returns whether a multi-frame transition crossfade is active.
func (*Mixer) IsProgramMuted ¶
IsProgramMuted returns whether program output is muted (FTB held).
func (*Mixer) LimiterSettings ¶
LimiterSettings returns the current limiter threshold, ceiling (in dBTP, true-peak), gain reduction (in dB, positive = reducing), and the measured true-peak from the last block (in dBTP). The TruePeak return value is JSON-safe: math.Inf(-1) and math.NaN — possible from the raw detector on cold start or after Reset — are coerced to -144 dBTP (far below any audible signal) so callers can flow the value through encoding/json without encode errors.
func (*Mixer) MasterLevel ¶
MasterLevel returns the current master level in dB.
func (*Mixer) MomentaryLUFS ¶
MomentaryLUFS returns the BS.1770-4 momentary loudness (400ms window).
func (*Mixer) OnCut ¶
OnCut initiates a 2-tick (~42ms) crossfade between old and new source, driven entirely by the output ticker. The ticker advances transCrossfadePosition linearly over cutTotalFrames ticks and auto-clears when complete. No timer goroutines needed.
func (*Mixer) OnProgramChange ¶
OnProgramChange updates AFV channel states based on the new program source. Channels with AFV enabled activate when they match the program source and deactivate when they don't. Non-AFV channels are unaffected.
To avoid audible clicks, a 4-tick (~85ms) crossfade ramp is set up between the outgoing and incoming AFV channels, identical to what OnCut does.
func (*Mixer) OnTransitionAbort ¶
OnTransitionAbort handles a cancelled transition (e.g. T-bar pulled back to 0). Snaps the crossfade position to 0.0 (fully original source) and clears all transition state, rolling back the incoming channel activation performed by OnTransitionStart so preview audio does not keep leaking into program.
token is the ownership token returned by the OnTransitionStart being aborted. The abort is a no-op when the token is stale — i.e. a newer OnTransitionStart, OnCut, or ramping OnProgramChange has since established its own transition state — so a pre-empted transition's late cleanup can never wipe crossfade state (or the deferred AFV deactivations) owned by a concurrent cut or newer transition.
func (*Mixer) OnTransitionComplete ¶
func (m *Mixer) OnTransitionComplete()
OnTransitionComplete clears the transition crossfade state. Called by the switcher when the video transition finishes.
func (*Mixer) OnTransitionPosition ¶
OnTransitionPosition updates the crossfade position (0.0 = fully old, 1.0 = fully new). Called by the switcher as the video transition progresses. Tracks the previous position for per-sample gain interpolation within audio frames.
func (*Mixer) OnTransitionStart ¶
func (m *Mixer) OnTransitionStart(oldSource, newSource string, mode TransitionMode, durationMs int) uint64
OnTransitionStart begins a multi-frame crossfade between old and new source, synchronized with a video transition. The mode selects the gain curve:
- Crossfade: equal-power A->B (mix/dissolve)
- DipToSilence: A->silence->B (dip through black)
- FadeOut: A->silence (fade to black)
- FadeIn: silence->A (fade from black)
The new source channel is activated so its audio frames are accepted.
It returns an ownership token identifying the transition state it just established. The token must be passed to OnTransitionAbort, which is a no-op unless that token still owns the mixer's transition state. The transCrossfades counter doubles as the state generation: every event that establishes new transition/cut crossfade state (OnTransitionStart, OnCut, a ramping OnProgramChange) bumps it, invalidating older tokens. This makes stale aborts — e.g. a pre-empted StartTransition cleaning up after a concurrent Cut already installed its own crossfade — harmless.
func (*Mixer) PerfSample ¶
func (m *Mixer) PerfSample() PerfMixerSample
PerfSample returns a performance snapshot of the mixer's current state. Safe for concurrent access from any goroutine.
func (*Mixer) ProduceSamples ¶
ProduceSamples produces exactly sampleCount mixed PCM samples on demand. This is the reference-clock-driven path — the caller (synchronous processing loop) controls timing, not an internal ticker. sampleCount is in SAMPLES (not samples*channels — the method handles channels).
func (*Mixer) ProgramPeak ¶
ProgramPeak returns the current program output peak levels in dBFS. Returns [leftDBFS, rightDBFS]. Silence is -Inf.
func (*Mixer) RemoveAudioForward ¶
RemoveAudioForward removes a previously set audio forward.
func (*Mixer) RemoveChannel ¶
RemoveChannel unregisters a source.
func (*Mixer) ResetLoudness ¶
func (m *Mixer) ResetLoudness()
ResetLoudness clears the integrated loudness measurement.
func (*Mixer) ResetMaxInterFrameGap ¶
func (m *Mixer) ResetMaxInterFrameGap()
ResetMaxInterFrameGap resets the max inter-frame gap counter, allowing fresh measurement after a transition or other event.
func (*Mixer) RingBufferLatency90k ¶
RingBufferLatency90k returns the program source's ring buffer depth in 90kHz ticks. Used by the muxer as a dynamic lip-sync offset — the ring buffer adds FIFO latency to audio that the video path (newest-wins frame sync) doesn't have.
func (*Mixer) SeedPTSFromVideo ¶
SeedPTSFromVideo sets the audio PTS epoch from the video pipeline's first frame. Called by the switcher when the first program video frame is processed, ensuring audio and video PTS share the same starting point. Thread-safe — can be called from any goroutine. No-op when ClockDriven is true (PTS comes from the external clock).
func (*Mixer) SetAFV ¶
SetAFV enables or disables audio-follows-video for a channel. When AFV is enabled, the channel activates when its source goes to program and deactivates when it leaves program.
func (*Mixer) SetAudioDelay ¶
SetAudioDelay sets the audio delay in milliseconds for a source channel. Used for lip-sync correction in multi-camera setups.
func (*Mixer) SetBalance ¶
SetBalance sets the stereo balance for a source channel. balance: -1.0 (hard left) to +1.0 (hard right), 0.0 = center. At center both L/R gains are 1.0 (transparent, zero CPU).
func (*Mixer) SetCompressor ¶
func (m *Mixer) SetCompressor(sourceKey string, threshold, ratio, attack, release, makeupGain float64) error
SetCompressor sets all compressor parameters for a channel.
func (*Mixer) SetGate ¶
func (m *Mixer) SetGate(sourceKey string, threshold, rangeDB, attack, release, hold float64, enabled bool) error
SetGate sets the noise gate parameters for a channel.
func (*Mixer) SetLimiterCeiling ¶
SetLimiterCeiling changes the master limiter output ceiling. ceilingDB: -3 to 0 dBTP (true-peak). -1 dBTP matches EBU R128 s1 for streaming; -2 dBTP matches ATSC A/85 §6.5 for broadcast delivery.
func (*Mixer) SetLimiterThreshold ¶
SetLimiterThreshold changes the master limiter threshold. thresholdDB: -12 to 0 dBTP (true-peak).
func (*Mixer) SetMasterLevel ¶
SetMasterLevel sets the master output level in dB. Returns an error if the level is NaN, Inf, or outside [-100, 20].
func (*Mixer) SetMetrics ¶
SetMetrics attaches Prometheus metrics to the mixer.
func (*Mixer) SetOutputAudioCallback ¶
func (m *Mixer) SetOutputAudioCallback(fn func(*media.AudioFrame))
SetOutputAudioCallback registers a direct audio output callback that bypasses the relay for zero-latency delivery to the MPEG-TS muxer. Called synchronously from the output ticker goroutine. Pass nil to clear.
func (*Mixer) SetPhaseInvert ¶
SetPhaseInvert enables or disables phase inversion for a source channel. When enabled, all PCM samples are multiplied by -1.0 (180-degree phase flip).
func (*Mixer) SetProgramMute ¶
SetProgramMute sets the program output mute state. When muted, the mixer produces silent output (FTB held). Metering reflects silence. On unmute, a 5ms fade-in ramp prevents an uncompressed burst caused by the compressor/limiter envelopes starting from zero.
func (*Mixer) SetRawAudioSink ¶
func (m *Mixer) SetRawAudioSink(sink RawAudioSink)
SetRawAudioSink sets or clears the raw audio output tap. The sink receives a copy of the mixed PCM after master processing (fader + limiter) but before AAC encode. This is used by MXL output to write raw audio to shared memory. Pass nil to disable.
func (*Mixer) SetStingerAudio ¶
SetStingerAudio provides the stinger clip's audio PCM for additive overlay during a stinger transition. The audio is consumed sample-by-sample during mix cycles until exhausted or cleared by OnTransitionComplete. Sample-rate mismatch is resampled; channel-count mismatch is converted to m.numChannels (BS.775-3 Lo/Ro downmix for surround sources, AES3 §6.3 duplicate for mono upmix). Channels outside [1, 6] are rejected — the historical log-and-drop path now applies only to that invalid range and to the unsupported stereo→surround upmix case (deferred: layout-aware matrix).
func (*Mixer) SetTrim ¶
SetTrim sets the input trim in dB for a channel (-20 to +20 dB). Trim is applied before the fader in the mix pipeline.
func (*Mixer) ShortTermLUFS ¶
ShortTermLUFS returns the BS.1770-4 short-term loudness (3s window).
func (*Mixer) TransitionGains ¶
TransitionGains returns the crossfade gains for the old and new sources based on the current transition position and mode. When no transition is active, returns (1.0, 0.0).
func (*Mixer) TransitionPosition ¶
TransitionPosition returns the current transition crossfade position (0.0-1.0).
type MixerConfig ¶
type MixerConfig struct {
SampleRate int
Channels int
Output func(*media.AudioFrame)
DecoderFactory DecoderFactory // nil = no AAC decoding (PCM-only sources)
EncoderFactory EncoderFactory // nil = no AAC encoding (tick returns nil)
ClockDriven bool // when true, skip internal ticker (external clock drives output via ProduceSamples)
}
MixerConfig configures the Mixer.
type PCMRingBuffer ¶
type PCMRingBuffer struct {
// contains filtered or unexported fields
}
PCMRingBuffer is a sample-level circular buffer for processed PCM. NOT thread-safe — caller must provide synchronization (mixer mutex).
Sources push variable-length PCM chunks as they arrive (bursty). The output ticker pulls exactly N samples per tick (fixed cadence). When the buffer has fewer than N samples, silence (zeros) is returned — less audible than freeze-repeat with bursty SRT delivery.
func NewPCMRingBuffer ¶
func NewPCMRingBuffer(capacitySamples int) *PCMRingBuffer
NewPCMRingBuffer creates a ring buffer with the given capacity in samples. For stereo 48kHz with 10 frames of buffer: 10 * 1024 * 2 = 20480 samples. The channel count defaults to 1, so FrameCount() == Len() until callers use NewPCMRingBufferWithChannels.
func NewPCMRingBufferWithChannels ¶
func NewPCMRingBufferWithChannels(capacitySamples, channels int) *PCMRingBuffer
NewPCMRingBufferWithChannels creates a ring buffer that knows its interleaved channel count. FrameCount() returns samples/channels so time conversions don't leak channel arithmetic into callers.
func (*PCMRingBuffer) FrameCount ¶
func (rb *PCMRingBuffer) FrameCount() int
FrameCount returns the current number of buffered frames (samples / channels). Prefer this over Len() for time-domain math — it avoids callers redoing channel division themselves and getting it wrong for non-stereo configs.
func (*PCMRingBuffer) Len ¶
func (rb *PCMRingBuffer) Len() int
Len returns the current number of buffered samples (interleaved values).
func (*PCMRingBuffer) Pop ¶
func (rb *PCMRingBuffer) Pop(n int) []float32
Pop removes and returns exactly n samples from the buffer. If fewer than n samples are available, returns silence (zeros). Returns nil if n <= 0.
func (*PCMRingBuffer) PopDriftCompensated ¶
func (rb *PCMRingBuffer) PopDriftCompensated(n, targetDepth, channels int) []float32
PopDriftCompensated reads samples with adaptive drift compensation. Always returns exactly n samples. Adjusts the actual number consumed from the buffer to steer depth toward targetDepth:
- depth > target + threshold: consume n + channels (micro speed-up)
- depth < target - threshold: consume n - channels (micro slow-down)
- otherwise: consume exactly n (no correction)
The ±channels adjustment is absorbed by linear interpolation so the output is always exactly n samples with no discontinuities. This corrects clock drift between push source and pop consumer at a rate of ±1 sample per frame (~20μs at 48kHz) — completely inaudible.
channels is needed to keep adjustments frame-aligned (stereo pairs). targetDepth is in total sample values (samples × channels).
func (*PCMRingBuffer) Push ¶
func (rb *PCMRingBuffer) Push(pcm []float32)
Push appends processed PCM samples to the buffer. If the buffer would overflow, the oldest samples are discarded (newest-wins).
func (*PCMRingBuffer) Reset ¶
func (rb *PCMRingBuffer) Reset()
Reset clears all buffered samples. Scratch buffers are retained so the next pops reuse their capacity without reallocating.
type PerfMixerSample ¶
type PerfMixerSample struct {
Mode string
MixCycleLastNs int64
FramesOutput int64
FramesMixed int64
MaxInterFrameGapNs int64
DecodeErrors int64
EncodeErrors int64
MomentaryLUFS float64
ShortTermLUFS float64
IntegratedLUFS float64
}
PerfMixerSample mirrors perf.MixerSample for interface satisfaction. We can't import the perf package from audio (circular dependency), so we define compatible types here. The perf.Sampler wraps these via a thin adapter.
type RawAudioSink ¶
RawAudioSink receives a copy of the mixed PCM after master processing (fader + limiter) but before AAC encode. Used by MXL output to write raw audio to shared memory.
type Resampler ¶
type Resampler struct {
// contains filtered or unexported fields
}
Resampler performs sample rate conversion using a polyphase FIR filter with Kaiser-windowed sinc kernel. It supports arbitrary rational rate pairs and maintains state across calls for click-free block processing.
func NewResampler ¶
NewResampler creates a resampler that converts audio from srcRate to dstRate. The channels parameter specifies the number of interleaved audio channels.
func (*Resampler) DownFactor ¶
DownFactor returns M (decimation factor).
func (*Resampler) Resample ¶
Resample converts interleaved float32 PCM from srcRate to dstRate. State is maintained across calls so consecutive blocks produce a continuous output stream with no discontinuities.
func (*Resampler) ResampleFrameAligned ¶
ResampleFrameAligned resamples the input and returns exactly frameSize*channels output samples. Internally it accumulates resampled samples in a FIFO and drains exactly one frame per call. This ensures AAC encoders (which require exactly 1024 samples per frame) always receive the right amount.
If not enough resampled data is available yet (startup transient), the output is zero-padded. Over time the FIFO converges and the padding disappears.
func (*Resampler) Reset ¶
func (r *Resampler) Reset()
Reset clears the resampler's internal state, allowing it to be reused for a new audio stream without creating a new instance.
func (*Resampler) TapsPerPhase ¶
TapsPerPhase returns the number of filter taps per polyphase branch.
type TransitionMode ¶
type TransitionMode int
TransitionMode describes how audio should behave during a video transition.
const ( Crossfade TransitionMode = iota // Mix: equal-power A→B DipToSilence // Dip: A→silence→B FadeOut // FTB: A→silence FadeIn // FTB Reverse: silence→A )