P2b: offline WAV render of the arrangement
This commit is contained in:
@@ -20,6 +20,46 @@ use zhila_protocol::{
|
||||
|
||||
const VOICES: usize = 32;
|
||||
|
||||
/// Input for the offline renderer. The same voices, gains and arrangement as
|
||||
/// the real-time engine; no cpal device is opened.
|
||||
pub struct RenderRequest {
|
||||
pub sections: Vec<Pattern>,
|
||||
pub material: Option<MaterialSnapshot>,
|
||||
pub gains: [f32; VEIN_COUNT],
|
||||
pub bpm: f32,
|
||||
pub sample_rate: u32,
|
||||
pub tail_sec: f32,
|
||||
}
|
||||
|
||||
/// Render the full arrangement to interleaved stereo f32. The result includes
|
||||
/// the tail so the last voices decay instead of being cut.
|
||||
pub fn render_arrangement(request: RenderRequest) -> Vec<f32> {
|
||||
let sections = request.sections;
|
||||
let sample_rate = request.sample_rate.max(8_000);
|
||||
let fallback = sections
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(zhila_core::default_pattern);
|
||||
let (_, consumer) = RingBuffer::new(16);
|
||||
let mut state = AudioState::new(sample_rate, 2, &fallback, consumer);
|
||||
state.bpm = request.bpm.clamp(MIN_BPM, MAX_BPM);
|
||||
state.gains = request.gains;
|
||||
state.material = request.material.map(Arc::new);
|
||||
state.playing = true;
|
||||
state.step = 0;
|
||||
state.pending = 0.0;
|
||||
state.bar = 1.0;
|
||||
let steps = sections.len().max(1) * LOOP_STEPS;
|
||||
let step_samples = sample_rate as f64 * 60.0 / state.bpm as f64 / 2.0;
|
||||
let frames = (steps as f64 * step_samples
|
||||
+ request.tail_sec.max(0.0) as f64 * sample_rate as f64)
|
||||
.round() as usize;
|
||||
let mut buffer = vec![0.0f32; frames * 2];
|
||||
let levels = Levels::new();
|
||||
state.render(&mut buffer, &levels);
|
||||
buffer
|
||||
}
|
||||
|
||||
/// (vein index, velocity, optional (core index, face index)) for one step.
|
||||
type StepTrigger = (usize, u8, Option<(usize, u8)>);
|
||||
|
||||
@@ -744,3 +784,29 @@ impl EngineControl for NullEngine {
|
||||
MeterFrame::silent()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn renders_a_non_silent_arrangement() {
|
||||
let sections = vec![zhila_core::default_pattern(); 4];
|
||||
let buffer = render_arrangement(RenderRequest {
|
||||
sections,
|
||||
material: None,
|
||||
gains: zhila_core::INITIAL_GAINS,
|
||||
bpm: 128.0,
|
||||
sample_rate: 48_000,
|
||||
tail_sec: 0.25,
|
||||
});
|
||||
let frames = buffer.len() / 2;
|
||||
let expected =
|
||||
(64.0f64 * (48_000.0 * 60.0 / 128.0 / 2.0) + 0.25 * 48_000.0).round() as usize;
|
||||
assert!((frames as i64 - expected as i64).abs() < 4);
|
||||
let peak = buffer
|
||||
.iter()
|
||||
.fold(0.0f32, |peak, sample| peak.max(sample.abs()));
|
||||
assert!(peak > 0.01, "render was silent: {peak}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user