summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md67
-rw-r--r--src/decoder.rs1
-rw-r--r--src/lib.rs28
-rw-r--r--src/main.rs167
-rw-r--r--tests/integration_tests.rs3
5 files changed, 173 insertions, 93 deletions
diff --git a/README.md b/README.md
index a19d4ae..258d170 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,8 @@ A high-performance Rust implementation of a Morse code decoder that can process
## Features
- **High Accuracy**: Achieves 100% pass rate on comprehensive test suite
+- **Clean Library API**: High-level functions for easy integration (`decode_wav_file`, `decode_samples`)
+- **Full-Featured CLI**: Decode files, generate test audio, verbose output, timing information
- **Audio Processing**: Supports WAV files with various sample rates (12kHz, 44.1kHz) and formats
- **Signal Processing**: Uses FFT-based pitch detection, Goertzel filtering, and adaptive threshold detection
- **Self-Calibrating**: Automatically determines timing, WPM, and optimal thresholds
@@ -37,33 +39,45 @@ cargo build --release
### Command Line Interface
-Decode a WAV file containing Morse code:
-
+**Decode a WAV file:**
```bash
cargo run -- input.wav
```
-With debug output:
+**With verbose output and timing:**
+```bash
+cargo run -- input.wav --verbose --time
+```
+
+**Generate test Morse code WAV files:**
+```bash
+cargo run -- --generate "SOS" --verbose
+cargo run -- --generate "HELLO WORLD" --output test.wav --frequency 800 --wpm 25
+```
+
+**With debug logging:**
```bash
RUST_LOG=info cargo run -- input.wav
```
### Library Usage
+**High-level API (recommended):**
```rust
-use ditdah::{MorseDecoder, MorseGenerator};
-
-// Create a decoder
-let mut decoder = MorseDecoder::new(44100, 12000)?; // source_rate, target_rate
+use ditdah::{decode_wav_file, decode_samples, MorseGenerator};
-// Process audio chunks
-for chunk in audio_chunks {
- decoder.process(&chunk)?;
-}
+// Decode a WAV file directly
+let decoded_text = decode_wav_file("morse.wav")?;
+println!("Decoded: {}", decoded_text);
-// Get decoded text
-let decoded_text = decoder.finalize()?;
+// Decode audio samples directly
+let samples: Vec<f32> = /* your audio data */;
+let decoded_text = decode_samples(&samples, 12000)?;
println!("Decoded: {}", decoded_text);
+
+// Generate Morse code WAV files
+let generator = MorseGenerator::new(12000, 600.0, 20.0);
+generator.generate_wav_file("SOS", "output.wav")?;
```
## Testing
@@ -113,30 +127,35 @@ The decoder uses a sophisticated multi-stage approach:
- **Adaptive gap detection**: Distinguishes element gaps, letter gaps, and word gaps
- **Robust parameter estimation**: Works across different speeds and frequencies
-## Configuration
+## Library API
-Key constants in `src/decoder.rs`:
+The library provides a clean, high-level API:
```rust
-const FREQ_MIN_HZ: f32 = 200.0; // Minimum frequency to detect
-const FREQ_MAX_HZ: f32 = 1200.0; // Maximum frequency to detect
-const DIT_DAH_BOUNDARY: f32 = 2.0; // Threshold between dots and dashes
-const LETTER_SPACE_BOUNDARY: f32 = 2.0; // Threshold to end current letter
-const WORD_SPACE_BOUNDARY: f32 = 5.0; // Threshold to add word space
+pub fn decode_wav_file<P: AsRef<std::path::Path>>(path: P) -> Result<String>
+pub fn decode_samples(samples: &[f32], sample_rate: u32) -> Result<String>
+pub use generator::MorseGenerator;
```
+**All signal processing complexity is handled internally** - the library automatically:
+- Detects audio format and converts to the required sample rate
+- Performs frequency analysis and filtering
+- Calibrates timing parameters
+- Decodes Morse patterns to text
+
## Project Structure
```
ditdah/
├── src/
│ ├── main.rs # CLI application
-│ ├── lib.rs # Library interface
-│ ├── decoder.rs # Core Morse decoder logic
-│ └── generator.rs # Morse code generator (for testing)
+│ ├── lib.rs # Public library API
+│ ├── decoder.rs # Internal Morse decoder implementation
+│ └── generator.rs # Public Morse code generator
├── tests/
│ └── integration_tests.rs # Comprehensive test suite
-├── Cargo.toml # Project configuration
+├── .github/workflows/ # CI pipeline
+├── Cargo.toml # Rust 2024 edition project configuration
├── LICENSE # MIT License
└── README.md # This file
```
diff --git a/src/decoder.rs b/src/decoder.rs
index 1a217d3..4bf075b 100644
--- a/src/decoder.rs
+++ b/src/decoder.rs
@@ -245,7 +245,6 @@ impl MorseDecoder {
}
// --- The complex analysis functions below are unchanged ---
-
fn detect_pitch_stft(&self) -> Result<f32> {
let fft_size = 4096;
let step_size = fft_size / 4;
diff --git a/src/lib.rs b/src/lib.rs
index 32eb119..df2665e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,26 +4,26 @@
mod decoder;
pub mod generator;
-pub use generator::MorseGenerator;
use decoder::MorseDecoder;
+pub use generator::MorseGenerator;
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!(
@@ -31,10 +31,10 @@ pub fn decode_wav_file<P: AsRef<std::path::Path>>(path: P) -> Result<String> {
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
@@ -44,7 +44,7 @@ pub fn decode_wav_file<P: AsRef<std::path::Path>>(path: P) -> Result<String> {
} 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
@@ -54,22 +54,22 @@ pub fn decode_wav_file<P: AsRef<std::path::Path>>(path: P) -> Result<String> {
} 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();
@@ -77,11 +77,11 @@ pub fn decode_wav_file<P: AsRef<std::path::Path>>(path: P) -> Result<String> {
/// ```
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 c711d3d..98429ee 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,72 +16,97 @@ struct Cli {
/// Path to the input WAV file
#[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")]
+ #[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")]
+ #[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")]
+ #[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")]
+ #[arg(
+ long,
+ value_name = "WPM",
+ default_value = "20",
+ help = "Words per minute for generated audio"
+ )]
wpm: f32,
}
fn main() -> Result<()> {
let cli = Cli::parse();
-
+
// 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);
}
-
+
// 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."))?;
-
+ 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 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");
@@ -90,11 +115,11 @@ fn main() -> Result<()> {
}
} else {
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 {
@@ -102,52 +127,90 @@ fn main() -> Result<()> {
}
}
}
-
+
Ok(())
}
fn generate_wav_file(cli: &Cli, text: &str) -> Result<()> {
use generator::MorseGenerator;
-
- let output_path = cli.output.as_ref()
+
+ 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!(
+ "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);
-
+ 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', "----."),
+ ('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<_, _>>();
-
+ ]
+ .into_iter()
+ .collect::<std::collections::HashMap<_, _>>();
+
text.to_uppercase()
.chars()
.filter_map(|c| {
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index 4bc0887..a68fd70 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -2,10 +2,9 @@
// Comprehensive integration tests for the Morse decoder
use anyhow::Result;
-use ditdah::{decode_wav_file, MorseGenerator};
+use ditdah::{MorseGenerator, decode_wav_file};
use std::{fs, io::Write};
-
#[derive(Debug)]
struct TestCase {
name: &'static str,