From 1682a26c61c606d9cb843e7a62cf4b3aa510e951 Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Sun, 18 Oct 2026 12:21:06 +0200 Subject: [PATCH] feat: modularize sin wave generator --- raust-cli/src/main.rs | 14 +++--------- raust-engine/src/audio_player.rs | 6 ++--- raust-engine/src/generators/mod.rs | 14 ++++++++++++ raust-engine/src/generators/sin_wave.rs | 30 +++++++++++++++++++++++++ raust-engine/src/lib.rs | 11 ++++----- raust-engine/src/phase.rs | 24 ++++++++++++++++++++ 6 files changed, 78 insertions(+), 21 deletions(-) create mode 100644 raust-engine/src/generators/mod.rs create mode 100644 raust-engine/src/generators/sin_wave.rs create mode 100644 raust-engine/src/phase.rs diff --git a/raust-cli/src/main.rs b/raust-cli/src/main.rs index 77a5bf5..7b0e5f8 100644 --- a/raust-cli/src/main.rs +++ b/raust-cli/src/main.rs @@ -1,6 +1,6 @@ use clap::Parser; use cpal::traits::DeviceTrait; -use raust_engine::{AudioPlayer, Note}; +use raust_engine::{AudioPlayer, Note, generators::Generator}; /// Play a single tone through the default audio output device. #[derive(Parser, Debug)] @@ -24,16 +24,8 @@ fn main() -> anyhow::Result<()> { let (device, config) = raust_engine::get_device_and_config()?; println!("Output device: {}", device.id()?); println!("Default output config: {config:?}"); - let mut clock = 0f32; - // Sample rates are a few hundred kHz at most, nowhere near f32's - // 24-bit exact-integer range, so this cast never loses precision. - #[allow(clippy::cast_precision_loss, clippy::as_conversions)] - let sample_rate = config.sample_rate() as f32; - let generator = move || { - clock = (clock + 1.0) % sample_rate; - (clock * note.pitch * 2.0 * std::f32::consts::PI / sample_rate).sin() - }; - let audio_player = AudioPlayer::try_new(&device, config, Box::new(generator))?; + let mut sin = raust_engine::generators::SinWave::new(config.sample_rate(), note.pitch); + let audio_player = AudioPlayer::try_new(&device, config, Box::new(move || sin.generate()))?; audio_player.play()?; std::thread::sleep(std::time::Duration::from_millis(note.length)); diff --git a/raust-engine/src/audio_player.rs b/raust-engine/src/audio_player.rs index 79e1494..509b63b 100644 --- a/raust-engine/src/audio_player.rs +++ b/raust-engine/src/audio_player.rs @@ -8,7 +8,7 @@ pub struct AudioPlayer { stream: Stream, } -use super::Generator; +use crate::generators::GeneratorFn; use cpal::{ Device, Error, ErrorKind, FromSample, I24, OutputCallbackInfo, Sample, SampleFormat, SizedSample, Stream, StreamConfig, SupportedStreamConfig, traits::{DeviceTrait, StreamTrait}, }; @@ -27,7 +27,7 @@ impl AudioPlayer { /// Returns an error if: /// - the device reports a sample format `raust` does not support, or /// - CPAL fails to build the output stream. - pub fn try_new(device: &Device, config: SupportedStreamConfig, generator: Generator) -> anyhow::Result { + pub fn try_new(device: &Device, config: SupportedStreamConfig, generator: GeneratorFn) -> anyhow::Result { let stream = match config.sample_format() { SampleFormat::I8 => Self::create_stream::(device, config.into(), generator), SampleFormat::I16 => Self::create_stream::(device, config.into(), generator), @@ -57,7 +57,7 @@ impl AudioPlayer { pub fn create_stream( device: &Device, config: StreamConfig, - mut generator: Generator, + mut generator: GeneratorFn, ) -> Result where T: SizedSample + FromSample, diff --git a/raust-engine/src/generators/mod.rs b/raust-engine/src/generators/mod.rs new file mode 100644 index 0000000..052df9a --- /dev/null +++ b/raust-engine/src/generators/mod.rs @@ -0,0 +1,14 @@ +mod sin_wave; +pub use sin_wave::SinWave; + +pub trait Generator { + fn generate(&mut self) -> f32; +} + +/// Produces one `f32` sample per call. +/// +/// `AudioPlayer` consumes a `Generator` when it builds its stream and moves +/// it onto the audio callback thread, so `Send + 'static` must hold — the +/// same bound [`cpal::Device::build_output_stream`] places on its data +/// callback. +pub type GeneratorFn = Box f32 + Send + 'static>; diff --git a/raust-engine/src/generators/sin_wave.rs b/raust-engine/src/generators/sin_wave.rs new file mode 100644 index 0000000..fef3661 --- /dev/null +++ b/raust-engine/src/generators/sin_wave.rs @@ -0,0 +1,30 @@ +use cpal::SampleRate; + +use crate::{Phase, Pitch}; + +use super::Generator; + +pub struct SinWave { + phase: Phase, + pitch: Pitch, + sample_rate: f32 +} + +impl SinWave { + #[must_use] + pub fn new(sample_rate: SampleRate, pitch: Pitch) -> Self { + #[allow(clippy::cast_precision_loss, clippy::as_conversions)] + let sample_rate = sample_rate as f32; + Self { + phase: Phase::new(sample_rate, None), + pitch, + sample_rate + } + } +} + +impl Generator for SinWave { + fn generate(&mut self) -> f32 { + (self.phase.next() * self.pitch * 2.0 * std::f32::consts::PI / self.sample_rate).sin() + } +} diff --git a/raust-engine/src/lib.rs b/raust-engine/src/lib.rs index 3f650df..bdc0529 100644 --- a/raust-engine/src/lib.rs +++ b/raust-engine/src/lib.rs @@ -5,14 +5,11 @@ mod audio_player; pub use audio_player::AudioPlayer; mod init; pub use init::*; +pub mod generators; +mod phase; +pub(crate) use phase::Phase; -/// Produces one `f32` sample per call. -/// -/// `AudioPlayer` consumes a `Generator` when it builds its stream and moves -/// it onto the audio callback thread, so `Send + 'static` must hold — the -/// same bound [`cpal::Device::build_output_stream`] places on its data -/// callback. -pub type Generator = Box f32 + Send + 'static>; +type Pitch = f32; /// A single tone: a pitch held for a fixed duration. #[derive(Debug, Clone, Copy)] diff --git a/raust-engine/src/phase.rs b/raust-engine/src/phase.rs new file mode 100644 index 0000000..7ce7094 --- /dev/null +++ b/raust-engine/src/phase.rs @@ -0,0 +1,24 @@ +type PhaseFn = Box f32 + Send + 'static>; // (current, sample_rate) -> next + +pub struct Phase { + current: f32, + sample_rate: f32, + next_fn: PhaseFn +} + +impl Phase { + pub fn new(sample_rate: f32, next_fn: Option) -> Self { + Self { + current: 0.0, + sample_rate, + next_fn: next_fn.unwrap_or_else(|| { + Box::new(|current: f32, sample_rate: f32| (current + 1.0) % sample_rate) + }) + } + } + + pub fn next(&mut self) -> f32 { + self.current = (self.next_fn)(self.current, self.sample_rate); + self.current + } +}