Skip to content

Commit

Permalink
#1124 Make it possible to transform to another MIDI input device
Browse files Browse the repository at this point in the history
  • Loading branch information
helgoboss committed Aug 26, 2024
1 parent a379f6b commit 5982288
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 202 deletions.
16 changes: 11 additions & 5 deletions api/src/persistence/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1014,7 +1014,7 @@ pub struct SendMidiTarget {
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination: Option<MidiDestination>,
pub destination: Option<SendMidiDestination>,
}

#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
Expand Down Expand Up @@ -2265,15 +2265,21 @@ impl Default for PlaytimeRowDescriptor {
}
}

#[derive(Eq, PartialEq, Serialize, Deserialize)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum MidiDestination {
pub enum SendMidiDestination {
FxOutput,
FeedbackOutput,
DeviceInput,
InputDevice(InputDeviceMidiDestination),
}

#[derive(Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct InputDeviceMidiDestination {
#[serde(skip_serializing_if = "Option::is_none")]
pub device_id: Option<u8>,
}

impl Default for MidiDestination {
impl Default for SendMidiDestination {
fn default() -> Self {
Self::FeedbackOutput
}
Expand Down
59 changes: 41 additions & 18 deletions main/src/application/target_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::domain::{
FxParameterDescriptor, GroupId, MappingId, MappingKey, MappingRef, MappingSnapshotId,
MouseActionType, OscDeviceId, PotFilterItemsTargetSettings, ProcessorContext,
QualifiedMappingId, RealearnTarget, ReaperTarget, ReaperTargetType, SeekOptions,
SendMidiDestination, SoloBehavior, Tag, TagScope, TouchedRouteParameterType,
SendMidiDestinationType, SoloBehavior, Tag, TagScope, TouchedRouteParameterType,
TouchedTrackParameterType, TrackDescriptor, TrackExclusivity, TrackGangBehavior,
TrackRouteDescriptor, TrackRouteSelector, TrackRouteType, TransportAction,
UnresolvedActionTarget, UnresolvedAllTrackFxEnableTarget, UnresolvedAnyOnTarget,
Expand Down Expand Up @@ -61,18 +61,19 @@ use crate::domain::ui_util::format_tags_as_csv;
use base::hash_util::NonCryptoHashSet;
use helgobox_api::persistence::{
ActionScope, Axis, BrowseTracksMode, ClipColumnTrackContext, FxChainDescriptor,
FxDescriptorCommons, FxToolAction, LearnTargetMappingModification, LearnableTargetKind,
MappingModification, MappingSnapshotDescForLoad, MappingSnapshotDescForTake, MonitoringMode,
MouseAction, MouseButton, PlaytimeColumnAction, PlaytimeColumnDescriptor, PlaytimeMatrixAction,
PlaytimeRowAction, PlaytimeRowDescriptor, PlaytimeSlotDescriptor, PlaytimeSlotManagementAction,
PlaytimeSlotTransportAction, PotFilterKind, SeekBehavior,
SetTargetToLastTouchedMappingModification, TargetTouchCause, TrackDescriptorCommons,
TrackFxChain, TrackScope, TrackToolAction, VirtualControlElementCharacter,
FxDescriptorCommons, FxToolAction, InputDeviceMidiDestination, LearnTargetMappingModification,
LearnableTargetKind, MappingModification, MappingSnapshotDescForLoad,
MappingSnapshotDescForTake, MonitoringMode, MouseAction, MouseButton, PlaytimeColumnAction,
PlaytimeColumnDescriptor, PlaytimeMatrixAction, PlaytimeRowAction, PlaytimeRowDescriptor,
PlaytimeSlotDescriptor, PlaytimeSlotManagementAction, PlaytimeSlotTransportAction,
PotFilterKind, SeekBehavior, SendMidiDestination, SetTargetToLastTouchedMappingModification,
TargetTouchCause, TrackDescriptorCommons, TrackFxChain, TrackScope, TrackToolAction,
VirtualControlElementCharacter,
};
use playtime_api::persistence::ColumnAddress;
use reaper_medium::{
AutomationMode, BookmarkId, GlobalAutomationModeOverride, InputMonitoringMode, SectionId,
TrackArea, TrackLocation, TrackSendDirection,
AutomationMode, BookmarkId, GlobalAutomationModeOverride, InputMonitoringMode,
MidiInputDeviceId, SectionId, TrackArea, TrackLocation, TrackSendDirection,
};
use std::fmt;
use std::fmt::{Display, Formatter};
Expand Down Expand Up @@ -142,7 +143,8 @@ pub enum TargetCommand {
SetScrollArrangeView(bool),
SetScrollMixer(bool),
SetRawMidiPattern(String),
SetSendMidiDestination(SendMidiDestination),
SetSendMidiDestinationType(SendMidiDestinationType),
SetMidiInputDevice(Option<MidiInputDeviceId>),
SetOscAddressPattern(String),
SetOscArgIndex(Option<u32>),
SetOscArgTypeTag(OscTypeTag),
Expand Down Expand Up @@ -243,6 +245,7 @@ pub enum TargetProp {
ScrollMixer,
RawMidiPattern,
SendMidiDestination,
MidiInputDevice,
OscAddressPattern,
OscArgIndex,
OscArgTypeTag,
Expand Down Expand Up @@ -532,10 +535,14 @@ impl<'a> Change<'a> for TargetModel {
self.raw_midi_pattern = v;
One(P::RawMidiPattern)
}
C::SetSendMidiDestination(v) => {
self.send_midi_destination = v;
C::SetSendMidiDestinationType(v) => {
self.send_midi_destination_type = v;
One(P::SendMidiDestination)
}
C::SetMidiInputDevice(v) => {
self.midi_input_device = v;
One(P::MidiInputDevice)
}
C::SetOscAddressPattern(v) => {
self.osc_address_pattern = v;
One(P::OscAddressPattern)
Expand Down Expand Up @@ -760,7 +767,8 @@ pub struct TargetModel {
scroll_mixer: bool,
// # For Send MIDI target
raw_midi_pattern: String,
send_midi_destination: SendMidiDestination,
send_midi_destination_type: SendMidiDestinationType,
midi_input_device: Option<MidiInputDeviceId>,
// # For Send OSC target
osc_address_pattern: String,
osc_arg_index: Option<u32>,
Expand Down Expand Up @@ -908,7 +916,8 @@ impl Default for TargetModel {
scroll_arrange_view: false,
scroll_mixer: false,
raw_midi_pattern: Default::default(),
send_midi_destination: Default::default(),
send_midi_destination_type: Default::default(),
midi_input_device: None,
osc_address_pattern: "".to_owned(),
osc_arg_index: Some(0),
osc_arg_type_tag: Default::default(),
Expand Down Expand Up @@ -1218,8 +1227,12 @@ impl TargetModel {
&self.raw_midi_pattern
}

pub fn send_midi_destination(&self) -> SendMidiDestination {
self.send_midi_destination
pub fn send_midi_destination_type(&self) -> SendMidiDestinationType {
self.send_midi_destination_type
}

pub fn midi_input_device(&self) -> Option<MidiInputDeviceId> {
self.midi_input_device
}

pub fn osc_address_pattern(&self) -> &str {
Expand Down Expand Up @@ -2494,7 +2507,17 @@ impl TargetModel {
}),
SendMidi => UnresolvedReaperTarget::SendMidi(UnresolvedMidiSendTarget {
pattern: self.raw_midi_pattern.parse().unwrap_or_default(),
destination: self.send_midi_destination,
destination: match self.send_midi_destination_type {
SendMidiDestinationType::FxOutput => SendMidiDestination::FxOutput,
SendMidiDestinationType::FeedbackOutput => {
SendMidiDestination::FeedbackOutput
}
SendMidiDestinationType::InputDevice => {
SendMidiDestination::InputDevice(InputDeviceMidiDestination {
device_id: self.midi_input_device.map(|d| d.get()),
})
}
},
}),
SendOsc => UnresolvedReaperTarget::SendOsc(UnresolvedOscSendTarget {
address_pattern: self.osc_address_pattern.clone(),
Expand Down
20 changes: 18 additions & 2 deletions main/src/domain/audio_hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ impl RealearnAudioHook {
time_of_last_run: None,
initialized: false,
counter,
midi_transformation_container: MidiTransformationContainer::with_capacity(1000),
midi_transformation_container: MidiTransformationContainer::new(),
#[cfg(feature = "playtime")]
clip_engine_audio_hook: playtime_clip_engine::rt::audio_hook::PlaytimeAudioHook::new(),
}
Expand Down Expand Up @@ -433,13 +433,29 @@ impl RealearnAudioHook {
}
}
// Add transformed events *after* iterating
for event in self.midi_transformation_container.drain() {
for event in self
.midi_transformation_container
.drain_same_device_events()
{
let reaper_event = reaper_medium::MidiEvent::from_raw_ref(event.as_ref());
event_list.add_item(reaper_event);
}
}
});
}
// Process MIDI "MIDI: Send message" to "Device input" across multiple devices
for evt in self
.midi_transformation_container
.drain_other_device_events()
{
MidiInputDevice::new(evt.input_device_id).with_midi_input(|mi| {
if let Some(mi) = mi {
let event_list = mi.get_read_buf();
let reaper_event = reaper_medium::MidiEvent::from_raw_ref(evt.event.as_ref());
event_list.add_item(reaper_event);
}
});
}
}

fn process_midi_device_inquiry_command(
Expand Down
51 changes: 44 additions & 7 deletions main/src/domain/midi_transformation_container.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,59 @@
use helgoboss_learn::RawMidiEvent;
use reaper_medium::MidiInputDeviceId;

#[derive(Debug)]
pub struct MidiTransformationContainer {
events: Vec<RawMidiEvent>,
/// Emptied right after reading the input buffer of a device.
same_device_events: Vec<RawMidiEvent>,
/// Emptied later, after reading the input buffers of **all** devices (should have a larger capacity).
other_device_events: Vec<DevQualifiedRawMidiEvent>,
}

#[derive(Debug)]
pub struct DevQualifiedRawMidiEvent {
pub input_device_id: MidiInputDeviceId,
pub event: RawMidiEvent,
}

impl DevQualifiedRawMidiEvent {
fn new(input_device_id: MidiInputDeviceId, event: RawMidiEvent) -> Self {
Self {
input_device_id,
event,
}
}
}

impl Default for MidiTransformationContainer {
fn default() -> Self {
Self::new()
}
}

impl MidiTransformationContainer {
pub fn with_capacity(capacity: usize) -> Self {
pub fn new() -> Self {
Self {
events: Vec::with_capacity(capacity),
same_device_events: Vec::with_capacity(100),
other_device_events: Vec::with_capacity(900),
}
}

pub fn push(&mut self, device: Option<MidiInputDeviceId>, event: RawMidiEvent) {
if let Some(dev) = device {
self.other_device_events
.push(DevQualifiedRawMidiEvent::new(dev, event));
} else {
self.same_device_events.push(event);
}
}

pub fn push(&mut self, event: RawMidiEvent) {
self.events.push(event);
pub fn drain_same_device_events(&mut self) -> impl Iterator<Item = RawMidiEvent> + '_ {
self.same_device_events.drain(..)
}

pub fn drain(&mut self) -> impl Iterator<Item = RawMidiEvent> + '_ {
self.events.drain(..)
pub fn drain_other_device_events(
&mut self,
) -> impl Iterator<Item = DevQualifiedRawMidiEvent> + '_ {
self.other_device_events.drain(..)
}
}
10 changes: 5 additions & 5 deletions main/src/domain/reaper_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,19 +176,19 @@ pub enum ReaperTarget {
Display,
)]
#[repr(usize)]
pub enum SendMidiDestination {
pub enum SendMidiDestinationType {
#[serde(rename = "fx-output")]
#[display(fmt = "FX output")]
FxOutput,
#[serde(rename = "feedback-output")]
#[display(fmt = "Feedback output")]
FeedbackOutput,
#[serde(rename = "device-input")]
#[display(fmt = "Device input")]
DeviceInput,
#[serde(rename = "input-device")]
#[display(fmt = "Input device")]
InputDevice,
}

impl Default for SendMidiDestination {
impl Default for SendMidiDestinationType {
fn default() -> Self {
Self::FxOutput
}
Expand Down
26 changes: 15 additions & 11 deletions main/src/domain/targets/midi_send_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ use crate::domain::{
ExtendedProcessorContext, FeedbackAudioHookTask, FeedbackOutput, FeedbackRealTimeTask,
HitResponse, LogOptions, MappingControlContext, MidiDestination, MidiEvent,
MidiTransformationContainer, RealTimeReaperTarget, RealearnTarget, ReaperTarget,
ReaperTargetType, SendMidiDestination, TargetCharacter, TargetSection, TargetTypeDef,
UnresolvedReaperTargetDef, DEFAULT_TARGET,
ReaperTargetType, TargetCharacter, TargetSection, TargetTypeDef, UnresolvedReaperTargetDef,
DEFAULT_TARGET,
};
use base::{NamedChannelSender, SenderToNormalThread, SenderToRealTimeThread};
use helgoboss_learn::{
create_raw_midi_events_singleton, AbsoluteValue, ControlType, ControlValue, Fraction,
MidiSourceValue, RawMidiPattern, Target, UnitValue,
};
use helgobox_allocator::permit_alloc;
use helgobox_api::persistence::SendMidiDestination;
use reaper_high::MidiOutputDevice;
use reaper_medium::SendMidiTime;
use reaper_medium::{MidiInputDeviceId, SendMidiTime};
use std::convert::TryInto;

#[derive(Debug)]
Expand Down Expand Up @@ -66,6 +67,7 @@ impl MidiSendTarget {
self.destination
}

#[allow(clippy::too_many_arguments)]
pub fn midi_send_target_send_midi_in_rt_thread(
&mut self,
caller: Caller,
Expand All @@ -82,7 +84,7 @@ impl MidiSendTarget {
// send a MIDI message and this needs to happen in the audio thread.
// Going to the main thread and back would be such a waste!
let raw_midi_event = self.pattern().to_concrete_midi_event(v);
match self.destination() {
match self.destination {
SendMidiDestination::FxOutput | SendMidiDestination::FeedbackOutput => {
let midi_destination = match caller {
Caller::Vst(_) => match self.destination() {
Expand Down Expand Up @@ -132,10 +134,12 @@ impl MidiSendTarget {
}
};
}
SendMidiDestination::DeviceInput => {
if let Some(container) = transformation_container {
container.push(raw_midi_event);
}
SendMidiDestination::InputDevice(d) => {
let container = transformation_container.as_mut().ok_or(
"can't send to device input when MIDI doesn't come from device directly",
)?;
let dev_id = d.device_id.map(MidiInputDeviceId::new);
container.push(dev_id, raw_midi_event);
}
}
// We end up here only if the message was successfully sent
Expand Down Expand Up @@ -247,9 +251,6 @@ impl RealearnTarget for MidiSendTarget {
let resolved_destination =
match self.destination {
SendMidiDestination::FxOutput => MidiDestination::FxOutput,
SendMidiDestination::DeviceInput => return Err(
"sending to device input is only possible in response to a MIDI source event coming from a MIDI device",
),
SendMidiDestination::FeedbackOutput => {
let feedback_output = context
.control_context
Expand All @@ -261,6 +262,9 @@ impl RealearnTarget for MidiSendTarget {
return Err("feedback output is not MIDI");
}
}
SendMidiDestination::InputDevice(_) => return Err(
"sending to device input is only possible in response to a MIDI source event coming from a MIDI device",
),
};
self.artificial_value = value;
let raw_midi_events =
Expand Down
Loading

0 comments on commit 5982288

Please sign in to comment.