Compare commits
10 Commits
9eec30779a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e584b03d12 | |||
| bb6347f810 | |||
| 4d256f0c19 | |||
| 9a47b28fdb | |||
| 47802ba782 | |||
| 0da8fd9cb2 | |||
| 03a62ac251 | |||
| 39034b3317 | |||
| b8bcba2585 | |||
| 87d842c2ad |
Generated
+8
@@ -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",
|
||||
|
||||
+3
-1
@@ -9,4 +9,6 @@ tokio = { version = "1.47.1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
rodio = { version = "0.21.1", features = ["noise"] }
|
||||
toml = "0.9.7"
|
||||
serde = { version = "1.0.227", features = ["derive"] }
|
||||
rfd = "0.15.4"
|
||||
rfd = "0.15.4"
|
||||
color = "0.3.2"
|
||||
rand = "0.9.2"
|
||||
|
||||
@@ -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
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
+105
-78
@@ -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]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
self.open = open;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
-37
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user