use std::collections::BTreeMap;
use std::sync::{Arc, mpsc};
use crate::ast::{FxArg, FxValue, PatternDef, Ramp, Transform};
use crate::piece::{Piece, Section};
use treble::app::prelude::{AudioGraph, AutomationSpec, AutomationTarget, BusSpec, ParameterRamp};
use treble::audio::{EventScheduler, InstrumentAudioMessage, render_block};
use treble::core::graph::{AudioOutputSink, Entry, Sink, SinkTelemetry, System};
use treble::core::{Block, Note};
use treble::instruments::prelude::InstrumentRegistry;
use super::compile::{
CompiledPattern, LineTravel, NOTE_GATE, PatternGate, RampWindow, Strike, Travel,
compile_pattern, core_curve, cycle_seconds, cycle_strikes_at, fx_ramp_window, pattern_fx,
transform_travels, travel_f64,
};
#[derive(Debug, Clone)]
pub struct RenderedPiece {
pub sample_rate: u32,
pub samples: Vec<f32>,
pub seconds: f64,
pub rendered_seconds: f64,
pub occurrences: usize,
pub notes: usize,
pub unused: Vec<String>,
pub telemetry: RenderTelemetry,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct RenderTelemetry {
pub islands: usize,
pub slots: usize,
pub worker_threads: usize,
pub pre_limiter_peak: f32,
pub post_limiter_peak: f32,
pub max_gain_reduction_db: f32,
pub limited_samples: usize,
}
struct Placed {
pattern: CompiledPattern,
slot: usize,
start_frame: u64,
origin_cycle: u64,
cycle_frames: u64,
from_cycle: u32,
to_cycle: u32,
section_cycles: u32,
}
#[derive(Default)]
struct IslandBuilder {
graph: AudioGraph,
placed: Vec<Placed>,
automations: Vec<AutomationSpec>,
bus_members: Vec<usize>,
bus: Option<(String, Vec<treble::instruments::spec::FxSpec>)>,
start_frame: u64,
end_frame: u64,
}
struct RenderIsland {
system: System,
scheduler: EventScheduler,
start_frame: u64,
end_frame: u64,
}
struct IslandAudio {
start_frame: u64,
samples: Vec<f32>,
}
enum WorkerMessage {
Advanced(u64),
Done {
index: usize,
result: Result<IslandAudio, String>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum Progress {
Compiling { done: usize, total: usize },
Building { slots: usize },
Rendering { frames: u64, total_frames: u64 },
Done { frames: u64 },
}
pub fn render_with_progress(
piece: &Piece,
registry: &InstrumentRegistry,
sample_rate: u32,
progress: &mut dyn FnMut(Progress),
) -> Result<RenderedPiece, String> {
render_inner(piece, registry, sample_rate, progress)
}
pub fn render(
piece: &Piece,
registry: &InstrumentRegistry,
sample_rate: u32,
) -> Result<RenderedPiece, String> {
render_inner(piece, registry, sample_rate, &mut |_| {})
}
fn render_inner(
piece: &Piece,
registry: &InstrumentRegistry,
sample_rate: u32,
progress: &mut dyn FnMut(Progress),
) -> Result<RenderedPiece, String> {
if piece.timeline.is_empty() {
return Err("the arrangement plays no section, so there is nothing to render".into());
}
let tail_frames = (piece.tail * f64::from(sample_rate)).round() as u64;
let mut occurrence_islands = Vec::new();
let mut grouped_islands: BTreeMap<String, IslandBuilder> = BTreeMap::new();
let mut bus_chains: BTreeMap<String, Vec<treble::instruments::spec::FxSpec>> = BTreeMap::new();
let mut at_frame = 0u64;
for (index, occurrence) in piece.timeline.iter().enumerate() {
progress(Progress::Compiling {
done: index,
total: piece.timeline.len(),
});
let section = &piece.sections[occurrence.section];
let cycle_frames = section_cycle_frames(section, sample_rate);
let occurrence_frames = cycle_frames * u64::from(section.cycles);
for group in §ion.groups {
if group.transforms.iter().any(transform_travels) {
return Err(format!(
"group '{}': a bus serves the whole piece, so its filters cannot travel — \
move the range onto the member lines",
group.name
));
}
if !bus_chains.contains_key(&group.name) {
let (fx, _) = pattern_fx(
&group.transforms,
section.cycle_seconds(),
LineTravel {
line: Travel::START,
cycle: occurrence.start_cycle as f64,
origin: occurrence.start_cycle,
divisions: section.sig.0,
},
)
.map_err(|error| format!("group '{}': {error}", group.name))?;
bus_chains.insert(group.name.clone(), fx);
}
}
let origin_cycle = occurrence.start_cycle;
let lines = section
.patterns
.iter()
.chain(&piece.throughout)
.filter(|line| !line.muted && !section.muted);
let mut occurrence = IslandBuilder {
start_frame: at_frame,
end_frame: at_frame + occurrence_frames + tail_frames,
..IslandBuilder::default()
};
for line in lines {
let island = if let Some(group) = &line.group {
grouped_islands
.entry(group.clone())
.or_insert_with(|| IslandBuilder {
start_frame: 0,
..IslandBuilder::default()
})
} else {
&mut occurrence
};
let placed_line = mount_line(
&mut island.graph,
registry,
section,
line,
origin_cycle,
at_frame,
cycle_frames,
&mut island.automations,
piece.seed,
)?;
if line.group.is_some() {
island.bus_members.push(placed_line.slot);
}
island.placed.push(placed_line);
}
if !occurrence.placed.is_empty() {
occurrence_islands.push(occurrence);
}
at_frame += occurrence_frames;
}
let arrangement_frames = at_frame;
let total_frames = arrangement_frames + tail_frames;
for (name, island) in &mut grouped_islands {
island.end_frame = total_frames;
island.bus = Some((name.clone(), bus_chains.remove(name).unwrap_or_default()));
}
progress(Progress::Compiling {
done: piece.timeline.len(),
total: piece.timeline.len(),
});
let builders = occurrence_islands
.into_iter()
.chain(grouped_islands.into_values())
.collect::<Vec<_>>();
let slots = builders.iter().map(|island| island.placed.len()).sum();
progress(Progress::Building { slots });
let mut notes = 0;
let mut islands = Vec::with_capacity(builders.len());
for builder in builders {
let (island, island_notes) = build_island(builder, sample_rate)?;
notes += island_notes;
islands.push(island);
}
let island_count = islands.len();
let worker_threads = if island_count == 0 {
0
} else {
rayon::current_num_threads().min(island_count)
};
let rendered = render_islands(islands, total_frames, progress)?;
let (samples, master) = mix_and_limit(rendered, total_frames, sample_rate);
progress(Progress::Done {
frames: total_frames,
});
Ok(RenderedPiece {
sample_rate,
samples,
seconds: piece.seconds(),
rendered_seconds: total_frames as f64 / f64::from(sample_rate),
occurrences: piece.timeline.len(),
notes,
unused: piece.unused.clone(),
telemetry: RenderTelemetry {
islands: island_count,
slots,
worker_threads,
pre_limiter_peak: master.pre_limiter_peak,
post_limiter_peak: master.post_limiter_peak,
max_gain_reduction_db: master.max_gain_reduction_db,
limited_samples: master.limited_samples,
},
})
}
fn build_island(
mut builder: IslandBuilder,
sample_rate: u32,
) -> Result<(RenderIsland, usize), String> {
if let Some((name, fx)) = builder.bus.take() {
builder.graph.set_buses(vec![BusSpec {
name,
fx,
members: builder.bus_members,
}]);
}
builder.graph.set_automations(builder.automations);
let mut system = builder
.graph
.compile(sample_rate as f32)
.map_err(|error| format!("could not build a render island: {error:?}"))?;
system
.set_sink_parameter(0, "limiter_threshold", f32::MAX)
.map_err(|error| format!("could not configure a render island: {error}"))?;
let mut scheduler = EventScheduler::new();
let notes = schedule(&mut scheduler, &builder.placed, &builder.graph);
Ok((
RenderIsland {
system,
scheduler,
start_frame: builder.start_frame,
end_frame: builder.end_frame,
},
notes,
))
}
fn render_islands(
islands: Vec<RenderIsland>,
piece_frames: u64,
progress: &mut dyn FnMut(Progress),
) -> Result<Vec<IslandAudio>, String> {
let total_work = islands
.iter()
.map(|island| island.end_frame - island.start_frame)
.sum::<u64>()
.max(1);
let island_count = islands.len();
let mut completed_work = 0u64;
let mut completed = 0usize;
let mut results = (0..island_count).map(|_| None).collect::<Vec<_>>();
let (sender, receiver) = mpsc::channel();
let report_every = (total_work / 100).max(1);
std::thread::scope(|thread_scope| {
let worker_sender = sender.clone();
thread_scope.spawn(move || {
rayon::scope(|scope| {
for (index, island) in islands.into_iter().enumerate() {
let sender = worker_sender.clone();
scope.spawn(move |_| {
let result = render_island(island, report_every, &sender);
let _ = sender.send(WorkerMessage::Done { index, result });
});
}
});
});
drop(sender);
while completed < island_count {
let Ok(message) = receiver.recv() else {
break;
};
match message {
WorkerMessage::Advanced(frames) => {
completed_work = (completed_work + frames).min(total_work);
let piece_progress = ((completed_work as u128 * piece_frames as u128)
/ total_work as u128) as u64;
progress(Progress::Rendering {
frames: piece_progress.min(piece_frames),
total_frames: piece_frames,
});
}
WorkerMessage::Done { index, result } => {
results[index] = Some(result);
completed += 1;
}
}
}
});
results
.into_iter()
.enumerate()
.map(|(index, result)| {
result.ok_or_else(|| format!("render island {index} stopped without a result"))?
})
.collect()
}
fn render_island(
mut island: RenderIsland,
report_every: u64,
progress: &mpsc::Sender<WorkerMessage>,
) -> Result<IslandAudio, String> {
let frames = island.end_frame - island.start_frame;
let mut samples = Vec::with_capacity(frames as usize * 2);
let mut frame = island.start_frame;
let mut reported = frame;
let mut next_report = frame + report_every;
while frame < island.end_frame {
frame = render_block(
&mut island.system,
&mut island.scheduler,
frame,
&mut samples,
);
if frame >= next_report {
let bounded = frame.min(island.end_frame);
let _ = progress.send(WorkerMessage::Advanced(bounded - reported));
reported = bounded;
next_report = frame + report_every;
}
}
if reported < island.end_frame {
let _ = progress.send(WorkerMessage::Advanced(island.end_frame - reported));
}
samples.truncate(frames as usize * 2);
Ok(IslandAudio {
start_frame: island.start_frame,
samples,
})
}
fn mix_and_limit(
islands: Vec<IslandAudio>,
total_frames: u64,
sample_rate: u32,
) -> (Vec<f32>, SinkTelemetry) {
let mut mixed = vec![0.0f32; total_frames as usize * 2];
for island in islands {
let start = island.start_frame as usize;
for (offset, frame) in island.samples.chunks_exact(2).enumerate() {
let target = (start + offset) * 2;
if target + 1 >= mixed.len() {
break;
}
mixed[target] += frame[0];
mixed[target + 1] += frame[1];
}
}
let mut sink = AudioOutputSink::new(sample_rate as f32);
let mut telemetry = SinkTelemetry::default();
for chunk in mixed.chunks_mut(512 * 2) {
let block: Block = chunk
.chunks_exact(2)
.map(|frame| [frame[0], frame[1]])
.collect();
sink.push(Arc::new(block), 0);
for (target, frame) in chunk.chunks_exact_mut(2).zip(sink.consume()) {
target.copy_from_slice(&frame);
}
if let Some(block) = sink.telemetry() {
telemetry.pre_limiter_peak = telemetry.pre_limiter_peak.max(block.pre_limiter_peak);
telemetry.post_limiter_peak = telemetry.post_limiter_peak.max(block.post_limiter_peak);
telemetry.max_gain_reduction_db = telemetry
.max_gain_reduction_db
.max(block.max_gain_reduction_db);
telemetry.limited_samples += block.limited_samples;
}
}
(mixed, telemetry)
}
pub(crate) fn section_cycle_frames(section: &Section, sample_rate: u32) -> u64 {
(cycle_seconds(section.bpm, section.sig) * f64::from(sample_rate)).round() as u64
}
#[allow(clippy::too_many_arguments)]
fn mount_line(
graph: &mut AudioGraph,
registry: &InstrumentRegistry,
section: &Section,
line: &PatternDef,
origin_cycle: u64,
start_frame: u64,
cycle_frames: u64,
automations: &mut Vec<AutomationSpec>,
seed: u64,
) -> Result<Placed, String> {
let line_origin = travel_origin(line, origin_cycle);
let travel = LineTravel {
line: Travel::START,
cycle: line_origin as f64,
origin: line_origin,
divisions: section.sig.0,
};
let (audio_fx, fx_ramps) = pattern_fx(&line.transforms, section.cycle_seconds(), travel)
.map_err(|error| format!("'{}': {error}", line.name))?;
let mut spec = registry
.get(&line.instrument)
.ok_or_else(|| format!("'{}': unknown instrument '{}'", line.name, line.instrument))?
.clone();
let instrument_fx = spec.fx.len();
spec.fx.extend(audio_fx.clone());
let slot = graph
.add_spec(spec.clone())
.map_err(|error| format!("'{}': {error}", line.name))?;
let compiled = compile_pattern(
line,
section.scale,
audio_fx,
fx_ramps,
instrument_fx,
spec,
PatternGate {
name: line.name.clone(),
..PatternGate::default()
},
line_origin,
section.sig.0,
seed,
);
for ramp in &compiled.fx_ramps {
let Some(window) =
fx_ramp_window(ramp, compiled.window, compiled.ramp_origin, section.sig.0)
else {
continue;
};
let (start, end) = window_frames(window, origin_cycle, start_frame, cycle_frames);
automations.push(AutomationSpec {
target: AutomationTarget::InstrumentFx {
slot,
fx_index: compiled.instrument_fx + ramp.chain_index,
},
ramp: ParameterRamp {
param: ramp.param.clone(),
from: ramp.from,
to: ramp.to,
start_frame: start,
end_frame: end,
curve: core_curve(window.curve),
},
});
}
let span = line.span;
Ok(Placed {
pattern: compiled,
slot,
start_frame,
origin_cycle,
cycle_frames,
from_cycle: span.map_or(1, |span| span.start()),
to_cycle: span.map_or(section.cycles, |span| span.end(section.cycles)),
section_cycles: section.cycles,
})
}
fn window_frames(
window: RampWindow,
origin_cycle: u64,
start_frame: u64,
cycle_frames: u64,
) -> (u64, u64) {
let into = window.origin.saturating_sub(origin_cycle);
let start = start_frame + into * cycle_frames;
let span = (window.span * cycle_frames as f64).round().max(1.0) as u64;
(start, start + span)
}
fn schedule(scheduler: &mut EventScheduler, placed: &[Placed], graph: &AudioGraph) -> usize {
let mut notes = 0;
for line in placed {
let Some(&source_index) = graph.source_map.get(&line.slot) else {
continue;
};
for cycle in line.from_cycle..=line.to_cycle.min(line.section_cycles) {
let absolute = line.origin_cycle + u64::from(cycle - 1);
let within_section = u64::from(cycle - 1);
let cycle_start = line.start_frame + u64::from(cycle - 1) * line.cycle_frames;
for strike in cycle_strikes_at(&line.pattern, absolute, within_section) {
let start = cycle_start + (strike.start * line.cycle_frames as f64).round() as u64;
let end = start
+ ((strike.end - strike.start) * NOTE_GATE * line.cycle_frames as f64).round()
as u64;
for midi in strike.notes {
let note = Note::from_midi(midi);
scheduler.schedule(
start,
InstrumentAudioMessage::NoteStart {
source_index,
note,
velocity: strike.velocity,
},
);
scheduler.schedule(
end.max(start + 1),
InstrumentAudioMessage::NoteStop { source_index, note },
);
notes += 1;
}
}
}
}
notes
}
#[cfg(test)]
mod tests {
use super::*;
use treble::instruments::prelude::InstrumentRegistry;
fn rendered(source: &str) -> RenderedPiece {
let (program, errors) = crate::parser::parse_program(source);
assert!(errors.is_empty(), "parse: {errors:?}");
let (piece, errors) = crate::piece::resolve(&program, (120, (4, 4), None));
assert!(errors.is_empty(), "resolve: {errors:?}");
render(&piece, &InstrumentRegistry::built_in(), 44_100).expect("render")
}
fn peak(rendered: &RenderedPiece, from: u64, to: u64) -> f32 {
rendered.samples[(from as usize * 2)..(to as usize * 2).min(rendered.samples.len())]
.iter()
.fold(0.0f32, |loudest, sample| loudest.max(sample.abs()))
}
#[test]
fn a_piece_renders_audible_samples() {
let piece = rendered("bpm 120\ntail 1.0\nsection a 2 {\n beat kick \"x ~ x ~\"\n}\n");
assert_eq!(piece.sample_rate, 44_100);
assert!((piece.seconds - 4.0).abs() < 1e-6, "{}", piece.seconds);
assert_eq!(piece.samples.len(), 5 * 44_100 * 2);
assert_eq!(piece.notes, 4);
assert!(
peak(&piece, 0, 44_100) > 0.001,
"the first second is silent"
);
}
#[test]
fn section_strikes_point_back_at_the_written_steps() {
let source = "bpm 120\ntail 0\nsection a 2 {\n bass sine \"<c3 [e3 g3]> ~\" @ 2\n hat sine \"c5*2\"\n}\n";
let (program, errors) = crate::parser::parse_program(source);
assert!(errors.is_empty(), "parse: {errors:?}");
let (piece, errors) = crate::piece::resolve(&program, (120, (4, 4), None));
assert!(errors.is_empty(), "resolve: {errors:?}");
let lines = vec!["bass".to_string()];
let strikes = section_strikes(&piece, &InstrumentRegistry::built_in(), 0, Some(&lines))
.expect("strikes");
assert_eq!(strikes.len(), 1, "only the line asked for");
let notation = "<c3 [e3 g3]> ~";
let written: Vec<(u32, &str)> = strikes[0]
.strikes
.iter()
.map(|(cycle, strike)| {
(
*cycle,
¬ation[strike.source.start as usize..strike.source.end as usize],
)
})
.collect();
assert_eq!(written, vec![(1, "e3"), (1, "g3")]);
}
#[test]
fn a_late_line_ramps_from_its_own_first_bar() {
let source =
"bpm 120\ntail 0\nsection a 4 {\n late sine \"c4\" @ 3.. | vel 0.2..1.0 | ramp 2\n}\n";
let (program, errors) = crate::parser::parse_program(source);
assert!(errors.is_empty(), "parse: {errors:?}");
let (piece, errors) = crate::piece::resolve(&program, (120, (4, 4), None));
assert!(errors.is_empty(), "resolve: {errors:?}");
let lanes =
section_strikes(&piece, &InstrumentRegistry::built_in(), 0, None).expect("strikes");
let velocities: Vec<(u32, f32)> = lanes[0]
.strikes
.iter()
.map(|(cycle, strike)| (*cycle, (strike.velocity * 100.0).round() / 100.0))
.collect();
assert_eq!(velocities, vec![(2, 0.2), (3, 0.6)]);
let vel = &lanes[0].automations[0];
assert_eq!(
vel.points
.first()
.map(|p| (p.0, (p.1 * 100.0).round() / 100.0)),
Some((2.0, 0.2))
);
}
fn midi_of(source: &str, line: &str) -> Vec<u8> {
let (program, errors) = crate::parser::parse_program(source);
assert!(errors.is_empty(), "parse: {errors:?}");
let (piece, errors) = crate::piece::resolve(&program, (120, (4, 4), None));
assert!(errors.is_empty(), "resolve: {errors:?}");
let mut notes = scheduled_notes(&piece, &InstrumentRegistry::built_in(), 44_100)
.expect("scheduled notes");
notes.sort_by_key(|note| note.start);
notes
.iter()
.filter(|note| note.line == line)
.map(|note| note.midi)
.collect()
}
#[test]
fn alternation_counts_from_the_section_not_the_piece() {
let source = "bpm 120\ntail 0\n\
section pad 4 {\n rest sine \"~\"\n}\n\
section one 3 {\n a sine \"<c3 e4 c5>\"\n}\n\
section gap 1 {\n quiet sine \"~\"\n}\n\
section two 3 {\n b sine \"<c3 e4 c5>\"\n}\n\
arrange pad one gap two\n";
let written = vec![48, 64, 72];
assert_eq!(midi_of(source, "a"), written, "the first playing");
assert_eq!(
midi_of(source, "b"),
written,
"an identical section must not rotate because it starts elsewhere"
);
let spanned = "bpm 120\ntail 0\n\
section s 4 {\n chords sine \"<c3 e4 c5 g4>\"\n late sine \"<c3 e4 c5 g4>\" @ 3..\n}\n";
assert_eq!(midi_of(spanned, "chords"), vec![48, 64, 72, 67]);
assert_eq!(
midi_of(spanned, "late"),
vec![72, 67],
"a line entering at bar 3 takes the third alternative, not the first"
);
}
#[test]
fn the_arrangement_orders_what_is_heard() {
let piece = rendered(
"bpm 120\ntail 0\n\
section a 1 {\n beat kick \"x\"\n}\n\
section b 1 {\n rest kick \"~\"\n}\n\
arrange b a\n",
);
let cycle = 2 * 44_100;
assert!(
peak(&piece, 0, cycle) < 1e-6,
"the silent section must be silent"
);
assert!(
peak(&piece, cycle, 2 * cycle) > 0.001,
"the sounding one must follow"
);
}
#[test]
fn ungrouped_occurrences_become_independent_render_islands() {
let piece = rendered("bpm 120\ntail 0\nsection a 1 {\n k kick \"x\"\n}\narrange a a a\n");
assert_eq!(piece.occurrences, 3);
assert_eq!(piece.telemetry.islands, 3);
assert_eq!(piece.telemetry.slots, 3);
assert!(piece.telemetry.worker_threads >= 1);
}
#[test]
fn a_shared_bus_remains_one_piece_wide_island() {
let piece = rendered(
"bpm 120\ntail 0\nsection a 1 {\n group drums {\n k kick \"x\"\n } | reverb 0.25\n}\narrange a a a\n",
);
assert_eq!(piece.occurrences, 3);
assert_eq!(piece.telemetry.islands, 1);
assert_eq!(piece.telemetry.slots, 3);
}
#[test]
fn a_span_places_a_line_inside_its_section() {
let piece = rendered("bpm 120\ntail 0\nsection a 4 {\n fill kick \"x\" @ 4\n}\n");
assert_eq!(piece.notes, 1);
let cycle = 2 * 44_100;
assert!(peak(&piece, 0, 3 * cycle) < 1e-6, "cycles 1-3 are silent");
assert!(peak(&piece, 3 * cycle, 4 * cycle) > 0.001, "cycle 4 sounds");
}
#[test]
fn nothing_leaves_above_the_master_ceiling() {
let piece = rendered(
"bpm 120\ntail 0\nsection dense 2 {\n\
\x20 k kick \"x*4\"\n s snare \"x*4\"\n h hihat \"x*8\"\n c clap \"x*4\"\n\
\x20 t tom \"x*4\"\n r rim \"x*4\"\n p pad \"[c3,e3,g3]\"\n b bass \"c2*4\"\n}\n",
);
let worst = piece.samples.iter().fold(0.0f32, |m, s| m.max(s.abs()));
assert!(worst <= 0.95 + 1e-6, "peaked at {worst}");
assert!(piece.telemetry.pre_limiter_peak >= piece.telemetry.post_limiter_peak);
assert!(piece.telemetry.max_gain_reduction_db > 0.0);
assert!(piece.telemetry.limited_samples > 0);
}
#[test]
fn progress_is_reported_in_order_and_reaches_the_end() {
let (program, _) = crate::parser::parse_program(
"bpm 120\ntail 0\nsection a 2 {\n beat kick \"x ~ x ~\"\n}\n",
);
let (piece, _) = crate::piece::resolve(&program, (120, (4, 4), None));
let mut seen: Vec<String> = Vec::new();
let mut last_fraction = 0.0f64;
let out = render_with_progress(
&piece,
&InstrumentRegistry::built_in(),
44_100,
&mut |progress| {
match &progress {
Progress::Compiling { .. } => seen.push("compiling".into()),
Progress::Building { .. } => seen.push("building".into()),
Progress::Rendering {
frames,
total_frames,
} => {
let fraction = *frames as f64 / *total_frames as f64;
assert!(fraction >= last_fraction, "progress went backwards");
assert!(fraction <= 1.0, "progress exceeded 100%");
last_fraction = fraction;
seen.push("rendering".into());
}
Progress::Done { .. } => seen.push("done".into()),
};
},
)
.expect("render");
assert_eq!(seen.first().map(String::as_str), Some("compiling"));
assert_eq!(seen.last().map(String::as_str), Some("done"));
assert!(seen.contains(&"building".to_string()));
assert!(
seen.iter().filter(|s| *s == "rendering").count() > 10,
"too few progress reports to drive a bar"
);
assert!(!out.samples.is_empty());
}
#[test]
fn the_seed_rerolls_a_generated_walk_and_repeats_exactly() {
let render_with = |seed: u64| {
let source = format!(
"seed {seed}\nbpm 120\ntail 0\nsection a 4 {{\n walk pluck \"solo(0..7, 6)\"\n}}\n"
);
let (program, errors) = crate::parser::parse_program(&source);
assert!(errors.is_empty(), "{errors:?}");
let (piece, errors) = crate::piece::resolve(&program, (120, (4, 4), None));
assert!(errors.is_empty(), "{errors:?}");
let registry = InstrumentRegistry::built_in();
let notes = scheduled_notes(&piece, ®istry, 44_100).expect("notes");
notes.into_iter().map(|note| note.midi).collect::<Vec<u8>>()
};
let zero = render_with(0);
let seven = render_with(7);
assert!(!zero.is_empty(), "the walk generated nothing");
assert_ne!(zero, seven, "the seed changed nothing");
assert_eq!(zero, render_with(0), "the same seed did not reproduce");
assert_eq!(seven, render_with(7), "the same seed did not reproduce");
}
#[test]
fn two_renders_of_one_piece_are_identical() {
let source = "bpm 120\ntail 0\nsection a 2 {\n k kick \"x*4\"\n h hihat \"x*8\"\n p pad \"[c3,e3,g3]\"\n}\n";
let (program, _) = crate::parser::parse_program(source);
let (piece, _) = crate::piece::resolve(&program, (120, (4, 4), None));
let registry = InstrumentRegistry::built_in();
let first = render(&piece, ®istry, 44_100).expect("render");
let second = render(&piece, ®istry, 44_100).expect("render");
assert_eq!(
first.samples, second.samples,
"two renders of one piece differed"
);
}
#[test]
fn an_empty_arrangement_is_refused_rather_than_rendered_silent() {
let (program, _) = crate::parser::parse_program("bpm 120\n");
let (piece, _) = crate::piece::resolve(&program, (120, (4, 4), None));
let error =
render(&piece, &InstrumentRegistry::built_in(), 44_100).expect_err("nothing to render");
assert!(error.contains("nothing to render"), "{error}");
}
}
pub fn scheduled_notes(
piece: &Piece,
registry: &InstrumentRegistry,
sample_rate: u32,
) -> Result<Vec<ScheduledNote>, String> {
let mut notes = Vec::new();
let mut at_frame = 0u64;
for occurrence in &piece.timeline {
let section = &piece.sections[occurrence.section];
let cycle_frames = section_cycle_frames(section, sample_rate);
let origin_cycle = occurrence.start_cycle;
for line in section
.patterns
.iter()
.chain(&piece.throughout)
.filter(|line| !line.muted && !section.muted)
{
let compiled = compile_line(piece, section, line, registry, origin_cycle)?;
for (cycle, strike) in line_strikes(section, line, &compiled, origin_cycle) {
let cycle_start = at_frame + u64::from(cycle) * cycle_frames;
let start = cycle_start + (strike.start * cycle_frames as f64).round() as u64;
let end = start
+ ((strike.end - strike.start) * NOTE_GATE * cycle_frames as f64).round()
as u64;
for midi in strike.notes {
notes.push(ScheduledNote {
start,
end: end.max(start + 1),
midi,
line: line.name.clone(),
section: section.name.clone(),
});
}
}
}
at_frame += cycle_frames * u64::from(section.cycles);
}
notes.sort_by_key(|note| (note.start, note.midi));
Ok(notes)
}
pub fn section_strikes(
piece: &Piece,
registry: &InstrumentRegistry,
section: usize,
lines: Option<&[String]>,
) -> Result<Vec<LineStrikes>, String> {
let placed = &piece.sections[section];
let origin_cycle = first_playing(piece, section);
placed
.patterns
.iter()
.filter(|line| lines.is_none_or(|names| names.contains(&line.name)))
.map(|line| {
let compiled = compile_line(piece, placed, line, registry, origin_cycle)?;
let strikes = line_strikes(placed, line, &compiled, origin_cycle).collect();
Ok(LineStrikes {
line: line.name.clone(),
instrument: line.instrument.clone(),
strikes,
automations: line_automations(placed, line, &compiled, origin_cycle),
})
})
.collect()
}
pub fn first_playing(piece: &Piece, section: usize) -> u64 {
piece
.timeline
.iter()
.find(|occurrence| occurrence.section == section)
.map_or(0, |occurrence| occurrence.start_cycle)
}
#[derive(Debug, Clone, PartialEq)]
pub struct LineStrikes {
pub line: String,
pub instrument: String,
pub strikes: Vec<(u32, Strike)>,
pub automations: Vec<Automation>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Automation {
pub label: String,
pub unit: &'static str,
pub low: f64,
pub high: f64,
pub points: Vec<(f64, f64)>,
}
const SAMPLES_PER_CYCLE: u32 = 16;
const SAMPLES_PER_CYCLE_TIMED: u32 = 64;
fn line_automations(
section: &Section,
line: &PatternDef,
compiled: &CompiledPattern,
origin_cycle: u64,
) -> Vec<Automation> {
let mut found: Vec<(String, &'static str, Ramp<f64>)> = Vec::new();
for transform in &line.transforms {
travelling_values(transform, &mut found);
}
let from = line.span.map_or(1, |s| s.start());
let to = line
.span
.map_or(section.cycles, |s| s.end(section.cycles))
.min(section.cycles);
found
.into_iter()
.map(|(label, unit, ramp)| {
let per_cycle = if ramp.own_window().is_some() {
SAMPLES_PER_CYCLE_TIMED
} else {
SAMPLES_PER_CYCLE
};
let value_at = |within: f64| {
let travel = compiled.line_travel(origin_cycle as f64 + within);
travel_f64(&ramp, travel).start()
};
let mut points = Vec::new();
for cycle in from..=to {
for sample in 0..per_cycle {
let within = f64::from(cycle - 1) + f64::from(sample) / f64::from(per_cycle);
points.push((within, value_at(within)));
}
}
let end = f64::from(to);
points.push((end, value_at(end - 1e-9)));
let values = ramp.values();
let low = values.iter().copied().fold(f64::INFINITY, f64::min);
let high = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
Automation {
label,
unit,
low,
high,
points,
}
})
.collect()
}
fn travelling_values(transform: &Transform, found: &mut Vec<(String, &'static str, Ramp<f64>)>) {
let mut push = |label: &str, unit: &'static str, ramp: &Ramp<f64>| {
if ramp.travels() {
found.push((label.to_string(), unit, ramp.clone()));
}
};
match transform {
Transform::Vel(ramp) => push("vel", "", ramp),
Transform::Gain(ramp) => push("gain", "", ramp),
Transform::Pan(ramp) => push("pan", "", ramp),
Transform::Lpf(ramp) => push("lpf", "Hz", ramp),
Transform::Hpf(ramp) => push("hpf", "Hz", ramp),
Transform::Reverb(ramp) => push("reverb", "", ramp),
Transform::Fast(ramp) => push("fast", "", ramp),
Transform::Slow(ramp) => push("slow", "", ramp),
Transform::Oct(ramp) => push("oct", "", &ramp_as_f64(ramp)),
Transform::Delay(time, feedback, mix) => {
push("delay time", "s", time);
push("delay feedback", "", feedback);
if let Some(mix) = mix {
push("delay mix", "", mix);
}
}
Transform::Fx(call) => {
for (index, arg) in call.args.iter().enumerate() {
let (name, value) = match arg {
FxArg::Positional(value) => (format!("#{}", index + 1), value),
FxArg::Named(name, value) => (name.clone(), value),
};
let unit = match value {
FxValue::Hertz(_) => "Hz",
FxValue::Plain(_) => "",
};
push(&format!("fx {} {name}", call.filter), unit, value.ramp());
}
}
_ => {}
}
}
fn ramp_as_f64(ramp: &Ramp<i32>) -> Ramp<f64> {
match ramp {
Ramp::Fixed(value) => Ramp::Fixed(f64::from(*value)),
Ramp::Sweep { from, to } => Ramp::Sweep {
from: f64::from(*from),
to: f64::from(*to),
},
Ramp::Steps { first, rest } => Ramp::Steps {
first: f64::from(*first),
rest: rest.iter().copied().map(f64::from).collect(),
},
Ramp::Timed {
ramp,
span_divisions,
curve,
} => Ramp::Timed {
ramp: Box::new(ramp_as_f64(ramp)),
span_divisions: *span_divisions,
curve: *curve,
},
}
}
fn compile_line(
piece: &Piece,
section: &Section,
line: &PatternDef,
registry: &InstrumentRegistry,
origin_cycle: u64,
) -> Result<CompiledPattern, String> {
let line_origin = travel_origin(line, origin_cycle);
let travel = LineTravel {
line: Travel::START,
cycle: line_origin as f64,
origin: line_origin,
divisions: section.sig.0,
};
let (audio_fx, fx_ramps) = pattern_fx(&line.transforms, section.cycle_seconds(), travel)
.map_err(|error| format!("'{}': {error}", line.name))?;
let spec = registry
.get(&line.instrument)
.ok_or_else(|| format!("'{}': unknown instrument", line.name))?
.clone();
let instrument_fx = spec.fx.len();
Ok(compile_pattern(
line,
section.scale,
audio_fx,
fx_ramps,
instrument_fx,
spec,
PatternGate::default(),
line_origin,
section.sig.0,
piece.seed,
))
}
fn travel_origin(line: &PatternDef, occurrence_start: u64) -> u64 {
occurrence_start + u64::from(line.span.map_or(1, |span| span.start()) - 1)
}
fn line_strikes<'a>(
section: &Section,
line: &PatternDef,
compiled: &'a CompiledPattern,
origin_cycle: u64,
) -> impl Iterator<Item = (u32, Strike)> + 'a {
let from = line.span.map_or(1, |s| s.start());
let to = line
.span
.map_or(section.cycles, |s| s.end(section.cycles))
.min(section.cycles);
(from..=to).flat_map(move |cycle| {
let within = cycle - 1;
cycle_strikes_at(
compiled,
origin_cycle + u64::from(within),
u64::from(within),
)
.into_iter()
.map(move |strike| (within, strike))
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScheduledNote {
pub start: u64,
pub end: u64,
pub midi: u8,
pub line: String,
pub section: String,
}