1use truce_rack_core::buffer::{AudioBuffer, BusRange};
19use truce_rack_core::bus::BusLayout;
20use truce_rack_core::error::{Error, Result};
21use truce_rack_core::events::EventList;
22use truce_rack_core::info::PluginInfo;
23use truce_rack_core::plugin::{Plugin, PluginCore, ProcessContext};
24
25pub mod cli;
26pub mod device;
27pub mod midi;
28pub mod midi_queue;
29pub mod transport;
30#[cfg(any(
31 feature = "clap",
32 feature = "vst3",
33 feature = "lv2",
34 all(feature = "au", target_vendor = "apple"),
35))]
36use truce_rack_core::scanner::PluginScanner;
37
38use cpal::Stream;
39use cpal::traits::{DeviceTrait, StreamTrait};
40
41#[cfg(feature = "gui")]
42pub mod keyboard;
43#[cfg(feature = "gui")]
44pub mod windowed;
45
46#[cfg(all(target_os = "macos", feature = "gui"))]
47pub mod menu_macos;
48
49#[cfg(target_os = "macos")]
50pub mod screenshot;
51
52pub const GUI_AVAILABLE: bool = cfg!(feature = "gui");
57
58#[derive(Debug, Clone, Copy)]
62pub enum RunMode {
63 Seconds(f32),
65 UntilSignal,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Format {
73 Clap,
75 Vst3,
77 Au,
79 Lv2,
81}
82
83impl Format {
84 #[must_use]
87 pub fn parse(s: &str) -> Option<Self> {
88 match s.to_ascii_lowercase().as_str() {
89 "clap" => Some(Self::Clap),
90 "vst3" => Some(Self::Vst3),
91 "au" => Some(Self::Au),
92 "lv2" => Some(Self::Lv2),
93 _ => None,
94 }
95 }
96
97 #[must_use]
100 pub fn tag(self) -> &'static str {
101 match self {
102 Self::Clap => "clap",
103 Self::Vst3 => "vst3",
104 Self::Au => "au",
105 Self::Lv2 => "lv2",
106 }
107 }
108}
109
110#[cfg(feature = "clap")]
120pub fn run_clap(selector: &PluginSelector, mode: RunMode, gui: bool) -> Result<()> {
121 let scanner = truce_rack_clap::ClapScanner::new();
122 let plugin = load_by_selector(&scanner, selector)?;
123 dispatch_run(plugin, mode, gui)
124}
125
126#[cfg(feature = "vst3")]
131pub fn run_vst3(selector: &PluginSelector, mode: RunMode, gui: bool) -> Result<()> {
132 let scanner = truce_rack_vst3::Vst3Scanner::new();
133 let plugin = load_by_selector(&scanner, selector)?;
134 dispatch_run(plugin, mode, gui)
135}
136
137#[cfg(all(feature = "au", target_vendor = "apple"))]
142pub fn run_au(selector: &PluginSelector, mode: RunMode, gui: bool) -> Result<()> {
143 let scanner = truce_rack_au::AuScanner::new();
144 let plugin = load_by_selector(&scanner, selector)?;
145 dispatch_run(plugin, mode, gui)
146}
147
148#[cfg(feature = "lv2")]
153pub fn run_lv2(selector: &PluginSelector, mode: RunMode, gui: bool) -> Result<()> {
154 let scanner = truce_rack_lv2::Lv2Scanner::new();
155 let plugin = load_by_selector(&scanner, selector)?;
156 dispatch_run(plugin, mode, gui)
157}
158
159#[derive(Debug, Clone)]
164pub enum PluginSelector {
165 Id(String),
167 Name(String),
169}
170
171#[cfg(any(
174 feature = "clap",
175 feature = "vst3",
176 feature = "lv2",
177 all(feature = "au", target_vendor = "apple"),
178))]
179fn load_by_selector<S>(scanner: &S, selector: &PluginSelector) -> Result<S::Plugin>
180where
181 S: PluginScanner,
182{
183 let entries = scanner.scan()?;
184 let info = match selector {
185 PluginSelector::Id(id) => entries
186 .into_iter()
187 .find(|p| p.unique_id == *id || p.name == *id),
188 PluginSelector::Name(name) => {
189 let needle = name.to_ascii_lowercase();
190 entries
191 .into_iter()
192 .find(|p| p.name.to_ascii_lowercase().contains(&needle))
193 }
194 };
195 let info = info.ok_or_else(|| {
196 let label = match selector {
197 PluginSelector::Id(s) | PluginSelector::Name(s) => s.clone(),
198 };
199 Error::PluginNotFound(label)
200 })?;
201 scanner.load(&info)
202}
203
204#[cfg(any(
207 feature = "clap",
208 feature = "vst3",
209 feature = "lv2",
210 all(feature = "au", target_vendor = "apple"),
211))]
212#[cfg(feature = "gui")]
213fn dispatch_run<P>(plugin: P, mode: RunMode, gui: bool) -> Result<()>
214where
215 P: PluginCore + Plugin<f32> + Send + 'static,
216{
217 if gui {
218 windowed::run(plugin)
219 } else {
220 run_with_plugin(plugin, mode)
221 }
222}
223
224#[cfg(any(
225 feature = "clap",
226 feature = "vst3",
227 feature = "lv2",
228 all(feature = "au", target_vendor = "apple"),
229))]
230#[cfg(not(feature = "gui"))]
231fn dispatch_run<P>(plugin: P, mode: RunMode, gui: bool) -> Result<()>
232where
233 P: PluginCore + Plugin<f32> + Send + 'static,
234{
235 if gui {
236 return Err(Error::Other(
237 "--gui requires the `gui` feature to be enabled at build time".into(),
238 ));
239 }
240 run_with_plugin(plugin, mode)
241}
242
243pub fn run_with_plugin<P>(plugin: P, mode: RunMode) -> Result<()>
249where
250 P: PluginCore + Plugin<f32> + Send + 'static,
251{
252 let (device, supported) = device::open_output_device()?;
253 let config = device::resolve_stream_config(&device, &supported);
254 let sample_rate = f64::from(config.sample_rate.0);
255 let channels = usize::from(config.channels.max(1));
256 let max_block = 1024usize;
257
258 let stream = build_audio_stream(plugin, &device, &config, sample_rate, channels, max_block)?;
259 stream
260 .play()
261 .map_err(|e| Error::Other(format!("stream.play: {e}")))?;
262
263 let _midi_in = midi::MidiInputThread::start();
266
267 match mode {
268 RunMode::Seconds(secs) => {
269 std::thread::sleep(std::time::Duration::from_secs_f32(secs));
270 }
271 RunMode::UntilSignal => loop {
272 std::thread::sleep(std::time::Duration::from_secs(1));
273 },
274 }
275 drop(stream);
276 Ok(())
277}
278
279fn build_audio_stream<P>(
280 mut plugin: P,
281 device: &cpal::Device,
282 stream_config: &cpal::StreamConfig,
283 sample_rate: f64,
284 channels: usize,
285 max_block: usize,
286) -> Result<Stream>
287where
288 P: PluginCore + Plugin<f32> + Send + 'static,
289{
290 plugin.activate(BusLayout::stereo(), sample_rate, max_block)?;
291 let stream_config = stream_config.clone();
292
293 let mut input_buf = vec![vec![0.0f32; max_block]; channels];
294 let mut output_buf = vec![vec![0.0f32; max_block]; channels];
295 let bus_in = vec![BusRange::new(0, channels)];
296 let bus_out = vec![BusRange::new(0, channels)];
297 let mut clock = transport::TransportClock::new();
298
299 let stream = device
300 .build_output_stream(
301 &stream_config,
302 move |out: &mut [f32], _: &cpal::OutputCallbackInfo| {
303 let frames = out.len() / channels.max(1);
304
305 for ch in &mut input_buf {
306 if ch.len() < frames {
307 ch.resize(frames, 0.0);
308 }
309 for v in &mut ch[..frames] {
310 *v = 0.0;
311 }
312 }
313 for ch in &mut output_buf {
314 if ch.len() < frames {
315 ch.resize(frames, 0.0);
316 }
317 for v in &mut ch[..frames] {
318 *v = 0.0;
319 }
320 }
321
322 {
323 let inputs: Vec<&[f32]> = input_buf.iter().map(|c| &c[..frames]).collect();
324 let mut outputs: Vec<&mut [f32]> =
325 output_buf.iter_mut().map(|c| &mut c[..frames]).collect();
326
327 let mut buffer =
328 AudioBuffer::new(&inputs, &mut outputs, frames, &bus_in, &bus_out);
329 let mut events = EventList::default();
330 midi_queue::drain_into(&mut events);
331 let mut out_events = EventList::default();
332 let mut ctx = ProcessContext {
333 sample_rate,
334 max_block_size: max_block,
335 transport: clock.next_block(frames, sample_rate),
336 output_events: &mut out_events,
337 };
338 let _ = plugin.process(&mut buffer, &events, &mut ctx);
339 }
340
341 device::live_route().write(out, &output_buf, channels, frames);
342 },
343 move |err| eprintln!("[truce-rack-standalone] stream error: {err}"),
344 None,
345 )
346 .map_err(|e| Error::Other(format!("build_output_stream: {e}")))?;
347 Ok(stream)
348}
349
350#[must_use]
358pub fn list_plugins() -> Vec<(Format, PluginInfo)> {
359 #[allow(unused_mut)]
362 let mut out: Vec<(Format, PluginInfo)> = Vec::new();
363
364 #[cfg(feature = "clap")]
365 if let Ok(entries) = truce_rack_clap::ClapScanner::new().scan() {
366 for e in entries {
367 out.push((Format::Clap, e));
368 }
369 }
370
371 #[cfg(feature = "vst3")]
372 if let Ok(entries) = truce_rack_vst3::Vst3Scanner::new().scan() {
373 for e in entries {
374 out.push((Format::Vst3, e));
375 }
376 }
377
378 #[cfg(all(feature = "au", target_vendor = "apple"))]
379 if let Ok(entries) = truce_rack_au::AuScanner::new().scan() {
380 for e in entries {
381 out.push((Format::Au, e));
382 }
383 }
384
385 #[cfg(feature = "lv2")]
386 if let Ok(entries) = truce_rack_lv2::Lv2Scanner::new().scan() {
387 for e in entries {
388 out.push((Format::Lv2, e));
389 }
390 }
391
392 out
393}