// moonaudio — pure MoonBit audio decoding and DSP toolkit.
// Copyright 2026 Sakuraqwq1
// Licensed under the Apache License, Version 2.0.
///|
/// A guided tour of the library: synthesis, analysis, filtering, resampling and
/// container roundtrips, each printed as one section.
///
/// Run with `moon run examples/tour`.
fn main {
synthesis()
analysis()
filtering()
resampling()
containers()
}
///|
/// Build a chord (440 Hz + 660 Hz) and report its levels.
fn synthesis() -> Unit {
section("synthesis")
let a = @dsp.tone(440.0, 44100, 22050, amplitude=0.4) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
let b = @dsp.tone(660.0, 44100, 22050, amplitude=0.3) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
let chord = @dsp.mix(a, b) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
println(" tone 440 Hz : \{a.frames()} frames, peak \{round3(a.peak())}")
println(" tone 660 Hz : \{b.frames()} frames, peak \{round3(b.peak())}")
println(
" mixed : peak \{round3(chord.peak())}, rms \{round3(chord.rms())}",
)
let noise = @dsp.white_noise(22050, 44100, 42L, amplitude=0.2)
println(" noise seed 42: peak \{round3(noise.peak())}")
let repeat = @dsp.white_noise(22050, 44100, 42L, amplitude=0.2)
println(" deterministic: \{noise.data[0] == repeat.data[0]}")
}
///|
/// FFT peak detection, spectrogram and statistics on the same tone.
fn analysis() -> Unit {
section("analysis")
let tone = @dsp.tone(1000.0, 8000, 8000, amplitude=0.8) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
let spectrum = @dsp.real_fft(pad_to(tone.data, 1024))
let magnitudes = @dsp.magnitude(spectrum, n=1024)
let mut best = 0.0
let mut best_index = 0
for index = 0; index < magnitudes.length(); index = index + 1 {
if magnitudes[index] > best {
best = magnitudes[index]
best_index = index
}
}
let peak_hz = best_index.to_double() * 8000.0 / 1024.0
println(
" fft peak at bin \{best_index}: \{round3(peak_hz)} Hz (input 1000 Hz)",
)
let spectrogram = @dsp.spectrogram(
tone,
fft_size=512,
hop=256,
window=@dsp.WindowKind::BlackmanHarris,
) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
let (frames, bins) = spectrogram.size()
println(
" spectrogram: \{frames} frames x \{bins} bins, \{round3(spectrogram.bin_hz())} Hz per bin",
)
println(" frame 0 peak: \{round3(spectrogram.peak_frequency(0))} Hz")
let stats = @dsp.analyse(tone)
println(
" stats lines: \{stats.to_lines().split("\n").count()} key=value entries",
)
println(" zero crossing rate: \{round3(stats.zero_crossing_rate)}")
}
///|
/// Low-pass a square-ish signal and show the level drop.
fn filtering() -> Unit {
section("filtering")
let noisy = @dsp.white_noise(2000, 8000, 7L, amplitude=0.5)
let low_pass = @dsp.Biquad::new(@dsp.LowPass, 8000, 500.0) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
let filtered = low_pass.apply_buffer(noisy)
println(" white noise peak : \{round3(noisy.peak())}")
println(" low passed peak : \{round3(filtered.peak())}")
let trimmed = @dsp.trim_silence(filtered, threshold=0.01)
println(" after trim : \{trimmed.frames()} frames")
let faded = @dsp.fade_out(trimmed, trimmed.frames() / 2)
println(" fade out tail : \{round3(faded.data[faded.data.length() - 1])}")
}
///|
/// Resample a tone and confirm the frequency survives.
fn resampling() -> Unit {
section("resampling")
let tone = @dsp.tone(440.0, 44100, 4410, amplitude=0.5) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
let up = @resample.resample(tone, 48000) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
let (num, den) = @resample.reduce_ratio(44100, 48000)
println(" ratio 44100:48000 reduces to \{num}:\{den}")
println(
" expected frames: \{@resample.sample_count(tone.frames(), 44100, 48000)}",
)
println(" actual frames : \{up.frames()}")
println(" peak preserved : \{round3(up.peak())}")
let back = @resample.resample(up, 44100) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
println(" roundtrip frames: \{back.frames()}")
}
///|
/// Encode the same signal as WAV, AIFF and back through the facade.
fn containers() -> Unit {
section("containers")
let tone = @dsp.tone(440.0, 22050, 2205, amplitude=0.6) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
let wav_bytes = @wav.encode(tone) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
let aiff_bytes = @aiff.encode(tone, format=@audio.I24) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
for bytes in [wav_bytes, aiff_bytes] {
let container = match @moonaudio.sniff(bytes) {
Some(container) => container.to_name()
None => "unknown"
}
let summary = @moonaudio.describe(bytes) catch {
(error : @audio.AudioError) => {
fail(error)
return
}
}
println(" \{container}: \{bytes.length()} bytes — \{summary}")
}
}
///|
/// Print a section header.
fn section(title : String) -> Unit {
println("")
println("== \{title} ==")
}
///|
/// Print an error in the tour format.
fn fail(error : @audio.AudioError) -> Unit {
println(" error: \{error.message()}")
}
///|
/// Round to three decimals for readable output.
fn round3(value : Double) -> String {
let scaled = (value * 1000.0).round().to_int()
(scaled.to_double() / 1000.0).to_string()
}
///|
/// Copy `data` into a fixed size array, zero padding the tail.
fn pad_to(data : Array[Double], size : Int32) -> Array[Double] {
let out : Array[Double] = Array::make(size, 0.0)
let count = if data.length() < size { data.length() } else { size }
for index = 0; index < count; index = index + 1 {
out[index] = data[index]
}
out
}