Skip to content

Commit 21d4e64

Browse files
authored
Merge pull request #173 from helgoboss/feature/osc-performance
Vast OSC feedback performance improvement
2 parents ab931fa + ae9e73d commit 21d4e64

17 files changed

+1111
-521
lines changed

.vscode/launch.json

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,9 @@
2626
"stopAtEntry": false,
2727
"cwd": "${workspaceFolder}",
2828
"environment": [
29-
{"name": "REALEARN_LOG", "value": "debug,vst=info,hyper=trace"},
30-
{"name": "REALEARN_METER", "value": "true"}
31-
],
32-
"externalConsole": false
29+
{"name": "REALEARN_LOG", "value": "debug,vst=info,hyper=trace"}
30+
// {"name": "REALEARN_METER", "value": "true"}
31+
]
3332
},
3433
{
3534
"name": "Portable Windows 32-bit",
@@ -39,8 +38,7 @@
3938
"args": [],
4039
"stopAtEntry": false,
4140
"cwd": "${workspaceFolder}",
42-
"environment": [],
43-
"externalConsole": false
41+
"environment": []
4442
},
4543
{
4644
"name": "Global Windows 64-bit",
@@ -50,8 +48,7 @@
5048
"args": [],
5149
"stopAtEntry": false,
5250
"cwd": "${workspaceFolder}",
53-
"environment": [],
54-
"externalConsole": false
51+
"environment": []
5552
},
5653
{
5754
"name": "Windows attach",

Cargo.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

main/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "realearn"
3-
version = "2.3.1"
3+
version = "2.3.2"
44
authors = ["Benjamin Klum <[email protected]>"]
55
edition = "2018"
66
build = "build.rs"
@@ -83,7 +83,7 @@ tempfile = "3.1.0"
8383
runas = "0.2.1"
8484
# For building URls, e.g. companion web app URL
8585
url = "2.1.1"
86-
# For grouping mappings by virtual control elements
86+
# For grouping mappings by virtual control elements and grouping outgoing OSC messages by device
8787
itertools = "0.9"
8888
# For being able to configure logging via environment variable RUST_LOG
8989
env_logger = "0.8.2"

main/src/core/global_macros.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ macro_rules! make_available_globally_in_main_thread {
1818
};
1919
}
2020

21+
/// Use only where absolutely necessary because of static-only FFI stuff!
22+
macro_rules! make_available_globally_in_main_thread_on_demand {
23+
($instance_struct:path) => {
24+
// This is safe (see https://doc.rust-lang.org/std/sync/struct.Once.html#examples-1).
25+
static mut INSTANCE: Option<$instance_struct> = None;
26+
27+
impl $instance_struct {
28+
pub fn make_available_globally(instance: $instance_struct) {
29+
static INIT_INSTANCE: std::sync::Once = std::sync::Once::new();
30+
unsafe {
31+
INIT_INSTANCE.call_once(|| {
32+
INSTANCE = Some(instance);
33+
reaper_low::register_plugin_destroy_hook(|| INSTANCE = None);
34+
});
35+
}
36+
}
37+
38+
/// Panics if not in main thread.
39+
pub fn get() -> &'static $instance_struct {
40+
reaper_high::Reaper::get().require_main_thread();
41+
unsafe {
42+
INSTANCE
43+
.as_ref()
44+
.expect("call `make_available_globally()` before using `get()`")
45+
}
46+
}
47+
}
48+
};
49+
}
50+
2151
/// Use only where absolutely necessary because of static-only FFI stuff!
2252
///
2353
/// The given struct must be thread-safe. If not, all of its public methods should first check if

main/src/domain/control_surface.rs

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@ use crate::domain::{
55
use crossbeam_channel::Receiver;
66
use helgoboss_learn::OscSource;
77
use reaper_high::{
8-
ChangeDetectionMiddleware, ControlSurfaceEvent, ControlSurfaceMiddleware, FutureMiddleware,
9-
MainTaskMiddleware, MeterMiddleware,
8+
ChangeDetectionMiddleware, ControlSurfaceEvent, ControlSurfaceMiddleware, FutureMiddleware, Fx,
9+
FxParameter, MainTaskMiddleware, MeterMiddleware,
1010
};
1111
use reaper_rx::ControlSurfaceRxMiddleware;
1212
use rosc::{OscMessage, OscPacket};
1313

14+
use reaper_medium::{CommandId, ReaperNormalizedFxParamValue};
15+
use rxrust::prelude::*;
1416
use smallvec::SmallVec;
1517
use std::collections::HashMap;
1618

1719
type LearnSourceSender = async_channel::Sender<(OscDeviceId, OscSource)>;
1820

19-
const OSC_BULK_SIZE: usize = 32;
21+
const OSC_INCOMING_BULK_SIZE: usize = 32;
2022

2123
#[derive(Debug)]
2224
pub struct RealearnControlSurfaceMiddleware<EH: DomainEventHandler> {
@@ -26,6 +28,7 @@ pub struct RealearnControlSurfaceMiddleware<EH: DomainEventHandler> {
2628
main_processors: Vec<MainProcessor<EH>>,
2729
main_task_receiver: Receiver<RealearnControlSurfaceMainTask<EH>>,
2830
server_task_receiver: Receiver<RealearnControlSurfaceServerTask>,
31+
additional_feedback_event_receiver: Receiver<AdditionalFeedbackEvent>,
2932
meter_middleware: MeterMiddleware,
3033
main_task_middleware: MainTaskMiddleware,
3134
future_middleware: FutureMiddleware,
@@ -52,6 +55,22 @@ pub enum RealearnControlSurfaceMainTask<EH: DomainEventHandler> {
5255
StopLearning,
5356
}
5457

58+
/// Not all events in REAPER are communicated via a control surface, e.g. action invocations.
59+
#[derive(Debug)]
60+
pub enum AdditionalFeedbackEvent {
61+
ActionInvoked(CommandId),
62+
FxSnapshotLoaded(Fx),
63+
/// Work around REAPER's inability to notify about parameter changes in
64+
/// monitoring FX by simulating the notification ourselves.
65+
/// Then parameter learning and feedback works at least for
66+
/// ReaLearn monitoring FX instances, which is especially
67+
/// useful for conditional activation.
68+
RealearnMonitoringFxParameterValueChanged {
69+
parameter: FxParameter,
70+
new_value: ReaperNormalizedFxParamValue,
71+
},
72+
}
73+
5574
pub enum RealearnControlSurfaceServerTask {
5675
ProvidePrometheusMetrics(tokio::sync::oneshot::Sender<String>),
5776
}
@@ -61,6 +80,7 @@ impl<EH: DomainEventHandler> RealearnControlSurfaceMiddleware<EH> {
6180
parent_logger: &slog::Logger,
6281
main_task_receiver: Receiver<RealearnControlSurfaceMainTask<EH>>,
6382
server_task_receiver: Receiver<RealearnControlSurfaceServerTask>,
83+
additional_feedback_event_receiver: Receiver<AdditionalFeedbackEvent>,
6484
metrics_enabled: bool,
6585
) -> Self {
6686
let logger = parent_logger.new(slog::o!("struct" => "RealearnControlSurfaceMiddleware"));
@@ -71,6 +91,7 @@ impl<EH: DomainEventHandler> RealearnControlSurfaceMiddleware<EH> {
7191
main_processors: Default::default(),
7292
main_task_receiver,
7393
server_task_receiver,
94+
additional_feedback_event_receiver,
7495
meter_middleware: MeterMiddleware::new(logger.clone()),
7596
main_task_middleware: MainTaskMiddleware::new(
7697
logger.clone(),
@@ -103,6 +124,9 @@ impl<EH: DomainEventHandler> RealearnControlSurfaceMiddleware<EH> {
103124

104125
pub fn reset(&self) {
105126
self.change_detection_middleware.reset(|e| {
127+
for m in &self.main_processors {
128+
m.process_control_surface_change_event(&e);
129+
}
106130
self.rx_middleware.handle_change(e);
107131
});
108132
// We don't want to execute tasks which accumulated during the "downtime" of Reaper.
@@ -149,7 +173,23 @@ impl<EH: DomainEventHandler> RealearnControlSurfaceMiddleware<EH> {
149173
}
150174
}
151175
}
152-
self.process_osc();
176+
for event in self.additional_feedback_event_receiver.try_iter().take(30) {
177+
if let AdditionalFeedbackEvent::RealearnMonitoringFxParameterValueChanged {
178+
parameter,
179+
..
180+
} = &event
181+
{
182+
let rx = Global::control_surface_rx();
183+
rx.fx_parameter_value_changed
184+
.borrow_mut()
185+
.next(parameter.clone());
186+
rx.fx_parameter_touched.borrow_mut().next(parameter.clone());
187+
}
188+
for p in &mut self.main_processors {
189+
p.process_additional_feedback_event(&event)
190+
}
191+
}
192+
self.process_incoming_osc_messages();
153193
match &self.state {
154194
State::Normal => {
155195
for p in &mut self.main_processors {
@@ -173,12 +213,17 @@ impl<EH: DomainEventHandler> RealearnControlSurfaceMiddleware<EH> {
173213
}
174214
}
175215

176-
fn process_osc(&mut self) {
177-
pub type PacketVec = SmallVec<[OscPacket; OSC_BULK_SIZE]>;
216+
fn process_incoming_osc_messages(&mut self) {
217+
pub type PacketVec = SmallVec<[OscPacket; OSC_INCOMING_BULK_SIZE]>;
178218
let packets_by_device: SmallVec<[(OscDeviceId, PacketVec); 32]> = self
179219
.osc_input_devices
180220
.iter_mut()
181-
.map(|dev| (*dev.id(), dev.poll_multiple(OSC_BULK_SIZE).collect()))
221+
.map(|dev| {
222+
(
223+
*dev.id(),
224+
dev.poll_multiple(OSC_INCOMING_BULK_SIZE).collect(),
225+
)
226+
})
182227
.collect();
183228
for (dev_id, packets) in packets_by_device {
184229
match &self.state {
@@ -207,8 +252,17 @@ impl<EH: DomainEventHandler> RealearnControlSurfaceMiddleware<EH> {
207252
self.change_detection_middleware.process(event, |e| {
208253
match &self.state {
209254
State::Normal => {
255+
// This is for feedback processing. No Rx!
256+
for m in &self.main_processors {
257+
m.process_control_surface_change_event(&e);
258+
}
259+
// The rest is only for upper layers (e.g. UI), not for processing.
210260
self.rx_middleware.handle_change(e.clone());
211261
if let Some(target) = ReaperTarget::touched_from_change_event(e) {
262+
// TODO-medium Now we have the necessary framework (AdditionalFeedbackEvent)
263+
// to also support action, FX snapshot and ReaLearn monitoring FX parameter
264+
// touching for "Last touched" target and global learning (see
265+
// LearningTarget state)! Connect the dots!
212266
DomainGlobal::get().set_last_touched_target(target);
213267
for p in &self.main_processors {
214268
p.notify_target_touched();

main/src/domain/domain_global.rs

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
use crate::domain::{OscDeviceId, OscOutputDevice, RealearnTargetContext, ReaperTarget};
2-
use rosc::{OscMessage, OscPacket};
1+
use crate::domain::{RealearnTargetContext, ReaperTarget};
32
use std::cell::RefCell;
43

5-
make_available_globally_in_main_thread!(DomainGlobal);
4+
make_available_globally_in_main_thread_on_demand!(DomainGlobal);
65

7-
#[derive(Default)]
86
pub struct DomainGlobal {
97
target_context: RefCell<RealearnTargetContext>,
108
last_touched_target: RefCell<Option<ReaperTarget>>,
11-
osc_output_devices: RefCell<Vec<OscOutputDevice>>,
129
}
1310

1411
impl DomainGlobal {
12+
pub fn new(target_context: RealearnTargetContext) -> Self {
13+
Self {
14+
target_context: RefCell::new(target_context),
15+
last_touched_target: RefCell::new(None),
16+
}
17+
}
18+
1519
pub fn target_context() -> &'static RefCell<RealearnTargetContext> {
1620
&DomainGlobal::get().target_context
1721
}
@@ -20,21 +24,6 @@ impl DomainGlobal {
2024
self.last_touched_target.borrow().clone()
2125
}
2226

23-
pub fn send_osc_feedback(&self, dev_id: &OscDeviceId, msg: OscMessage) {
24-
let devices = self.osc_output_devices.borrow();
25-
if let Some(dev) = devices.iter().find(|d| d.id() == dev_id) {
26-
let _ = dev.send(&OscPacket::Message(msg));
27-
}
28-
}
29-
30-
pub fn set_osc_output_devices(&self, devices: Vec<OscOutputDevice>) {
31-
*self.osc_output_devices.borrow_mut() = devices;
32-
}
33-
34-
pub fn clear_osc_output_devices(&self) {
35-
self.osc_output_devices.borrow_mut().clear();
36-
}
37-
3827
pub(super) fn set_last_touched_target(&self, target: ReaperTarget) {
3928
*self.last_touched_target.borrow_mut() = Some(target);
4029
}

main/src/domain/feedback_buffer.rs

Lines changed: 0 additions & 55 deletions
This file was deleted.

0 commit comments

Comments
 (0)