feat: modularize sin wave generator
This commit is contained in:
+3
-11
@@ -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));
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>;
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user