feat: modularize sin wave generator
This commit is contained in:
+3
-11
@@ -1,6 +1,6 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cpal::traits::DeviceTrait;
|
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.
|
/// Play a single tone through the default audio output device.
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -24,16 +24,8 @@ fn main() -> anyhow::Result<()> {
|
|||||||
let (device, config) = raust_engine::get_device_and_config()?;
|
let (device, config) = raust_engine::get_device_and_config()?;
|
||||||
println!("Output device: {}", device.id()?);
|
println!("Output device: {}", device.id()?);
|
||||||
println!("Default output config: {config:?}");
|
println!("Default output config: {config:?}");
|
||||||
let mut clock = 0f32;
|
let mut sin = raust_engine::generators::SinWave::new(config.sample_rate(), note.pitch);
|
||||||
// Sample rates are a few hundred kHz at most, nowhere near f32's
|
let audio_player = AudioPlayer::try_new(&device, config, Box::new(move || sin.generate()))?;
|
||||||
// 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))?;
|
|
||||||
|
|
||||||
audio_player.play()?;
|
audio_player.play()?;
|
||||||
std::thread::sleep(std::time::Duration::from_millis(note.length));
|
std::thread::sleep(std::time::Duration::from_millis(note.length));
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
pub struct AudioPlayer {
|
pub struct AudioPlayer {
|
||||||
stream: Stream,
|
stream: Stream,
|
||||||
}
|
}
|
||||||
use super::Generator;
|
use crate::generators::GeneratorFn;
|
||||||
use cpal::{
|
use cpal::{
|
||||||
Device, Error, ErrorKind, FromSample, I24, OutputCallbackInfo, Sample, SampleFormat, SizedSample, Stream, StreamConfig, SupportedStreamConfig, traits::{DeviceTrait, StreamTrait},
|
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:
|
/// Returns an error if:
|
||||||
/// - the device reports a sample format `raust` does not support, or
|
/// - the device reports a sample format `raust` does not support, or
|
||||||
/// - CPAL fails to build the output stream.
|
/// - 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() {
|
let stream = match config.sample_format() {
|
||||||
SampleFormat::I8 => Self::create_stream::<i8>(device, config.into(), generator),
|
SampleFormat::I8 => Self::create_stream::<i8>(device, config.into(), generator),
|
||||||
SampleFormat::I16 => Self::create_stream::<i16>(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>(
|
pub fn create_stream<T>(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
config: StreamConfig,
|
config: StreamConfig,
|
||||||
mut generator: Generator,
|
mut generator: GeneratorFn,
|
||||||
) -> Result<Stream, anyhow::Error>
|
) -> Result<Stream, anyhow::Error>
|
||||||
where
|
where
|
||||||
T: SizedSample + FromSample<f32>,
|
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;
|
pub use audio_player::AudioPlayer;
|
||||||
mod init;
|
mod init;
|
||||||
pub use init::*;
|
pub use init::*;
|
||||||
|
pub mod generators;
|
||||||
|
mod phase;
|
||||||
|
pub(crate) use phase::Phase;
|
||||||
|
|
||||||
/// Produces one `f32` sample per call.
|
type Pitch = f32;
|
||||||
///
|
|
||||||
/// `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>;
|
|
||||||
|
|
||||||
/// A single tone: a pitch held for a fixed duration.
|
/// A single tone: a pitch held for a fixed duration.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[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