From 4033e0e98282d9cbf672b2b0fa9370d61c5527aa Mon Sep 17 00:00:00 2001 From: Benjamin Klum Date: Tue, 29 Oct 2024 10:28:42 +0100 Subject: [PATCH] #715 Make realearn_timestamp sample-accurate for MIDI events - calculate timestamp based on audio callbacks - respect the frame offset for MIDI events --- Cargo.lock | 1 + Cargo.toml | 1 + main/Cargo.toml | 2 + main/lib/helgoboss-learn | 2 +- main/src/domain/accelerator.rs | 2 +- main/src/domain/audio_hook.rs | 48 +++++++++++----- main/src/domain/control_event.rs | 36 +++++++++--- main/src/domain/control_surface.rs | 2 +- main/src/domain/global_audio_state.rs | 55 +++++++++++++++++++ main/src/domain/main_processor.rs | 21 ++++--- main/src/domain/midi_types.rs | 5 ++ main/src/domain/mod.rs | 3 + main/src/domain/parameter_manager.rs | 14 +++-- .../infrastructure/plugin/backbone_shell.rs | 20 +++---- .../infrastructure/plugin/helgobox_plugin.rs | 17 +++++- playtime-clip-engine | 2 +- 16 files changed, 178 insertions(+), 53 deletions(-) create mode 100644 main/src/domain/global_audio_state.rs diff --git a/Cargo.lock b/Cargo.lock index 535749145..2875c64d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3055,6 +3055,7 @@ dependencies = [ "serde_yaml", "slug", "smallvec", + "static_assertions", "strum", "swell-ui", "sys-info", diff --git a/Cargo.toml b/Cargo.toml index 7f509b39a..9b8546ccd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,6 +120,7 @@ pathdiff = "0.2.1" open = "5.0.1" url = "2.5.2" atomic = "0.6.0" +static_assertions = "1.1.0" [profile.release] # This is important for having line numbers in bug reports. diff --git a/main/Cargo.toml b/main/Cargo.toml index 99bbc584a..6a209a1b0 100644 --- a/main/Cargo.toml +++ b/main/Cargo.toml @@ -207,6 +207,8 @@ palette.workspace = true serde_plain.workspace = true # For sharing variables between main thread and real-time threads atomic.workspace = true +# For making sure that sharing global audio state uses atomics +static_assertions.workspace = true [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] # For speech source diff --git a/main/lib/helgoboss-learn b/main/lib/helgoboss-learn index 2d170d762..9759b17c1 160000 --- a/main/lib/helgoboss-learn +++ b/main/lib/helgoboss-learn @@ -1 +1 @@ -Subproject commit 2d170d762e75c796c713858409df5119da3517e8 +Subproject commit 9759b17c1f9585d55d21b2a4328a86625cf22121 diff --git a/main/src/domain/accelerator.rs b/main/src/domain/accelerator.rs index 82a35233c..74db7e814 100644 --- a/main/src/domain/accelerator.rs +++ b/main/src/domain/accelerator.rs @@ -44,7 +44,7 @@ where /// /// If yes, we should completely "eat" the message and don't do anything else with it. fn process_control(&mut self, msg: KeyMessage) -> bool { - let evt = ControlEvent::new(msg, ControlEventTimestamp::now()); + let evt = ControlEvent::new(msg, ControlEventTimestamp::from_main_thread()); let mut filter_out_event = false; let mut notified_backbone = false; for proc in &mut *self.main_processors.borrow_mut() { diff --git a/main/src/domain/audio_hook.rs b/main/src/domain/audio_hook.rs index acac0df6d..1a3ab31ac 100644 --- a/main/src/domain/audio_hook.rs +++ b/main/src/domain/audio_hook.rs @@ -2,7 +2,7 @@ use crate::domain::{ classify_midi_message, AudioBlockProps, ControlEvent, ControlEventTimestamp, DisplayAsPrettyHex, IncomingMidiMessage, InstanceId, MidiControlInput, MidiEvent, MidiMessageClassification, MidiScanResult, MidiScanner, MidiTransformationContainer, - RealTimeProcessor, SharedRealTimeInstance, UnitId, + RealTimeProcessor, SharedRealTimeInstance, UnitId, GLOBAL_AUDIO_STATE, }; use base::byte_pattern::{BytePattern, PatternByte}; use base::metrics_util::{measure_time, record_duration}; @@ -10,13 +10,15 @@ use base::non_blocking_lock; use helgoboss_learn::{AbstractTimestamp, MidiSourceValue, RawMidiEvent, RawMidiEvents}; use helgoboss_midi::{DataEntryByteOrder, RawShortMessage, ShortMessage, ShortMessageType}; use helgobox_allocator::*; +use reaper_common_types::DurationInSeconds; use reaper_high::{MidiInputDevice, MidiOutputDevice, Reaper}; use reaper_medium::{ MidiInputDeviceId, MidiOutputDeviceId, OnAudioBuffer, OnAudioBufferArgs, SendMidiTime, + MIDI_INPUT_FRAME_RATE, }; use smallvec::SmallVec; use std::fmt::{Display, Formatter}; -use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; use std::time::{Duration, Instant}; use tinyvec::ArrayVec; @@ -134,7 +136,6 @@ pub struct RealearnAudioHook { feedback_task_receiver: crossbeam_channel::Receiver, time_of_last_run: Option, initialized: bool, - counter: Arc, midi_transformation_container: MidiTransformationContainer, #[cfg(feature = "playtime")] clip_engine_audio_hook: playtime_clip_engine::rt::audio_hook::PlaytimeAudioHook, @@ -155,7 +156,6 @@ impl RealearnAudioHook { pub fn new( normal_task_receiver: crossbeam_channel::Receiver, feedback_task_receiver: crossbeam_channel::Receiver, - counter: Arc, ) -> RealearnAudioHook { Self { state: AudioHookState::Normal, @@ -166,7 +166,6 @@ impl RealearnAudioHook { feedback_task_receiver, time_of_last_run: None, initialized: false, - counter, midi_transformation_container: MidiTransformationContainer::new(), #[cfg(feature = "playtime")] clip_engine_audio_hook: playtime_clip_engine::rt::audio_hook::PlaytimeAudioHook::new(), @@ -204,8 +203,9 @@ impl RealearnAudioHook { let current_time = Instant::now(); let time_of_last_run = self.time_of_last_run.replace(current_time); // Increment counter - self.counter.fetch_add(1, Ordering::Relaxed); let block_props = AudioBlockProps::from_on_audio_buffer_args(&args); + let block_count = GLOBAL_AUDIO_STATE.advance(block_props); + let sample_count = block_count * args.len as u64; // Call ReaLearn real-time processors (= process MIDI messages coming in from hardware devices). // We do this here already, *before* pre-polling recording and advancing Playtime's tempo buffer (done in `on_pre_poll_1`)! // Reason: When recording a new clip with tempo detection (= recording in silence mode), it's ideal @@ -238,7 +238,7 @@ impl RealearnAudioHook { false } }; - self.call_real_time_processors(block_props, might_be_rebirth); + self.call_real_time_processors(block_props, sample_count, might_be_rebirth); // Process ReaLearn feedback commands self.process_feedback_commands(); // Process incoming commands, including Playtime commands @@ -317,14 +317,18 @@ impl RealearnAudioHook { } } - fn call_real_time_processors(&mut self, block_props: AudioBlockProps, might_be_rebirth: bool) { + fn call_real_time_processors( + &mut self, + block_props: AudioBlockProps, + sample_count: u64, + might_be_rebirth: bool, + ) { match &mut self.state { AudioHookState::Normal => { - let timestamp = ControlEventTimestamp::now(); self.call_real_time_processors_in_normal_state( block_props, might_be_rebirth, - timestamp, + sample_count, ); } AudioHookState::LearningSource { @@ -358,7 +362,7 @@ impl RealearnAudioHook { &mut self, block_props: AudioBlockProps, might_be_rebirth: bool, - timestamp: ControlEventTimestamp, + sample_count: u64, ) { // 1a. Drive real-time processors and determine used MIDI devices "on the go". // @@ -371,6 +375,11 @@ impl RealearnAudioHook { // let mut midi_dev_id_is_used = [false; MidiInputDeviceId::MAX_DEVICE_COUNT as usize]; let mut midi_devs_used_at_all = false; + let start_of_block_timestamp = ControlEventTimestamp::from_rt( + sample_count, + block_props.frame_rate, + DurationInSeconds::ZERO, + ); for (_, p) in self.real_time_processors.iter() { // Since 1.12.0, we "drive" each plug-in instance's real-time processor // primarily by the global audio hook. See https://github.com/helgoboss/helgobox/issues/84 why this is @@ -379,7 +388,7 @@ impl RealearnAudioHook { // stop doing so synchronously if the plug-in is // gone. let mut guard = p.lock_recover(); - guard.run_from_audio_hook_all(block_props, might_be_rebirth, timestamp); + guard.run_from_audio_hook_all(block_props, might_be_rebirth, start_of_block_timestamp); if guard.control_is_globally_enabled() { if let MidiControlInput::Device(dev_id) = guard.midi_control_input() { midi_dev_id_is_used[dev_id.get() as usize] = true; @@ -390,7 +399,11 @@ impl RealearnAudioHook { // 1b. Forward MIDI events from MIDI devices to ReaLearn instances and filter // them globally if desired by the instance. if midi_devs_used_at_all { - self.distribute_midi_events_to_processors(block_props, &midi_dev_id_is_used, timestamp); + self.distribute_midi_events_to_processors( + block_props, + &midi_dev_id_is_used, + sample_count, + ); } } @@ -398,7 +411,7 @@ impl RealearnAudioHook { &mut self, block_props: AudioBlockProps, midi_dev_id_is_used: &[bool; MidiInputDeviceId::MAX_DEVICE_COUNT as usize], - timestamp: ControlEventTimestamp, + sample_count: u64, ) { self.midi_transformation_container .prepare(block_props.frame_rate); @@ -420,6 +433,13 @@ impl RealearnAudioHook { Err(_) => continue, Ok(e) => e, }; + let frame_offset_in_secs = + res.midi_event.frame_offset() as f64 / MIDI_INPUT_FRAME_RATE.get(); + let timestamp = ControlEventTimestamp::from_rt( + sample_count, + block_props.frame_rate, + DurationInSeconds::new_panic(frame_offset_in_secs), + ); let our_event = ControlEvent::new(our_event, timestamp); let mut filter_out_event = false; for (_, p) in self.real_time_processors.iter() { diff --git a/main/src/domain/control_event.rs b/main/src/domain/control_event.rs index f5a2cbc2a..eb586cca3 100644 --- a/main/src/domain/control_event.rs +++ b/main/src/domain/control_event.rs @@ -1,4 +1,6 @@ +use crate::domain::GLOBAL_AUDIO_STATE; use helgoboss_learn::AbstractTimestamp; +use reaper_common_types::{DurationInSeconds, Hz}; use std::fmt::{Display, Formatter}; use std::ops::Sub; use std::sync::LazyLock; @@ -11,16 +13,36 @@ pub type ControlEvent

= helgoboss_learn::ControlEvent Self { - Self(Instant::now()) +impl ControlEventTimestamp { + pub fn from_main_thread() -> Self { + let block_count = GLOBAL_AUDIO_STATE.load_block_count(); + let block_size = GLOBAL_AUDIO_STATE.load_block_size(); + let sample_count = block_count * block_size as u64; + Self::from_rt( + sample_count, + GLOBAL_AUDIO_STATE.load_sample_rate(), + DurationInSeconds::ZERO, + ) } +} +impl AbstractTimestamp for ControlEventTimestamp { fn duration(&self) -> Duration { - static INSTANT: LazyLock = LazyLock::new(|| Instant::now()); - self.0.saturating_duration_since(*INSTANT) + self.0 + } +} + +impl ControlEventTimestamp { + pub fn from_rt( + sample_count: u64, + sample_rate: Hz, + intra_block_offset: DurationInSeconds, + ) -> Self { + let start_secs = sample_count as f64 / sample_rate.get(); + let final_secs = start_secs + intra_block_offset.get(); + Self(Duration::from_secs_f64(final_secs)) } } @@ -28,7 +50,7 @@ impl Sub for ControlEventTimestamp { type Output = Duration; fn sub(self, rhs: Self) -> Self::Output { - self.0 - rhs.0 + self.0.saturating_sub(rhs.0) } } diff --git a/main/src/domain/control_surface.rs b/main/src/domain/control_surface.rs index 0d2b2a1fd..e14b787fb 100644 --- a/main/src/domain/control_surface.rs +++ b/main/src/domain/control_surface.rs @@ -253,7 +253,7 @@ impl RealearnControlSurfaceMiddleware { } fn run_internal(&mut self) { - let timestamp = ControlEventTimestamp::now(); + let timestamp = ControlEventTimestamp::from_main_thread(); #[cfg(debug_assertions)] { // TODO-high-playtime-refactoring This is propagated using main processors but it's a global event. We diff --git a/main/src/domain/global_audio_state.rs b/main/src/domain/global_audio_state.rs new file mode 100644 index 000000000..8fe9cd14b --- /dev/null +++ b/main/src/domain/global_audio_state.rs @@ -0,0 +1,55 @@ +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; + +use atomic::Atomic; +use reaper_medium::Hz; +use static_assertions::const_assert; + +use crate::domain::AudioBlockProps; + +pub static GLOBAL_AUDIO_STATE: GlobalAudioState = GlobalAudioState::new(); + +#[derive(Debug)] +pub struct GlobalAudioState { + block_count: AtomicU64, + block_size: AtomicU32, + sample_rate: Atomic, +} + +impl Default for GlobalAudioState { + fn default() -> Self { + Self::new() + } +} + +impl GlobalAudioState { + pub const fn new() -> Self { + const_assert!(Atomic::::is_lock_free()); + Self { + block_count: AtomicU64::new(0), + block_size: AtomicU32::new(0), + sample_rate: Atomic::new(1.0), + } + } + + /// Returns previous block count + pub fn advance(&self, block_props: AudioBlockProps) -> u64 { + let prev_block_count = self.block_count.fetch_add(1, Ordering::Relaxed); + self.block_size + .store(block_props.block_length as u32, Ordering::Relaxed); + self.sample_rate + .store(block_props.frame_rate.get(), Ordering::Relaxed); + prev_block_count + } + + pub fn load_block_count(&self) -> u64 { + self.block_count.load(Ordering::Relaxed) + } + + pub fn load_block_size(&self) -> u32 { + self.block_size.load(Ordering::Relaxed) + } + + pub fn load_sample_rate(&self) -> Hz { + Hz::new_panic(self.sample_rate.load(Ordering::Relaxed)) + } +} diff --git a/main/src/domain/main_processor.rs b/main/src/domain/main_processor.rs index 4973235a6..445ae4d27 100644 --- a/main/src/domain/main_processor.rs +++ b/main/src/domain/main_processor.rs @@ -979,9 +979,11 @@ impl MainProcessor { UpdateAllParams(params) => { self.update_all_params(params); } - UpdateSingleParamValue { index, value } => { - self.update_single_param_value(index, value) - } + UpdateSingleParamValue { + index, + value, + timestamp, + } => self.update_single_param_value(index, value, timestamp), } count += 1; if count == PARAMETER_TASK_BULK_SIZE { @@ -992,7 +994,12 @@ impl MainProcessor { // https://github.com/rust-lang/rust-clippy/issues/6066 #[allow(clippy::needless_collect)] - fn update_single_param_value(&mut self, index: PluginParamIndex, value: RawParamValue) { + fn update_single_param_value( + &mut self, + index: PluginParamIndex, + value: RawParamValue, + timestamp: ControlEventTimestamp, + ) { debug!("Updating parameter {} to {}...", index, value); // Work around REAPER's inability to notify about parameter changes in // monitoring FX by simulating the notification ourselves. @@ -1052,10 +1059,7 @@ impl MainProcessor { if self.basics.settings.real_input_logging_enabled { self.log_incoming_message(&control_msg); } - let control_event = ControlEvent::new( - MainSourceMessage::Reaper(&control_msg), - ControlEventTimestamp::now(), - ); + let control_event = ControlEvent::new(MainSourceMessage::Reaper(&control_msg), timestamp); self.process_incoming_message_internal(control_event); } @@ -2982,6 +2986,7 @@ pub enum ParameterMainTask { UpdateSingleParamValue { index: PluginParamIndex, value: RawParamValue, + timestamp: ControlEventTimestamp, }, UpdateAllParams(PluginParams), } diff --git a/main/src/domain/midi_types.rs b/main/src/domain/midi_types.rs index 3609e4655..b93c17bc1 100644 --- a/main/src/domain/midi_types.rs +++ b/main/src/domain/midi_types.rs @@ -1,3 +1,4 @@ +use reaper_common_types::DurationInSeconds; use reaper_medium::{Hz, MIDI_INPUT_FRAME_RATE}; #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] @@ -48,4 +49,8 @@ impl SampleOffset { pub fn get(self) -> u64 { self.0 } + + pub fn to_seconds(&self, sample_rate: Hz) -> DurationInSeconds { + DurationInSeconds::new_panic(self.0 as f64 / sample_rate.get()) + } } diff --git a/main/src/domain/mod.rs b/main/src/domain/mod.rs index 5af9fd613..39fc49af9 100644 --- a/main/src/domain/mod.rs +++ b/main/src/domain/mod.rs @@ -170,3 +170,6 @@ mod playtime_util; mod hex; pub use hex::*; + +mod global_audio_state; +pub use global_audio_state::*; diff --git a/main/src/domain/parameter_manager.rs b/main/src/domain/parameter_manager.rs index a43690a1a..2e654df58 100644 --- a/main/src/domain/parameter_manager.rs +++ b/main/src/domain/parameter_manager.rs @@ -1,6 +1,6 @@ use crate::domain::{ - CompartmentKind, CompartmentParamIndex, CompartmentParams, ParamSetting, ParameterMainTask, - PluginParamIndex, PluginParams, RawParamValue, + CompartmentKind, CompartmentParamIndex, CompartmentParams, ControlEventTimestamp, ParamSetting, + ParameterMainTask, PluginParamIndex, PluginParams, RawParamValue, }; use base::{blocking_read_lock, blocking_write_lock, NamedChannelSender, SenderToNormalThread}; use reaper_high::Reaper; @@ -79,8 +79,14 @@ impl ParameterManager { // When rendering, we don't do it because that will accumulate until the rendering is // finished, which is pointless. if !is_rendering() { - self.parameter_main_task_sender - .send_complaining(ParameterMainTask::UpdateSingleParamValue { index, value }); + let timestamp = ControlEventTimestamp::from_main_thread(); + self.parameter_main_task_sender.send_complaining( + ParameterMainTask::UpdateSingleParamValue { + index, + value, + timestamp, + }, + ); } } diff --git a/main/src/infrastructure/plugin/backbone_shell.rs b/main/src/infrastructure/plugin/backbone_shell.rs index 7507bf208..9bc77eadb 100644 --- a/main/src/infrastructure/plugin/backbone_shell.rs +++ b/main/src/infrastructure/plugin/backbone_shell.rs @@ -16,7 +16,7 @@ use crate::domain::{ RealearnTarget, RealearnTargetState, ReaperTarget, ReaperTargetType, RequestMidiDeviceIdentityCommand, RequestMidiDeviceIdentityReply, SharedInstance, SharedMainProcessors, SharedRealTimeProcessor, Tag, UnitContainer, UnitId, - UnitOrchestrationEvent, WeakInstance, WeakUnit, + UnitOrchestrationEvent, WeakInstance, WeakUnit, GLOBAL_AUDIO_STATE, }; use crate::infrastructure::data::{ CommonCompartmentPresetManager, CompartmentPresetManagerEventHandler, ControllerManager, @@ -91,7 +91,7 @@ use std::collections::{HashMap, HashSet}; use std::error::Error; use std::future::Future; use std::rc::{Rc, Weak}; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; use std::thread::JoinHandle; use std::time::Duration; @@ -196,7 +196,6 @@ pub struct BackboneShell { welcome_panel: RefCell>>, /// We need to keep this panel in memory in order to be informed when it's destroyed. _shutdown_detection_panel: SharedView, - audio_block_counter: Arc, } #[derive(Clone, Debug)] @@ -414,11 +413,9 @@ impl BackboneShell { Box::new(BackboneControlSurfaceEventHandler), )); // This doesn't yet activate the audio hook (will happen on wake up) - let audio_block_counter = Arc::new(AtomicU32::new(0)); let audio_hook = RealearnAudioHook::new( normal_audio_hook_task_receiver, feedback_audio_hook_task_receiver, - audio_block_counter.clone(), ); // This doesn't yet activate the accelerator (will happen on wake up) let accelerator = @@ -483,7 +480,6 @@ impl BackboneShell { proto_hub: crate::infrastructure::proto::ProtoHub::new(), welcome_panel: Default::default(), _shutdown_detection_panel: shutdown_detection_panel, - audio_block_counter, } } @@ -594,12 +590,14 @@ impl BackboneShell { let middleware = control_surface.middleware_mut(); middleware.shutdown(); }); - // It's important to wait a bit otherwise we risk the MIDI is not being sent. + // It's important to wait a bit, otherwise we risk the MIDI is not being sent. // We wait for 3 audio blocks, a maximum of 100 milliseconds. Justin's recommendation. - let initial_block_count = self.audio_block_count(); + let initial_block_count = GLOBAL_AUDIO_STATE.load_block_count(); for _ in 0..100 { std::thread::sleep(Duration::from_millis(1)); - let elapsed_blocks = self.audio_block_count().saturating_sub(initial_block_count); + let elapsed_blocks = GLOBAL_AUDIO_STATE + .load_block_count() + .saturating_sub(initial_block_count); if elapsed_blocks > 2 { debug!("Waited a total of {elapsed_blocks} blocks after sending shutdown MIDI messages"); break; @@ -607,10 +605,6 @@ impl BackboneShell { } } - fn audio_block_count(&self) -> u32 { - self.audio_block_counter.load(Ordering::Relaxed) - } - fn reconnect_osc_devices(&self) { self.temporarily_reclaim_control_surface_ownership(|control_surface| { let middleware = control_surface.middleware_mut(); diff --git a/main/src/infrastructure/plugin/helgobox_plugin.rs b/main/src/infrastructure/plugin/helgobox_plugin.rs index 944a7a0f1..12eb0c2e4 100644 --- a/main/src/infrastructure/plugin/helgobox_plugin.rs +++ b/main/src/infrastructure/plugin/helgobox_plugin.rs @@ -7,7 +7,7 @@ use vst::plugin::{ use crate::domain::{ ControlEvent, ControlEventTimestamp, InstanceId, MidiEvent, ParameterManager, PluginParamIndex, - ProcessorContext, RawParamValue, PLUGIN_PARAMETER_COUNT, + ProcessorContext, RawParamValue, GLOBAL_AUDIO_STATE, PLUGIN_PARAMETER_COUNT, }; use crate::infrastructure::plugin::instance_parameter_container::InstanceParameterContainer; use crate::infrastructure::plugin::{init_backbone_shell, SET_STATE_PARAM_NAME}; @@ -59,7 +59,9 @@ pub struct HelgoboxPlugin { param_container: Arc, // For detecting play state changes was_playing_in_last_cycle: bool, + /// I think this sample rate can be different from the device sample rate in some cases (oversampling). sample_rate: Hz, + block_size: u32, /// **After `init` has been called**, this should be `true` if the plug-in was loaded by the /// REAPER VST scan (and is not going to be used "for real"). is_plugin_scan: bool, @@ -105,6 +107,7 @@ impl Plugin for HelgoboxPlugin { param_container: Arc::new(InstanceParameterContainer::new()), was_playing_in_last_cycle: false, sample_rate: Default::default(), + block_size: 0, is_plugin_scan: false, lazy_data: OnceLock::new(), instance_panel: Rc::new(InstancePanel::new(instance_id)), @@ -224,8 +227,10 @@ impl Plugin for HelgoboxPlugin { fn process_events(&mut self, events: &Events) { firewall(|| { assert_no_alloc(|| { - let timestamp = ControlEventTimestamp::now(); let is_transport_start = !self.was_playing_in_last_cycle && self.is_now_playing(); + let block_count = GLOBAL_AUDIO_STATE.load_block_count(); + let sample_count = block_count * self.block_size as u64; + let device_sample_rate = GLOBAL_AUDIO_STATE.load_sample_rate(); for e in events.events() { let our_event = match MidiEvent::from_vst(e) { Err(_) => { @@ -235,6 +240,11 @@ impl Plugin for HelgoboxPlugin { } Ok(e) => e, }; + let timestamp = ControlEventTimestamp::from_rt( + sample_count, + device_sample_rate, + our_event.offset().to_seconds(self.sample_rate), + ); let our_event = ControlEvent::new(our_event, timestamp); if let Some(lazy_data) = self.lazy_data.get() { lazy_data.instance_shell.process_incoming_midi_from_plugin( @@ -285,8 +295,9 @@ impl Plugin for HelgoboxPlugin { tracing::debug!("VST resume"); } - fn set_block_size(&mut self, _size: i64) { + fn set_block_size(&mut self, size: i64) { tracing::debug!("VST set block size"); + self.block_size = size as u32; } fn start_process(&mut self) { diff --git a/playtime-clip-engine b/playtime-clip-engine index 9c1781354..ffb4f4558 160000 --- a/playtime-clip-engine +++ b/playtime-clip-engine @@ -1 +1 @@ -Subproject commit 9c1781354049b3f00caea470819828032638b7ec +Subproject commit ffb4f4558790f17fa3be5589c7e313dd317c73e0