audio

package
v0.0.0-...-3c84d77 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 16 Imported by: 0

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

Examples

Constants

This section is empty.

Variables

View Source
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

View Source
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

View Source
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.

View Source
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

func BalanceGains(balance float64) (gainL, gainR float32)

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

func DBToLinear(db float64) float64

DBToLinear converts decibels to a linear gain multiplier.

func EqualPowerCrossfade

func EqualPowerCrossfade(oldPCM, newPCM []float32) []float32

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

func EqualPowerCrossfadeInto(dst, oldPCM, newPCM []float32) []float32

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

func EqualPowerCrossfadeStereo(oldPCM, newPCM []float32, channels int) []float32

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

func EqualPowerCrossfadeStereoInto(dst, oldPCM, newPCM []float32, channels int) []float32

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

func LinearToDBFS(linear float64) float64

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

func PeakLevel(pcm []float32, channels int) (peakL, peakR float64)

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

func (*BiquadFilter) Reset

func (f *BiquadFilter) Reset()

Reset clears the filter state.

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

type DecoderFactory func(sampleRate, channels int) (Decoder, error)

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

func NewEQ(sampleRate, channels int) *EQ

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

func (eq *EQ) IsBypassed() bool

IsBypassed returns true when all bands are either at 0dB gain or disabled. Lock-free: reads the atomic params snapshot.

func (*EQ) Process

func (eq *EQ) Process(samples []float32, channels int) []float32

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

func (*EQ) SetBand

func (eq *EQ) SetBand(band int, frequency, gain, q float64, enabled bool) error

SetBand sets the parameters for a single EQ band and recalculates coefficients. band: 0 (Low), 1 (Mid), 2 (High) frequency: center frequency in Hz (must be within band range) gain: dB gain (-12 to +12) q: filter Q (0.5 to 4.0) enabled: whether the band is active

type EQBandSettings

type EQBandSettings struct {
	Frequency float64
	Gain      float64
	Q         float64
	Enabled   bool
}

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

type EncoderFactory func(sampleRate, channels int) (Encoder, error)

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.

func (*FDKDecoder) Decode

func (d *FDKDecoder) Decode(aacFrame []byte) ([]float32, error)

Decode decodes an ADTS-framed AAC frame into interleaved float32 PCM.

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.

func (*FDKEncoder) Encode

func (e *FDKEncoder) Encode(pcm []float32) ([]byte, error)

Encode encodes interleaved float32 PCM into an ADTS-framed AAC frame. The input must contain exactly frameSize * channels samples (1024 * channels for AAC-LC).

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

func NewGate(sampleRate, channels int) *Gate

NewGate creates a new noise gate, starting bypassed (disabled). channels specifies the interleaved channel count for linked stereo envelope tracking.

func (*Gate) GainReduction

func (g *Gate) GainReduction() float64

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

func (g *Gate) IsBypassed() bool

IsBypassed returns true when the gate is disabled or has no params. Lock-free: reads the atomic params snapshot.

func (*Gate) Process

func (g *Gate) Process(pcm []float32)

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.

func (*Gate) SetParams

func (g *Gate) SetParams(threshold, rangeDB, attackMs, releaseMs, holdMs float64, enabled bool) error

SetParams sets all gate parameters at once. Validates all parameters and returns an error if any are out of range.

func (*Gate) Settings

func (g *Gate) Settings() (threshold, rangeDB, attack, release, hold float64, enabled bool)

Settings returns current gate parameters for state broadcast.

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 NewHPF

func NewHPF(sampleRate int) *HPF

NewHPF creates a new high-pass filter, starting bypassed at 80 Hz.

func (*HPF) Enabled

func (h *HPF) Enabled() bool

Enabled returns whether the HPF is currently active.

func (*HPF) Frequency

func (h *HPF) Frequency() float64

Frequency returns the current HPF cutoff frequency.

func (*HPF) IsBypassed

func (h *HPF) IsBypassed() bool

IsBypassed returns true when the HPF is disabled or has no params. Lock-free: reads the atomic params snapshot.

func (*HPF) Process

func (h *HPF) Process(pcm []float32, channels int)

Process applies the HPF to interleaved PCM in-place. Uses per-channel biquad filters to prevent stereo crosstalk.

func (*HPF) Reset

func (h *HPF) Reset()

Reset clears filter state (call on mute/unmute to prevent transients).

func (*HPF) SetParams

func (h *HPF) SetParams(frequency float64, enabled bool) error

SetParams sets the HPF frequency and enabled state. Computes RBJ cookbook high-pass biquad coefficients.

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

func NewLimiter(sampleRate, channels int) *Limiter

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

func (l *Limiter) CeilingDB() float64

CeilingDB returns the current ceiling in dBTP.

func (*Limiter) GainReduction

func (l *Limiter) GainReduction() float64

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

func (l *Limiter) Process(samples []float32) float64

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

func (l *Limiter) SetCeiling(ceilingDB float64)

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

func (l *Limiter) SetThreshold(thresholdDB float64)

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

func (l *Limiter) ThresholdDB() float64

ThresholdDB returns the current threshold in dBTP.

func (*Limiter) TruePeakDBTP

func (l *Limiter) TruePeakDBTP() float64

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 NewMixer

func NewMixer(config MixerConfig) *Mixer

NewMixer creates a Mixer.

func (*Mixer) AddAudioForward

func (m *Mixer) AddAudioForward(srcKey, dstKey string)

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

func (m *Mixer) AddChannel(sourceKey string)

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

func (m *Mixer) AudioDelayMs(sourceKey string) int

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

func (m *Mixer) Close() error

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

func (m *Mixer) DebugSnapshot() map[string]any

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

func (m *Mixer) GainReduction() float64

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

func (m *Mixer) GetGate(sourceKey string) (GateState, error)

GetGate returns the current noise gate settings and gain reduction for a channel.

func (*Mixer) GetHPF

func (m *Mixer) GetHPF(sourceKey string) (frequency float64, enabled bool)

GetHPF returns the current HPF frequency and enabled state 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

func (m *Mixer) IngestPCM(sourceKey string, pcm []float32, pts int64, channels int)

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

func (m *Mixer) IntegratedLUFS() float64

IntegratedLUFS returns the BS.1770-4 integrated loudness (gated, since last reset).

func (*Mixer) IsChannelActive

func (m *Mixer) IsChannelActive(sourceKey string) bool

IsChannelActive returns whether a channel is currently active.

func (*Mixer) IsInTransitionCrossfade

func (m *Mixer) IsInTransitionCrossfade() bool

IsInTransitionCrossfade returns whether a multi-frame transition crossfade is active.

func (*Mixer) IsProgramMuted

func (m *Mixer) IsProgramMuted() bool

IsProgramMuted returns whether program output is muted (FTB held).

func (*Mixer) LimiterSettings

func (m *Mixer) LimiterSettings() (thresholdDB, ceilingDB, gainReduction, truePeakDBTP float64)

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

func (m *Mixer) MasterLevel() float64

MasterLevel returns the current master level in dB.

func (*Mixer) MomentaryLUFS

func (m *Mixer) MomentaryLUFS() float64

MomentaryLUFS returns the BS.1770-4 momentary loudness (400ms window).

func (*Mixer) OnCut

func (m *Mixer) OnCut(oldSource, newSource string)

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

func (m *Mixer) OnProgramChange(newProgramSource string)

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

func (m *Mixer) OnTransitionAbort(token uint64)

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

func (m *Mixer) OnTransitionPosition(position float64)

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

func (m *Mixer) ProduceSamples(sampleCount int) []float32

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

func (m *Mixer) ProgramPeak() [2]float64

ProgramPeak returns the current program output peak levels in dBFS. Returns [leftDBFS, rightDBFS]. Silence is -Inf.

func (*Mixer) RemoveAudioForward

func (m *Mixer) RemoveAudioForward(srcKey string)

RemoveAudioForward removes a previously set audio forward.

func (*Mixer) RemoveChannel

func (m *Mixer) RemoveChannel(sourceKey string)

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

func (m *Mixer) RingBufferLatency90k() int64

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

func (m *Mixer) SeedPTSFromVideo(videoPTS int64)

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

func (m *Mixer) SetAFV(sourceKey string, afv bool) error

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

func (m *Mixer) SetActive(sourceKey string, active bool)

SetActive activates or deactivates a channel.

func (*Mixer) SetAudioDelay

func (m *Mixer) SetAudioDelay(sourceKey string, delayMs int) error

SetAudioDelay sets the audio delay in milliseconds for a source channel. Used for lip-sync correction in multi-camera setups.

func (*Mixer) SetBalance

func (m *Mixer) SetBalance(sourceKey string, balance float64) error

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

func (m *Mixer) SetEQ(sourceKey string, band int, frequency, gain, q float64, enabled bool) error

SetEQ sets a single EQ band on 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) SetHPF

func (m *Mixer) SetHPF(sourceKey string, frequency float64, enabled bool) error

SetHPF sets the high-pass filter parameters for a channel.

func (*Mixer) SetLevel

func (m *Mixer) SetLevel(sourceKey string, levelDB float64) error

SetLevel sets the gain in dB for a channel.

func (*Mixer) SetLimiterCeiling

func (m *Mixer) SetLimiterCeiling(ceilingDB float64) error

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

func (m *Mixer) SetLimiterThreshold(thresholdDB float64) error

SetLimiterThreshold changes the master limiter threshold. thresholdDB: -12 to 0 dBTP (true-peak).

func (*Mixer) SetMasterLevel

func (m *Mixer) SetMasterLevel(levelDB float64) error

SetMasterLevel sets the master output level in dB. Returns an error if the level is NaN, Inf, or outside [-100, 20].

func (*Mixer) SetMetrics

func (m *Mixer) SetMetrics(pm *metrics.Metrics)

SetMetrics attaches Prometheus metrics to the mixer.

func (*Mixer) SetMuted

func (m *Mixer) SetMuted(sourceKey string, muted bool) error

SetMuted sets the mute state for a channel.

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

func (m *Mixer) SetPhaseInvert(sourceKey string, invert bool) error

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

func (m *Mixer) SetProgramMute(muted bool)

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

func (m *Mixer) SetStingerAudio(audio []float32, sampleRate, channels int)

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

func (m *Mixer) SetTrim(sourceKey string, trimDB float64) error

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

func (m *Mixer) ShortTermLUFS() float64

ShortTermLUFS returns the BS.1770-4 short-term loudness (3s window).

func (*Mixer) TransitionGains

func (m *Mixer) TransitionGains() (oldGain, newGain float64)

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

func (m *Mixer) TransitionPosition() float64

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

type RawAudioSink func(pcm []float32, pts int64, sampleRate, channels int)

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

func NewResampler(srcRate, dstRate, channels int) *Resampler

NewResampler creates a resampler that converts audio from srcRate to dstRate. The channels parameter specifies the number of interleaved audio channels.

func (*Resampler) DownFactor

func (r *Resampler) DownFactor() int

DownFactor returns M (decimation factor).

func (*Resampler) Resample

func (r *Resampler) Resample(in []float32) []float32

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

func (r *Resampler) ResampleFrameAligned(in []float32, frameSize int) []float32

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

func (r *Resampler) TapsPerPhase() int

TapsPerPhase returns the number of filter taps per polyphase branch.

func (*Resampler) UpFactor

func (r *Resampler) UpFactor() int

UpFactor returns L (interpolation factor).

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
)

Directories

Path Synopsis
Package vec provides SIMD-accelerated float32 vector operations for the audio mixing hot path.
Package vec provides SIMD-accelerated float32 vector operations for the audio mixing hot path.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL