From 1dd9cabe68cb0bfa0c20748f9c2a014596e422a3 Mon Sep 17 00:00:00 2001 From: Greg Shiner Date: Tue, 10 Mar 2026 22:45:01 -0400 Subject: [PATCH] Little synth interface --- .gitignore | 1 + Cargo.toml | 12 ++++ src/main.rs | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9559b76 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "testing" +version = "0.1.0" +edition = "2024" + +[dependencies] +synthesis = { path = "../synthesis" } +dasp_signal = { version = "0.11.0", features = ["all"] } +cpal = "0.17.3" +ringbuf = "0.4.8" +egui = "0.33.3" +eframe = "0.33.3" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6475fed --- /dev/null +++ b/src/main.rs @@ -0,0 +1,168 @@ +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use dasp_signal::Signal; +use eframe::egui; +use ringbuf::{ + HeapRb, + traits::{Consumer, Observer, Producer, Split}, +}; +use std::sync::{Arc, Mutex}; +use synthesis::{saw_oscillator, sine_oscillator, square_oscillator}; + +const SAMPLE_RATE_I: u32 = 44100; +const SAMPLE_RATE: f64 = SAMPLE_RATE_I as f64; +const FREQUENCY: f64 = 440.0; +const BUFFER_SIZE: usize = 1024; + +struct OscilloscopeApp { + samples: Arc>>, + frequency: f64, + osc: Box>, + osc_type: OscillatorType, + producer: ringbuf::HeapProd, +} + +#[derive(PartialEq, Clone, Copy)] +enum OscillatorType { + Sine, + Square, + Saw, +} + +impl OscillatorType { + fn build(&self, sample_rate: f64, freq: f64) -> Box> { + match self { + OscillatorType::Sine => Box::new(sine_oscillator(sample_rate, freq)), + OscillatorType::Square => Box::new(square_oscillator(sample_rate, freq)), + OscillatorType::Saw => Box::new(saw_oscillator(sample_rate, freq)), + } + } +} + +impl eframe::App for OscilloscopeApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Technically a Synth"); + + let prev_type = self.osc_type; + let prev_freq = self.frequency; + + ui.horizontal(|ui| { + ui.radio_value(&mut self.osc_type, OscillatorType::Sine, "Sine"); + ui.radio_value(&mut self.osc_type, OscillatorType::Square, "Square"); + ui.radio_value(&mut self.osc_type, OscillatorType::Saw, "Saw"); + }); + + ui.add( + egui::Slider::new(&mut self.frequency, 20.0..=2000.0) + .text("Frequency (Hz)") + .logarithmic(true), + ); + + if self.osc_type != prev_type || self.frequency != prev_freq { + self.osc = self.osc_type.build(SAMPLE_RATE, self.frequency); + } + + // Fill the ring buffer with fresh samples + { + let mut buf = self.samples.lock().unwrap(); + while self.producer.vacant_len() > 0 { + let s = self.osc.next() as f32; + self.producer.try_push(s).ok(); + buf.rotate_left(1); + *buf.last_mut().unwrap() = s; + } + } + + let samples = self.samples.lock().unwrap().clone(); + + // Draw the waveform using egui's Painter + let (response, painter) = ui.allocate_painter( + egui::vec2(ui.available_width(), 300.0), + egui::Sense::hover(), + ); + + let rect = response.rect; + painter.rect_filled(rect, 0.0, egui::Color32::BLACK); + + if samples.len() > 1 { + let mid_y = rect.center().y; + let amplitude = rect.height() / 2.0 * 0.8; + let step = rect.width() / samples.len() as f32; + + let points: Vec = samples + .iter() + .enumerate() + .map(|(i, &s)| egui::pos2(rect.left() + i as f32 * step, mid_y - s * amplitude)) + .collect(); + + for window in points.windows(2) { + painter.line_segment( + [window[0], window[1]], + egui::Stroke::new(1.5, egui::Color32::from_rgb(0, 255, 100)), + ); + } + } + }); + + // Continuously repaint so the oscilloscope updates + ctx.request_repaint(); + } +} + +fn main() { + let samples = Arc::new(Mutex::new(vec![0.0f32; BUFFER_SIZE])); + let samples_for_audio = Arc::clone(&samples); + + // Set up cpal audio stream + let host = cpal::default_host(); + let device = host.default_output_device().expect("no output device"); + let config = cpal::StreamConfig { + channels: 1, + sample_rate: SAMPLE_RATE_I, + buffer_size: cpal::BufferSize::Default, + }; + // Set up a ring buffer between app and audio thread + let rb = HeapRb::::new(BUFFER_SIZE * 4); + let (mut producer, mut consumer) = rb.split(); + + let stream = device + .build_output_stream( + &config, + move |data: &mut [f32], _| { + let mut buf = samples_for_audio.lock().unwrap(); + for sample in data.iter_mut() { + let s = consumer.try_pop().unwrap_or(0.0); + *sample = s; + // Rolling buffer — shift old samples out + buf.rotate_left(1); + *buf.last_mut().unwrap() = s; + } + }, + |err| eprintln!("audio error: {err}"), + None, + ) + .expect("failed to build stream"); + + stream.play().expect("failed to play stream"); + + // Launch egui window + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 400.0]), + ..Default::default() + }; + + eframe::run_native( + "Oscilloscope", + options, + Box::new(|_cc| { + Ok(Box::new(OscilloscopeApp { + samples, + frequency: FREQUENCY, + osc: Box::new(sine_oscillator(SAMPLE_RATE, FREQUENCY)), + osc_type: OscillatorType::Sine, + producer, + })) + }), + ) + .unwrap(); +}