use std::sync::{Arc, Mutex};
use baseview::{
Event, EventStatus, Size, Window, WindowHandler, WindowOpenOptions, WindowScalePolicy,
};
const INITIAL_WINDOW: (u32, u32) = (320, 240);
use keyboard_types::{Code, KeyState, Modifiers};
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle as RwhHandle};
use truce_rack_core::buffer::{AudioBuffer, BusRange};
use truce_rack_core::bus::BusLayout;
use truce_rack_core::editor::WindowHandle as PluginParent;
use truce_rack_core::error::{Error, Result};
use truce_rack_core::events::{EventBody, EventList, MidiData};
use truce_rack_core::plugin::{Plugin, PluginCore, ProcessContext};
use crate::keyboard;
use crate::midi_queue;
const MAX_BLOCK: usize = 1024;
pub trait HostPlugin: PluginCore + Plugin<f32> + Send {}
impl<T: PluginCore + Plugin<f32> + Send> HostPlugin for T {}
pub(crate) type SharedPlugin = Arc<Mutex<dyn HostPlugin>>;
pub fn run<P>(mut plugin: P) -> Result<()>
where
P: PluginCore + Plugin<f32> + Send + 'static,
{
let plugin_name = plugin.info().name.clone();
if plugin.editor().is_none() {
eprintln!(
"[truce-rack-standalone] plugin '{plugin_name}' has no editor — running headless"
);
return crate::run_with_plugin(plugin, crate::RunMode::UntilSignal);
}
let plugin: SharedPlugin = Arc::new(Mutex::new(plugin));
let initial_size = INITIAL_WINDOW;
let mut audio = AudioController::start(Arc::clone(&plugin))?;
let mut midi = crate::midi::MidiController::start();
#[cfg(all(target_os = "macos", feature = "gui"))]
{
let channels = audio.channels();
crate::menu_macos::set_controllers(&raw mut audio, &raw mut midi, channels);
}
let window_opts = WindowOpenOptions {
title: plugin_name.clone(),
size: Size::new(f64::from(initial_size.0), f64::from(initial_size.1)),
scale: WindowScalePolicy::SystemScaleFactor,
};
let plugin_for_handler = Arc::clone(&plugin);
let plugin_name_for_handler = plugin_name.clone();
Window::open_blocking(window_opts, move |window| {
let parent = raw_handle_to_plugin_handle(window.raw_window_handle());
#[cfg(all(target_os = "macos", feature = "gui"))]
crate::menu_macos::install(&plugin_name_for_handler);
let mut editor_size: Option<(u32, u32)> = None;
{
let mut guard = plugin_for_handler.lock().expect("plugin mutex");
if let Some(editor) = guard.editor() {
if let Err(e) = editor.open(parent, 1.0) {
eprintln!("[truce-rack-standalone] editor.open failed: {e}");
}
editor.show();
editor_size = editor.size();
}
}
if let Some((w, h)) = editor_size {
window.resize(Size::new(f64::from(w), f64::from(h)));
let mut guard = plugin_for_handler.lock().expect("plugin mutex");
if let Some(editor) = guard.editor() {
editor.set_size(w, h);
}
}
StandaloneHandler {
plugin: Arc::clone(&plugin_for_handler),
plugin_name: plugin_name_for_handler,
octave_offset: 0,
}
});
#[cfg(all(target_os = "macos", feature = "gui"))]
crate::menu_macos::clear();
{
let mut guard = plugin.lock().expect("plugin mutex");
if let Some(editor) = guard.editor() {
editor.close();
}
}
drop(audio);
drop(midi);
Ok(())
}
fn raw_handle_to_plugin_handle(handle: RwhHandle) -> PluginParent {
match handle {
RwhHandle::AppKit(h) => PluginParent::NSView(h.ns_view),
RwhHandle::Win32(h) => PluginParent::HWND(h.hwnd),
#[allow(clippy::useless_conversion)]
RwhHandle::Xlib(h) => PluginParent::X11(h.window.into()),
_ => panic!("[truce-rack-standalone] unsupported raw-window-handle variant"),
}
}
struct StandaloneHandler {
plugin: SharedPlugin,
plugin_name: String,
octave_offset: i8,
}
impl WindowHandler for StandaloneHandler {
fn on_frame(&mut self, _window: &mut Window) {
if let Ok(mut guard) = self.plugin.try_lock()
&& let Some(editor) = guard.editor()
{
editor.on_idle();
}
}
fn on_event(&mut self, _window: &mut Window, event: Event) -> EventStatus {
match event {
Event::Keyboard(kb) => self.handle_keyboard(&kb),
_ => EventStatus::Ignored,
}
}
}
impl StandaloneHandler {
fn handle_keyboard(&mut self, kb: &keyboard_types::KeyboardEvent) -> EventStatus {
if kb.state == KeyState::Down && kb.code == Code::KeyS && is_mod_pressed(kb.modifiers) {
self.save_state();
return EventStatus::Captured;
}
if kb.state == KeyState::Down && kb.code == Code::KeyO && is_mod_pressed(kb.modifiers) {
self.load_state();
return EventStatus::Captured;
}
if kb.state == KeyState::Down && kb.code == Code::Space {
eprintln!("[truce-rack-standalone] transport: toggle (placeholder)");
return EventStatus::Captured;
}
if kb.state == KeyState::Down
&& let Some(shift) = keyboard::code_to_octave_shift(kb.code)
{
self.octave_offset = (self.octave_offset + shift).clamp(-3, 3);
return EventStatus::Captured;
}
if let Some(note) = keyboard::code_to_midi_note(kb.code, self.octave_offset) {
let body = match kb.state {
KeyState::Down => EventBody::Midi(MidiData::NoteOn {
channel: 0,
note,
velocity: 102,
}),
KeyState::Up => EventBody::Midi(MidiData::NoteOff {
channel: 0,
note,
velocity: 0,
}),
};
midi_queue::enqueue(body);
return EventStatus::Captured;
}
EventStatus::Ignored
}
fn save_state(&self) {
let path = state_path(&self.plugin_name);
let Ok(guard) = self.plugin.lock() else {
eprintln!("[truce-rack-standalone] could not lock plugin to save state");
return;
};
match guard.save_state() {
Ok(blob) => match std::fs::write(&path, &blob) {
Ok(()) => eprintln!(
"[truce-rack-standalone] state saved: {} ({} bytes)",
path.display(),
blob.len()
),
Err(e) => eprintln!("[truce-rack-standalone] write {}: {e}", path.display()),
},
Err(e) => eprintln!("[truce-rack-standalone] save_state failed: {e}"),
}
}
fn load_state(&self) {
let path = state_path(&self.plugin_name);
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(e) => {
eprintln!("[truce-rack-standalone] read {}: {e}", path.display());
return;
}
};
let Ok(mut guard) = self.plugin.lock() else {
eprintln!("[truce-rack-standalone] could not lock plugin to load state");
return;
};
match guard.load_state(&bytes) {
Ok(()) => eprintln!(
"[truce-rack-standalone] state loaded: {} ({} bytes)",
path.display(),
bytes.len()
),
Err(e) => eprintln!("[truce-rack-standalone] load_state failed: {e}"),
}
}
}
fn state_path(plugin_name: &str) -> std::path::PathBuf {
let slug: String = plugin_name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c.to_ascii_lowercase()
} else {
'-'
}
})
.collect();
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
home.join(format!("{slug}.state"))
}
fn is_mod_pressed(mods: Modifiers) -> bool {
if cfg!(target_os = "macos") {
mods.contains(Modifiers::META)
} else {
mods.contains(Modifiers::CONTROL)
}
}
pub(crate) struct AudioController {
plugin: SharedPlugin,
device_name: Option<String>,
channels: usize,
stream: Option<cpal::Stream>,
}
impl AudioController {
pub(crate) fn start(plugin: SharedPlugin) -> Result<Self> {
let mut controller = Self {
plugin,
device_name: crate::device::config().output_device,
channels: 0,
stream: None,
};
controller.rebuild()?;
Ok(controller)
}
pub(crate) fn channels(&self) -> usize {
self.channels
}
pub(crate) fn device_name(&self) -> Option<&str> {
self.device_name.as_deref()
}
pub(crate) fn set_output_device(&mut self, name: Option<String>) {
self.device_name = name;
if let Err(e) = self.rebuild() {
eprintln!("[truce-rack-standalone] output device switch failed: {e}");
}
}
fn rebuild(&mut self) -> Result<()> {
use cpal::traits::StreamTrait;
self.stream = None;
let (device, supported) = crate::device::open_output(self.device_name.as_deref())?;
let stream_config = crate::device::resolve_stream_config(&device, &supported);
let sample_rate = f64::from(stream_config.sample_rate.0);
let channels = usize::from(stream_config.channels.max(1));
self.channels = channels;
{
let mut guard = self.plugin.lock().expect("plugin mutex");
guard.deactivate();
guard.activate(BusLayout::stereo(), sample_rate, MAX_BLOCK)?;
}
let stream =
build_shared_stream(&device, &stream_config, Arc::clone(&self.plugin), channels)?;
stream
.play()
.map_err(|e| Error::Other(format!("stream.play: {e}")))?;
self.stream = Some(stream);
Ok(())
}
}
fn build_shared_stream(
device: &cpal::Device,
stream_config: &cpal::StreamConfig,
plugin: SharedPlugin,
channels: usize,
) -> Result<cpal::Stream> {
use cpal::traits::DeviceTrait;
let sample_rate = f64::from(stream_config.sample_rate.0);
let bus_in = vec![BusRange::new(0, channels)];
let bus_out = vec![BusRange::new(0, channels)];
let mut input_buf = vec![vec![0.0f32; MAX_BLOCK]; channels];
let mut output_buf = vec![vec![0.0f32; MAX_BLOCK]; channels];
let mut clock = crate::transport::TransportClock::new();
device
.build_output_stream(
stream_config,
move |out: &mut [f32], _: &cpal::OutputCallbackInfo| {
let frames = out.len() / channels.max(1);
for ch in &mut input_buf {
if ch.len() < frames {
ch.resize(frames, 0.0);
}
for v in &mut ch[..frames] {
*v = 0.0;
}
}
for ch in &mut output_buf {
if ch.len() < frames {
ch.resize(frames, 0.0);
}
for v in &mut ch[..frames] {
*v = 0.0;
}
}
let mut events = EventList::default();
midi_queue::drain_into(&mut events);
if let Ok(mut guard) = plugin.try_lock() {
let inputs: Vec<&[f32]> = input_buf.iter().map(|c| &c[..frames]).collect();
let mut outputs: Vec<&mut [f32]> =
output_buf.iter_mut().map(|c| &mut c[..frames]).collect();
let mut buffer =
AudioBuffer::new(&inputs, &mut outputs, frames, &bus_in, &bus_out);
let mut out_events = EventList::default();
let mut ctx = ProcessContext {
sample_rate,
max_block_size: MAX_BLOCK,
transport: clock.next_block(frames, sample_rate),
output_events: &mut out_events,
};
let _ = guard.process(&mut buffer, &events, &mut ctx);
}
crate::device::live_route().write(out, &output_buf, channels, frames);
},
move |err| eprintln!("[truce-rack-standalone] stream error: {err}"),
None,
)
.map_err(|e| Error::Other(format!("build_output_stream: {e}")))
}