adsr
This commit is contained in:
+209
@@ -1,6 +1,8 @@
|
|||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
|
use std::time::Duration;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use eframe::egui::{Context, Id, Label, Slider, Window};
|
use eframe::egui::{Context, Id, Label, Slider, Window};
|
||||||
|
use rodio::{ChannelCount, SampleRate, Source};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::gui::Showable;
|
use crate::gui::Showable;
|
||||||
|
|
||||||
@@ -9,6 +11,7 @@ pub struct Envelope {
|
|||||||
pub attack: f32,
|
pub attack: f32,
|
||||||
pub decay: f32,
|
pub decay: f32,
|
||||||
pub sustain: f32,
|
pub sustain: f32,
|
||||||
|
pub hold: f32,
|
||||||
pub release: f32
|
pub release: f32
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,11 +21,92 @@ impl Default for Envelope {
|
|||||||
attack: 0.0,
|
attack: 0.0,
|
||||||
decay: 0.0,
|
decay: 0.0,
|
||||||
sustain: 1.0,
|
sustain: 1.0,
|
||||||
|
hold: 0.5,
|
||||||
release: 0.0,
|
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) {
|
impl Showable<(Id, String)> for (&mut Envelope, &mut bool) {
|
||||||
fn show(&mut self, ctx: &Context, (id, title): &(Id, String)) {
|
fn show(&mut self, ctx: &Context, (id, title): &(Id, String)) {
|
||||||
let mut open = true;
|
let mut open = true;
|
||||||
@@ -43,6 +127,9 @@ impl Showable<(Id, String)> for (&mut Envelope, &mut bool) {
|
|||||||
ui.add(Label::new("Sustain"));
|
ui.add(Label::new("Sustain"));
|
||||||
ui.add(Slider::new(&mut self.0.sustain, RangeInclusive::new(0.0, 1.0)));
|
ui.add(Slider::new(&mut self.0.sustain, RangeInclusive::new(0.0, 1.0)));
|
||||||
ui.end_row();
|
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(Label::new("Release"));
|
||||||
ui.add(Slider::new(&mut self.0.release, RangeInclusive::new(0.0, 1.0)));
|
ui.add(Slider::new(&mut self.0.release, RangeInclusive::new(0.0, 1.0)));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
@@ -51,4 +138,126 @@ impl Showable<(Id, String)> for (&mut Envelope, &mut bool) {
|
|||||||
|
|
||||||
*self.1 = open;
|
*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 struct Grid {
|
||||||
pub note_type: NoteType,
|
pub note_type: NoteType,
|
||||||
pub volume: f32,
|
pub volume: f32,
|
||||||
pub length: u64,
|
|
||||||
pub scale: Scale,
|
pub scale: Scale,
|
||||||
pub notes: Vec<bool>,
|
pub notes: Vec<bool>,
|
||||||
pub id: Id,
|
pub id: Id,
|
||||||
@@ -48,7 +47,6 @@ impl Grid {
|
|||||||
Self {
|
Self {
|
||||||
note_type,
|
note_type,
|
||||||
volume: 1.0,
|
volume: 1.0,
|
||||||
length: 250,
|
|
||||||
open: true,
|
open: true,
|
||||||
scale: Scale::CMajor,
|
scale: Scale::CMajor,
|
||||||
notes: vec![false; (LOOP_LENGTH * LOOP_LENGTH) as usize],
|
notes: vec![false; (LOOP_LENGTH * LOOP_LENGTH) as usize],
|
||||||
@@ -144,10 +142,6 @@ impl Showable<f32> for Grid {
|
|||||||
egui::MenuBar::new().ui(ui, |ui| {
|
egui::MenuBar::new().ui(ui, |ui| {
|
||||||
ui.label("Volume");
|
ui.label("Volume");
|
||||||
ui.add(egui::Slider::new(&mut self.volume, RangeInclusive::new(0.0, 2.0)).show_value(false));
|
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| {
|
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::mixer::Mixer;
|
||||||
use rodio::Source;
|
use rodio::Source;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::envelope::Envelope;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum NoteType {
|
pub enum NoteType {
|
||||||
@@ -23,8 +23,8 @@ pub struct Note {
|
|||||||
/// How loud, 0.0 .. 2.0
|
/// How loud, 0.0 .. 2.0
|
||||||
pub volume: f32,
|
pub volume: f32,
|
||||||
|
|
||||||
/// How long to fade out
|
/// ADSR envelope
|
||||||
pub duration: Duration
|
pub envelope: Envelope
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The frequency for a given tone, in Hz.
|
/// The frequency for a given tone, in Hz.
|
||||||
@@ -53,9 +53,8 @@ impl NoteType {
|
|||||||
impl Note {
|
impl Note {
|
||||||
pub fn play(self, mixer: &Mixer) {
|
pub fn play(self, mixer: &Mixer) {
|
||||||
let note = self.note_type.source(self.tone);
|
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.amplify(self.volume.clamp(0.0, 1.0));
|
||||||
let note = note.take_duration(self.duration);
|
|
||||||
mixer.add(note)
|
mixer.add(note)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ impl PersistedTenori {
|
|||||||
struct PersistedGrid {
|
struct PersistedGrid {
|
||||||
note_type: NoteType,
|
note_type: NoteType,
|
||||||
volume: f32,
|
volume: f32,
|
||||||
length: u64,
|
|
||||||
scale: Scale,
|
scale: Scale,
|
||||||
notes: String,
|
notes: String,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -47,7 +46,6 @@ impl From<&Grid> for PersistedGrid {
|
|||||||
Self {
|
Self {
|
||||||
note_type: value.note_type,
|
note_type: value.note_type,
|
||||||
volume: value.volume,
|
volume: value.volume,
|
||||||
length: value.length,
|
|
||||||
scale: value.scale,
|
scale: value.scale,
|
||||||
name: value.name.clone(),
|
name: value.name.clone(),
|
||||||
envelope: value.envelope,
|
envelope: value.envelope,
|
||||||
@@ -62,7 +60,6 @@ impl PersistedGrid {
|
|||||||
Grid {
|
Grid {
|
||||||
note_type: self.note_type,
|
note_type: self.note_type,
|
||||||
volume: self.volume,
|
volume: self.volume,
|
||||||
length: self.length,
|
|
||||||
scale: self.scale,
|
scale: self.scale,
|
||||||
name: self.name,
|
name: self.name,
|
||||||
envelope: self.envelope,
|
envelope: self.envelope,
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::Instant;
|
||||||
use rodio::OutputStream;
|
use rodio::OutputStream;
|
||||||
use crate::grid::Grid;
|
use crate::grid::Grid;
|
||||||
use crate::dialog::Dialog;
|
use crate::dialog::Dialog;
|
||||||
@@ -106,7 +106,7 @@ impl Tenori {
|
|||||||
note_type: grid.note_type,
|
note_type: grid.note_type,
|
||||||
tone,
|
tone,
|
||||||
volume: grid.volume,
|
volume: grid.volume,
|
||||||
duration: Duration::from_millis(grid.length as u64),
|
envelope: grid.envelope
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user