1#[cfg(feature = "m4a-encode")]
10mod aac;
11#[cfg(feature = "m4a-encode")]
12mod m4a;
13#[cfg(feature = "fdk-aac-encoder")]
14mod m4a_fdk;
15mod mp3;
16mod opus;
17mod pcm;
18
19#[cfg(feature = "m4a-encode")]
20pub use aac::{write_adts_aac, write_adts_aac_with_downmix};
21#[cfg(feature = "m4a-encode")]
22pub use m4a::{write_m4a, write_m4a_with_downmix};
23#[cfg(feature = "fdk-aac-encoder")]
24pub use m4a_fdk::{write_m4a_fdk, write_m4a_fdk_with_downmix};
25pub use mp3::{write_mp3, write_mp3_with_downmix, DEFAULT_MP3_BITRATE};
26
27pub const DEFAULT_M4A_BITRATE: u32 = 192_000;
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
36pub enum DownmixMode {
37 #[default]
39 Preserve,
40 Stereo,
42}
43
44impl DownmixMode {
45 pub fn parse(value: &str) -> Option<Self> {
46 match value.to_ascii_lowercase().as_str() {
47 "preserve" | "none" | "off" => Some(Self::Preserve),
48 "stereo" | "2" | "on" => Some(Self::Stereo),
49 _ => None,
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub enum AacEncoder {
56 #[default]
57 Oxide,
58 Fdk,
59}
60
61impl AacEncoder {
62 pub fn parse(value: &str) -> Option<Self> {
63 match value.to_ascii_lowercase().as_str() {
64 "oxide" | "oxideav" | "rust" => Some(Self::Oxide),
65 "fdk" | "fdk-aac" => Some(Self::Fdk),
66 _ => None,
67 }
68 }
69}
70
71use std::path::Path;
72
73use crate::audio::{sanitize_sample, write_wav, Audio};
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum OutputFormat {
78 Wav,
79 Flac,
80 OggOpus,
81 Mp3,
82 M4a,
83 AacAdts,
84}
85
86impl OutputFormat {
87 pub fn from_path(path: &Path) -> Result<Self, String> {
88 match path
89 .extension()
90 .and_then(|e| e.to_str())
91 .map(|s| s.to_ascii_lowercase())
92 .as_deref()
93 {
94 Some("wav") => Ok(OutputFormat::Wav),
95 Some("flac") => Ok(OutputFormat::Flac),
96 Some("opus" | "ogg") => Ok(OutputFormat::OggOpus),
97 Some("mp3") => Ok(OutputFormat::Mp3),
98 Some("m4a" | "m4b" | "mp4") => Ok(OutputFormat::M4a),
99 Some("aac") => Ok(OutputFormat::AacAdts),
100 Some(ext) => Err(format!(
101 "unsupported output format '.{ext}'; use .wav, .flac, .opus, .mp3, .m4a, or .aac"
102 )),
103 None => Err(
104 "output path has no extension; use .wav, .flac, .opus, .mp3, .m4a, or .aac".into(),
105 ),
106 }
107 }
108}
109
110#[derive(Clone, Copy, Debug)]
112pub struct EncodeOptions {
113 pub mp3_bitrate_kbps: u32,
115 pub m4a_bitrate_bps: u32,
117 pub aac_encoder: AacEncoder,
118 pub downmix: DownmixMode,
120}
121
122impl Default for EncodeOptions {
123 fn default() -> Self {
124 Self {
125 mp3_bitrate_kbps: DEFAULT_MP3_BITRATE,
126 m4a_bitrate_bps: DEFAULT_M4A_BITRATE,
127 aac_encoder: AacEncoder::Oxide,
128 downmix: DownmixMode::Preserve,
129 }
130 }
131}
132
133pub fn write_audio<P: AsRef<Path>>(
135 path: P,
136 audio: &Audio,
137 options: EncodeOptions,
138) -> Result<(), String> {
139 let path = path.as_ref();
140 match OutputFormat::from_path(path)? {
141 OutputFormat::Wav => write_wav(path, audio),
142 OutputFormat::Flac => write_flac(path, audio),
143 OutputFormat::OggOpus => opus::write_ogg_opus(path, audio, 128_000, options.downmix),
144 OutputFormat::Mp3 => {
145 write_mp3_with_downmix(path, audio, options.mp3_bitrate_kbps, options.downmix)
146 }
147 OutputFormat::M4a => {
148 #[cfg(feature = "m4a-encode")]
149 {
150 match options.aac_encoder {
151 AacEncoder::Oxide => write_m4a_with_downmix(
152 path,
153 audio,
154 options.m4a_bitrate_bps,
155 options.downmix,
156 ),
157 AacEncoder::Fdk => {
158 #[cfg(feature = "fdk-aac-encoder")]
159 {
160 write_m4a_fdk_with_downmix(
161 path,
162 audio,
163 options.m4a_bitrate_bps,
164 options.downmix,
165 )
166 }
167 #[cfg(not(feature = "fdk-aac-encoder"))]
168 {
169 Err("FDK-AAC is unavailable in this build; rebuild with --features fdk-aac-encoder".into())
170 }
171 }
172 }
173 }
174 #[cfg(not(feature = "m4a-encode"))]
175 {
176 let _ = options;
177 Err("M4A output is unavailable in the crates.io build; use WAV/MP3 or a GitHub release binary".into())
178 }
179 }
180 OutputFormat::AacAdts => {
181 #[cfg(feature = "m4a-encode")]
182 {
183 if options.aac_encoder == AacEncoder::Fdk {
184 return Err(
185 "FDK-AAC ADTS output is not available; use M4A or --aac-encoder oxide"
186 .into(),
187 );
188 }
189 write_adts_aac_with_downmix(path, audio, options.m4a_bitrate_bps, options.downmix)
190 }
191 #[cfg(not(feature = "m4a-encode"))]
192 {
193 Err(
194 "AAC output is unavailable in this build; rebuild with --features m4a-encode"
195 .into(),
196 )
197 }
198 }
199 }
200}
201
202fn write_flac(path: &Path, audio: &Audio) -> Result<(), String> {
203 if audio.channels() == 0 {
204 return Err("FLAC output requires at least one channel".into());
205 }
206 if audio.frames() == 0 {
207 return Err("FLAC output requires at least one frame".into());
208 }
209 use flacenc::component::BitRepr;
210 use flacenc::error::Verify;
211 let bits = audio.bits_per_sample.clamp(8, 24) as usize;
212 let scale = (1_i64 << (bits - 1)) as f64;
213 let mut samples = Vec::with_capacity(audio.frames() * audio.channels());
214 for frame in 0..audio.frames() {
215 for channel in &audio.channels {
216 samples.push(
217 (sanitize_sample(channel[frame]) * scale)
218 .round()
219 .clamp(-scale, scale - 1.0) as i32,
220 );
221 }
222 }
223 let config = flacenc::config::Encoder::default()
224 .into_verified()
225 .map_err(|e| format!("FLAC config: {:?}", e.1))?;
226 let source = flacenc::source::MemSource::from_samples(
227 &samples,
228 audio.channels(),
229 bits,
230 audio.sample_rate as usize,
231 );
232 let stream = flacenc::encode_with_fixed_block_size(&config, source, config.block_size)
233 .map_err(|e| format!("FLAC encode: {e}"))?;
234 let mut sink = flacenc::bitsink::ByteSink::new();
235 stream
236 .write(&mut sink)
237 .map_err(|e| format!("FLAC serialize: {e}"))?;
238 std::fs::write(path, sink.as_slice()).map_err(|e| format!("FLAC write: {e}"))
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn detect_output_formats() {
247 assert_eq!(
248 OutputFormat::from_path(Path::new("out.mp3")).unwrap(),
249 OutputFormat::Mp3
250 );
251 assert_eq!(
252 OutputFormat::from_path(Path::new("out.m4a")).unwrap(),
253 OutputFormat::M4a
254 );
255 assert_eq!(
256 OutputFormat::from_path(Path::new("out.aac")).unwrap(),
257 OutputFormat::AacAdts
258 );
259 assert_eq!(
260 OutputFormat::from_path(Path::new("out.flac")).unwrap(),
261 OutputFormat::Flac
262 );
263 assert_eq!(
264 OutputFormat::from_path(Path::new("out.opus")).unwrap(),
265 OutputFormat::OggOpus
266 );
267 }
268}