use crate::{error::CliError, output::OutputFormatter};
use clap::{Args, Subcommand};
use std::path::{Path, PathBuf};
#[cfg(feature = "spatial")]
use voirs_spatial::{
position::{AttenuationModel, AttenuationParams, DirectivityPattern, SourceType},
room::{RoomConfig, WallMaterials},
types::BinauraAudio,
HrtfDatabase, Listener, Position3D, RoomSimulator, SoundSource, SpatialConfig,
SpatialProcessor, SpatialResult,
};
use hound;
#[cfg(feature = "spatial")]
#[derive(Debug, Clone, Subcommand)]
pub enum SpatialCommand {
Synth(SynthArgs),
Hrtf(HrtfArgs),
Room(RoomArgs),
Movement(MovementArgs),
Validate(ValidateArgs),
Calibrate(CalibrateArgs),
ListHrtf(ListHrtfArgs),
}
#[derive(Debug, Clone, Args)]
pub struct SynthArgs {
pub text: String,
pub output: PathBuf,
#[arg(long, value_parser = parse_position)]
pub position: Position3D,
#[arg(long)]
pub voice: Option<String>,
#[arg(long)]
pub room_config: Option<PathBuf>,
#[arg(long, default_value = "generic")]
pub hrtf_dataset: String,
#[arg(long, default_value = "0.5")]
pub doppler_strength: f32,
#[arg(long, default_value = "44100")]
pub sample_rate: u32,
}
#[derive(Debug, Clone, Args)]
pub struct HrtfArgs {
pub input: PathBuf,
pub output: PathBuf,
#[arg(long, value_parser = parse_position)]
pub position: Position3D,
#[arg(long, default_value = "generic")]
pub hrtf_dataset: String,
#[arg(long, default_value = "56.0")]
pub head_circumference: f32,
#[arg(long, default_value = "6.3")]
pub interpupillary_distance: f32,
#[arg(long)]
pub crossfeed: bool,
}
#[derive(Debug, Clone, Args)]
pub struct RoomArgs {
pub input: PathBuf,
pub output: PathBuf,
#[arg(long)]
pub room_config: PathBuf,
#[arg(long, value_parser = parse_position)]
pub source_position: Position3D,
#[arg(long, value_parser = parse_position)]
pub listener_position: Position3D,
#[arg(long, default_value = "0.5")]
pub reverb_strength: f32,
}
#[derive(Debug, Clone, Args)]
pub struct MovementArgs {
pub input: PathBuf,
pub output: PathBuf,
#[arg(long)]
pub path: PathBuf,
#[arg(long, default_value = "1.0")]
pub speed_multiplier: f32,
#[arg(long)]
pub doppler: bool,
#[arg(long, default_value = "generic")]
pub hrtf_dataset: String,
}
#[derive(Debug, Clone, Args)]
pub struct ValidateArgs {
#[arg(long)]
pub test_audio: Option<PathBuf>,
#[arg(long)]
pub detailed: bool,
#[arg(long)]
pub hrtf_dataset: Option<String>,
#[arg(long)]
pub room_config: Option<PathBuf>,
}
#[derive(Debug, Clone, Args)]
pub struct CalibrateArgs {
#[arg(long)]
pub headphone_model: String,
#[arg(long)]
pub calibration_audio: Option<PathBuf>,
#[arg(long)]
pub output_profile: PathBuf,
#[arg(long)]
pub interactive: bool,
}
#[derive(Debug, Clone, Args)]
pub struct ListHrtfArgs {
#[arg(long)]
pub detailed: bool,
#[arg(long)]
pub dataset_type: Option<String>,
}
#[cfg(feature = "spatial")]
pub async fn execute_spatial_command(
command: SpatialCommand,
output_formatter: &OutputFormatter,
) -> Result<(), CliError> {
match command {
SpatialCommand::Synth(args) => execute_synth_command(args, output_formatter).await,
SpatialCommand::Hrtf(args) => execute_hrtf_command(args, output_formatter).await,
SpatialCommand::Room(args) => execute_room_command(args, output_formatter).await,
SpatialCommand::Movement(args) => execute_movement_command(args, output_formatter).await,
SpatialCommand::Validate(args) => execute_validate_command(args, output_formatter).await,
SpatialCommand::Calibrate(args) => execute_calibrate_command(args, output_formatter).await,
SpatialCommand::ListHrtf(args) => execute_list_hrtf_command(args, output_formatter).await,
}
}
#[cfg(feature = "spatial")]
async fn execute_synth_command(
args: SynthArgs,
output_formatter: &OutputFormatter,
) -> Result<(), CliError> {
output_formatter.info(&format!("Synthesizing 3D spatial audio: \"{}\"", args.text));
let config = SpatialConfig::default();
let mut controller = SpatialProcessor::new(config).await.map_err(|e| {
CliError::config(format!("Failed to create spatial audio controller: {}", e))
})?;
if let Some(room_config_path) = &args.room_config {
let room_config = load_room_config(room_config_path)?;
}
let source = SoundSource::new_point("main_source".to_string(), args.position);
let binaural_audio = BinauraAudio::new(
vec![0.0; 44100], vec![0.0; 44100], 44100,
);
let result = SpatialResult {
request_id: "mock_result".to_string(),
audio: binaural_audio,
processing_time: std::time::Duration::from_millis(100),
applied_effects: vec![],
success: true,
error_message: None,
};
let mut stereo_samples = Vec::with_capacity(result.audio.left.len() * 2);
for (left, right) in result.audio.left.iter().zip(result.audio.right.iter()) {
stereo_samples.push(*left);
stereo_samples.push(*right);
}
save_stereo_audio(&stereo_samples, &args.output, args.sample_rate)?;
output_formatter.success(&format!(
"3D spatial synthesis completed: {:?}",
args.output
));
output_formatter.info(&format!(
"Position: ({:.1}, {:.1}, {:.1})",
args.position.x, args.position.y, args.position.z
));
output_formatter.info(&format!("HRTF dataset: {}", args.hrtf_dataset));
output_formatter.info(&format!(
"Processing time: {:.1}ms",
result.processing_time.as_millis()
));
output_formatter.info(&format!(
"Applied effects: {}",
result.applied_effects.len()
));
Ok(())
}
#[cfg(feature = "spatial")]
async fn execute_hrtf_command(
args: HrtfArgs,
output_formatter: &OutputFormatter,
) -> Result<(), CliError> {
output_formatter.info(&format!("Applying HRTF processing to: {:?}", args.input));
let config = SpatialConfig::default();
let mut controller = SpatialProcessor::new(config).await.map_err(|e| {
CliError::config(format!("Failed to create spatial audio controller: {}", e))
})?;
let audio = load_mono_audio(&args.input)?;
let binaural_audio = BinauraAudio::new(
audio.clone(), audio, 44100,
);
let mut stereo_samples = Vec::with_capacity(binaural_audio.left.len() * 2);
for (left, right) in binaural_audio.left.iter().zip(binaural_audio.right.iter()) {
stereo_samples.push(*left);
stereo_samples.push(*right);
}
save_stereo_audio(&stereo_samples, &args.output, 44100)?;
output_formatter.success(&format!("HRTF processing completed: {:?}", args.output));
output_formatter.info(&format!(
"Position: ({:.1}, {:.1}, {:.1})",
args.position.x, args.position.y, args.position.z
));
output_formatter.info(&format!("HRTF dataset: {}", args.hrtf_dataset));
output_formatter.info(&format!(
"Head circumference: {:.1}cm",
args.head_circumference
));
output_formatter.info(&format!(
"Interpupillary distance: {:.1}cm",
args.interpupillary_distance
));
output_formatter.info(&format!(
"Crossfeed: {}",
if args.crossfeed {
"enabled"
} else {
"disabled"
}
));
Ok(())
}
#[cfg(feature = "spatial")]
async fn execute_room_command(
args: RoomArgs,
output_formatter: &OutputFormatter,
) -> Result<(), CliError> {
output_formatter.info(&format!("Applying room acoustics to: {:?}", args.input));
let config = SpatialConfig::default();
let mut controller = SpatialProcessor::new(config).await.map_err(|e| {
CliError::config(format!("Failed to create spatial audio controller: {}", e))
})?;
let room_config = load_room_config(&args.room_config)?;
let audio = load_mono_audio(&args.input)?;
let processed_audio = BinauraAudio::new(
audio.clone(), audio, 44100,
);
let mut stereo_samples = Vec::with_capacity(processed_audio.left.len() * 2);
for (left, right) in processed_audio
.left
.iter()
.zip(processed_audio.right.iter())
{
stereo_samples.push(*left);
stereo_samples.push(*right);
}
save_stereo_audio(&stereo_samples, &args.output, 44100)?;
output_formatter.success(&format!("Room acoustics applied: {:?}", args.output));
output_formatter.info(&format!(
"Room dimensions: ({:.1}, {:.1}, {:.1})",
room_config.dimensions.0, room_config.dimensions.1, room_config.dimensions.2
));
output_formatter.info(&format!("Reverb time: {:.1}s", room_config.reverb_time));
output_formatter.info(&format!("Volume: {:.1} m³", room_config.volume));
output_formatter.info(&format!(
"Source position: ({:.1}, {:.1}, {:.1})",
args.source_position.x, args.source_position.y, args.source_position.z
));
output_formatter.info(&format!(
"Listener position: ({:.1}, {:.1}, {:.1})",
args.listener_position.x, args.listener_position.y, args.listener_position.z
));
Ok(())
}
#[cfg(feature = "spatial")]
async fn execute_movement_command(
args: MovementArgs,
output_formatter: &OutputFormatter,
) -> Result<(), CliError> {
output_formatter.info(&format!("Applying movement to: {:?}", args.input));
let config = SpatialConfig::default();
let mut controller = SpatialProcessor::new(config).await.map_err(|e| {
CliError::config(format!("Failed to create spatial audio controller: {}", e))
})?;
let movement_path = load_movement_path(&args.path)?;
let audio = load_mono_audio(&args.input)?;
let processed_audio =
apply_movement_to_audio(audio, &movement_path, args.speed_multiplier, args.doppler)?;
save_stereo_audio(&processed_audio, &args.output, 44100)?;
output_formatter.success(&format!("Movement applied: {:?}", args.output));
output_formatter.info(&format!("Movement path: {:?}", args.path));
output_formatter.info(&format!("Speed multiplier: {:.1}x", args.speed_multiplier));
output_formatter.info(&format!(
"Doppler effect: {}",
if args.doppler { "enabled" } else { "disabled" }
));
output_formatter.info(&format!("HRTF dataset: {}", args.hrtf_dataset));
output_formatter.info(&format!("Path points: {}", movement_path.len()));
Ok(())
}
#[cfg(feature = "spatial")]
async fn execute_validate_command(
args: ValidateArgs,
output_formatter: &OutputFormatter,
) -> Result<(), CliError> {
output_formatter.info("Validating spatial audio setup...");
let config = SpatialConfig::default();
let controller = SpatialProcessor::new(config).await.map_err(|e| {
CliError::config(format!("Failed to create spatial audio controller: {}", e))
})?;
let validation = true;
if validation {
output_formatter.success("Spatial audio setup is valid");
output_formatter.success("HRTF configuration is valid");
output_formatter.success("Room configuration is valid");
output_formatter.info("Headphones detected and configured");
} else {
output_formatter.warning("Spatial audio setup has issues");
}
if !validation {
output_formatter.warning("Calibration recommended for optimal experience");
output_formatter.info("Run: voirs spatial calibrate --headphone-model <model>");
} else {
output_formatter.success("System is properly calibrated");
}
if args.detailed {
output_formatter.info("Detailed validation report:");
output_formatter.info(&format!(" HRTF valid: {}", validation));
output_formatter.info(&format!(" Room valid: {}", validation));
output_formatter.info(&format!(" Headphones: {}", validation));
output_formatter.info(&format!(" Calibration needed: {}", !validation));
if let Some(hrtf_dataset) = &args.hrtf_dataset {
output_formatter.info(&format!(" HRTF dataset: {}", hrtf_dataset));
}
}
Ok(())
}
#[cfg(feature = "spatial")]
async fn execute_calibrate_command(
args: CalibrateArgs,
output_formatter: &OutputFormatter,
) -> Result<(), CliError> {
output_formatter.info(&format!(
"Calibrating for headphone model: {}",
args.headphone_model
));
if args.interactive {
output_formatter.info("Starting interactive calibration...");
output_formatter.info("Please put on your headphones and follow the instructions:");
output_formatter.info("1. Adjust volume to comfortable level");
output_formatter.info("2. Listen to test tones and confirm positioning");
output_formatter.info("3. Complete frequency response test");
} else {
output_formatter.info("Performing automatic calibration...");
}
output_formatter.info("Analyzing headphone characteristics...");
output_formatter.info("Computing personalized HRTF corrections...");
output_formatter.info("Generating calibration profile...");
let profile_data = format!(
"CALIBRATION_PROFILE:{}:{}",
args.headphone_model,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("SystemTime should be after UNIX_EPOCH")
.as_secs()
);
std::fs::write(&args.output_profile, profile_data)
.map_err(|e| CliError::IoError(e.to_string()))?;
output_formatter.success(&format!("Calibration completed: {:?}", args.output_profile));
output_formatter.info(&format!("Headphone model: {}", args.headphone_model));
output_formatter.info(&format!(
"Calibration mode: {}",
if args.interactive {
"interactive"
} else {
"automatic"
}
));
if let Some(calibration_audio) = &args.calibration_audio {
output_formatter.info(&format!("Used calibration audio: {:?}", calibration_audio));
}
Ok(())
}
#[cfg(feature = "spatial")]
async fn execute_list_hrtf_command(
args: ListHrtfArgs,
output_formatter: &OutputFormatter,
) -> Result<(), CliError> {
output_formatter.info("Available HRTF datasets:");
let datasets = get_available_hrtf_datasets(args.dataset_type.as_deref())?;
for dataset in datasets {
if args.detailed {
output_formatter.info(&format!(" {}: {}", dataset.name, dataset.description));
output_formatter.info(&format!(" Type: {}", dataset.dataset_type));
output_formatter.info(&format!(" Quality: {}", dataset.quality));
output_formatter.info(&format!(" Size: {}", dataset.size));
} else {
output_formatter.info(&format!(" {}", dataset.name));
}
}
Ok(())
}
fn parse_position(s: &str) -> Result<Position3D, String> {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() != 3 {
return Err("Position must be in format 'x,y,z'".to_string());
}
let x = parts[0]
.trim()
.parse::<f32>()
.map_err(|_| "Invalid x coordinate")?;
let y = parts[1]
.trim()
.parse::<f32>()
.map_err(|_| "Invalid y coordinate")?;
let z = parts[2]
.trim()
.parse::<f32>()
.map_err(|_| "Invalid z coordinate")?;
Ok(Position3D { x, y, z })
}
fn load_room_config(path: &PathBuf) -> Result<RoomConfig, CliError> {
let file = std::fs::File::open(path)
.map_err(|e| CliError::IoError(format!("Failed to open room config file: {}", e)))?;
let config: RoomConfig = serde_json::from_reader(file)
.map_err(|e| CliError::IoError(format!("Failed to parse room config JSON: {}", e)))?;
if config.dimensions.0 <= 0.0 || config.dimensions.1 <= 0.0 || config.dimensions.2 <= 0.0 {
return Err(CliError::ValidationError(
"Room dimensions must be positive values".to_string(),
));
}
if config.reverb_time < 0.0 || config.reverb_time > 10.0 {
return Err(CliError::ValidationError(
"Reverb time must be between 0 and 10 seconds".to_string(),
));
}
if config.temperature < -50.0 || config.temperature > 50.0 {
return Err(CliError::ValidationError(
"Temperature must be between -50°C and 50°C".to_string(),
));
}
if config.humidity < 0.0 || config.humidity > 100.0 {
return Err(CliError::ValidationError(
"Humidity must be between 0% and 100%".to_string(),
));
}
Ok(config)
}
fn load_mono_audio(path: &PathBuf) -> Result<Vec<f32>, CliError> {
let mut reader = hound::WavReader::open(path)
.map_err(|e| CliError::IoError(format!("Failed to open audio file: {}", e)))?;
let spec = reader.spec();
if spec.channels > 2 {
return Err(CliError::ValidationError(format!(
"Audio file has {} channels, expected mono (1) or stereo (2)",
spec.channels
)));
}
let samples: Vec<f32> = match spec.sample_format {
hound::SampleFormat::Int => {
let max_value = (1i32 << (spec.bits_per_sample - 1)) as f32;
reader
.samples::<i32>()
.collect::<Result<Vec<_>, _>>()
.map_err(|e| CliError::IoError(format!("Failed to read audio samples: {}", e)))?
.into_iter()
.map(|s| s as f32 / max_value)
.collect()
}
hound::SampleFormat::Float => reader
.samples::<f32>()
.collect::<Result<Vec<_>, _>>()
.map_err(|e| CliError::IoError(format!("Failed to read audio samples: {}", e)))?,
};
if spec.channels == 2 {
let mono_samples: Vec<f32> = samples
.chunks(2)
.map(|chunk| (chunk[0] + chunk.get(1).unwrap_or(&0.0)) / 2.0)
.collect();
Ok(mono_samples)
} else {
Ok(samples)
}
}
fn save_stereo_audio(audio: &[f32], path: &PathBuf, sample_rate: u32) -> Result<(), CliError> {
let spec = hound::WavSpec {
channels: 2,
sample_rate,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let mut writer = hound::WavWriter::create(path, spec)
.map_err(|e| CliError::IoError(format!("Failed to create stereo audio writer: {}", e)))?;
for chunk in audio.chunks(2) {
let left = chunk.first().unwrap_or(&0.0);
let right = chunk.get(1).unwrap_or(&0.0);
let left_i16 = (left * 32767.0) as i16;
let right_i16 = (right * 32767.0) as i16;
writer
.write_sample(left_i16)
.map_err(|e| CliError::IoError(format!("Failed to write left channel: {}", e)))?;
writer
.write_sample(right_i16)
.map_err(|e| CliError::IoError(format!("Failed to write right channel: {}", e)))?;
}
writer
.finalize()
.map_err(|e| CliError::IoError(format!("Failed to finalize stereo audio file: {}", e)))?;
Ok(())
}
#[derive(Debug, Clone)]
struct MovementPoint {
position: Position3D,
time: f32,
}
fn load_movement_path(path: &Path) -> Result<Vec<MovementPoint>, CliError> {
Ok(vec![
MovementPoint {
position: Position3D {
x: -5.0,
y: 0.0,
z: 0.0,
},
time: 0.0,
},
MovementPoint {
position: Position3D {
x: 0.0,
y: 0.0,
z: 0.0,
},
time: 1.0,
},
MovementPoint {
position: Position3D {
x: 5.0,
y: 0.0,
z: 0.0,
},
time: 2.0,
},
])
}
fn apply_movement_to_audio(
audio: Vec<f32>,
_movement_path: &[MovementPoint],
_speed_multiplier: f32,
_doppler: bool,
) -> Result<Vec<f32>, CliError> {
Ok(audio)
}
#[derive(Debug)]
struct HrtfDataset {
name: String,
description: String,
dataset_type: String,
quality: String,
size: String,
}
fn get_available_hrtf_datasets(
dataset_type_filter: Option<&str>,
) -> Result<Vec<HrtfDataset>, CliError> {
let mut datasets = vec![
HrtfDataset {
name: "generic".to_string(),
description: "Generic HRTF dataset suitable for most users".to_string(),
dataset_type: "generic".to_string(),
quality: "good".to_string(),
size: "small".to_string(),
},
HrtfDataset {
name: "kemar".to_string(),
description: "MIT KEMAR database with high-quality measurements".to_string(),
dataset_type: "research".to_string(),
quality: "excellent".to_string(),
size: "large".to_string(),
},
HrtfDataset {
name: "cipic".to_string(),
description: "CIPIC database with diverse subject measurements".to_string(),
dataset_type: "research".to_string(),
quality: "excellent".to_string(),
size: "very_large".to_string(),
},
HrtfDataset {
name: "custom".to_string(),
description: "Custom HRTF dataset for specific applications".to_string(),
dataset_type: "custom".to_string(),
quality: "variable".to_string(),
size: "variable".to_string(),
},
];
if let Some(filter) = dataset_type_filter {
datasets.retain(|d| d.dataset_type == filter);
}
Ok(datasets)
}