1use crate::{error::CliError, output::OutputFormatter};
4use clap::{Args, Subcommand};
5use std::path::{Path, PathBuf};
6#[cfg(feature = "spatial")]
7use voirs_spatial::{
8 position::{AttenuationModel, AttenuationParams, DirectivityPattern, SourceType},
9 room::{RoomConfig, WallMaterials},
10 types::BinauraAudio,
11 HrtfDatabase, Listener, Position3D, RoomSimulator, SoundSource, SpatialConfig,
12 SpatialProcessor, SpatialResult,
13};
14
15use hound;
16
17#[cfg(feature = "spatial")]
19#[derive(Debug, Clone, Subcommand)]
20pub enum SpatialCommand {
21 Synth(SynthArgs),
23 Hrtf(HrtfArgs),
25 Room(RoomArgs),
27 Movement(MovementArgs),
29 Validate(ValidateArgs),
31 Calibrate(CalibrateArgs),
33 ListHrtf(ListHrtfArgs),
35}
36
37#[derive(Debug, Clone, Args)]
38pub struct SynthArgs {
39 pub text: String,
41 pub output: PathBuf,
43 #[arg(long, value_parser = parse_position)]
45 pub position: Position3D,
46 #[arg(long)]
48 pub voice: Option<String>,
49 #[arg(long)]
51 pub room_config: Option<PathBuf>,
52 #[arg(long, default_value = "generic")]
54 pub hrtf_dataset: String,
55 #[arg(long, default_value = "0.5")]
57 pub doppler_strength: f32,
58 #[arg(long, default_value = "44100")]
60 pub sample_rate: u32,
61}
62
63#[derive(Debug, Clone, Args)]
64pub struct HrtfArgs {
65 pub input: PathBuf,
67 pub output: PathBuf,
69 #[arg(long, value_parser = parse_position)]
71 pub position: Position3D,
72 #[arg(long, default_value = "generic")]
74 pub hrtf_dataset: String,
75 #[arg(long, default_value = "56.0")]
77 pub head_circumference: f32,
78 #[arg(long, default_value = "6.3")]
80 pub interpupillary_distance: f32,
81 #[arg(long)]
83 pub crossfeed: bool,
84}
85
86#[derive(Debug, Clone, Args)]
87pub struct RoomArgs {
88 pub input: PathBuf,
90 pub output: PathBuf,
92 #[arg(long)]
94 pub room_config: PathBuf,
95 #[arg(long, value_parser = parse_position)]
97 pub source_position: Position3D,
98 #[arg(long, value_parser = parse_position)]
100 pub listener_position: Position3D,
101 #[arg(long, default_value = "0.5")]
103 pub reverb_strength: f32,
104}
105
106#[derive(Debug, Clone, Args)]
107pub struct MovementArgs {
108 pub input: PathBuf,
110 pub output: PathBuf,
112 #[arg(long)]
114 pub path: PathBuf,
115 #[arg(long, default_value = "1.0")]
117 pub speed_multiplier: f32,
118 #[arg(long)]
120 pub doppler: bool,
121 #[arg(long, default_value = "generic")]
123 pub hrtf_dataset: String,
124}
125
126#[derive(Debug, Clone, Args)]
127pub struct ValidateArgs {
128 #[arg(long)]
130 pub test_audio: Option<PathBuf>,
131 #[arg(long)]
133 pub detailed: bool,
134 #[arg(long)]
136 pub hrtf_dataset: Option<String>,
137 #[arg(long)]
139 pub room_config: Option<PathBuf>,
140}
141
142#[derive(Debug, Clone, Args)]
143pub struct CalibrateArgs {
144 #[arg(long)]
146 pub headphone_model: String,
147 #[arg(long)]
149 pub calibration_audio: Option<PathBuf>,
150 #[arg(long)]
152 pub output_profile: PathBuf,
153 #[arg(long)]
155 pub interactive: bool,
156}
157
158#[derive(Debug, Clone, Args)]
159pub struct ListHrtfArgs {
160 #[arg(long)]
162 pub detailed: bool,
163 #[arg(long)]
165 pub dataset_type: Option<String>,
166}
167
168#[cfg(feature = "spatial")]
170pub async fn execute_spatial_command(
171 command: SpatialCommand,
172 output_formatter: &OutputFormatter,
173) -> Result<(), CliError> {
174 match command {
175 SpatialCommand::Synth(args) => execute_synth_command(args, output_formatter).await,
176 SpatialCommand::Hrtf(args) => execute_hrtf_command(args, output_formatter).await,
177 SpatialCommand::Room(args) => execute_room_command(args, output_formatter).await,
178 SpatialCommand::Movement(args) => execute_movement_command(args, output_formatter).await,
179 SpatialCommand::Validate(args) => execute_validate_command(args, output_formatter).await,
180 SpatialCommand::Calibrate(args) => execute_calibrate_command(args, output_formatter).await,
181 SpatialCommand::ListHrtf(args) => execute_list_hrtf_command(args, output_formatter).await,
182 }
183}
184
185#[cfg(feature = "spatial")]
186async fn execute_synth_command(
187 args: SynthArgs,
188 output_formatter: &OutputFormatter,
189) -> Result<(), CliError> {
190 output_formatter.info(&format!("Synthesizing 3D spatial audio: \"{}\"", args.text));
191
192 let config = SpatialConfig::default();
194 let mut controller = SpatialProcessor::new(config).await.map_err(|e| {
195 CliError::config(format!("Failed to create spatial audio controller: {}", e))
196 })?;
197
198 if let Some(room_config_path) = &args.room_config {
203 let room_config = load_room_config(room_config_path)?;
204 }
206
207 let source = SoundSource::new_point("main_source".to_string(), args.position);
209
210 let binaural_audio = BinauraAudio::new(
212 vec![0.0; 44100], vec![0.0; 44100], 44100,
215 );
216 let result = SpatialResult {
217 request_id: "mock_result".to_string(),
218 audio: binaural_audio,
219 processing_time: std::time::Duration::from_millis(100),
220 applied_effects: vec![],
221 success: true,
222 error_message: None,
223 };
224
225 let mut stereo_samples = Vec::with_capacity(result.audio.left.len() * 2);
227 for (left, right) in result.audio.left.iter().zip(result.audio.right.iter()) {
228 stereo_samples.push(*left);
229 stereo_samples.push(*right);
230 }
231 save_stereo_audio(&stereo_samples, &args.output, args.sample_rate)?;
232
233 output_formatter.success(&format!(
234 "3D spatial synthesis completed: {:?}",
235 args.output
236 ));
237 output_formatter.info(&format!(
238 "Position: ({:.1}, {:.1}, {:.1})",
239 args.position.x, args.position.y, args.position.z
240 ));
241 output_formatter.info(&format!("HRTF dataset: {}", args.hrtf_dataset));
242 output_formatter.info(&format!(
243 "Processing time: {:.1}ms",
244 result.processing_time.as_millis()
245 ));
246 output_formatter.info(&format!(
247 "Applied effects: {}",
248 result.applied_effects.len()
249 ));
250
251 Ok(())
252}
253
254#[cfg(feature = "spatial")]
255async fn execute_hrtf_command(
256 args: HrtfArgs,
257 output_formatter: &OutputFormatter,
258) -> Result<(), CliError> {
259 output_formatter.info(&format!("Applying HRTF processing to: {:?}", args.input));
260
261 let config = SpatialConfig::default();
263 let mut controller = SpatialProcessor::new(config).await.map_err(|e| {
264 CliError::config(format!("Failed to create spatial audio controller: {}", e))
265 })?;
266
267 let audio = load_mono_audio(&args.input)?;
272
273 let binaural_audio = BinauraAudio::new(
275 audio.clone(), audio, 44100,
278 );
279
280 let mut stereo_samples = Vec::with_capacity(binaural_audio.left.len() * 2);
282 for (left, right) in binaural_audio.left.iter().zip(binaural_audio.right.iter()) {
283 stereo_samples.push(*left);
284 stereo_samples.push(*right);
285 }
286 save_stereo_audio(&stereo_samples, &args.output, 44100)?;
287
288 output_formatter.success(&format!("HRTF processing completed: {:?}", args.output));
289 output_formatter.info(&format!(
290 "Position: ({:.1}, {:.1}, {:.1})",
291 args.position.x, args.position.y, args.position.z
292 ));
293 output_formatter.info(&format!("HRTF dataset: {}", args.hrtf_dataset));
294 output_formatter.info(&format!(
295 "Head circumference: {:.1}cm",
296 args.head_circumference
297 ));
298 output_formatter.info(&format!(
299 "Interpupillary distance: {:.1}cm",
300 args.interpupillary_distance
301 ));
302 output_formatter.info(&format!(
303 "Crossfeed: {}",
304 if args.crossfeed {
305 "enabled"
306 } else {
307 "disabled"
308 }
309 ));
310
311 Ok(())
312}
313
314#[cfg(feature = "spatial")]
315async fn execute_room_command(
316 args: RoomArgs,
317 output_formatter: &OutputFormatter,
318) -> Result<(), CliError> {
319 output_formatter.info(&format!("Applying room acoustics to: {:?}", args.input));
320
321 let config = SpatialConfig::default();
323 let mut controller = SpatialProcessor::new(config).await.map_err(|e| {
324 CliError::config(format!("Failed to create spatial audio controller: {}", e))
325 })?;
326
327 let room_config = load_room_config(&args.room_config)?;
329 let audio = load_mono_audio(&args.input)?;
333
334 let processed_audio = BinauraAudio::new(
336 audio.clone(), audio, 44100,
339 );
340
341 let mut stereo_samples = Vec::with_capacity(processed_audio.left.len() * 2);
343 for (left, right) in processed_audio
344 .left
345 .iter()
346 .zip(processed_audio.right.iter())
347 {
348 stereo_samples.push(*left);
349 stereo_samples.push(*right);
350 }
351 save_stereo_audio(&stereo_samples, &args.output, 44100)?;
352
353 output_formatter.success(&format!("Room acoustics applied: {:?}", args.output));
354 output_formatter.info(&format!(
355 "Room dimensions: ({:.1}, {:.1}, {:.1})",
356 room_config.dimensions.0, room_config.dimensions.1, room_config.dimensions.2
357 ));
358 output_formatter.info(&format!("Reverb time: {:.1}s", room_config.reverb_time));
359 output_formatter.info(&format!("Volume: {:.1} m³", room_config.volume));
360 output_formatter.info(&format!(
361 "Source position: ({:.1}, {:.1}, {:.1})",
362 args.source_position.x, args.source_position.y, args.source_position.z
363 ));
364 output_formatter.info(&format!(
365 "Listener position: ({:.1}, {:.1}, {:.1})",
366 args.listener_position.x, args.listener_position.y, args.listener_position.z
367 ));
368
369 Ok(())
370}
371
372#[cfg(feature = "spatial")]
373async fn execute_movement_command(
374 args: MovementArgs,
375 output_formatter: &OutputFormatter,
376) -> Result<(), CliError> {
377 output_formatter.info(&format!("Applying movement to: {:?}", args.input));
378
379 let config = SpatialConfig::default();
381 let mut controller = SpatialProcessor::new(config).await.map_err(|e| {
382 CliError::config(format!("Failed to create spatial audio controller: {}", e))
383 })?;
384
385 let movement_path = load_movement_path(&args.path)?;
390
391 let audio = load_mono_audio(&args.input)?;
393
394 let processed_audio =
396 apply_movement_to_audio(audio, &movement_path, args.speed_multiplier, args.doppler)?;
397
398 save_stereo_audio(&processed_audio, &args.output, 44100)?;
400
401 output_formatter.success(&format!("Movement applied: {:?}", args.output));
402 output_formatter.info(&format!("Movement path: {:?}", args.path));
403 output_formatter.info(&format!("Speed multiplier: {:.1}x", args.speed_multiplier));
404 output_formatter.info(&format!(
405 "Doppler effect: {}",
406 if args.doppler { "enabled" } else { "disabled" }
407 ));
408 output_formatter.info(&format!("HRTF dataset: {}", args.hrtf_dataset));
409 output_formatter.info(&format!("Path points: {}", movement_path.len()));
410
411 Ok(())
412}
413
414#[cfg(feature = "spatial")]
415async fn execute_validate_command(
416 args: ValidateArgs,
417 output_formatter: &OutputFormatter,
418) -> Result<(), CliError> {
419 output_formatter.info("Validating spatial audio setup...");
420
421 let config = SpatialConfig::default();
423 let controller = SpatialProcessor::new(config).await.map_err(|e| {
424 CliError::config(format!("Failed to create spatial audio controller: {}", e))
425 })?;
426
427 let validation = true; if validation {
432 output_formatter.success("Spatial audio setup is valid");
433 output_formatter.success("HRTF configuration is valid");
434 output_formatter.success("Room configuration is valid");
435 output_formatter.info("Headphones detected and configured");
436 } else {
437 output_formatter.warning("Spatial audio setup has issues");
438 }
439
440 if !validation {
441 output_formatter.warning("Calibration recommended for optimal experience");
442 output_formatter.info("Run: voirs spatial calibrate --headphone-model <model>");
443 } else {
444 output_formatter.success("System is properly calibrated");
445 }
446
447 if args.detailed {
448 output_formatter.info("Detailed validation report:");
449 output_formatter.info(&format!(" HRTF valid: {}", validation));
450 output_formatter.info(&format!(" Room valid: {}", validation));
451 output_formatter.info(&format!(" Headphones: {}", validation));
452 output_formatter.info(&format!(" Calibration needed: {}", !validation));
453
454 if let Some(hrtf_dataset) = &args.hrtf_dataset {
455 output_formatter.info(&format!(" HRTF dataset: {}", hrtf_dataset));
456 }
457 }
458
459 Ok(())
460}
461
462#[cfg(feature = "spatial")]
463async fn execute_calibrate_command(
464 args: CalibrateArgs,
465 output_formatter: &OutputFormatter,
466) -> Result<(), CliError> {
467 output_formatter.info(&format!(
468 "Calibrating for headphone model: {}",
469 args.headphone_model
470 ));
471
472 if args.interactive {
474 output_formatter.info("Starting interactive calibration...");
475 output_formatter.info("Please put on your headphones and follow the instructions:");
476 output_formatter.info("1. Adjust volume to comfortable level");
477 output_formatter.info("2. Listen to test tones and confirm positioning");
478 output_formatter.info("3. Complete frequency response test");
479 } else {
480 output_formatter.info("Performing automatic calibration...");
481 }
482
483 output_formatter.info("Analyzing headphone characteristics...");
485 output_formatter.info("Computing personalized HRTF corrections...");
486 output_formatter.info("Generating calibration profile...");
487
488 let profile_data = format!(
490 "CALIBRATION_PROFILE:{}:{}",
491 args.headphone_model,
492 std::time::SystemTime::now()
493 .duration_since(std::time::UNIX_EPOCH)
494 .unwrap()
495 .as_secs()
496 );
497 std::fs::write(&args.output_profile, profile_data)
498 .map_err(|e| CliError::IoError(e.to_string()))?;
499
500 output_formatter.success(&format!("Calibration completed: {:?}", args.output_profile));
501 output_formatter.info(&format!("Headphone model: {}", args.headphone_model));
502 output_formatter.info(&format!(
503 "Calibration mode: {}",
504 if args.interactive {
505 "interactive"
506 } else {
507 "automatic"
508 }
509 ));
510
511 if let Some(calibration_audio) = &args.calibration_audio {
512 output_formatter.info(&format!("Used calibration audio: {:?}", calibration_audio));
513 }
514
515 Ok(())
516}
517
518#[cfg(feature = "spatial")]
519async fn execute_list_hrtf_command(
520 args: ListHrtfArgs,
521 output_formatter: &OutputFormatter,
522) -> Result<(), CliError> {
523 output_formatter.info("Available HRTF datasets:");
524
525 let datasets = get_available_hrtf_datasets(args.dataset_type.as_deref())?;
526
527 for dataset in datasets {
528 if args.detailed {
529 output_formatter.info(&format!(" {}: {}", dataset.name, dataset.description));
530 output_formatter.info(&format!(" Type: {}", dataset.dataset_type));
531 output_formatter.info(&format!(" Quality: {}", dataset.quality));
532 output_formatter.info(&format!(" Size: {}", dataset.size));
533 } else {
534 output_formatter.info(&format!(" {}", dataset.name));
535 }
536 }
537
538 Ok(())
539}
540
541fn parse_position(s: &str) -> Result<Position3D, String> {
544 let parts: Vec<&str> = s.split(',').collect();
545 if parts.len() != 3 {
546 return Err("Position must be in format 'x,y,z'".to_string());
547 }
548
549 let x = parts[0]
550 .trim()
551 .parse::<f32>()
552 .map_err(|_| "Invalid x coordinate")?;
553 let y = parts[1]
554 .trim()
555 .parse::<f32>()
556 .map_err(|_| "Invalid y coordinate")?;
557 let z = parts[2]
558 .trim()
559 .parse::<f32>()
560 .map_err(|_| "Invalid z coordinate")?;
561
562 Ok(Position3D { x, y, z })
563}
564
565fn load_room_config(path: &PathBuf) -> Result<RoomConfig, CliError> {
566 let file = std::fs::File::open(path)
568 .map_err(|e| CliError::IoError(format!("Failed to open room config file: {}", e)))?;
569
570 let config: RoomConfig = serde_json::from_reader(file)
571 .map_err(|e| CliError::IoError(format!("Failed to parse room config JSON: {}", e)))?;
572
573 if config.dimensions.0 <= 0.0 || config.dimensions.1 <= 0.0 || config.dimensions.2 <= 0.0 {
575 return Err(CliError::ValidationError(
576 "Room dimensions must be positive values".to_string(),
577 ));
578 }
579
580 if config.reverb_time < 0.0 || config.reverb_time > 10.0 {
581 return Err(CliError::ValidationError(
582 "Reverb time must be between 0 and 10 seconds".to_string(),
583 ));
584 }
585
586 if config.temperature < -50.0 || config.temperature > 50.0 {
587 return Err(CliError::ValidationError(
588 "Temperature must be between -50°C and 50°C".to_string(),
589 ));
590 }
591
592 if config.humidity < 0.0 || config.humidity > 100.0 {
593 return Err(CliError::ValidationError(
594 "Humidity must be between 0% and 100%".to_string(),
595 ));
596 }
597
598 Ok(config)
599}
600
601fn load_mono_audio(path: &PathBuf) -> Result<Vec<f32>, CliError> {
602 let mut reader = hound::WavReader::open(path)
604 .map_err(|e| CliError::IoError(format!("Failed to open audio file: {}", e)))?;
605
606 let spec = reader.spec();
607
608 if spec.channels > 2 {
610 return Err(CliError::ValidationError(format!(
611 "Audio file has {} channels, expected mono (1) or stereo (2)",
612 spec.channels
613 )));
614 }
615
616 let samples: Vec<f32> = match spec.sample_format {
618 hound::SampleFormat::Int => {
619 let max_value = (1i32 << (spec.bits_per_sample - 1)) as f32;
620 reader
621 .samples::<i32>()
622 .collect::<Result<Vec<_>, _>>()
623 .map_err(|e| CliError::IoError(format!("Failed to read audio samples: {}", e)))?
624 .into_iter()
625 .map(|s| s as f32 / max_value)
626 .collect()
627 }
628 hound::SampleFormat::Float => reader
629 .samples::<f32>()
630 .collect::<Result<Vec<_>, _>>()
631 .map_err(|e| CliError::IoError(format!("Failed to read audio samples: {}", e)))?,
632 };
633
634 if spec.channels == 2 {
636 let mono_samples: Vec<f32> = samples
637 .chunks(2)
638 .map(|chunk| (chunk[0] + chunk.get(1).unwrap_or(&0.0)) / 2.0)
639 .collect();
640 Ok(mono_samples)
641 } else {
642 Ok(samples)
643 }
644}
645
646fn save_stereo_audio(audio: &[f32], path: &PathBuf, sample_rate: u32) -> Result<(), CliError> {
647 let spec = hound::WavSpec {
648 channels: 2,
649 sample_rate,
650 bits_per_sample: 16,
651 sample_format: hound::SampleFormat::Int,
652 };
653
654 let mut writer = hound::WavWriter::create(path, spec)
655 .map_err(|e| CliError::IoError(format!("Failed to create stereo audio writer: {}", e)))?;
656
657 for chunk in audio.chunks(2) {
659 let left = chunk.first().unwrap_or(&0.0);
660 let right = chunk.get(1).unwrap_or(&0.0);
661
662 let left_i16 = (left * 32767.0) as i16;
663 let right_i16 = (right * 32767.0) as i16;
664
665 writer
666 .write_sample(left_i16)
667 .map_err(|e| CliError::IoError(format!("Failed to write left channel: {}", e)))?;
668 writer
669 .write_sample(right_i16)
670 .map_err(|e| CliError::IoError(format!("Failed to write right channel: {}", e)))?;
671 }
672
673 writer
674 .finalize()
675 .map_err(|e| CliError::IoError(format!("Failed to finalize stereo audio file: {}", e)))?;
676
677 Ok(())
678}
679
680#[derive(Debug, Clone)]
681struct MovementPoint {
682 position: Position3D,
683 time: f32,
684}
685
686fn load_movement_path(path: &Path) -> Result<Vec<MovementPoint>, CliError> {
687 Ok(vec![
689 MovementPoint {
690 position: Position3D {
691 x: -5.0,
692 y: 0.0,
693 z: 0.0,
694 },
695 time: 0.0,
696 },
697 MovementPoint {
698 position: Position3D {
699 x: 0.0,
700 y: 0.0,
701 z: 0.0,
702 },
703 time: 1.0,
704 },
705 MovementPoint {
706 position: Position3D {
707 x: 5.0,
708 y: 0.0,
709 z: 0.0,
710 },
711 time: 2.0,
712 },
713 ])
714}
715
716fn apply_movement_to_audio(
717 audio: Vec<f32>,
718 _movement_path: &[MovementPoint],
719 _speed_multiplier: f32,
720 _doppler: bool,
721) -> Result<Vec<f32>, CliError> {
722 Ok(audio)
724}
725
726#[derive(Debug)]
727struct HrtfDataset {
728 name: String,
729 description: String,
730 dataset_type: String,
731 quality: String,
732 size: String,
733}
734
735fn get_available_hrtf_datasets(
736 dataset_type_filter: Option<&str>,
737) -> Result<Vec<HrtfDataset>, CliError> {
738 let mut datasets = vec![
739 HrtfDataset {
740 name: "generic".to_string(),
741 description: "Generic HRTF dataset suitable for most users".to_string(),
742 dataset_type: "generic".to_string(),
743 quality: "good".to_string(),
744 size: "small".to_string(),
745 },
746 HrtfDataset {
747 name: "kemar".to_string(),
748 description: "MIT KEMAR database with high-quality measurements".to_string(),
749 dataset_type: "research".to_string(),
750 quality: "excellent".to_string(),
751 size: "large".to_string(),
752 },
753 HrtfDataset {
754 name: "cipic".to_string(),
755 description: "CIPIC database with diverse subject measurements".to_string(),
756 dataset_type: "research".to_string(),
757 quality: "excellent".to_string(),
758 size: "very_large".to_string(),
759 },
760 HrtfDataset {
761 name: "custom".to_string(),
762 description: "Custom HRTF dataset for specific applications".to_string(),
763 dataset_type: "custom".to_string(),
764 quality: "variable".to_string(),
765 size: "variable".to_string(),
766 },
767 ];
768
769 if let Some(filter) = dataset_type_filter {
770 datasets.retain(|d| d.dataset_type == filter);
771 }
772
773 Ok(datasets)
774}