// moonaudio — pure MoonBit audio decoding and DSP toolkit.
// Copyright 2026 Sakuraqwq1
// Licensed under the Apache License, Version 2.0.
///|
/// A decoded audio signal: interleaved `Double` samples normalised to
/// `[-1.0, 1.0]`, plus the sample rate and channel count needed to interpret
/// them.
///
/// Interleaved layout keeps decoding, resampling and re-encoding simple: frame
/// `f` channel `c` lives at `data[f * channels + c]`. Samples are stored as
/// `Double` because every integer PCM width up to 32 bit converts losslessly
/// into it, and DSP stages work in floating point anyway.
pub(all) struct AudioBuffer {
sample_rate : Int32
channels : Int32
data : Array[Double]
}
///|
/// Create an empty buffer with `sample_rate` and `channels`.
pub fn AudioBuffer::new(sample_rate : Int32, channels : Int32) -> AudioBuffer {
AudioBuffer::{ sample_rate, channels, data: [], }
}
///|
/// Create a silent buffer that already contains `frames` frames.
pub fn AudioBuffer::silence(
sample_rate : Int32,
channels : Int32,
frames : Int32,
) -> AudioBuffer raise AudioError {
if sample_rate <= 0 {
raise OutOfRange("sample rate must be positive, got \{sample_rate}")
}
if channels <= 0 {
raise OutOfRange("channel count must be positive, got \{channels}")
}
if frames < 0 {
raise OutOfRange("frame count must not be negative, got \{frames}")
}
AudioBuffer::{
sample_rate,
channels,
data: Array::make(frames * channels, 0.0),
}
}
///|
/// Number of frames (one sample per channel) in the buffer.
pub fn AudioBuffer::frames(self : AudioBuffer) -> Int32 {
if self.channels <= 0 {
0
} else {
self.data.length() / self.channels
}
}
///|
/// `true` when the buffer carries no frames.
pub fn AudioBuffer::is_empty(self : AudioBuffer) -> Bool {
self.frames() == 0
}
///|
/// Playback duration in seconds.
pub fn AudioBuffer::duration_seconds(self : AudioBuffer) -> Double {
if self.sample_rate <= 0 {
0.0
} else {
self.frames().to_double() / self.sample_rate.to_double()
}
}
///|
/// Read one sample; out of range reads yield `0.0` so DSP stages stay total.
pub fn AudioBuffer::sample(
self : AudioBuffer,
frame : Int32,
channel : Int32,
) -> Double {
if channel < 0 || channel >= self.channels {
return 0.0
}
let index = frame * self.channels + channel
if index < 0 || index >= self.data.length() {
0.0
} else {
self.data[index]
}
}
///|
/// Overwrite one sample, ignoring writes outside the buffer.
pub fn AudioBuffer::set_sample(
self : AudioBuffer,
frame : Int32,
channel : Int32,
value : Double,
) -> Unit {
if channel < 0 || channel >= self.channels {
return
}
let index = frame * self.channels + channel
if index >= 0 && index < self.data.length() {
self.data[index] = value
}
}
///|
/// Append one frame given as a slice of per-channel samples.
///
/// The slice is padded with silence or truncated when its length differs from
/// the channel count.
pub fn AudioBuffer::push_frame(
self : AudioBuffer,
samples : ArrayView[Double],
) -> Unit {
for channel = 0; channel < self.channels; channel = channel + 1 {
self.data.push(
if channel < samples.length() {
samples[channel]
} else {
0.0
},
)
}
}
///|
/// Append every frame of another buffer that shares the same layout.
pub fn AudioBuffer::append(
self : AudioBuffer,
other : AudioBuffer,
) -> Unit raise AudioError {
if other.channels != self.channels {
raise Invalid("channel mismatch: \{self.channels} vs \{other.channels}")
}
if other.sample_rate != self.sample_rate {
raise Invalid(
"sample rate mismatch: \{self.sample_rate} vs \{other.sample_rate}",
)
}
for sample in other.data {
self.data.push(sample)
}
}
///|
/// Build a buffer from per-channel arrays, converting to interleaved layout.
pub fn AudioBuffer::from_channels(
sample_rate : Int32,
channels : Array[Array[Double]],
) -> AudioBuffer raise AudioError {
let count = channels.length()
if count == 0 {
raise OutOfRange("at least one channel is required")
}
let mut frames = channels[0].length()
for channel in channels {
if channel.length() < frames {
frames = channel.length()
}
}
let data : Array[Double] = Array::make(frames * count, 0.0)
for channel = 0; channel < count; channel = channel + 1 {
let source = channels[channel]
for frame = 0; frame < frames; frame = frame + 1 {
data[frame * count + channel] = source[frame]
}
}
AudioBuffer::{ sample_rate, channels: count, data, }
}
///|
/// Split the buffer into per-channel arrays.
pub fn AudioBuffer::to_channels(self : AudioBuffer) -> Array[Array[Double]] {
let frames = self.frames()
let result : Array[Array[Double]] = []
for channel = 0; channel < self.channels; channel = channel + 1 {
let samples : Array[Double] = Array::make(frames, 0.0)
for frame = 0; frame < frames; frame = frame + 1 {
samples[frame] = self.data[frame * self.channels + channel]
}
result.push(samples)
}
result
}
///|
/// Largest absolute sample value in the buffer.
pub fn AudioBuffer::peak(self : AudioBuffer) -> Double {
let mut peak = 0.0
for sample in self.data {
let magnitude = sample.abs()
if magnitude > peak {
peak = magnitude
}
}
peak
}
///|
/// Root mean square level of all samples.
pub fn AudioBuffer::rms(self : AudioBuffer) -> Double {
if self.data.length() == 0 {
return 0.0
}
let mut sum = 0.0
for sample in self.data {
sum += sample * sample
}
(sum / self.data.length().to_double()).sqrt()
}
///|
/// Peak level in dBFS (`-inf` for silence).
pub fn AudioBuffer::peak_dbfs(self : AudioBuffer) -> Double {
dbfs(self.peak())
}
///|
/// RMS level in dBFS (`-inf` for silence).
pub fn AudioBuffer::rms_dbfs(self : AudioBuffer) -> Double {
dbfs(self.rms())
}
///|
/// Linear amplitude to dBFS, with 0.0 mapped to negative infinity.
pub fn dbfs(amplitude : Double) -> Double {
let magnitude = amplitude.abs()
if magnitude <= 0.0 {
-@double.infinity
} else {
20.0 * @math.log10(magnitude)
}
}
///|
/// dBFS back to linear amplitude.
pub fn from_dbfs(decibels : Double) -> Double {
@math.pow(10.0, decibels / 20.0)
}
///|
/// Scale every sample so the peak equals `target` (in `[0, 1]`).
///
/// Returns the applied linear gain; a silent buffer is left untouched.
pub fn AudioBuffer::normalize(
self : AudioBuffer,
target~ : Double = 0.9,
) -> Double {
let goal = if target < 0.0 {
0.0
} else if target > 1.0 {
1.0
} else {
target
}
let peak = self.peak()
if peak <= 0.0 {
return 1.0
}
let gain = goal / peak
for index = 0; index < self.data.length(); index = index + 1 {
self.data[index] = self.data[index] * gain
}
gain
}
///|
/// Multiply every sample by `gain`.
pub fn AudioBuffer::apply_gain(self : AudioBuffer, gain : Double) -> Unit {
for index = 0; index < self.data.length(); index = index + 1 {
self.data[index] = self.data[index] * gain
}
}
///|
/// Multiply every sample by `gain` while keeping `|sample| <= 1.0`.
pub fn AudioBuffer::apply_gain_clamped(
self : AudioBuffer,
gain : Double,
) -> Unit {
for index = 0; index < self.data.length(); index = index + 1 {
self.data[index] = clamp_sample(self.data[index] * gain)
}
}
///|
/// Return a copy of the buffer.
pub fn AudioBuffer::copy(self : AudioBuffer) -> AudioBuffer {
AudioBuffer::{
sample_rate: self.sample_rate,
channels: self.channels,
data: self.data.copy(),
}
}
///|
/// Return the frame range `[start, end)` as a new buffer.
pub fn AudioBuffer::slice(
self : AudioBuffer,
start : Int32,
end : Int32,
) -> AudioBuffer {
let frames = self.frames()
let from = start.max(0).min(frames)
let to = end.max(from).min(frames)
let data : Array[Double] = Array::make((to - from) * self.channels, 0.0)
let offset = from * self.channels
for index = 0; index < data.length(); index = index + 1 {
data[index] = self.data[offset + index]
}
AudioBuffer::{ sample_rate: self.sample_rate, channels: self.channels, data, }
}
///|
/// Downmix or upmix to `channels` channels.
///
/// Mono to stereo duplicates the channel, stereo to mono averages both
/// channels, and other conversions average all source channels then replicate
/// the result, which keeps analysis pipelines predictable.
pub fn AudioBuffer::remix(
self : AudioBuffer,
channels : Int32,
) -> AudioBuffer raise AudioError {
if channels <= 0 {
raise OutOfRange("channel count must be positive, got \{channels}")
}
if channels == self.channels {
return self.copy()
}
let frames = self.frames()
let data : Array[Double] = Array::make(frames * channels, 0.0)
if self.channels == 1 && channels == 2 {
for frame = 0; frame < frames; frame = frame + 1 {
let value = self.data[frame]
data[frame * 2] = value
data[frame * 2 + 1] = value
}
} else if self.channels == 2 && channels == 1 {
for frame = 0; frame < frames; frame = frame + 1 {
data[frame] = (self.data[frame * 2] + self.data[frame * 2 + 1]) * 0.5
}
} else {
for frame = 0; frame < frames; frame = frame + 1 {
let mut sum = 0.0
for channel = 0; channel < self.channels; channel = channel + 1 {
sum += self.data[frame * self.channels + channel]
}
let value = sum / self.channels.to_double()
for channel = 0; channel < channels; channel = channel + 1 {
data[frame * channels + channel] = value
}
}
}
AudioBuffer::{ sample_rate: self.sample_rate, channels, data, }
}
///|
/// Linear interpolation between `a` and `b`.
pub fn lerp(a : Double, b : Double, t : Double) -> Double {
a + (b - a) * t
}