///|
pub(all) struct FrameRange {
start_frame : Int
frame_count : Int
} derive(Eq, @debug.Debug)
///|
pub fn FrameRange::new(start_frame : Int, frame_count : Int) -> FrameRange {
{ start_frame, frame_count }
}
///|
pub fn FrameRange::end_frame(self : FrameRange) -> Int {
self.start_frame + self.frame_count
}
///|
pub fn FrameRange::is_valid(self : FrameRange) -> Bool {
self.start_frame >= 0 && self.frame_count >= 0
}
///|
pub(all) struct WaveformPoint {
index : Int
start_frame : Int
end_frame : Int
min : Double
max : Double
rms : Double
} derive(Eq, @debug.Debug)
///|
pub fn WaveformPoint::empty(index : Int) -> WaveformPoint {
{ index, start_frame: 0, end_frame: 0, min: 0.0, max: 0.0, rms: 0.0 }
}
///|
pub(all) struct AmplitudeBin {
index : Int
lower : Double
upper : Double
count : Int
} derive(Eq, @debug.Debug)
///|
pub fn AmplitudeBin::empty(index : Int) -> AmplitudeBin {
{ index, lower: 0.0, upper: 0.0, count: 0 }
}
///|
pub(all) struct AmplitudeHistogram {
bins : Array[AmplitudeBin]
sample_count : Int
bin_count : Int
} derive(Eq, @debug.Debug)
///|
pub fn AmplitudeHistogram::empty() -> AmplitudeHistogram {
{ bins: [], sample_count: 0, bin_count: 0 }
}
///|
pub fn AmplitudeHistogram::is_valid(self : AmplitudeHistogram) -> Bool {
self.bin_count == self.bins.length() && self.sample_count >= 0
}
///|
pub(all) struct BufferMixResult {
ok : Bool
mixed_frames : Int
buffer : FloatBuffer
error : WavError
} derive(Eq, @debug.Debug)
///|
pub fn BufferMixResult::failure(error : WavError) -> BufferMixResult {
{ ok: false, mixed_frames: 0, buffer: FloatBuffer::empty(), error }
}
///|
pub fn BufferMixResult::success(buffer : FloatBuffer) -> BufferMixResult {
{
ok: true,
mixed_frames: buffer.frame_count(),
buffer,
error: WavError::none(),
}
}
///|
pub(all) struct LoopResult {
ok : Bool
repeats : Int
source_range : FrameRange
buffer : FloatBuffer
error : WavError
} derive(Eq, @debug.Debug)
///|
pub fn LoopResult::failure(error : WavError) -> LoopResult {
{
ok: false,
repeats: 0,
source_range: FrameRange::new(0, 0),
buffer: FloatBuffer::empty(),
error,
}
}
///|
pub fn LoopResult::success(
repeats : Int,
source_range : FrameRange,
buffer : FloatBuffer,
) -> LoopResult {
{ ok: true, repeats, source_range, buffer, error: WavError::none() }
}
///|
fn compatible_format(a : FloatBuffer, b : FloatBuffer) -> Bool {
a.is_valid() &&
b.is_valid() &&
a.channels == b.channels &&
a.sample_rate == b.sample_rate
}
///|
fn frame_range_in_buffer(buffer : FloatBuffer, range : FrameRange) -> Bool {
range.is_valid() && range.end_frame() <= buffer.frame_count()
}
///|
pub fn frame_slice(
buffer : FloatBuffer,
start_frame : Int,
frame_count : Int,
) -> FloatBuffer {
let range = FrameRange::new(start_frame, frame_count)
if !buffer.is_valid() || !frame_range_in_buffer(buffer, range) {
FloatBuffer::empty()
} else {
FloatBuffer::new(
buffer.channels,
buffer.sample_rate,
copy_frames(buffer, start_frame, start_frame + frame_count),
)
}
}
///|
pub fn slice_seconds(
buffer : FloatBuffer,
start_seconds : Double,
duration_seconds : Double,
) -> FloatBuffer {
if !buffer.is_valid() || start_seconds < 0.0 || duration_seconds < 0.0 {
FloatBuffer::empty()
} else {
let start_frame = rounded_to_int(
start_seconds * buffer.sample_rate.to_double(),
)
let requested_frames = rounded_to_int(
duration_seconds * buffer.sample_rate.to_double(),
)
let available = buffer.frame_count() - start_frame
let frames = if requested_frames < available {
requested_frames
} else {
available
}
if frames < 0 {
FloatBuffer::empty()
} else {
frame_slice(buffer, start_frame, frames)
}
}
}
///|
pub fn append_buffers(a : FloatBuffer, b : FloatBuffer) -> BufferMixResult {
if !compatible_format(a, b) {
BufferMixResult::failure(
WavError::new(
ErrorInvalidArgument,
"buffers must share channels and sample rate",
),
)
} else {
let samples : Array[Double] = []
for sample in a.samples {
samples.push(sample)
}
for sample in b.samples {
samples.push(sample)
}
BufferMixResult::success(
FloatBuffer::new(a.channels, a.sample_rate, samples),
)
}
}
///|
pub fn pad_buffer(
buffer : FloatBuffer,
left_frames? : Int = 0,
right_frames? : Int = 0,
) -> FloatBuffer {
if !buffer.is_valid() || left_frames < 0 || right_frames < 0 {
FloatBuffer::empty()
} else {
let total = (left_frames + buffer.frame_count() + right_frames) *
buffer.channels
let source_start = left_frames * buffer.channels
let samples = Array::makei(total, i => {
if i < source_start || i >= source_start + buffer.samples.length() {
0.0
} else {
buffer.samples[i - source_start]
}
})
FloatBuffer::new(buffer.channels, buffer.sample_rate, samples)
}
}
///|
pub fn mix_buffers(
a : FloatBuffer,
b : FloatBuffer,
gain_a? : Double = 1.0,
gain_b? : Double = 1.0,
) -> BufferMixResult {
if !compatible_format(a, b) {
BufferMixResult::failure(
WavError::new(
ErrorInvalidArgument,
"buffers must share channels and sample rate",
),
)
} else {
let sample_count = if a.samples.length() < b.samples.length() {
a.samples.length()
} else {
b.samples.length()
}
let samples = Array::makei(sample_count, i => {
clamp_range(a.samples[i] * gain_a + b.samples[i] * gain_b, -1.0, 1.0)
})
BufferMixResult::success(
FloatBuffer::new(a.channels, a.sample_rate, samples),
)
}
}
///|
pub fn loop_region(
buffer : FloatBuffer,
start_frame : Int,
frame_count : Int,
repeats : Int,
) -> LoopResult {
let range = FrameRange::new(start_frame, frame_count)
if !buffer.is_valid() || repeats <= 0 || !frame_range_in_buffer(buffer, range) {
LoopResult::failure(
WavError::new(ErrorInvalidArgument, "invalid loop range"),
)
} else {
let slice = frame_slice(buffer, start_frame, frame_count)
let samples : Array[Double] = []
for _ in 0.. Array[WaveformPoint] {
if !buffer.is_valid() || points <= 0 || buffer.frame_count() == 0 {
[]
} else {
let frames = buffer.frame_count()
let safe_points = if points > frames { frames } else { points }
Array::makei(safe_points, point => {
let start = point * frames / safe_points
let end = (point + 1) * frames / safe_points
let real_end = if end <= start { start + 1 } else { end }
let first = buffer.samples[start * buffer.channels]
let min_value = for
i in (start * buffer.channels)..<(real_end * buffer.channels)
value = first {
continue min_double(value, buffer.samples[i])
} nobreak {
value
}
let max_value = for
i in (start * buffer.channels)..<(real_end * buffer.channels)
value = first {
continue max_double(value, buffer.samples[i])
} nobreak {
value
}
let sum_sq = for
i in (start * buffer.channels)..<(real_end * buffer.channels)
value = 0.0 {
let sample = buffer.samples[i]
continue value + sample * sample
} nobreak {
value
}
let count = (real_end - start) * buffer.channels
{
index: point,
start_frame: start,
end_frame: real_end,
min: min_value,
max: max_value,
rms: (sum_sq / count.to_double()).sqrt(),
}
})
}
}
///|
fn histogram_index(value : Double, bins : Int) -> Int {
let normalized = (clamp_range(value, -1.0, 1.0) + 1.0) / 2.0
let raw = (normalized * bins.to_double()).floor().to_int()
if raw < 0 {
0
} else if raw >= bins {
bins - 1
} else {
raw
}
}
///|
pub fn amplitude_histogram(
buffer : FloatBuffer,
bins : Int,
) -> AmplitudeHistogram {
if !buffer.is_valid() || bins <= 0 {
AmplitudeHistogram::empty()
} else {
let counts = Array::makei(bins, _ => 0)
for sample in buffer.samples {
let index = histogram_index(sample, bins)
counts[index] = counts[index] + 1
}
let items = Array::makei(bins, index => {
let lower = -1.0 + 2.0 * index.to_double() / bins.to_double()
let upper = -1.0 + 2.0 * (index + 1).to_double() / bins.to_double()
{ index, lower, upper, count: counts[index] }
})
{ bins: items, sample_count: buffer.samples.length(), bin_count: bins }
}
}
///|
fn bar_for_peak(value : Double, width : Int) -> String {
let filled = rounded_to_int(clamp_range(value, 0.0, 1.0) * width.to_double())
for i in 0.. String {
if !buffer.is_valid() || points <= 0 || width <= 0 {
"MoonWavKit waveform: empty"
} else {
let pts = waveform_points(buffer, points)
for point in pts; text = "MoonWavKit waveform" {
let peak = max_double(abs_double(point.min), abs_double(point.max))
continue "\{text}\n\{point.index}: \{bar_for_peak(peak, width)} peak=\{peak}"
} nobreak {
text
}
}
}