summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs83
-rw-r--r--src/main.rs199
2 files changed, 225 insertions, 57 deletions
diff --git a/src/lib.rs b/src/lib.rs
index 599694c..32eb119 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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()
+}