Skip to content

Commit

Permalink
#6 Implement "MIDI: Send message" target (step 2: also works for OSC …
Browse files Browse the repository at this point in the history
…sources)
  • Loading branch information
helgoboss committed Apr 14, 2021
1 parent 9c7d159 commit cc87a3c
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 41 deletions.
50 changes: 37 additions & 13 deletions main/src/domain/main_processor.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use crate::domain::{
ActivationChange, AdditionalFeedbackEvent, BackboneState, CompoundMappingSource,
CompoundMappingTarget, ControlInput, ControlMode, DeviceFeedbackOutput, DomainEvent,
DomainEventHandler, ExtendedProcessorContext, FeedbackAudioHookTask, FeedbackOutput,
FeedbackRealTimeTask, FeedbackValue, InstanceOrchestrationEvent, IoUpdatedEvent, MainMapping,
MappingActivationEffect, MappingCompartment, MappingId, MidiDestination, MidiSource,
NormalRealTimeTask, OscDeviceId, OscFeedbackTask, PartialControlMatch,
PlayPosFeedbackResolution, ProcessorContext, QualifiedSource, RealFeedbackValue, RealSource,
RealTimeSender, RealearnMonitoringFxParameterValueChangedEvent, ReaperTarget,
SourceFeedbackValue, SourceReleasedEvent, TargetValueChangedEvent, VirtualSourceValue,
CompoundMappingTarget, ControlContext, ControlInput, ControlMode, DeviceFeedbackOutput,
DomainEvent, DomainEventHandler, ExtendedProcessorContext, FeedbackAudioHookTask,
FeedbackOutput, FeedbackRealTimeTask, FeedbackValue, InstanceOrchestrationEvent,
IoUpdatedEvent, MainMapping, MappingActivationEffect, MappingCompartment, MappingId,
MidiDestination, MidiSource, NormalRealTimeTask, OscDeviceId, OscFeedbackTask,
PartialControlMatch, PlayPosFeedbackResolution, ProcessorContext, QualifiedSource,
RealFeedbackValue, RealSource, RealTimeSender, RealearnMonitoringFxParameterValueChangedEvent,
ReaperTarget, SourceFeedbackValue, SourceReleasedEvent, TargetValueChangedEvent,
VirtualSourceValue,
};
use enum_map::EnumMap;
use helgoboss_learn::{ControlValue, ModeControlOptions, OscSource, UnitValue};
Expand Down Expand Up @@ -225,7 +226,15 @@ impl<EH: DomainEventHandler> MainProcessor<EH> {
// there might be a short amount of time
// where we still receive control
// statements. We filter them here.
let feedback = m.control_if_enabled(value, options);
let feedback = m.control_if_enabled(
value,
options,
ControlContext {
feedback_audio_hook_task_sender: &self
.feedback_audio_hook_task_sender,
feedback_output: self.feedback_output,
},
);
self.send_feedback(FeedbackReason::Normal, feedback);
};
}
Expand All @@ -234,7 +243,10 @@ impl<EH: DomainEventHandler> MainProcessor<EH> {
for compartment in MappingCompartment::enum_iter() {
for id in self.poll_control_mappings[compartment].iter() {
if let Some(m) = self.mappings[compartment].get_mut(id) {
let feedback = m.poll_if_control_enabled();
let feedback = m.poll_if_control_enabled(ControlContext {
feedback_audio_hook_task_sender: &self.feedback_audio_hook_task_sender,
feedback_output: self.feedback_output,
});
self.send_feedback(FeedbackReason::Normal, feedback);
}
}
Expand Down Expand Up @@ -1048,8 +1060,15 @@ impl<EH: DomainEventHandler> MainProcessor<EH> {
{
if let CompoundMappingSource::Osc(s) = m.source() {
if let Some(control_value) = s.control(msg) {
let feedback =
m.control_if_enabled(control_value, ControlOptions::default());
let feedback = m.control_if_enabled(
control_value,
ControlOptions::default(),
ControlContext {
feedback_audio_hook_task_sender: &self
.feedback_audio_hook_task_sender,
feedback_output: self.feedback_output,
},
);
send_direct_and_virtual_feedback(
&InstanceProps {
rt_sender: &self.feedback_real_time_task_sender,
Expand Down Expand Up @@ -1713,6 +1732,10 @@ fn control_virtual_mappings_osc<EH: DomainEventHandler>(
.send_feedback_after_control,
mode_control_options: m.mode_control_options(),
},
ControlContext {
feedback_audio_hook_task_sender: instance.fb_audio_hook_task_sender,
feedback_output: instance.feedback_output,
},
)
}
ProcessDirect(_) => {
Expand All @@ -1737,6 +1760,7 @@ fn control_main_mappings_virtual(
main_mappings: &mut HashMap<MappingId, MainMapping>,
value: VirtualSourceValue,
options: ControlOptions,
context: ControlContext,
) -> Vec<FeedbackValue> {
// Controller mappings can't have virtual sources, so for now we only need to check
// main mappings.
Expand All @@ -1746,7 +1770,7 @@ fn control_main_mappings_virtual(
.filter_map(|m| {
if let CompoundMappingSource::Virtual(s) = &m.source() {
let control_value = s.control(&value)?;
m.control_if_enabled(control_value, options)
m.control_if_enabled(control_value, options, context)
} else {
None
}
Expand Down
21 changes: 11 additions & 10 deletions main/src/domain/mapping.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::domain::{
ActivationChange, ActivationCondition, ControlOptions, ExtendedProcessorContext,
MappingActivationEffect, MidiSource, Mode, ParameterArray, ParameterSlice,
PlayPosFeedbackResolution, RealSource, RealearnTarget, ReaperTarget, TargetCharacter,
UnresolvedReaperTarget, VirtualControlElement, VirtualSource, VirtualSourceValue,
VirtualTarget, COMPARTMENT_PARAMETER_COUNT,
ActivationChange, ActivationCondition, ControlContext, ControlOptions,
ExtendedProcessorContext, MappingActivationEffect, MidiSource, Mode, ParameterArray,
ParameterSlice, PlayPosFeedbackResolution, RealSource, RealearnTarget, ReaperTarget,
TargetCharacter, UnresolvedReaperTarget, VirtualControlElement, VirtualSource,
VirtualSourceValue, VirtualTarget, COMPARTMENT_PARAMETER_COUNT,
};
use derive_more::Display;
use enum_iterator::IntoEnumIterator;
Expand Down Expand Up @@ -339,7 +339,7 @@ impl MainMapping {
}

/// This is for timer-triggered control and works like `control_if_enabled`.
pub fn poll_if_control_enabled(&mut self) -> Option<FeedbackValue> {
pub fn poll_if_control_enabled(&mut self, context: ControlContext) -> Option<FeedbackValue> {
if !self.control_is_effectively_on() {
return None;
}
Expand All @@ -352,7 +352,7 @@ impl MainMapping {
// firing triggered by a timer.
// Be graceful here.
// TODO-medium In future we could display some kind of small unintrusive error message.
let _ = target.control(final_value);
let _ = target.control(final_value, context);
self.feedback_after_control_if_unsupported_by_target(target)
}

Expand All @@ -364,6 +364,7 @@ impl MainMapping {
&mut self,
value: ControlValue,
options: ControlOptions,
context: ControlContext,
) -> Option<FeedbackValue> {
if !self.control_is_effectively_on() {
return None;
Expand All @@ -382,7 +383,7 @@ impl MainMapping {
}
// Be graceful here.
// TODO-medium In future we could display some kind of small unintrusive error message.
let _ = target.control(v);
let _ = target.control(v, context);
self.feedback_after_control_if_unsupported_by_target(target)
} else {
// The target value was not changed. If `send_feedback_after_control` is enabled, we
Expand Down Expand Up @@ -986,10 +987,10 @@ impl RealearnTarget for CompoundMappingTarget {
}
}

fn control(&self, value: ControlValue) -> Result<(), &'static str> {
fn control(&self, value: ControlValue, context: ControlContext) -> Result<(), &'static str> {
use CompoundMappingTarget::*;
match self {
Reaper(t) => t.control(value),
Reaper(t) => t.control(value, context),
Virtual(_) => Err("not supported for virtual targets"),
}
}
Expand Down
2 changes: 1 addition & 1 deletion main/src/domain/real_time_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::domain::{
NormalMainTask, PartialControlMatch, RealTimeMapping, ReaperTarget, SendMidiDestination,
VirtualSourceValue,
};
use helgoboss_learn::{ControlValue, MidiSourceValue, RawMidiEvent, RawMidiPattern};
use helgoboss_learn::{ControlValue, MidiSourceValue, RawMidiEvent};
use helgoboss_midi::{
Channel, ControlChange14BitMessage, ControlChange14BitMessageScanner, DataEntryByteOrder,
ParameterNumberMessage, PollingParameterNumberMessageScanner, RawShortMessage, ShortMessage,
Expand Down
10 changes: 8 additions & 2 deletions main/src/domain/realearn_target.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::domain::TargetCharacter;
use crate::domain::{FeedbackAudioHookTask, FeedbackOutput, RealTimeSender, TargetCharacter};
use helgoboss_learn::{ControlValue, UnitValue};

pub trait RealearnTarget {
Expand Down Expand Up @@ -37,6 +37,12 @@ pub trait RealearnTarget {
fn step_size_unit(&self) -> &'static str;
/// Formats the value completely (including a possible unit).
fn format_value(&self, value: UnitValue) -> String;
fn control(&self, value: ControlValue) -> Result<(), &'static str>;
fn control(&self, value: ControlValue, context: ControlContext) -> Result<(), &'static str>;
fn can_report_current_value(&self) -> bool;
}

#[derive(Copy, Clone, Debug)]
pub struct ControlContext<'a> {
pub feedback_audio_hook_task_sender: &'a RealTimeSender<FeedbackAudioHookTask>,
pub feedback_output: Option<FeedbackOutput>,
}
38 changes: 34 additions & 4 deletions main/src/domain/reaper_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ use crate::domain::ui_util::{
parse_from_symmetric_percentage, parse_unit_value_from_percentage,
};
use crate::domain::{
handle_exclusivity, AdditionalFeedbackEvent, BackboneState, HierarchyEntry,
HierarchyEntryProvider, RealearnTarget,
handle_exclusivity, AdditionalFeedbackEvent, BackboneState, ControlContext,
FeedbackAudioHookTask, FeedbackOutput, HierarchyEntry, HierarchyEntryProvider, MidiDestination,
RealearnTarget,
};
use std::convert::TryInto;
use std::num::NonZeroU32;
Expand Down Expand Up @@ -683,7 +684,7 @@ impl RealearnTarget for ReaperTarget {
}
}

fn control(&self, value: ControlValue) -> Result<(), &'static str> {
fn control(&self, value: ControlValue, context: ControlContext) -> Result<(), &'static str> {
use ControlValue::*;
use ReaperTarget::*;
match self {
Expand Down Expand Up @@ -1086,7 +1087,36 @@ impl RealearnTarget for ReaperTarget {
},
);
}
SendMidi { .. } => return Err("SendMidi is handled in other ways"),
SendMidi {
pattern,
destination,
} => {
// We arrive here only if controlled via OSC. Sending MIDI in response to incoming
// MIDI messages is handled directly in the real-time processor.
let raw_midi_event = pattern.to_concrete_midi_event(value.as_absolute()?);
match *destination {
SendMidiDestination::FxOutput => {
return Err("OSC => MIDI FX output not supported");
}
SendMidiDestination::FeedbackOutput => {
let feedback_output =
context.feedback_output.ok_or("no feedback output set")?;
if let FeedbackOutput::Midi(MidiDestination::Device(dev_id)) =
feedback_output
{
let _ = context
.feedback_audio_hook_task_sender
.send(FeedbackAudioHookTask::SendMidi(
dev_id,
Box::new(raw_midi_event),
))
.unwrap();
} else {
return Err("feedback output is not a MIDI device");
}
}
}
}
};
Ok(())
}
Expand Down
4 changes: 2 additions & 2 deletions main/src/infrastructure/plugin/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,8 +535,8 @@ impl App {
});
}

pub fn feedback_audio_hook_task_sender(&self) -> RealTimeSender<FeedbackAudioHookTask> {
self.feedback_audio_hook_task_sender.clone()
pub fn feedback_audio_hook_task_sender(&self) -> &RealTimeSender<FeedbackAudioHookTask> {
&self.feedback_audio_hook_task_sender
}

pub fn additional_feedback_event_sender(
Expand Down
2 changes: 1 addition & 1 deletion main/src/infrastructure/plugin/realearn_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ impl RealearnPlugin {
control_main_task_receiver,
normal_real_time_task_sender,
feedback_real_time_task_sender,
App::get().feedback_audio_hook_task_sender(),
App::get().feedback_audio_hook_task_sender().clone(),
App::get().additional_feedback_event_sender(),
App::get().instance_orchestration_event_sender(),
App::get().osc_feedback_task_sender(),
Expand Down
29 changes: 21 additions & 8 deletions main/src/infrastructure/ui/mapping_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use crate::application::{
VirtualFxParameterType, VirtualFxType, VirtualTrackType, WeakSession,
};
use crate::core::Global;
use crate::domain::{control_element_domains, SendMidiDestination};
use crate::domain::{control_element_domains, ControlContext, FeedbackOutput, SendMidiDestination};
use crate::domain::{
get_non_present_virtual_route_label, get_non_present_virtual_track_label,
resolve_track_route_by_index, ActionInvocationType, CompoundMappingTarget,
Expand All @@ -48,6 +48,7 @@ use crate::domain::{
};
use itertools::Itertools;

use crate::infrastructure::plugin::App;
use crate::infrastructure::ui::util::open_in_browser;
use std::collections::HashMap;
use std::time::Duration;
Expand Down Expand Up @@ -4805,8 +4806,10 @@ impl View for MappingPanel {
self.write(|p| p.update_mode_max_jump_from_slider(s));
}
s if s == sliders.target_value => {
if let Ok(Some(t)) = self.read(|p| p.real_target()) {
update_target_value(&t, s.slider_unit_value());
if let Ok((Some(t), feedback_output)) =
self.read(|p| (p.real_target(), p.session.feedback_output()))
{
update_target_value(&t, s.slider_unit_value(), feedback_output);
}
}
_ => unreachable!(),
Expand Down Expand Up @@ -4863,14 +4866,14 @@ impl View for MappingPanel {
view.write(|p| p.handle_target_line_4_edit_control_change())
}
root::ID_TARGET_VALUE_EDIT_CONTROL => {
let (target, value) = view.write(|p| {
let (target, value, feedback_output) = view.write(|p| {
let value = p
.get_value_from_target_edit_control(root::ID_TARGET_VALUE_EDIT_CONTROL)
.unwrap_or(UnitValue::MIN);
(p.real_target(), value)
(p.real_target(), value, p.session.feedback_output())
});
if let Some(t) = target {
update_target_value(&t, value);
update_target_value(&t, value, feedback_output);
}
}
_ => return false,
Expand Down Expand Up @@ -4946,9 +4949,19 @@ enum PositiveOrSymmetricUnitValue {
Symmetric(SoftSymmetricUnitValue),
}

fn update_target_value(target: &CompoundMappingTarget, value: UnitValue) {
fn update_target_value(
target: &CompoundMappingTarget,
value: UnitValue,
feedback_output: Option<FeedbackOutput>,
) {
// If it doesn't work in some cases, so what.
let _ = target.control(ControlValue::Absolute(value));
let _ = target.control(
ControlValue::Absolute(value),
ControlContext {
feedback_audio_hook_task_sender: App::get().feedback_audio_hook_task_sender(),
feedback_output,
},
);
}

fn group_mappings_by_virtual_control_element<'a>(
Expand Down

0 comments on commit cc87a3c

Please sign in to comment.