Skip to content

Commit

Permalink
#160 Add dynamic track type WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
helgoboss committed Mar 8, 2021
1 parent ec0c218 commit 28e8238
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 52 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions main/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ edit = { git = "https://github.com/helgoboss/edit", branch = "realearn" }
serde_yaml = "0.8.17"
# For parsing hexadecimal data notation to byte vector (for system-exclusive lifecycle MIDI messages)
hex = "0.4.2"
# For evaluation of <Dynamic> formulas
fasteval = { version = "0.2.4", default-features = false }

[target.'cfg(windows)'.dependencies]
# For detecting the Windows version (to determine whether special charactes can be displayed)
Expand Down
41 changes: 16 additions & 25 deletions main/src/application/target_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ use serde::{Deserialize, Serialize};
use crate::application::VirtualControlElementType;
use crate::domain::{
find_bookmark, get_fx, get_fx_chain, get_fx_param, get_track_send, ActionInvocationType,
CompoundMappingTarget, FxAnchor, FxDescriptor, ProcessorContext, ReaperTarget, SoloBehavior,
TouchedParameterType, TrackDescriptor, TrackExclusivity, TransportAction,
UnresolvedCompoundMappingTarget, UnresolvedReaperTarget, VirtualControlElement, VirtualFx,
VirtualTarget, VirtualTrack,
CompoundMappingTarget, ExpressionEvaluator, FxAnchor, FxDescriptor, ProcessorContext,
ReaperTarget, SoloBehavior, TouchedParameterType, TrackDescriptor, TrackExclusivity,
TransportAction, UnresolvedCompoundMappingTarget, UnresolvedReaperTarget,
VirtualControlElement, VirtualFx, VirtualTarget, VirtualTrack,
};
use serde_repr::*;
use std::borrow::Cow;

use fasteval::Compiler;
use reaper_medium::BookmarkId;
use std::fmt;
use std::fmt::{Display, Formatter};
Expand All @@ -45,6 +46,7 @@ pub struct TargetModel {
pub track_id: Prop<Option<Guid>>,
pub track_name: Prop<String>,
pub track_index: Prop<u32>,
pub track_expression: Prop<String>,
pub enable_only_if_track_selected: Prop<bool>,
// # For track FX targets
pub fx: Prop<Option<VirtualFx>>,
Expand Down Expand Up @@ -82,6 +84,7 @@ impl Default for TargetModel {
track_id: prop(None),
track_name: prop("".to_owned()),
track_index: prop(0),
track_expression: prop("".to_owned()),
enable_only_if_track_selected: prop(false),
fx: prop(None),
enable_only_if_fx_has_focus: prop(false),
Expand Down Expand Up @@ -254,6 +257,7 @@ impl TargetModel {
.merge(self.track_id.changed())
.merge(self.track_name.changed())
.merge(self.track_index.changed())
.merge(self.track_expression.changed())
.merge(self.enable_only_if_track_selected.changed())
.merge(self.fx.changed())
.merge(self.enable_only_if_fx_has_focus.changed())
Expand All @@ -280,10 +284,14 @@ impl TargetModel {
ById => VirtualTrack::ById(self.track_id.get()?),
ByName => VirtualTrack::ByName(self.track_name.get_ref().clone()),
ByIndex => VirtualTrack::ByIndex(self.track_index.get()),
// TODO-high The unwrap is not cool
ByIdOrName => {
VirtualTrack::ByIdOrName(self.track_id.get()?, self.track_name.get_ref().clone())
}
Dynamic => {
let evaluator =
ExpressionEvaluator::compile(self.track_expression.get_ref()).ok()?;
VirtualTrack::Dynamic(evaluator)
}
};
Some(track)
}
Expand Down Expand Up @@ -965,6 +973,8 @@ pub enum VirtualTrackType {
This,
#[display(fmt = "<Selected>")]
Selected,
#[display(fmt = "<Dynamic>")]
Dynamic,
#[display(fmt = "<Master>")]
Master,
#[display(fmt = "By ID")]
Expand Down Expand Up @@ -1021,29 +1031,10 @@ impl VirtualTrackType {
ById(_) => VirtualTrackType::ById,
ByName(_) => VirtualTrackType::ByName,
ByIndex(_) => VirtualTrackType::ByIndex,
Dynamic(_) => VirtualTrackType::Dynamic,
}
}

pub fn to_virtual_track(self, track: Track) -> Result<VirtualTrack, &'static str> {
use VirtualTrackType::*;
let get_name = || {
track
.name()
.map(|n| n.into_string())
.ok_or("track must have name")
};
let virtual_track = match self {
This => VirtualTrack::This,
Selected => VirtualTrack::Selected,
Master => VirtualTrack::Master,
ById => VirtualTrack::ById(*track.guid()),
ByName => VirtualTrack::ByName(get_name()?),
ByIndex => VirtualTrack::ByIndex(track.index().ok_or("track must have index")?),
ByIdOrName => VirtualTrack::ByIdOrName(*track.guid(), get_name()?),
};
Ok(virtual_track)
}

pub fn refers_to_project(&self) -> bool {
use VirtualTrackType::*;
matches!(self, ByIdOrName | ById)
Expand Down
27 changes: 8 additions & 19 deletions main/src/domain/mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ impl MappingExtension {
#[derive(Debug)]
pub struct MainMapping {
core: MappingCore,
unresolved_target: Option<UnresolvedCompoundMappingTarget>,
activation_condition_1: ActivationCondition,
activation_condition_2: ActivationCondition,
is_active_1: bool,
Expand All @@ -108,11 +109,11 @@ impl MainMapping {
id,
source,
mode,
unresolved_target,
target: None,
options,
time_of_last_control: None,
},
unresolved_target,
activation_condition_1,
activation_condition_2,
is_active_1: false,
Expand Down Expand Up @@ -196,7 +197,7 @@ impl MainMapping {

pub fn needs_refresh_when_target_touched(&self) -> bool {
matches!(
self.core.unresolved_target,
self.unresolved_target,
Some(UnresolvedCompoundMappingTarget::Reaper(
UnresolvedReaperTarget::LastTouched
))
Expand All @@ -205,7 +206,7 @@ impl MainMapping {

pub fn wants_to_be_informed_about_beat_changes(&self) -> bool {
matches!(
self.core.unresolved_target,
self.unresolved_target,
Some(UnresolvedCompoundMappingTarget::Reaper(
UnresolvedReaperTarget::GoToBookmark { .. }
))
Expand All @@ -214,7 +215,7 @@ impl MainMapping {

pub fn refresh_target(&mut self, context: &ProcessorContext) -> Option<ActivationChange> {
let was_active_before = self.core.options.target_is_active;
let (target, is_active) = match self.core.unresolved_target.as_ref() {
let (target, is_active) = match self.unresolved_target.as_ref() {
None => (None, false),
Some(t) => match t.resolve(context).ok() {
None => (None, false),
Expand Down Expand Up @@ -467,22 +468,12 @@ impl RealTimeMapping {
}
}

pub fn target(&self) -> Option<&UnresolvedCompoundMappingTarget> {
self.core.unresolved_target.as_ref()
}

pub fn has_virtual_target(&self) -> bool {
matches!(
self.target(),
Some(UnresolvedCompoundMappingTarget::Virtual(_))
)
matches!(&self.core.target, Some(CompoundMappingTarget::Virtual(_)))
}

pub fn has_reaper_target(&self) -> bool {
matches!(
self.core.unresolved_target,
Some(UnresolvedCompoundMappingTarget::Reaper(_))
)
matches!(&self.core.target, Some(CompoundMappingTarget::Reaper(_)))
}

pub fn consumes(&self, msg: RawShortMessage) -> bool {
Expand Down Expand Up @@ -517,8 +508,6 @@ pub struct MappingCore {
id: MappingId,
source: CompoundMappingSource,
mode: Mode,
// TODO-medium Take targets out of MappingCore because RealTimeMapping doesn't need most of it!
unresolved_target: Option<UnresolvedCompoundMappingTarget>,
target: Option<CompoundMappingTarget>,
options: ProcessorMappingOptions,
time_of_last_control: Option<Instant>,
Expand Down Expand Up @@ -594,7 +583,7 @@ pub enum SourceValue {
Osc(OscMessage),
}

#[derive(Clone, PartialEq, Debug)]
#[derive(Debug)]
pub enum UnresolvedCompoundMappingTarget {
Reaper(UnresolvedReaperTarget),
Virtual(VirtualTarget),
Expand Down
58 changes: 53 additions & 5 deletions main/src/domain/unresolved_reaper_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::domain::{
TouchedParameterType, TrackExclusivity, TransportAction,
};
use derive_more::{Display, Error};
use fasteval::{Compiler, Evaler, Instruction, Slab};
use reaper_high::{
Action, BookmarkType, FindBookmarkResult, Fx, FxChain, FxParameter, Guid, Project, Reaper,
Track, TrackSend,
Expand All @@ -15,7 +16,7 @@ use std::fmt;
use std::num::NonZeroU32;
use std::rc::Rc;

#[derive(Clone, Debug, PartialEq)]
#[derive(Debug)]
pub enum UnresolvedReaperTarget {
Action {
action: Action,
Expand Down Expand Up @@ -339,42 +340,78 @@ pub fn get_track_send(
Ok(send)
}

#[derive(Clone, Debug, PartialEq)]
#[derive(Debug)]
pub struct TrackDescriptor {
pub track: VirtualTrack,
pub enable_only_if_track_selected: bool,
}

#[derive(Clone, Debug, PartialEq)]
#[derive(Debug)]
pub struct FxDescriptor {
pub track_descriptor: TrackDescriptor,
pub fx: VirtualFx,
pub enable_only_if_fx_has_focus: bool,
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Debug)]
pub enum VirtualTrack {
/// Current track (the one which contains the ReaLearn instance).
This,
/// Currently selected track.
Selected,
/// Based on parameter values.
Dynamic(ExpressionEvaluator),
/// Master track.
Master,
/// Particular.
ById(Guid),
/// Particular.
ByName(String),
/// Particular.
ByIndex(u32),
/// This is the old default for targeting a particular track and it exists solely for backward
/// compatibility.
ByIdOrName(Guid, String),
}

#[derive(Debug)]
pub struct ExpressionEvaluator {
slab: Slab,
instruction: Instruction,
}

impl ExpressionEvaluator {
pub fn compile(expression: &str) -> Result<ExpressionEvaluator, Box<dyn std::error::Error>> {
let parser = fasteval::Parser::new();
let mut slab = fasteval::Slab::new();
let instruction = parser
.parse(expression, &mut slab.ps)?
.from(&slab.ps)
.compile(&slab.ps, &mut slab.cs);
let evaluator = Self { slab, instruction };
Ok(evaluator)
}

pub fn evaluate(&self) -> f64 {
self.evaluate_internal().unwrap_or_default()
}

fn evaluate_internal(&self) -> Result<f64, fasteval::Error> {
use fasteval::eval_compiled_ref;
let mut ns = fasteval::EmptyNamespace;
let val = eval_compiled_ref!(&self.instruction, &self.slab, &mut ns);
Ok(val)
}
}

impl fmt::Display for VirtualTrack {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
use VirtualTrack::*;
match self {
This => f.write_str("<This>"),
Selected => f.write_str("<Selected>"),
Master => f.write_str("<Master>"),
Dynamic(_) => f.write_str("<Dynamic>"),
ByIdOrName(id, name) => write!(f, "{} or \"{}\"", id.to_string_without_braces(), name),
ById(id) => write!(f, "{}", id.to_string_without_braces()),
ByName(name) => write!(f, "\"{}\"", name),
Expand Down Expand Up @@ -425,6 +462,17 @@ impl VirtualTrack {
Selected => project
.first_selected_track(MasterTrackBehavior::IncludeMasterTrack)
.ok_or(TrackResolveError::NoTrackSelected)?,
Dynamic(evaluator) => {
let result = evaluator.evaluate();
let index = result.max(0.0) as u32;
project
.track_by_index(index)
.ok_or(TrackResolveError::TrackNotFound {
guid: None,
name: None,
index: Some(index),
})?
}
Master => project.master_track(),
ByIdOrName(guid, name) => {
let t = project.track_by_guid(guid);
Expand Down Expand Up @@ -678,7 +726,7 @@ pub fn get_fx(context: &ProcessorContext, descriptor: &FxDescriptor) -> Result<F
// resync the FX whenever something has changed anyway. But
// for monitoring FX it could still be good (which we don't get notified
// about unfortunately).
if descriptor.track_descriptor.track == VirtualTrack::Selected {
if matches!(descriptor.track_descriptor.track, VirtualTrack::Selected) {
FxAnchor::Index(*index)
} else {
anchor.clone()
Expand Down
1 change: 1 addition & 0 deletions main/src/infrastructure/data/target_model_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ fn serialize_track(virtual_track: &VirtualTrack) -> TrackData {
name: None,
index: Some(*index),
},
Dynamic(_) => todo!(),
}
}

Expand Down
17 changes: 14 additions & 3 deletions main/src/infrastructure/ui/mapping_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1115,17 +1115,24 @@ impl<'a> MutableMappingPanel<'a> {
match self.target_category() {
TargetCategory::Reaper => match self.reaper_target_type() {
t if t.supports_track() => match self.mapping.target_model.track_type.get() {
VirtualTrackType::Dynamic => {
let expression = control.text().unwrap_or_default();
self.mapping.target_model.track_expression.set(expression);
}
VirtualTrackType::ByName => {
let name = control.text().unwrap_or_default();
self.mapping.target_model.track_name.set(name);
}
VirtualTrackType::ByIndex => {
let index = control
let position: i32 = control
.text()
.unwrap_or_default()
.parse()
.unwrap_or_default();
self.mapping.target_model.track_index.set(index);
self.mapping
.target_model
.track_index
.set(position.max(0) as _);
}
_ => {}
},
Expand Down Expand Up @@ -1820,7 +1827,11 @@ impl<'a> ImmutableMappingPanel<'a> {
TargetCategory::Reaper => match self.reaper_target_type() {
t if t.supports_track() => {
let text = match self.target.track_type.get() {
VirtualTrackType::ByIndex => self.target.track_index.get().to_string(),
VirtualTrackType::Dynamic => self.target.track_expression.get_ref().clone(),
VirtualTrackType::ByIndex => {
let index = self.target.track_index.get();
(index + 1).to_string()
}
VirtualTrackType::ByName => self.target.track_name.get_ref().clone(),
_ => {
control.hide();
Expand Down

0 comments on commit 28e8238

Please sign in to comment.