From 03a62ac251f6d5f6dd4aad8379bac949ac99a430 Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Sun, 5 Oct 2025 00:45:49 -0500 Subject: [PATCH] adsr --- src/envelope.rs | 209 ++++++++++++++++++++++++++++++++++++++++++++++++ src/grid.rs | 6 -- src/noise.rs | 9 +-- src/saveload.rs | 3 - src/tenori.rs | 4 +- 5 files changed, 215 insertions(+), 16 deletions(-) diff --git a/src/envelope.rs b/src/envelope.rs index e1d2456..e66fc6c 100644 --- a/src/envelope.rs +++ b/src/envelope.rs @@ -1,6 +1,8 @@ use std::ops::RangeInclusive; +use std::time::Duration; use eframe::egui; use eframe::egui::{Context, Id, Label, Slider, Window}; +use rodio::{ChannelCount, SampleRate, Source}; use serde::{Deserialize, Serialize}; use crate::gui::Showable; @@ -9,6 +11,7 @@ pub struct Envelope { pub attack: f32, pub decay: f32, pub sustain: f32, + pub hold: f32, pub release: f32 } @@ -18,11 +21,92 @@ impl Default for Envelope { attack: 0.0, decay: 0.0, sustain: 1.0, + hold: 0.5, release: 0.0, } } } +impl Envelope { + pub fn modulate(&self, source: S) -> EnvelopeSource { + EnvelopeSource { + envelope: *self, + source, + elapsed: 0, + } + } +} + +pub struct EnvelopeSource { + envelope: Envelope, + source: S, + elapsed: usize +} + +impl Iterator for EnvelopeSource { + type Item = f32; + + fn next(&mut self) -> Option { + if let Some(val) = self.source.next() { + let rate = self.source.sample_rate() as f32; // Number of samples per sec + // Which tick are we on (a given tick might be more than one call, + // since multiple channels) + let mut tick = (self.elapsed / self.source.channels() as usize) as f32; + // No matter what happens we have now consumed a sample: + self.elapsed += 1; + + // Attack phase: + if tick < rate * self.envelope.attack { + let m = 1.0 / self.envelope.attack; + return Some(val * tick / rate * m) + } + tick -= rate * self.envelope.attack; + + // Decay phase, reduce to sustain level + if tick < rate * self.envelope.decay { + let m = (1.0 - self.envelope.sustain) / self.envelope.decay; + return Some(val * (1.0 - tick / rate * m)) + } + tick -= rate * self.envelope.decay; + + // Hold phase, hold at sustain level: + if tick < rate * self.envelope.hold { + return Some(val * self.envelope.sustain) + } + tick -= rate * self.envelope.hold; + + // Release phase, fade to zero: + if tick < rate * self.envelope.release { + let m = self.envelope.sustain / self.envelope.release; + return Some(val * (self.envelope.sustain - tick / rate * m)) + } + + None + } else { + None // Inner sample is done so, so are we + } + } +} + +impl Source for EnvelopeSource { + fn current_span_len(&self) -> Option { + None // Length of the sound + } + + fn channels(&self) -> ChannelCount { + self.source.channels() + } + + fn sample_rate(&self) -> SampleRate { + self.source.sample_rate() + } + + fn total_duration(&self) -> Option { + // We don't know right here what the duration is, but it will eventually stop + None + } +} + impl Showable<(Id, String)> for (&mut Envelope, &mut bool) { fn show(&mut self, ctx: &Context, (id, title): &(Id, String)) { let mut open = true; @@ -43,6 +127,9 @@ impl Showable<(Id, String)> for (&mut Envelope, &mut bool) { ui.add(Label::new("Sustain")); ui.add(Slider::new(&mut self.0.sustain, RangeInclusive::new(0.0, 1.0))); ui.end_row(); + ui.add(Label::new("Hold")); + ui.add(Slider::new(&mut self.0.hold, RangeInclusive::new(0.0, 2.0))); + ui.end_row(); ui.add(Label::new("Release")); ui.add(Slider::new(&mut self.0.release, RangeInclusive::new(0.0, 1.0))); ui.end_row(); @@ -51,4 +138,126 @@ impl Showable<(Id, String)> for (&mut Envelope, &mut bool) { *self.1 = open; } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct ConstSource(f32); + impl Iterator for ConstSource { + type Item = f32; + fn next(&mut self) -> Option { + Some(self.0) + } + } + impl Source for ConstSource { + fn current_span_len(&self) -> Option { + None + } + + fn channels(&self) -> ChannelCount { + 1 + } + + fn sample_rate(&self) -> SampleRate { + 10 + } + + fn total_duration(&self) -> Option { + None + } + } + + fn env(attack: f32, decay: f32, sustain: f32, hold: f32, release: f32) -> Envelope { + Envelope { + attack, decay, sustain, hold, release + } + } + + fn assert_close_enough(mut src: S, expected: Vec) { + for e in expected.into_iter() { + let val = src.next().unwrap(); + assert!(val - e < 0.0001) + } + } + + #[test] + fn test_attack() { + // Normal: ramp up to 1.0 over 10 samples + assert_close_enough( + env(1.0, 0.0, 0.0, 0.0, 0.0).modulate(ConstSource(10.0)), + vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]); + + // 0 attack: immediately at 1.0 + assert_close_enough( + env(0.0, 0.0, 1.0, 100.0, 0.0).modulate(ConstSource(10.0)), + vec![10.0; 5]); + } + + #[test] + fn test_decay() { + // Ramp down from 1.0 to sustain: + assert_close_enough( + env(0.0, 0.3, 0.7, 100.0, 0.0).modulate(ConstSource(10.0)), + vec![10.0, 9.0, 8.0, 7.0, 7.0, 7.0]); + + // No decay, immediately at sustain: + assert_close_enough( + env(0.0, 0.0, 0.7, 100.0, 0.0).modulate(ConstSource(10.0)), + vec![7.0; 5]); + + // Sustain at 1, stay there: + assert_close_enough( + env(0.0, 0.5, 1.0, 100.0, 0.0).modulate(ConstSource(10.0)), + vec![10.0; 20]); + + // Attack and then decay: + // No decay, immediately at sustain: + assert_close_enough( + env(0.5, 0.3, 0.7, 100.0, 0.0).modulate(ConstSource(10.0)), + vec![0.0, 2.0, 4.0, 6.0, 8.0, // 5 samples of attack + 10.0, 9.0, 8.0, // three sample decay phase + 7.0, 7.0, 7.0, 7.0]); // stuck at sustain + } + + #[test] + fn test_hold() { + // No A/D, hold at sustain for the hold length: + assert_close_enough( + env(0.0, 0.0, 0.7, 1.0, 0.0).modulate(ConstSource(10.0)), + vec![7.0; 10]); + + // It stops holding after the right length (10 hz, 1.0 secs) + assert_eq!( + env(0.0, 0.0, 0.7, 1.0, 0.0).modulate(ConstSource(10.0)).collect::>().len(), + 10 + ); + } + + #[test] + fn test_release() { + // Simple case, release from full + assert_close_enough( + env(0.0, 0.0, 1.0, 0.0, 1.0).modulate(ConstSource(10.0)), + vec![10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0]); + + // Release from sustain + assert_close_enough( + env(0.0, 0.0, 0.5, 0.0, 1.0).modulate(ConstSource(10.0)), + vec![5.0, 4.5, 4.0, 3.5, 3.0, 2.5, 2.0, 1.5, 1.0, 0.5]); + } + + #[test] + fn test_adsr() { + // The entire envelope + assert_close_enough( + env(0.5, 0.3, 0.7, 0.3, 0.7).modulate(ConstSource(10.0)), + vec![ + 0.0, 2.0, 4.0, 6.0, 8.0, // Attack phase + 10.0, 9.0, 8.0, // Decay phase + 7.0, 7.0, 7.0, // Hold at sustain + 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0 // Release + ]); + } } \ No newline at end of file diff --git a/src/grid.rs b/src/grid.rs index 9dd3339..2763921 100644 --- a/src/grid.rs +++ b/src/grid.rs @@ -33,7 +33,6 @@ impl NoteType { pub struct Grid { pub note_type: NoteType, pub volume: f32, - pub length: u64, pub scale: Scale, pub notes: Vec, pub id: Id, @@ -48,7 +47,6 @@ impl Grid { Self { note_type, volume: 1.0, - length: 250, open: true, scale: Scale::CMajor, notes: vec![false; (LOOP_LENGTH * LOOP_LENGTH) as usize], @@ -144,10 +142,6 @@ impl Showable for Grid { egui::MenuBar::new().ui(ui, |ui| { ui.label("Volume"); ui.add(egui::Slider::new(&mut self.volume, RangeInclusive::new(0.0, 2.0)).show_value(false)); - - ui.label("Length"); - ui.add(egui::Slider::new(&mut self.length, RangeInclusive::new(0, 2000)).show_value(false)); - }); egui::Frame::new().inner_margin(3).show(ui, |ui| { diff --git a/src/noise.rs b/src/noise.rs index f2ba2b3..443dc2e 100644 --- a/src/noise.rs +++ b/src/noise.rs @@ -1,7 +1,7 @@ -use std::time::Duration; use rodio::mixer::Mixer; use rodio::Source; use serde::{Deserialize, Serialize}; +use crate::envelope::Envelope; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum NoteType { @@ -23,8 +23,8 @@ pub struct Note { /// How loud, 0.0 .. 2.0 pub volume: f32, - /// How long to fade out - pub duration: Duration + /// ADSR envelope + pub envelope: Envelope } /// The frequency for a given tone, in Hz. @@ -53,9 +53,8 @@ impl NoteType { impl Note { pub fn play(self, mixer: &Mixer) { let note = self.note_type.source(self.tone); - let note = note.fade_out(self.duration); + let note = self.envelope.modulate(note); let note = note.amplify(self.volume.clamp(0.0, 1.0)); - let note = note.take_duration(self.duration); mixer.add(note) } } diff --git a/src/saveload.rs b/src/saveload.rs index 28f5462..8a07741 100644 --- a/src/saveload.rs +++ b/src/saveload.rs @@ -34,7 +34,6 @@ impl PersistedTenori { struct PersistedGrid { note_type: NoteType, volume: f32, - length: u64, scale: Scale, notes: String, name: String, @@ -47,7 +46,6 @@ impl From<&Grid> for PersistedGrid { Self { note_type: value.note_type, volume: value.volume, - length: value.length, scale: value.scale, name: value.name.clone(), envelope: value.envelope, @@ -62,7 +60,6 @@ impl PersistedGrid { Grid { note_type: self.note_type, volume: self.volume, - length: self.length, scale: self.scale, name: self.name, envelope: self.envelope, diff --git a/src/tenori.rs b/src/tenori.rs index 6a277ab..1032294 100644 --- a/src/tenori.rs +++ b/src/tenori.rs @@ -1,4 +1,4 @@ -use std::time::{Duration, Instant}; +use std::time::Instant; use rodio::OutputStream; use crate::grid::Grid; use crate::dialog::Dialog; @@ -106,7 +106,7 @@ impl Tenori { note_type: grid.note_type, tone, volume: grid.volume, - duration: Duration::from_millis(grid.length as u64), + envelope: grid.envelope }) } }