feat: modularize sin wave generator

This commit is contained in:
2026-10-18 13:52:00 +02:00
parent 27ee2efd00
commit 1682a26c61
6 changed files with 78 additions and 21 deletions
+3 -11
View File
@@ -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));
+3 -3
View File
@@ -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<Self> {
pub fn try_new(device: &Device, config: SupportedStreamConfig, generator: GeneratorFn) -> anyhow::Result<Self> {
let stream = match config.sample_format() {
SampleFormat::I8 => Self::create_stream::<i8>(device, config.into(), generator),
SampleFormat::I16 => Self::create_stream::<i16>(device, config.into(), generator),
@@ -57,7 +57,7 @@ impl AudioPlayer {
pub fn create_stream<T>(
device: &Device,
config: StreamConfig,
mut generator: Generator,
mut generator: GeneratorFn,
) -> Result<Stream, anyhow::Error>
where
T: SizedSample + FromSample<f32>,
+14
View File
@@ -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<dyn FnMut() -> f32 + Send + 'static>;
+30
View File
@@ -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()
}
}
+4 -7
View File
@@ -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<dyn FnMut() -> f32 + Send + 'static>;
type Pitch = f32;
/// A single tone: a pitch held for a fixed duration.
#[derive(Debug, Clone, Copy)]
+24
View File
@@ -0,0 +1,24 @@
type PhaseFn = Box<dyn FnMut(f32, f32) -> 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<PhaseFn>) -> 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
}
}