///|
/// Create a new stereo mixer at the given sample rate.
pub fn new_mixer(
sample_rate : Int,
resample_quality? : ResampleQuality = Linear,
) -> Mixer {
Mixer::{
voices: [],
master_gain: 1.0,
channels: 2,
sample_rate,
resample_quality,
next_voice_id: 0,
}
}
///|
/// Create a new voice from an audio source.
pub fn new_voice(
mixer : Mixer,
source : AudioSource,
gain? : Float = 1.0,
pan? : Float = 0.0,
looping? : Bool = false,
envelope? : Envelope? = None,
) -> Voice {
let id = VoiceId(mixer.next_voice_id)
mixer.next_voice_id = mixer.next_voice_id + 1
let sample_rate = source_sample_rate(source)
Voice::{
id,
source,
position: 0.0,
gain,
pan,
state: VoiceState::Playing,
looping,
sample_rate,
loop_start: 0,
loop_end: 0,
envelope,
effects: [],
}
}
///|
/// Create a new voice from an AudioBuffer (convenience wrapper).
pub fn new_voice_from_buffer(
mixer : Mixer,
buffer : AudioBuffer,
gain? : Float = 1.0,
pan? : Float = 0.0,
looping? : Bool = false,
envelope? : Envelope? = None,
) -> Voice {
new_voice(
mixer,
AudioSource::Buffer(buffer),
gain~,
pan~,
looping~,
envelope~,
)
}
///|
/// Add a voice to the mixer. Returns the voice ID.
pub fn add_voice(mixer : Mixer, voice : Voice) -> VoiceId {
mixer.voices.push(voice)
voice.id
}
///|
/// Compute constant-power pan gains for left and right channels.
/// pan: -1.0 (full left) to 1.0 (full right), 0.0 = center.
pub fn pan_gains(pan : Float) -> (Float, Float) {
// Map pan [-1, 1] to angle [0, pi/2]
let half_pi = Float::from_double(@math.PI / 2.0)
let angle = (pan + 1.0) * 0.5 * half_pi
let left = @math.cosf(angle)
let right = @math.sinf(angle)
(left, right)
}
///|
/// Mix all playing voices into the output buffer for the given number of frames.
/// Output is interleaved stereo (2 channels). Buffer must have length >= frames * 2.
pub fn tick(mixer : Mixer, frames : Int, output : FixedArray[Float]) -> Unit {
// Zero the output buffer
output.fill(0.0, end=frames * 2)
for voice in mixer.voices {
if voice.state != VoiceState::Playing {
continue
}
let src = voice.source
let src_channels = source_channels(src)
let src_frames = source_frame_count(src)
let (pan_l, pan_r) = pan_gains(voice.pan)
let voice_gain = voice.gain
let ratio = resample_ratio(voice.sample_rate, mixer.sample_rate)
for f in 0.. 0 {
voice.loop_end
} else {
src_frames
}
if int_pos >= end_frame {
if voice.looping {
voice.position = Float::from_int(voice.loop_start)
} else {
voice.state = VoiceState::Stopped
break
}
}
let env_gain : Float = match voice.envelope {
Some(env) => {
let g = envelope_tick(env)
if env.phase == EnvelopePhase::Done {
voice.state = VoiceState::Stopped
break
}
g
}
None => 1.0
}
let combined_gain = voice_gain * env_gain
let raw_l = resample_frame(
mixer.resample_quality,
src,
voice.position,
0,
src_frames,
)
let proc_l = effects_chain_process(voice.effects, raw_l)
let sample_l = proc_l * combined_gain * pan_l
let sample_r = if src_channels >= 2 {
resample_frame(
mixer.resample_quality,
src,
voice.position,
1,
src_frames,
) *
combined_gain *
pan_r
} else {
proc_l * combined_gain * pan_r
}
let out_idx = f * 2
output[out_idx] = output[out_idx] + sample_l
output[out_idx + 1] = output[out_idx + 1] + sample_r
voice.position = voice.position + ratio
}
}
// Apply master gain
if mixer.master_gain != 1.0 {
for i in 0..<(frames * 2) {
output[i] = output[i] * mixer.master_gain
}
}
}
///|
/// Remove stopped voices from the mixer.
pub fn collect_stopped(mixer : Mixer) -> Unit {
mixer.voices = mixer.voices.filter(fn(v) { v.state != VoiceState::Stopped })
}