Compare commits

..

10 Commits

Author SHA1 Message Date
randrews e584b03d12 tinyrand -> rand 2025-11-30 20:14:42 -06:00
randrews bb6347f810 Random colors 2025-11-28 15:33:12 -06:00
randrews 4d256f0c19 simplify 2025-10-10 23:37:35 -05:00
randrews 9a47b28fdb distortion and reverb 2025-10-05 23:57:23 -05:00
randrews 47802ba782 cleanup 2025-10-05 14:14:47 -05:00
randrews 0da8fd9cb2 timbre controls 2025-10-05 13:57:18 -05:00
randrews 03a62ac251 adsr 2025-10-05 00:45:49 -05:00
randrews 39034b3317 envelope ui 2025-10-04 15:54:38 -05:00
randrews b8bcba2585 various grid fixes 2025-10-04 13:24:53 -05:00
randrews 87d842c2ad wip2 2025-10-04 00:43:45 -05:00
11 changed files with 634 additions and 178 deletions
Generated
+8
View File
@@ -683,6 +683,12 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "color"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb"
[[package]]
name = "combine"
version = "4.6.7"
@@ -3185,7 +3191,9 @@ dependencies = [
name = "tenori-ish"
version = "0.1.0"
dependencies = [
"color",
"eframe",
"rand",
"rfd",
"rodio",
"serde",
+2
View File
@@ -10,3 +10,5 @@ rodio = { version = "0.21.1", features = ["noise"] }
toml = "0.9.7"
serde = { version = "1.0.227", features = ["derive"] }
rfd = "0.15.4"
color = "0.3.2"
rand = "0.9.2"
+27
View File
@@ -0,0 +1,27 @@
use eframe::egui::{Context, Window};
use crate::gui::Showable;
/// A struct to represent a simple messagebox, like for an error or something.
/// Contains a string (the message) and a bool which is cleared when the window is
/// closed.
pub struct Dialog(pub String, pub bool);
impl<T> Showable<T> for Dialog {
fn show(&mut self, ctx: &Context, _state: &T) {
let win = Window::new("Hey!").resizable(false).scroll([false, false]);
let mut open = true;
win.open(&mut open).show(ctx, |ui| {
ui.label(&self.0);
if ui.button("Okay").clicked() {
self.1 = false
}
});
if !open { self.1 = false }
}
}
impl<T: AsRef<str>> From<T> for Dialog {
fn from(value: T) -> Self {
Self(value.as_ref().to_string(), true)
}
}
+226
View File
@@ -0,0 +1,226 @@
use std::time::Duration;
use rodio::{ChannelCount, SampleRate, Source};
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, PartialEq, Debug, Deserialize, Serialize)]
pub struct Envelope {
pub attack: f32,
pub decay: f32,
pub sustain: f32,
pub hold: f32,
pub release: f32
}
impl Default for Envelope {
fn default() -> Self {
Self {
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
}
}
#[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
]);
}
}
+104 -77
View File
@@ -1,47 +1,107 @@
use color::ColorSpace;
use std::ops::RangeInclusive;
use eframe::egui;
use eframe::egui::{Color32, Context, Id, PointerButton, Pos2, Rangef, RichText, Sense, Ui, Vec2, Widget};
use serde::{Deserialize, Serialize};
use crate::noise::NoteType;
use eframe::egui::{Color32, Context, Id, PointerButton, Pos2, Rangef, Sense, Ui, Vec2};
use rand::Rng;
use crate::gui::Showable;
use crate::scale::Scale;
use crate::tenori::LOOP_LENGTH;
use crate::timbre::Timbre;
impl NoteType {
fn color(&self) -> Color32 {
match self {
NoteType::Sine => Color32::from_rgb(3, 211, 252),
NoteType::Triangle => Color32::from_rgb(252, 202, 3),
NoteType::Sawtooth => Color32::from_rgb(252, 18, 61),
NoteType::Square => Color32::from_rgb(4, 219, 51),
NoteType::Noise => Color32::from_rgb(240, 240, 240),
}
}
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Clone)]
pub struct Grid {
pub note_type: NoteType,
pub volume: f32,
pub length: u64,
scale: Scale,
notes: Vec<bool>,
id: String
pub scale: Scale,
pub notes: Vec<bool>,
pub id: Id,
pub name: String,
pub open: bool,
pub timbre: Timbre,
pub timbre_open: bool,
pub color: Color32
}
impl Grid {
pub fn for_note_type(note_type: NoteType, counter: usize) -> Self {
pub fn new(id: Id) -> Self {
let color = Self::random_color();
Self {
note_type,
volume: 1.0,
length: 250,
open: true,
scale: Scale::CMajor,
notes: vec![false; (LOOP_LENGTH * LOOP_LENGTH) as usize],
id: format!("Track {}", counter)
name: "New Track".to_string(),
timbre: Timbre::default(),
timbre_open: false,
color,
id
}
}
pub fn show(&mut self, ctx: &Context, cursor: f32) {
let win = egui::Window::new(&self.id).resizable(false).scroll([false, false]);
fn random_color() -> Color32 {
let angle = (rand::rng().random_range(0..72) * 5) as f32;
let rgb = color::Hsl::convert::<color::Srgb>([angle, 100.0, 50.0]);
Color32::from_rgb(
(rgb[0] * 255.0) as u8,
(rgb[1] * 255.0) as u8,
(rgb[2] * 255.0) as u8)
}
fn draw_grid(&mut self, ui: &mut Ui, cursor: f32) {
let dim = 20.0 * LOOP_LENGTH as f32;
let (rect, response) = ui.allocate_exact_size(Vec2::new(dim, dim), Sense::click_and_drag());
for (i, lit) in self.notes.iter().enumerate() {
let (x, y) = (i as u32 % LOOP_LENGTH, i as u32 / LOOP_LENGTH);
let center = Pos2::new(
(x * 20 + 10) as f32 + rect.left(),
(y * 20 + 10) as f32 + rect.top());
if *lit {
ui.painter().circle_filled(center, 10.0, self.color);
} else {
ui.painter().circle_stroke(center, 10.0, (1.0, Color32::from_gray(0x88)));
}
}
ui.painter().vline(
cursor * dim + rect.left(),
Rangef::new(rect.top(), rect.top() + dim),
(1.0, self.color)
);
if response.contains_pointer() {
ui.input(|input| {
if input.pointer.button_clicked(PointerButton::Primary) &&
let Some(pos) = input.pointer.latest_pos() {
let x = ((pos.x - rect.left()) / 20.0).floor() as usize;
let y = ((pos.y - rect.top()) / 20.0).floor() as usize;
let n = x + y * LOOP_LENGTH as usize;
self.notes[n] = !self.notes[n]
}
})
}
}
pub fn notes(&self, beat: u32) -> Vec<i32> {
let mut notes = vec![];
for y in 0..LOOP_LENGTH {
if self.notes[(y * LOOP_LENGTH + beat) as usize] {
let row = LOOP_LENGTH - y - 1;
notes.push(self.scale.tone(row))
}
}
notes
}
}
impl Showable<f32> for Grid {
fn show(&mut self, ctx: &Context, cursor: &f32) {
let mut open = true;
let win = egui::Window::new(&self.name)
.id(self.id)
.resizable(false)
.scroll([false, false])
.open(&mut open);
win.show(ctx, |ui| {
egui::MenuBar::new().ui(ui, |ui| {
if ui.button("Clear").clicked() {
@@ -61,69 +121,36 @@ impl Grid {
if ui.button(Scale::Pentatonic.label_text(self.scale)).clicked() {
self.scale = Scale::Pentatonic
}
});
})
if ui.button("Timbre...").clicked() {
self.timbre_open = !self.timbre_open;
};
if ui.button("Color").clicked() {
self.color = Self::random_color();
}
});
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| {
self.draw_grid(ui, cursor)
self.draw_grid(ui, *cursor)
});
});
}
fn draw_grid(&mut self, ui: &mut Ui, cursor: f32) {
let dim = 20.0 * LOOP_LENGTH as f32;
let (rect, response) = ui.allocate_exact_size(Vec2::new(dim, dim), Sense::click_and_drag());
for (i, lit) in self.notes.iter().enumerate() {
let (x, y) = (i as u32 % LOOP_LENGTH, i as u32 / LOOP_LENGTH);
let center = Pos2::new(
(x * 20 + 10) as f32 + rect.left(),
(y * 20 + 10) as f32 + rect.top());
if *lit {
ui.painter().circle_filled(center, 10.0, self.note_type.color());
} else {
ui.painter().circle_stroke(center, 10.0, (1.0, Color32::from_gray(0x88)));
}
if self.timbre_open {
let mut topen = true;
let mut name = self.name.clone();
let (id, title) = (format!("{} timbre", self.id.value()).into(), format!("{} Timbre", self.name));
(&mut self.timbre, &mut topen, &mut name).show(ctx, &(id, title));
self.timbre_open = topen;
self.name = name;
}
ui.painter().vline(
cursor * dim + rect.left(),
Rangef::new(rect.top(), rect.top() + dim),
(1.0, self.note_type.color())
);
if response.contains_pointer() {
ui.input(|input| {
if input.pointer.button_clicked(PointerButton::Primary) {
if let Some(pos) = input.pointer.latest_pos() {
let x = ((pos.x - rect.left()) / 20.0).floor() as usize;
let y = ((pos.y - rect.top()) / 20.0).floor() as usize;
let n = x + y * LOOP_LENGTH as usize;
self.notes[n] = !self.notes[n]
}
}
})
}
}
pub fn notes(&self, beat: u32) -> Vec<i32> {
let mut notes = vec![];
for y in 0..LOOP_LENGTH {
if self.notes[(y * LOOP_LENGTH + beat) as usize] {
let row = LOOP_LENGTH - y - 1;
notes.push(self.scale.tone(row))
}
}
notes
self.open = open;
}
}
+77 -37
View File
@@ -1,63 +1,103 @@
use std::fs;
use std::ops::RangeInclusive;
use std::path::Path;
use eframe::egui;
use eframe::egui::{Context, TopBottomPanel};
use crate::noise::NoteType;
use eframe::egui::{Context, Id, TopBottomPanel};
use crate::grid::Grid;
use crate::saveload::PersistedTenori;
use crate::Tenori;
/// A trait for things that can be shown in a gui, given a Context.
pub trait Showable<T> {
fn show(&mut self, ctx: &Context, state: &T);
}
impl Tenori {
pub fn menu(&mut self, ctx: &Context) {
fn menu(&mut self, ctx: &Context) {
TopBottomPanel::top("menu_panel").show(ctx, |ui| {
egui::MenuBar::new().ui(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Save").clicked() &&
let Ok(serialized) = toml::to_string(&PersistedTenori::from(&*self)) &&
let Some(path) = rfd::FileDialog::new().add_filter("Tenori files", &["tenori"]).set_file_name("song.tenori").save_file() {
match fs::write(path, serialized) {
Ok(_) => {}
Err(e) => { dbg!(e); }
if ui.button("Load").clicked() && let Err(s) = self.load_from_file() {
self.dialogs.push(s.into())
}
if ui.add_enabled(self.default_filename.is_some(), egui::Button::new("Save")).clicked() {
let path = self.default_filename.as_ref().unwrap_or_else(|| unreachable!());
if let Err(s) = self.save_to_file(path) {
self.dialogs.push(s.into())
}
}
if ui.button("Load").clicked() &&
let Some(path) = rfd::FileDialog::new().add_filter("Tenori files", &["tenori"]).pick_file() &&
let Ok(serialized) = fs::read_to_string(path) &&
let Ok(persisted) = toml::from_str::<PersistedTenori>(serialized.as_str()) {
self.grids = persisted.grids;
self.tempo = persisted.tempo;
self.playing = false;
self.timer = 0.0;
if ui.button("Save As...").clicked() && let Err(s) = self.save_as() {
self.dialogs.push(s.into())
}
});
ui.menu_button("Add track", |ui| {
if ui.button("Square").clicked() {
self.add_window(NoteType::Square)
}
if ui.button("Sine").clicked() {
self.add_window(NoteType::Sine)
}
if ui.button("Triangle").clicked() {
self.add_window(NoteType::Triangle)
}
if ui.button("Sawtooth").clicked() {
self.add_window(NoteType::Sawtooth)
}
if ui.button("Noise").clicked() {
self.add_window(NoteType::Noise)
}
});
if ui.button("Add track").clicked() {
let id = self.window_id();
self.grids.push(Grid::new(id));
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add(egui::Slider::new(&mut self.tempo, RangeInclusive::new(20, 180)));
if self.playing {
if ui.button("||").clicked() { self.playing = false }
} else {
if ui.button(">").clicked() { self.playing = true }
}
} else if ui.button(">").clicked() { self.playing = true }
});
})
});
}
pub fn display_dialogs(&mut self, ctx: &Context) {
for d in self.dialogs.iter_mut() {
d.show(ctx, &());
}
self.dialogs.retain(|d| d.1);
}
fn save_as(&self) -> Result<(), String> {
if let Some(path) = rfd::FileDialog::new()
.add_filter("Tenori files", &["tenori"])
.set_file_name("song.tenori").save_file() {
return self.save_to_file(path)
}
Ok(())
}
fn save_to_file<P: AsRef<Path>>(&self, filename: P) -> Result<(), String> {
let serialized = toml::to_string(&PersistedTenori::from(self)).map_err(|e| e.to_string())?;
fs::write(filename, serialized).map_err(|e| e.to_string())
}
fn load_from_file(&mut self) -> Result<(), String> {
if let Some(path) = rfd::FileDialog::new()
.add_filter("Tenori files", &["tenori"])
.pick_file() {
let serialized = fs::read_to_string(path).map_err(|e| e.to_string())?;
let persisted = toml::from_str::<PersistedTenori>(serialized.as_str()).map_err(|e| e.to_string())?;
persisted.apply_to(self);
}
Ok(())
}
fn display_grids(&mut self, ctx: &Context, cursor: &f32) {
for g in self.grids.iter_mut() {
g.show(ctx, cursor)
}
self.grids.retain(|g| g.open);
}
/// Return a unique (among all the windows created since a file load) id string for a window.
pub fn window_id(&mut self) -> Id {
self.window_counter += 1;
format!("window {}", self.window_counter).into()
}
}
impl Showable<f32> for Tenori {
fn show(&mut self, ctx: &Context, cursor: &f32) {
self.menu(ctx);
self.display_grids(ctx, cursor);
self.display_dialogs(ctx);
}
}
+7 -6
View File
@@ -4,10 +4,14 @@ mod grid;
mod noise;
mod scale;
mod saveload;
mod dialog;
mod envelope;
mod timbre;
use std::time::Duration;
use eframe::{App, Frame};
use eframe::egui::Context;
use crate::gui::Showable;
use crate::tenori::Tenori;
#[tokio::main]
@@ -19,15 +23,12 @@ async fn main() {
}
impl App for Tenori {
fn update(&mut self, ctx: &Context, frame: &mut Frame) {
fn update(&mut self, ctx: &Context, _frame: &mut Frame) {
let play = self.tick();
let cursor = self.ratio();
self.menu(ctx);
for g in self.grids.iter_mut() {
g.show(ctx, cursor)
}
for d in self.dialogs
self.show(ctx, &cursor);
if play {
for note in self.notes_for_beat() {
self.play(note)
+7 -36
View File
@@ -1,30 +1,17 @@
use std::time::Duration;
use rodio::mixer::Mixer;
use rodio::Source;
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum NoteType {
Sine,
Triangle,
Sawtooth,
Square,
Noise
}
use crate::timbre::Timbre;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Note {
/// What kind of timbre
pub note_type: NoteType,
/// Which semitone up / down from A4
pub tone: i32,
/// How loud, 0.0 .. 2.0
pub volume: f32,
/// How long to fade out
pub duration: Duration
/// ADSR envelope
pub timbre: Timbre
}
/// The frequency for a given tone, in Hz.
@@ -35,27 +22,11 @@ fn freq(tone: i32) -> f32 {
440.0 * 1.0595f32.powf(tone as f32)
}
impl NoteType {
fn source(self, tone: i32) -> Box<dyn Source + Send> {
let freq = freq(tone);
let source: Box<dyn Source + Send> = match self {
NoteType::Sine => Box::new(rodio::source::SineWave::new(freq)),
NoteType::Triangle => Box::new(rodio::source::TriangleWave::new(freq)),
NoteType::Sawtooth => Box::new(rodio::source::SawtoothWave::new(freq)),
NoteType::Square => Box::new(rodio::source::SquareWave::new(freq)),
NoteType::Noise => Box::new(rodio::source::noise::WhiteUniform::new(44100)
.low_pass_with_q(freq as u32, 2.0))
};
source
}
}
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 = note.amplify(self.volume.clamp(0.0, 1.0));
let note = note.take_duration(self.duration);
mixer.add(note)
let freq = freq(self.tone);
let source = self.timbre.source(freq);
let source = source.amplify_normalized(self.volume);
mixer.add(source)
}
}
+56 -3
View File
@@ -1,18 +1,71 @@
use eframe::egui::{Color32, Id};
use serde::{Deserialize, Serialize};
use crate::grid::Grid;
use crate::scale::Scale;
use crate::tenori::Tenori;
use crate::timbre::Timbre;
#[derive(Serialize, Deserialize)]
pub struct PersistedTenori {
pub tempo: u32,
pub grids: Vec<Grid>
tempo: u32,
grids: Vec<PersistedGrid>
}
impl From<&Tenori> for PersistedTenori {
fn from(value: &Tenori) -> Self {
Self {
tempo: value.tempo,
grids: value.grids.iter().map(Grid::clone).collect()
grids: value.grids.iter().map(PersistedGrid::from).collect()
}
}
}
impl PersistedTenori {
pub fn apply_to(self, tenori: &mut Tenori) {
tenori.grids = self.grids.into_iter().map(|g| g.into_grid(tenori.window_id())).collect();
tenori.tempo = self.tempo;
tenori.playing = false; // Start paused
tenori.timer = 0.0; // Start at the beginning of the loop
}
}
#[derive(Serialize, Deserialize)]
struct PersistedGrid {
volume: f32,
scale: Scale,
notes: String,
name: String,
timbre: Timbre,
color: (u8, u8, u8)
}
impl From<&Grid> for PersistedGrid {
fn from(value: &Grid) -> Self {
let notes: String = value.notes.iter().map(|n| if *n { '1' } else { '0' }).collect();
Self {
volume: value.volume,
scale: value.scale,
name: value.name.clone(),
timbre: value.timbre,
color: (value.color.r(), value.color.g(), value.color.b()),
notes
}
}
}
impl PersistedGrid {
pub fn into_grid(self, id: Id) -> Grid {
let notes: Vec<_> = self.notes.chars().map(|c| c == '1').collect();
Grid {
volume: self.volume,
scale: self.scale,
name: self.name,
timbre: self.timbre,
open: true,
timbre_open: false,
color: Color32::from_rgb(self.color.0, self.color.1, self.color.2),
notes,
id
}
}
}
+13 -17
View File
@@ -1,7 +1,8 @@
use std::time::{Duration, Instant};
use std::time::Instant;
use rodio::OutputStream;
use crate::grid::Grid;
use crate::noise::{Note, NoteType};
use crate::dialog::Dialog;
use crate::noise::Note;
pub const LOOP_LENGTH: u32 = 16;
@@ -21,14 +22,18 @@ pub struct Tenori {
/// The grids that we currently have going
pub grids: Vec<Grid>,
// Running count of windows created (for ids)
window_counter: usize,
/// Running count of windows created (for ids)
pub window_counter: usize,
// The audio output stream to which we will play notes
output_stream: OutputStream,
// Error dialogs we're currently showing
dialogs: Vec<String>
/// Error dialogs we're currently showing
pub dialogs: Vec<Dialog>,
/// If present, we can save to this file without asking the
/// user to select a file first.
pub default_filename: Option<String>,
}
impl Default for Tenori {
@@ -44,6 +49,7 @@ impl Default for Tenori {
grids: vec![],
window_counter: 0,
dialogs: vec![],
default_filename: None,
output_stream
}
}
@@ -73,10 +79,6 @@ impl Tenori {
self.beat() != old_beat
}
pub fn display_dialogs(&mut self) {
}
/// Which beat (0..loop_length) we're on
pub fn beat(&self) -> u32 {
self.timer.floor() as u32
@@ -88,11 +90,6 @@ impl Tenori {
self.timer / LOOP_LENGTH as f32
}
pub fn add_window(&mut self, note_type: NoteType) {
self.window_counter += 1;
self.grids.push(Grid::for_note_type(note_type, self.window_counter));
}
pub fn notes_for_beat(&self) -> Vec<Note> {
let mut notes = vec![];
let beat = self.beat();
@@ -101,10 +98,9 @@ impl Tenori {
for tone in grid.notes(beat).into_iter() {
notes.push(Note {
note_type: grid.note_type,
tone,
volume: grid.volume,
duration: Duration::from_millis(grid.length as u64),
timbre: grid.timbre
})
}
}
+105
View File
@@ -0,0 +1,105 @@
use std::ops::RangeInclusive;
use eframe::egui;
use eframe::egui::{Context, Id, Label, Slider, Window};
use rodio::Source;
use rodio::source::{SawtoothWave, SineWave, SquareWave, TriangleWave, WhiteUniform};
use serde::{Deserialize, Serialize};
use crate::envelope::Envelope;
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) -> impl Source {
let (mixer, source) = rodio::mixer::mixer(1, 44100);
if self.sine > 0.0 {
mixer.add(SineWave::new(frequency).amplify(self.sine))
}
if self.triangle > 0.0 {
mixer.add(TriangleWave::new(frequency).amplify(self.triangle))
}
if self.square > 0.0 {
mixer.add(SquareWave::new(frequency).amplify(self.square))
}
if self.sawtooth > 0.0 {
mixer.add(SawtoothWave::new(frequency).amplify(self.sawtooth))
}
if self.noise > 0.0 {
mixer.add(WhiteUniform::new(44100).amplify(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;
}
}