// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
pub suberror DecoderError {
Backend(@decoder.DecoderError)
UnrecognizedFormat
} derive(Show, Eq)
///|
pub suberror PlayError {
DecoderError(DecoderError)
} derive(Show, Eq)
///|
enum DecoderKind {
Wav
Flac
Vorbis
Mp3
Mp4a
} derive(Show, Eq)
///|
pub struct Decoder {
inner : @decoder.DecodedSamples
seekable : Bool
allow_backward_seek : Bool
kind : DecoderKind
}
///|
pub struct Settings {
byte_len : Int?
coarse_seek : Bool
gapless : Bool
hint : String?
mime_type : String?
is_seekable : Bool
} derive(Show, Eq)
///|
pub fn Settings::default() -> Settings {
{
byte_len: None,
coarse_seek: false,
gapless: true,
hint: None,
mime_type: None,
is_seekable: false,
}
}
///|
pub struct DecoderBuilder {
data : Bytes?
settings : Settings
}
///|
pub struct Reader {
bytes : Bytes
}
///|
pub fn Reader::from_bytes(bytes : Bytes) -> Reader {
{ bytes, }
}
///|
pub fn Reader::from_file(path : StringView) -> Reader raise PlayError {
let bytes = try @fs.read_file_to_bytes(path.to_string()) catch {
_ => raise PlayError::DecoderError(DecoderError::UnrecognizedFormat)
} noraise {
bs => bs
}
{ bytes, }
}
///|
pub fn Reader::into_bytes(self : Reader) -> Bytes {
self.bytes
}
///|
pub fn DecoderBuilder::new() -> DecoderBuilder {
{ data: None, settings: Settings::default() }
}
///|
pub fn DecoderBuilder::with_data(
self : DecoderBuilder,
data : Bytes,
) -> DecoderBuilder {
{ ..self, data: Some(data) }
}
///|
pub fn DecoderBuilder::with_byte_len(
self : DecoderBuilder,
byte_len : Int,
) -> DecoderBuilder {
guard byte_len >= 0 else { panic() }
{
..self,
settings: { ..self.settings, byte_len: Some(byte_len), is_seekable: true },
}
}
///|
pub fn DecoderBuilder::with_coarse_seek(
self : DecoderBuilder,
coarse_seek : Bool,
) -> DecoderBuilder {
{ ..self, settings: { ..self.settings, coarse_seek, } }
}
///|
pub fn DecoderBuilder::with_gapless(
self : DecoderBuilder,
gapless : Bool,
) -> DecoderBuilder {
{ ..self, settings: { ..self.settings, gapless, } }
}
///|
pub fn DecoderBuilder::with_hint(
self : DecoderBuilder,
hint : StringView,
) -> DecoderBuilder {
{ ..self, settings: { ..self.settings, hint: Some(hint.to_string()) } }
}
///|
pub fn DecoderBuilder::with_mime_type(
self : DecoderBuilder,
mime_type : StringView,
) -> DecoderBuilder {
{
..self,
settings: { ..self.settings, mime_type: Some(mime_type.to_string()) },
}
}
///|
pub fn DecoderBuilder::with_seekable(
self : DecoderBuilder,
is_seekable : Bool,
) -> DecoderBuilder {
{ ..self, settings: { ..self.settings, is_seekable, } }
}
///|
pub struct LoopedDecoder {
channels : ChannelCount
sample_rate : SampleRate
samples : Array[Sample]
cursor : Ref[Int]
seekable : Bool
}
///|
fn hinted_decode(bytes : Bytes, hint : String) -> Decoder? {
if hint.contains("wav") || hint.contains("WAV") {
return Some(decode_wav_or_raise(bytes)) catch { _ => None }
}
if hint.contains("m4a") ||
hint.contains("M4A") ||
hint.contains("mp4") ||
hint.contains("MP4") ||
hint.contains("aac") ||
hint.contains("AAC") {
return Some(decode_mp4a_or_raise(bytes)) catch { _ => None }
}
if hint.contains("flac") || hint.contains("FLAC") {
return Some(decode_flac_or_raise(bytes)) catch { _ => None }
}
if hint.contains("ogg") ||
hint.contains("OGG") ||
hint.contains("vorbis") ||
hint.contains("VORBIS") {
return Some(decode_vorbis_or_raise(bytes)) catch { _ => None }
}
if hint.contains("mp3") ||
hint.contains("MP3") ||
hint.contains("mpeg") ||
hint.contains("MPEG") {
return Some(decode_mp3_or_raise(bytes)) catch { _ => None }
}
None
}
///|
fn DecoderBuilder::decoder_builder_build(
self : DecoderBuilder,
) -> Decoder raise DecoderError {
let data = match self.data {
None => raise DecoderError::UnrecognizedFormat
Some(data) => data
}
let hinted = match self.settings.hint {
None => None
Some(hint) => hinted_decode(data, hint)
}
let decoded = match hinted {
Some(decoder) => decoder
None => {
let mime_hint = match self.settings.mime_type {
None => None
Some(hint) => hinted_decode(data, hint)
}
match mime_hint {
Some(decoder) => decoder
None => Decoder::new(data)
}
}
}
let allow_backward_seek = if !self.settings.is_seekable {
false
} else {
match decoded.kind {
DecoderKind::Mp3 => self.settings.byte_len is Some(_)
_ => true
}
}
{ ..decoded, seekable: self.settings.is_seekable, allow_backward_seek }
}
///|
pub fn DecoderBuilder::build(
self : DecoderBuilder,
) -> Decoder raise DecoderError {
self.decoder_builder_build()
}
///|
fn to_looped_decoder(decoder : Decoder) -> LoopedDecoder {
let channels = decoder.channels()
let sample_rate = decoder.sample_rate()
let seekable = decoder.seekable
let samples : Array[Sample] = []
while true {
match decoder.next() {
None => break
Some(s) => samples.push(s)
}
}
{ channels, sample_rate, samples, cursor: @ref.new(0), seekable }
}
///|
pub fn DecoderBuilder::build_looped(
self : DecoderBuilder,
) -> LoopedDecoder raise DecoderError {
to_looped_decoder(self.decoder_builder_build())
}
///|
pub fn LoopedDecoder::next(self : LoopedDecoder) -> Sample? {
if self.samples.is_empty() {
None
} else {
let idx = self.cursor.val
let value = self.samples[idx]
self.cursor.val = (idx + 1) % self.samples.length()
Some(value)
}
}
///|
pub fn LoopedDecoder::channels(self : LoopedDecoder) -> ChannelCount {
self.channels
}
///|
pub fn LoopedDecoder::sample_rate(self : LoopedDecoder) -> SampleRate {
self.sample_rate
}
///|
pub impl Source for LoopedDecoder with next(self : LoopedDecoder) {
self.next()
}
///|
pub impl Source for LoopedDecoder with channels(self : LoopedDecoder) {
self.channels()
}
///|
pub impl Source for LoopedDecoder with sample_rate(self : LoopedDecoder) {
self.sample_rate()
}
///|
pub impl Source for LoopedDecoder with current_span_len(self : LoopedDecoder) {
if self.samples.is_empty() {
None
} else {
Some(self.samples.length() - self.cursor.val)
}
}
///|
pub impl Source for LoopedDecoder with total_duration(self : LoopedDecoder) {
ignore(self)
None
}
///|
pub impl Source for LoopedDecoder with try_seek(
self : LoopedDecoder,
pos : @moon_cpal.Duration,
) -> Unit raise SeekError {
if !self.seekable {
raise SeekError::NotSupported
}
if self.samples.is_empty() {
raise SeekError::NotSupported
}
let max_index = self.samples.length() - 1
let target = sample_index_from_duration(
pos,
self.channels(),
self.sample_rate(),
)
let clamped = if target < 0 {
0
} else if target > max_index {
max_index
} else {
target
}
self.cursor.val = clamped
}
///|
fn bytes_eq4(
bytes : Bytes,
offset : Int,
b0 : Int,
b1 : Int,
b2 : Int,
b3 : Int,
) -> Bool {
offset + 4 <= bytes.length() &&
bytes[offset].to_int() == b0 &&
bytes[offset + 1].to_int() == b1 &&
bytes[offset + 2].to_int() == b2 &&
bytes[offset + 3].to_int() == b3
}
///|
fn looks_like_wav(bytes : Bytes) -> Bool {
bytes.length() >= 12 &&
bytes_eq4(bytes, 0, 0x52, 0x49, 0x46, 0x46) &&
bytes_eq4(bytes, 8, 0x57, 0x41, 0x56, 0x45)
}
///|
fn looks_like_flac(bytes : Bytes) -> Bool {
bytes_eq4(bytes, 0, 0x66, 0x4c, 0x61, 0x43)
}
///|
fn looks_like_ogg(bytes : Bytes) -> Bool {
bytes_eq4(bytes, 0, 0x4f, 0x67, 0x67, 0x53)
}
///|
fn looks_like_mp3(bytes : Bytes) -> Bool {
if bytes.length() >= 3 &&
bytes[0].to_int() == 0x49 &&
bytes[1].to_int() == 0x44 &&
bytes[2].to_int() == 0x33 {
return true
}
if bytes.length() >= 2 {
let b0 = bytes[0].to_int()
let b1 = bytes[1].to_int()
return b0 == 0xff && (b1 & 0xe0) == 0xe0
}
false
}
///|
fn looks_like_mp4a(bytes : Bytes) -> Bool {
bytes.length() >= 12 && bytes_eq4(bytes, 4, 0x66, 0x74, 0x79, 0x70)
}
///|
fn decode_wav_or_raise(bytes : Bytes) -> Decoder raise DecoderError {
let decoded = try @decoder.decode_wav_bytes(bytes) catch {
err => raise DecoderError::Backend(err)
} noraise {
src => src
}
{
inner: decoded,
seekable: true,
allow_backward_seek: true,
kind: DecoderKind::Wav,
}
}
///|
fn decode_flac_or_raise(bytes : Bytes) -> Decoder raise DecoderError {
let decoded = try @decoder.decode_flac_bytes(bytes) catch {
err => raise DecoderError::Backend(err)
} noraise {
src => src
}
{
inner: decoded,
seekable: true,
allow_backward_seek: true,
kind: DecoderKind::Flac,
}
}
///|
fn decode_vorbis_or_raise(bytes : Bytes) -> Decoder raise DecoderError {
let decoded = try @decoder.decode_vorbis_bytes(bytes) catch {
err => raise DecoderError::Backend(err)
} noraise {
src => src
}
{
inner: decoded,
seekable: true,
allow_backward_seek: true,
kind: DecoderKind::Vorbis,
}
}
///|
fn decode_mp3_or_raise(bytes : Bytes) -> Decoder raise DecoderError {
let decoded = try @decoder.decode_mp3_bytes(bytes) catch {
err => raise DecoderError::Backend(err)
} noraise {
src => src
}
{
inner: decoded,
seekable: true,
allow_backward_seek: true,
kind: DecoderKind::Mp3,
}
}
///|
fn decode_mp4a_or_raise(bytes : Bytes) -> Decoder raise DecoderError {
let decoded = try @decoder.decode_mp4a_bytes(bytes) catch {
err => raise DecoderError::Backend(err)
} noraise {
src => src
}
{
inner: decoded,
seekable: true,
allow_backward_seek: true,
kind: DecoderKind::Mp4a,
}
}
///|
pub fn Decoder::new(bytes : Bytes) -> Decoder raise DecoderError {
guard bytes.length() > 0 else { raise DecoderError::UnrecognizedFormat }
if looks_like_wav(bytes) {
return decode_wav_or_raise(bytes)
}
if looks_like_mp4a(bytes) {
return decode_mp4a_or_raise(bytes)
}
if looks_like_flac(bytes) {
return decode_flac_or_raise(bytes)
}
if looks_like_ogg(bytes) {
return decode_vorbis_or_raise(bytes)
}
if looks_like_mp3(bytes) {
return decode_mp3_or_raise(bytes)
}
let try_wav = Some(decode_wav_or_raise(bytes)) catch { _ => None }
match try_wav {
Some(decoder) => return decoder
None => ()
}
let try_flac = Some(decode_flac_or_raise(bytes)) catch { _ => None }
match try_flac {
Some(decoder) => return decoder
None => ()
}
let try_vorbis = Some(decode_vorbis_or_raise(bytes)) catch { _ => None }
match try_vorbis {
Some(decoder) => return decoder
None => ()
}
let try_mp3 = Some(decode_mp3_or_raise(bytes)) catch { _ => None }
match try_mp3 {
Some(decoder) => return decoder
None => ()
}
let try_mp4a = Some(decode_mp4a_or_raise(bytes)) catch { _ => None }
match try_mp4a {
Some(decoder) => return decoder
None => raise DecoderError::UnrecognizedFormat
}
}
///|
pub fn Decoder::builder() -> DecoderBuilder {
DecoderBuilder::new()
}
///|
pub fn Decoder::new_looped(bytes : Bytes) -> LoopedDecoder raise DecoderError {
Decoder::builder().with_data(bytes).build_looped()
}
///|
pub fn Decoder::try_from_file(path : StringView) -> Decoder raise DecoderError {
let bytes = try @fs.read_file_to_bytes(path.to_string()) catch {
_ => raise DecoderError::UnrecognizedFormat
} noraise {
bs => bs
}
Decoder::new(bytes)
}
///|
pub fn Decoder::try_from_file_looped(
path : StringView,
) -> LoopedDecoder raise DecoderError {
let bytes = try @fs.read_file_to_bytes(path.to_string()) catch {
_ => raise DecoderError::UnrecognizedFormat
} noraise {
bs => bs
}
Decoder::new_looped(bytes)
}
///|
pub fn Decoder::new_wav(bytes : Bytes) -> Decoder raise DecoderError {
decode_wav_or_raise(bytes)
}
///|
pub fn Decoder::new_flac(bytes : Bytes) -> Decoder raise DecoderError {
decode_flac_or_raise(bytes)
}
///|
pub fn Decoder::new_vorbis(bytes : Bytes) -> Decoder raise DecoderError {
decode_vorbis_or_raise(bytes)
}
///|
pub fn Decoder::new_mp3(bytes : Bytes) -> Decoder raise DecoderError {
decode_mp3_or_raise(bytes)
}
///|
pub fn Decoder::new_mp4a(bytes : Bytes) -> Decoder raise DecoderError {
decode_mp4a_or_raise(bytes)
}
///|
pub fn Decoder::new_aac(bytes : Bytes) -> Decoder raise DecoderError {
Decoder::new_mp4a(bytes)
}
///|
pub fn Decoder::new_mp4(bytes : Bytes) -> Decoder raise DecoderError {
Decoder::new_mp4a(bytes)
}
///|
pub fn Decoder::next(self : Decoder) -> Sample? {
self.inner.next()
}
///|
pub fn Decoder::channels(self : Decoder) -> ChannelCount {
self.inner.channels()
}
///|
pub fn Decoder::sample_rate(self : Decoder) -> SampleRate {
self.inner.sample_rate()
}
///|
pub impl Source for Decoder with next(self : Decoder) {
self.next()
}
///|
pub impl Source for Decoder with channels(self : Decoder) {
self.channels()
}
///|
pub impl Source for Decoder with sample_rate(self : Decoder) {
self.sample_rate()
}
///|
pub impl Source for Decoder with current_span_len(self : Decoder) {
let remaining = self.inner.len() - self.inner.position()
if remaining <= 0 {
Some(0)
} else {
Some(remaining)
}
}
///|
pub impl Source for Decoder with total_duration(self : Decoder) {
duration_from_sample_count(
self.inner.len(),
self.channels(),
self.sample_rate(),
)
}
///|
pub impl Source for Decoder with try_seek(
self : Decoder,
pos : @moon_cpal.Duration,
) -> Unit raise SeekError {
if !self.seekable {
raise SeekError::NotSupported
}
let channel_count = self.channels()
let current_channel = if channel_count > 0 {
self.inner.position() % channel_count
} else {
0
}
let target = sample_index_from_duration(
pos,
channel_count,
self.sample_rate(),
)
let clamped_target = if target < 0 {
0
} else if target > self.inner.len() {
self.inner.len()
} else {
target
}
let aligned_target = if channel_count > 0 {
let rem = clamped_target % channel_count
if rem == 0 {
clamped_target
} else {
clamped_target + channel_count - rem
}
} else {
clamped_target
}
let with_channel_offset = if aligned_target >= current_channel {
aligned_target - current_channel
} else {
0
}
let final_target = if with_channel_offset > self.inner.len() {
self.inner.len()
} else {
with_channel_offset
}
if !self.allow_backward_seek && final_target < self.inner.position() {
raise SeekError::NotSupported
}
self.inner.seek_to(final_target)
}
///|
pub fn play(mixer : Mixer, reader : Reader) -> Player raise PlayError {
play_bytes(mixer, reader.into_bytes())
}
///|
pub fn play_bytes(mixer : Mixer, bytes : Bytes) -> Player raise PlayError {
let source = try Decoder::new(bytes) catch {
err => raise PlayError::DecoderError(err)
} noraise {
src => src
}
let player = Player::connect_new(mixer)
player.append(source)
player
}
///|
pub fn play_file(mixer : Mixer, path : StringView) -> Player raise PlayError {
let reader = Reader::from_file(path)
play(mixer, reader)
}
///|
pub fn play_reader(mixer : Mixer, reader : Reader) -> Player raise PlayError {
play(mixer, reader)
}