timbre controls

This commit is contained in:
2025-10-05 13:57:18 -05:00
parent 03a62ac251
commit 0da8fd9cb2
6 changed files with 133 additions and 22 deletions
+14 -11
View File
@@ -6,6 +6,7 @@ use crate::gui::Showable;
use crate::noise::NoteType; use crate::noise::NoteType;
use crate::scale::Scale; use crate::scale::Scale;
use crate::tenori::LOOP_LENGTH; use crate::tenori::LOOP_LENGTH;
use crate::timbre::Timbre;
impl NoteType { impl NoteType {
fn color(&self) -> Color32 { fn color(&self) -> Color32 {
@@ -38,8 +39,8 @@ pub struct Grid {
pub id: Id, pub id: Id,
pub name: String, pub name: String,
pub open: bool, pub open: bool,
pub envelope: Envelope, pub timbre: Timbre,
pub envelope_open: bool pub timbre_open: bool
} }
impl Grid { impl Grid {
@@ -51,8 +52,8 @@ impl Grid {
scale: Scale::CMajor, scale: Scale::CMajor,
notes: vec![false; (LOOP_LENGTH * LOOP_LENGTH) as usize], notes: vec![false; (LOOP_LENGTH * LOOP_LENGTH) as usize],
name: format!("{} Track", note_type.name()), name: format!("{} Track", note_type.name()),
envelope: Envelope::default(), timbre: Timbre::default(),
envelope_open: false, timbre_open: false,
id id
} }
} }
@@ -134,8 +135,8 @@ impl Showable<f32> for Grid {
} }
}); });
if ui.button("Envelope...").clicked() { if ui.button("Timbre...").clicked() {
self.envelope_open = !self.envelope_open; self.timbre_open = !self.timbre_open;
} }
}); });
@@ -149,11 +150,13 @@ impl Showable<f32> for Grid {
}); });
}); });
if self.envelope_open { if self.timbre_open {
let mut eopen = true; let mut topen = true;
let (id, title) = (format!("{} envelope", self.id.value()).into(), format!("{} Envelope", self.name)); let mut name = self.name.clone();
(&mut self.envelope, &mut eopen).show(ctx, &(id, title)); let (id, title) = (format!("{} timbre", self.id.value()).into(), format!("{} Timbre", self.name));
self.envelope_open = eopen; (&mut self.timbre, &mut topen, &mut name).show(ctx, &(id, title));
self.timbre_open = topen;
self.name = name;
} }
self.open = open; self.open = open;
+1
View File
@@ -6,6 +6,7 @@ mod scale;
mod saveload; mod saveload;
mod dialog; mod dialog;
mod envelope; mod envelope;
mod timbre;
use std::time::Duration; use std::time::Duration;
use eframe::{App, Frame}; use eframe::{App, Frame};
+6 -5
View File
@@ -2,6 +2,7 @@ use rodio::mixer::Mixer;
use rodio::Source; use rodio::Source;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::envelope::Envelope; use crate::envelope::Envelope;
use crate::timbre::Timbre;
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum NoteType { pub enum NoteType {
@@ -24,7 +25,7 @@ pub struct Note {
pub volume: f32, pub volume: f32,
/// ADSR envelope /// ADSR envelope
pub envelope: Envelope pub timbre: Timbre
} }
/// The frequency for a given tone, in Hz. /// The frequency for a given tone, in Hz.
@@ -52,9 +53,9 @@ 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 freq = freq(self.tone);
let note = self.envelope.modulate(note); let source = self.timbre.source(freq);
let note = note.amplify(self.volume.clamp(0.0, 1.0)); let source = source.amplify_normalized(self.volume);
mixer.add(note) mixer.add(source)
} }
} }
+5 -5
View File
@@ -1,10 +1,10 @@
use eframe::egui::Id; use eframe::egui::Id;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::envelope::Envelope;
use crate::grid::Grid; use crate::grid::Grid;
use crate::noise::NoteType; use crate::noise::NoteType;
use crate::scale::Scale; use crate::scale::Scale;
use crate::tenori::Tenori; use crate::tenori::Tenori;
use crate::timbre::Timbre;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct PersistedTenori { pub struct PersistedTenori {
@@ -37,7 +37,7 @@ struct PersistedGrid {
scale: Scale, scale: Scale,
notes: String, notes: String,
name: String, name: String,
envelope: Envelope, timbre: Timbre,
} }
impl From<&Grid> for PersistedGrid { impl From<&Grid> for PersistedGrid {
@@ -48,7 +48,7 @@ impl From<&Grid> for PersistedGrid {
volume: value.volume, volume: value.volume,
scale: value.scale, scale: value.scale,
name: value.name.clone(), name: value.name.clone(),
envelope: value.envelope, timbre: value.timbre,
notes notes
} }
} }
@@ -62,9 +62,9 @@ impl PersistedGrid {
volume: self.volume, volume: self.volume,
scale: self.scale, scale: self.scale,
name: self.name, name: self.name,
envelope: self.envelope, timbre: self.timbre,
open: true, open: true,
envelope_open: false, timbre_open: false,
notes, notes,
id id
} }
+1 -1
View File
@@ -106,7 +106,7 @@ impl Tenori {
note_type: grid.note_type, note_type: grid.note_type,
tone, tone,
volume: grid.volume, volume: grid.volume,
envelope: grid.envelope timbre: grid.timbre
}) })
} }
} }
+106
View File
@@ -0,0 +1,106 @@
use std::ops::RangeInclusive;
use eframe::egui;
use eframe::egui::{Context, Id, Label, Slider, Window};
use rodio::mixer::MixerSource;
use rodio::Source;
use rodio::source::{SawtoothWave, SineWave, SquareWave, TriangleWave, WhiteUniform};
use serde::{Deserialize, Serialize};
use crate::envelope::{Envelope, EnvelopeSource};
use crate::gui::Showable;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Timbre {
sine: f32,
triangle: f32,
square: f32,
sawtooth: f32,
noise: f32,
envelope: Envelope
}
impl Default for Timbre {
fn default() -> Self {
Self {
sine: 0.0,
triangle: 0.0,
square: 1.0,
sawtooth: 0.0,
noise: 0.0,
envelope: Default::default(),
}
}
}
impl Timbre {
pub fn source(&self, frequency: f32) -> EnvelopeSource<MixerSource> {
let (mut mixer, source) = rodio::mixer::mixer(1, 44100);
if self.sine > 0.0 {
mixer.add(SineWave::new(frequency).amplify_normalized(self.sine))
}
if self.triangle > 0.0 {
mixer.add(TriangleWave::new(frequency).amplify_normalized(self.triangle))
}
if self.square > 0.0 {
mixer.add(SquareWave::new(frequency).amplify_normalized(self.square))
}
if self.sawtooth > 0.0 {
mixer.add(SawtoothWave::new(frequency).amplify_normalized(self.sawtooth))
}
if self.noise > 0.0 {
mixer.add(WhiteUniform::new(44100).amplify_normalized(self.noise))
}
self.envelope.modulate(source)
}
}
impl Showable<(Id, String)> for (&mut Timbre, &mut bool, &mut String) {
fn show(&mut self, ctx: &Context, (id, title): &(Id, String)) {
let mut open = true;
let window = Window::new(title)
.id(*id)
.open(&mut open)
.resizable([false, false])
.scroll([false, false]);
window.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Name");
ui.text_edit_singleline(self.2);
});
egui::Grid::new(id).show(ui, |ui| {
ui.add(Label::new("Sine"));
ui.add(Slider::new(&mut self.0.sine, RangeInclusive::new(0.0, 1.0)));
ui.add(Label::new("Attack"));
ui.add(Slider::new(&mut self.0.envelope.attack, RangeInclusive::new(0.0, 1.0)));
ui.end_row();
ui.add(Label::new("Triangle"));
ui.add(Slider::new(&mut self.0.triangle, RangeInclusive::new(0.0, 1.0)));
ui.add(Label::new("Decay"));
ui.add(Slider::new(&mut self.0.envelope.decay, RangeInclusive::new(0.0, 1.0)));
ui.end_row();
ui.add(Label::new("Square"));
ui.add(Slider::new(&mut self.0.square, RangeInclusive::new(0.0, 1.0)));
ui.add(Label::new("Sustain"));
ui.add(Slider::new(&mut self.0.envelope.sustain, RangeInclusive::new(0.0, 1.0)));
ui.end_row();
ui.add(Label::new("Sawtooth"));
ui.add(Slider::new(&mut self.0.sawtooth, RangeInclusive::new(0.0, 1.0)));
ui.add(Label::new("Hold"));
ui.add(Slider::new(&mut self.0.envelope.hold, RangeInclusive::new(0.0, 2.0)));
ui.end_row();
ui.add(Label::new("Noise"));
ui.add(Slider::new(&mut self.0.noise, RangeInclusive::new(0.0, 1.0)));
ui.add(Label::new("Release"));
ui.add(Slider::new(&mut self.0.envelope.release, RangeInclusive::new(0.0, 1.0)));
ui.end_row();
});
});
*self.1 = open;
}
}