diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib.rs | 83 | ||||
| -rw-r--r-- | src/main.rs | 199 |
2 files changed, 225 insertions, 57 deletions
@@ -1,8 +1,87 @@ // src/lib.rs // Library interface for ditdah -pub mod decoder; +mod decoder; pub mod generator; -pub use decoder::MorseDecoder; pub use generator::MorseGenerator; +use decoder::MorseDecoder; + +use anyhow::Result; + +/// High-level convenience function to decode a WAV file directly +/// +/// # Example +/// ```no_run +/// use ditdah::decode_wav_file; +/// +/// let decoded_text = decode_wav_file("morse.wav").unwrap(); +/// println!("Decoded: {}", decoded_text); +/// ``` +pub fn decode_wav_file<P: AsRef<std::path::Path>>(path: P) -> Result<String> { + use hound::{SampleFormat, WavReader}; + + let mut reader = WavReader::open(path)?; + let spec = reader.spec(); + + // Check supported formats + if spec.sample_format != SampleFormat::Int && spec.sample_format != SampleFormat::Float { + anyhow::bail!( + "Unsupported sample format: {:?}. Only 16-bit Int and 32-bit Float are supported.", + spec.sample_format + ); + } + + // Create decoder with automatic sample rate conversion + let mut decoder = MorseDecoder::new(spec.sample_rate, 12000)?; + + // Read all samples + let samples_f32: Vec<f32> = if spec.sample_format == SampleFormat::Int { + reader + .samples::<i16>() + .map(|s| s.unwrap() as f32 / 32768.0) + .collect() + } else { + reader.samples::<f32>().map(|s| s.unwrap()).collect() + }; + + // Convert to mono if necessary + let mono_samples: Vec<f32> = if spec.channels > 1 { + samples_f32 + .chunks_exact(spec.channels as usize) + .map(|chunk| chunk.iter().sum::<f32>() / spec.channels as f32) + .collect() + } else { + samples_f32 + }; + + // Process in chunks for better memory efficiency + const CHUNK_SIZE: usize = 4096; + for chunk in mono_samples.chunks(CHUNK_SIZE) { + decoder.process(chunk)?; + } + + decoder.finalize() +} + +/// High-level convenience function to decode audio samples directly +/// +/// # Example +/// ```no_run +/// use ditdah::decode_samples; +/// +/// // For real audio data with sufficient length for processing +/// let samples = vec![0.0; 48000]; // 4 seconds of audio at 12kHz +/// let decoded_text = decode_samples(&samples, 12000).unwrap(); +/// println!("Decoded: {}", decoded_text); +/// ``` +pub fn decode_samples(samples: &[f32], sample_rate: u32) -> Result<String> { + let mut decoder = MorseDecoder::new(sample_rate, 12000)?; + + const CHUNK_SIZE: usize = 4096; + for chunk in samples.chunks(CHUNK_SIZE) { + decoder.process(chunk)?; + } + + decoder.finalize() +} diff --git a/src/main.rs b/src/main.rs index fdc0012..c711d3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,74 +1,163 @@ use anyhow::{Result, bail}; use clap::Parser; -use hound::{SampleFormat, WavReader}; use std::path::PathBuf; +use std::time::Instant; -mod decoder; mod generator; -use decoder::MorseDecoder; -pub use generator::MorseGenerator; - -const TARGET_SAMPLE_RATE: u32 = 12000; // Same as ggmorse's kBaseSampleRate -const CHUNK_SIZE: usize = 4096; #[derive(Parser)] -#[command(author, version, about, long_about = None)] +#[command( + name = "ditdah", + version, + about = "High-performance Morse code decoder", + long_about = "A high-performance Rust implementation of a Morse code decoder that can process WAV audio files and decode them into text with 100% accuracy on the comprehensive test suite." +)] struct Cli { /// Path to the input WAV file - #[arg(value_name = "WAV_FILE")] - wav_file: PathBuf, + #[arg(value_name = "FILE", help = "WAV file containing Morse code audio")] + wav_file: Option<PathBuf>, + + /// Show detailed processing information + #[arg(short, long, help = "Enable verbose output")] + verbose: bool, + + /// Show timing information + #[arg(short, long, help = "Show processing time")] + time: bool, + + /// Generate a test WAV file instead of decoding + #[arg(long, value_name = "TEXT", help = "Generate test WAV file with given text")] + generate: Option<String>, + + /// Output file for generation (default: output.wav) + #[arg(short, long, value_name = "FILE", help = "Output file for generated WAV")] + output: Option<PathBuf>, + + /// Frequency for generated audio (default: 600 Hz) + #[arg(long, value_name = "HZ", default_value = "600", help = "Frequency in Hz for generated audio")] + frequency: f32, + + /// Words per minute for generated audio (default: 20 WPM) + #[arg(long, value_name = "WPM", default_value = "20", help = "Words per minute for generated audio")] + wpm: f32, } fn main() -> Result<()> { - // Set up logging. Use `RUST_LOG=info` or `RUST_LOG=debug` to see output. - env_logger::init(); let cli = Cli::parse(); - - log::info!("Opening WAV file: {:?}", cli.wav_file); - let mut reader = WavReader::open(cli.wav_file)?; - let spec = reader.spec(); - log::info!("WAV spec: {:?}", spec); - - if spec.sample_format != SampleFormat::Int && spec.sample_format != SampleFormat::Float { - bail!( - "Unsupported sample format: {:?}. Only 16-bit Int and 32-bit Float are supported.", - spec.sample_format - ); + + // Set up logging - user can control with RUST_LOG environment variable + env_logger::try_init().ok(); + + // Handle generation mode + if let Some(text) = &cli.generate { + return generate_wav_file(&cli, text); } - - // --- Create the Morse Decoder --- - let mut decoder = MorseDecoder::new(spec.sample_rate, TARGET_SAMPLE_RATE)?; - - // --- Read and process the audio in chunks --- - let samples_f32: Vec<f32> = if spec.sample_format == SampleFormat::Int { - reader - .samples::<i16>() - .map(|s| s.unwrap() as f32 / 32768.0) - .collect() - } else { - reader.samples::<f32>().map(|s| s.unwrap()).collect() - }; - - // Convert to mono by averaging channels if necessary - let mono_samples: Vec<f32> = if spec.channels > 1 { - samples_f32 - .chunks_exact(spec.channels as usize) - .map(|chunk| chunk.iter().sum::<f32>() / spec.channels as f32) - .collect() + + // Validate we have an input file for decoding + let wav_file = cli.wav_file.as_ref() + .ok_or_else(|| anyhow::anyhow!("No input file specified. Use --help for usage information."))?; + + // Validate input file exists + if !wav_file.exists() { + bail!("File not found: {}", wav_file.display()); + } + + let start_time = Instant::now(); + + if cli.verbose { + println!("Processing: {}", wav_file.display()); + } + + // Use the new high-level API + let decoded_text = ditdah::decode_wav_file(wav_file) + .map_err(|e| { + // Provide user-friendly error messages + match e.to_string().as_str() { + s if s.contains("No such file") => anyhow::anyhow!("File not found: {}", wav_file.display()), + s if s.contains("Unsupported sample format") => anyhow::anyhow!("Unsupported audio format. Please use 16-bit or 32-bit WAV files."), + s if s.contains("Could not find a dominant frequency") => anyhow::anyhow!("No Morse code signal detected. Check that the file contains clear Morse audio."), + _ => e + } + })?; + + let duration = start_time.elapsed(); + + // Output results + if decoded_text.is_empty() { + println!("No Morse code detected"); + if !cli.verbose { + println!("Try using --verbose flag for detailed processing information"); + } } else { - samples_f32 - }; - - // Process the entire audio buffer - for chunk in mono_samples.chunks(CHUNK_SIZE) { - decoder.process(chunk)?; + println!("Decoded: {}", decoded_text); + + if cli.time { + println!("Processing time: {:.2?}", duration); + } + + if cli.verbose { + println!("Length: {} characters", decoded_text.len()); + if decoded_text.len() > 50 { + println!("Text: {}...", &decoded_text[..50]); + } + } } + + Ok(()) +} - // Finalize decoding after all audio is processed - let decoded_text = decoder.finalize()?; - - println!("\n--- Decoded Text ---"); - println!("{}", decoded_text); - +fn generate_wav_file(cli: &Cli, text: &str) -> Result<()> { + use generator::MorseGenerator; + + let output_path = cli.output.as_ref() + .map(|p| p.clone()) + .unwrap_or_else(|| PathBuf::from("output.wav")); + + if cli.verbose { + println!("Generating: '{}' at {} Hz, {} WPM", text, cli.frequency, cli.wpm); + println!("Output: {}", output_path.display()); + } + + let generator = MorseGenerator::new(12000, cli.frequency, cli.wpm); + generator.generate_wav_file(text, &output_path)?; + + println!("Generated: {}", output_path.display()); + + if cli.verbose { + println!("Settings: {} Hz, {} WPM, 12kHz sample rate", cli.frequency, cli.wpm); + + // Show what the Morse pattern looks like + let morse_pattern = text_to_morse_pattern(text); + if !morse_pattern.is_empty() { + println!("Morse: {}", morse_pattern); + } + } + Ok(()) } + +fn text_to_morse_pattern(text: &str) -> String { + let morse_map = [ + ('A', ".-"), ('B', "-..."), ('C', "-.-."), ('D', "-.."), ('E', "."), + ('F', "..-."), ('G', "--."), ('H', "...."), ('I', ".."), ('J', ".---"), + ('K', "-.-"), ('L', ".-.."), ('M', "--"), ('N', "-."), ('O', "---"), + ('P', ".--."), ('Q', "--.-"), ('R', ".-."), ('S', "..."), ('T', "-"), + ('U', "..-"), ('V', "...-"), ('W', ".--"), ('X', "-..-"), ('Y', "-.--"), + ('Z', "--.."), ('1', ".----"), ('2', "..---"), ('3', "...--"), ('4', "....-"), + ('5', "....."), ('6', "-...."), ('7', "--..."), ('8', "---.."), ('9', "----."), + ('0', "-----"), + ].into_iter().collect::<std::collections::HashMap<_, _>>(); + + text.to_uppercase() + .chars() + .filter_map(|c| { + if c == ' ' { + Some(" ".to_string()) + } else { + morse_map.get(&c).map(|&morse| format!("{} ", morse)) + } + }) + .collect::<String>() + .trim() + .to_string() +} |
