Skip to content

Commit a2d6b08

Browse files
committed
Implement waveform envelope visualization with peak and RMS analysis
1 parent 9cffc87 commit a2d6b08

4 files changed

Lines changed: 293 additions & 3 deletions

File tree

examples/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This creates a `samples/` directory with test WAV files including sine waves, ch
2020
- **onset_detection.rs** - Detect note onsets and transients in audio
2121
- **cached_analysis.rs** - Using the cache system for faster repeated analysis
2222
- **batch_processing.rs** - Process multiple files in parallel
23+
- **envelope_visualization.rs** - Generate waveform visualization with peak and RMS envelopes
2324
- **generate_samples.rs** - Generate test WAV files for the examples
2425

2526
## Running Examples
@@ -33,8 +34,11 @@ cargo run --example spectral_analysis
3334
cargo run --example onset_detection
3435
cargo run --example cached_analysis
3536
cargo run --example batch_processing
37+
cargo run --example envelope_visualization
3638
```
3739

40+
The `envelope_visualization` example creates a PNG image showing waveform with peak and RMS envelopes.
41+
3842
## MCP Server
3943

4044
The `mcp_client.rs` example shows the JSON format for MCP tool calls. To use the actual MCP server:

examples/envelope_visualization.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use ferrous_waves::visualization::waveform::render_waveform_with_envelope;
2+
use std::fs;
3+
4+
fn main() -> Result<(), Box<dyn std::error::Error>> {
5+
// Make sure samples directory exists
6+
fs::create_dir_all("samples")?;
7+
8+
println!("Generating envelope visualization example...");
9+
10+
// Create an interesting audio signal with varying dynamics
11+
let sample_rate = 44100;
12+
let duration = 3.0;
13+
let num_samples = (sample_rate as f32 * duration) as usize;
14+
let mut samples = Vec::with_capacity(num_samples);
15+
16+
for i in 0..num_samples {
17+
let t = i as f32 / sample_rate as f32;
18+
19+
// Create an ADSR-like envelope
20+
let envelope = if t < 0.1 {
21+
// Attack: quick rise
22+
t * 10.0
23+
} else if t < 0.3 {
24+
// Decay: fall to sustain level
25+
1.0 - (t - 0.1) * 2.0
26+
} else if t < 2.0 {
27+
// Sustain: hold at 60% with some variation
28+
0.6 + (t * 5.0).sin() * 0.1
29+
} else {
30+
// Release: fade out
31+
(3.0 - t).max(0.0) * 0.6
32+
};
33+
34+
// Mix multiple frequencies for richer sound
35+
let carrier = (2.0 * std::f32::consts::PI * 440.0 * t).sin();
36+
let modulator = (2.0 * std::f32::consts::PI * 110.0 * t).sin() * 0.3;
37+
let high = (2.0 * std::f32::consts::PI * 880.0 * t).sin() * 0.2;
38+
39+
let sample = (carrier + modulator + high) * envelope * 0.5;
40+
samples.push(sample);
41+
}
42+
43+
// Generate the envelope visualization
44+
let output_path = "samples/envelope_example.png";
45+
render_waveform_with_envelope(&samples, output_path.as_ref(), 1200, 400)?;
46+
47+
println!("✓ Created envelope visualization at: {}", output_path);
48+
println!();
49+
println!("The visualization shows:");
50+
println!(" - Blue filled area: Peak envelope (min/max values)");
51+
println!(" - Blue lines: Peak envelope outline");
52+
println!(" - Red lines: RMS envelope (average power)");
53+
println!(" - Black line: Zero crossing");
54+
println!();
55+
println!("Open {} to view the result!", output_path);
56+
57+
Ok(())
58+
}

src/visualization/waveform.rs

Lines changed: 154 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,158 @@ pub fn render_waveform_with_envelope(
1313
width: u32,
1414
height: u32,
1515
) -> Result<()> {
16-
// For now, delegate to regular waveform rendering
17-
// Envelope rendering can be added as enhancement
18-
render_waveform(samples, output_path, width, height)
16+
use plotters::prelude::*;
17+
18+
// Calculate envelope with a window size
19+
let window_size = (samples.len() / width as usize).max(1);
20+
let mut peak_envelope_upper = Vec::new();
21+
let mut peak_envelope_lower = Vec::new();
22+
let mut rms_envelope = Vec::new();
23+
24+
// Process samples in windows
25+
for chunk in samples.chunks(window_size) {
26+
let mut max_val = 0.0f32;
27+
let mut min_val = 0.0f32;
28+
let mut rms_sum = 0.0f32;
29+
30+
for &sample in chunk {
31+
max_val = max_val.max(sample);
32+
min_val = min_val.min(sample);
33+
rms_sum += sample * sample;
34+
}
35+
36+
peak_envelope_upper.push(max_val);
37+
peak_envelope_lower.push(min_val);
38+
39+
let rms = (rms_sum / chunk.len() as f32).sqrt();
40+
rms_envelope.push(rms);
41+
}
42+
43+
// Create the plot
44+
let root = BitMapBackend::new(output_path, (width, height)).into_drawing_area();
45+
46+
root.fill(&WHITE).map_err(|e| {
47+
crate::utils::error::FerrousError::Visualization(format!(
48+
"Failed to fill background: {}",
49+
e
50+
))
51+
})?;
52+
53+
let max_val = samples.iter().fold(0.0f32, |a, &b| a.max(b.abs()));
54+
let y_range = if max_val > 0.0 { max_val * 1.1 } else { 1.0 };
55+
56+
let mut chart = ChartBuilder::on(&root)
57+
.margin(10)
58+
.build_cartesian_2d(0f32..peak_envelope_upper.len() as f32, -y_range..y_range)
59+
.map_err(|e| {
60+
crate::utils::error::FerrousError::Visualization(format!(
61+
"Failed to build chart: {}",
62+
e
63+
))
64+
})?;
65+
66+
// Draw peak envelope as filled area
67+
let upper_points: Vec<(f32, f32)> = peak_envelope_upper
68+
.iter()
69+
.enumerate()
70+
.map(|(i, &v)| (i as f32, v))
71+
.collect();
72+
73+
let lower_points: Vec<(f32, f32)> = peak_envelope_lower
74+
.iter()
75+
.enumerate()
76+
.map(|(i, &v)| (i as f32, v))
77+
.collect();
78+
79+
// Draw filled area between envelopes using polygons
80+
let mut polygon_points = upper_points.clone();
81+
polygon_points.extend(lower_points.iter().rev());
82+
polygon_points.push(upper_points[0]); // Close the polygon
83+
84+
chart
85+
.draw_series(std::iter::once(Polygon::new(
86+
polygon_points,
87+
BLUE.mix(0.2).filled(),
88+
)))
89+
.map_err(|e| {
90+
crate::utils::error::FerrousError::Visualization(format!(
91+
"Failed to draw envelope fill: {}",
92+
e
93+
))
94+
})?;
95+
96+
// Draw RMS envelope
97+
let rms_points_upper: Vec<(f32, f32)> = rms_envelope
98+
.iter()
99+
.enumerate()
100+
.map(|(i, &v)| (i as f32, v))
101+
.collect();
102+
103+
let rms_points_lower: Vec<(f32, f32)> = rms_envelope
104+
.iter()
105+
.enumerate()
106+
.map(|(i, &v)| (i as f32, -v))
107+
.collect();
108+
109+
chart
110+
.draw_series(LineSeries::new(rms_points_upper, &RED))
111+
.map_err(|e| {
112+
crate::utils::error::FerrousError::Visualization(format!(
113+
"Failed to draw RMS upper: {}",
114+
e
115+
))
116+
})?;
117+
118+
chart
119+
.draw_series(LineSeries::new(rms_points_lower, &RED))
120+
.map_err(|e| {
121+
crate::utils::error::FerrousError::Visualization(format!(
122+
"Failed to draw RMS lower: {}",
123+
e
124+
))
125+
})?;
126+
127+
// Draw peak envelope outlines
128+
chart
129+
.draw_series(LineSeries::new(
130+
upper_points,
131+
ShapeStyle::from(&BLUE).stroke_width(2),
132+
))
133+
.map_err(|e| {
134+
crate::utils::error::FerrousError::Visualization(format!(
135+
"Failed to draw peak upper: {}",
136+
e
137+
))
138+
})?;
139+
140+
chart
141+
.draw_series(LineSeries::new(
142+
lower_points,
143+
ShapeStyle::from(&BLUE).stroke_width(2),
144+
))
145+
.map_err(|e| {
146+
crate::utils::error::FerrousError::Visualization(format!(
147+
"Failed to draw peak lower: {}",
148+
e
149+
))
150+
})?;
151+
152+
// Draw zero line
153+
chart
154+
.draw_series(LineSeries::new(
155+
vec![(0.0, 0.0), (peak_envelope_upper.len() as f32, 0.0)],
156+
ShapeStyle::from(&BLACK).stroke_width(1),
157+
))
158+
.map_err(|e| {
159+
crate::utils::error::FerrousError::Visualization(format!(
160+
"Failed to draw zero line: {}",
161+
e
162+
))
163+
})?;
164+
165+
root.present().map_err(|e| {
166+
crate::utils::error::FerrousError::Visualization(format!("Failed to present: {}", e))
167+
})?;
168+
169+
Ok(())
19170
}

tests/envelope_test.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use ferrous_waves::visualization::waveform::render_waveform_with_envelope;
2+
use std::fs;
3+
use tempfile::TempDir;
4+
5+
#[test]
6+
fn test_envelope_rendering() {
7+
// Create test signal with varying amplitude
8+
let sample_rate = 44100;
9+
let duration = 2.0;
10+
let num_samples = (sample_rate as f32 * duration) as usize;
11+
let mut samples = Vec::with_capacity(num_samples);
12+
13+
for i in 0..num_samples {
14+
let t = i as f32 / sample_rate as f32;
15+
16+
// Create amplitude envelope that changes over time
17+
let envelope = if t < 0.5 {
18+
t * 2.0 // Ramp up
19+
} else if t < 1.5 {
20+
1.0 // Sustain
21+
} else {
22+
(2.0 - t) * 2.0 // Ramp down
23+
};
24+
25+
// Modulate a 440Hz sine wave with the envelope
26+
let sample = (2.0 * std::f32::consts::PI * 440.0 * t).sin() * envelope * 0.8;
27+
samples.push(sample);
28+
}
29+
30+
// Render to temp file
31+
let temp_dir = TempDir::new().unwrap();
32+
let output_path = temp_dir.path().join("envelope_test.png");
33+
34+
let result = render_waveform_with_envelope(&samples, &output_path, 800, 400);
35+
assert!(result.is_ok());
36+
assert!(output_path.exists());
37+
38+
// Verify the file is not empty
39+
let metadata = fs::metadata(&output_path).unwrap();
40+
assert!(metadata.len() > 0);
41+
}
42+
43+
#[test]
44+
fn test_envelope_with_silence() {
45+
// Create signal with silence periods
46+
let mut samples = vec![0.0; 44100]; // 1 second of silence
47+
48+
// Add a burst in the middle
49+
for (i, sample) in samples.iter_mut().enumerate().take(33075).skip(11025) {
50+
let t = (i - 11025) as f32 / 44100.0;
51+
*sample = (2.0 * std::f32::consts::PI * 1000.0 * t).sin() * 0.5;
52+
}
53+
54+
let temp_dir = TempDir::new().unwrap();
55+
let output_path = temp_dir.path().join("envelope_silence_test.png");
56+
57+
let result = render_waveform_with_envelope(&samples, &output_path, 600, 300);
58+
assert!(result.is_ok());
59+
assert!(output_path.exists());
60+
}
61+
62+
#[test]
63+
fn test_envelope_with_clipping() {
64+
// Create signal that clips
65+
let mut samples = Vec::new();
66+
for i in 0..22050 {
67+
let t = i as f32 / 44100.0;
68+
let sample = (2.0 * std::f32::consts::PI * 200.0 * t).sin() * 2.0; // Amplitude > 1
69+
samples.push(sample.clamp(-1.0, 1.0)); // Clip to valid range
70+
}
71+
72+
let temp_dir = TempDir::new().unwrap();
73+
let output_path = temp_dir.path().join("envelope_clipping_test.png");
74+
75+
let result = render_waveform_with_envelope(&samples, &output_path, 600, 300);
76+
assert!(result.is_ok());
77+
}

0 commit comments

Comments
 (0)