///|
/// A single audio track within the multi-track Mixer.
#derive(Eq)
pub struct Track {
buffer : AudioBuffer
offset_seconds : Double
volume : Double
pan : Double
mut mute : Bool
}
///|
/// Creates a new Track.
pub fn Track::new(
buffer : AudioBuffer,
offset_seconds : Double,
volume : Double,
pan : Double,
) -> Track {
let off = if offset_seconds < 0.0 { 0.0 } else { offset_seconds }
let vol = if volume < 0.0 { 0.0 } else { volume }
let p = if pan < -1.0 { -1.0 } else if pan > 1.0 { 1.0 } else { pan }
{ buffer, offset_seconds: off, volume: vol, pan: p, mute: false, }
}
///|
/// Mutes or unmutes this track.
pub fn Track::set_mute(self : Track, mute : Bool) -> Unit {
self.mute = mute
}
///|
/// Multi-track game audio mixer supporting polyphonic track scheduling,
/// stereo panning per track, and anti-clipping master soft limiting.
#derive(Eq)
pub struct Mixer {
sample_rate : Int32
tracks : Array[Track]
mut master_volume : Double
}
///|
/// Creates an empty Mixer with the specified sample rate.
pub fn Mixer::new(sample_rate : Int32) -> Mixer {
let sr = if sample_rate <= 0 { 44100 } else { sample_rate }
{ sample_rate: sr, tracks: [], master_volume: 1.0, }
}
///|
/// Adds an audio buffer to the mixer scheduled to start at `offset_seconds`
/// with individual volume and stereo pan (-1.0 to 1.0).
pub fn Mixer::add_track(
self : Mixer,
buffer : AudioBuffer,
offset_seconds : Double,
volume : Double,
pan : Double,
) -> Unit {
self.tracks.push(Track::new(buffer, offset_seconds, volume, pan))
}
///|
/// Sets the overall master volume of the mixer.
pub fn Mixer::set_master_volume(self : Mixer, vol : Double) -> Unit {
self.master_volume = if vol < 0.0 { 0.0 } else { vol }
}
///|
/// Returns total duration of the mixed master output in seconds.
pub fn Mixer::duration(self : Mixer) -> Double {
let mut max_dur = 0.0
for i = 0; i < self.tracks.length(); i = i + 1 {
let t = self.tracks[i]
let track_end = t.offset_seconds + t.buffer.duration()
if track_end > max_dur {
max_dur = track_end
}
}
max_dur
}
///|
/// Renders all scheduled tracks into a 2-channel StereoBuffer.
/// Implements Constant-Power Panning per track and master soft-limiting.
pub fn Mixer::render_stereo(self : Mixer) -> StereoBuffer {
let sr_d = self.sample_rate.to_double()
let total_dur = self.duration()
let total_samples = (total_dur * sr_d).to_int()
let left_samples = Array::make(total_samples, 0.0)
let right_samples = Array::make(total_samples, 0.0)
let pi_fourth = 3.141592653589793 / 4.0
for t_idx = 0; t_idx < self.tracks.length(); t_idx = t_idx + 1 {
let track = self.tracks[t_idx]
if track.mute {
continue
}
let offset_sample = (track.offset_seconds * sr_d).to_int()
let (gain_l, gain_r) = if track.pan <= -0.9999 {
(track.volume * self.master_volume, 0.0)
} else if track.pan >= 0.9999 {
(0.0, track.volume * self.master_volume)
} else {
let angle = (track.pan + 1.0) * pi_fourth
(
@math.cos(angle) * track.volume * self.master_volume,
@math.sin(angle) * track.volume * self.master_volume,
)
}
let t_samples = track.buffer.samples
for i = 0; i < t_samples.length(); i = i + 1 {
let target_idx = offset_sample + i
if target_idx < total_samples {
let s = t_samples[i]
left_samples[target_idx] = left_samples[target_idx] + s * gain_l
right_samples[target_idx] = right_samples[target_idx] + s * gain_r
}
}
}
let left_buf : AudioBuffer = {
sample_rate: self.sample_rate,
samples: left_samples,
}
let right_buf : AudioBuffer = {
sample_rate: self.sample_rate,
samples: right_samples,
}
// Apply master anti-clipping soft-limiting
left_buf.soft_limit(0.85)
right_buf.soft_limit(0.85)
{ sample_rate: self.sample_rate, left: left_buf, right: right_buf, }
}
///|
/// Renders all scheduled tracks into a mono AudioBuffer with master soft-limiting.
pub fn Mixer::render_mono(self : Mixer) -> AudioBuffer {
let sr_d = self.sample_rate.to_double()
let total_dur = self.duration()
let total_samples = (total_dur * sr_d).to_int()
let samples = Array::make(total_samples, 0.0)
for t_idx = 0; t_idx < self.tracks.length(); t_idx = t_idx + 1 {
let track = self.tracks[t_idx]
if track.mute {
continue
}
let offset_sample = (track.offset_seconds * sr_d).to_int()
let track_gain = track.volume * self.master_volume
let t_samples = track.buffer.samples
for i = 0; i < t_samples.length(); i = i + 1 {
let target_idx = offset_sample + i
if target_idx < total_samples {
samples[target_idx] = samples[target_idx] + t_samples[i] * track_gain
}
}
}
let buf : AudioBuffer = { sample_rate: self.sample_rate, samples, }
buf.soft_limit(0.85)
buf
}