diff --git a/Cargo.lock b/Cargo.lock index 7af12e6..e52033c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,6 +211,15 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -738,6 +747,16 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libflac-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b01d08e4f670c184ffe3c3e5efbf0bd6e88c8cca796c278c86bbf49583545c" +dependencies = [ + "cmake", + "libc", +] + [[package]] name = "libtlg-rs" version = "0.2.2" @@ -878,6 +897,7 @@ dependencies = [ "int-enum", "json", "lazy_static", + "libflac-sys", "libtlg-rs", "markup5ever", "markup5ever_rcdom", @@ -905,8 +925,6 @@ dependencies = [ [[package]] name = "msg_tool_macro" version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e18f79ab2d0d7a8685961a7d2007f53570ff988aa0d363503a190eef08c983a1" dependencies = [ "quote", "syn", diff --git a/Cargo.toml b/Cargo.toml index 916eb0a..865b8d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,12 +21,13 @@ flate2 = { version = "1.1", optional = true } int-enum = { version = "1.2", optional = true } json = { version = "0.12", optional = true } lazy_static = "1.5.0" +libflac-sys = { version = "0.3", optional = true } libtlg-rs = { version = "0.2", optional = true, features = ["encode"] } markup5ever = { version = "0.35", optional = true } markup5ever_rcdom = { version = "0.35", optional = true } memchr = { version = "2.7", optional = true } mozjpeg = { version = "0.10", optional = true } -msg_tool_macro = { version = "0.1.6" } +msg_tool_macro = { path = "./msg_tool_macro" } overf = "0.1" pelite = { version = "0.10", optional = true } png = { version = "0.17", optional = true } @@ -44,7 +45,7 @@ xml5ever = { version = "0.35", optional = true } zstd = { version = "0.13", optional = true } [features] -default = ["all-fmt", "image-jpg", "image-webp"] +default = ["all-fmt", "image-jpg", "image-webp", "audio-flac"] all-fmt = ["all-script", "all-img", "all-arc", "all-audio"] all-script = ["artemis", "artemis-panmimisoft", "bgi", "cat-system", "circus", "entis-gls", "escude", "ex-hibit", "hexen-haus", "kirikiri", "softpal", "will-plus", "yaneurao", "yaneurao-itufuru"] all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "kirikiri-img"] @@ -62,7 +63,7 @@ cat-system-arc = ["cat-system", "pelite", "utils-blowfish", "utils-crc32"] cat-system-img = ["cat-system", "flate2", "image", "mozjpeg", "utils-bit-stream"] circus = [] circus-arc = ["circus"] -circus-audio = ["circus", "flate2", "int-enum", "utils-pcm"] +circus-audio = ["circus", "flate2", "int-enum", "lossless-audio"] circus-img = ["circus", "image", "flate2", "zstd"] emote-img = ["emote-psb", "image", "libtlg-rs", "url"] entis-gls = ["xml5ever", "markup5ever", "markup5ever_rcdom"] @@ -80,6 +81,8 @@ yaneurao-itufuru = ["yaneurao"] image = ["png"] image-jpg = ["mozjpeg"] image-webp = ["webp"] +lossless-audio = ["utils-pcm"] +audio-flac = ["libflac-sys"] unstable = ["msg_tool_macro/unstable"] # utils feature utils-bit-stream = [] diff --git a/msg_tool_macro/src/lib.rs b/msg_tool_macro/src/lib.rs index 3785e78..e215b3b 100644 --- a/msg_tool_macro/src/lib.rs +++ b/msg_tool_macro/src/lib.rs @@ -660,3 +660,120 @@ pub fn gen_artemis_arc_ext(_: TokenStream) -> TokenStream { }; output.into() } + +/// A procedural macro for `#[derive(Default)]` that supports a `#[default(expr)]` attribute. +/// +/// This macro automatically implements the `Default` trait for a struct or an enum. +/// If a field or enum variant does not have the `#[default(expr)]` attribute, it will +/// use `Default::default()`. If the attribute is present, it will use the specified +/// expression as the default value. +#[proc_macro_derive(Default, attributes(default))] +pub fn default_macro_derive(input: TokenStream) -> TokenStream { + // Parse the input tokens into a syntax tree + let ast = syn::parse_macro_input!(input as syn::DeriveInput); + let name = &ast.ident; + let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + + let default_body = match &ast.data { + syn::Data::Struct(data_struct) => { + // Handle struct fields + let field_defaults = data_struct.fields.iter().map(|f| { + let name = &f.ident; + // Find the `#[default(...)]` attribute + let default_value = if let Some(default_attr) = + f.attrs.iter().find(|attr| attr.path().is_ident("default")) + { + // Parse the expression inside the attribute's parentheses + if let Ok(value) = default_attr.parse_args::() { + quote::quote! { #value } + } else { + // If parsing fails, panic with a descriptive error + panic!("Invalid `#[default]` attribute syntax"); + } + } else { + // If no `#[default]` attribute is present, fall back to `Default::default()` + quote::quote! { Default::default() } + }; + quote::quote! { + #name: #default_value, + } + }); + + match &data_struct.fields { + syn::Fields::Named(_) => quote::quote! { Self { #(#field_defaults)* } }, + syn::Fields::Unnamed(_) => quote::quote! { Self(#(#field_defaults)*) }, + syn::Fields::Unit => quote::quote! { Self }, + } + } + syn::Data::Enum(data_enum) => { + // Handle enum variants + // Find the single variant with the `#[default]` attribute + if let Some(default_variant) = data_enum + .variants + .iter() + .find(|v| v.attrs.iter().any(|attr| attr.path().is_ident("default"))) + { + let variant_name = &default_variant.ident; + match &default_variant.fields { + syn::Fields::Unit => quote::quote! { Self::#variant_name }, + syn::Fields::Unnamed(fields_unnamed) => { + let field_defaults = fields_unnamed.unnamed.iter().map(|f| { + let default_value = if let Some(default_attr) = + f.attrs.iter().find(|attr| attr.path().is_ident("default")) + { + // Parse the expression inside the attribute's parentheses + if let Ok(value) = default_attr.parse_args::() { + quote::quote! { #value } + } else { + // If parsing fails, panic with a descriptive error + panic!("Invalid `#[default]` attribute syntax"); + } + } else { + // If no `#[default]` attribute is present, fall back to `Default::default()` + quote::quote! { Default::default() } + }; + quote::quote! { #default_value } + }); + quote::quote! { Self::#variant_name(#(#field_defaults)*) } + } + syn::Fields::Named(fields_named) => { + let field_defaults = fields_named.named.iter().map(|f| { + let name = &f.ident; + let default_value = if let Some(default_attr) = + f.attrs.iter().find(|attr| attr.path().is_ident("default")) + { + // Parse the expression inside the attribute's parentheses + if let Ok(value) = default_attr.parse_args::() { + quote::quote! { #value } + } else { + // If parsing fails, panic with a descriptive error + panic!("Invalid `#[default]` attribute syntax"); + } + } else { + // If no `#[default]` attribute is present, fall back to `Default::default()` + quote::quote! { Default::default() } + }; + quote::quote! { #name: #default_value } + }); + quote::quote! { Self::#variant_name { #(#field_defaults)* } } + } + } + } else { + // Enums must have exactly one default variant + panic!("Enum must have one variant with `#[default]` attribute."); + } + } + syn::Data::Union(_) => panic!("`Default` macro cannot be derived for unions"), + }; + + // Construct the final `impl Default for ...` block + quote::quote! { + #[automatically_derived] + impl #impl_generics Default for #name #ty_generics #where_clause { + fn default() -> Self { + #default_body + } + } + } + .into() +} diff --git a/src/args.rs b/src/args.rs index 7df9301..8f02c0e 100644 --- a/src/args.rs +++ b/src/args.rs @@ -45,6 +45,19 @@ fn parse_webp_quality(quality: &str) -> Result { clap_num::number_range(quality, 0, 100) } +#[cfg(feature = "audio-flac")] +fn parse_flac_compression_level(level: &str) -> Result { + let lower = level.to_ascii_lowercase(); + if lower == "fast" { + return Ok(0); + } else if lower == "best" { + return Ok(8); + } else if lower == "default" { + return Ok(5); + } + clap_num::number_range(level, 0, 8) +} + /// Tools for export and import scripts #[derive(Parser, Debug)] #[clap( @@ -369,6 +382,14 @@ pub struct Arg { /// Use another parser to parse the script. /// Should only be used when the default parser not works well. pub will_plus_ws2_no_disasm: bool, + #[cfg(feature = "lossless-audio")] + #[arg(short = 'l', long, global = true, value_enum, default_value_t = LosslessAudioFormat::Wav)] + /// Audio format for output lossless audio files. + pub lossless_audio_fmt: LosslessAudioFormat, + #[cfg(feature = "audio-flac")] + #[arg(short = 'L', long, global = true, default_value_t = 5, value_parser = parse_flac_compression_level)] + /// FLAC compression level for output FLAC audio files. 0 means fastest compression, 8 means best compression. + pub flac_compression_level: u32, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/main.rs b/src/main.rs index 71b684a..033bffc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1771,6 +1771,10 @@ fn main() { ), #[cfg(feature = "artemis-panmimisoft")] artemis_panmimisoft_txt_lang: arg.artemis_panmimisoft_txt_lang.clone(), + #[cfg(feature = "lossless-audio")] + lossless_audio_fmt: arg.lossless_audio_fmt, + #[cfg(feature = "audio-flac")] + flac_compression_level: arg.flac_compression_level, }; match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/circus/audio/pcm.rs b/src/scripts/circus/audio/pcm.rs index b08bb60..2ff7a2b 100644 --- a/src/scripts/circus/audio/pcm.rs +++ b/src/scripts/circus/audio/pcm.rs @@ -2,6 +2,7 @@ use crate::ext::io::*; use crate::scripts::base::*; use crate::types::*; +use crate::utils::lossless_audio::*; use crate::utils::pcm::*; use crate::utils::struct_pack::*; use anyhow::Result; @@ -117,6 +118,7 @@ impl Header { pub struct Pcm { header: Header, data: MemReader, + config: ExtraConfig, } impl Pcm { @@ -124,7 +126,7 @@ impl Pcm { /// /// * `reader` - The reader to read the PCM data from. /// * `config` - Extra configuration options. - pub fn new(mut reader: R, _config: &ExtraConfig) -> Result { + pub fn new(mut reader: R, config: &ExtraConfig) -> Result { let mut magic = [0u8; 4]; reader.read_exact(&mut magic)?; if &magic != b"XPCM" { @@ -155,6 +157,7 @@ impl Pcm { Ok(Self { header, data: MemReader::new(data), + config: config.clone(), }) } @@ -205,7 +208,7 @@ impl Script for Pcm { if self.header.mode() == 5 { "ogg" } else { - "wav" + self.config.lossless_audio_fmt.as_ref() } } @@ -219,7 +222,7 @@ impl Script for Pcm { .pcm .as_ref() .ok_or_else(|| anyhow::anyhow!("PCM format not found in header"))?; - write_pcm(fmt, self.data.to_ref(), writer)?; + write_audio(fmt, self.data.to_ref(), writer, &self.config)?; } Ok(()) } diff --git a/src/types.rs b/src/types.rs index d70f3b2..ad02260 100644 --- a/src/types.rs +++ b/src/types.rs @@ -206,7 +206,7 @@ impl AsRef for CircusMesType { } /// Extra configuration options for the script. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, msg_tool_macro::Default)] pub struct ExtraConfig { #[cfg(feature = "circus")] /// Circus Game for circus MES script. @@ -291,6 +291,7 @@ pub struct ExtraConfig { /// If not specified, the first language will be used. pub cat_system_cstl_lang: Option, #[cfg(feature = "flate2")] + #[default(6)] /// Zlib compression level. 0 means no compression, 9 means best compression. pub zlib_compression_level: u32, #[cfg(feature = "image")] @@ -303,6 +304,7 @@ pub struct ExtraConfig { /// Use zstd compression for Circus CRX images. (CIRCUS Engine don't support this. Hook is required.) pub circus_crx_zstd: bool, #[cfg(feature = "zstd")] + #[default(3)] /// Zstd compression level. 0 means default compression level (3), 22 means best compression. pub zstd_compression_level: i32, #[cfg(feature = "circus-img")] @@ -322,12 +324,14 @@ pub struct ExtraConfig { /// ExHibit def.rld xor keys. pub ex_hibit_rld_def_keys: Option>, #[cfg(feature = "mozjpeg")] + #[default(80)] /// JPEG quality for output images, 0-100. 100 means best quality. pub jpeg_quality: u8, #[cfg(feature = "webp")] /// Use WebP lossless compression for output images. pub webp_lossless: bool, #[cfg(feature = "webp")] + #[default(80)] /// WebP quality for output images, 0-100. 100 means best quality. pub webp_quality: u8, #[cfg(feature = "circus-img")] @@ -352,6 +356,13 @@ pub struct ExtraConfig { /// Specify the language of Artemis TXT (ぱんみみそふと) script. /// If not specified, the first language will be used. pub artemis_panmimisoft_txt_lang: Option, + #[cfg(feature = "lossless-audio")] + /// Audio format for output lossless audio files. + pub lossless_audio_fmt: LosslessAudioFormat, + #[cfg(feature = "audio-flac")] + #[default(5)] + /// FLAC compression level for output FLAC audio files. 0 means fastest compression, 8 means best compression. Default level is 5. + pub flac_compression_level: u32, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] @@ -753,3 +764,63 @@ impl PngCompressionLevel { } } } + +#[cfg(feature = "lossless-audio")] +#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] +/// Lossless audio format +pub enum LosslessAudioFormat { + /// Wav + Wav, + #[cfg(feature = "audio-flac")] + /// FLAC Format + Flac, +} + +impl Default for LosslessAudioFormat { + fn default() -> Self { + LosslessAudioFormat::Wav + } +} + +#[cfg(feature = "lossless-audio")] +impl TryFrom<&str> for LosslessAudioFormat { + type Error = anyhow::Error; + /// Try to convert a extension string to an `LosslessAudioFormat`. + /// Extensions are case-insensitive. + fn try_from(value: &str) -> Result { + match value.to_ascii_lowercase().as_str() { + "wav" => Ok(LosslessAudioFormat::Wav), + #[cfg(feature = "audio-flac")] + "flac" => Ok(LosslessAudioFormat::Flac), + _ => Err(anyhow::anyhow!( + "Unsupported lossless audio format: {}", + value + )), + } + } +} + +#[cfg(feature = "lossless-audio")] +impl TryFrom<&std::path::Path> for LosslessAudioFormat { + type Error = anyhow::Error; + + fn try_from(value: &std::path::Path) -> Result { + if let Some(ext) = value.extension() { + Self::try_from(ext.to_string_lossy().as_ref()) + } else { + Err(anyhow::anyhow!("No extension found in path")) + } + } +} + +#[cfg(feature = "lossless-audio")] +impl AsRef for LosslessAudioFormat { + /// Returns the extension for the lossless audio format. + fn as_ref(&self) -> &str { + match self { + LosslessAudioFormat::Wav => "wav", + #[cfg(feature = "audio-flac")] + LosslessAudioFormat::Flac => "flac", + } + } +} diff --git a/src/utils/flac.rs b/src/utils/flac.rs new file mode 100644 index 0000000..2dd1ff9 --- /dev/null +++ b/src/utils/flac.rs @@ -0,0 +1,192 @@ +//! FLAC audio utilities. +use super::pcm::*; +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use anyhow::Result; +use libflac_sys::*; +use std::ffi::CStr; +use std::io::{Read, Seek, Write}; + +extern "C" fn write_callback( + _encoder: *const FLAC__StreamEncoder, + buffer: *const u8, + bytes: usize, + _samples: u32, + _current_frame: u32, + client_data: *mut std::ffi::c_void, +) -> FLAC__StreamEncoderWriteStatus { + let writer = unsafe { &mut *(client_data as *mut &mut dyn WriteSeek) }; + let slice = unsafe { std::slice::from_raw_parts(buffer, bytes) }; + match writer.write_all(slice) { + Ok(_) => FLAC__STREAM_ENCODER_WRITE_STATUS_OK, + Err(_) => FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR, + } +} + +extern "C" fn tell_callback( + _encoder: *const FLAC__StreamEncoder, + absolute_byte_offset: *mut u64, + client_data: *mut std::ffi::c_void, +) -> FLAC__StreamEncoderTellStatus { + if absolute_byte_offset.is_null() { + return FLAC__STREAM_ENCODER_TELL_STATUS_ERROR; + } + let writer = unsafe { &mut *(client_data as *mut &mut dyn WriteSeek) }; + match writer.stream_position() { + Ok(pos) => { + unsafe { + *absolute_byte_offset = pos; + } + FLAC__STREAM_ENCODER_TELL_STATUS_OK + } + Err(_) => FLAC__STREAM_ENCODER_TELL_STATUS_ERROR, + } +} + +extern "C" fn seek_callback( + _encoder: *const FLAC__StreamEncoder, + absolute_byte_offset: u64, + client_data: *mut std::ffi::c_void, +) -> FLAC__StreamEncoderSeekStatus { + let writer = unsafe { &mut *(client_data as *mut &mut dyn WriteSeek) }; + match writer.seek(std::io::SeekFrom::Start(absolute_byte_offset)) { + Ok(_) => FLAC__STREAM_ENCODER_SEEK_STATUS_OK, + Err(_) => FLAC__STREAM_ENCODER_SEEK_STATUS_ERROR, + } +} + +fn handle_init_error(status: u32) -> Result<()> { + if status == 0 { + return Ok(()); + } + let index = status as usize; + let s = unsafe { CStr::from_ptr(FLAC__StreamEncoderInitStatusString[index]) }; + Err(anyhow::anyhow!( + "FLAC encoder error: {}", + s.to_string_lossy() + )) +} + +struct EncoderHandle { + encoder: *mut FLAC__StreamEncoder, +} + +impl Drop for EncoderHandle { + fn drop(&mut self) { + unsafe { + FLAC__stream_encoder_delete(self.encoder); + } + } +} + +/// Writes lossless audio data to a flac file. +/// +/// * `header` - The PCM format header. +/// * `reader` - The reader to read audio data from. +/// * `writer` - The writer to write audio data to. +/// * `config` - Extra configuration options. +pub fn write_flac( + header: &PcmFormat, + mut reader: R, + mut writer: W, + config: &ExtraConfig, +) -> Result<()> { + if header.bits_per_sample > 32 { + return Err(anyhow::anyhow!( + "FLAC supports up to 32 bits per sample, got {}", + header.bits_per_sample + )); + } + let encoder = unsafe { FLAC__stream_encoder_new() }; + if encoder.is_null() { + return Err(anyhow::anyhow!("Failed to create FLAC encoder")); + } + let encoder = EncoderHandle { encoder }; + unsafe { + FLAC__stream_encoder_set_channels(encoder.encoder, header.channels as u32); + FLAC__stream_encoder_set_compression_level(encoder.encoder, config.flac_compression_level); + FLAC__stream_encoder_set_bits_per_sample(encoder.encoder, header.bits_per_sample as u32); + FLAC__stream_encoder_set_sample_rate(encoder.encoder, header.sample_rate); + FLAC__stream_encoder_set_verify(encoder.encoder, 1); + } + let mut raw_writer: &mut dyn WriteSeek = &mut writer; + let raw_writer = &mut raw_writer as *mut _; + handle_init_error(unsafe { + FLAC__stream_encoder_init_stream( + encoder.encoder, + Some(write_callback), + Some(seek_callback), + Some(tell_callback), + None, + raw_writer as *mut std::ffi::c_void, + ) + })?; + let mut buf = Vec::::with_capacity(1024 * header.channels as usize); + buf.resize(buf.capacity(), 0); + let mut read_buf = Vec::::with_capacity( + (header.bits_per_sample / 8) as usize * 1024 * header.channels as usize, + ); + read_buf.resize(read_buf.capacity(), 0); + loop { + let readed = reader.read(&mut read_buf)?; + if readed == 0 { + break; + } + let mut r = MemReaderRef::new(&read_buf[..readed]); + let samples = + readed as usize / (header.bits_per_sample as usize / 8) / header.channels as usize; + let mut i = 0; + for _ in 0..samples { + for _ in 0..header.channels { + let sample = match header.bits_per_sample { + 8 => r.read_i8()? as i32, + 16 => r.read_i16()? as i32, + 24 => { + let b1 = r.read_u8()? as i32; + let b2 = r.read_u8()? as i32; + let b3 = r.read_u8()? as i32; + let mut val = (b3 << 16) | (b2 << 8) | b1; + // Sign extend from 24 bits to 32 + if val & 0x800000 != 0 { + val |= !0xffffff; + } + val + } + 32 => r.read_i32()?, + _ => { + return Err(anyhow::anyhow!( + "Unsupported bits per sample: {}", + header.bits_per_sample + )); + } + }; + buf[i] = sample; + i += 1; + } + } + if samples == 0 { + break; + } + if unsafe { + FLAC__stream_encoder_process_interleaved(encoder.encoder, buf.as_ptr(), samples as u32) + } == 0 + { + let state = unsafe { FLAC__stream_encoder_get_state(encoder.encoder) }; + let s = unsafe { CStr::from_ptr(FLAC__StreamEncoderStateString[state as usize]) }; + return Err(anyhow::anyhow!( + "FLAC encoding error: {}", + s.to_string_lossy() + )); + } + } + if unsafe { FLAC__stream_encoder_finish(encoder.encoder) } == 0 { + let state = unsafe { FLAC__stream_encoder_get_state(encoder.encoder) }; + let s = unsafe { CStr::from_ptr(FLAC__StreamEncoderStateString[state as usize]) }; + return Err(anyhow::anyhow!( + "FLAC encoding error: {}", + s.to_string_lossy() + )); + } + Ok(()) +} diff --git a/src/utils/lossless_audio.rs b/src/utils/lossless_audio.rs new file mode 100644 index 0000000..48f6e78 --- /dev/null +++ b/src/utils/lossless_audio.rs @@ -0,0 +1,19 @@ +//! Lossless audio utilities. +use super::flac::*; +use super::pcm::*; +use crate::types::*; +use anyhow::Result; +use std::io::{Read, Seek, Write}; + +pub fn write_audio( + header: &PcmFormat, + reader: R, + writer: W, + config: &ExtraConfig, +) -> Result<()> { + match config.lossless_audio_fmt { + LosslessAudioFormat::Wav => write_pcm(header, reader, writer)?, + LosslessAudioFormat::Flac => write_flac(header, reader, writer, config)?, + } + Ok(()) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 20d38f6..d9d82c2 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -12,8 +12,12 @@ mod encoding_win; #[cfg(feature = "utils-escape")] pub mod escape; pub mod files; +#[cfg(feature = "audio-flac")] +pub mod flac; #[cfg(feature = "image")] pub mod img; +#[cfg(feature = "lossless-audio")] +pub mod lossless_audio; mod macros; pub mod name_replacement; #[cfg(feature = "utils-pcm")]