mirror of
https://github.com/storytold/soundcraft.git
synced 2026-11-10 04:09:58 -06:00
Bounces ran VST3 plugins with processMode kRealtime and never set clap.render, so plugins that work differently offline never could. Offline renders now create their plugins with Plugin::set_offline(true): VST3 plugins get kOffline in setupProcessing and in every process call, and CLAP plugins with clap.render are set to CLAP_RENDER_OFFLINE. Playback stays realtime. Fixes #1289.
202 lines
8.4 KiB
Rust
202 lines
8.4 KiB
Rust
use super::clips::{ClipFx, clip_fx_params, render_clip};
|
|
use super::pan::itu_stereo_fold;
|
|
use super::{AutoParam, BusId, ChannelFormat, MixEngine, Range, Route, Samples, Session, TrackId, main_channels};
|
|
|
|
/// Ceiling on one in-memory offline render (samples summed over channels): 2^29 f32 = 2 GiB, about
|
|
/// 93 minutes of stereo at 48 kHz. Longer requests are truncated rather than exhausting memory;
|
|
/// [`RangeRender`] streams any length.
|
|
pub const MAX_RENDER_SAMPLES: usize = 1 << 29;
|
|
|
|
/// Offline render of the main mix over `range` (planar, [`main_channels`] channels in SMPTE/WAV
|
|
/// order). The mix's delay-compensation latency is removed, so the result lines up with the
|
|
/// timeline.
|
|
pub fn render_range(s: &Session, range: Range, block: usize) -> Vec<Vec<f32>> {
|
|
render_in_memory(s, None, range, block)
|
|
}
|
|
|
|
/// Offline render of one bus over `range` (planar, the bus format's channel count).
|
|
/// The compensation delay written onto the bus is removed (track inserts, and the aux returns
|
|
/// when an aux returns onto it), so the bus lines up with the timeline. An unknown bus is silent.
|
|
pub fn render_bus(s: &Session, bus: BusId, range: Range, block: usize) -> Vec<Vec<f32>> {
|
|
render_in_memory(s, Some(bus), range, block)
|
|
}
|
|
|
|
fn render_in_memory(s: &Session, bus: Option<BusId>, range: Range, block: usize) -> Vec<Vec<f32>> {
|
|
let nch = source_channels(s, bus);
|
|
let len = usize::try_from(range.len().max(0)).unwrap_or(0).min(MAX_RENDER_SAMPLES / nch.max(1));
|
|
let mut out: Vec<Vec<f32>> = (0..nch).map(|_| Vec::with_capacity(len)).collect();
|
|
let mut render = RangeRender::new(s, bus, Range::new(range.start, range.start + len as i64), block);
|
|
while let Some((chunk, n)) = render.next_block(s) {
|
|
for (o, c) in out.iter_mut().zip(chunk) {
|
|
o.extend_from_slice(c.get(..n).unwrap_or_default());
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Channels of the main mix, or of `bus` (none for an unknown bus).
|
|
pub fn source_channels(s: &Session, bus: Option<BusId>) -> usize {
|
|
match bus {
|
|
Some(id) => s.bus(id).map_or(0, |b| b.format.channels()),
|
|
None => main_channels(s),
|
|
}
|
|
}
|
|
|
|
/// [`render_range`] or [`render_bus`] of any length, a block at a time.
|
|
pub struct RangeRender {
|
|
eng: MixEngine,
|
|
bus: Option<BusId>,
|
|
tmp: Vec<Vec<f32>>,
|
|
start: Samples,
|
|
done: usize,
|
|
total: usize,
|
|
latency: usize,
|
|
}
|
|
|
|
impl RangeRender {
|
|
/// Renders the main mix over `range`, or `bus` when one is named.
|
|
pub fn new(s: &Session, bus: Option<BusId>, range: Range, block: usize) -> Self {
|
|
let nch = source_channels(s, bus);
|
|
let mut eng = MixEngine::new(s.sample_rate.as_f64() as f32, block);
|
|
eng.set_offline(true);
|
|
let tmp = vec![vec![0.0f32; eng.max_block()]; nch];
|
|
let len = if nch == 0 { 0 } else { usize::try_from(range.len().max(0)).unwrap_or(0) };
|
|
RangeRender { eng, bus, tmp, start: range.start, done: 0, total: len, latency: 0 }
|
|
}
|
|
|
|
fn render(&mut self, s: &Session, pos: Samples, n: usize) {
|
|
match self.bus {
|
|
Some(id) => {
|
|
self.eng.render(s, pos, n, &mut []);
|
|
self.eng.copy_bus(id, n, &mut self.tmp);
|
|
}
|
|
None => self.eng.render(s, pos, n, &mut self.tmp),
|
|
}
|
|
}
|
|
|
|
/// Renders one more block of the mix of `s` (the session given to [`RangeRender::new`]) and
|
|
/// returns it as `(channels, n)`: the next `n` samples of the range lead each channel (none
|
|
/// while the mix's latency is still being skipped). `None` once the range is done.
|
|
pub fn next_block(&mut self, s: &Session) -> Option<(&[Vec<f32>], usize)> {
|
|
if self.done >= self.total {
|
|
return None;
|
|
}
|
|
let n = (self.total - self.done).min(self.eng.max_block());
|
|
self.render(s, self.start + self.done as i64, n);
|
|
if self.done == 0 {
|
|
// The first block tells the mix's latency: render that much more and drop the head.
|
|
self.latency = match self.bus {
|
|
Some(b) => self.eng.bus_latency(b),
|
|
None => self.eng.latency,
|
|
};
|
|
self.total = self.total.saturating_add(self.latency);
|
|
}
|
|
let skip = self.latency.saturating_sub(self.done).min(n);
|
|
self.done += n;
|
|
if skip > 0 && skip < n {
|
|
for c in &mut self.tmp {
|
|
c.copy_within(skip..n, 0);
|
|
}
|
|
}
|
|
Some((&self.tmp, n - skip))
|
|
}
|
|
}
|
|
|
|
/// ITU-R BS.775 stereo fold-down of planar audio in `fmt` (C and surrounds at -3 dB, LFE dropped;
|
|
/// Ambisonics play their W channel). Mono/stereo input is returned unchanged.
|
|
pub fn fold_down_stereo(fmt: ChannelFormat, ch: &[Vec<f32>]) -> Vec<Vec<f32>> {
|
|
if ch.len() <= 2 {
|
|
return ch.to_vec();
|
|
}
|
|
let len = ch.iter().map(Vec::len).max().unwrap_or(0);
|
|
let mut out = vec![vec![0.0f32; len]; 2];
|
|
let sp = fmt.speakers();
|
|
let h = std::f32::consts::FRAC_1_SQRT_2;
|
|
for (c, src) in ch.iter().enumerate() {
|
|
let (gl, gr) = match sp.get(c) {
|
|
Some(x) if sp.len() == ch.len() => itu_stereo_fold(*x),
|
|
_ if c == 0 => (h, h),
|
|
_ => (0.0, 0.0),
|
|
};
|
|
for (g, dst) in [(gl, 0usize), (gr, 1)] {
|
|
if g == 0.0 {
|
|
continue;
|
|
}
|
|
if let Some(d) = out.get_mut(dst) {
|
|
for (o, x) in d.iter_mut().zip(src.iter()) {
|
|
*o += x * g;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Render only one track's clips (no mixer processing) — Consolidate.
|
|
pub fn render_clips(s: &Session, track: TrackId, range: Range) -> Vec<Vec<f32>> {
|
|
let Some(t) = s.track(track) else { return Vec::new() };
|
|
let len = usize::try_from(range.len().max(0)).unwrap_or(0).min(MAX_RENDER_SAMPLES / t.channels().max(1));
|
|
let mut out = vec![vec![0.0f32; len]; t.channels().max(1)];
|
|
let mut scratch = Vec::new();
|
|
for c in t.clips().iter().filter(|c| !c.muted && c.range().overlaps(&range)) {
|
|
match clip_fx_params(s, c.id.0) {
|
|
None => render_clip(s, c, range.start, len, &mut out, &mut scratch),
|
|
Some(params) => {
|
|
let mut tmp = vec![vec![0.0f32; len]; out.len()];
|
|
render_clip(s, c, range.start, len, &mut tmp, &mut scratch);
|
|
let mut fx = ClipFx::new(s.sample_rate.as_f64() as f32, tmp.len());
|
|
fx.configure(params);
|
|
let mut done = 0;
|
|
while done < len {
|
|
let n = (len - done).min(4096);
|
|
let mut block: Vec<Vec<f32>> = tmp.iter().map(|ch| ch.get(done..done + n).map(<[f32]>::to_vec).unwrap_or_default()).collect();
|
|
fx.process(&mut block, n);
|
|
for (o, b) in out.iter_mut().zip(block.iter()) {
|
|
if let Some(dst) = o.get_mut(done..done + n) {
|
|
for (x, y) in dst.iter_mut().zip(b.iter()) {
|
|
*x += *y;
|
|
}
|
|
}
|
|
}
|
|
done += n;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Render one track through its inserts (pre-fader) — Commit / Freeze.
|
|
pub fn render_track_pre_fader(s: &Session, track: TrackId, range: Range) -> Vec<Vec<f32>> {
|
|
let mut solo = s.clone();
|
|
// Multichannel tracks render into a main mix of their own format (straight through).
|
|
let fmt = s.track(track).map_or(ChannelFormat::Stereo, |t| if t.channels() > 2 { t.format } else { ChannelFormat::Stereo });
|
|
solo.outputs = vec![soundcraft_model::OutputPath { name: "Out".into(), first_channel: 0, format: fmt }];
|
|
for t in &mut solo.tracks {
|
|
if t.id == track {
|
|
t.mixer.volume_db = 0.0;
|
|
t.mixer.mute = false;
|
|
t.mixer.solo = false;
|
|
t.mixer.output = Route::Main;
|
|
t.mixer.pan = match t.channels() {
|
|
1 => vec![0.0],
|
|
_ => vec![-1.0, 1.0],
|
|
};
|
|
t.automation.retain(|l| matches!(l.param, AutoParam::Plugin { .. }));
|
|
t.mixer.sends.iter_mut().for_each(|x| *x = None);
|
|
t.mixer.surround = None;
|
|
} else {
|
|
t.inactive = true;
|
|
}
|
|
}
|
|
let mut out = render_range(&solo, range, 1024);
|
|
// Mono tracks: fold back to one channel (undo the centre pan law).
|
|
if s.track(track).is_some_and(|t| t.channels() == 1)
|
|
&& let Some(l) = out.first().cloned()
|
|
{
|
|
let g = std::f32::consts::SQRT_2;
|
|
out = vec![l.iter().map(|x| x * g).collect()];
|
|
}
|
|
out
|
|
}
|