Little synth interface
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@@ -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"
|
||||||
168
src/main.rs
Normal file
168
src/main.rs
Normal file
@@ -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<Mutex<Vec<f32>>>,
|
||||||
|
frequency: f64,
|
||||||
|
osc: Box<dyn Signal<Frame = f64>>,
|
||||||
|
osc_type: OscillatorType,
|
||||||
|
producer: ringbuf::HeapProd<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
|
enum OscillatorType {
|
||||||
|
Sine,
|
||||||
|
Square,
|
||||||
|
Saw,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OscillatorType {
|
||||||
|
fn build(&self, sample_rate: f64, freq: f64) -> Box<dyn Signal<Frame = f64>> {
|
||||||
|
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<egui::Pos2> = 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::<f32>::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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user