use crate::cli::support::{ExportMetadata, ExportSessionData, ExportTrack};
use maolan_engine::{kind::Kind, message::AudioClipData};
use std::{
collections::{BTreeSet, HashMap},
fmt, fs, io,
io::Write,
path::{Path, PathBuf},
time::Duration,
};
use ebur128::{EbuR128, Mode as LoudnessMode};
use ffmpeg_next::{
Dictionary,
codec::{Context as CodecContext, Id as CodecId},
format::output,
frame::Audio,
};
pub const STANDARD_EXPORT_SAMPLE_RATES: [u32; 12] = [
8000, 11025, 16000, 22050, 32000, 44100, 48000, 88200, 96000, 176400, 192000, 384000,
];
pub const EXPORT_MP3_BITRATES_KBPS: [u16; 7] = [96, 128, 160, 192, 224, 256, 320];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportFormat {
Wav,
Mp3,
Ogg,
Flac,
}
impl fmt::Display for ExportFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Wav => write!(f, "WAV"),
Self::Mp3 => write!(f, "MP3"),
Self::Ogg => write!(f, "OGG"),
Self::Flac => write!(f, "FLAC"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportMp3Mode {
Cbr,
Vbr,
}
impl fmt::Display for ExportMp3Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cbr => write!(f, "CBR"),
Self::Vbr => write!(f, "VBR"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportNormalizeMode {
Peak,
Loudness,
}
impl fmt::Display for ExportNormalizeMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Peak => write!(f, "Peak"),
Self::Loudness => write!(f, "Loudness"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportRenderMode {
Mixdown,
StemsPostFader,
StemsPreFader,
}
impl fmt::Display for ExportRenderMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Mixdown => write!(f, "Mixdown"),
Self::StemsPostFader => write!(f, "Stems (Post-Fader)"),
Self::StemsPreFader => write!(f, "Stems (Pre-Fader)"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportBitDepth {
Int16,
Int24,
Int32,
Float32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportDither {
None,
Rectangular,
Triangular,
}
impl fmt::Display for ExportBitDepth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Int16 => write!(f, "16-bit PCM"),
Self::Int24 => write!(f, "24-bit PCM"),
Self::Int32 => write!(f, "32-bit PCM"),
Self::Float32 => write!(f, "32-bit float"),
}
}
}
impl fmt::Display for ExportDither {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::None => write!(f, "None"),
Self::Rectangular => write!(f, "Rectangular"),
Self::Triangular => write!(f, "Triangular"),
}
}
}
pub const EXPORT_MP3_MODE_ALL: [ExportMp3Mode; 2] = [ExportMp3Mode::Cbr, ExportMp3Mode::Vbr];
pub const EXPORT_RENDER_MODE_ALL: [ExportRenderMode; 3] = [
ExportRenderMode::Mixdown,
ExportRenderMode::StemsPostFader,
ExportRenderMode::StemsPreFader,
];
pub const EXPORT_BIT_DEPTH_ALL: [ExportBitDepth; 4] = [
ExportBitDepth::Int16,
ExportBitDepth::Int24,
ExportBitDepth::Int32,
ExportBitDepth::Float32,
];
pub const EXPORT_DITHER_ALL: [ExportDither; 3] = [
ExportDither::None,
ExportDither::Rectangular,
ExportDither::Triangular,
];
pub const EXPORT_NORMALIZE_MODE_ALL: [ExportNormalizeMode; 2] =
[ExportNormalizeMode::Peak, ExportNormalizeMode::Loudness];
#[derive(Debug, Clone)]
pub struct ExportSettings {
pub sample_rate_hz: u32,
pub format_wav: bool,
pub format_mp3: bool,
pub format_ogg: bool,
pub format_flac: bool,
pub bit_depth: ExportBitDepth,
pub dither: ExportDither,
pub mp3_mode: ExportMp3Mode,
pub mp3_bitrate_kbps: u16,
pub ogg_quality: f32,
pub render_mode: ExportRenderMode,
pub hw_out_ports: BTreeSet<usize>,
pub realtime_fallback: bool,
pub normalize: bool,
pub normalize_mode: ExportNormalizeMode,
pub normalize_dbfs: f32,
pub normalize_lufs: f32,
pub normalize_dbtp: f32,
pub normalize_tp_limiter: bool,
pub master_limiter: bool,
pub master_limiter_ceiling_dbtp: f32,
}
impl ExportSettings {
pub fn new(default_sample_rate_hz: u32, hw_output_channels: usize) -> Self {
Self {
sample_rate_hz: default_sample_rate_hz,
format_wav: true,
format_mp3: false,
format_ogg: false,
format_flac: false,
bit_depth: ExportBitDepth::Int24,
dither: ExportDither::Triangular,
mp3_mode: ExportMp3Mode::Cbr,
mp3_bitrate_kbps: 320,
ogg_quality: 0.6,
render_mode: ExportRenderMode::Mixdown,
hw_out_ports: default_hw_out_ports(hw_output_channels),
realtime_fallback: false,
normalize: false,
normalize_mode: ExportNormalizeMode::Peak,
normalize_dbfs: 0.0,
normalize_lufs: -23.0,
normalize_dbtp: -1.0,
normalize_tp_limiter: true,
master_limiter: true,
master_limiter_ceiling_dbtp: -1.0,
}
}
pub fn selected_formats(&self) -> Vec<ExportFormat> {
let mut formats = Vec::new();
if self.format_wav {
formats.push(ExportFormat::Wav);
}
if self.format_mp3 {
formats.push(ExportFormat::Mp3);
}
if self.format_ogg {
formats.push(ExportFormat::Ogg);
}
if self.format_flac {
formats.push(ExportFormat::Flac);
}
formats
}
pub fn normalize_hw_out_ports(&mut self, hw_output_channels: usize) {
let available: BTreeSet<usize> = (0..hw_output_channels).collect();
self.hw_out_ports.retain(|port| available.contains(port));
if self.hw_out_ports.is_empty() {
self.hw_out_ports = default_hw_out_ports(hw_output_channels);
}
}
}
fn default_hw_out_ports(hw_output_channels: usize) -> BTreeSet<usize> {
(0..hw_output_channels).take(2).collect()
}
pub fn export_bit_depth_options(formats: &[ExportFormat]) -> Vec<ExportBitDepth> {
if formats
.iter()
.any(|f| matches!(f, ExportFormat::Wav | ExportFormat::Flac))
{
EXPORT_BIT_DEPTH_ALL.to_vec()
} else {
vec![ExportBitDepth::Float32]
}
}
pub fn export_mp3_supported(settings: &ExportSettings, session: &ExportSessionData) -> bool {
export_max_channels(settings, session) <= 2
}
pub fn export_max_channels(settings: &ExportSettings, session: &ExportSessionData) -> usize {
if matches!(settings.render_mode, ExportRenderMode::Mixdown) {
settings.hw_out_ports.len()
} else {
session
.tracks
.iter()
.map(|track| track.output_ports.max(1))
.max()
.unwrap_or(0)
}
}
pub fn validate_export_settings(
settings: &ExportSettings,
session: &ExportSessionData,
) -> Result<(), String> {
if settings.selected_formats().is_empty() {
return Err("Select at least one export format".to_string());
}
if settings.format_mp3 && !export_mp3_supported(settings, session) {
return Err("MP3 export supports only mono or stereo".to_string());
}
if matches!(settings.render_mode, ExportRenderMode::Mixdown) && settings.hw_out_ports.is_empty()
{
return Err("Select at least one hw:out port for mixdown export".to_string());
}
if !(-20.0..=0.0).contains(&settings.master_limiter_ceiling_dbtp) {
return Err("Master limiter ceiling must be between -20.0 and 0.0 dBTP".to_string());
}
if !(-0.1..=1.0).contains(&settings.ogg_quality) {
return Err("OGG quality must be between -0.1 and 1.0".to_string());
}
if settings.normalize {
match settings.normalize_mode {
ExportNormalizeMode::Peak => {
if !(-60.0..=0.0).contains(&settings.normalize_dbfs) {
return Err("Normalize target must be between -60.0 and 0.0 dBFS".to_string());
}
}
ExportNormalizeMode::Loudness => {
if !(-70.0..=-5.0).contains(&settings.normalize_lufs) {
return Err("LUFS target must be between -70.0 and -5.0".to_string());
}
if !(-20.0..=0.0).contains(&settings.normalize_dbtp) {
return Err("dBTP ceiling must be between -20.0 and 0.0".to_string());
}
}
}
}
if session.tracks.is_empty() {
return Err("No tracks found. Nothing to export.".to_string());
}
Ok(())
}
pub fn default_export_base_path(session_dir: &Path) -> PathBuf {
session_dir.join("export")
}
pub async fn export_session<F>(
session: &ExportSessionData,
session_root: &Path,
export_base_path: &Path,
settings: &ExportSettings,
mut progress_callback: F,
) -> io::Result<Vec<PathBuf>>
where
F: FnMut(f32, Option<String>),
{
let mut tracks = session.tracks.clone();
let connections = session.connections.clone();
let total_length = tracks
.iter()
.flat_map(|track| track.audio_clips.iter())
.map(audio_clip_end)
.max()
.unwrap_or(0);
if total_length == 0 {
return Err(io::Error::other("No audio clips found. Nothing to export."));
}
let export_formats = settings.selected_formats();
let codec = ExportCodecSettings {
mp3_mode: settings.mp3_mode,
mp3_bitrate_kbps: settings.mp3_bitrate_kbps,
ogg_quality: settings.ogg_quality,
};
let has_solo = tracks.iter().any(|track| track.soloed);
let metadata = session.metadata.clone();
progress_callback(0.0, Some("Analyzing tracks".to_string()));
tokio::task::yield_now().await;
if matches!(settings.render_mode, ExportRenderMode::Mixdown) {
let output_ports: Vec<usize> = settings.hw_out_ports.iter().copied().collect();
let output_channels = output_ports.len().max(1);
let hw_out_channel_map: HashMap<usize, usize> = output_ports
.iter()
.enumerate()
.map(|(channel_idx, port)| (*port, channel_idx))
.collect();
let mut mixed_buffer = vec![0.0_f32; total_length * output_channels];
let track_count = tracks.len().max(1);
for (track_idx, track) in tracks.iter_mut().enumerate() {
if track.muted || (has_solo && !track.soloed) {
continue;
}
let progress_start = 0.1 + (track_idx as f32 / track_count as f32) * 0.7;
let progress_span = 0.7 / track_count as f32;
progress_callback(
progress_start,
Some(format!("Processing track: {}", track.name)),
);
tokio::task::yield_now().await;
let routed_ports: Vec<(usize, usize)> = connections
.iter()
.filter(|conn| {
conn.kind == Kind::Audio
&& conn.from_track == track.name
&& conn.to_track == "hw:out"
})
.filter_map(|conn| {
hw_out_channel_map
.get(&conn.to_port)
.map(|dest_idx| (conn.from_port, *dest_idx))
})
.collect();
if routed_ports.is_empty() {
continue;
}
let track_buffer = mix_track_clips_to_channels(
&track.audio_clips,
session_root,
total_length,
track.output_ports,
track.level,
track.balance,
true,
)?;
for frame in 0..total_length {
let track_base = frame * track.output_ports.max(1);
let mixed_base = frame * output_channels;
for (source_port, dest_channel) in &routed_ports {
if *source_port >= track.output_ports.max(1) {
continue;
}
mixed_buffer[mixed_base + *dest_channel] +=
track_buffer[track_base + *source_port];
}
}
progress_callback(
progress_start + progress_span,
Some(format!("Finished: {}", track.name)),
);
}
if settings.realtime_fallback {
progress_callback(0.82, Some("Real-time fallback pacing".to_string()));
let seconds = (total_length as f64 / settings.sample_rate_hz.max(1) as f64).max(0.0);
tokio::time::sleep(Duration::from_secs_f64(seconds)).await;
}
if settings.normalize {
apply_export_normalization(
&mut mixed_buffer,
ExportNormalizeParams {
mode: settings.normalize_mode,
target_dbfs: settings.normalize_dbfs,
target_lufs: settings.normalize_lufs,
true_peak_dbtp: settings.normalize_dbtp,
tp_limiter: settings.normalize_tp_limiter,
sample_rate: settings.sample_rate_hz as i32,
output_channels,
},
)?;
}
apply_master_limiter(
&mut mixed_buffer,
settings.master_limiter,
settings.master_limiter_ceiling_dbtp,
);
let base_path = export_base_path.to_path_buf();
let write_span = 0.1 / export_formats.len().max(1) as f32;
let mut written = Vec::new();
for (format_idx, format) in export_formats.iter().enumerate() {
progress_callback(
(0.9 + write_span * format_idx as f32).clamp(0.0, 0.99),
Some(format!("Writing {} ({})", format, settings.bit_depth)),
);
let out_path = base_path.with_extension(export_format_extension(*format));
write_export_audio(ExportWriteRequest {
export_path: &out_path,
mixed_buffer: &mixed_buffer,
sample_rate: settings.sample_rate_hz as i32,
output_channels,
bit_depth: settings.bit_depth,
dither: settings.dither,
format: *format,
codec,
metadata: &metadata,
})?;
written.push(out_path);
}
progress_callback(1.0, Some("Complete".to_string()));
return Ok(written);
}
let stem_mode_label = if matches!(settings.render_mode, ExportRenderMode::StemsPreFader) {
"pre"
} else {
"post"
};
let export_parent = export_base_path.parent().unwrap_or_else(|| Path::new("."));
let export_stem = export_base_path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("export");
let stem_dir = export_parent.join(format!("{export_stem}_stems"));
fs::create_dir_all(&stem_dir)?;
let selected_tracks: Vec<&ExportTrack> = tracks
.iter()
.filter(|track| !track.muted && (!has_solo || track.soloed))
.collect();
if selected_tracks.is_empty() {
return Err(io::Error::other("No tracks are eligible for stem export"));
}
let mut written = Vec::new();
for (idx, track) in selected_tracks.iter().enumerate() {
progress_callback(
0.1 + (idx as f32 / selected_tracks.len().max(1) as f32) * 0.75,
Some(format!("Rendering stem: {}", track.name)),
);
let output_channels = track.output_ports.max(1);
let mut stem_buffer = mix_track_clips_to_channels(
&track.audio_clips,
session_root,
total_length,
output_channels,
track.level,
track.balance,
matches!(settings.render_mode, ExportRenderMode::StemsPostFader),
)?;
if settings.normalize {
apply_export_normalization(
&mut stem_buffer,
ExportNormalizeParams {
mode: settings.normalize_mode,
target_dbfs: settings.normalize_dbfs,
target_lufs: settings.normalize_lufs,
true_peak_dbtp: settings.normalize_dbtp,
tp_limiter: settings.normalize_tp_limiter,
sample_rate: settings.sample_rate_hz as i32,
output_channels,
},
)?;
}
apply_master_limiter(
&mut stem_buffer,
settings.master_limiter,
settings.master_limiter_ceiling_dbtp,
);
for format in &export_formats {
let stem_file = stem_dir.join(format!(
"{}_{}.{}",
sanitize_export_component(&track.name),
stem_mode_label,
export_format_extension(*format)
));
write_export_audio(ExportWriteRequest {
export_path: &stem_file,
mixed_buffer: &stem_buffer,
sample_rate: settings.sample_rate_hz as i32,
output_channels,
bit_depth: settings.bit_depth,
dither: settings.dither,
format: *format,
codec,
metadata: &metadata,
})?;
written.push(stem_file);
}
if settings.realtime_fallback {
let seconds = (total_length as f64 / settings.sample_rate_hz.max(1) as f64).max(0.0);
tokio::time::sleep(Duration::from_secs_f64(seconds)).await;
}
}
progress_callback(1.0, Some("Complete".to_string()));
Ok(written)
}
fn audio_clip_end(clip: &AudioClipData) -> usize {
if !clip.grouped_clips.is_empty() {
clip.grouped_clips
.iter()
.map(audio_clip_end)
.max()
.unwrap_or(0)
} else {
clip.start + clip.length
}
}
fn mix_track_clips_to_channels(
clips: &[AudioClipData],
session_root: &Path,
total_length: usize,
output_channels: usize,
level_db: f32,
balance: f32,
apply_fader: bool,
) -> io::Result<Vec<f32>> {
let output_channels = output_channels.max(1);
let mut mixed = vec![0.0_f32; total_length * output_channels];
let channel_gains = if apply_fader {
let level_amp = 10.0_f32.powf(level_db / 20.0);
if output_channels == 2 {
vec![
if balance <= 0.0 {
level_amp
} else {
level_amp * (1.0 - balance)
},
if balance >= 0.0 {
level_amp
} else {
level_amp * (1.0 + balance)
},
]
} else {
vec![level_amp; output_channels]
}
} else {
vec![1.0; output_channels]
};
for clip in clips {
mix_clip_into_buffer(
clip,
session_root,
&mut mixed,
total_length,
output_channels,
&channel_gains,
)?;
}
Ok(mixed)
}
fn mix_clip_into_buffer(
clip: &AudioClipData,
session_root: &Path,
mixed: &mut [f32],
total_length: usize,
output_channels: usize,
channel_gains: &[f32],
) -> io::Result<()> {
if clip.muted {
return Ok(());
}
if !clip.grouped_clips.is_empty() {
for child in &clip.grouped_clips {
mix_clip_into_buffer(
child,
session_root,
mixed,
total_length,
output_channels,
channel_gains,
)?;
}
return Ok(());
}
let clip_path = resolve_audio_clip_path(clip, session_root);
let (samples, clip_channels, _) = decode_audio_to_f32_interleaved_sync(&clip_path)?;
if samples.is_empty() {
return Ok(());
}
let clip_frames = samples.len() / clip_channels;
let offset_frame = clip.offset.min(clip_frames);
let length_frames = clip.length.min(clip_frames.saturating_sub(offset_frame));
for frame_idx in 0..length_frames {
let src_frame = offset_frame + frame_idx;
let dst_frame = clip.start + frame_idx;
if dst_frame >= total_length {
break;
}
let src_idx = src_frame * clip_channels;
let dst_idx = dst_frame * output_channels;
for out_ch in 0..output_channels {
let source_sample = if clip_channels == 1 {
samples[src_idx]
} else {
samples[src_idx + out_ch.min(clip_channels.saturating_sub(1))]
};
mixed[dst_idx + out_ch] += source_sample * channel_gains[out_ch];
}
}
Ok(())
}
fn resolve_audio_clip_path(clip: &AudioClipData, session_root: &Path) -> PathBuf {
let name = clip
.preview_name
.as_ref()
.or(clip.source_name.as_ref())
.unwrap_or(&clip.name);
let path = PathBuf::from(name);
if path.is_absolute() {
path
} else {
session_root.join(path)
}
}
fn apply_master_limiter(samples: &mut [f32], enabled: bool, ceiling_dbtp: f32) {
if !enabled {
return;
}
let ceiling_amp = 10.0_f32.powf(ceiling_dbtp / 20.0).clamp(0.0, 1.0);
maolan_engine::simd::clamp_inplace(samples, -ceiling_amp, ceiling_amp);
}
fn export_format_extension(format: ExportFormat) -> &'static str {
match format {
ExportFormat::Wav => "wav",
ExportFormat::Mp3 => "mp3",
ExportFormat::Ogg => "ogg",
ExportFormat::Flac => "flac",
}
}
fn sanitize_export_component(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for ch in value.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
out.push(ch);
} else {
out.push('_');
}
}
if out.is_empty() {
"track".to_string()
} else {
out
}
}
#[derive(Clone, Copy)]
struct ExportCodecSettings {
mp3_mode: ExportMp3Mode,
mp3_bitrate_kbps: u16,
ogg_quality: f32,
}
struct ExportWriteRequest<'a> {
export_path: &'a Path,
mixed_buffer: &'a [f32],
sample_rate: i32,
output_channels: usize,
bit_depth: ExportBitDepth,
dither: ExportDither,
format: ExportFormat,
codec: ExportCodecSettings,
metadata: &'a ExportMetadata,
}
#[derive(Clone, Copy)]
struct ExportNormalizeParams {
mode: ExportNormalizeMode,
target_dbfs: f32,
target_lufs: f32,
true_peak_dbtp: f32,
tp_limiter: bool,
sample_rate: i32,
output_channels: usize,
}
struct DitherRng {
state: u64,
}
impl DitherRng {
fn new(seed: u64) -> Self {
Self { state: seed.max(1) }
}
fn next_u64(&mut self) -> u64 {
self.state ^= self.state >> 12;
self.state ^= self.state << 25;
self.state ^= self.state >> 27;
self.state.wrapping_mul(0x2545_f491_4f6c_dd1d)
}
fn uniform_half(&mut self) -> f32 {
let u = self.next_u64() >> 32;
(u as f32 / 4_294_967_296.0) - 0.5
}
fn triangular(&mut self) -> f32 {
self.uniform_half() + self.uniform_half()
}
fn rectangular(&mut self) -> f32 {
self.uniform_half()
}
}
fn dither_is_applicable(bit_depth: ExportBitDepth, dither: ExportDither) -> bool {
!matches!(dither, ExportDither::None) && !matches!(bit_depth, ExportBitDepth::Float32)
}
fn quantize_with_dither(sample: f32, scale: f32, rng: &mut DitherRng, dither: ExportDither) -> f32 {
let d = match dither {
ExportDither::None => 0.0,
ExportDither::Rectangular => rng.rectangular(),
ExportDither::Triangular => rng.triangular(),
};
(sample + d / scale).clamp(-1.0, 1.0) * scale
}
fn write_export_audio(req: ExportWriteRequest<'_>) -> io::Result<()> {
let export_path = req.export_path;
let tmp_path = export_path.with_extension(format!(
"{}.tmp",
export_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("export")
));
let result = match req.format {
ExportFormat::Wav => write_wav_with_bit_depth(
&tmp_path,
req.mixed_buffer,
req.sample_rate,
req.output_channels,
req.bit_depth,
req.dither,
),
ExportFormat::Flac => write_flac_with_bit_depth(
&tmp_path,
req.mixed_buffer,
req.sample_rate,
req.output_channels,
req.bit_depth,
req.dither,
),
ExportFormat::Mp3 => write_mp3(
&tmp_path,
req.mixed_buffer,
req.sample_rate,
req.output_channels,
req.codec,
req.metadata,
),
ExportFormat::Ogg => write_ogg_vorbis(
&tmp_path,
req.mixed_buffer,
req.sample_rate,
req.output_channels,
req.codec,
req.metadata,
),
};
if result.is_ok() {
fs::rename(&tmp_path, export_path)?;
}
result
}
fn write_wav_with_bit_depth(
export_path: &Path,
mixed_buffer: &[f32],
sample_rate: i32,
output_channels: usize,
bit_depth: ExportBitDepth,
dither: ExportDither,
) -> io::Result<()> {
let bits_per_sample = match bit_depth {
ExportBitDepth::Int16 => 16u16,
ExportBitDepth::Int24 => 24u16,
ExportBitDepth::Int32 => 32u16,
ExportBitDepth::Float32 => 32u16,
};
let is_float = matches!(bit_depth, ExportBitDepth::Float32);
write_wav_pcm(
export_path,
mixed_buffer,
output_channels.max(1),
sample_rate as u32,
bits_per_sample,
is_float,
dither,
)
}
fn write_wav_pcm(
path: &Path,
samples: &[f32],
channels: usize,
sample_rate: u32,
bits_per_sample: u16,
is_float: bool,
dither: ExportDither,
) -> io::Result<()> {
let bytes_per_sample = usize::from(bits_per_sample / 8);
let block_align = (channels * bytes_per_sample) as u16;
let byte_rate = sample_rate * u32::from(block_align);
let data_size = samples
.len()
.checked_mul(bytes_per_sample)
.ok_or_else(|| io::Error::other("WAV data too large"))? as u32;
let riff_size = 36u32
.checked_add(data_size)
.ok_or_else(|| io::Error::other("WAV file too large"))?;
let mut file = fs::File::create(path)?;
file.write_all(b"RIFF")?;
file.write_all(&riff_size.to_le_bytes())?;
file.write_all(b"WAVE")?;
file.write_all(b"fmt ")?;
file.write_all(&16u32.to_le_bytes())?;
let audio_format: u16 = if is_float { 3 } else { 1 };
file.write_all(&audio_format.to_le_bytes())?;
file.write_all(&(channels as u16).to_le_bytes())?;
file.write_all(&sample_rate.to_le_bytes())?;
file.write_all(&byte_rate.to_le_bytes())?;
file.write_all(&block_align.to_le_bytes())?;
file.write_all(&bits_per_sample.to_le_bytes())?;
file.write_all(b"data")?;
file.write_all(&data_size.to_le_bytes())?;
let mut rng = DitherRng::new(0x1234_5678_9abc_defe);
let apply_dither = !is_float
&& dither_is_applicable(
match bits_per_sample {
16 => ExportBitDepth::Int16,
24 => ExportBitDepth::Int24,
32 => ExportBitDepth::Int32,
_ => ExportBitDepth::Float32,
},
dither,
);
for &sample in samples {
let s = sample.clamp(-1.0, 1.0);
match (is_float, bits_per_sample) {
(true, 32) => file.write_all(&s.to_le_bytes())?,
(false, 16) => {
let scale = i16::MAX as f32;
let q = if apply_dither {
quantize_with_dither(s, scale, &mut rng, dither)
} else {
s * scale
}
.round()
.clamp(i16::MIN as f32, i16::MAX as f32) as i16;
file.write_all(&q.to_le_bytes())?;
}
(false, 24) => {
let scale = 8_388_607.0;
let q = if apply_dither {
quantize_with_dither(s, scale, &mut rng, dither)
} else {
s * scale
}
.round()
.clamp(-8_388_608.0, 8_388_607.0) as i32;
let b = q.to_le_bytes();
file.write_all(&b[..3])?;
}
(false, 32) => {
let scale = i32::MAX as f32;
let q = if apply_dither {
quantize_with_dither(s, scale, &mut rng, dither)
} else {
s * scale
}
.round()
.clamp(i32::MIN as f32, i32::MAX as f32) as i32;
file.write_all(&q.to_le_bytes())?;
}
_ => return Err(io::Error::other("Unsupported WAV format")),
}
}
Ok(())
}
fn decode_audio_to_f32_interleaved_sync(path: &Path) -> io::Result<(Vec<f32>, usize, u32)> {
use ffmpeg_next::{format::sample::Type as SampleType, media::Type};
ffmpeg_init().map_err(|e| io::Error::other(format!("FFmpeg init failed: {e}")))?;
let mut ictx = ffmpeg_next::format::input(path)
.map_err(|e| io::Error::other(format!("Failed to open '{}': {e}", path.display())))?;
let stream = ictx
.streams()
.best(Type::Audio)
.ok_or_else(|| io::Error::other(format!("No audio stream in '{}'", path.display())))?;
let stream_index = stream.index();
let mut decoder = ffmpeg_next::codec::Context::from_parameters(stream.parameters())
.map_err(|e| io::Error::other(format!("Decoder init failed: {e}")))?
.decoder()
.audio()
.map_err(|e| io::Error::other(format!("Audio decoder init failed: {e}")))?;
let sample_rate = decoder.rate() as u32;
let channels = decoder.channels().max(1) as usize;
let mut samples = Vec::<f32>::new();
let mut raw_frame = ffmpeg_next::frame::Audio::empty();
let append_frame = |frame: &ffmpeg_next::frame::Audio,
channels: usize,
out: &mut Vec<f32>|
-> io::Result<()> {
let frame_samples = frame.samples();
match frame.format() {
ffmpeg_next::format::Sample::F32(SampleType::Packed) => {
let plane = frame.plane::<f32>(0);
out.extend_from_slice(plane);
}
ffmpeg_next::format::Sample::F32(SampleType::Planar) => {
let start = out.len();
out.resize(start + frame_samples * channels, 0.0);
for ch in 0..channels {
let plane = frame.plane::<f32>(ch);
for i in 0..frame_samples {
out[start + i * channels + ch] = plane[i];
}
}
}
ffmpeg_next::format::Sample::I16(SampleType::Packed) => {
let plane = frame.plane::<i16>(0);
out.extend(plane.iter().map(|&v| v as f32 / 32768.0));
}
ffmpeg_next::format::Sample::I16(SampleType::Planar) => {
let start = out.len();
out.resize(start + frame_samples * channels, 0.0);
for ch in 0..channels {
let plane = frame.plane::<i16>(ch);
for i in 0..frame_samples {
out[start + i * channels + ch] = plane[i] as f32 / 32768.0;
}
}
}
ffmpeg_next::format::Sample::I32(SampleType::Packed) => {
let plane = frame.plane::<i32>(0);
out.extend(plane.iter().map(|&v| v as f32 / 2_147_483_648.0));
}
ffmpeg_next::format::Sample::I32(SampleType::Planar) => {
let start = out.len();
out.resize(start + frame_samples * channels, 0.0);
for ch in 0..channels {
let plane = frame.plane::<i32>(ch);
for i in 0..frame_samples {
out[start + i * channels + ch] = plane[i] as f32 / 2_147_483_648.0;
}
}
}
other => {
return Err(io::Error::other(format!(
"Unsupported decoded sample format: {other:?}"
)));
}
}
Ok(())
};
for (stream, packet) in ictx.packets() {
if stream.index() != stream_index {
continue;
}
decoder
.send_packet(&packet)
.map_err(|e| io::Error::other(format!("Failed to send packet: {e}")))?;
while decoder.receive_frame(&mut raw_frame).is_ok() {
append_frame(&raw_frame, channels, &mut samples)?;
}
}
let _ = decoder.send_eof();
while decoder.receive_frame(&mut raw_frame).is_ok() {
append_frame(&raw_frame, channels, &mut samples)?;
}
if samples.is_empty() {
return Err(io::Error::other(format!(
"Audio file '{}' contains no samples",
path.display()
)));
}
Ok((samples, channels, sample_rate))
}
fn quantize_samples_for_bit_depth(
mixed_buffer: &[f32],
bit_depth: ExportBitDepth,
dither: ExportDither,
) -> (Vec<i32>, u8) {
let (scale, min, max, bits_per_sample) = match bit_depth {
ExportBitDepth::Int16 => (i16::MAX as f32, i16::MIN as f32, i16::MAX as f32, 16),
ExportBitDepth::Int24 => (8_388_607.0, -8_388_608.0, 8_388_607.0, 24),
ExportBitDepth::Int32 => (i32::MAX as f32, i32::MIN as f32, i32::MAX as f32, 32),
ExportBitDepth::Float32 => (8_388_607.0, -8_388_608.0, 8_388_607.0, 24),
};
if dither_is_applicable(bit_depth, dither) {
let mut rng = DitherRng::new(0x1234_5678_9abc_defe);
let samples = mixed_buffer
.iter()
.map(|s| {
quantize_with_dither(s.clamp(-1.0, 1.0), scale, &mut rng, dither)
.round()
.clamp(min, max) as i32
})
.collect();
return (samples, bits_per_sample);
}
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
{
use wide::{f32x4, i32x4};
let mut quantized = Vec::with_capacity(mixed_buffer.len());
let n = mixed_buffer.len() / 4;
let vmin_f = f32x4::splat(min);
let vmax_f = f32x4::splat(max);
let scale_f = f32x4::splat(scale);
let vmin_i = i32x4::splat(min as i32);
let vmax_i = i32x4::splat(max as i32);
for i in 0..n {
let chunk = &mixed_buffer[i * 4..(i + 1) * 4];
let v: f32x4 = [chunk[0], chunk[1], chunk[2], chunk[3]].into();
let clamped = v.clamp(vmin_f, vmax_f);
let scaled = clamped * scale_f;
let rounded = scaled.round_int();
let clamped_i = rounded.max(vmin_i).min(vmax_i);
quantized.extend_from_slice(&clamped_i.to_array());
}
for s in &mixed_buffer[n * 4..] {
quantized.push(
(*s).clamp(-1.0, 1.0)
.mul_add(scale, 0.0)
.round()
.clamp(min, max) as i32,
);
}
(quantized, bits_per_sample)
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))]
{
(
mixed_buffer
.iter()
.map(|s| (s.clamp(-1.0, 1.0) * scale).round().clamp(min, max) as i32)
.collect(),
bits_per_sample,
)
}
}
fn write_flac_with_bit_depth(
export_path: &Path,
mixed_buffer: &[f32],
sample_rate: i32,
output_channels: usize,
bit_depth: ExportBitDepth,
dither: ExportDither,
) -> io::Result<()> {
let (quantized, bits_per_sample) =
quantize_samples_for_bit_depth(mixed_buffer, bit_depth, dither);
let use_i16 = bits_per_sample <= 16;
ffmpeg_init().map_err(|e| io::Error::other(format!("FFmpeg init failed: {e}")))?;
let mut octx = output(export_path.to_str().unwrap_or("output.flac"))
.map_err(|e| io::Error::other(format!("Failed to create output context: {e}")))?;
let encoder_codec = ffmpeg_next::codec::encoder::find(CodecId::FLAC)
.ok_or_else(|| io::Error::other("FLAC encoder not found"))?;
let mut encoder_ctx = CodecContext::new_with_codec(encoder_codec);
unsafe {
(*encoder_ctx.as_mut_ptr()).bits_per_raw_sample = i32::from(bits_per_sample);
}
let mut encoder = encoder_ctx
.encoder()
.audio()
.map_err(|e| io::Error::other(format!("Failed to create audio encoder: {e}")))?;
encoder.set_rate(sample_rate);
encoder.set_channel_layout(ffmpeg_next::channel_layout::ChannelLayout::default(
output_channels as i32,
));
if use_i16 {
encoder.set_format(ffmpeg_next::format::Sample::I16(
ffmpeg_next::format::sample::Type::Packed,
));
} else {
encoder.set_format(ffmpeg_next::format::Sample::I32(
ffmpeg_next::format::sample::Type::Packed,
));
}
let mut output_stream = octx
.add_stream(encoder_codec)
.map_err(|e| io::Error::other(format!("Failed to add stream: {e}")))?;
output_stream.set_parameters(&encoder);
let mut encoder = encoder
.open_as(encoder_codec)
.map_err(|e| io::Error::other(format!("Failed to open encoder: {e}")))?;
octx.write_header()
.map_err(|e| io::Error::other(format!("Failed to write header: {e}")))?;
let quantized_i16: Option<Vec<i16>> = if use_i16 {
Some(quantized.iter().map(|&s| s as i16).collect())
} else {
None
};
const FRAME_SIZE: usize = 4096;
for chunk_start in (0..mixed_buffer.len()).step_by(FRAME_SIZE * output_channels) {
let chunk_end = (chunk_start + FRAME_SIZE * output_channels).min(mixed_buffer.len());
let actual_frames = (chunk_end - chunk_start) / output_channels;
if actual_frames == 0 {
continue;
}
let mut frame = Audio::empty();
frame.set_format(encoder.format());
frame.set_channel_layout(encoder.channel_layout());
frame.set_rate(encoder.rate());
frame.set_samples(actual_frames);
unsafe {
ffmpeg_next::ffi::av_frame_get_buffer(frame.as_mut_ptr(), 0);
}
if let Some(ref narrow) = quantized_i16 {
let src = &narrow[chunk_start..chunk_end];
let dst = frame.data_mut(0).as_mut_ptr().cast::<i16>();
unsafe {
std::ptr::copy_nonoverlapping(src.as_ptr(), dst, src.len());
}
} else {
let src = &quantized[chunk_start..chunk_end];
let dst = frame.data_mut(0).as_mut_ptr().cast::<i32>();
unsafe {
std::ptr::copy_nonoverlapping(src.as_ptr(), dst, src.len());
}
}
encoder
.send_frame(&frame)
.map_err(|e| io::Error::other(format!("Failed to send frame: {e}")))?;
let mut packet = ffmpeg_next::packet::Packet::empty();
while encoder.receive_packet(&mut packet).is_ok() {
packet.set_stream(0);
packet
.write_interleaved(&mut octx)
.map_err(|e| io::Error::other(format!("Failed to write packet: {e}")))?;
}
}
encoder
.send_eof()
.map_err(|e| io::Error::other(format!("Failed to send EOF: {e}")))?;
let mut packet = ffmpeg_next::packet::Packet::empty();
while encoder.receive_packet(&mut packet).is_ok() {
packet.set_stream(0);
packet
.write_interleaved(&mut octx)
.map_err(|e| io::Error::other(format!("Failed to write packet: {e}")))?;
}
octx.write_trailer()
.map_err(|e| io::Error::other(format!("Failed to write trailer: {e}")))?;
Ok(())
}
fn write_mp3(
export_path: &Path,
mixed_buffer: &[f32],
sample_rate: i32,
output_channels: usize,
codec: ExportCodecSettings,
metadata: &ExportMetadata,
) -> io::Result<()> {
if output_channels != 1 && output_channels != 2 {
return Err(io::Error::other(format!(
"MP3 export supports only mono/stereo, got {} channels",
output_channels
)));
}
ffmpeg_init().map_err(|e| io::Error::other(format!("FFmpeg init failed: {e}")))?;
let mut octx = output(export_path.to_str().unwrap_or("output.mp3"))
.map_err(|e| io::Error::other(format!("Failed to create output context: {e}")))?;
let codec_id = CodecId::MP3;
let encoder_codec = ffmpeg_next::codec::encoder::find(codec_id)
.ok_or_else(|| io::Error::other("MP3 encoder not found"))?;
let encoder_ctx = CodecContext::new_with_codec(encoder_codec);
let mut encoder = encoder_ctx
.encoder()
.audio()
.map_err(|e| io::Error::other(format!("Failed to create audio encoder: {e}")))?;
encoder.set_rate(sample_rate);
encoder.set_format(ffmpeg_next::format::Sample::F32(
ffmpeg_next::format::sample::Type::Planar,
));
encoder.set_channel_layout(match output_channels {
1 => ffmpeg_next::channel_layout::ChannelLayout::MONO,
_ => ffmpeg_next::channel_layout::ChannelLayout::STEREO,
});
let bitrate = (codec.mp3_bitrate_kbps as usize) * 1000;
encoder.set_bit_rate(bitrate);
if matches!(codec.mp3_mode, ExportMp3Mode::Vbr) {
encoder.set_quality(2usize);
}
let mut metadata_dict = Dictionary::new();
if !metadata.author.is_empty() {
metadata_dict.set("artist", &metadata.author);
}
if !metadata.album.is_empty() {
metadata_dict.set("album", &metadata.album);
}
if let Some(year) = metadata.year {
metadata_dict.set("date", &year.to_string());
}
if let Some(track_number) = metadata.track_number {
metadata_dict.set("track", &track_number.to_string());
}
if !metadata.genre.is_empty() {
metadata_dict.set("genre", &metadata.genre);
}
let mut output_stream = octx
.add_stream(encoder_codec)
.map_err(|e| io::Error::other(format!("Failed to add stream: {e}")))?;
output_stream.set_parameters(&encoder);
let mut encoder = encoder
.open_as(encoder_codec)
.map_err(|e| io::Error::other(format!("Failed to open encoder: {e}")))?;
octx.write_header()
.map_err(|e| io::Error::other(format!("Failed to write header: {e}")))?;
let frame_size = 1152;
for chunk_start in (0..mixed_buffer.len()).step_by(frame_size * output_channels) {
let chunk_end = (chunk_start + frame_size * output_channels).min(mixed_buffer.len());
let chunk = &mixed_buffer[chunk_start..chunk_end];
let actual_frames = chunk.len() / output_channels;
if actual_frames == 0 {
continue;
}
let mut frame = Audio::empty();
frame.set_format(encoder.format());
frame.set_channel_layout(encoder.channel_layout());
frame.set_rate(encoder.rate());
frame.set_samples(actual_frames);
unsafe {
ffmpeg_next::ffi::av_frame_get_buffer(frame.as_mut_ptr(), 0);
}
for ch in 0..output_channels {
let data_ptr = frame.data_mut(ch).as_mut_ptr() as *mut f32;
for frame_idx in 0..actual_frames {
let src_idx = frame_idx * output_channels + ch;
if src_idx < chunk.len() {
unsafe {
*data_ptr.add(frame_idx) = chunk[src_idx];
}
}
}
}
match encoder.send_frame(&frame) {
Ok(()) => {}
Err(e) => return Err(io::Error::other(format!("Failed to send frame: {e}"))),
}
let mut packet = ffmpeg_next::packet::Packet::empty();
while encoder.receive_packet(&mut packet).is_ok() {
packet.set_stream(0);
packet
.write_interleaved(&mut octx)
.map_err(|e| io::Error::other(format!("Failed to write packet: {e}")))?;
}
}
encoder
.send_eof()
.map_err(|e| io::Error::other(format!("Failed to send EOF: {e}")))?;
let mut packet = ffmpeg_next::packet::Packet::empty();
while encoder.receive_packet(&mut packet).is_ok() {
packet.set_stream(0);
packet
.write_interleaved(&mut octx)
.map_err(|e| io::Error::other(format!("Failed to write packet: {e}")))?;
}
octx.write_trailer()
.map_err(|e| io::Error::other(format!("Failed to write trailer: {e}")))?;
Ok(())
}
fn write_ogg_vorbis(
export_path: &Path,
mixed_buffer: &[f32],
sample_rate: i32,
output_channels: usize,
codec: ExportCodecSettings,
metadata: &ExportMetadata,
) -> io::Result<()> {
ffmpeg_init().map_err(|e| io::Error::other(format!("FFmpeg init failed: {e}")))?;
let mut octx = output(export_path.to_str().unwrap_or("output.ogg"))
.map_err(|e| io::Error::other(format!("Failed to create output context: {e}")))?;
let codec_id = CodecId::VORBIS;
let encoder_codec = ffmpeg_next::codec::encoder::find(codec_id)
.ok_or_else(|| io::Error::other("Vorbis encoder not found"))?;
let encoder_ctx = CodecContext::new_with_codec(encoder_codec);
let mut encoder = encoder_ctx
.encoder()
.audio()
.map_err(|e| io::Error::other(format!("Failed to create audio encoder: {e}")))?;
encoder.set_rate(sample_rate);
encoder.set_format(ffmpeg_next::format::Sample::F32(
ffmpeg_next::format::sample::Type::Planar,
));
encoder.set_channel_layout(match output_channels {
1 => ffmpeg_next::channel_layout::ChannelLayout::MONO,
_ => ffmpeg_next::channel_layout::ChannelLayout::STEREO,
});
let quality = ((codec.ogg_quality + 0.1) * 10.0).clamp(0.0, 10.0) as i32;
encoder.set_quality(quality as usize);
let mut metadata_dict = Dictionary::new();
if !metadata.author.is_empty() {
metadata_dict.set("artist", &metadata.author);
}
if !metadata.album.is_empty() {
metadata_dict.set("album", &metadata.album);
}
if let Some(year) = metadata.year {
metadata_dict.set("date", &year.to_string());
}
if let Some(track_number) = metadata.track_number {
metadata_dict.set("track", &track_number.to_string());
}
if !metadata.genre.is_empty() {
metadata_dict.set("genre", &metadata.genre);
}
let mut output_stream = octx
.add_stream(encoder_codec)
.map_err(|e| io::Error::other(format!("Failed to add stream: {e}")))?;
output_stream.set_parameters(&encoder);
let mut encoder = encoder
.open_as(encoder_codec)
.map_err(|e| io::Error::other(format!("Failed to open encoder: {e}")))?;
octx.write_header()
.map_err(|e| io::Error::other(format!("Failed to write header: {e}")))?;
let frame_size = 1024;
for chunk_start in (0..mixed_buffer.len()).step_by(frame_size * output_channels) {
let chunk_end = (chunk_start + frame_size * output_channels).min(mixed_buffer.len());
let chunk = &mixed_buffer[chunk_start..chunk_end];
let actual_frames = chunk.len() / output_channels;
if actual_frames == 0 {
continue;
}
let mut frame = Audio::empty();
frame.set_format(encoder.format());
frame.set_channel_layout(encoder.channel_layout());
frame.set_rate(encoder.rate());
frame.set_samples(actual_frames);
unsafe {
ffmpeg_next::ffi::av_frame_get_buffer(frame.as_mut_ptr(), 0);
}
for ch in 0..output_channels {
let data_ptr = frame.data_mut(ch).as_mut_ptr() as *mut f32;
for frame_idx in 0..actual_frames {
let src_idx = frame_idx * output_channels + ch;
if src_idx < chunk.len() {
unsafe {
*data_ptr.add(frame_idx) = chunk[src_idx];
}
}
}
}
match encoder.send_frame(&frame) {
Ok(()) => {}
Err(e) => return Err(io::Error::other(format!("Failed to send frame: {e}"))),
}
let mut packet = ffmpeg_next::packet::Packet::empty();
while encoder.receive_packet(&mut packet).is_ok() {
packet.set_stream(0);
packet
.write_interleaved(&mut octx)
.map_err(|e| io::Error::other(format!("Failed to write packet: {e}")))?;
}
}
encoder
.send_eof()
.map_err(|e| io::Error::other(format!("Failed to send EOF: {e}")))?;
let mut packet = ffmpeg_next::packet::Packet::empty();
while encoder.receive_packet(&mut packet).is_ok() {
packet.set_stream(0);
packet
.write_interleaved(&mut octx)
.map_err(|e| io::Error::other(format!("Failed to write packet: {e}")))?;
}
octx.write_trailer()
.map_err(|e| io::Error::other(format!("Failed to write trailer: {e}")))?;
Ok(())
}
fn ffmpeg_init() -> Result<(), ffmpeg_next::Error> {
static RESULT: std::sync::OnceLock<Result<(), ffmpeg_next::Error>> = std::sync::OnceLock::new();
*RESULT.get_or_init(ffmpeg_next::init)
}
fn measure_lufs_and_true_peak(
samples: &[f32],
channels: usize,
sample_rate: i32,
) -> io::Result<(f32, f32)> {
let mut meter = EbuR128::new(
channels as u32,
sample_rate as u32,
LoudnessMode::I | LoudnessMode::TRUE_PEAK,
)
.map_err(|e| io::Error::other(format!("Failed to initialize loudness meter: {e}")))?;
meter
.add_frames_f32(samples)
.map_err(|e| io::Error::other(format!("Loudness analysis failed: {e}")))?;
let lufs = meter
.loudness_global()
.map_err(|e| io::Error::other(format!("Failed to get integrated loudness: {e}")))?
as f32;
if !lufs.is_finite() {
return Err(io::Error::other("Integrated loudness is not finite"));
}
let mut tp = 0.0_f32;
for ch in 0..channels as u32 {
tp = tp.max(
meter
.true_peak(ch)
.map_err(|e| io::Error::other(format!("Failed to get true peak: {e}")))?
as f32,
);
}
Ok((lufs, tp))
}
fn apply_export_normalization(
samples: &mut [f32],
params: ExportNormalizeParams,
) -> io::Result<()> {
match params.mode {
ExportNormalizeMode::Peak => {
let peak = maolan_engine::simd::peak_abs(samples);
if peak > 0.0 {
let target_amp = 10.0_f32.powf(params.target_dbfs / 20.0).clamp(0.0, 1.0);
let gain = target_amp / peak;
maolan_engine::simd::mul_inplace(samples, gain);
}
}
ExportNormalizeMode::Loudness => {
let (measured_lufs, measured_tp_amp) =
measure_lufs_and_true_peak(samples, params.output_channels, params.sample_rate)?;
let gain_loudness_db = params.target_lufs - measured_lufs;
let gain_loudness = 10.0_f32.powf(gain_loudness_db / 20.0);
let ceiling_amp = 10.0_f32.powf(params.true_peak_dbtp / 20.0).clamp(0.0, 1.0);
let gain_tp = if measured_tp_amp > 0.0 {
ceiling_amp / measured_tp_amp
} else {
gain_loudness
};
let applied_gain = if params.tp_limiter {
gain_loudness
} else {
gain_loudness.min(gain_tp)
};
maolan_engine::simd::mul_inplace(samples, applied_gain);
if params.tp_limiter {
let predicted_tp = measured_tp_amp * applied_gain;
if predicted_tp > ceiling_amp && ceiling_amp > 0.0 {
maolan_engine::simd::clamp_inplace(samples, -ceiling_amp, ceiling_amp);
}
}
}
}
Ok(())
}
#[cfg(test)]
mod dither_tests {
use super::*;
#[test]
fn dither_is_applicable_for_integer_formats() {
assert!(dither_is_applicable(
ExportBitDepth::Int16,
ExportDither::Triangular
));
assert!(dither_is_applicable(
ExportBitDepth::Int24,
ExportDither::Rectangular
));
assert!(dither_is_applicable(
ExportBitDepth::Int32,
ExportDither::Triangular
));
}
#[test]
fn dither_is_not_applicable_for_none_or_float() {
assert!(!dither_is_applicable(
ExportBitDepth::Int16,
ExportDither::None
));
assert!(!dither_is_applicable(
ExportBitDepth::Float32,
ExportDither::Triangular
));
assert!(!dither_is_applicable(
ExportBitDepth::Float32,
ExportDither::None
));
}
#[test]
fn triangular_dither_changes_quantized_values() {
let buffer = vec![0.00001_f32; 64];
let (without_dither, _) =
quantize_samples_for_bit_depth(&buffer, ExportBitDepth::Int16, ExportDither::None);
assert!(without_dither.iter().all(|&s| s == 0));
let (with_dither, _) = quantize_samples_for_bit_depth(
&buffer,
ExportBitDepth::Int16,
ExportDither::Triangular,
);
assert_ne!(with_dither, without_dither);
let sum: i64 = with_dither.iter().map(|&s| s as i64).sum();
assert_ne!(sum, 0);
}
#[test]
fn rectangular_dither_is_zero_mean_over_many_samples() {
let buffer = vec![0.0_f32; 4096];
let (quantized, _) = quantize_samples_for_bit_depth(
&buffer,
ExportBitDepth::Int16,
ExportDither::Rectangular,
);
let sum: i64 = quantized.iter().map(|&s| s as i64).sum();
assert!(sum.abs() < 1000, "rectangular dither sum was {sum}");
}
#[test]
fn triangular_dither_is_zero_mean_over_many_samples() {
let buffer = vec![0.0_f32; 4096];
let (quantized, _) = quantize_samples_for_bit_depth(
&buffer,
ExportBitDepth::Int16,
ExportDither::Triangular,
);
let sum: i64 = quantized.iter().map(|&s| s as i64).sum();
assert!(sum.abs() < 1000, "triangular dither sum was {sum}");
}
#[test]
fn dither_preserves_silence_for_float_export() {
let buffer = vec![0.0_f32; 64];
let (with_dither, bits) = quantize_samples_for_bit_depth(
&buffer,
ExportBitDepth::Float32,
ExportDither::Triangular,
);
assert_eq!(bits, 24);
assert!(with_dither.iter().all(|&s| s == 0));
}
}