Skip to main content

shuyi_assets/audio/
formats.rs

1//! The audio kinds and formats (ADR-0007): a sound file's header, a music
2//! track's, and the manifest. Only headers are read; the platform decodes
3//! the samples when it plays them.
4use crate::prelude::*;
5use serde::{Deserialize, Serialize};
6
7use alloc::borrow::Cow;
8
9use super::{
10    AudioManifest, Files, NO_LENGTH, from_source, ogg_length_ms, playable_problem, vorbis_format,
11    wav_format,
12};
13use crate::error::ContentError;
14use crate::format::{AssetKind, AssetPath, DecodeCx, Format, TEXT_MAX_BYTES, join, text};
15
16/// The most bytes a sound or music file may have: 64 MiB, six minutes of
17/// stereo WAV at [`SAMPLE_RATE`](super::SAMPLE_RATE).
18pub const MAX_SOUND_BYTES: usize = 64 << 20;
19
20/// A sound file's header: what the engine knows of a sound before the
21/// platform plays it. Every one is mono or stereo, at
22/// [`SAMPLE_RATE`](super::SAMPLE_RATE).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub struct Sound {
25    /// 1 or 2.
26    pub channels: u16,
27    /// Samples a second.
28    pub sample_rate: u32,
29}
30
31impl AssetKind for Sound {
32    const NAME: &'static str = "sound";
33}
34
35/// A music track's header, and how long it plays.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37pub struct MusicTrack {
38    /// 1 or 2.
39    pub channels: u16,
40    /// Samples a second.
41    pub sample_rate: u32,
42    /// How long it plays, in milliseconds (to the nearest).
43    pub length_ms: u32,
44}
45
46impl AssetKind for MusicTrack {
47    const NAME: &'static str = "music track";
48}
49
50/// A sound of `(channels, rate)`, from the format of a file `file` that
51/// isn't one if `format` is `None` (then `not_one` says so), or every
52/// problem with it.
53fn sound(
54    file: &AssetPath,
55    format: Option<(u16, u32)>,
56    not_one: &str,
57) -> Result<Sound, Vec<ContentError>> {
58    let error = |message: String| vec![ContentError::new(file.shown(), message)];
59    let (channels, sample_rate) = format.ok_or_else(|| error(not_one.to_owned()))?;
60    match playable_problem(channels, sample_rate) {
61        Some(problem) => Err(error(problem)),
62        None => Ok(Sound {
63            channels,
64            sample_rate,
65        }),
66    }
67}
68
69/// WAV, PCM or float (`wav`, `.wav`).
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
71pub struct Wav;
72
73impl Format for Wav {
74    type Asset = Sound;
75
76    fn id(&self) -> &'static str {
77        "wav"
78    }
79
80    fn extensions(&self) -> &'static [&'static str] {
81        &["wav"]
82    }
83
84    fn sniff(&self, bytes: &[u8]) -> bool {
85        bytes.get(..4) == Some(b"RIFF") && bytes.get(8..12) == Some(b"WAVE")
86    }
87
88    fn max_bytes(&self) -> usize {
89        MAX_SOUND_BYTES
90    }
91
92    fn decode(
93        &self,
94        file: &AssetPath,
95        bytes: &[u8],
96        _cx: &DecodeCx<'_>,
97    ) -> Result<Sound, Vec<ContentError>> {
98        sound(file, wav_format(bytes), "not a PCM WAV file")
99    }
100}
101
102/// The first four bytes of every Ogg page.
103const OGG_MAGIC: &[u8; 4] = b"OggS";
104
105/// OGG Vorbis (`ogg-vorbis`, `.ogg`), read for a sound.
106#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
107pub struct OggVorbis;
108
109impl Format for OggVorbis {
110    type Asset = Sound;
111
112    fn id(&self) -> &'static str {
113        "ogg-vorbis"
114    }
115
116    fn extensions(&self) -> &'static [&'static str] {
117        &["ogg"]
118    }
119
120    fn sniff(&self, bytes: &[u8]) -> bool {
121        bytes.starts_with(OGG_MAGIC)
122    }
123
124    fn max_bytes(&self) -> usize {
125        MAX_SOUND_BYTES
126    }
127
128    fn decode(
129        &self,
130        file: &AssetPath,
131        bytes: &[u8],
132        _cx: &DecodeCx<'_>,
133    ) -> Result<Sound, Vec<ContentError>> {
134        sound(file, vorbis_format(bytes), "not an OGG Vorbis file")
135    }
136}
137
138/// OGG Vorbis (`ogg-vorbis`, `.ogg`), read for a music track: a whole
139/// file, whose last page gives its length ([`ogg_length_ms`]).
140#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
141pub struct OggVorbisTrack;
142
143impl Format for OggVorbisTrack {
144    type Asset = MusicTrack;
145
146    fn id(&self) -> &'static str {
147        "ogg-vorbis"
148    }
149
150    fn extensions(&self) -> &'static [&'static str] {
151        &["ogg"]
152    }
153
154    fn sniff(&self, bytes: &[u8]) -> bool {
155        bytes.starts_with(OGG_MAGIC)
156    }
157
158    fn max_bytes(&self) -> usize {
159        MAX_SOUND_BYTES
160    }
161
162    fn decode(
163        &self,
164        file: &AssetPath,
165        bytes: &[u8],
166        cx: &DecodeCx<'_>,
167    ) -> Result<MusicTrack, Vec<ContentError>> {
168        let Sound {
169            channels,
170            sample_rate,
171        } = OggVorbis.decode(file, bytes, cx)?;
172        let length_ms =
173            ogg_length_ms(bytes).ok_or_else(|| vec![ContentError::new(file.shown(), NO_LENGTH)])?;
174        Ok(MusicTrack {
175            channels,
176            sample_rate,
177            length_ms,
178        })
179    }
180}
181
182impl AssetKind for AudioManifest {
183    const NAME: &'static str = "audio manifest";
184}
185
186/// The audio manifest (`audio-manifest`, `.ron`), checked as
187/// [`from_source`] says. Its sound files are named from its own folder and
188/// read from the bundle beside it (`audio/audio.ron` names `sfx/a.wav`,
189/// `audio/sfx/a.wav`). The music folder isn't in the bundle, so its tracks
190/// aren't read here: a tool that can see the folder checks them with
191/// [`from_source`].
192#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
193pub struct AudioManifestRon;
194
195impl Format for AudioManifestRon {
196    type Asset = AudioManifest;
197
198    fn id(&self) -> &'static str {
199        "audio-manifest"
200    }
201
202    fn extensions(&self) -> &'static [&'static str] {
203        &["ron"]
204    }
205
206    fn max_bytes(&self) -> usize {
207        TEXT_MAX_BYTES
208    }
209
210    fn decode(
211        &self,
212        file: &AssetPath,
213        bytes: &[u8],
214        cx: &DecodeCx<'_>,
215    ) -> Result<AudioManifest, Vec<ContentError>> {
216        let source = text(file, bytes)?;
217        let path = |name: &str| join(file.dir(), name);
218        let files = Files {
219            sound: &|name| cx.bytes(&path(name)).map(Cow::Borrowed),
220            sound_shown: &|name| cx.shown(&path(name)),
221            sound_left_out: &|name| cx.left_out(&path(name)),
222            music_dir: "",
223            music: None,
224        };
225        from_source(file.shown(), source, &files)
226    }
227}