diff --git a/.gitignore b/.gitignore index ea8c4bf..f979fa7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +**/.DS_Store diff --git a/Cargo.lock b/Cargo.lock index 9369623..86db363 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,54 +160,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" -[[package]] -name = "anstream" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" - -[[package]] -name = "anstyle-parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" -dependencies = [ - "anstyle", - "windows-sys 0.48.0", -] - [[package]] name = "anyhow" version = "1.0.75" @@ -649,33 +601,6 @@ dependencies = [ "libloading 0.7.4", ] -[[package]] -name = "clap" -version = "4.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" -dependencies = [ - "clap_builder", -] - -[[package]] -name = "clap_builder" -version = "4.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_lex" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" - [[package]] name = "clipboard-win" version = "4.5.0" @@ -723,12 +648,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - [[package]] name = "combine" version = "4.6.6" @@ -1389,11 +1308,9 @@ name = "ichiro" version = "0.1.0" dependencies = [ "anyhow", - "clap", "cpal", "eframe", "egui_plot", - "interpolation", "rand", "ringbuf", "rtrb", @@ -1445,12 +1362,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "interpolation" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b7357d2bbc5ee92f8e899ab645233e43d21407573cceb37fed8bc3dede2c02" - [[package]] name = "io-lifetimes" version = "1.0.11" @@ -2434,12 +2345,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "syn" version = "1.0.109" @@ -2637,12 +2542,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - [[package]] name = "vec_map" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index f396dbf..0a2999e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,9 @@ version = "0.1.0" [dependencies] anyhow = "*" -clap = "*" cpal = "*" eframe = "0.23.0" egui_plot = "0.23.0" -interpolation = "0.2.0" rand = "*" ringbuf = "*" rtrb = "0.2" diff --git a/ROADMAP.md b/ROADMAP.md index 0ff991e..2d630d4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,6 +9,7 @@ Roadmap * Presets. * Routing and modulation system (dynamic? macros?) * Voices. +* Use lenses for state selectors. ## Oscillators * Anti-aliasing. diff --git a/src/app.rs b/src/app.rs index 9f694e6..47fe938 100644 --- a/src/app.rs +++ b/src/app.rs @@ -47,9 +47,9 @@ impl SynthApp { impl SynthState { pub fn new() -> Self { SynthState { - osc_freq: 440.0, + osc_freq: 440, amp: 1.0, - mod_freq: 440.0, + mod_freq: 440, osc_mix: 0.0, mod_amount: 0.0, sh_rate: 0.0, @@ -112,9 +112,9 @@ impl PlotState { #[derive(Copy, Clone)] pub struct SynthState { - pub osc_freq: f32, + pub osc_freq: u32, pub amp: f32, - pub mod_freq: f32, + pub mod_freq: u32, pub osc_mix: f32, pub mod_amount: f32, pub sh_rate: f32, @@ -184,7 +184,7 @@ impl eframe::App for SynthApp { ui.add( egui::Slider::new( &mut self.state.osc_freq, - 20f32..=8000f32, + 2..=16000, ) .suffix("Hz") .smart_aim(false), @@ -215,7 +215,7 @@ impl eframe::App for SynthApp { ui.add( egui::Slider::new( &mut self.state.mod_freq, - 2f32..=4000f32, + 2..=16000, ) .suffix("Hz") .smart_aim(false), @@ -342,7 +342,7 @@ impl eframe::App for SynthApp { .height(100.0) .show(ui, |plot_ui| plot_ui.line(line)); }); - + // Publish updated state to the UI thread. let _ = self.state_tx.push(self.state); diff --git a/src/dsp.rs b/src/dsp.rs index 0851045..ccb5ae2 100644 --- a/src/dsp.rs +++ b/src/dsp.rs @@ -1,14 +1,14 @@ use std::f32::consts::PI; -use interpolation::Lerp; - use crate::app::{OscShape, SynthState}; const TAU: f32 = 2.0 * PI; #[allow(dead_code)] -fn smooth_step(old_value: f32, desired_value: f32) -> f32 { - old_value.lerp(&desired_value, &1.0) +// Linear interpolation. +// step is between [1, 0]. +fn lerp(current_value: f32, desired_value: f32, step: f32) -> f32 { + current_value * (1.0 - step) + (desired_value * step) } // Computes the relative volumes between two channels. @@ -25,9 +25,11 @@ pub struct Osc { sample_rate: u32, sample_clock: u32, phi: f32, - counter: f32, + counter: i32, step_change: f32, shape: OscShape, + prev: f32, + frequency: u32, } impl Osc { @@ -37,59 +39,96 @@ impl Osc { shape, sample_clock: 0, phi: 0.0, - counter: 0.0, + counter: 0, + prev: 0.0, step_change: 1.0 / sample_rate as f32, + frequency: 440, } } pub fn next_sample( &mut self, - frequency: f32, + frequency: u32, fm: Option, fm_amount: Option, ) -> f32 { let fm = fm.unwrap_or(1.0); let fm_amount = fm_amount.unwrap_or(1.0); - let freq = frequency * mod_amount(fm, fm_amount); + + // FIXME: This is weird. + let old_frequency = self.frequency; + if frequency != old_frequency { + // FIXME: Return back to floats for frequency + // and maybe use rounding and local ints! + self.frequency = (frequency as f32 * mod_amount(fm, fm_amount)) as u32; + + // FIXME: FM?! Won't work! + // self.counter = 0; + // self.prev = 0.0; + } // Sample clock. self.sample_clock = (self.sample_clock + 1) % self.sample_rate; - self.phi += TAU * frequency * self.step_change; + // Keep phase. + self.phi += TAU * frequency as f32 * self.step_change; match self.shape { OscShape::Sine => sine(self.phi), OscShape::Square => square(self.phi), - OscShape::Triangle => triangle(self.phi, frequency, self.counter), + OscShape::Triangle => { + let (triangle, counter) = triangle( + self.phi, + frequency, + self.sample_rate, + self.counter, + self.prev, + ); + self.counter = counter; + self.prev = triangle; + triangle + } } } pub fn set_shape(&mut self, shape: OscShape) { - self.shape = shape; - // FIXME: ? - self.counter = 0.0; + if self.shape != shape { + self.shape = shape; + // FIXME: Keep phase? + self.counter = 0; + self.prev = -1.0; + } } } fn triangle( phi: f32, - freq: f32, - counter: f32, -) -> f32 { - let mut counter = counter; - let sin = sine(phi); - - let increment = 1.0f32 / (freq * 2.0f32); - - // TODO: This won't work between calls! - // FIXME: It's Copy?! - if sin >= 0f32 { - counter += increment; + freq: u32, + sample_rate: u32, + counter: i32, + prev: f32, +) -> (f32, i32) { + // samples per second = sample_rate + // samples per single up or down phase = samples per second / frequency / 2 + // + let cycle_samples = sample_rate / freq; + let half = cycle_samples / 2; + + // FIXME: Phase. + let mut counter_next = (counter + 1) % cycle_samples as i32; + let step = 1.0f32 / (half as f32); + let triangle = if counter >= half as i32 { + prev - step } else { - counter -= increment; + prev + step + }; + + if counter_next >= cycle_samples as i32 { + counter_next = 0; } - truncate(counter) + let (triangle_out, counter) = (truncate(triangle), counter); + (triangle_out, counter_next) } fn saw() { @@ -97,25 +136,24 @@ fn saw() { } fn sine(phi: f32) -> f32 { - phi.sin() - + let sin = phi.sin(); + sin } fn square(phi: f32) -> f32 { - let sin = sine(phi); - - if sin >= 0f32 { - 1f32 + let square= if phi.sin() >= 0.0 { + 1.0 } else { - 0f32 - } + -1.0 + }; + square } fn truncate(x: f32) -> f32 { - if x > 1.0 { + if x >= 1.0 { return 1.0; } - if x < -1.0 { + if x <= -1.0 { return -1.0; } @@ -130,24 +168,15 @@ pub struct LowPassFilter { impl LowPassFilter { pub fn new() -> Self { - Self { buffer: [0f32; N], prev: 0.0 } + Self { + buffer: [0f32; N], + prev: 0.0, + } } pub fn filter_sample(&mut self, sample: f32, cutoff: f32) -> f32 { - /* - double simplp (double *x, double *y, - int M, double xm1) - { - int n; - y[0] = x[0] + xm1; - for (n=1; n < M ; n++) { - y[n] = x[n] + x[n-1]; - } - return x[M-1]; - } - */ // Average-based simple filter. - // self.average_filter(sample); + // self.average_filter(sample); // self.lpf(sample, cutoff) @@ -163,7 +192,6 @@ impl LowPassFilter { self.buffer[0] = sample; } - // TODO: Be generic over floating point sizes (num crate) fn average(&self) -> f32 { self.buffer.iter().sum::() / self.buffer.len() as f32 } @@ -178,7 +206,7 @@ impl LowPassFilter { } fn mod_amount(x: f32, amount: f32) -> f32 { - if amount == 0.0 { + if amount >= 1.0 { 1.0 } else { x * amount diff --git a/src/main.rs b/src/main.rs index 6b6dca6..44f76ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,7 +31,6 @@ fn write_data( let mut plot_buf: [f32; 512] = [0.0; 512]; let mut plot_buf_idx = 0usize; - for frame in output.chunks_mut(channels) { // FIXME: How often? Fo each sample?! Match FPS! // Try to receive new state from the UI. @@ -39,7 +38,6 @@ fn write_data( state = new_state; } - // Compute next sample and write it to buffers. let value = voice.next_sample(state); for sample in frame.iter_mut() {