From 8050b9c28e4b7592efee914fafcfb170ee7d6dd8 Mon Sep 17 00:00:00 2001 From: Petr Gladkikh Date: Tue, 30 Jan 2024 19:43:28 +0100 Subject: [PATCH] Use system's sequencer for audio output Removing VST2 dependency using system's MIDI output port. Now in order to hear the sound user would need to use some external synth to listen to the MIDI output port. Other optionss considered Should eventually migrate away from VST2. The bindings are unsupported anymore, and there are SIGSEGVs that I have not managed to resolve so far. Also, there are licensing issues with the VST2 API itself. VST3 is GPL - I would like to keep the project more accessible to various uses. LV2 seem like a decent choice. Here seem to be an LV2 host implementation in Rust https://github.com/wmedrano/livi-rs. Can also implement one from scratch, or use JACK API and register `emmate` as a MIDI sequencer. Pipewire seems to SUPPORT JACK API as well (see `pw-jack`). --- Cargo.lock | 15 ---- Cargo.toml | 3 - README.md | 6 +- src/audio_setup.rs | 75 ++++-------------- src/engine.rs | 51 ++++-------- src/main.rs | 12 ++- src/midi_vst.rs | 188 --------------------------------------------- 7 files changed, 39 insertions(+), 311 deletions(-) delete mode 100644 src/midi_vst.rs diff --git a/Cargo.lock b/Cargo.lock index 05b9d50..44e3522 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1180,7 +1180,6 @@ dependencies = [ "serde", "stderrlog", "toml", - "vst", "wav", "wmidi", ] @@ -3200,20 +3199,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "vst" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111c58168c9208a20c3f5cedeca0721aee2cac0e92c5ec2a16dc4beb5886a40a" -dependencies = [ - "bitflags 1.3.2", - "libc", - "libloading 0.7.4", - "log", - "num-traits", - "num_enum 0.5.11", -] - [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 708a281..95c9d74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,6 @@ ordered-float = "4.1.0" eframe = { version = "0.23.0" } egui_extras = { version = "0.23.0" } -# https://github.com/RustAudio/vst-rs -vst = "0.3.0" - # https://github.com/negamartin/midly midly = { version = "0.5.3", features = ["alloc"] } diff --git a/README.md b/README.md index 2f102b5..a6ded2b 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ I personally use Pianoteq, but that is a commercial product. easier to read and enable to have a generated cheatsheet/help UI. - [ ] Flight recorder (always record what is coming from the MIDI controller into a separate file or track). - [ ] (improvement) Ensure changes are visible even when zoomed out (the events may be too small to be visible as is). +- [x] Remove VST2 dependency. Using MIDI sequencer port instead. - [x] Highlight undo/redo changes (implemented for notes, need also to emphasise CC values). - [x] Visual hint for out-of-view selected notes. Scroll to the earliest of the selected notes on an action, if none of them are currently visible. @@ -88,11 +89,6 @@ I personally use Pianoteq, but that is a commercial product. Also, some big scary problems -* Should eventually migrate away from VST2. The bindings are unsupported anymore, and there are SIGSEGVs that I have not - managed to resolve so far. Also, there are licensing issues with the VST2 API itself. VST3 is GPL - I would like to - keep the project more accessible to various uses. LV2 seem like a decent choice. Here seem to be an LV2 host - implementation in Rust https://github.com/wmedrano/livi-rs. Can also implement one from scratch, or use JACK API and - register `emmate` as a MIDI sequencer. Pipewire seems to SUPPORT JACK API as well (see `pw-jack`). * May need to use midi events directly (instead of intermediate internal representation). E.g. `track::from_midi_events` may not be necessary. In particular tail shifts will become simpler. This will require * To handle ignored/unused events along with notes and sustain. diff --git a/src/audio_setup.rs b/src/audio_setup.rs index 6f5fa6a..9eec4db 100644 --- a/src/audio_setup.rs +++ b/src/audio_setup.rs @@ -1,52 +1,17 @@ use std::sync::mpsc::Sender; use std::sync::{mpsc, Arc, Mutex}; -use cpal::traits::{DeviceTrait, HostTrait}; -use cpal::SampleFormat::F32; -use cpal::{BufferSize, StreamConfig}; -use midir::{MidiInput, MidiInputConnection}; +use midir::{MidiInput, MidiInputConnection, MidiOutputConnection}; use midly::live::LiveEvent; -use rodio::OutputStream; -use vst::event::{Event, MidiEvent}; use crate::engine::{Engine, EngineCommand}; -use crate::midi_vst::{OutputSource, Vst}; pub fn setup_audio_engine( - vst_plugin_path: &String, - vst_preset_id: &i32, -) -> (OutputStream, Arc>, Sender>) { - let buffer_size = 256; - let audio_host = cpal::default_host(); - let out_device = audio_host.default_output_device().unwrap(); - println!("INFO Default output device: {:?}", out_device.name()); - let out_conf = out_device.default_output_config().unwrap(); - println!("INFO Default output config: {:?}", out_conf); - assert_eq!(out_conf.sample_format(), F32); // Required by VST - let out_stream_conf = StreamConfig { - channels: out_conf.channels(), - sample_rate: out_conf.sample_rate(), - buffer_size: BufferSize::Fixed(buffer_size), - }; - println!("INFO Output config: {:?}", out_stream_conf); - let (stream, stream_handle) = rodio::OutputStream::try_from_config( - &out_device, - &out_stream_conf, - &out_conf.sample_format(), - ) - .unwrap(); + midi_output: MidiOutputConnection, +) -> (Arc>, Sender>) { let (command_sender, command_receiver) = mpsc::channel(); - let vst = Vst::init( - vst_plugin_path, - &out_stream_conf.sample_rate, - &buffer_size, - *vst_preset_id, - ); - stream_handle - .play_raw(OutputSource::new(&vst, &buffer_size)) - .unwrap(); - let engine = Engine::new(vst, command_receiver); - (stream, engine.start(), command_sender) + let engine = Engine::new(midi_output, command_receiver); + (engine.start(), command_sender) } // TODO (refactoring) Convert this into event source? Note: on pause engine stops all sources, @@ -82,33 +47,19 @@ pub fn midi_keyboard_input( &port, "midi-input", move |t, ev, _data| { - { - let le = LiveEvent::parse(ev) - .expect("Unparseable input controller event.") - .to_static(); - println!("Input MIDI event: {} {:?}", t, le); - } + let le = LiveEvent::parse(ev) + .expect("Unparseable input controller event.") + .to_static(); + println!("Input MIDI event: {} {:?}", t, le); if ev[0] == 254 { return; // Ignore keep-alives. } - let mut ev_buf = [0u8; 3]; - for (i, x) in ev.iter().enumerate() { - ev_buf[i] = *x; - } - let event = Event::Midi(MidiEvent { - data: ev_buf, - delta_frames: 0, - live: true, - note_length: None, - note_offset: None, - detune: 0, - note_off_velocity: 0, - }); - // TODO (bug) Sustain events seem to be ignored by the VST plugin. - engine.lock().unwrap().process(event); + // TODO (bug) Effect of sustain events does not last for some reason. + // Triggering noise is there but subsequent notes do not feel the effect. + engine.lock().unwrap().process(le); }, (), ) - .unwrap(), + .expect("MIDI input port"), ) } diff --git a/src/engine.rs b/src/engine.rs index 7141bb8..db49f52 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,3 +1,4 @@ +use midir::MidiOutputConnection; use std::cmp::Ordering; use std::collections::BinaryHeap; use std::sync::{mpsc, Arc, Mutex}; @@ -6,11 +7,8 @@ use std::time::{Duration, Instant}; use midly::live::LiveEvent; use midly::MidiMessage; -use vst::event::Event; -use vst::plugin::Plugin; use crate::common::Time; -use crate::midi_vst::Vst; use crate::track::MIDI_CC_SUSTAIN_ID; /** Event that is produced by engine. */ @@ -58,7 +56,7 @@ type EventSourceHandle = dyn EventSource + Send; pub type EngineCommand = dyn FnOnce(&mut Engine) + Send; pub struct Engine { - vst: Vst, + midi_output: MidiOutputConnection, sources: Vec>, running_at: Time, reset_at: Instant, @@ -68,9 +66,12 @@ pub struct Engine { } impl Engine { - pub fn new(vst: Vst, commands: mpsc::Receiver>) -> Engine { + pub fn new( + midi_output: MidiOutputConnection, + commands: mpsc::Receiver>, + ) -> Engine { Engine { - vst, + midi_output, sources: Vec::new(), running_at: 0, reset_at: Instant::now(), @@ -84,6 +85,7 @@ impl Engine { let engine = Arc::new(Mutex::new(self)); let engine2 = engine.clone(); thread::spawn(move || { + // Use async instead? engine2.lock().unwrap().seek(0); let mut queue: BinaryHeap = BinaryHeap::new(); loop { @@ -108,7 +110,7 @@ impl Engine { } = ev.event { if let MidiMessage::NoteOff { .. } = message { - locked.process(smf_to_vst(ev.event)); + locked.process(ev.event); } } } @@ -130,14 +132,14 @@ impl Engine { batch.push(queue.pop().unwrap().event); } for ev in batch { - locked.process(smf_to_vst(ev)); + locked.process(ev); } } }); engine } - fn close_damper(&self) { + fn close_damper(&mut self) { let ev = LiveEvent::Midi { channel: 0.into(), message: MidiMessage::Controller { @@ -145,7 +147,7 @@ impl Engine { value: 0.into(), }, }; - self.process(smf_to_vst(ev)); + self.process(ev); } fn update_track_time(&mut self) { @@ -174,9 +176,6 @@ impl Engine { /// Stop all sounds. pub fn reset(&mut self) { self.paused = true; - // TODO These SIGSEV. Use other API instead (LV2)? See also https://github.com/RustAudio/vst-rs/issues/193 - // self.vst.host.lock().unwrap().idle(); - // self.vst.plugin.lock().unwrap().suspend(); } pub fn update_realtime(&mut self) { @@ -188,31 +187,13 @@ impl Engine { } /// Process the event immediately. - pub fn process(&self, event: Event) { - let events_list = [event]; - let mut events_buffer = vst::buffer::SendEventBuffer::new(events_list.len()); - events_buffer.store_events(events_list); - let mut plugin = self.vst.plugin.lock().unwrap(); - plugin.process_events(events_buffer.events()); + pub fn process(&mut self, event: LiveEvent) { + let mut midi_buf = vec![]; + event.write(&mut midi_buf).unwrap(); + self.midi_output.send(&midi_buf).unwrap(); } pub fn set_status_receiver(&mut self, receiver: Option>) { self.status_receiver = receiver; } } - -fn smf_to_vst(event: LiveEvent<'static>) -> Event<'static> { - let mut ev_buf = Vec::new(); - event - .write(&mut ev_buf) - .expect("The live event should be writable."); - Event::Midi(vst::event::MidiEvent { - data: ev_buf.try_into().unwrap(), - delta_frames: 0, - live: true, - note_length: None, - note_offset: None, - detune: 0, - note_off_velocity: 0, - }) -} diff --git a/src/main.rs b/src/main.rs index d051cf5..f3acea7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,6 @@ use eframe::{egui, Theme}; +use midir::os::unix::VirtualOutput; +use midir::MidiOutput; use crate::app::EmApp; use crate::config::Config; @@ -13,7 +15,6 @@ mod common; mod config; mod engine; mod midi; -mod midi_vst; mod project; mod stave; mod track; @@ -43,6 +44,7 @@ pub fn main() { .default_value("yellow.mid"), ) .get_matches(); + // TODO (implementation) Do not need the config after VST is removed, keeping it here just in case. let config = Config::load(arg_matches.get_one::("config-file")); let midi_file_path = arg_matches @@ -51,9 +53,13 @@ pub fn main() { println!("MIDI file name {:?}", midi_file_path); let project = Project::open_file(midi_file_path); + let midi_output = MidiOutput::new("emmate") + .expect("MIDI sequencer client") + .create_virtual("emmate") + .expect("MIDI sequencer out"); + // Stream and engine references keep them open. - let (_stream, mut engine, engine_command_sender) = - audio_setup::setup_audio_engine(&config.vst_plugin_path, &config.vst_preset_id); + let (mut engine, engine_command_sender) = audio_setup::setup_audio_engine(midi_output); if false { // Want the section to still be compilable. // Play MIDI from an SMD file. diff --git a/src/midi_vst.rs b/src/midi_vst.rs deleted file mode 100644 index bb0d4c6..0000000 --- a/src/midi_vst.rs +++ /dev/null @@ -1,188 +0,0 @@ -use std::path::Path; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use cpal::{FrameCount, SampleRate}; -use rodio::Source; -use vst::api::Supported; -use vst::host::{Host, HostBuffer, PluginInstance, PluginLoader}; -use vst::plugin::{CanDo, Plugin}; - -pub struct VstHost; - -impl Host for VstHost {} - -pub struct Vst { - pub host: Arc>, - pub plugin: Arc>, - pub sample_rate: f32, -} - -impl Vst { - pub fn init( - plugin_path: &String, - sample_rate: &SampleRate, - buffer_size: &FrameCount, - preset_id: i32, - ) -> Vst { - let sample_rate_f = sample_rate.0 as f32; - let path = Path::new(plugin_path); - println!("Loading {}", path.to_str().unwrap()); - - let host = Arc::new(Mutex::new(VstHost)); - let mut loader = PluginLoader::load(path, Arc::clone(&host)) - .unwrap_or_else(|e| panic!("Failed to load plugin: {}", e)); - let plugin_holder = Arc::new(Mutex::new(loader.instance().unwrap())); - { - let mut plugin = plugin_holder.lock().unwrap(); - plugin.suspend(); - - let info = plugin.get_info(); - // Diagnostics: get the plugin information - println!( - "Loaded '{}':\n\t\ - Vendor: {}\n\t\ - Presets: {}\n\t\ - Parameters count: {}\n\t\ - VST ID: {}\n\t\ - Version: {}\n\t\ - Initial delay: {} samples\n\t\ - Inputs {}\n\t\ - Outputs {}", - info.name, - info.vendor, - info.presets, - info.parameters, - info.unique_id, - info.version, - info.initial_delay, - info.inputs, - info.outputs - ); - let params = plugin.get_parameter_object(); - params.change_preset(preset_id); - println!( - "Current preset #{}: {}", - params.get_preset_num(), - params.get_preset_name(params.get_preset_num()) - ); - - plugin.init(); - println!("Initialized VST instance."); - println!( - "Can receive MIDI events {}", - plugin.can_do(CanDo::ReceiveMidiEvent) == Supported::Yes - ); - - plugin.suspend(); // Just to be explicit, the plugin is created in suspended state. - plugin.set_sample_rate(sample_rate_f.to_owned()); - plugin.set_block_size(*buffer_size as i64); - plugin.resume(); - plugin.start_process(); - } - Vst { - host, - plugin: plugin_holder, - sample_rate: sample_rate_f, - } - } -} - -pub struct OutputSource { - sample_idx: usize, - channel_idx: usize, - sample_rate: u32, - outputs: Vec>, - plugin: Arc>, - empty: bool, -} - -impl OutputSource { - pub fn new(vst: &Vst, buf_size: &FrameCount) -> OutputSource { - assert!(*buf_size > 0); - let outputs; - { - let plugin_holder = vst.plugin.clone(); - let plugin = plugin_holder.try_lock().unwrap(); - let info = plugin.get_info(); - outputs = vec![vec![0.0; *buf_size as usize]; info.outputs as usize]; - } - OutputSource { - sample_rate: vst.sample_rate.to_owned() as u32, - sample_idx: 0, - channel_idx: 0, - outputs, - plugin: vst.plugin.clone(), - empty: true, - } - } - - fn fill_buffer(&mut self) { - let mut plugin = self.plugin.lock().unwrap(); - let info = plugin.get_info(); - let output_count = info.outputs as usize; - let input_count = info.inputs as usize; - let inputs = vec![vec![0.0; 0]; input_count]; - let mut host_buffer: HostBuffer = HostBuffer::new(input_count, output_count); - let mut buffer = host_buffer.bind(&inputs, &mut self.outputs); - - plugin.process(&mut buffer); - self.sample_idx = 0; - self.channel_idx = 0; - } -} - -// Produces audio output PCM samples from VST. -impl Iterator for OutputSource { - type Item = f32; - - #[inline] - fn next(&mut self) -> Option { - if self.empty { - self.fill_buffer(); - self.empty = false; - } - let mut_outputs = &mut self.outputs; - let mut output = mut_outputs.get_mut(self.channel_idx.to_owned()); - if output == None { - /* Channels are interleaved (see https://github.com/RustAudio/rodio/blob/master/src/source/channel_volume.rs) - So for 2 channels we have to put 2 samples in sequence */ - self.channel_idx = 0; - self.sample_idx += 1; - output = mut_outputs.get_mut(self.channel_idx.to_owned()); - } - let sample = output.unwrap().get(self.sample_idx.to_owned()); - match sample { - Some(&x) => { - self.channel_idx += 1; - Some(x) - } - None => { - self.empty = true; - self.next() - } - } - } -} - -impl Source for OutputSource { - #[inline] - fn current_frame_len(&self) -> Option { - None - } - - #[inline] - fn channels(&self) -> u16 { - self.outputs.len() as u16 - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.sample_rate.to_owned() - } - - #[inline] - fn total_duration(&self) -> Option { - None - } -}