pub mod control;
pub mod graph;
pub mod profiles;
pub mod source;
pub mod stage;
pub mod stages;
pub use control::{EngineController, MAX_TEMPO_RATE, MIN_TEMPO_RATE};
pub use graph::EngineProcessor;
pub use profiles::EngineProfile;
pub use source::SourceProducer;
pub use stage::{BlockBuf, Stage, StageCtx, BLOCK_FRAMES};
use crate::engine::control::EngineShared;
use crate::engine::source::SourceRing;
use crate::error::StretchError;
#[derive(Debug, Clone)]
pub struct EngineConfig {
pub sample_rate: u32,
pub channels: usize,
pub profile: EngineProfile,
pub initial_tempo_rate: f64,
pub max_block_frames: usize,
pub source_capacity_frames: usize,
pub pre_analysis: Option<std::sync::Arc<crate::core::preanalysis::PreAnalysisArtifact>>,
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
sample_rate: 44_100,
channels: 2,
profile: EngineProfile::Tape,
initial_tempo_rate: 1.0,
max_block_frames: 1024,
source_capacity_frames: 32_768,
pre_analysis: None,
}
}
}
impl EngineConfig {
fn validate(&self) -> Result<(), StretchError> {
if !(8_000..=192_000).contains(&self.sample_rate) {
return Err(StretchError::InvalidFormat(format!(
"sample rate {} outside 8000..=192000",
self.sample_rate
)));
}
if !(1..=8).contains(&self.channels) {
return Err(StretchError::InvalidFormat(format!(
"channel count {} outside 1..=8",
self.channels
)));
}
if !(64..=8192).contains(&self.max_block_frames) {
return Err(StretchError::InvalidFormat(format!(
"max_block_frames {} outside 64..=8192",
self.max_block_frames
)));
}
let min_source = (self.max_block_frames as f64 * control::MAX_TEMPO_RATE) as usize * 4;
if self.source_capacity_frames < min_source {
return Err(StretchError::InvalidFormat(format!(
"source_capacity_frames {} below minimum {} for max_block_frames {}",
self.source_capacity_frames, min_source, self.max_block_frames
)));
}
if !self.initial_tempo_rate.is_finite() {
return Err(StretchError::InvalidRatio(
"initial_tempo_rate must be finite".to_string(),
));
}
Ok(())
}
}
#[derive(Debug)]
pub struct EngineHandles {
pub controller: EngineController,
pub processor: EngineProcessor,
pub source: SourceProducer,
}
#[derive(Debug)]
pub struct Engine;
impl Engine {
pub fn build(config: EngineConfig) -> Result<EngineHandles, StretchError> {
let stages = profiles::build_stages(config.profile, config.sample_rate, config.channels);
Self::build_with_stages(config, stages)
}
pub fn build_with_stages(
config: EngineConfig,
stages: Vec<Box<dyn Stage>>,
) -> Result<EngineHandles, StretchError> {
config.validate()?;
let initial_rate = control::clamp_tempo_rate(config.initial_tempo_rate);
let shared = EngineShared::new(initial_rate);
let ring = SourceRing::new(config.source_capacity_frames, config.channels);
let controller = EngineController::new(shared.clone(), config.sample_rate);
let processor = EngineProcessor::new(
shared,
ring.clone(),
stages,
config.channels,
config.sample_rate,
initial_rate,
config.max_block_frames,
config.pre_analysis.clone(),
);
let source = SourceProducer::new(ring);
Ok(EngineHandles {
controller,
processor,
source,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_validation_rejects_bad_values() {
assert!(Engine::build(EngineConfig {
channels: 0,
..EngineConfig::default()
})
.is_err());
assert!(Engine::build(EngineConfig {
sample_rate: 1000,
..EngineConfig::default()
})
.is_err());
assert!(Engine::build(EngineConfig {
max_block_frames: 16,
..EngineConfig::default()
})
.is_err());
assert!(Engine::build(EngineConfig {
source_capacity_frames: 100,
..EngineConfig::default()
})
.is_err());
assert!(Engine::build(EngineConfig::default()).is_ok());
}
#[test]
fn initial_tempo_rate_is_clamped_not_rejected() {
let handles = Engine::build(EngineConfig {
initial_tempo_rate: 100.0,
..EngineConfig::default()
})
.unwrap();
assert_eq!(handles.processor.current_tempo_rate(), MAX_TEMPO_RATE);
}
}