diff --git a/Cargo.lock b/Cargo.lock index ac5e1ba2e9..c74073690c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,6 +208,13 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "afconvert" +version = "0.1.0" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "agc" version = "0.1.0" @@ -18661,6 +18668,7 @@ dependencies = [ name = "tauri-plugin-fs-sync" version = "0.1.0" dependencies = [ + "afconvert", "assert_fs", "audio-utils", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 43d6ca1ae4..413862fbdc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ exclude = ["plugins/cli2", "plugins/db", "plugins/export"] [workspace.dependencies] hypr-aec = { path = "crates/aec", package = "aec" } +hypr-afconvert = { path = "crates/afconvert", package = "afconvert" } hypr-agc = { path = "crates/agc", package = "agc" } hypr-am = { path = "crates/am", package = "am" } hypr-am2 = { path = "crates/am2", package = "am2" } diff --git a/crates/afconvert/Cargo.toml b/crates/afconvert/Cargo.toml new file mode 100644 index 0000000000..fa04c105e4 --- /dev/null +++ b/crates/afconvert/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "afconvert" +version = "0.1.0" +edition = "2024" + +[dependencies] +thiserror = { workspace = true } diff --git a/crates/afconvert/src/lib.rs b/crates/afconvert/src/lib.rs new file mode 100644 index 0000000000..3ffd41e528 --- /dev/null +++ b/crates/afconvert/src/lib.rs @@ -0,0 +1,39 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("afconvert failed: {0}")] + Failed(String), +} + +pub fn to_wav(source_path: &Path) -> Result { + let file_stem = source_path + .file_stem() + .unwrap_or_default() + .to_string_lossy(); + let wav_path = std::env::temp_dir().join(format!( + "{}_afconvert_{}.wav", + file_stem, + std::process::id() + )); + + let output = Command::new("afconvert") + .arg("-f") + .arg("WAVE") + .arg("-d") + .arg("LEI16") + .arg(source_path) + .arg(&wav_path) + .output()?; + + if !output.status.success() { + let _ = std::fs::remove_file(&wav_path); + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(Error::Failed(stderr.into_owned())); + } + + Ok(wav_path) +} diff --git a/plugins/fs-sync/Cargo.toml b/plugins/fs-sync/Cargo.toml index 98f9796aa0..e3a48f7319 100644 --- a/plugins/fs-sync/Cargo.toml +++ b/plugins/fs-sync/Cargo.toml @@ -35,6 +35,7 @@ serde_yaml = { workspace = true } specta = { workspace = true, features = ["serde_json"] } glob = "0.3" +hypr-afconvert = { workspace = true } rayon = { workspace = true } rodio = { workspace = true, features = ["symphonia-all"] } diff --git a/plugins/fs-sync/src/audio.rs b/plugins/fs-sync/src/audio.rs index a9574f4619..ee40818490 100644 --- a/plugins/fs-sync/src/audio.rs +++ b/plugins/fs-sync/src/audio.rs @@ -68,7 +68,34 @@ pub fn import_audio( target_path: &Path, ) -> Result { let file = File::open(source_path)?; - let decoder = rodio::Decoder::try_from(file)?; + match rodio::Decoder::try_from(file) { + Ok(decoder) => import_audio_from_decoder(decoder, tmp_path, target_path), + Err(_original_err) => { + #[cfg(target_os = "macos")] + { + let wav_path = hypr_afconvert::to_wav(source_path) + .map_err(|e| AudioProcessingError::AfconvertFailed(e.to_string()))?; + let result = (|| { + let file = File::open(&wav_path)?; + let decoder = rodio::Decoder::try_from(file)?; + import_audio_from_decoder(decoder, tmp_path, target_path) + })(); + let _ = std::fs::remove_file(&wav_path); + result + } + #[cfg(not(target_os = "macos"))] + { + Err(_original_err.into()) + } + } + } +} + +fn import_audio_from_decoder( + decoder: rodio::Decoder, + tmp_path: &Path, + target_path: &Path, +) -> Result { let channel_count_raw = decoder.channels().max(1); let channel_count_u8 = u8::try_from(channel_count_raw).map_err(|_| { AudioProcessingError::UnsupportedChannelCount { diff --git a/plugins/fs-sync/src/error.rs b/plugins/fs-sync/src/error.rs index 0d27d15d51..07c32ecf7f 100644 --- a/plugins/fs-sync/src/error.rs +++ b/plugins/fs-sync/src/error.rs @@ -41,6 +41,8 @@ pub enum AudioProcessingError { EmptyInput, #[error("audio_import_invalid_target_rate")] InvalidTargetSampleRate, + #[error("audio_import_afconvert_failed: {0}")] + AfconvertFailed(String), } #[derive(Debug, thiserror::Error)]