adsr
This commit is contained in:
+209
@@ -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<S: Source>(&self, source: S) -> EnvelopeSource<S> {
|
||||
EnvelopeSource {
|
||||
envelope: *self,
|
||||
source,
|
||||
elapsed: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnvelopeSource<S: Source> {
|
||||
envelope: Envelope,
|
||||
source: S,
|
||||
elapsed: usize
|
||||
}
|
||||
|
||||
impl<S: Source> Iterator for EnvelopeSource<S> {
|
||||
type Item = f32;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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<S: Source> Source for EnvelopeSource<S> {
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
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<Duration> {
|
||||
// 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<Self::Item> {
|
||||
Some(self.0)
|
||||
}
|
||||
}
|
||||
impl Source for ConstSource {
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn channels(&self) -> ChannelCount {
|
||||
1
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> SampleRate {
|
||||
10
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn env(attack: f32, decay: f32, sustain: f32, hold: f32, release: f32) -> Envelope {
|
||||
Envelope {
|
||||
attack, decay, sustain, hold, release
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_close_enough<S: Source>(mut src: S, expected: Vec<f32>) {
|
||||
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::<Vec<_>>().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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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<bool>,
|
||||
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<f32> 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| {
|
||||
|
||||
+4
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
-2
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user