Skip to main content

khora_io/asset/decoders/audio/
symphonia.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Universal audio decoder using `symphonia`.
16
17use anyhow::{anyhow, Result};
18use khora_data::assets::SoundData;
19use std::{error::Error, io::Cursor};
20use symphonia::core::{
21    audio::SampleBuffer, codecs::DecoderOptions, formats::FormatOptions, io::MediaSourceStream,
22    meta::MetadataOptions, probe::Hint,
23};
24
25use crate::asset::AssetDecoder;
26
27/// Decodes multiple audio formats via `symphonia`.
28#[derive(Default)]
29pub struct SymphoniaDecoder;
30
31impl SymphoniaDecoder {
32    /// Creates a new instance of `SymphoniaDecoder`.
33    pub fn new() -> Self {
34        Self
35    }
36}
37
38impl AssetDecoder<SoundData> for SymphoniaDecoder {
39    fn load(&self, bytes: &[u8]) -> Result<SoundData, Box<dyn Error + Send + Sync>> {
40        let mss = MediaSourceStream::new(Box::new(Cursor::new(bytes.to_vec())), Default::default());
41
42        let hint = Hint::new();
43        let meta_opts: MetadataOptions = Default::default();
44        let fmt_opts: FormatOptions = Default::default();
45        let probed = symphonia::default::get_probe().format(&hint, mss, &fmt_opts, &meta_opts)?;
46        let mut format_reader = probed.format;
47
48        let track = format_reader
49            .default_track()
50            .ok_or_else(|| anyhow!("No default audio track found"))?;
51
52        let track_id = track.id;
53        let sample_rate = track
54            .codec_params
55            .sample_rate
56            .ok_or_else(|| anyhow!("Unknown sample rate"))?;
57        let channels = track
58            .codec_params
59            .channels
60            .ok_or_else(|| anyhow!("Unknown channel count"))?;
61
62        let dec_opts: DecoderOptions = Default::default();
63        let mut decoder = symphonia::default::get_codecs().make(&track.codec_params, &dec_opts)?;
64
65        let mut all_samples = Vec::<f32>::new();
66        // A corrupt stream can fail to decode on every packet; warn once on the
67        // first failure (with debug! for the rest) and summarise the total after
68        // the loop so a bad file cannot flood the log.
69        let mut decode_errors: u64 = 0;
70
71        loop {
72            match format_reader.next_packet() {
73                Ok(packet) => {
74                    if packet.track_id() != track_id {
75                        continue;
76                    }
77
78                    match decoder.decode(&packet) {
79                        Ok(decoded) => {
80                            let mut sample_buf = SampleBuffer::<f32>::new(
81                                decoded.capacity() as u64,
82                                *decoded.spec(),
83                            );
84                            sample_buf.copy_interleaved_ref(decoded);
85                            all_samples.extend_from_slice(sample_buf.samples());
86                        }
87                        Err(e) => {
88                            if decode_errors == 0 {
89                                log::warn!("Audio packet decode error: {e}");
90                            } else {
91                                log::debug!("Audio packet decode error: {e}");
92                            }
93                            decode_errors += 1;
94                        }
95                    }
96                }
97                Err(symphonia::core::errors::Error::IoError(_)) => {
98                    break;
99                }
100                Err(e) => {
101                    return Err(Box::new(e));
102                }
103            }
104        }
105
106        if decode_errors > 1 {
107            log::warn!("Audio decoding skipped {decode_errors} corrupt packets in total");
108        }
109
110        Ok(SoundData {
111            samples: all_samples,
112            channels: channels.count() as u16,
113            sample_rate,
114        })
115    }
116}