Vendor helgoboss/helgobox (ReaLearn) as basis for custom UI fork

Stripped upstream git history; starting point for replacing the native
SWELL/Win32 mapping UI with something more suited to bulk editing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Paul Lipscomb
2026-07-15 17:38:29 -04:00
parent 583ed77a67
commit e58f06d9fa
2232 changed files with 685575 additions and 1 deletions
@@ -0,0 +1,120 @@
# Created by https://www.gitignore.io/api/rust,clion+all
# Edit at https://www.gitignore.io/?templates=rust,clion+all
### CLion+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### CLion+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
### Rust ###
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# End of https://www.gitignore.io/api/rust,clion+all
# Created by https://www.gitignore.io/api/visualstudiocode
# Edit at https://www.gitignore.io/?templates=visualstudiocode
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
# End of https://www.gitignore.io/api/visualstudiocode
@@ -0,0 +1,43 @@
[package]
name = "helgoboss-learn"
version = "0.1.0"
authors = ["Benjamin Klum <benjamin.klum@helgoboss.org>"]
edition = "2021"
publish = false
[dependencies]
base.workspace = true
helgoboss-midi.workspace = true
tracing.workspace = true
num.workspace = true
num_enum.workspace = true
approx.workspace = true
derive_more.workspace = true
serde.workspace = true
serde_repr.workspace = true
# For being able to (de)serialize using FromStr
serde_with.workspace = true
lazycell.workspace = true
# For being able to exclude some fields from the hash function (necessary for ignoring already learned sources)
derivative.workspace = true
# For OSC
rosc.workspace = true
# For using the Bpm type
reaper-common-types.workspace = true
# For letting the here defined raw MIDI data structure implement the REAPER MIDI event type
reaper-low = { workspace = true, optional = true }
# For RgbColor type conversion
image = { workspace = true, default-features = false }
# For tokenizing sys-ex patterns
logos.workspace = true
# For easy error types
thiserror.workspace = true
partial-min-max = "0.4.0"
nom.workspace = true
regex.workspace = true
once_cell.workspace = true
# For convenient converting of OSC feedback arg prop to enum variant and back
strum.workspace = true
serde_json.workspace = true
# For making consumers being able to use some newtypes as atomics
bytemuck = { workspace = true, features = ["derive"] }
@@ -0,0 +1,12 @@
# helgoboss-learn
Rust crate that provides DAW-agnostic MIDI-learn functionality.
## Status
At the moment, development of this crate is still closely coupled to [ReaLearn](https://github.com/helgoboss/realearn),
currently the only downstream crate. Therefore you won't find it on [crates.io](https://crates.io/) or
[docs.rs](https://docs.rs/).
However, it's designed to be completely independent of ReaLearn and REAPER. So if in future another potential
downstream crate pops up, it will be easy to make *helgoboss-learn* and independent library.
@@ -0,0 +1,4 @@
max_width = 100
#comment_width = 100
#wrap_comments = true
#version = "Two"
@@ -0,0 +1,781 @@
use crate::{
ControlType, DiscreteIncrement, Fraction, Interval, IntervalMatchResult, MinIsMaxBehavior,
Transformation, TransformationInput, TransformationInputContext, TransformationInputEvent,
TransformationInstruction, UnitIncrement, UnitValue, BASE_EPSILON,
};
use num_enum::TryFromPrimitive;
// Use once_cell::sync::Lazy instead of std::sync::LazyLock to be able to build with Rust 1.77.2 (to stay Win7-compatible)
use once_cell::sync::Lazy as LazyLock;
use std::fmt::{Display, Formatter};
use std::ops::Sub;
use std::time::{Duration, Instant};
/// The timestamp is intended to be used for things like takeover modes. Ideally, the event
/// time should be captured when the event occurs but it's also okay to do that somewhat later
/// in the same callstack because event processing within the same thread happens very fast.
/// Most importantly, if the event is sent to another thread, then the time should be captured
/// *before* the event leaves the thread and saved. That allows more accurate processing in the
/// destination thread.
pub trait AbstractTimestamp: Copy + Sub<Output = Duration> + std::fmt::Debug {
fn duration(&self) -> Duration;
}
/// A timestamp that does nothing and takes no space.
#[derive(Copy, Clone, Debug, Default)]
pub struct NoopTimestamp;
impl AbstractTimestamp for NoopTimestamp {
fn duration(&self) -> Duration {
Duration::ZERO
}
}
impl Sub for NoopTimestamp {
type Output = Duration;
fn sub(self, _: Self) -> Duration {
Duration::ZERO
}
}
impl AbstractTimestamp for Instant {
fn duration(&self) -> Duration {
static INSTANT: LazyLock<Instant> = LazyLock::new(Instant::now);
self.saturating_duration_since(*INSTANT)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ControlEvent<P, T: AbstractTimestamp> {
payload: P,
timestamp: T,
}
impl<P: Display, T: AbstractTimestamp + Display> Display for ControlEvent<P, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{} at {}", &self.payload, self.timestamp)
}
}
impl<P, T: AbstractTimestamp> ControlEvent<P, T> {
/// Creates an event.
pub fn new(payload: P, timestamp: T) -> Self {
Self { timestamp, payload }
}
/// Returns the timestamp of this event.
pub fn timestamp(&self) -> T {
self.timestamp
}
/// Returns the payload of this event.
pub fn payload(&self) -> P
where
P: Copy,
{
self.payload
}
/// Consumes this event and returns the payload.
pub fn into_payload(self) -> P {
self.payload
}
/// Replaces the payload of this event but keeps the timestamp.
pub fn with_payload<O>(&self, payload: O) -> ControlEvent<O, T> {
ControlEvent {
timestamp: self.timestamp,
payload,
}
}
/// Transforms the payload of this event.
pub fn map_payload<O>(self, map: impl FnOnce(P) -> O) -> ControlEvent<O, T> {
let transformed_payload = map(self.payload);
ControlEvent {
timestamp: self.timestamp,
payload: transformed_payload,
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, TryFromPrimitive)]
#[repr(u8)]
pub enum ControlValueKind {
#[default]
AbsoluteContinuous = 0,
RelativeDiscrete = 1,
RelativeContinuous = 2,
AbsoluteDiscrete = 3,
}
/// Value coming from a source (e.g. a MIDI source) which is supposed to control something.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum ControlValue {
/// Absolute value that represents a percentage (e.g. fader position on the scale from lowest to
/// highest, knob position on the scale from closed to fully opened, key press on the scale from
/// not pressed to pressed with full velocity, key release).
AbsoluteContinuous(UnitValue),
/// Relative increment that represents a number of increments/decrements.
RelativeDiscrete(DiscreteIncrement),
/// Relative increment that represents a continuous adjustment.
RelativeContinuous(UnitIncrement),
/// Absolute value that is capable of retaining the original discrete value, e.g. the played
/// note number, without immediately converting it into a UnitValue and thereby losing that
/// information - which is important for the new "Discrete" mode.
AbsoluteDiscrete(Fraction),
}
impl Display for ControlValue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ControlValue::AbsoluteContinuous(v) => v.fmt(f),
ControlValue::AbsoluteDiscrete(v) => v.fmt(f),
ControlValue::RelativeContinuous(v) => v.fmt(f),
ControlValue::RelativeDiscrete(v) => v.fmt(f),
}
}
}
impl ControlValue {
/// Convenience method for creating an absolute control value
pub fn absolute_continuous(number: f64) -> ControlValue {
ControlValue::AbsoluteContinuous(UnitValue::new(number))
}
/// Convenience method for creating a discrete absolute control value
pub fn absolute_discrete(actual: u32, max: u32) -> ControlValue {
ControlValue::AbsoluteDiscrete(Fraction::new(actual, max))
}
/// Convenience method for creating a relative control value
pub fn relative(increment: i32) -> ControlValue {
ControlValue::RelativeDiscrete(DiscreteIncrement::new(increment))
}
pub fn from_absolute(value: AbsoluteValue) -> ControlValue {
match value {
AbsoluteValue::Continuous(v) => Self::AbsoluteContinuous(v),
AbsoluteValue::Discrete(f) => Self::AbsoluteDiscrete(f),
}
}
pub fn from_relative(increment: Increment) -> ControlValue {
match increment {
Increment::Continuous(i) => Self::RelativeContinuous(i),
Increment::Discrete(i) => Self::RelativeDiscrete(i),
}
}
/// Extracts the unit value if this is an absolute control value.
pub fn to_unit_value(self) -> Result<UnitValue, &'static str> {
match self {
ControlValue::AbsoluteContinuous(v) => Ok(v),
ControlValue::AbsoluteDiscrete(f) => Ok(f.to_unit_value()),
_ => Err("control value is not absolute"),
}
}
/// Extracts the discrete value if this is an absolute control value.
///
/// The `value_count` is only used if this value is a unit value, in order to transform it into a discrete value.
pub fn to_discrete_value(self, value_count: u32) -> Result<Fraction, &'static str> {
match self {
ControlValue::AbsoluteContinuous(v) => {
if value_count == 0 {
return Ok(Fraction::new_max(0));
}
let actual = (v.get() * (value_count - 1) as f64).round() as u32;
Ok(Fraction::new(actual, value_count))
}
ControlValue::AbsoluteDiscrete(f) => Ok(f),
_ => Err("control value is not absolute"),
}
}
/// Extracts an absolute value if this is an absolute control value.
pub fn to_absolute_value(self) -> Result<AbsoluteValue, &'static str> {
match self {
ControlValue::AbsoluteContinuous(v) => Ok(AbsoluteValue::Continuous(v)),
ControlValue::AbsoluteDiscrete(f) => Ok(AbsoluteValue::Discrete(f)),
_ => Err("control value is not absolute"),
}
}
/// Extracts the discrete increment if this is a relative control value.
pub fn as_discrete_increment(self) -> Result<DiscreteIncrement, &'static str> {
match self {
ControlValue::RelativeDiscrete(v) => Ok(v),
_ => Err("control value is not relative"),
}
}
pub fn inverse(self) -> ControlValue {
match self {
ControlValue::AbsoluteContinuous(v) => ControlValue::AbsoluteContinuous(v.inverse()),
ControlValue::RelativeDiscrete(v) => ControlValue::RelativeDiscrete(v.inverse()),
ControlValue::RelativeContinuous(v) => ControlValue::RelativeContinuous(v.inverse()),
ControlValue::AbsoluteDiscrete(v) => ControlValue::AbsoluteDiscrete(v.inverse()),
}
}
pub fn to_absolute_continuous(self) -> Result<ControlValue, &'static str> {
match self {
ControlValue::AbsoluteContinuous(v) => Ok(ControlValue::AbsoluteContinuous(v)),
ControlValue::AbsoluteDiscrete(v) => {
Ok(ControlValue::AbsoluteContinuous(v.to_unit_value()))
}
ControlValue::RelativeContinuous(_) | ControlValue::RelativeDiscrete(_) => {
Err("relative values can't be normalized")
}
}
}
pub fn is_on(self) -> bool {
self.to_unit_value()
.map(|uv| !uv.is_zero())
.unwrap_or(false)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum AbsoluteValue {
Continuous(UnitValue),
Discrete(Fraction),
}
impl AbsoluteValue {
pub fn from_bool(on: bool) -> Self {
if on {
AbsoluteValue::Continuous(UnitValue::MAX)
} else {
AbsoluteValue::Continuous(UnitValue::MIN)
}
}
pub fn is_on(&self) -> bool {
!self.is_zero()
}
pub fn continuous_value(self) -> Option<UnitValue> {
match self {
AbsoluteValue::Continuous(v) => Some(v),
AbsoluteValue::Discrete(_) => None,
}
}
pub fn discrete_value(self) -> Option<Fraction> {
match self {
AbsoluteValue::Continuous(_) => None,
AbsoluteValue::Discrete(f) => Some(f),
}
}
pub fn to_unit_value(self) -> UnitValue {
match self {
AbsoluteValue::Continuous(v) => v,
AbsoluteValue::Discrete(v) => v.to_unit_value(),
}
}
pub fn to_continuous_value(self) -> AbsoluteValue {
AbsoluteValue::Continuous(self.to_unit_value())
}
pub fn is_zero(&self) -> bool {
match self {
AbsoluteValue::Continuous(v) => v.is_zero(),
AbsoluteValue::Discrete(v) => v.is_zero(),
}
}
pub fn is_continuous(&self) -> bool {
matches!(self, AbsoluteValue::Continuous(_))
}
pub fn matches_tolerant(
self,
continuous_interval: &Interval<UnitValue>,
discrete_interval: &Interval<u32>,
use_discrete_processing: bool,
epsilon: f64,
) -> IntervalMatchResult {
match self {
AbsoluteValue::Continuous(v) => continuous_interval.value_matches_tolerant(v, epsilon),
AbsoluteValue::Discrete(v) => {
if use_discrete_processing {
discrete_interval.value_matches(v.actual())
} else {
continuous_interval.value_matches_tolerant(v.to_unit_value(), epsilon)
}
}
}
}
pub fn select_appropriate_interval_min(
self,
continuous_interval: &Interval<UnitValue>,
discrete_interval: &Interval<u32>,
) -> AbsoluteValue {
use AbsoluteValue as V;
match self {
V::Continuous(_) => V::Continuous(continuous_interval.min_val()),
V::Discrete(v) => V::Discrete(v.with_actual(discrete_interval.min_val())),
}
}
pub fn select_appropriate_interval_max(
self,
continuous_interval: &Interval<UnitValue>,
discrete_interval: &Interval<u32>,
) -> AbsoluteValue {
use AbsoluteValue as V;
match self {
V::Continuous(_) => V::Continuous(continuous_interval.max_val()),
V::Discrete(v) => V::Discrete(v.with_actual(discrete_interval.max_val())),
}
}
/// Normalizes this value with regard to the given interval.
///
/// This value should be in the given interval!
///
/// - Continuous: Scales to unit interval (= scales up = decreases resolution).
/// - Discrete: Uses the interval minimum as zero.
pub fn normalize(
self,
continuous_interval: &Interval<UnitValue>,
discrete_interval: &Interval<u32>,
min_is_max_behavior: MinIsMaxBehavior,
is_discrete_mode: bool,
epsilon: f64,
) -> Self {
use AbsoluteValue as V;
match self {
V::Continuous(v) => {
let scaled = v.normalize(continuous_interval, min_is_max_behavior, epsilon);
V::Continuous(scaled)
}
V::Discrete(v) => {
if is_discrete_mode {
// Normalize without scaling.
let rooted = v.normalize(discrete_interval, min_is_max_behavior);
V::Discrete(rooted)
} else if continuous_interval.is_full() {
// Retain discreteness of value even in non-discrete mode if this is a no-op!
V::Discrete(v)
} else {
// Use scaling if we are in non-discrete mode, thereby destroying the
// value's discreteness.
let scaled = v.to_unit_value().normalize(
continuous_interval,
min_is_max_behavior,
epsilon,
);
V::Continuous(scaled)
}
}
}
}
/// Denormalizes this value with regard to the given interval.
///
/// This value should be normalized!
///
/// - Continuous: Scales from unit interval (= scales down = increases resolution).
/// - Discrete: Adds the interval minimum.
pub fn denormalize(
self,
continuous_interval: &Interval<UnitValue>,
discrete_interval: &Interval<u32>,
is_discrete_mode: bool,
discrete_max: Option<u32>,
) -> Self {
use AbsoluteValue as V;
match self {
V::Continuous(v) => {
let scaled = v.denormalize(continuous_interval);
V::Continuous(scaled)
}
V::Discrete(v) => {
if is_discrete_mode {
// Denormalize without scaling.
let unrooted = v.denormalize(discrete_interval, discrete_max);
V::Discrete(unrooted)
} else if continuous_interval.is_full() {
// Retain discreteness of value even in non-discrete mode if this is a no-op!
V::Discrete(v)
} else {
// Use scaling if we are in non-discrete mode, thereby destroying the
// value's discreteness.
let scaled = v.to_unit_value().denormalize(continuous_interval);
V::Continuous(scaled)
}
}
}
}
pub fn transform<T: Transformation>(
self,
transformation: &T,
current_target_value: Option<AbsoluteValue>,
is_discrete_mode: bool,
rel_time: Duration,
timestamp: Duration,
additional_input: T::AdditionalInput,
) -> Result<EnhancedTransformationOutput<ControlValue>, &'static str> {
use AbsoluteValue as V;
match self {
V::Continuous(v) => {
// Input value is continuous.
let current_target_value = current_target_value
.map(|t| t.to_unit_value())
.unwrap_or_default();
self.transform_continuous(
transformation,
v,
current_target_value,
rel_time,
timestamp,
additional_input,
)
}
V::Discrete(v) => {
// Input value is discrete.
let current_target_value = current_target_value
.unwrap_or_else(|| AbsoluteValue::Discrete(v.with_actual(0)));
match current_target_value {
V::Continuous(t) => {
// Target value is continuous.
self.transform_continuous(
transformation,
v.to_unit_value(),
t,
rel_time,
timestamp,
additional_input,
)
}
V::Discrete(t) => {
// Target value is also discrete.
if is_discrete_mode {
// Discrete mode.
// Transform using non-normalized rounded floating point values.
self.transform_discrete(
transformation,
v,
t,
rel_time,
timestamp,
additional_input,
)
} else {
// Continuous mode.
// Transform using normalized floating point values, thereby destroying
// the value's discreteness.
self.transform_continuous(
transformation,
v.to_unit_value(),
t.to_unit_value(),
rel_time,
timestamp,
additional_input,
)
}
}
}
}
}
}
fn transform_continuous<T: Transformation>(
self,
transformation: &T,
input_value: UnitValue,
output_value: UnitValue,
rel_time: Duration,
timestamp: Duration,
additional_input: T::AdditionalInput,
) -> Result<EnhancedTransformationOutput<ControlValue>, &'static str> {
let input = TransformationInput {
event: TransformationInputEvent {
input_value: input_value.get(),
timestamp,
},
context: TransformationInputContext {
output_value: output_value.get(),
rel_time,
},
additional_input,
};
let output = transformation.transform(input)?;
let output = EnhancedTransformationOutput {
produced_kind: output.produced_kind,
value: output.extract_control_value(None),
instruction: output.instruction,
};
Ok(output)
}
// Not currently used as discrete control not yet unlocked.
fn transform_discrete<T: Transformation>(
self,
transformation: &T,
input_value: Fraction,
output_value: Fraction,
rel_time: Duration,
timestamp: Duration,
additional_input: T::AdditionalInput,
) -> Result<EnhancedTransformationOutput<ControlValue>, &'static str> {
let input = TransformationInput {
event: TransformationInputEvent {
input_value: input_value.actual() as _,
timestamp,
},
context: TransformationInputContext {
output_value: output_value.actual() as _,
rel_time,
},
additional_input,
};
let output = transformation.transform(input)?;
let out = EnhancedTransformationOutput {
produced_kind: output.produced_kind,
value: output.extract_control_value(Some(input_value.max_val())),
instruction: output.instruction,
};
Ok(out)
}
pub fn inverse(self, new_discrete_max: Option<u32>) -> Self {
use AbsoluteValue as V;
match self {
V::Continuous(v) => Self::Continuous(v.inverse()),
// 100/100 (max 150) => 0/150
// 0/100 (max 150) => 100/150
// 100/100 (max 50) => 0/50
// 0/100 (max 50) => 50/50
V::Discrete(f) => {
let res = if let Some(new_max) = new_discrete_max {
let min_max = std::cmp::min(new_max, f.max_val());
let inversed_with_min_max = f.with_max_clamped(min_max).inverse();
inversed_with_min_max.with_max(new_max)
} else {
f.inverse()
};
Self::Discrete(res)
}
}
}
pub fn round(self, control_type: ControlType) -> Self {
use AbsoluteValue as V;
match self {
V::Continuous(v) => {
let value = round_to_nearest_discrete_value(control_type, v);
Self::Continuous(value)
}
V::Discrete(f) => Self::Discrete(f),
}
}
pub fn has_same_effect_as(self, other: AbsoluteValue) -> bool {
if let (AbsoluteValue::Discrete(f1), AbsoluteValue::Discrete(f2)) = (self, other) {
f1.actual() == f2.actual()
} else {
// We do an exact comparison here for the moment (no BASE_EPSILON tolerance).
// Reasoning: We don't know the target epsilon. It's very unlikely but maybe the
// target cares about minimal differences and then not hitting the target would be
// bad. Better hit it redundantly instead of omitting a hit that would have made a
// difference.
self.to_unit_value() == other.to_unit_value()
}
}
pub fn calc_distance_from(self, rhs: Self) -> Self {
use AbsoluteValue as V;
match (self, rhs) {
(V::Discrete(f1), V::Discrete(f2)) => {
let distance = (f2.actual() as i32 - f1.actual() as i32).unsigned_abs();
Self::Discrete(Fraction::new_max(distance))
}
_ => {
let distance = self.to_unit_value().calc_distance_from(rhs.to_unit_value());
Self::Continuous(distance)
}
}
}
pub fn is_greater_than(&self, continuous_jump_max: UnitValue, discrete_jump_max: u32) -> bool {
use AbsoluteValue as V;
match self {
V::Continuous(d) => d.get() > continuous_jump_max.get() + BASE_EPSILON,
V::Discrete(d) => d.actual() > discrete_jump_max,
}
}
pub fn is_lower_than(&self, continuous_jump_min: UnitValue, discrete_jump_min: u32) -> bool {
use AbsoluteValue as V;
match self {
V::Continuous(d) => d.get() + BASE_EPSILON < continuous_jump_min.get(),
V::Discrete(d) => d.actual() < discrete_jump_min,
}
}
}
impl Default for AbsoluteValue {
fn default() -> Self {
Self::Continuous(Default::default())
}
}
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub enum Increment {
Continuous(UnitIncrement),
Discrete(DiscreteIncrement),
}
impl Increment {
pub fn is_positive(&self) -> bool {
match self {
Increment::Continuous(i) => i.is_positive(),
Increment::Discrete(i) => i.is_positive(),
}
}
/// Returns a unit increment.
///
/// For continuous increments, this just returns the contained value.
///
/// For discrete increments, the atomic unit value is used to convert the integer into
/// a unit increment. Return `None` if the result would be zero (non-increment).
pub fn to_unit_increment(&self, atomic_unit_value: UnitValue) -> Option<UnitIncrement> {
match self {
Increment::Continuous(i) => Some(*i),
Increment::Discrete(i) => i.to_unit_increment(atomic_unit_value),
}
}
/// Returns a discrete increment.
///
/// For discrete increments, this just returns the contained value.
///
/// For continuous increments, this returns a +1 or -1 depending on the direction of the
/// increment. The actual amount is ignored.
pub fn to_discrete_increment(&self) -> DiscreteIncrement {
match self {
Increment::Continuous(i) => i.to_discrete_increment(),
Increment::Discrete(i) => *i,
}
}
pub fn inverse(&self) -> Increment {
match self {
Increment::Continuous(i) => Increment::Continuous(i.inverse()),
Increment::Discrete(i) => Increment::Discrete(i.inverse()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BASE_EPSILON;
use approx::*;
#[test]
fn normalize_comparison() {
// Given
let continuous = AbsoluteValue::Continuous(UnitValue::new(105.0 / 127.0));
let continuous_interval =
Interval::new(UnitValue::new(100.0 / 127.0), UnitValue::new(120.0 / 127.0));
let discrete = AbsoluteValue::Discrete(Fraction::new(105, 127));
let discrete_interval = Interval::new(100, 120);
// When
let continuous_normalized = continuous.normalize(
&continuous_interval,
&discrete_interval,
MinIsMaxBehavior::PreferZero,
true,
BASE_EPSILON,
);
let discrete_normalized = discrete.normalize(
&continuous_interval,
&discrete_interval,
MinIsMaxBehavior::PreferZero,
true,
BASE_EPSILON,
);
// Then
assert_abs_diff_eq!(
continuous_normalized.to_unit_value().get(),
0.25,
epsilon = BASE_EPSILON
);
assert_eq!(
discrete_normalized,
AbsoluteValue::Discrete(Fraction::new(5, 20))
);
assert_abs_diff_eq!(
discrete_normalized.to_unit_value().get(),
0.25,
epsilon = BASE_EPSILON
);
}
#[test]
fn denormalize_comparison() {
// Given
let continuous = AbsoluteValue::Continuous(UnitValue::new(105.0 / 127.0));
let continuous_interval = Interval::new(
UnitValue::new(100.0 / 1000.0),
UnitValue::new(500.0 / 1000.0),
);
let discrete = AbsoluteValue::Discrete(Fraction::new(105, 127));
let discrete_interval = Interval::new(100, 500);
// When
let continuous_normalized =
continuous.denormalize(&continuous_interval, &discrete_interval, true, Some(500));
let discrete_normalized =
discrete.denormalize(&continuous_interval, &discrete_interval, true, Some(500));
// Then
assert_abs_diff_eq!(
continuous_normalized.to_unit_value().get(),
0.4307086614173229,
epsilon = BASE_EPSILON
);
assert_eq!(
discrete_normalized,
AbsoluteValue::Discrete(Fraction::new(205, 500))
);
}
}
fn round_to_nearest_discrete_value(
control_type: ControlType,
approximate_control_value: UnitValue,
) -> UnitValue {
// round() is the right choice here vs. floor() because we don't want slight numerical
// inaccuracies lead to surprising jumps
use ControlType as T;
let step_size = match control_type {
T::AbsoluteContinuousRoundable { rounding_step_size } => rounding_step_size,
T::AbsoluteDiscrete {
atomic_step_size, ..
} => atomic_step_size,
T::AbsoluteContinuousRetriggerable
| T::AbsoluteContinuous
| T::Relative
| T::VirtualMulti
| T::VirtualButton => {
return approximate_control_value;
}
};
approximate_control_value.snap_to_grid_by_interval_size(step_size)
}
pub struct EnhancedTransformationOutput<T> {
pub produced_kind: ControlValueKind,
pub value: Option<T>,
pub instruction: Option<TransformationInstruction>,
}
@@ -0,0 +1,270 @@
use crate::{Interval, UnitIncrement, UnitValue};
use derive_more::Display;
use helgoboss_midi::U7;
use std::cmp;
use std::convert::TryFrom;
use std::fmt::{Display, Formatter};
use std::ops::Sub;
/// A positive discrete number most likely representing a step count.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Display)]
pub struct DiscreteValue(u32);
impl DiscreteValue {
/// Creates the discrete value.
pub const fn new(value: u32) -> DiscreteValue {
DiscreteValue(value)
}
/// Returns the underlying number.
pub fn get(&self) -> u32 {
self.0
}
/// Converts this discrete value to a discrete increment, either negative or positive depending
/// on the given signum. Returns `None` if this value is zero.
pub fn to_increment(self, signum: i32) -> Option<DiscreteIncrement> {
if self.is_zero() {
return None;
}
Some(unsafe { DiscreteIncrement::new_unchecked(signum * self.0 as i32) })
}
/// Returns whether this is 0.
pub fn is_zero(&self) -> bool {
self.0 == 0
}
/// Clamps this value to the given interval bounds.
pub fn clamp_to_interval(&self, interval: &Interval<DiscreteValue>) -> DiscreteValue {
DiscreteValue::new(num::clamp(
self.0,
interval.min_val().0,
interval.max_val().0,
))
}
}
impl std::str::FromStr for DiscreteValue {
type Err = &'static str;
fn from_str(source: &str) -> Result<Self, Self::Err> {
let primitive = u32::from_str(source).map_err(|_| "not a valid positive integer")?;
Ok(DiscreteValue(primitive))
}
}
impl Sub for DiscreteValue {
type Output = u32;
fn sub(self, rhs: Self) -> Self::Output {
self.0 - rhs.0
}
}
/// A discrete number representing a positive or negative increment, never 0 (otherwise it wouldn't
/// be an increment after all).
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[serde(try_from = "i32")]
pub struct DiscreteIncrement(i32);
impl Display for DiscreteIncrement {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:+}", self.0)
}
}
impl DiscreteIncrement {
pub const NEGATIVE_MIN: Self = Self(-1);
pub const POSITIVE_MIN: Self = Self(1);
/// Creates the discrete increment. Panics if the given number is 0.
pub fn new(increment: i32) -> DiscreteIncrement {
assert_ne!(increment, 0);
DiscreteIncrement(increment)
}
/// Creates the discrete increment. Panics if the given number is 0.
pub fn new_checked(increment: i32) -> Option<DiscreteIncrement> {
if increment == 0 {
return None;
}
Some(DiscreteIncrement(increment))
}
/// Checks preconditions only in debug build. Should only be used if you want to squeeze out
/// every last bit of performance and you are super sure that the number meets the
/// preconditions. This constructor is offered because it's not unlikely that a lot of those
/// values will be constructed in audio thread.
///
/// # Safety
///
/// Make sure the given increment is not zero.
pub unsafe fn new_unchecked(increment: i32) -> DiscreteIncrement {
debug_assert_ne!(increment, 0);
DiscreteIncrement(increment)
}
/// Creates an increment from the given MIDI control-change value assuming that the device
/// emitting the control-change messages uses a protocol which is called "Relative 1" in REAPER.
///
/// - 127 = decrement; 0 = none; 1 = increment
/// - 127 > value > 63 results in higher decrement step sizes (64 possible decrement step sizes)
/// - 1 < value <= 63 results in higher increment step sizes (63 possible increment step sizes)
pub fn from_encoder_1_value(value: U7) -> Result<DiscreteIncrement, &'static str> {
let value = value.get();
if value == 0 {
return Err("increment must not be zero");
}
let increment = if value <= 63 {
// Zero and increment
value as i32
} else {
// Decrement
-((128 - value) as i32)
};
Ok(unsafe { DiscreteIncrement::new_unchecked(increment) })
}
/// Creates an increment from the given MIDI control-change value assuming that the device
/// emitting the control-change messages uses a protocol which is called "Relative 2" in REAPER.
///
/// - 63 = decrement; 64 = none; 65 = increment
/// - 63 > value >= 0 results in higher decrement step sizes (64 possible decrement step sizes)
/// - 65 < value <= 127 results in higher increment step sizes (63 possible increment step
/// sizes)
pub fn from_encoder_2_value(value: U7) -> Result<DiscreteIncrement, &'static str> {
let value = value.get();
if value == 64 {
return Err("increment must not be zero");
}
let increment = if value > 64 {
// Zero and increment
(value - 64) as i32
} else {
// Decrement
-((64 - value) as i32)
};
Ok(unsafe { DiscreteIncrement::new_unchecked(increment) })
}
/// Creates an increment from the given MIDI control-change value assuming that the device
/// emitting the control-change messages uses a protocol which is called "Relative 3" in REAPER.
///
/// - 65 = decrement; 0 = none; 1 = increment
/// - 65 < value <= 127 results in higher decrement step sizes (63 possible decrement step
/// sizes)
/// - 1 < value <= 64 results in higher increment step sizes (64 possible increment step sizes)
pub fn from_encoder_3_value(value: U7) -> Result<DiscreteIncrement, &'static str> {
let value = value.get();
if value == 0 {
return Err("increment must not be zero");
}
let increment = if value <= 64 {
// Zero and increment
value as i32
} else {
// Decrement
-((value - 64) as i32)
};
Ok(unsafe { DiscreteIncrement::new_unchecked(increment) })
}
/// Clamps this increment to the given interval bounds.
pub fn clamp_to_interval(&self, interval: &Interval<DiscreteIncrement>) -> DiscreteIncrement {
// Step count interval: (-3, 4) = -3, -2, -1, 1, 2, 3, 4
// 1 => -3
// 2 => -2
// 7 => 4
// 8 => 4
// Step count interval: (4, 10) = 4, 5, 6, 7, 8, 9, 10
// 1 => 4
// 2 => 5
// 7 => 10
// 8 => 10
let positive_increment = self.0.unsigned_abs();
let min: i32 = interval.min_val().get();
let max: i32 = interval.max_val().get();
let count: u32 = if min < 0 && max > 0 {
(max - min) as u32
} else {
(max - min) as u32 + 1
};
let addend: u32 = cmp::min(positive_increment - 1, count - 1);
let sum = min + addend as i32;
let skip_zero_sum = if min < 0 && sum >= 0 { sum + 1 } else { sum };
let clamped = cmp::min(skip_zero_sum, max);
DiscreteIncrement::new(clamped)
}
/// Converts this discrete increment into a discrete value thereby "losing" its direction.
pub fn to_value(self) -> DiscreteValue {
DiscreteValue::new(self.0.unsigned_abs())
}
/// Switches the direction of this increment (makes a positive one negative and vice versa).
pub fn inverse(&self) -> DiscreteIncrement {
unsafe { DiscreteIncrement::new_unchecked(-self.0) }
}
pub fn with_direction(&self, signum: i32) -> DiscreteIncrement {
let abs = self.0.abs();
let inner = if signum >= 0 { abs } else { -abs };
DiscreteIncrement::new(inner)
}
/// Returns the underlying number.
pub fn get(&self) -> i32 {
self.0
}
/// Returns if this increment is positive.
pub fn is_positive(&self) -> bool {
self.0 >= 0
}
/// Returns the signum (-1 if it's a negative increment, otherwise +1).
pub fn signum(&self) -> i32 {
if self.is_positive() {
1
} else {
-1
}
}
/// Returns a unit increment or None in case of 0.0.
///
/// The unit increment is built by creating a multiple of the given atomic unit value (= minimum
/// step size) and clamping the result if it exceeds the unit interval.
pub fn to_unit_increment(self, atomic_unit_value: UnitValue) -> Option<UnitIncrement> {
let positive_large = self.to_value().get() as f64 * atomic_unit_value.get();
let unit_value = UnitValue::new(num::clamp(positive_large, 0.0, 1.0));
unit_value.to_increment(self.signum())
}
}
impl Sub for DiscreteIncrement {
type Output = i32;
fn sub(self, rhs: Self) -> Self::Output {
self.0 - rhs.0
}
}
impl TryFrom<i32> for DiscreteIncrement {
type Error = &'static str;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value == 0 {
return Err("zero is not an increment");
}
Ok(DiscreteIncrement::new(value))
}
}
/// Convenience method for creating an interval of discrete increments.
pub fn create_discrete_increment_interval(min: i32, max: i32) -> Interval<DiscreteIncrement> {
Interval::new(DiscreteIncrement::new(min), DiscreteIncrement::new(max))
}
@@ -0,0 +1,132 @@
use crate::{format_percentage_without_unit, AbsoluteValue, RgbColor, UnitValue};
use core::fmt;
use std::borrow::Cow;
use std::fmt::{Display, Formatter};
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum FeedbackValue<'a> {
/// Switch lights and displays completely off. Used for example if target inactive.
Off,
Numeric(NumericFeedbackValue),
// This Cow is in case the producer of the feedback value can use the borrowed value. At the
// moment this is not the case because the target API is designed to return owned strings.
Textual(TextualFeedbackValue<'a>),
Complex(ComplexFeedbackValue),
}
#[derive(Clone, Eq, PartialEq, Debug, Default)]
pub struct ComplexFeedbackValue {
pub style: FeedbackStyle,
pub value: serde_json::Value,
}
impl ComplexFeedbackValue {
pub fn new(style: FeedbackStyle, value: serde_json::Value) -> Self {
Self { style, value }
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub struct NumericFeedbackValue {
pub style: FeedbackStyle,
pub value: AbsoluteValue,
}
impl NumericFeedbackValue {
pub fn new(style: FeedbackStyle, value: AbsoluteValue) -> Self {
Self { style, value }
}
}
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct TextualFeedbackValue<'a> {
pub style: FeedbackStyle,
pub text: Cow<'a, str>,
}
impl<'a> TextualFeedbackValue<'a> {
pub fn new(style: FeedbackStyle, text: Cow<'a, str>) -> Self {
Self { style, text }
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct FeedbackStyle {
pub color: Option<RgbColor>,
pub background_color: Option<RgbColor>,
}
impl FeedbackValue<'_> {
pub fn to_numeric(&self) -> Option<NumericFeedbackValue> {
use FeedbackValue as V;
match self {
V::Off => Some(NumericFeedbackValue::new(
Default::default(),
AbsoluteValue::Continuous(UnitValue::MIN),
)),
V::Numeric(v) => Some(*v),
V::Textual(_) | V::Complex(_) => None,
}
}
pub fn to_textual(&self) -> TextualFeedbackValue {
use FeedbackValue as V;
match self {
V::Off | V::Complex(_) => Default::default(),
V::Numeric(v) => TextualFeedbackValue::new(
v.style,
Cow::Owned(format_percentage_without_unit(
v.value.to_unit_value().get(),
)),
),
V::Textual(v) => TextualFeedbackValue::new(v.style, Cow::Borrowed(v.text.as_ref())),
}
}
pub fn make_owned(self) -> FeedbackValue<'static> {
use FeedbackValue as V;
match self {
V::Off => V::Off,
V::Numeric(v) => V::Numeric(v),
V::Textual(v) => {
let new = TextualFeedbackValue::new(v.style, Cow::Owned(v.text.into_owned()));
V::Textual(new)
}
V::Complex(v) => V::Complex(v),
}
}
pub fn color(&self) -> Option<RgbColor> {
use FeedbackValue as V;
match self {
V::Off => None,
V::Numeric(v) => v.style.color,
V::Textual(v) => v.style.color,
V::Complex(v) => v.style.color,
}
}
pub fn background_color(&self) -> Option<RgbColor> {
use FeedbackValue as V;
match self {
V::Off => None,
V::Numeric(v) => v.style.background_color,
V::Textual(v) => v.style.background_color,
V::Complex(v) => v.style.background_color,
}
}
}
impl Display for FeedbackValue<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let textual = self.to_textual();
f.write_str(textual.text.as_ref())?;
if let Some(c) = textual.style.color {
write!(f, " with color {c}")?;
}
if let Some(c) = textual.style.background_color {
write!(f, " with background color {c}")?;
}
Ok(())
}
}
@@ -0,0 +1,343 @@
use crate::{DiscreteIncrement, Interval, IntervalMatchResult, MinIsMaxBehavior, UnitValue};
use std::fmt::{Display, Formatter};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct Fraction {
/// Concrete discrete value.
actual: u32,
/// Soft maximum value: Good to know in order to be able to instantly convert to a UnitValue
/// whenever we want to go absolute-continuous.
max: u32,
}
impl Display for Fraction {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}", self.actual, self.max)
}
}
impl Fraction {
pub const MIN: Fraction = Fraction::new_max(0);
pub const fn new(actual: u32, max: u32) -> Self {
Self { actual, max }
}
pub const fn new_min(max: u32) -> Self {
Self::new(0, max)
}
pub const fn new_max(max: u32) -> Self {
Self::new(max, max)
}
pub const fn actual(&self) -> u32 {
self.actual
}
pub fn actual_clamped(&self) -> u32 {
std::cmp::min(self.actual, self.max)
}
pub const fn max_val(&self) -> u32 {
self.max
}
pub fn with_actual(&self, actual: u32) -> Self {
Self::new(actual, self.max)
}
pub fn with_max(&self, max: u32) -> Self {
Self::new(self.actual, max)
}
pub fn with_max_clamped(&self, max: u32) -> Self {
Self::new(std::cmp::min(self.actual, max), max)
}
pub fn inverse(&self) -> Self {
Self {
actual: self.max - self.actual_clamped(),
max: self.max,
}
}
pub fn is_zero(&self) -> bool {
self.actual == 0
}
pub fn to_unit_value(self) -> UnitValue {
if self.max == 0 {
return UnitValue::MIN;
}
UnitValue::new(std::cmp::min(self.actual, self.max) as f64 / self.max as f64)
}
/// Tests if this value is within the given interval.
pub fn is_within_interval(&self, interval: &Interval<u32>) -> bool {
use IntervalMatchResult as R;
match interval.value_matches(self.actual) {
R::Between | R::Min | R::Max | R::MinAndMax => true,
R::Lower | R::Greater => false,
}
}
/// This value is supposed to be in the given interval.
pub fn normalize(
&self,
interval: &Interval<u32>,
min_is_max_behavior: MinIsMaxBehavior,
) -> Self {
let rooted_max = {
let unrooted_max = self.max;
let min_span = unrooted_max - interval.min_val();
std::cmp::min(min_span, interval.span())
};
use IntervalMatchResult as R;
match interval.value_matches(self.actual) {
R::Between => {
let unrooted_actual = self.actual;
// actual
let rooted_actual = unrooted_actual - interval.min_val();
// fraction
Fraction::new(rooted_actual, rooted_max)
}
R::MinAndMax => {
use MinIsMaxBehavior as B;
match min_is_max_behavior {
B::PreferZero => Self::new_min(0),
B::PreferOne => Self::new_max(1),
}
}
R::Min | R::Lower => Fraction::new_min(rooted_max),
R::Max | R::Greater => Fraction::new_max(rooted_max),
}
}
/// This value is supposed to be normalized (0-rooted).
pub fn denormalize(&self, interval: &Interval<u32>, discrete_max: Option<u32>) -> Self {
let new_max = discrete_max.unwrap_or(self.max);
let clamped_interval_max = std::cmp::min(interval.max_val(), new_max);
let denorm_actual = std::cmp::min(interval.min_val() + self.actual, clamped_interval_max);
Fraction::new(denorm_actual, new_max)
}
/// Adds the given increment. If the result doesn't fit into the given interval anymore, it just
/// snaps to the opposite bound of that interval. If this fraction is not within the given
/// interval in the first place, it returns an appropriate interval bound instead of doing the
/// addition.
pub fn add_rotating(&self, increment: DiscreteIncrement, interval: &Interval<u32>) -> Fraction {
let (min, max) = (interval.min_val(), interval.max_val());
use IntervalMatchResult as R;
let new_actual = match interval.value_matches(self.actual) {
R::Lower | R::Greater => {
if increment.is_positive() {
min
} else {
max
}
}
R::Between | R::Min | R::Max | R::MinAndMax => {
let sum = self.actual as i32 + increment.get();
if sum < 0 {
max
} else {
let sum = sum as u32;
match interval.value_matches(sum) {
R::Between => sum,
R::Min | R::Greater => min,
R::Max | R::Lower | R::MinAndMax => max,
}
}
}
};
Fraction::new(new_actual, max)
}
/// Adds the given increment. If the result doesn't fit into the given interval anymore, it just
/// snaps to the bound of that interval. If this fraction is not within the given interval in
/// the first place, it returns the closest interval bound instead of doing the addition.
pub fn add_clamping(&self, increment: DiscreteIncrement, interval: &Interval<u32>) -> Fraction {
let (min, max) = (interval.min_val(), interval.max_val());
use IntervalMatchResult as R;
let new_actual = match interval.value_matches(self.actual) {
R::Lower => min,
R::Greater => max,
R::Between | R::Min | R::Max | R::MinAndMax => {
let sum = self.actual as i32 + increment.get();
if sum < 0 {
min
} else {
let sum = sum as u32;
match interval.value_matches(sum) {
R::Between => sum,
R::Min | R::Lower => min,
R::Max | R::Greater | R::MinAndMax => max,
}
}
}
};
Fraction::new(new_actual, max)
}
}
impl Interval<u32> {
pub fn normalize_to_min(&self, value: u32) -> u32 {
let value = std::cmp::min(value, self.max_val());
let difference = value as i32 - self.min_val() as i32;
std::cmp::max(difference, 0) as u32
}
}
pub fn full_discrete_interval() -> Interval<u32> {
Interval::new(0, u32::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_subset() {
// Given
let source_interval = Interval::new(100, 120);
// When
// Then
assert_eq!(
Fraction::new(105, 127).normalize(&source_interval, MinIsMaxBehavior::PreferZero),
Fraction::new(5, 20)
);
assert_eq!(
Fraction::new(100, 127).normalize(&source_interval, MinIsMaxBehavior::PreferZero),
Fraction::new(0, 20)
);
assert_eq!(
Fraction::new(50, 127).normalize(&source_interval, MinIsMaxBehavior::PreferZero),
Fraction::new(0, 20)
);
assert_eq!(
Fraction::new(120, 127).normalize(&source_interval, MinIsMaxBehavior::PreferZero),
Fraction::new(20, 20)
);
assert_eq!(
Fraction::new(127, 127).normalize(&source_interval, MinIsMaxBehavior::PreferZero),
Fraction::new(20, 20)
);
}
#[test]
fn normalize_intersection() {
// Given
let source_interval = Interval::new(10, 100);
// When
// Then
assert_eq!(
Fraction::new(0, 20).normalize(&source_interval, MinIsMaxBehavior::PreferZero),
Fraction::new(0, 10)
);
assert_eq!(
Fraction::new(10, 20).normalize(&source_interval, MinIsMaxBehavior::PreferZero),
Fraction::new(0, 10)
);
assert_eq!(
Fraction::new(15, 20).normalize(&source_interval, MinIsMaxBehavior::PreferZero),
Fraction::new(5, 10)
);
assert_eq!(
Fraction::new(20, 20).normalize(&source_interval, MinIsMaxBehavior::PreferZero),
Fraction::new(10, 10)
);
assert_eq!(
Fraction::new(127, 20).normalize(&source_interval, MinIsMaxBehavior::PreferZero),
Fraction::new(10, 10)
);
}
#[test]
fn denormalize_subset() {
// Given
let source_interval = Interval::new(100, 120);
// When
// Then
assert_eq!(
Fraction::new(5, 127).denormalize(&source_interval, Some(130)),
Fraction::new(105, 130)
);
assert_eq!(
Fraction::new(0, 127).denormalize(&source_interval, Some(130)),
Fraction::new(100, 130)
);
assert_eq!(
Fraction::new(20, 127).denormalize(&source_interval, Some(130)),
Fraction::new(120, 130)
);
assert_eq!(
Fraction::new(30, 127).denormalize(&source_interval, Some(130)),
Fraction::new(120, 130)
);
assert_eq!(
Fraction::new(5, 127).denormalize(&source_interval, Some(110)),
Fraction::new(105, 110)
);
assert_eq!(
Fraction::new(0, 127).denormalize(&source_interval, Some(110)),
Fraction::new(100, 110)
);
assert_eq!(
Fraction::new(20, 127).denormalize(&source_interval, Some(110)),
Fraction::new(110, 110)
);
assert_eq!(
Fraction::new(30, 127).denormalize(&source_interval, Some(110)),
Fraction::new(110, 110)
);
assert_eq!(
Fraction::new(30, 127).denormalize(&source_interval, None),
Fraction::new(120, 127)
);
}
#[test]
fn denormalize_intersection() {
// Given
let source_interval = Interval::new(10, 100);
// When
// Then
assert_eq!(
Fraction::new(0, 20).denormalize(&source_interval, Some(40)),
Fraction::new(10, 40)
);
assert_eq!(
Fraction::new(5, 20).denormalize(&source_interval, Some(40)),
Fraction::new(15, 40)
);
assert_eq!(
Fraction::new(10, 20).denormalize(&source_interval, Some(40)),
Fraction::new(20, 40)
);
assert_eq!(
Fraction::new(15, 20).denormalize(&source_interval, Some(40)),
Fraction::new(25, 40)
);
assert_eq!(
Fraction::new(0, 20).denormalize(&source_interval, Some(17)),
Fraction::new(10, 17)
);
assert_eq!(
Fraction::new(5, 20).denormalize(&source_interval, Some(17)),
Fraction::new(15, 17)
);
assert_eq!(
Fraction::new(10, 20).denormalize(&source_interval, Some(17)),
Fraction::new(17, 17)
);
assert_eq!(
Fraction::new(15, 20).denormalize(&source_interval, Some(17)),
Fraction::new(17, 17)
);
assert_eq!(
Fraction::new(15, 20).denormalize(&source_interval, None),
Fraction::new(20, 20)
);
}
}
@@ -0,0 +1,173 @@
use std::fmt::Debug;
use std::ops::{RangeInclusive, Sub};
/// An interval which has an inclusive min and inclusive max value.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct Interval<T> {
min: T,
max: T,
}
pub const UNIT_INTERVAL: Interval<f64> = Interval { min: 0.0, max: 1.0 };
impl<T: PartialOrd + Copy> Interval<T> {
/// Creates an interval. Panics if `min` is greater than `max`.
pub fn new(min: T, max: T) -> Interval<T>
where
T: Debug,
{
assert!(min <= max, "min = {min:?} is greater than max = {max:?}",);
Interval { min, max }
}
pub fn try_new(min: T, max: T) -> Result<Interval<T>, String>
where
T: Debug,
{
if min > max {
return Err(format!("min = {min:?} is greater than max = {max:?}"));
}
Ok(Interval { min, max })
}
pub fn new_auto(bound_1: T, bound_2: T) -> Interval<T> {
Interval {
min: if bound_1 <= bound_2 { bound_1 } else { bound_2 },
max: if bound_1 >= bound_2 { bound_1 } else { bound_2 },
}
}
/// Checks if this interval contains the given value.
///
/// **Attention:** This is very strict at the interval bounds and doesn't consider numerical
/// inaccuracies. Consider using `value_matches_tolerant()` instead.
pub fn contains(&self, value: T) -> bool {
self.min <= value && value <= self.max
}
pub fn min_is_max(&self, epsilon: f64) -> bool
where
T: Sub<Output = f64>,
{
(self.max - self.min).abs() < epsilon
}
pub fn value_matches_tolerant(&self, value: T, epsilon: f64) -> IntervalMatchResult
where
T: Sub<Output = f64>,
{
let is_min = (self.min - value).abs() < epsilon;
let is_max = (value - self.max).abs() < epsilon;
self.value_matches_internal(value, is_min, is_max)
}
pub fn value_matches(&self, value: T) -> IntervalMatchResult {
let is_min = value == self.min;
let is_max = value == self.max;
self.value_matches_internal(value, is_min, is_max)
}
fn value_matches_internal(&self, value: T, is_min: bool, is_max: bool) -> IntervalMatchResult {
if is_min && is_max {
IntervalMatchResult::MinAndMax
} else if is_min {
IntervalMatchResult::Min
} else if is_max {
IntervalMatchResult::Max
} else if value < self.min {
IntervalMatchResult::Lower
} else if value > self.max {
IntervalMatchResult::Greater
} else {
IntervalMatchResult::Between
}
}
/// Returns the low bound of this interval.
pub fn min_val(&self) -> T {
self.min
}
/// Returns a new interval containing the given minimum.
///
/// If the given minimum is greater than the current maximum, the maximum will be set to given
/// minimum.
pub fn with_min(&self, min: T) -> Interval<T>
where
T: Debug,
{
Interval::new(min, if min <= self.max { self.max } else { min })
}
/// Range from min to (inclusive) max.
pub fn range(&self) -> RangeInclusive<T> {
self.min..=self.max
}
/// Returns a new interval containing the given maximum.
///
/// If the given maximum is lower than the current minimum, the minimum will be set to the given
/// maximum.
pub fn with_max(&self, max: T) -> Interval<T>
where
T: Debug,
{
Interval::new(if self.min <= max { self.min } else { max }, max)
}
/// Returns the high bound of this interval.
pub fn max_val(&self) -> T {
self.max
}
/// Returns the distance between the low and high bound of this interval.
pub fn span(&self) -> T::Output
where
T: Sub,
{
self.max - self.min
}
/// If there's no intersection, a zero interval (with default values) will be returned.
pub fn intersect(&self, other: &Interval<T>) -> Interval<T>
where
T: Default + Debug,
{
let greatest_min = partial_min_max::max(self.min, other.min);
let lowest_max = partial_min_max::min(self.max, other.max);
if greatest_min <= lowest_max {
Interval::new(greatest_min, lowest_max)
} else {
Interval::new(Default::default(), Default::default())
}
}
pub fn union(&self, other: &Interval<T>) -> Interval<T>
where
T: Default + Debug,
{
let lowest_min = partial_min_max::min(self.min, other.min);
let greatest_max = partial_min_max::max(self.max, other.max);
Interval::new(lowest_min, greatest_max)
}
}
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
pub enum IntervalMatchResult {
Between,
Min,
Max,
MinAndMax,
Lower,
Greater,
}
impl IntervalMatchResult {
pub fn matches(self) -> bool {
use IntervalMatchResult as R;
match self {
R::Between | R::Min | R::Max | R::MinAndMax => true,
R::Lower | R::Greater => false,
}
}
}
@@ -0,0 +1,26 @@
#[macro_use]
mod regex_util;
mod control_value;
pub use control_value::*;
mod feedback_value;
pub use feedback_value::*;
mod unit;
pub use unit::*;
mod discrete;
pub use discrete::*;
mod fraction;
pub use fraction::*;
mod interval;
pub use interval::*;
mod ui_util;
pub use ui_util::*;
mod util;
pub(crate) use util::*;
@@ -0,0 +1,6 @@
macro_rules! regex {
($re:literal $(,)?) => {{
static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new();
RE.get_or_init(|| regex::Regex::new($re).unwrap())
}};
}
@@ -0,0 +1,9 @@
pub fn format_percentage_without_unit(value: f64) -> String {
let percentage = value * 100.0;
format!("{percentage:.4}")
}
pub fn parse_percentage_without_unit(text: &str) -> Result<f64, &'static str> {
let percentage: f64 = text.parse().map_err(|_| "not a valid decimal value")?;
Ok(percentage / 100.0)
}
@@ -0,0 +1,604 @@
use crate::{DiscreteIncrement, DiscreteValue, Interval, IntervalMatchResult};
use bytemuck::NoUninit;
use derive_more::Display;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::convert::{TryFrom, TryInto};
use std::fmt::{Debug, Display, Formatter};
use std::ops::{Add, Sub};
/// A number that is primarily within the negative and positive unit interval `(-1.0..=1.0)` but
/// can also take higher values.
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Display, Default, Serialize, Deserialize)]
#[serde(try_from = "f64")]
pub struct SoftSymmetricUnitValue(f64);
impl SoftSymmetricUnitValue {
/// -1.0
pub const SOFT_MIN: SoftSymmetricUnitValue = SoftSymmetricUnitValue(-1.0);
/// 1.0
pub const SOFT_MAX: SoftSymmetricUnitValue = SoftSymmetricUnitValue(1.0);
/// Creates the symmetric unit value. Panics if the given number is not within the positive unit
/// interval.
pub fn new(number: f64) -> SoftSymmetricUnitValue {
SoftSymmetricUnitValue(number)
}
/// Returns the underlying number.
pub fn get(&self) -> f64 {
self.0
}
pub fn abs(&self) -> UnitValue {
UnitValue::new_clamped(self.0.abs())
}
pub fn map_to_positive_unit_interval(&self) -> UnitValue {
UnitValue::new_clamped((self.0 + 1.0) / 2.0)
}
pub fn clamp_to_positive_unit_interval(&self) -> UnitValue {
if self.0 < 0.0 {
UnitValue::MIN
} else {
UnitValue::new_clamped(self.0)
}
}
}
impl Add for SoftSymmetricUnitValue {
type Output = f64;
fn add(self, rhs: Self) -> Self::Output {
self.0 + rhs.0
}
}
impl Sub for SoftSymmetricUnitValue {
type Output = f64;
fn sub(self, rhs: Self) -> Self::Output {
self.0 - rhs.0
}
}
impl From<f64> for SoftSymmetricUnitValue {
fn from(v: f64) -> Self {
SoftSymmetricUnitValue(v)
}
}
impl From<UnitValue> for f64 {
fn from(v: UnitValue) -> Self {
v.get()
}
}
impl std::str::FromStr for SoftSymmetricUnitValue {
type Err = &'static str;
fn from_str(source: &str) -> Result<Self, Self::Err> {
let primitive = f64::from_str(source).map_err(|_| "not a valid decimal number")?;
Ok(SoftSymmetricUnitValue(primitive))
}
}
/// Defines the normalization behavior if the range span is zero (that is min == max).
pub enum MinIsMaxBehavior {
PreferZero,
PreferOne,
}
/// A number within the unit interval `(0.0..=1.0)`.
#[derive(Clone, Copy, Debug, PartialEq, Display, Default, Serialize, Deserialize, NoUninit)]
#[serde(try_from = "f64")]
#[repr(transparent)]
pub struct UnitValue(f64);
impl Eq for UnitValue {}
impl PartialOrd for UnitValue {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for UnitValue {
fn cmp(&self, other: &Self) -> Ordering {
self.0
.partial_cmp(&other.0)
.expect("unit values are never NaN")
}
}
impl UnitValue {
/// 0.0
pub const MIN: UnitValue = UnitValue(0.0);
/// 1.0
pub const MAX: UnitValue = UnitValue(1.0);
pub fn is_valid(number: f64) -> bool {
(0.0..=1.0).contains(&number)
}
/// Creates the unit value. Panics if the given number is not within the positive unit interval.
pub fn new(number: f64) -> UnitValue {
assert!(Self::is_valid(number), "{number} is not a valid unit value",);
UnitValue(number)
}
pub fn try_new(number: f64) -> Option<UnitValue> {
if Self::is_valid(number) {
Some(Self(number))
} else {
None
}
}
pub fn new_clamped(number: f64) -> UnitValue {
let actual_number = if number > 1.0 {
1.0
} else if number < 0.0 || number.is_nan() {
0.0
} else {
number
};
UnitValue(actual_number)
}
/// Checks preconditions only in debug build. Should only be used if you want to squeeze out
/// every last bit of performance and you are super sure that the number meets the
/// preconditions. This constructor is offered because it's not unlikely that a lot of those
/// values will be constructed in audio thread.
///
/// # Safety
///
/// You need to make sure that the given number is a valid unit value.
pub unsafe fn new_unchecked(number: f64) -> UnitValue {
debug_assert!(Self::is_valid(number), "{number} is not a valid unit value",);
UnitValue(number)
}
// TODO Maybe we should rather implement From<UnitValue> for f64? Same with other newtypes.
/// Returns the underlying number.
pub fn get(&self) -> f64 {
self.0
}
pub fn to_symmetric(self) -> SoftSymmetricUnitValue {
SoftSymmetricUnitValue::new(self.0)
}
pub fn map_to_symmetric_unit_interval(&self) -> SoftSymmetricUnitValue {
SoftSymmetricUnitValue::new((self.0 * 2.0) - 1.0)
}
pub fn is_within_interval(&self, interval: &Interval<UnitValue>) -> bool {
interval.contains(*self)
}
/// Calculates the distance between this and another unit value.
pub fn calc_distance_from(&self, rhs: Self) -> UnitValue {
unsafe { UnitValue::new_unchecked((self.0 - rhs.0).abs()) }
}
/// Maps this value to the given destination interval assuming that this value currently
/// exhausts the complete unit interval.
pub fn denormalize(&self, destination_interval: &Interval<UnitValue>) -> UnitValue {
let min = destination_interval.min_val().get();
let span = destination_interval.span();
unsafe { UnitValue::new_unchecked(min + self.get() * span) }
}
/// Maps this value to the unit interval assuming that this value currently exhausts the given
/// current interval. If this value is outside the current interval, this method returns either
/// 0.0 or 1.0. If value == min == max, it returns 0.0 or 1.0 depending on the given behavior.
pub fn normalize(
&self,
current_interval: &Interval<UnitValue>,
min_is_max_behavior: MinIsMaxBehavior,
epsilon: f64,
) -> UnitValue {
use IntervalMatchResult as R;
match current_interval.value_matches_tolerant(*self, epsilon) {
R::Between => UnitValue::new_clamped(
(*self - current_interval.min_val()) / current_interval.span(),
),
R::MinAndMax => {
use MinIsMaxBehavior as B;
match min_is_max_behavior {
B::PreferZero => UnitValue::MIN,
B::PreferOne => UnitValue::MAX,
}
}
R::Min | R::Lower => UnitValue::MIN,
R::Max | R::Greater => UnitValue::MAX,
}
}
/// Like `map_from_unit_interval_to` but mapping to a discrete range (with additional rounding).
/// round() is used here instead of floor() in order to not give advantage to any direction.
pub fn denormalize_discrete(
&self,
destination_interval: &Interval<DiscreteValue>,
) -> DiscreteValue {
let min = destination_interval.min_val().get();
let span = destination_interval.span();
DiscreteValue::new(min + (self.get() * span as f64).round() as u32)
}
pub fn denormalize_discrete_increment(
&self,
destination_interval: &Interval<DiscreteIncrement>,
) -> DiscreteIncrement {
let min: i32 = destination_interval.min_val().get();
let max: i32 = destination_interval.max_val().get();
let count: u32 = if min < 0 && max > 0 {
(max - min) as u32
} else {
(max - min) as u32 + 1
};
let addend: u32 = (self.0 * (count - 1) as f64).round() as _;
let sum = min + addend as i32;
let skip_zero_sum = if min < 0 && sum >= 0 { sum + 1 } else { sum };
DiscreteIncrement::new(skip_zero_sum)
}
/// Converts this unit value to a unit increment, either negative or positive depending
/// on the given signum. Returns `None` if this value is zero.
pub fn to_increment(self, signum: i32) -> Option<UnitIncrement> {
if self.is_zero() {
return None;
}
Some(unsafe { UnitIncrement::new_unchecked(signum as f64 * self.0) })
}
/// Returns the value on the "other side" of the unit interval.
///
/// # Examples
/// - 0.8 => 0.2
/// - 0.6 => 0.4
pub fn inverse(&self) -> UnitValue {
unsafe { UnitValue::new_unchecked(1.0 - self.0) }
}
/// "Rounds" value to its nearest grid value using the grid's number of intervals. Using the
/// number of intervals guarantees that each grid interval will have the same size. So if you
/// have the accurate number of intervals at disposal, use this method.
pub fn snap_to_grid_by_interval_count(&self, interval_count: u32) -> UnitValue {
assert_ne!(interval_count, 0);
let interval_count = interval_count as f64;
unsafe { UnitValue::new_unchecked((self.0 * interval_count).round() / interval_count) }
}
/// Rounds value to its nearest grid value using the grid's interval size. If you pass an
/// interval size whose multiple doesn't perfectly fit into the unit interval, the last
/// interval will be smaller than all the others. Better don't do that.
pub fn snap_to_grid_by_interval_size(&self, interval_size: UnitValue) -> UnitValue {
if interval_size.is_zero() {
return *self;
}
unsafe {
UnitValue::new_unchecked(
((self.0 / interval_size.0).round() * interval_size.0).min(1.0),
)
}
}
/// Returns whether this is exactly 0.0.
#[allow(clippy::float_cmp)]
pub fn is_zero(&self) -> bool {
self.0 == 0.0
}
/// Returns whether this is exactly 1.0.
#[allow(clippy::float_cmp)]
pub fn is_one(&self) -> bool {
self.0 == 1.0
}
/// Adds the given increment. If the result doesn't fit into the given interval anymore, it just
/// snaps to the opposite bound of that interval. If this unit value is not within the given
/// interval in the first place, it returns an appropriate interval bound instead of doing the
/// addition.
///
/// Slight inaccuracies can have a big effect when actually rotating:
/// https://github.com/helgoboss/helgobox/issues/208. That's why an epsilon needs to be passed
/// for the comparison that decides whether it's time to rotate already.
pub fn add_rotating(
&self,
increment: UnitIncrement,
interval: &Interval<UnitValue>,
epsilon: f64,
) -> UnitValue {
let (min, max) = (interval.min_val(), interval.max_val());
use IntervalMatchResult as R;
match interval.value_matches_tolerant(*self, epsilon) {
R::Lower | R::Greater => {
if increment.is_positive() {
min
} else {
max
}
}
R::Between | R::Min | R::Max | R::MinAndMax => {
let sum = self.0 + increment.get();
let raw_interval: Interval<f64> = (*interval).into();
match raw_interval.value_matches_tolerant(sum, epsilon) {
R::Between => UnitValue::new_clamped(sum),
R::Min | R::Greater => min,
R::Max | R::Lower | R::MinAndMax => max,
}
}
}
}
/// Adds the given increment. If the result doesn't fit into the given interval anymore, it just
/// snaps to the bound of that interval. If this unit value is not within the given interval in
/// the first place, it returns the closest interval bound instead of doing the addition.
pub fn add_clamping(
&self,
increment: UnitIncrement,
interval: &Interval<UnitValue>,
epsilon: f64,
) -> UnitValue {
let (min, max) = (interval.min_val(), interval.max_val());
use IntervalMatchResult as R;
match interval.value_matches_tolerant(*self, epsilon) {
R::Lower => min,
R::Greater => max,
R::Between | R::Min | R::Max | R::MinAndMax => {
UnitValue::new_clamped(num::clamp(self.0 + increment.get(), min.get(), max.get()))
}
}
}
/// Clamps this value to the given interval bounds.
pub fn clamp_to_interval(&self, interval: &Interval<UnitValue>) -> UnitValue {
unsafe {
UnitValue::new_unchecked(num::clamp(
self.0,
interval.min_val().0,
interval.max_val().0,
))
}
}
pub fn to_discrete<T: TryFrom<u64> + Into<u64>>(self, max_value: T) -> T
where
<T as TryFrom<u64>>::Error: Debug,
{
let discrete = (self.get() * max_value.into() as f64).round() as u64;
discrete.try_into().unwrap()
}
pub fn try_from_discrete<T: TryFrom<u64> + Into<u64>>(
actual_value: T,
max_value: T,
) -> Result<UnitValue, &'static str> {
let actual_value = actual_value.into();
let max_value = max_value.into();
if actual_value > max_value {
return Err("value too large");
}
let unit_value = Self::new_clamped(actual_value as f64 / max_value as f64);
Ok(unit_value)
}
}
impl Add for UnitValue {
type Output = f64;
fn add(self, rhs: Self) -> Self::Output {
self.0 + rhs.0
}
}
impl Sub for UnitValue {
type Output = f64;
fn sub(self, rhs: Self) -> Self::Output {
self.0 - rhs.0
}
}
impl From<Interval<UnitValue>> for Interval<f64> {
fn from(source: Interval<UnitValue>) -> Self {
Interval::new(source.min_val().get(), source.max_val().get())
}
}
impl TryFrom<f64> for UnitValue {
type Error = &'static str;
fn try_from(value: f64) -> Result<Self, Self::Error> {
if !UnitValue::is_valid(value) {
return Err("value is not between 0.0 and 1.0");
}
Ok(UnitValue(value))
}
}
impl std::str::FromStr for UnitValue {
type Err = &'static str;
fn from_str(source: &str) -> Result<Self, Self::Err> {
let primitive = f64::from_str(source).map_err(|_| "not a valid decimal number")?;
if !UnitValue::is_valid(primitive) {
return Err("not a value between 0.0 and 1.0");
}
Ok(UnitValue(primitive))
}
}
impl Interval<UnitValue> {
/// Returns the value which is exactly in the middle between the interval bounds.
pub fn center(&self) -> UnitValue {
unsafe { UnitValue::new_unchecked((self.min_val() + self.max_val()) / 2.0) }
}
/// Returns whether this interval is the complete unit interval.
pub fn is_full(&self) -> bool {
self.min_val().is_zero() && self.max_val().is_one()
}
/// Inverts the interval.
pub fn inverse(&self) -> Interval<UnitValue> {
Interval::new(self.max_val().inverse(), self.min_val().inverse())
}
}
/// Convenience method for getting the complete unit interval.
pub fn full_unit_interval() -> Interval<UnitValue> {
create_unit_value_interval(0.0, 1.0)
}
/// Convenience method for creating an interval of unit values.
pub fn create_unit_value_interval(min: f64, max: f64) -> Interval<UnitValue> {
Interval::new(UnitValue::new(min), UnitValue::new(max))
}
/// A number within the negative or positive unit interval `(-1.0..=1.0)` representing a positive or
/// negative increment, never 0 (otherwise it wouldn't be an increment after all).
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct UnitIncrement(f64);
impl TryFrom<f64> for UnitIncrement {
type Error = &'static str;
fn try_from(value: f64) -> Result<Self, Self::Error> {
if !Self::is_valid(value) {
return Err("not a valid unit increment");
}
Ok(Self::new(value))
}
}
impl Display for UnitIncrement {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:+}", self.0)
}
}
impl UnitIncrement {
pub fn is_valid(number: f64) -> bool {
number != 0.0 && (-1.0..=1.0).contains(&number)
}
/// Creates the unit increment. Panics if the given number is 0.0 or not within the positive or
/// negative unit interval.
#[allow(clippy::float_cmp)]
pub fn new(number: f64) -> UnitIncrement {
assert!(
Self::is_valid(number),
"{number} is not a valid unit increment",
);
UnitIncrement(number)
}
/// Creates the unit increment. Panics if the given number is 0.0 but clamps it if not in the
/// positive or negative unit interval.
#[allow(clippy::float_cmp)]
pub fn new_clamped(number: f64) -> UnitIncrement {
assert_ne!(number, 0.0);
UnitIncrement(number.clamp(-1.0, 1.0))
}
/// Creates the unit increment. Clamps it if not in the positive or negative unit interval.
#[allow(clippy::float_cmp)]
pub fn new_clamped_checked(number: f64) -> Option<UnitIncrement> {
if number == 0.0 {
return None;
}
Some(UnitIncrement(number.clamp(-1.0, 1.0)))
}
/// Checks preconditions only in debug build. Should only be used if you want to squeeze out
/// every last bit of performance and you are super sure that the number meets the
/// preconditions. This constructor is offered because it's not unlikely that a lot of those
/// values will be constructed in audio thread.
///
/// # Safety
///
/// Make sure the given increment is not zero.
#[allow(clippy::float_cmp)]
pub unsafe fn new_unchecked(number: f64) -> UnitIncrement {
debug_assert!(
Self::is_valid(number),
"{number} is not a valid unit increment",
);
UnitIncrement(number)
}
/// Returns the underlying number.
pub fn get(&self) -> f64 {
self.0
}
/// Returns if this increment is positive.
pub fn is_positive(&self) -> bool {
self.0 >= 0.0
}
/// Returns a +1 or -1 depending on the direction of the increment. The actual amount is ignored.
pub fn to_discrete_increment(&self) -> DiscreteIncrement {
if self.is_positive() {
DiscreteIncrement::new(1)
} else {
DiscreteIncrement::new(-1)
}
}
/// Returns the signum (-1 if it's a negative increment, otherwise +1).
pub fn signum(&self) -> i32 {
if self.is_positive() {
1
} else {
-1
}
}
/// Converts this unit increment into a unit value thereby "losing" its direction.
pub fn to_value(self) -> UnitValue {
unsafe { UnitValue::new_unchecked(self.0.abs()) }
}
/// Clamps this increment to the given interval bounds.
pub fn clamp_to_interval(&self, interval: &Interval<UnitValue>) -> Option<UnitIncrement> {
let clamped_value = self.to_value().clamp_to_interval(interval);
clamped_value.to_increment(self.signum())
}
pub fn inverse(&self) -> Self {
Self(-self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn map_from_unit_interval_to_discrete_increment() {
// Given
// Contains elements -3, -2, -1, 1, 2, 3, 4
let interval = Interval::new(DiscreteIncrement::new(-3), DiscreteIncrement::new(4));
// When
// Then
assert_eq!(
UnitValue::new(0.0).denormalize_discrete_increment(&interval),
DiscreteIncrement::new(-3)
);
assert_eq!(
UnitValue::new(0.5).denormalize_discrete_increment(&interval),
DiscreteIncrement::new(1)
);
assert_eq!(
UnitValue::new(1.0).denormalize_discrete_increment(&interval),
DiscreteIncrement::new(4)
);
}
}
@@ -0,0 +1,8 @@
/// Returns an appropriate signum depending on the given condition.
pub(crate) fn negative_if(condition: bool) -> i32 {
if condition {
-1
} else {
1
}
}
@@ -0,0 +1,12 @@
#[macro_use]
mod base;
pub use base::*;
mod source;
pub use source::*;
mod mode;
pub use mode::*;
#[cfg(test)]
mod test_util;
@@ -0,0 +1,309 @@
use crate::{AbsoluteValue, Increment, Interval, IntervalMatchResult, MinIsMaxBehavior, UnitValue};
use derive_more::Display;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
use strum::EnumIter;
/// This epsilon is used in helgoboss-learn at some places to make floating point comparison
/// more tolerant. This is the same epsilon used in JSFX/EEL.
pub const BASE_EPSILON: f64 = 0.00001;
/// Determines how out-of-range source (control) or target (feedback) values are handled.
#[derive(
Copy,
Clone,
Eq,
PartialEq,
Hash,
Debug,
Default,
EnumIter,
TryFromPrimitive,
IntoPrimitive,
Display,
Serialize,
Deserialize,
)]
#[repr(usize)]
pub enum OutOfRangeBehavior {
/// Yields range minimum if lower than range minimum and range maximum if greater.
#[default]
#[serde(rename = "minOrMax")]
#[display(fmt = "Min or max")]
MinOrMax,
/// Yields range minimum if out-of-range.
#[serde(rename = "min")]
#[display(fmt = "Min")]
Min,
/// Totally ignores out-of-range values.
#[serde(rename = "ignore")]
#[display(fmt = "Ignore")]
Ignore,
}
impl OutOfRangeBehavior {
pub fn process(
&self,
control_value: AbsoluteValue,
interval_match_result: IntervalMatchResult,
continuous_interval: &Interval<UnitValue>,
discrete_interval: &Interval<u32>,
) -> Option<(AbsoluteValue, MinIsMaxBehavior)> {
use OutOfRangeBehavior as B;
match self {
B::MinOrMax => {
if interval_match_result == IntervalMatchResult::Lower {
Some((
control_value.select_appropriate_interval_min(
continuous_interval,
discrete_interval,
),
MinIsMaxBehavior::PreferZero,
))
} else {
Some((
control_value.select_appropriate_interval_max(
continuous_interval,
discrete_interval,
),
MinIsMaxBehavior::PreferOne,
))
}
}
B::Min => Some((
control_value
.select_appropriate_interval_min(continuous_interval, discrete_interval),
MinIsMaxBehavior::PreferZero,
)),
B::Ignore => None,
}
}
}
#[derive(
Copy,
Clone,
Eq,
PartialEq,
Hash,
Debug,
Default,
EnumIter,
TryFromPrimitive,
IntoPrimitive,
Display,
Serialize,
Deserialize,
)]
#[repr(usize)]
pub enum ButtonUsage {
#[default]
#[serde(rename = "both")]
#[display(fmt = "Press & release")]
Both,
#[serde(rename = "press-only")]
#[display(fmt = "Press only")]
PressOnly,
#[serde(rename = "release-only")]
#[display(fmt = "Release only")]
ReleaseOnly,
}
impl ButtonUsage {
pub fn should_ignore(&self, value: AbsoluteValue) -> bool {
match self {
ButtonUsage::PressOnly if value.is_zero() => true,
ButtonUsage::ReleaseOnly if !value.is_zero() => true,
_ => false,
}
}
}
#[derive(
Copy,
Clone,
Eq,
PartialEq,
Hash,
Debug,
Default,
EnumIter,
TryFromPrimitive,
IntoPrimitive,
Display,
Serialize,
Deserialize,
)]
#[repr(usize)]
pub enum EncoderUsage {
#[default]
#[serde(rename = "both")]
#[display(fmt = "Increment & decrement")]
Both,
#[serde(rename = "increment-only")]
#[display(fmt = "Increment only")]
IncrementOnly,
#[serde(rename = "decrement-only")]
#[display(fmt = "Decrement only")]
DecrementOnly,
}
impl EncoderUsage {
pub fn matches(&self, i: Increment) -> bool {
match self {
EncoderUsage::IncrementOnly if !i.is_positive() => false,
EncoderUsage::DecrementOnly if i.is_positive() => false,
_ => true,
}
}
}
#[derive(
Copy,
Clone,
Eq,
PartialEq,
Hash,
Debug,
EnumIter,
TryFromPrimitive,
IntoPrimitive,
Display,
Serialize,
Deserialize,
)]
#[repr(usize)]
pub enum FireMode {
#[serde(rename = "release")]
#[display(fmt = "Fire on press (or release if > 0 ms)")]
Normal,
#[serde(rename = "timeout")]
#[display(fmt = "Fire after timeout")]
AfterTimeout,
#[serde(rename = "turbo")]
#[display(fmt = "Fire after timeout, keep firing (turbo)")]
AfterTimeoutKeepFiring,
#[serde(rename = "single")]
#[display(fmt = "Fire after single press")]
OnSinglePress,
#[serde(rename = "double")]
#[display(fmt = "Fire on double press")]
OnDoublePress,
}
impl Default for FireMode {
fn default() -> Self {
Self::Normal
}
}
#[derive(
Copy,
Clone,
Eq,
PartialEq,
Hash,
Debug,
EnumIter,
TryFromPrimitive,
IntoPrimitive,
Display,
Serialize,
Deserialize,
)]
#[repr(usize)]
pub enum TakeoverMode {
#[serde(rename = "off")]
#[display(fmt = "Off (may cause jumps)")]
Off,
#[serde(rename = "pickup")]
#[display(fmt = "Pick up")]
Pickup,
#[serde(rename = "pickup-tolerant")]
#[display(fmt = "Pick up (tolerant)")]
PickupTolerant,
#[serde(rename = "longTimeNoSee")]
#[display(fmt = "Long time no see")]
LongTimeNoSee,
#[serde(rename = "parallel")]
#[display(fmt = "Parallel")]
Parallel,
#[serde(rename = "valueScaling")]
#[display(fmt = "Catch up")]
CatchUp,
}
impl TakeoverMode {
pub fn prevents_jumps(&self) -> bool {
*self != Self::Off
}
}
impl Default for TakeoverMode {
fn default() -> Self {
Self::Off
}
}
#[derive(
Copy,
Clone,
Eq,
PartialEq,
Hash,
Debug,
EnumIter,
TryFromPrimitive,
IntoPrimitive,
Display,
Serialize,
Deserialize,
)]
#[repr(usize)]
pub enum GroupInteraction {
#[serde(rename = "none")]
#[display(fmt = "None")]
None,
#[serde(rename = "same-control")]
#[display(fmt = "Same control")]
SameControl,
#[serde(rename = "same-target-value")]
#[display(fmt = "Same target value")]
SameTargetValue,
#[serde(rename = "inverse-control")]
#[display(fmt = "Inverse control")]
InverseControl,
#[serde(rename = "inverse-target-value")]
#[display(fmt = "Inverse target value")]
InverseTargetValue,
#[serde(rename = "inverse-target-value-on-only")]
#[display(fmt = "Inverse target value (on only)")]
InverseTargetValueOnOnly,
#[serde(rename = "inverse-target-value-off-only")]
#[display(fmt = "Inverse target value (off only)")]
InverseTargetValueOffOnly,
}
impl Default for GroupInteraction {
fn default() -> Self {
Self::None
}
}
impl GroupInteraction {
pub fn is_target_based(&self) -> bool {
use GroupInteraction as I;
matches!(self, I::SameTargetValue | I::InverseTargetValue)
}
pub fn is_inverse(self) -> bool {
use GroupInteraction as I;
matches!(
self,
I::InverseControl
| I::InverseTargetValue
| I::InverseTargetValueOnOnly
| I::InverseTargetValueOffOnly
)
}
}
@@ -0,0 +1,19 @@
mod common;
pub use common::*;
mod target;
pub use target::*;
mod mode_struct;
pub use mode_struct::*;
mod mode_applicability;
pub use mode_applicability::*;
mod transformation;
pub use transformation::*;
mod press_duration_processor;
pub use press_duration_processor::*;
mod value_sequence;
pub use value_sequence::*;
mod mode_context;
pub use mode_context::*;
#[cfg(test)]
mod test_util;
@@ -0,0 +1,874 @@
use crate::AbsoluteMode::PerformanceControl;
use crate::{AbsoluteMode, FireMode, GroupInteraction, OutOfRangeBehavior};
use derive_more::Display;
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, TryFromPrimitive, IntoPrimitive)]
#[repr(isize)]
pub enum DetailedSourceCharacter {
/// Feature-wise a superset of `MomentaryOnOffButton` and `PressOnlyButton`.
#[display(fmt = "Momentary velocity-sensitive button")]
MomentaryVelocitySensitiveButton,
/// Feature-wise a superset of `PressOnlyButton`.
#[display(fmt = "Momentary on/off button")]
MomentaryOnOffButton,
/// Doesn't send message on release ("Toggle-only button").
#[display(fmt = "Trigger (doesn't fire on release)")]
Trigger,
#[display(fmt = "Range control element (e.g. knob or fader)")]
RangeControl,
#[display(fmt = "Relative control element (e.g. encoder)")]
Relative,
}
impl DetailedSourceCharacter {
fn is_button(self) -> bool {
use DetailedSourceCharacter as C;
matches!(
self,
C::MomentaryOnOffButton | C::MomentaryVelocitySensitiveButton | C::Trigger
)
}
}
#[derive(Copy, Clone, Debug)]
pub struct ModeApplicabilityCheckInput {
pub target_is_virtual: bool,
pub target_supports_discrete_values: bool,
pub control_transformation_uses_time: bool,
pub control_transformation_produces_relative_values: bool,
pub is_feedback: bool,
pub make_absolute: bool,
pub use_textual_feedback: bool,
pub source_character: DetailedSourceCharacter,
pub absolute_mode: AbsoluteMode,
pub target_value_sequence_is_set: bool,
pub fire_mode: FireMode,
}
impl ModeApplicabilityCheckInput {
pub fn source_is_button(&self) -> bool {
self.source_character.is_button()
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Display)]
pub enum ModeParameter {
#[display(fmt = "Use discrete processing (prevents scaling)")]
UseDiscreteProcessing,
#[display(fmt = "Source min/max")]
SourceMinMax,
#[display(fmt = "Reverse")]
Reverse,
#[display(fmt = "Out-of-range behavior")]
OutOfRangeBehavior,
#[display(fmt = "Out-of-range behavior \"{_0}\"")]
SpecificOutOfRangeBehavior(OutOfRangeBehavior),
#[display(fmt = "Jump min/max")]
JumpMinMax,
#[display(fmt = "Takeover mode")]
TakeoverMode,
#[display(fmt = "Control transformation")]
ControlTransformation,
#[display(fmt = "Target value sequence")]
TargetValueSequence,
#[display(fmt = "Target min/max")]
TargetMinMax,
#[display(fmt = "Feedback transformation")]
FeedbackTransformation,
#[display(fmt = "Textual feedback expression")]
TextualFeedbackExpression,
#[display(fmt = "Step size min")]
StepSizeMin,
#[display(fmt = "Step size max")]
StepSizeMax,
#[display(fmt = "Speed min")]
StepFactorMin,
#[display(fmt = "Speed max")]
StepFactorMax,
#[display(fmt = "Relative filter")]
RelativeFilter,
#[display(fmt = "Wrap")]
Rotate,
#[display(fmt = "Fire mode")]
FireMode,
#[display(fmt = "Fire mode \"{_0}\"")]
SpecificFireMode(FireMode),
#[display(fmt = "Button filter")]
ButtonFilter,
#[display(fmt = "Make absolute")]
MakeAbsolute,
#[display(fmt = "Feedback type")]
FeedbackType,
#[display(fmt = "Round target value")]
RoundTargetValue,
#[display(fmt = "Absolute mode")]
AbsoluteMode,
#[display(fmt = "Absolute mode \"{_0}\"")]
SpecificAbsoluteMode(AbsoluteMode),
#[display(fmt = "Group interaction")]
GroupInteraction,
#[display(fmt = "Group interaction \"{_0}\"")]
SpecificGroupInteraction(GroupInteraction),
}
#[derive(Copy, Clone, Debug)]
pub enum ModeApplicability {
/// Parameter is completely ignored.
HasNoEffect,
/// Makes no sense but not ignored. A sensible "no-op" default should be used.
MakesNoSenseUseDefault,
/// Doesn't make sense. Used for variants of an enum (e.g. AbsoluteMode "Toggle button") to
/// document that the applicability check of the enum itself (e.g. AbsoluteMode) will take care
/// of choosing the correct default.
MakesNoSenseParentTakesCareOfDefault,
/// Parameter is relevant. Contains description of what it does.
///
/// The contained text is not in use anymore (is now part of the ReaLearn Reference)!
MakesSense(&'static str),
/// Has an effect but a rather undesired one. The description should suggest alternatives if
/// possible.
///
/// The contained text is not in use anymore (is now part of the ReaLearn Reference)!
Awkward(&'static str),
}
impl ModeApplicability {
pub fn hint(self) -> Option<&'static str> {
use ModeApplicability as A;
match self {
A::HasNoEffect
| A::MakesNoSenseUseDefault
| A::MakesNoSenseParentTakesCareOfDefault => None,
A::MakesSense(h) | A::Awkward(h) => Some(h),
}
}
pub fn is_relevant(self) -> bool {
use ModeApplicability as A;
matches!(self, A::MakesSense(_) | A::Awkward(_))
}
}
const STEP_SIZE_MIN_FOR_RANGE_DESC: &str =
"Sets the target value change amount for an incoming non-accelerated increment/decrement.";
const SPEED_MIN_FOR_RANGE_DESC: &str =
"Sets the number of target increments for an incoming non-accelerated increment/decrement.";
const STEP_SIZE_MAX_FOR_RANGE_DESC: &str =
"Sets the target value change amount for an incoming most accelerated increment/decrement.";
const SPEED_MAX_FOR_RANGE_DESC: &str =
"Sets the number of target increments for an incoming most accelerated increment/decrement.";
const ROTATE_FOR_RANGE_DESC: &str = "If enabled, jumps from max target value to min target value for increments (opposite for decrements). Was called \"Rotate\" before.";
const NORMAL_ABSOLUTE_MODE_FOR_RANGE_DESC: &str = "Sets target to the value that corresponds to the knob/fader position. Proportionally maps from source to target range.";
pub fn check_mode_applicability(
mode_parameter: ModeParameter,
input: ModeApplicabilityCheckInput,
) -> ModeApplicability {
use ModeApplicability::*;
use ModeParameter::*;
match mode_parameter {
UseDiscreteProcessing => {
if input.target_supports_discrete_values {
MakesSense("By default, ReaLearn uses continuous processing logic. That means it considers all values as percentages and scales (stretches/squeezes) them as needed. If your target is discrete, you can enable discrete processing, which will prevent scaling and deliver your control value to the target as discrete integer (and vice versa in the feedback direction).")
} else {
MakesNoSenseUseDefault
}
}
SourceMinMax => {
if input.is_feedback {
if input.use_textual_feedback {
HasNoEffect
} else {
use DetailedSourceCharacter::*;
match input.source_character {
MomentaryVelocitySensitiveButton | MomentaryOnOffButton => {
MakesSense("Changes off/on LED colors.")
}
Trigger => MakesNoSenseUseDefault,
RangeControl | Relative => MakesSense(
"Changes lowest/highest position of motorized fader or LED ring.",
),
}
}
} else {
use DetailedSourceCharacter::*;
match input.source_character {
Trigger => MakesNoSenseUseDefault,
MomentaryOnOffButton => {
if input.absolute_mode == crate::AbsoluteMode::Normal {
Awkward(
"If min > 0 and out-of-range behavior is \"Ignore\", button releases are ignored. Also affects feedback. It's usually better to use the dedicated button filter (e.g. \"Press only\").",
)
} else {
// Releases don't have an effect anyway with incremental and toggle
// mode.
HasNoEffect
}
}
MomentaryVelocitySensitiveButton => {
MakesSense("Defines the observed button press velocity range.")
}
RangeControl | Relative => {
if input.source_character == RangeControl || input.make_absolute {
MakesSense("Defines the observed fader/knob position range.")
} else {
HasNoEffect
}
}
}
}
}
Reverse => {
if input.is_feedback {
if input.use_textual_feedback {
HasNoEffect
} else if input.source_is_button() {
MakesSense(
"If enabled, uses \"off\" LED color if target is on and \"on\" LED color if target is off.",
)
} else {
MakesSense("If enabled, reverses the direction of motorized fader or LED ring.")
}
} else {
use DetailedSourceCharacter::*;
match input.source_character {
MomentaryOnOffButton | MomentaryVelocitySensitiveButton | Trigger => {
match input.absolute_mode {
crate::AbsoluteMode::Normal => MakesSense(
"If enabled, switches the target off when pressed and on when released.",
),
crate::AbsoluteMode::IncrementalButton => MakesSense(
"If enabled, decreases the target value on press instead of increasing it.",
),
crate::AbsoluteMode::ToggleButton => MakesNoSenseUseDefault,
crate::AbsoluteMode::MakeRelative => MakesNoSenseUseDefault,
crate::AbsoluteMode::PerformanceControl => MakesNoSenseUseDefault,
}
}
RangeControl | Relative => {
if input.source_character == RangeControl || input.make_absolute {
MakesSense(
"If enabled, reverses the direction of the target value change.",
)
} else {
MakesSense(
"If enabled, converts increments to decrements and vice versa.",
)
}
}
}
}
}
OutOfRangeBehavior => {
if input.is_feedback {
if input.use_textual_feedback {
HasNoEffect
} else {
MakesSense("-")
}
} else {
use DetailedSourceCharacter::*;
match input.source_character {
Trigger => HasNoEffect,
MomentaryOnOffButton | MomentaryVelocitySensitiveButton => {
if input.absolute_mode == crate::AbsoluteMode::Normal {
MakesSense("-")
} else {
HasNoEffect
}
}
RangeControl | Relative => {
if input.source_character == Relative && !input.make_absolute {
HasNoEffect
} else {
MakesSense("-")
}
}
}
}
}
SpecificOutOfRangeBehavior(b) => {
use crate::OutOfRangeBehavior::*;
if input.is_feedback {
match b {
MinOrMax => {
MakesSense("Uses target min/max if target value below/above range.")
}
Min => MakesSense("Uses target min if target value out of range."),
Ignore => MakesSense("Doesn't send feedback if target value out of range."),
}
} else {
use DetailedSourceCharacter::*;
match input.source_character {
// Doesn't have an effect if source max is at 100% (which is a basic requirement
// and mentioned in the source min/max description).
Trigger => HasNoEffect,
MomentaryOnOffButton => {
if input.absolute_mode == crate::AbsoluteMode::Normal {
match b {
// Doesn't really have an effect so I guess this is
// backward-compatible.
MinOrMax | Min => HasNoEffect,
Ignore => {
Awkward("Ignores button press if \"on\" value out of range.")
}
}
} else {
HasNoEffect
}
}
MomentaryVelocitySensitiveButton => {
if input.absolute_mode == crate::AbsoluteMode::Normal {
match b {
MinOrMax => MakesSense(
"Uses min/max velocity if button velocity below/above velocity range.",
),
Min => {
MakesSense("Uses min velocity if button velocity out of range.")
}
Ignore => {
MakesSense("Ignores button press if velocity out of range.")
}
}
} else {
HasNoEffect
}
}
RangeControl | Relative => {
if input.source_character == Relative && !input.make_absolute {
HasNoEffect
} else {
match b {
MinOrMax => MakesSense(
"Uses source min/max if source value below/above source range.",
),
Min => MakesSense("Uses source min if source value out of range."),
Ignore => MakesSense("Ignores event if source value out of range."),
}
}
}
}
}
}
JumpMinMax | TakeoverMode => {
if input.target_is_virtual
|| input.is_feedback
|| input.absolute_mode == crate::AbsoluteMode::MakeRelative
{
HasNoEffect
} else if input.control_transformation_uses_time {
MakesNoSenseUseDefault
} else {
use DetailedSourceCharacter::*;
match input.source_character {
MomentaryOnOffButton | Trigger => MakesNoSenseUseDefault,
MomentaryVelocitySensitiveButton | RangeControl | Relative => {
if (input.source_character == MomentaryVelocitySensitiveButton
&& input.absolute_mode != crate::AbsoluteMode::Normal)
|| (input.source_character == Relative && !input.make_absolute)
{
HasNoEffect
} else if mode_parameter == JumpMinMax {
MakesSense(
"Sets the min/max allowed target parameter jump (set max very low for takeover).",
)
} else {
// Takeover mode
MakesSense("Defines how to deal with too long target parameter jumps.")
}
}
}
}
}
ControlTransformation => {
if input.is_feedback || input.absolute_mode == crate::AbsoluteMode::MakeRelative {
HasNoEffect
} else {
use DetailedSourceCharacter::*;
match input.source_character {
MomentaryOnOffButton | Trigger => {
if input.absolute_mode == crate::AbsoluteMode::Normal {
MakesSense(
"Defines via EEL how to transform incoming button presses or releases. Interesting use case for buttons: Stepping through a list of predefined target values. You can access the current target value as normalized value y (where 0.0 <= y <= 1.0). Example: a = 0.0; b = 0.2; c = 0.6; y = y == a ? b : (y == b ? c : a);",
)
} else {
MakesNoSenseUseDefault
}
}
MomentaryVelocitySensitiveButton => {
if input.absolute_mode == crate::AbsoluteMode::Normal {
MakesSense(
"Defines via EEL how to transform the button velocity (represented as normalized source value x where 0.0 <= x <= 1.0). See other button types for additional use cases. Example that creates a curve: y = x^8",
)
} else {
MakesNoSenseUseDefault
}
}
RangeControl | Relative => {
if input.source_character == Relative && !input.make_absolute {
HasNoEffect
} else {
MakesSense(
"Defines via EEL how to transform the knob/fader position (represented as normalized source value x where 0.0 <= x <= 1.0). Example that creates a curve: y = x^8",
)
}
}
}
}
}
TargetMinMax => {
if input.target_is_virtual {
HasNoEffect
} else if input.is_feedback {
if input.use_textual_feedback {
HasNoEffect
} else {
MakesSense("Defines the relevant target value range.")
}
} else if input.target_value_sequence_is_set
&& check_mode_applicability(TargetValueSequence, input).is_relevant()
{
HasNoEffect
} else {
MakesSense("Makes sure the target value will end up in the specified range.")
}
}
TargetValueSequence => {
if input.target_is_virtual || input.is_feedback {
HasNoEffect
} else {
use crate::AbsoluteMode::*;
match input.absolute_mode {
Normal | IncrementalButton | MakeRelative => {
MakesSense("Allows you to step through a sequence of comma-separated user-defined target values and value ranges. When using relative control, duplicate values and direction changes are ignored. Example: 25 - 50 (2), 75, 50, 100 %")
}
ToggleButton | PerformanceControl => {
MakesNoSenseUseDefault
}
}
}
}
FeedbackTransformation => {
if input.is_feedback && !input.use_textual_feedback {
MakesSense(
"Defines via EEL how to transform the normalized feedback value y (where 0.0 <= y <= 1.0). Example: x = 1 - y",
)
} else {
HasNoEffect
}
}
TextualFeedbackExpression => {
if input.is_feedback && input.use_textual_feedback && !input.target_is_virtual {
MakesSense("Text that you write here will appear on your hardware display. You can access lots of mapping and target properties using double braces. Example: \"{{ target.normalized_value }} %\".")
} else {
HasNoEffect
}
}
StepSizeMin | StepFactorMin => {
if input.is_feedback {
HasNoEffect
} else if input.control_transformation_produces_relative_values {
MakesSense("-")
} else {
use DetailedSourceCharacter::*;
match input.source_character {
MomentaryOnOffButton | Trigger | MomentaryVelocitySensitiveButton => {
if input.absolute_mode == crate::AbsoluteMode::IncrementalButton {
if input.source_character == MomentaryVelocitySensitiveButton {
if mode_parameter == StepSizeMin {
MakesSense(
"Sets the target value change amount when button pressed with lowest velocity.",
)
} else {
MakesSense(
"Sets the number of target increments when button pressed with lowest velocity.",
)
}
} else if mode_parameter == StepSizeMin {
MakesSense(
"Sets the target value change amount when button pressed.",
)
} else {
MakesSense(
"Sets the number of target increments when button pressed.",
)
}
} else {
HasNoEffect
}
}
RangeControl => HasNoEffect,
Relative => {
if input.make_absolute {
if mode_parameter == StepSizeMin {
MakesSense(
"Sets the amount added/subtracted to calculate the absolute value from an incoming non-accelerated increment/decrement.",
)
} else {
HasNoEffect
}
} else if mode_parameter == StepSizeMin {
MakesSense(STEP_SIZE_MIN_FOR_RANGE_DESC)
} else {
MakesSense(SPEED_MIN_FOR_RANGE_DESC)
}
}
}
}
}
StepSizeMax | StepFactorMax => {
if input.is_feedback {
HasNoEffect
} else if input.control_transformation_produces_relative_values {
MakesSense("-")
} else {
use DetailedSourceCharacter::*;
match input.source_character {
MomentaryOnOffButton | Trigger => MakesNoSenseUseDefault,
MomentaryVelocitySensitiveButton => {
if input.absolute_mode == crate::AbsoluteMode::IncrementalButton {
if mode_parameter == StepSizeMax {
MakesSense(
"Sets the target value change amount when button pressed with highest velocity.",
)
} else {
MakesSense(
"Sets the number of target increments when button pressed with highest velocity.",
)
}
} else {
HasNoEffect
}
}
RangeControl => HasNoEffect,
Relative => {
if input.make_absolute {
if mode_parameter == StepSizeMax {
MakesSense(
"Sets the amount added/subtracted to calculate the absolute value from an incoming most accelerated increment/decrement.",
)
} else {
HasNoEffect
}
} else if mode_parameter == StepSizeMin {
MakesSense(STEP_SIZE_MAX_FOR_RANGE_DESC)
} else {
MakesSense(SPEED_MAX_FOR_RANGE_DESC)
}
}
}
}
}
RelativeFilter => {
if !input.is_feedback
&& (input.source_character == DetailedSourceCharacter::Relative
|| input.absolute_mode == crate::AbsoluteMode::MakeRelative)
{
MakesSense("Defines whether to process increments only, decrements only or both.")
} else {
HasNoEffect
}
}
Rotate => {
if input.is_feedback {
HasNoEffect
} else if input.control_transformation_produces_relative_values {
MakesSense("-")
} else {
use DetailedSourceCharacter::*;
match input.source_character {
MomentaryOnOffButton | MomentaryVelocitySensitiveButton | Trigger => {
if input.absolute_mode == crate::AbsoluteMode::IncrementalButton {
MakesSense(
"If enabled, jumps from max target value to min target value (or opposite if reverse enabled). Was called \"Rotate\" before.",
)
} else {
HasNoEffect
}
}
RangeControl => {
if input.absolute_mode == crate::AbsoluteMode::MakeRelative {
MakesSense(ROTATE_FOR_RANGE_DESC)
} else {
HasNoEffect
}
}
Relative => {
if input.make_absolute {
MakesSense(
"If enabled, jumps from absolute value 100% to 0% for increments (opposite for decrements). Was called \"Rotate\" before.",
)
} else {
MakesSense(ROTATE_FOR_RANGE_DESC)
}
}
}
}
}
FireMode => {
if input.is_feedback {
HasNoEffect
} else if input.source_is_button()
&& !input.target_is_virtual
&& input.absolute_mode != crate::AbsoluteMode::MakeRelative
{
// Description not interesting, will be queried for specific fire mode only.
MakesSense("-")
} else {
MakesNoSenseUseDefault
}
}
SpecificFireMode(m) => {
use crate::FireMode::*;
match m {
Normal => {
if input.source_character == DetailedSourceCharacter::Trigger {
MakesNoSenseParentTakesCareOfDefault
} else {
MakesSense(
"If min and max is 0 ms, fires immediately on button press. If one of them is > 0 ms, fires on release if the button press duration was in range.",
)
}
}
AfterTimeout => {
if input.source_character == DetailedSourceCharacter::Trigger {
MakesSense("Fires after the specified timeout instead of immediately.")
} else {
MakesSense(
"Fires as soon as button pressed as long as the specified timeout.",
)
}
}
AfterTimeoutKeepFiring => {
if input.source_character == DetailedSourceCharacter::Trigger {
// What sense does it make if we can't turn the turbo off again ...
MakesNoSenseParentTakesCareOfDefault
} else {
MakesSense(
"When button pressed, waits until specified timeout and then fires continuously with the specified rate until button released.",
)
}
}
OnSinglePress => MakesSense("Reacts to single button presses only."),
OnDoublePress => {
MakesSense("Reacts to double button presses only (like a mouse double-click).")
}
}
}
ButtonFilter => {
if input.is_feedback {
HasNoEffect
} else {
use DetailedSourceCharacter::*;
match input.source_character {
MomentaryOnOffButton | MomentaryVelocitySensitiveButton
if input.absolute_mode == crate::AbsoluteMode::Normal =>
{
match input.fire_mode {
crate::FireMode::Normal | crate::FireMode::AfterTimeout | crate::FireMode::AfterTimeoutKeepFiring => {
MakesSense(
"Defines whether to process button presses only, releases only or both.",
)
}
crate::FireMode::OnSinglePress |
crate::FireMode::OnDoublePress => {
// In this case, we need both press and release as input for implementing the fire mode.
// And the output is only press.
MakesNoSenseUseDefault
}
}
}
RangeControl | Trigger => MakesNoSenseUseDefault,
_ => HasNoEffect,
}
}
}
MakeAbsolute => {
if input.is_feedback {
HasNoEffect
} else if input.source_character == DetailedSourceCharacter::Relative
|| input.absolute_mode == crate::AbsoluteMode::IncrementalButton
{
MakesSense(
"Converts relative increments/decrements into an absolute value. This allows you to use control transformation and discontinuous target value sequences but comes with the disadvantage of parameter jumps (which can be mitigated using the jump settings).",
)
} else {
HasNoEffect
}
}
FeedbackType => {
if input.is_feedback {
MakesSense(
"Allows you to switch to textual feedback (to be used with textual sources such as LCDs).",
)
} else {
HasNoEffect
}
}
RoundTargetValue => {
if input.target_is_virtual || input.is_feedback {
HasNoEffect
} else {
use DetailedSourceCharacter::*;
let makes_sense = match input.source_character {
MomentaryOnOffButton | MomentaryVelocitySensitiveButton | Trigger => {
input.absolute_mode == crate::AbsoluteMode::Normal
}
RangeControl => input.absolute_mode != crate::AbsoluteMode::MakeRelative,
Relative => input.make_absolute,
};
if makes_sense {
MakesSense(
"If enabled and target supports it, makes sure the target value is always rounded to discrete values without decimals (e.g. tempo in BPM).",
)
} else {
HasNoEffect
}
}
}
AbsoluteMode => {
if input.is_feedback {
HasNoEffect
} else if input.source_is_button()
|| input.source_character == DetailedSourceCharacter::RangeControl
{
// Description not interesting, will be queried for specific absolute mode only.
MakesSense("-")
} else {
MakesNoSenseUseDefault
}
}
SpecificAbsoluteMode(m) => {
if input.is_feedback {
HasNoEffect
} else if input.control_transformation_uses_time && m == PerformanceControl {
MakesNoSenseUseDefault
} else {
use crate::AbsoluteMode::*;
use DetailedSourceCharacter::*;
match input.source_character {
MomentaryOnOffButton | Trigger | MomentaryVelocitySensitiveButton => {
match m {
Normal => {
if input.source_character == MomentaryVelocitySensitiveButton {
MakesSense(
"When pressing the button, sets the target value to a velocity-dependent value. Sets it back to minimum when releasing it.",
)
} else {
MakesSense(
"Sets target value to its maximum when pressing the button and back to its minimum when releasing it.",
)
}
}
IncrementalButton => {
if input.source_character == MomentaryVelocitySensitiveButton {
MakesSense(
"Increases the target value with each button press with the defined step size range, taking the velocity of the button press into account.",
)
} else {
MakesSense(
"Increases the target value with each button press with the defined min step size.",
)
}
}
ToggleButton => MakesSense(
"Switches the target value between its minimum and maximum on each button press.",
),
MakeRelative | PerformanceControl => MakesNoSenseUseDefault,
}
}
RangeControl => {
match m {
Normal => {
MakesSense(
NORMAL_ABSOLUTE_MODE_FOR_RANGE_DESC,
)
}
MakeRelative => {
MakesSense(
"Attempts to convert incoming absolute control values to relative increments, making it possible to control targets relatively with absolute controls."
)
}
PerformanceControl => {
MakesSense(
"Changes the target value starting from its last position set within REAPER."
)
}
IncrementalButton | ToggleButton => MakesNoSenseParentTakesCareOfDefault
}
}
Relative => {
if input.make_absolute {
match m {
Normal => {
MakesSense(
NORMAL_ABSOLUTE_MODE_FOR_RANGE_DESC,
)
}
MakeRelative | IncrementalButton | ToggleButton | PerformanceControl => MakesNoSenseParentTakesCareOfDefault
}
} else {
HasNoEffect
}
}
}
}
}
GroupInteraction => {
if input.is_feedback || input.target_is_virtual {
HasNoEffect
} else {
// Description not interesting, will be queried for specific interaction only.
MakesSense("-")
}
}
SpecificGroupInteraction(i) => {
if input.is_feedback || input.target_is_virtual {
HasNoEffect
} else {
use crate::GroupInteraction::*;
match i {
None => MakesSense("Other mappings in the same group will not be touched."),
SameControl => {
MakesSense("Other non-virtual mappings in this group will receive the same control value. Unlike \"Same target value\", this will run the complete glue section of the other mapping.")
}
SameTargetValue => {
MakesSense(
"Other non-virtual mappings in this group will receive the same target value as this one with respect to their corresponding target range. This can lead to jumps. If you don't like this, use \"Same control\".",
)
}
InverseControl => {
MakesSense("Other non-virtual mappings in this group will receive the opposite control value. Unlike \"Inverse target value\", this will run the complete glue section of the other mapping.")
}
InverseTargetValue => {
use DetailedSourceCharacter::*;
match input.source_character {
MomentaryOnOffButton | Trigger => {
MakesSense("Other non-virtual mappings in this group will receive the opposite target value, e.g. their targets will be switched off when this target is switched on. Great for making something exclusive within a group!")
}
RangeControl | Relative | MomentaryVelocitySensitiveButton => {
MakesSense(
"Other non-virtual mappings in this group will receive the inverse target value with respect to their corresponding target range. This can lead to jumps. If you don't like this, use \"Inverse control\".",
)
}
}
}
InverseTargetValueOnOnly => {
MakesSense(
"Like \"Inverse target value\" but doesn't apply the inverse to other mappings if the target value is zero. Useful for exclusive toggle buttons.",
)
}
InverseTargetValueOffOnly => {
MakesSense(
"Like \"Inverse target value\" but doesn't apply the inverse to other mappings if the target value is not zero. Useful for exclusive toggle buttons.",
)
}
}
}
}
}
}
@@ -0,0 +1,5 @@
/// Context for mode-related functions.
#[derive(Copy, Clone, Debug, Default)]
pub struct ModeContext<A> {
pub additional_script_input: A,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,330 @@
use crate::{AbsoluteValue, ButtonUsage, FireMode, Interval};
use std::time::{Duration, Instant};
#[derive(Clone, Debug)]
pub struct PressDurationProcessor {
// # Configuration data (stays constant)
fire_mode: FireMode,
interval: Interval<Duration>,
/// Double press detection: How long to wait for a second press
multi_press_span: Duration,
turbo_rate: Duration,
// # Runtime data (changes during usage)
last_button_press: Option<ButtonPress>,
button_usage: ButtonUsage,
}
#[derive(Clone, Debug)]
struct ButtonPress {
time: Instant,
value: AbsoluteValue,
/// Used for after-timeout-keep-firing mode.
time_of_last_turbo_fire: Option<Instant>,
/// Whether we already fired in response to this press.
///
/// Important for after-timeout mode: We must not clear the press on first fire, otherwise we can't
/// decide anymore what will happen on release.
fired_already: bool,
/// Number of tap-downs so far. Used for double-press detection.
tap_down_count: u32,
/// Whether the button has been released already.
///
/// This is relevant for distinction between single and double press. A button press that is
/// released after a short time can still develop into a double press, so we can't clear the press yet.
released: bool,
}
impl ButtonPress {
pub fn new(value: AbsoluteValue) -> Self {
Self {
time: Instant::now(),
value,
time_of_last_turbo_fire: None,
fired_already: false,
tap_down_count: 1,
released: false,
}
}
}
const ZERO_DURATION: Duration = Duration::from_millis(0);
impl Default for PressDurationProcessor {
fn default() -> Self {
Self {
fire_mode: FireMode::Normal,
interval: Interval::new(ZERO_DURATION, ZERO_DURATION),
multi_press_span: Duration::from_millis(300),
turbo_rate: ZERO_DURATION,
last_button_press: None,
button_usage: ButtonUsage::Both,
}
}
}
impl PressDurationProcessor {
pub fn new(
mode: FireMode,
interval: Interval<Duration>,
turbo_rate: Duration,
button_usage: ButtonUsage,
) -> PressDurationProcessor {
PressDurationProcessor {
fire_mode: mode,
interval,
turbo_rate,
button_usage,
..Default::default()
}
}
/// Should be called once at initialization time to check if this processor wants that you call
/// `poll()`, regularly.
pub fn wants_to_be_polled(&self) -> bool {
// This must not depend on the button press state!
use FireMode::*;
match self.fire_mode {
AfterTimeout | AfterTimeoutKeepFiring | OnSinglePress => true,
Normal | OnDoublePress => false,
}
}
pub fn process_press_or_release(
&mut self,
control_value: AbsoluteValue,
button_usage: ButtonUsage,
) -> Option<AbsoluteValue> {
let min = self.interval.min_val();
let max = self.interval.max_val();
match self.fire_mode {
FireMode::Normal => {
// In the past, the button usage setting was always checked before processing the press duration.
// In normal fire mode, we keep doing it that way (although the button setting actually doesn't make
// sense in case of min/max being > 0).
if button_usage.should_ignore(control_value) {
return None;
}
if min == ZERO_DURATION && max == ZERO_DURATION {
// No-op case: Just fire immediately. If just min is zero, we don't fire
// immediately but wait for button release. That way we can support different
// stacked press durations (or just "fire on release" behavior no matter the
// press duration if user chooses max very high)!
return Some(control_value);
}
if control_value.is_on() {
// This is a button press.
// Don't fire now because we don't know yet how long it will be pressed.
self.last_button_press = Some(ButtonPress::new(control_value));
None
} else {
// Looks like a button release.
// Measure duration since button press.
match self.last_button_press.take() {
// Button has not been pressed before. Just ignore.
None => None,
// Button has been pressed before.
Some(press) => {
if self.interval.contains(press.time.elapsed()) {
// Duration within interval. Fire initial press value.
Some(press.value)
} else {
// Released too early or too late.
None
}
}
}
}
}
FireMode::AfterTimeout => {
// This fire mode has been improved in 2.16.0 to let button release fire 0% if not prevented
// by button usage setting.
if min == ZERO_DURATION {
// No-op case: Fire immediately.
if button_usage.should_ignore(control_value) {
return None;
}
return Some(control_value);
}
if control_value.is_on() {
// Button press
self.last_button_press = Some(ButtonPress::new(control_value));
None
} else {
// Button release
self.process_timeout_button_release(control_value)
}
}
FireMode::AfterTimeoutKeepFiring => {
// In the past, the button usage setting was always checked before processing the press duration.
// We should keep doing it that way in order to not destroy existing setups. Also, that makes it
// possible to keep firing even after releasing a button!
if button_usage.should_ignore(control_value) {
return None;
}
if control_value.is_on() {
// Button press
let mut button_press = ButtonPress::new(control_value);
let result = if min == ZERO_DURATION {
// No initial delay. Fire immediately and count as first turbo fire!
button_press.time_of_last_turbo_fire = Some(Instant::now());
Some(control_value)
} else {
// Initial delay (wait for timeout).
None
};
self.last_button_press = Some(button_press);
result
} else {
// Button release
self.process_timeout_button_release(control_value)
}
}
FireMode::OnSinglePress => {
// Button usage setting doesn't make sense here. We need to process both press and release but only
// output press. That's why we started hiding the dropdown in 2.16.1. If someone has previously used
// the button filter together with this fire mode, it would have been a weird misconfiguration,
// qualifying as "undefined behavior". Breaking change is okay.
if control_value.is_on() {
// Button press
if let Some(press) = self.last_button_press.as_mut() {
// Must be more than single press already.
press.tap_down_count += 1;
press.time = Instant::now();
} else {
// First press
self.last_button_press = Some(ButtonPress::new(control_value));
};
None
} else {
// Button release.
let fire_value = {
let press = self.last_button_press.as_mut()?;
if press.tap_down_count != 1 {
return None;
}
let elapsed = press.time.elapsed();
if elapsed < self.multi_press_span {
press.released = true;
return None;
}
if self.interval.max_val() != ZERO_DURATION
&& elapsed > self.interval.max_val()
{
// Exceeded max press time
return None;
}
press.value
};
self.last_button_press = None;
Some(fire_value)
}
}
FireMode::OnDoublePress => {
// Button usage setting doesn't make sense here. We need to process both press and release but only
// output press. That's why we started hiding the dropdown in 2.16.1. If someone has previously used
// the button filter together with this fire mode, it would have been a weird misconfiguration,
// qualifying as "undefined behavior". Breaking change is okay.
if control_value.is_on() {
if let Some(press) = &self.last_button_press {
// Button was pressed before
let (result, next_press) = if press.time.elapsed() <= self.multi_press_span
{
// Double press detected
(Some(press.value), None)
} else {
// Previous press too long in past. Handle just like first press.
(None, Some(ButtonPress::new(control_value)))
};
self.last_button_press = next_press;
result
} else {
// First press
self.last_button_press = Some(ButtonPress::new(control_value));
None
}
} else {
// Button release
None
}
}
}
}
/// Should be called regularly if `wants_to_be_polled()` returned `true` at initialization
/// time.
pub fn poll(&mut self) -> Option<AbsoluteValue> {
match self.fire_mode {
FireMode::Normal | FireMode::OnDoublePress => None,
FireMode::AfterTimeout => {
let last_button_press = self.last_button_press.as_mut()?;
if last_button_press.fired_already
|| last_button_press.time.elapsed() < self.interval.min_val()
{
return None;
}
last_button_press.fired_already = true;
Some(last_button_press.value)
}
FireMode::AfterTimeoutKeepFiring => {
let last_button_press = self.last_button_press.as_mut()?;
if let Some(last_turbo) = last_button_press.time_of_last_turbo_fire {
// We are in turbo stage already.
if last_turbo.elapsed() >= self.turbo_rate {
// Subsequent turbo fire!
last_button_press.time_of_last_turbo_fire = Some(Instant::now());
Some(last_button_press.value)
} else {
// Not yet time for next turbo fire.
None
}
} else if last_button_press.time.elapsed() >= self.interval.min_val() {
// We reached the initial delay. First turbo fire!
last_button_press.time_of_last_turbo_fire = Some(Instant::now());
Some(last_button_press.value)
} else {
None
}
}
FireMode::OnSinglePress => {
let fire_value = {
let press = self.last_button_press.as_ref()?;
let elapsed = press.time.elapsed();
if elapsed < self.multi_press_span {
// Can't decide yet if this is a single press.
return None;
}
if self.interval.max_val() > ZERO_DURATION && !press.released {
// The button is still being hold.
if elapsed > self.interval.max_val() {
// The maximum hold time is already exceeded. Reset!
self.last_button_press = None;
}
return None;
}
if press.tap_down_count > 1 {
// Button was pressed more than one time and waiting time is over. Reset!
self.last_button_press = None;
return None;
}
press.value
};
self.last_button_press = None;
Some(fire_value)
}
}
}
fn process_timeout_button_release(
&mut self,
control_value: AbsoluteValue,
) -> Option<AbsoluteValue> {
let last_button_press = self.last_button_press.take()?;
if self.button_usage == ButtonUsage::PressOnly {
return None;
}
if last_button_press.time.elapsed() < self.interval.min_val() {
return None;
}
Some(control_value)
}
}
@@ -0,0 +1,151 @@
use crate::{AbsoluteValue, UnitValue};
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum ControlType {
/// Targets which don't have a step size, targets that have just on/off states and trigger
/// targets.
AbsoluteContinuous,
/// The only difference to AbsoluteContinuous is that it gets retriggered even it already has
/// the desired target value.
///
/// This is commonly used for targets that can be triggered only instead of having a possible
/// range of values (e.g. load FX snapshot). But it can also be used for targets that can be
/// triggered with different values and still are somehow "trigger-like"
/// (e.g. send MIDI message).
AbsoluteContinuousRetriggerable,
/// Imagine a "tempo" target: Musical tempo is continuous in nature and still you might want to
/// offer the possibility to round on fraction-less bpm values. Discrete and continuous at the
/// same time.
///
/// In more recent versions, the same can be achieved using target value sequences.
AbsoluteContinuousRoundable { rounding_step_size: UnitValue },
/// Targets which have a grid of discrete values (and therefore a step size).
AbsoluteDiscrete {
atomic_step_size: UnitValue,
is_retriggerable: bool,
},
/// If target wants to be controlled via relative increments.
Relative,
/// For virtual continuous targets (that don't know about the nature of the real target).
VirtualMulti,
/// For virtual button targets (that don't know about the nature of the real target).
VirtualButton,
}
impl ControlType {
pub fn is_relative(&self) -> bool {
*self == ControlType::Relative
}
pub fn is_retriggerable(&self) -> bool {
matches!(
self,
ControlType::AbsoluteContinuousRetriggerable
| ControlType::AbsoluteDiscrete {
is_retriggerable: true,
..
}
)
}
pub fn step_size(&self) -> Option<UnitValue> {
use ControlType::*;
match self {
AbsoluteContinuousRoundable { rounding_step_size } => Some(*rounding_step_size),
AbsoluteDiscrete {
atomic_step_size, ..
} => Some(*atomic_step_size),
_ => None,
}
}
pub fn discrete_count(&self) -> Option<u32> {
Some(self.discrete_max()? + 1)
}
pub fn discrete_max(&self) -> Option<u32> {
let step_size = self.step_size()?;
if step_size.is_zero() {
return None;
}
Some((1.0 / step_size.get()).round() as u32)
}
pub fn is_virtual(&self) -> bool {
use ControlType::*;
matches!(self, VirtualMulti | VirtualButton)
}
}
pub trait Target<'a> {
type Context: Copy;
/// Should return the current value of the target.
///
/// Some targets don't have the notion of a current value, e.g. virtual targets (which are just
/// mediators really). Other targets might momentarily not be able to return a current value.
/// In such cases, `None` should be returned so that the mode can handle this situation
/// gracefully. Of course, some mode features won't work without knowing the current value,
/// but others will still work.
fn current_value(&self, context: Self::Context) -> Option<AbsoluteValue> {
let _ = context;
None
}
fn control_type(&self, context: Self::Context) -> ControlType {
let _ = context;
ControlType::AbsoluteContinuous
}
}
/// Some standardized property keys.
pub mod target_prop_keys {
/// Short text representing the current target value, including a possible unit.
///
/// This is the default value shown if textual feedback is enabled and the textual feedback
/// expression is empty. Choose the textual representation that's most likely to be desired.
/// If there's some name to display, prefer that name over a numeric representation.
///
/// Examples:
///
/// - Track: Volume → "-6.00 dB"
/// - Track: Mute/unmute → "Mute"
/// - Project: Browse tracks → "Guitar"
pub const TEXT_VALUE: &str = "text_value";
/// Non-normalized representing the current target value as a *human-friendly number*
/// (type: [`crate::NumericValue`]).
///
/// The purpose of this is to allow for more freedom in formatting numerical target values than
/// when using [`TEXT_VALUE`]. Future versions of ReaLearn might extend textual feedback
/// expressions in a way so the user can define how exactly the numerical value is presented
/// (decimal points etc.).
///
/// "Human-readable" also means that if it's a position, then it's really a position number
/// (one-rooted), not an index number (zero-rooted).
///
/// - Track: Volume → -6.00
/// - Track: Mute/unmute → 1.0
/// - Project: Browse tracks → 5
pub const NUMERIC_VALUE: &str = "numeric_value";
/// Unit of the non-normalized number in human-friendly form.
///
/// - Track: Volume → "dB"
/// - Track: Mute/unmute → ""
/// - Project: Browse tracks → ""
pub const NUMERIC_VALUE_UNIT: &str = "numeric_value.unit";
/// Normalized value in the unit interval. You can think of it as a percentage.
///
/// This value is available for most targets and good if you need a totally uniform
/// representation of the target value that doesn't differ between target types. By default,
/// this is formatted as percentage. Future versions of ReaLearn might offer user-defined
/// formatting. E.g. this will also be the preferred form to format on/off states in a
/// custom way (where 0% represents "off").
///
/// - Track: Volume → 0.5
/// - Track: Mute/unmute → 0.0
/// - Project: Browse tracks → 0.7
pub const NORMALIZED_VALUE: &str = "normalized_value";
}
@@ -0,0 +1,80 @@
use crate::{
AbsoluteValue, ControlType, ControlValueKind, FeedbackScript, FeedbackScriptInput,
FeedbackScriptOutput, Target, Transformation, TransformationInput, TransformationOutput,
};
use base::hash_util::NonCryptoHashSet;
use std::borrow::Cow;
use std::error::Error;
pub struct TestTarget {
pub current_value: Option<AbsoluteValue>,
pub control_type: ControlType,
}
impl<'a> Target<'a> for TestTarget {
type Context = ();
fn current_value(&self, _: ()) -> Option<AbsoluteValue> {
self.current_value
}
fn control_type(&self, _: ()) -> ControlType {
self.control_type
}
}
pub struct TestTransformation {
transformer: Box<dyn Fn(f64) -> Result<f64, &'static str>>,
produced_kind: ControlValueKind,
}
impl TestTransformation {
pub fn new(
produced_kind: ControlValueKind,
transformer: impl Fn(f64) -> Result<f64, &'static str> + 'static,
) -> TestTransformation {
Self {
transformer: Box::new(transformer),
produced_kind,
}
}
}
impl Transformation for TestTransformation {
type AdditionalInput = ();
fn transform(
&self,
input: TransformationInput<Self::AdditionalInput>,
) -> Result<TransformationOutput, &'static str> {
let out_val = (self.transformer)(input.event.input_value)?;
let out = TransformationOutput {
produced_kind: self.produced_kind,
value: Some(out_val),
instruction: None,
};
Ok(out)
}
fn wants_to_be_polled(&self) -> bool {
false
}
}
pub struct TestFeedbackScript;
impl FeedbackScript<'_> for TestFeedbackScript {
type AdditionalInput = ();
fn feedback(
&self,
_: FeedbackScriptInput,
_: (),
) -> Result<FeedbackScriptOutput, Cow<'static, str>> {
unimplemented!()
}
fn used_props(&self) -> Result<NonCryptoHashSet<String>, Box<dyn Error>> {
Ok(Default::default())
}
}
@@ -0,0 +1,100 @@
use crate::{
ControlValue, ControlValueKind, DiscreteIncrement, Fraction, UnitIncrement, UnitValue,
};
use std::time::Duration;
/// Represents an arbitrary transformation from one unit value into another one, intended to be
/// implemented by using some form of expression language.
pub trait Transformation {
type AdditionalInput: Default;
/// Applies the transformation.
///
/// Should execute fast. If you use an expression or scripting language, make sure that you
/// compile the expression beforehand.
fn transform(
&self,
input: TransformationInput<Self::AdditionalInput>,
) -> Result<TransformationOutput, &'static str>;
fn wants_to_be_polled(&self) -> bool;
}
#[derive(Default)]
pub struct TransformationInput<A> {
pub event: TransformationInputEvent,
pub context: TransformationInputContext,
/// Consumers can pass through more stuff to the transformation script if they want.
pub additional_input: A,
}
#[derive(Default)]
pub struct TransformationInputEvent {
pub input_value: f64,
pub timestamp: Duration,
}
#[derive(Default)]
pub struct TransformationInputContext {
pub output_value: f64,
/// Duration since last interaction. For modulations/transitions only.
pub rel_time: Duration,
}
/// Output of the transformation.
///
/// If both `value` and `instruction` are `None`, it means that the target shouldn't be invoked:
///
/// - Usually, each repeated invocation always results in a target invocation (unless the target is
/// not retriggerable and already has the desired value).
/// - Sometimes this is not desired. In this case, one can return `none`, in which case the target
/// will not be touched.
/// - Good for transitions that are not continuous, especially if other mappings want to control
/// the parameter as well from time to time.
#[derive(Copy, Clone, Debug)]
pub struct TransformationOutput {
/// The kind of control values which this transformation produces.
///
/// This should always be available, as it might be queried statically for GUI purposes.
pub produced_kind: ControlValueKind,
pub value: Option<f64>,
pub instruction: Option<TransformationInstruction>,
}
impl TransformationOutput {
pub fn extract_control_value(&self, in_discrete_max: Option<u32>) -> Option<ControlValue> {
let raw = self.value?;
let cv = match self.produced_kind {
ControlValueKind::AbsoluteContinuous => {
ControlValue::AbsoluteContinuous(UnitValue::new_clamped(raw))
}
ControlValueKind::RelativeDiscrete => {
let inc = raw.round() as i32;
ControlValue::RelativeDiscrete(DiscreteIncrement::new_checked(inc)?)
}
ControlValueKind::RelativeContinuous => {
ControlValue::RelativeContinuous(UnitIncrement::new_clamped_checked(raw)?)
}
ControlValueKind::AbsoluteDiscrete => {
let actual = raw.round() as _;
let max = match in_discrete_max {
None => actual,
Some(max) => std::cmp::max(max, actual),
};
ControlValue::AbsoluteDiscrete(Fraction::new(actual, max))
}
};
Some(cv)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum TransformationInstruction {
/// This stops repeated invocation of the formula until the mapping is triggered again.
///
/// - Good for building transitions with a defined end.
/// - Stopping the invocation at some point is also important if the same parameter shall be
/// controlled by other mappings as well. If multiple mappings continuously change the target
/// parameter, only the last one wins.
Stop,
}
@@ -0,0 +1,473 @@
use crate::mode::value_sequence::parser::RawEntry;
use crate::{
format_percentage_without_unit, parse_percentage_without_unit, UnitValue, BASE_EPSILON,
};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::convert::TryInto;
use std::fmt;
use std::fmt::{Debug, Display, Formatter, Write};
#[derive(Clone, Eq, PartialEq, Debug, Default, SerializeDisplay, DeserializeFromStr)]
pub struct ValueSequence {
entries: Vec<ValueSequenceEntry>,
}
impl ValueSequence {
pub fn parse<P: ValueParser>(
single_value_parser: &P,
input: &str,
) -> Result<Self, &'static str> {
let (_, raw_entries) =
super::parser::parse_entries(input).map_err(|_| "couldn't parse sequence")?;
let sequence = ValueSequence {
entries: {
raw_entries
.iter()
.map(|e| match e {
RawEntry::SingleValue(e) => ValueSequenceEntry::SingleValue(
single_value_parser.parse_value(e).unwrap_or_default(),
),
RawEntry::Range(e) => {
let entry = ValueSequenceRangeEntry {
from: single_value_parser
.parse_value(e.simple_range.from)
.unwrap_or_default(),
to: single_value_parser
.parse_value(e.simple_range.to)
.unwrap_or_default(),
step_size: e
.step_size
.map(|s| single_value_parser.parse_step(s).unwrap_or_default()),
};
ValueSequenceEntry::Range(entry)
}
})
.collect()
},
};
Ok(sequence)
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn entries(&self) -> &[ValueSequenceEntry] {
&self.entries
}
pub fn displayable<'a>(&'a self, f: &'a impl ValueFormatter) -> impl Display + 'a {
DisplayableValueSequence {
value_sequence: self,
value_formatter: f,
}
}
pub fn unpack(&self, default_step_size: UnitValue) -> Vec<UnitValue> {
self.entries
.iter()
.flat_map(|e| WithDefaultStepSize::new(e, default_step_size))
.collect()
}
}
struct WithDefaultStepSize<'a, A> {
actual: &'a A,
default_step_size: UnitValue,
}
impl<'a, A> WithDefaultStepSize<'a, A> {
fn new(actual: &'a A, default_step_size: UnitValue) -> Self {
Self {
actual,
default_step_size,
}
}
}
struct WithFormatter<'a, A, F: ValueFormatter> {
actual: &'a A,
value_formatter: &'a F,
}
impl<'a, A, F: ValueFormatter> WithFormatter<'a, A, F> {
fn new(actual: &'a A, value_formatter: &'a F) -> Self {
WithFormatter {
actual,
value_formatter,
}
}
}
struct DisplayableValueSequence<'a, F> {
value_sequence: &'a ValueSequence,
value_formatter: &'a F,
}
impl<F: ValueFormatter> Display for DisplayableValueSequence<'_, F> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let snippets: Vec<_> = self
.value_sequence
.entries
.iter()
.map(|e| WithFormatter::new(e, self.value_formatter).to_string())
.collect();
let csv = snippets.join(", ");
f.write_str(&csv)
}
}
pub trait ValueFormatter {
fn format_value(&self, value: UnitValue, f: &mut fmt::Formatter) -> fmt::Result;
fn format_step(&self, value: UnitValue, f: &mut fmt::Formatter) -> fmt::Result;
}
pub trait ValueParser {
fn parse_value(&self, text: &str) -> Result<UnitValue, &'static str>;
fn parse_step(&self, text: &str) -> Result<UnitValue, &'static str>;
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum ValueSequenceEntry {
SingleValue(UnitValue),
Range(ValueSequenceRangeEntry),
}
impl<F: ValueFormatter> Display for WithFormatter<'_, ValueSequenceEntry, F> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ValueSequenceEntry as E;
match self.actual {
E::SingleValue(v) => self.value_formatter.format_value(*v, f),
E::Range(r) => WithFormatter::new(r, self.value_formatter).fmt(f),
}
}
}
impl IntoIterator for WithDefaultStepSize<'_, ValueSequenceEntry> {
type Item = UnitValue;
type IntoIter = ValueSequenceRangeIterator;
fn into_iter(self) -> ValueSequenceRangeIterator {
use ValueSequenceEntry as E;
match self.actual {
E::SingleValue(uv) => {
let simple_range_entry = ValueSequenceRangeEntry {
from: *uv,
to: *uv,
step_size: Some(UnitValue::MAX),
};
WithDefaultStepSize::new(&simple_range_entry, self.default_step_size).into_iter()
}
E::Range(r) => WithDefaultStepSize::new(r, self.default_step_size).into_iter(),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct ValueSequenceRangeEntry {
from: UnitValue,
to: UnitValue,
step_size: Option<UnitValue>,
}
impl<F: ValueFormatter> Display for WithFormatter<'_, ValueSequenceRangeEntry, F> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.value_formatter.format_value(self.actual.from, f)?;
f.write_str(" - ")?;
self.value_formatter.format_value(self.actual.to, f)?;
if let Some(step_size) = self.actual.step_size {
f.write_str(" (")?;
self.value_formatter.format_step(step_size, f)?;
f.write_char(')')?;
}
Ok(())
}
}
impl IntoIterator for WithDefaultStepSize<'_, ValueSequenceRangeEntry> {
type Item = UnitValue;
type IntoIter = ValueSequenceRangeIterator;
fn into_iter(self) -> ValueSequenceRangeIterator {
ValueSequenceRangeIterator {
i: self.actual.from.get(),
from: self.actual.from.get(),
to: self.actual.to.get(),
step_size: self
.actual
.step_size
.unwrap_or(self.default_step_size)
.get(),
}
}
}
pub struct ValueSequenceRangeIterator {
i: f64,
from: f64,
to: f64,
step_size: f64,
}
impl Iterator for ValueSequenceRangeIterator {
type Item = UnitValue;
fn next(&mut self) -> Option<UnitValue> {
if self.step_size == 0.0 {
return None;
}
let i = self.i;
self.i = if self.from <= self.to {
// Forward
if i > self.to + BASE_EPSILON {
return None;
}
i + self.step_size
} else {
// Backward
if i + BASE_EPSILON < self.to {
return None;
}
i - self.step_size
};
i.try_into().ok()
}
}
impl Display for ValueSequence {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.displayable(&UnitValueIo).fmt(f)
}
}
impl std::str::FromStr for ValueSequence {
type Err = &'static str;
fn from_str(input: &str) -> Result<Self, Self::Err> {
ValueSequence::parse(&UnitValueIo, input)
}
}
pub struct UnitValueIo;
impl ValueFormatter for UnitValueIo {
fn format_value(&self, value: UnitValue, f: &mut Formatter) -> fmt::Result {
write!(f, "{value}")
}
fn format_step(&self, value: UnitValue, f: &mut Formatter) -> fmt::Result {
self.format_value(value, f)
}
}
impl ValueParser for UnitValueIo {
fn parse_value(&self, text: &str) -> Result<UnitValue, &'static str> {
text.parse()
}
fn parse_step(&self, text: &str) -> Result<UnitValue, &'static str> {
self.parse_value(text)
}
}
pub struct PercentIo;
impl ValueFormatter for PercentIo {
fn format_value(&self, value: UnitValue, f: &mut Formatter) -> fmt::Result {
f.write_str(&format_percentage_without_unit(value.get()))
}
fn format_step(&self, value: UnitValue, f: &mut Formatter) -> fmt::Result {
self.format_value(value, f)
}
}
impl ValueParser for PercentIo {
fn parse_value(&self, text: &str) -> Result<UnitValue, &'static str> {
parse_percentage_without_unit(text)?.try_into()
}
fn parse_step(&self, text: &str) -> Result<UnitValue, &'static str> {
self.parse_value(text)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
struct TestValueContext;
impl ValueFormatter for TestValueContext {
fn format_value(&self, value: UnitValue, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", (value.get() * 1000.0) as u32)
}
fn format_step(&self, value: UnitValue, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", value)
}
}
impl ValueParser for TestValueContext {
fn parse_value(&self, text: &str) -> Result<UnitValue, &'static str> {
let number: u32 = text.parse().map_err(|_| "")?;
(number as f64 / 1000.0).try_into()
}
fn parse_step(&self, text: &str) -> Result<UnitValue, &'static str> {
text.parse()
}
}
fn default_test_step_size() -> UnitValue {
UnitValue::new(0.001)
}
#[test]
fn simple_values() {
// Given
let sequence = ValueSequence::parse(&TestValueContext, "250, 500, 750, 500, 1000").unwrap();
// When
// Then
assert_eq!(
sequence.entries(),
&[
ValueSequenceEntry::SingleValue(uv(0.25)),
ValueSequenceEntry::SingleValue(uv(0.50)),
ValueSequenceEntry::SingleValue(uv(0.75)),
ValueSequenceEntry::SingleValue(uv(0.50)),
ValueSequenceEntry::SingleValue(uv(1.00)),
]
);
assert_eq!(
sequence.unpack(default_test_step_size()),
vec![uv(0.25), uv(0.50), uv(0.75), uv(0.50), uv(1.00)]
);
assert_eq!(
&sequence.displayable(&TestValueContext).to_string(),
"250, 500, 750, 500, 1000"
);
assert_eq!(&sequence.to_string(), "0.25, 0.5, 0.75, 0.5, 1")
}
#[test]
fn ranges_native() {
// Given
let sequence = ValueSequence::parse(
&TestValueContext,
"250 - 255, 500 - 501, 750 - 755 (0.002), 520 - 500(0.01), 999",
)
.unwrap();
// When
// Then
assert_eq!(
sequence.entries(),
&[
ValueSequenceEntry::Range(ValueSequenceRangeEntry {
from: uv(0.250),
to: uv(0.255),
step_size: None
}),
ValueSequenceEntry::Range(ValueSequenceRangeEntry {
from: uv(0.500),
to: uv(0.501),
step_size: None
}),
ValueSequenceEntry::Range(ValueSequenceRangeEntry {
from: uv(0.750),
to: uv(0.755),
step_size: Some(uv(0.002))
}),
ValueSequenceEntry::Range(ValueSequenceRangeEntry {
from: uv(0.520),
to: uv(0.500),
step_size: Some(uv(0.010))
}),
ValueSequenceEntry::SingleValue(uv(0.999))
]
);
assert_eq!(
sequence.unpack(default_test_step_size()),
vec![
uv(0.250),
uv(0.251),
uv(0.252),
uv(0.253),
uv(0.254),
uv(0.255),
uv(0.500),
uv(0.501),
uv(0.750),
uv(0.752),
uv(0.754),
uv(0.520),
uv(0.510),
uv(0.500),
uv(0.999)
]
);
assert_eq!(
&sequence.displayable(&TestValueContext).to_string(),
"250 - 255, 500 - 501, 750 - 755 (0.002), 520 - 500 (0.01), 999"
);
assert_eq!(
&sequence.to_string(),
"0.25 - 0.255, 0.5 - 0.501, 0.75 - 0.755 (0.002), 0.52 - 0.5 (0.01), 0.999"
)
}
#[test]
fn ranges_rounding() {
// Given
let sequence = ValueSequence::parse(&PercentIo, "25 - 50, 75, 50, 10").unwrap();
// When
let unpacked = sequence.unpack(UnitValue::new(0.01));
// Then
assert_eq!(unpacked.len(), 29);
let at = |i| *unpacked.get(i).unwrap();
assert_abs_diff_eq!(at(0), uv(0.25));
assert_abs_diff_eq!(at(1), uv(0.26));
assert_abs_diff_eq!(at(2), uv(0.27));
assert_abs_diff_eq!(at(3), uv(0.28));
assert_abs_diff_eq!(at(4), uv(0.29));
assert_abs_diff_eq!(at(5), uv(0.30));
assert_abs_diff_eq!(at(6), uv(0.31));
assert_abs_diff_eq!(at(7), uv(0.32));
assert_abs_diff_eq!(at(8), uv(0.33));
assert_abs_diff_eq!(at(9), uv(0.34));
assert_abs_diff_eq!(at(10), uv(0.35));
assert_abs_diff_eq!(at(11), uv(0.36));
assert_abs_diff_eq!(at(12), uv(0.37));
assert_abs_diff_eq!(at(13), uv(0.38));
assert_abs_diff_eq!(at(14), uv(0.39));
assert_abs_diff_eq!(at(15), uv(0.40));
assert_abs_diff_eq!(at(16), uv(0.41));
assert_abs_diff_eq!(at(17), uv(0.42));
assert_abs_diff_eq!(at(18), uv(0.43));
assert_abs_diff_eq!(at(19), uv(0.44));
assert_abs_diff_eq!(at(20), uv(0.45));
assert_abs_diff_eq!(at(21), uv(0.46));
assert_abs_diff_eq!(at(22), uv(0.47));
assert_abs_diff_eq!(at(23), uv(0.48));
assert_abs_diff_eq!(at(24), uv(0.49));
assert_abs_diff_eq!(at(25), uv(0.50));
assert_abs_diff_eq!(at(26), uv(0.75));
assert_abs_diff_eq!(at(27), uv(0.50));
assert_abs_diff_eq!(at(28), uv(0.10));
}
#[test]
fn range_corner_cases() {
// Given
let sequence =
ValueSequence::parse(&TestValueContext, "250 - 250, 500 - 501 (0), 601 - 600 (0)")
.unwrap();
// When
// Then
assert_eq!(sequence.unpack(default_test_step_size()), vec![uv(0.250)]);
}
fn uv(value: f64) -> UnitValue {
UnitValue::new(value)
}
}
@@ -0,0 +1,3 @@
mod base;
mod parser;
pub use base::*;
@@ -0,0 +1,165 @@
use nom::branch::alt;
use nom::character::complete::{space0, space1};
use nom::combinator::opt;
use nom::multi::separated_list0;
use nom::sequence::separated_pair;
use nom::{
bytes::complete::is_not, character::complete::char, sequence::delimited, sequence::tuple,
IResult,
};
fn parse_value(input: &str) -> IResult<&str, &str> {
let parser = is_not("(), ");
parser(input)
}
fn parse_step_size(input: &str) -> IResult<&str, &str> {
delimited(
tuple((char('('), space0)),
parse_value,
tuple((space0, char(')'))),
)(input)
}
fn parse_simple_range(input: &str) -> IResult<&str, RawSimpleRange> {
let mut parser = separated_pair(parse_value, tuple((space1, char('-'), space1)), parse_value);
let (remainder, (from, to)) = parser(input)?;
Ok((remainder, RawSimpleRange { from, to }))
}
fn parse_full_range(input: &str) -> IResult<&str, RawFullRange> {
let mut parser = tuple((parse_simple_range, space0, opt(parse_step_size)));
let (remainder, (simple_range, _, step_size)) = parser(input)?;
Ok((remainder, RawFullRange::new(simple_range, step_size)))
}
fn parse_range_entry(input: &str) -> IResult<&str, RawEntry> {
let (remainder, range) = parse_full_range(input)?;
Ok((remainder, RawEntry::Range(range)))
}
fn parse_single_value_entry(input: &str) -> IResult<&str, RawEntry> {
let (remainder, single_value) = parse_value(input)?;
Ok((remainder, RawEntry::SingleValue(single_value)))
}
fn parse_entry(input: &str) -> IResult<&str, RawEntry> {
let mut parser = alt((parse_range_entry, parse_single_value_entry));
parser(input)
}
pub fn parse_entries(input: &str) -> IResult<&str, Vec<RawEntry>> {
let mut parser = separated_list0(tuple((space0, char(','), space0)), parse_entry);
parser(input)
}
#[derive(Eq, PartialEq, Debug)]
pub enum RawEntry<'a> {
SingleValue(&'a str),
Range(RawFullRange<'a>),
}
#[derive(Eq, PartialEq, Debug)]
pub struct RawFullRange<'a> {
pub simple_range: RawSimpleRange<'a>,
pub step_size: Option<&'a str>,
}
impl<'a> RawFullRange<'a> {
fn new(simple_range: RawSimpleRange<'a>, step_size: Option<&'a str>) -> Self {
Self {
simple_range,
step_size,
}
}
}
#[derive(Eq, PartialEq, Debug)]
pub struct RawSimpleRange<'a> {
pub from: &'a str,
pub to: &'a str,
}
#[allow(clippy::needless_lifetimes)]
impl<'a> RawSimpleRange<'a> {
#[cfg(test)]
fn new(from: &'a str, to: &'a str) -> Self {
Self { from, to }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn step_size() {
assert_eq!(parse_step_size("(0.5)"), Ok(("", "0.5")));
assert_eq!(parse_step_size("(8)"), Ok(("", "8")));
assert_eq!(parse_step_size("( abc )"), Ok(("", "abc")));
}
#[test]
fn simple_range() {
assert_eq!(
parse_simple_range("5 - 10"),
Ok(("", RawSimpleRange::new("5", "10")))
);
assert_eq!(
parse_simple_range("5.0 - 10.0"),
Ok(("", RawSimpleRange::new("5.0", "10.0")))
);
assert_eq!(
parse_simple_range("a - f"),
Ok(("", RawSimpleRange::new("a", "f")))
);
}
#[test]
fn full_range() {
assert_eq!(
parse_full_range("5 - 10"),
Ok(("", RawFullRange::new(RawSimpleRange::new("5", "10"), None)))
);
assert_eq!(
parse_full_range("5 - 10 (0.1)"),
Ok((
"",
RawFullRange::new(RawSimpleRange::new("5", "10"), Some("0.1"))
))
);
}
#[test]
fn entry() {
assert_eq!(
parse_entry("5 - 10 (0.1)"),
Ok((
"",
RawEntry::Range(RawFullRange::new(
RawSimpleRange::new("5", "10"),
Some("0.1")
))
))
);
assert_eq!(parse_entry("75.5"), Ok(("", RawEntry::SingleValue("75.5"))));
}
#[test]
fn entries() {
assert_eq!(
parse_entries("5 - 10 (0.1), 12.5, 15 - 20"),
Ok((
"",
vec![
RawEntry::Range(RawFullRange::new(
RawSimpleRange::new("5", "10"),
Some("0.1")
)),
RawEntry::SingleValue("12.5"),
RawEntry::Range(RawFullRange::new(RawSimpleRange::new("15", "20"), None))
]
))
);
}
}
@@ -0,0 +1,22 @@
use crate::RgbColor;
// Initially taken from https://github.com/jamesmunns/launch-rs/blob/master/lib/src/color.rs
pub fn find_closest_color_in_palette(color: RgbColor, palette: &[RgbColor]) -> u8 {
let (red, green, blue) = (color.r(), color.g(), color.b());
let mut ifurthest = 0usize;
let mut furthest = 3 * 255_i32.pow(2) + 1;
for (i, c) in palette.iter().enumerate() {
if red == c.r() && green == c.g() && blue == c.b() {
// Exact match
return i as u8;
}
let distance = (red as i32 - c.r() as i32).pow(2)
+ (green as i32 - c.g() as i32).pow(2)
+ (blue as i32 - c.b() as i32).pow(2);
if distance < furthest {
furthest = distance;
ifurthest = i;
}
}
ifurthest as u8
}
@@ -0,0 +1,136 @@
//! Initially taken from https://github.com/jamesmunns/launch-rs/blob/master/lib/src/color.rs
use crate::RgbColor;
/// http://launchpaddr.com/mk2palette/
pub const COLOR_PALETTE: [RgbColor; 128] = [
// 0..64
RgbColor::new(0x00, 0x00, 0x00),
RgbColor::new(0x1c, 0x1c, 0x1c),
RgbColor::new(0x7c, 0x7c, 0x7c),
RgbColor::new(0xfc, 0xfc, 0xfc),
RgbColor::new(0xff, 0x4e, 0x48),
RgbColor::new(0xfe, 0x0a, 0x00),
RgbColor::new(0x5a, 0x00, 0x00),
RgbColor::new(0x18, 0x00, 0x02),
RgbColor::new(0xff, 0xbc, 0x63),
RgbColor::new(0xff, 0x57, 0x00),
RgbColor::new(0x5a, 0x1d, 0x00),
RgbColor::new(0x24, 0x18, 0x02),
RgbColor::new(0xfd, 0xfd, 0x21),
RgbColor::new(0xfd, 0xfd, 0x00),
RgbColor::new(0x58, 0x58, 0x00),
RgbColor::new(0x18, 0x18, 0x00),
RgbColor::new(0x81, 0xfd, 0x2b),
RgbColor::new(0x40, 0xfd, 0x01),
RgbColor::new(0x16, 0x58, 0x00),
RgbColor::new(0x13, 0x28, 0x01),
RgbColor::new(0x35, 0xfd, 0x2b),
RgbColor::new(0x00, 0xfe, 0x00),
RgbColor::new(0x00, 0x58, 0x01),
RgbColor::new(0x00, 0x18, 0x00),
RgbColor::new(0x35, 0xfc, 0x47),
RgbColor::new(0x00, 0xfe, 0x00),
RgbColor::new(0x00, 0x58, 0x01),
RgbColor::new(0x00, 0x18, 0x00),
RgbColor::new(0x32, 0xfd, 0x7f),
RgbColor::new(0x00, 0xfd, 0x3a),
RgbColor::new(0x01, 0x58, 0x14),
RgbColor::new(0x00, 0x1c, 0x0e),
RgbColor::new(0x2f, 0xfc, 0xb1),
RgbColor::new(0x00, 0xfb, 0x91),
RgbColor::new(0x01, 0x57, 0x32),
RgbColor::new(0x01, 0x18, 0x10),
RgbColor::new(0x39, 0xbe, 0xff),
RgbColor::new(0x00, 0xa7, 0xff),
RgbColor::new(0x01, 0x40, 0x51),
RgbColor::new(0x00, 0x10, 0x18),
RgbColor::new(0x41, 0x86, 0xff),
RgbColor::new(0x00, 0x50, 0xff),
RgbColor::new(0x01, 0x1a, 0x5a),
RgbColor::new(0x01, 0x06, 0x19),
RgbColor::new(0x47, 0x47, 0xff),
RgbColor::new(0x00, 0x00, 0xfe),
RgbColor::new(0x00, 0x00, 0x5a),
RgbColor::new(0x00, 0x00, 0x18),
RgbColor::new(0x83, 0x47, 0xff),
RgbColor::new(0x50, 0x00, 0xff),
RgbColor::new(0x16, 0x00, 0x67),
RgbColor::new(0x0a, 0x00, 0x32),
RgbColor::new(0xff, 0x48, 0xfe),
RgbColor::new(0xff, 0x00, 0xfe),
RgbColor::new(0x5a, 0x00, 0x5a),
RgbColor::new(0x18, 0x00, 0x18),
RgbColor::new(0xfb, 0x4e, 0x83),
RgbColor::new(0xff, 0x07, 0x53),
RgbColor::new(0x5a, 0x02, 0x1b),
RgbColor::new(0x21, 0x01, 0x10),
RgbColor::new(0xff, 0x19, 0x01),
RgbColor::new(0x9a, 0x35, 0x00),
RgbColor::new(0x7a, 0x51, 0x01),
RgbColor::new(0x3e, 0x65, 0x00),
// 64..128
RgbColor::new(0x01, 0x38, 0x00),
RgbColor::new(0x00, 0x54, 0x32),
RgbColor::new(0x00, 0x53, 0x7f),
RgbColor::new(0x00, 0x00, 0xfe),
RgbColor::new(0x01, 0x44, 0x4d),
RgbColor::new(0x1a, 0x00, 0xd1),
RgbColor::new(0x7c, 0x7c, 0x7c),
RgbColor::new(0x20, 0x20, 0x20),
RgbColor::new(0xff, 0x0a, 0x00),
RgbColor::new(0xba, 0xfd, 0x00),
RgbColor::new(0xac, 0xec, 0x00),
RgbColor::new(0x56, 0xfd, 0x00),
RgbColor::new(0x00, 0x88, 0x00),
RgbColor::new(0x01, 0xfc, 0x7b),
RgbColor::new(0x00, 0xa7, 0xff),
RgbColor::new(0x02, 0x1a, 0xff),
RgbColor::new(0x35, 0x00, 0xff),
RgbColor::new(0x78, 0x00, 0xff),
RgbColor::new(0xb4, 0x17, 0x7e),
RgbColor::new(0x41, 0x20, 0x00),
RgbColor::new(0xff, 0x4a, 0x01),
RgbColor::new(0x82, 0xe1, 0x00),
RgbColor::new(0x66, 0xfd, 0x00),
RgbColor::new(0x00, 0xfe, 0x00),
RgbColor::new(0x00, 0xfe, 0x00),
RgbColor::new(0x45, 0xfd, 0x61),
RgbColor::new(0x01, 0xfb, 0xcb),
RgbColor::new(0x50, 0x86, 0xff),
RgbColor::new(0x27, 0x4d, 0xc8),
RgbColor::new(0x84, 0x7a, 0xed),
RgbColor::new(0xd3, 0x0c, 0xff),
RgbColor::new(0xff, 0x06, 0x5a),
RgbColor::new(0xff, 0x7d, 0x01),
RgbColor::new(0xb8, 0xb1, 0x00),
RgbColor::new(0x8a, 0xfd, 0x00),
RgbColor::new(0x81, 0x5d, 0x00),
RgbColor::new(0x3a, 0x28, 0x02),
RgbColor::new(0x0d, 0x4c, 0x05),
RgbColor::new(0x00, 0x50, 0x37),
RgbColor::new(0x13, 0x14, 0x29),
RgbColor::new(0x10, 0x1f, 0x5a),
RgbColor::new(0x6a, 0x3c, 0x18),
RgbColor::new(0xac, 0x04, 0x01),
RgbColor::new(0xe1, 0x51, 0x36),
RgbColor::new(0xdc, 0x69, 0x00),
RgbColor::new(0xfe, 0xe1, 0x00),
RgbColor::new(0x99, 0xe1, 0x01),
RgbColor::new(0x60, 0xb5, 0x00),
RgbColor::new(0x1b, 0x1c, 0x31),
RgbColor::new(0xdc, 0xfd, 0x54),
RgbColor::new(0x76, 0xfb, 0xb9),
RgbColor::new(0x96, 0x98, 0xff),
RgbColor::new(0x8b, 0x62, 0xff),
RgbColor::new(0x40, 0x40, 0x40),
RgbColor::new(0x74, 0x74, 0x74),
RgbColor::new(0xde, 0xfc, 0xfc),
RgbColor::new(0xa2, 0x04, 0x01),
RgbColor::new(0x34, 0x01, 0x00),
RgbColor::new(0x00, 0xd2, 0x01),
RgbColor::new(0x00, 0x41, 0x01),
RgbColor::new(0xb8, 0xb1, 0x00),
RgbColor::new(0x3c, 0x30, 0x00),
RgbColor::new(0xb4, 0x5d, 0x00),
RgbColor::new(0x4c, 0x13, 0x00),
];
@@ -0,0 +1,2 @@
pub mod launchpad;
pub mod x_touch;
@@ -0,0 +1,96 @@
use crate::source::color_util::find_closest_color_in_palette;
use crate::{MackieLcdScope, RgbColor};
use base::hash_util::NonCryptoHashMap;
mod colors {
use crate::RgbColor;
pub const BLANK: RgbColor = RgbColor::new(0, 0, 0);
pub const RED: RgbColor = RgbColor::new(255, 0, 0);
pub const GREEN: RgbColor = RgbColor::new(0, 255, 0);
pub const YELLOW: RgbColor = RgbColor::new(255, 255, 0);
pub const BLUE: RgbColor = RgbColor::new(0, 0, 255);
pub const PURPLE: RgbColor = RgbColor::new(128, 0, 128);
pub const CYAN: RgbColor = RgbColor::new(0, 255, 255);
pub const WHITE: RgbColor = RgbColor::new(255, 255, 255);
}
use colors::*;
const COLOR_PALETTE: [RgbColor; 8] = [BLANK, RED, GREEN, YELLOW, BLUE, PURPLE, CYAN, WHITE];
/// Global state for a particular Behringer X-Touch device.
///
/// It's used when choosing the X-Touch Mackie display MIDI source in order to determine if a
/// sys-ex message needs to be sent to change the display color, and if yes, which one. We need
/// global state here because, unfortunately, the color can only be changed for all displays
/// (channels) at once. However, ReaLearn's color feedback design allows for defining the color
/// in a very fine-granular way - as part of the feedback value (its "style"), and thus resides
/// within the scope of a mapping.
///
/// We need to make sure that when changing the color for one display, that the colors of the other
/// displays remain unchanged. This is impossible without having access to the current state of the
/// other displays because there's no sys-ex to change the color of just one display.
///
/// One alternative would have been to somehow restructure ReaLearn's feedback design so that
/// we always transfer batches of texts and colors ... but that wouldn't go well with the
/// concept where one mapping can change something very small and specific (which makes ReaLearn so
/// flexible and composable).
///
/// Another alternative would have been to make the feedback source value something more
/// abstract than concrete MIDI messages and then creating the concrete MIDI message at a later
/// stage when all information is available (probably in the struct that has access to the global
/// source context state).
#[derive(Debug, Default)]
pub struct XTouchMackieLcdState {
state_by_extender: NonCryptoHashMap<u8, XTouchMackieExtenderLcdState>,
}
#[derive(Debug, Default)]
struct XTouchMackieExtenderLcdState {
color_index_by_channel: [Option<u8>; MackieLcdScope::CHANNEL_COUNT as usize],
}
const EMPTY_COLOR_INDEX_BY_CHANNEL: XTouchMackieExtenderLcdState = XTouchMackieExtenderLcdState {
color_index_by_channel: [None; MackieLcdScope::CHANNEL_COUNT as usize],
};
impl XTouchMackieLcdState {
/// Returns `true` if something has changed for the given extender.
///
/// In that case, the sys-ex should be sent again.
pub fn notify_color_requested(
&mut self,
extender_index: u8,
channel: u8,
color_index: Option<u8>,
) -> bool {
let extender_state = self.state_by_extender.entry(extender_index).or_default();
let previous_color_index = extender_state.color_index_by_channel[channel as usize];
extender_state.color_index_by_channel[channel as usize] = color_index;
color_index != previous_color_index
}
/// Returns the sys-ex bytes for setting the colors for the given extender.
pub fn sysex(&self, extender_index: u8) -> impl Iterator<Item = u8> + '_ {
let start = [0xF0, 0x00, 0x00, 0x66, 0x14 + extender_index, 0x72];
let extender_state = self
.state_by_extender
.get(&extender_index)
.unwrap_or(&EMPTY_COLOR_INDEX_BY_CHANNEL);
let color_indexes = extender_state
.color_index_by_channel
.iter()
.map(|color_index| color_index.unwrap_or(X_TOUCH_DEFAULT_COLOR_INDEX));
start
.into_iter()
.chain(color_indexes)
.chain(std::iter::once(0xF7))
}
}
pub fn get_x_touch_color_index_for_color(color: RgbColor) -> u8 {
find_closest_color_in_palette(color, &COLOR_PALETTE)
}
const X_TOUCH_DEFAULT_COLOR_INDEX: u8 = 0;
@@ -0,0 +1,41 @@
use crate::{FeedbackValue, PropValue};
use base::hash_util::NonCryptoHashSet;
use std::borrow::Cow;
use std::error::Error;
// The lifetime 'a is necessary in case we want to parameterize the lifetime
// of the additional input dynamically. An alternative would have been to
// require the additional input type to be static and take it by reference.
// But that would be less generic.
pub trait FeedbackScript<'a> {
type AdditionalInput: Default;
fn feedback(
&self,
input: FeedbackScriptInput,
additional_input: Self::AdditionalInput,
) -> Result<FeedbackScriptOutput, Cow<'static, str>>;
fn used_props(&self) -> Result<NonCryptoHashSet<String>, Box<dyn Error>>;
}
pub trait PropProvider {
fn get_prop_value(&self, key: &str) -> Option<PropValue>;
}
impl<F> PropProvider for F
where
F: Fn(&str) -> Option<PropValue>,
{
fn get_prop_value(&self, key: &str) -> Option<PropValue> {
(self)(key)
}
}
pub struct FeedbackScriptInput<'a> {
pub prop_provider: &'a dyn PropProvider,
}
pub struct FeedbackScriptOutput {
pub feedback_value: FeedbackValue<'static>,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
use crate::{FeedbackValue, MidiSourceAddress, RawMidiEvents};
use std::borrow::Cow;
// The lifetime 'a is necessary in case we want to parameterize the lifetime
// of the additional input dynamically. An alternative would have been to
// require the additional input type to be static and take it by reference.
// But that would be less generic.
pub trait MidiSourceScript<'a> {
type AdditionalInput: Default;
/// Returns raw MIDI bytes.
fn execute(
&self,
input_value: FeedbackValue,
additional_input: Self::AdditionalInput,
) -> Result<MidiSourceScriptOutcome, Cow<'static, str>>;
}
pub struct MidiSourceScriptOutcome {
pub address: Option<MidiSourceAddress>,
pub events: RawMidiEvents,
}
@@ -0,0 +1,349 @@
use crate::{DisplaySpecAddress, MidiSourceAddress, PatternByte, UnitValue};
use helgoboss_midi::{
Channel, ControlChange14BitMessage, DataEntryByteOrder, ParameterNumberMessage, ShortMessage,
ShortMessageFactory, StructuredShortMessage,
};
use reaper_common_types::Bpm;
use std::ops::RangeInclusive;
pub type RawMidiEvents = Vec<RawMidiEvent>;
/// Values produced when asking for feedback from MIDI sources.
///
/// At the moment, we always produce a final value and maybe a non-final one in addition, so this
/// isn't an enum.
#[derive(Clone, PartialEq, Debug)]
pub struct PreliminaryMidiSourceFeedbackValue<'a, M: ShortMessage> {
/// A concrete MIDI message.
pub final_value: MidiSourceValue<'a, M>,
/// Request to set the color of one particular XTouch channel display.
///
/// The XTouch doesn't provide a way to set the color for one particular channel, only one to
/// set the colors of all channels at once. That means we need to keep the current color of
/// each channel around as state, "integrate" these requests after collecting them from the
/// sources and then build the final sys-ex message.
pub x_touch_mackie_lcd_color_request: Option<XTouchMackieLcdColorRequest>,
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct XTouchMackieLcdColorRequest {
pub extender_index: u8,
pub channel: Option<u8>,
pub color_index: Option<u8>,
}
/// Incoming or outgoing value which might be used to control something or send feedback.
#[derive(Clone, PartialEq, Debug)]
pub enum MidiSourceValue<'a, M: ShortMessage> {
// Feedback and control
Plain(M),
ParameterNumber(ParameterNumberMessage),
ControlChange14Bit(ControlChange14BitMessage),
/// We must take care not to allocate this in real-time thread!
Raw {
feedback_address_info: Option<RawFeedbackAddressInfo>,
events: RawMidiEvents,
},
// Control-only
Tempo(Bpm),
// Control-only
BorrowedSysEx(&'a [u8]),
}
/// For being able to reconstructing the source address for feedback purposes (in particular,
/// source takeover).
///
/// Also important for preventing duplicate feedback.
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum RawFeedbackAddressInfo {
Raw {
variable_range: Option<RangeInclusive<usize>>,
},
Display {
spec: DisplaySpecAddress,
},
Custom(MidiSourceAddress),
}
impl<M: ShortMessage> MidiSourceValue<'_, M> {
pub fn single_raw(
feedback_address_info: Option<RawFeedbackAddressInfo>,
event: RawMidiEvent,
) -> Self {
Self::Raw {
feedback_address_info,
events: create_raw_midi_events_singleton(event),
}
}
}
pub fn create_raw_midi_events_singleton(event: RawMidiEvent) -> RawMidiEvents {
vec![event]
}
impl<M: ShortMessage + ShortMessageFactory + Copy> MidiSourceValue<'_, M> {
pub fn extract_feedback_address(&self) -> Option<MidiSourceAddress> {
use MidiSourceValue::*;
let res = match self {
Plain(m) => {
use StructuredShortMessage::*;
match m.to_structured() {
NoteOn {
channel,
key_number,
..
}
| NoteOff {
channel,
key_number,
..
} => MidiSourceAddress::Note {
channel,
key_number,
},
PolyphonicKeyPressure {
channel,
key_number,
..
} => MidiSourceAddress::PolyphonicKeyPressure {
channel,
key_number,
},
ControlChange {
channel,
controller_number,
..
} => MidiSourceAddress::ControlChange {
channel,
controller_number,
is_14_bit: false,
},
ProgramChange { channel, .. } => MidiSourceAddress::ProgramChange { channel },
ChannelPressure { channel, .. } => {
MidiSourceAddress::ChannelPressure { channel }
}
PitchBendChange { channel, .. } => {
MidiSourceAddress::PitchBendChange { channel }
}
// No feedback supported for other types of MIDI messages
_ => return None,
}
}
ParameterNumber(msg) => MidiSourceAddress::ParameterNumber {
channel: msg.channel(),
number: msg.number(),
is_registered: msg.is_registered(),
},
ControlChange14Bit(msg) => MidiSourceAddress::ControlChange {
channel: msg.channel(),
controller_number: msg.msb_controller_number(),
is_14_bit: true,
},
Raw {
feedback_address_info,
events,
} => match feedback_address_info.as_ref()? {
RawFeedbackAddressInfo::Raw { variable_range } => MidiSourceAddress::Raw {
pattern: events
.first()?
.bytes()
.iter()
.enumerate()
.map(|(i, b)| {
if let Some(vr) = variable_range {
if vr.contains(&i) {
PatternByte::Variable
} else {
PatternByte::Fixed(*b)
}
} else {
PatternByte::Fixed(*b)
}
})
.collect(),
},
RawFeedbackAddressInfo::Display { spec } => {
MidiSourceAddress::Display { spec: spec.clone() }
}
RawFeedbackAddressInfo::Custom(addr) => addr.clone(),
},
// No feedback
Tempo(_) | BorrowedSysEx(_) => return None,
};
Some(res)
}
pub fn channel(&self) -> Option<Channel> {
use MidiSourceValue::*;
match self {
Plain(m) => m.channel(),
ParameterNumber(m) => Some(m.channel()),
ControlChange14Bit(m) => Some(m.channel()),
_ => None,
}
}
/// Might allocate!
///
/// Not usable for producing feedback output that should participate in feedback relay
/// (since BorrowedSysEx doesn't contain a feedback address).
pub fn try_into_owned(self) -> Result<MidiSourceValue<'static, M>, &'static str> {
use MidiSourceValue::*;
let res = match self {
Plain(v) => Plain(v),
ParameterNumber(v) => ParameterNumber(v),
ControlChange14Bit(v) => ControlChange14Bit(v),
Tempo(v) => Tempo(v),
Raw {
feedback_address_info,
events,
} => Raw {
feedback_address_info,
events,
},
BorrowedSysEx(bytes) => {
// Situations where we convert a borrowed message into an owned are not
// situations in which we want to send a feedback value. So it's not bad that
// we can't provide a feedback address here.
let feedback_address_info = None;
let event = RawMidiEvent::try_from_slice(0, bytes)?;
MidiSourceValue::single_raw(feedback_address_info, event)
}
};
Ok(res)
}
pub fn into_garbage(self) -> Option<RawMidiEvents> {
use MidiSourceValue::*;
match self {
Raw { events, .. } => Some(events),
_ => None,
}
}
/// For values that are best sent raw, e.g. sys-ex.
pub fn to_raw(&self) -> Option<impl Iterator<Item = &RawMidiEvent>> {
use MidiSourceValue::*;
match self {
Raw { events, .. } => Some(events.iter()),
_ => None,
}
}
/// For values that are best sent as short messages.
pub fn to_short_messages(
&self,
nrpn_data_entry_byte_order: DataEntryByteOrder,
) -> [Option<M>; 4] {
use MidiSourceValue::*;
match self {
Plain(msg) => [Some(*msg), None, None, None],
ParameterNumber(msg) => msg.to_short_messages(nrpn_data_entry_byte_order),
ControlChange14Bit(msg) => {
let inner_shorts = msg.to_short_messages();
[Some(inner_shorts[0]), Some(inner_shorts[1]), None, None]
}
Tempo(_) | Raw { .. } | BorrowedSysEx(_) => [None; 4],
}
}
}
impl From<UnitValue> for Bpm {
fn from(value: UnitValue) -> Self {
let min = Bpm::ONE_BPM.get();
let span = Bpm::NINE_HUNDRED_SIXTY_BPM.get() - min;
Bpm::new_panic(min + value.get() * span)
}
}
impl From<Bpm> for UnitValue {
fn from(value: Bpm) -> Self {
let min = Bpm::ONE_BPM.get();
let span = Bpm::NINE_HUNDRED_SIXTY_BPM.get() - min;
// At some point, we allowed BPM values higher than 960 BPM (it's just that REAPER doesn't take them).
// That's why we clamp.
UnitValue::new_clamped((value.get() - min) / span)
}
}
/// Raw MIDI data which is compatible to both VST and REAPER MIDI data structures. The REAPER
/// struct is more picky in that it needs offset and size directly in front of the raw data whereas
/// the VST struct allows the data to be at a different address. That's why we need to follow the
/// REAPER requirement.
///
/// Conforms to the LongMidiEvent in `reaper-medium` but the goal of `helgoboss-learn` is to be
/// DAW-agnostic, so we have to recreate the lowest common denominator.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[repr(C)]
pub struct RawMidiEvent {
/// A MIDI frame offset.
///
/// This is a 1/1024000 of a second, *not* a sample frame!
frame_offset: i32,
size: i32,
midi_message: [u8; RawMidiEvent::MAX_LENGTH],
}
impl Default for RawMidiEvent {
fn default() -> Self {
Self {
frame_offset: 0,
size: 0,
midi_message: [0; RawMidiEvent::MAX_LENGTH],
}
}
}
impl RawMidiEvent {
pub const MAX_LENGTH: usize = 256;
pub const fn new(frame_offset: u32, size: u32, midi_message: [u8; Self::MAX_LENGTH]) -> Self {
Self {
frame_offset: frame_offset as _,
size: size as _,
midi_message,
}
}
/// If you already have a slice, use this. If you are just building something, `try_from_iter`
/// is probably more efficient.
pub fn try_from_slice(frame_offset: u32, midi_message: &[u8]) -> Result<Self, &'static str> {
if midi_message.len() > Self::MAX_LENGTH {
return Err("given MIDI message too long");
}
let mut array = [0; Self::MAX_LENGTH];
// TODO-low I think copying from a slice is the only way to go, even we own a vec or array.
// REAPER's struct layout requires us to put something in front of the vec, which is
// not or at least not easily possible without copying.
array[..midi_message.len()].copy_from_slice(midi_message);
Ok(Self::new(frame_offset, midi_message.len() as _, array))
}
pub fn try_from_iter<T: IntoIterator<Item = u8>>(
frame_offset: u32,
iter: T,
) -> Result<Self, &'static str> {
let mut array = [0; Self::MAX_LENGTH];
let mut i = 0usize;
for b in iter {
if i == Self::MAX_LENGTH {
return Err("given content too long");
}
let elem = unsafe { array.get_unchecked_mut(i) };
*elem = b;
i += 1;
}
Ok(Self::new(frame_offset, i as u32, array))
}
pub fn bytes(&self) -> &[u8] {
&self.midi_message[..self.size as usize]
}
}
#[cfg(feature = "reaper-low")]
impl AsRef<reaper_low::raw::MIDI_event_t> for RawMidiEvent {
fn as_ref(&self) -> &reaper_low::raw::MIDI_event_t {
unsafe { &*(self as *const RawMidiEvent as *const reaper_low::raw::MIDI_event_t) }
}
}
@@ -0,0 +1,27 @@
mod midi_source_value;
pub use midi_source_value::*;
mod midi_source;
pub use midi_source::*;
mod osc_source;
pub use osc_source::*;
mod raw_midi;
pub use raw_midi::*;
mod midi_source_script;
pub use midi_source_script::*;
mod feedback_script;
pub use feedback_script::*;
mod source_context;
pub use source_context::*;
mod color_util;
#[cfg(test)]
mod test_util;
pub mod devices;
@@ -0,0 +1,573 @@
use crate::DetailedSourceCharacter::Trigger;
use std::cmp;
use crate::{
format_percentage_without_unit, parse_percentage_without_unit, AbsoluteValue, ControlValue,
DetailedSourceCharacter, DiscreteIncrement, FeedbackValue, Fraction, Interval, RgbColor,
SourceCharacter, UnitValue, UNIT_INTERVAL,
};
use derive_more::Display;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use rosc::{OscColor, OscMessage, OscType};
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::convert::TryInto;
use strum::EnumIter;
/// With OSC it's easy: The source address is the address!
pub type OscSourceAddress = String;
#[derive(Clone, PartialEq, Debug)]
pub struct OscSource {
/// To filter out the correct messages.
address_pattern: String,
/// To process a value (not just trigger).
arg_descriptor: Option<OscArgDescriptor>,
/// If non-empty, these are used for mapping feedback data to arguments.
feedback_args: Vec<OscFeedbackProp>,
}
#[derive(
Copy,
Clone,
Eq,
PartialEq,
Debug,
strum::EnumString,
strum::Display,
SerializeDisplay,
DeserializeFromStr,
)]
pub enum OscFeedbackProp {
// Floats
#[strum(serialize = "value.float")]
ValueAsFloat,
// Doubles
#[strum(serialize = "value.double")]
ValueAsDouble,
// Bools
#[strum(serialize = "value.bool")]
ValueAsBool,
// Nil
#[strum(serialize = "nil")]
Nil,
// Inf
#[strum(serialize = "inf")]
Inf,
// Integers
#[strum(serialize = "value.int")]
ValueAsInt,
// Strings
#[strum(serialize = "value.string")]
ValueAsString,
// Longs
#[strum(serialize = "value.long")]
ValueAsLong,
#[strum(serialize = "style.color.rrggbb")]
ColorRrggbb,
#[strum(serialize = "style.background_color.rrggbb")]
BackgroundColorRrggbb,
// Colors
#[strum(serialize = "style.color")]
Color,
#[strum(serialize = "style.backround_color")]
BackgroundColor,
}
impl Default for OscFeedbackProp {
fn default() -> Self {
Self::Nil
}
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct OscArgDescriptor {
/// To select the correct value.
index: u32,
/// To send the correct value type on feedback.
type_tag: OscTypeTag,
/// Interpret 1 values as increments and 0 values as decrements.
is_relative: bool,
/// Value range for all range types (double, float, int, long).
value_range: Interval<f64>,
}
impl OscArgDescriptor {
pub fn new(
index: u32,
type_tag: OscTypeTag,
is_relative: bool,
value_range: Interval<f64>,
) -> Self {
Self {
index,
type_tag,
is_relative,
value_range,
}
}
pub fn index(self) -> u32 {
self.index
}
pub fn type_tag(self) -> OscTypeTag {
self.type_tag
}
pub fn is_relative(self) -> bool {
self.is_relative
}
pub fn value_range(&self) -> Interval<f64> {
self.value_range
}
pub fn from_msg(msg: &OscMessage, arg_index_hint: u32) -> Option<Self> {
let desc = if let Some(hinted_arg) = msg.args.get(arg_index_hint as usize) {
Self::from_arg(arg_index_hint, hinted_arg)
} else {
let first_arg = msg.args.first()?;
Self::from_arg(0, first_arg)
};
Some(desc)
}
pub fn to_concrete_args(self, value: FeedbackValue) -> Option<Vec<OscType>> {
self.type_tag
.to_concrete_args(self.index, value, self.value_range)
}
fn from_arg(index: u32, arg: &OscType) -> Self {
Self {
index,
type_tag: OscTypeTag::from_arg(arg),
// Relative is the exception, so we reset it when learning.
is_relative: false,
value_range: match get_range_value(arg) {
None => DEFAULT_OSC_ARG_VALUE_RANGE,
Some(v) => Interval::new_auto(0.0, v),
},
}
}
}
pub const DEFAULT_OSC_ARG_VALUE_RANGE: Interval<f64> = UNIT_INTERVAL;
fn get_range_value(arg: &OscType) -> Option<f64> {
use OscType::*;
match arg {
Int(v) => Some(*v as f64),
Float(v) => Some(*v as f64),
Long(v) => Some(*v as f64),
Double(v) => Some(*v),
_ => None,
}
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
EnumIter,
TryFromPrimitive,
IntoPrimitive,
Display,
Serialize,
Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[repr(usize)]
// TODO-low Rename. This it not the tag, it's rather the OscType without value.
pub enum OscTypeTag {
#[display(fmt = "Float")]
Float,
#[display(fmt = "Double")]
Double,
#[display(fmt = "Bool (on/off)")]
Bool,
#[display(fmt = "Nil (trigger only)")]
Nil,
#[display(fmt = "Infinitum (trigger only)")]
Inf,
#[display(fmt = "Int")]
Int,
#[display(fmt = "String (feedback only)")]
String,
#[display(fmt = "Blob (ignored)")]
Blob,
#[display(fmt = "Time (ignored)")]
Time,
#[display(fmt = "Long")]
Long,
#[display(fmt = "Char (ignored)")]
Char,
#[display(fmt = "Color (feedback only)")]
Color,
#[display(fmt = "MIDI (ignored)")]
Midi,
#[display(fmt = "Array (ignored)")]
Array,
}
impl Default for OscTypeTag {
fn default() -> Self {
Self::Float
}
}
impl OscTypeTag {
pub fn from_arg(arg: &OscType) -> Self {
use OscType::*;
match arg {
Int(_) => Self::Int,
Float(_) => Self::Float,
String(_) => Self::String,
Blob(_) => Self::Blob,
Time(_) => Self::Time,
Long(_) => Self::Long,
Double(_) => Self::Double,
Char(_) => Self::Char,
Color(_) => Self::Color,
Midi(_) => Self::Midi,
Bool(_) => Self::Bool,
Array(_) => Self::Array,
Nil => Self::Nil,
Inf => Self::Inf,
}
}
pub fn to_concrete_args(
self,
index: u32,
v: FeedbackValue,
value_range: Interval<f64>,
) -> Option<Vec<OscType>> {
use OscTypeTag::*;
let value = match self {
Float => convert_feedback_prop_to_arg(OscFeedbackProp::ValueAsFloat, &v, value_range)?,
Double => {
convert_feedback_prop_to_arg(OscFeedbackProp::ValueAsDouble, &v, value_range)?
}
Bool => convert_feedback_prop_to_arg(OscFeedbackProp::ValueAsBool, &v, value_range)?,
Nil => OscType::Nil,
Inf => OscType::Inf,
Int => convert_feedback_prop_to_arg(OscFeedbackProp::ValueAsInt, &v, value_range)?,
String => {
convert_feedback_prop_to_arg(OscFeedbackProp::ValueAsString, &v, value_range)?
}
Long => convert_feedback_prop_to_arg(OscFeedbackProp::ValueAsLong, &v, value_range)?,
Color => convert_feedback_prop_to_arg(OscFeedbackProp::Color, &v, value_range)?,
_ => return None,
};
// Send nil for all other elements
let mut vec = vec![OscType::Nil; (index + 1) as usize];
vec[index as usize] = value;
Some(vec)
}
pub fn supports_control(self) -> bool {
use OscTypeTag::*;
matches!(self, Float | Double | Bool | Nil | Inf | Int | Long)
}
pub fn supports_feedback(self) -> bool {
use OscTypeTag::*;
matches!(
self,
Float | Double | Bool | Nil | Inf | Int | String | Long | Color
)
}
pub fn supports_value_range(self) -> bool {
use OscTypeTag::*;
matches!(self, Float | Double | Int | Long)
}
pub fn is_discrete(self) -> bool {
use OscTypeTag::*;
matches!(self, Int | Long)
}
}
impl OscSource {
pub fn feedback_address(&self) -> &OscSourceAddress {
&self.address_pattern
}
/// Checks if the given message is directed to the same address as the one of this source.
///
/// Used for:
///
/// - Source takeover (feedback)
pub fn has_same_feedback_address_as_value(&self, value: &OscMessage) -> bool {
self.address_pattern == value.addr
}
/// Checks if this and the given source share the same address.
///
/// Used for:
///
/// - Feedback diffing
pub fn has_same_feedback_address_as_source(&self, other: &Self) -> bool {
self.address_pattern == other.address_pattern
}
pub fn new(
address_pattern: String,
arg_descriptor: Option<OscArgDescriptor>,
feedback_args: Vec<OscFeedbackProp>,
) -> Self {
Self {
address_pattern,
arg_descriptor,
feedback_args,
}
}
pub fn from_source_value(msg: OscMessage, arg_index_hint: Option<u32>) -> OscSource {
let arg_descriptor = OscArgDescriptor::from_msg(&msg, arg_index_hint.unwrap_or(0));
OscSource::new(msg.addr, arg_descriptor, vec![])
}
pub fn address_pattern(&self) -> &str {
&self.address_pattern
}
pub fn arg_descriptor(&self) -> Option<OscArgDescriptor> {
self.arg_descriptor
}
pub fn control(&self, msg: &OscMessage) -> Option<ControlValue> {
let (absolute_value, is_relative) = {
if msg.addr != self.address_pattern {
return None;
}
if let Some(desc) = self.arg_descriptor {
if let Some(arg) = msg.args.get(desc.index as usize) {
use OscType::*;
let v =
match arg {
Float(f) => AbsoluteValue::Continuous(
map_continuous_from_range_to_unit(*f as f64, desc.value_range),
),
Double(d) => AbsoluteValue::Continuous(
map_continuous_from_range_to_unit(*d, desc.value_range),
),
Bool(on) => AbsoluteValue::Continuous(if *on {
UnitValue::MAX
} else {
UnitValue::MIN
}),
// Infinity/impulse or nil/null - act like a trigger.
Inf | Nil => AbsoluteValue::Continuous(UnitValue::MAX),
Int(i) => AbsoluteValue::Discrete(map_discrete_from_range_to_positive(
*i,
desc.value_range,
)),
Long(l) => {
// TODO-low-discrete Maybe increase fraction integers to 64-bit? Right now
// we don't really take advantage of fractions, so we emit continuous control
// values as long as this doesn't change.
AbsoluteValue::Continuous(map_continuous_from_range_to_unit(
*l as f64,
desc.value_range,
))
}
String(_) | Blob(_) | Time(_) | Char(_) | Color(_) | Midi(_)
| Array(_) => return None,
};
(v, desc.is_relative)
} else {
// Argument not found. Don't do anything.
return None;
}
} else {
// Source shall not look at any argument. Act like a trigger.
(AbsoluteValue::Continuous(UnitValue::MAX), false)
}
};
let control_value = if is_relative {
let inc = if absolute_value.is_on() { 1 } else { -1 };
ControlValue::RelativeDiscrete(DiscreteIncrement::new(inc))
} else {
ControlValue::from_absolute(absolute_value)
};
Some(control_value)
}
pub fn format_control_value(&self, value: ControlValue) -> Result<String, &'static str> {
let v = value.to_unit_value()?.get();
Ok(format_percentage_without_unit(v))
}
pub fn parse_control_value(&self, text: &str) -> Result<UnitValue, &'static str> {
parse_percentage_without_unit(text)?.try_into()
}
pub fn character(&self) -> SourceCharacter {
use SourceCharacter::*;
if let Some(desc) = self.arg_descriptor {
use OscTypeTag::*;
match desc.type_tag {
Float | Double | Int | Long => RangeElement,
Bool | Nil | Inf => MomentaryButton,
_ => MomentaryButton,
}
} else {
MomentaryButton
}
}
pub fn possible_detailed_characters(&self) -> Vec<DetailedSourceCharacter> {
if let Some(desc) = self.arg_descriptor {
if desc.is_relative {
vec![DetailedSourceCharacter::Relative]
} else {
use OscTypeTag::*;
match desc.type_tag {
Float | Double | Int | Long => vec![
DetailedSourceCharacter::RangeControl,
DetailedSourceCharacter::MomentaryVelocitySensitiveButton,
DetailedSourceCharacter::MomentaryOnOffButton,
DetailedSourceCharacter::Trigger,
],
_ => vec![DetailedSourceCharacter::MomentaryOnOffButton, Trigger],
}
}
} else {
vec![DetailedSourceCharacter::Trigger]
}
}
pub fn feedback(&self, feedback_value: FeedbackValue) -> Option<OscMessage> {
let msg = OscMessage {
addr: self.address_pattern.clone(),
args: if !self.feedback_args.is_empty() {
// Explicit feedback args given.
let value_range = self
.arg_descriptor
.map(|desc| desc.value_range)
.unwrap_or(DEFAULT_OSC_ARG_VALUE_RANGE);
self.feedback_args
.iter()
.map(|prop| {
convert_feedback_prop_to_arg(*prop, &feedback_value, value_range)
.unwrap_or(OscType::Nil)
})
.collect()
} else if let Some(desc) = self.arg_descriptor {
// No explicit feedback args given. Just derive from argument descriptor.
desc.to_concrete_args(feedback_value)?
} else {
// No arguments shall be sent.
vec![]
},
};
Some(msg)
}
}
fn convert_feedback_prop_to_arg(
prop: OscFeedbackProp,
v: &FeedbackValue,
value_range: Interval<f64>,
) -> Option<OscType> {
use OscFeedbackProp::*;
let arg = match prop {
ValueAsFloat | ValueAsDouble | ValueAsLong => {
let unit_value = v.to_numeric()?.value.to_unit_value();
let range_value = map_continuous_from_unit_to_range(unit_value, value_range);
match prop {
ValueAsFloat => OscType::Float(range_value as _),
ValueAsDouble => OscType::Double(range_value),
ValueAsLong => OscType::Long(range_value.round() as i64),
_ => unreachable!(),
}
}
ValueAsBool => OscType::Bool(v.to_numeric()?.value.is_on()),
Nil => OscType::Nil,
Inf => OscType::Inf,
ValueAsInt => {
let range_value = match v.to_numeric()?.value {
AbsoluteValue::Continuous(uv) => {
map_continuous_from_unit_to_range(uv, value_range).round() as i32
}
AbsoluteValue::Discrete(f) => {
map_discrete_from_positive_to_range(f.actual(), value_range)
}
};
OscType::Int(range_value)
}
ValueAsString => OscType::String(v.to_textual().text.into_owned()),
ColorRrggbb => convert_color_to_rrggbb_string_arg(v.color()),
BackgroundColorRrggbb => convert_color_to_rrggbb_string_arg(v.background_color()),
Color => convert_color_to_native_color_arg(v.color()),
BackgroundColor => convert_color_to_native_color_arg(v.background_color()),
};
Some(arg)
}
fn convert_color_to_rrggbb_string_arg(v: Option<RgbColor>) -> OscType {
match v {
// Nil is hopefully interpreted as "Default color".
None => OscType::Nil,
Some(c) => {
let color_string = format!("{:02X}{:02X}{:02X}", c.r(), c.g(), c.b());
OscType::String(color_string)
}
}
}
fn convert_color_to_native_color_arg(v: Option<RgbColor>) -> OscType {
match v {
// Nil is hopefully interpreted as "Default color".
None => OscType::Nil,
Some(c) => OscType::Color(OscColor {
red: c.r(),
green: c.g(),
blue: c.b(),
alpha: 255,
}),
}
}
fn map_continuous_from_range_to_unit(x: f64, value_range: Interval<f64>) -> UnitValue {
// y = (x - min) / span
let y = (x - value_range.min_val()) / value_range.span();
UnitValue::new_clamped(y)
}
fn map_continuous_from_unit_to_range(y: UnitValue, value_range: Interval<f64>) -> f64 {
// y = (x - min) / span
// y * span = x - min
// x = y * span + min
y.get() * value_range.span() + value_range.min_val()
}
fn map_discrete_from_range_to_positive(x: i32, value_range: Interval<f64>) -> Fraction {
let rounded_range = round_value_range(value_range);
Fraction::new(
clamp_to_positive(x - rounded_range.min_val()),
clamp_to_positive(rounded_range.span()),
)
}
fn map_discrete_from_positive_to_range(y: u32, value_range: Interval<f64>) -> i32 {
let rounded_range = round_value_range(value_range);
y as i32 + rounded_range.min_val()
}
fn round_value_range(value_range: Interval<f64>) -> Interval<i32> {
Interval::new(
value_range.min_val().round() as i32,
value_range.max_val().round() as i32,
)
}
fn clamp_to_positive(v: i32) -> u32 {
cmp::max(0, v) as u32
}
@@ -0,0 +1,554 @@
use crate::{AbsoluteValue, Fraction, PatternByte, RawMidiEvent, UnitValue};
use logos::{Lexer, Logos};
use std::fmt;
use std::fmt::{Display, Formatter, Write};
use std::num::ParseIntError;
use std::ops::RangeInclusive;
use std::str::FromStr;
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct RawMidiPattern {
entries: Vec<RawMidiPatternEntry>,
resolution: u8,
}
impl RawMidiPattern {
pub fn from_entries(entries: Vec<RawMidiPatternEntry>) -> Self {
let max_variable_bit_index = entries
.iter()
.filter_map(|e| e.max_variable_bit_index())
.max();
Self {
entries,
resolution: if let Some(i) = max_variable_bit_index {
i + 1
} else {
0
},
}
}
pub fn fixed_from_slice(bytes: &[u8]) -> Self {
let entries = bytes
.iter()
.map(|byte| RawMidiPatternEntry::FixedByte(*byte))
.collect();
Self {
entries,
resolution: 0,
}
}
pub fn variable_range(&self) -> Option<RangeInclusive<usize>> {
let left = self.entries().iter().position(|e| !e.is_fixed())?;
let right = self.entries().iter().rposition(|e| !e.is_fixed())?;
Some(left..=right)
}
pub fn to_pattern_bytes(&self) -> Vec<PatternByte> {
self.entries()
.iter()
.map(|e| {
if let Some(b) = e.byte_if_fixed() {
PatternByte::Fixed(b)
} else {
PatternByte::Variable
}
})
.collect()
}
pub fn entries(&self) -> &[RawMidiPatternEntry] {
&self.entries
}
/// Resolution in bit (maximum 16 bit).
///
/// If no variable bytes, this returns 0.
pub fn resolution(&self) -> u8 {
self.resolution
}
/// If no variable bytes, this returns 0.
pub fn max_discrete_value(&self) -> u16 {
(2u32.pow(self.resolution as _) - 1) as u16
}
pub fn step_size(&self) -> Option<UnitValue> {
let max = self.max_discrete_value();
if max == 0 {
return None;
}
Some(UnitValue::new_clamped(1.0 / max as f64))
}
/// If it matches and there are no variable bytes in the pattern, this returns
/// `Some(Fraction(0, 0))`.
pub fn match_and_capture(&self, bytes: &[u8]) -> Option<Fraction> {
if bytes.len() != self.entries.len() {
return None;
}
let mut current_value: u16 = 0;
for (i, b) in bytes.iter().enumerate() {
let pattern_entry = self.entries[i];
if let Some(v) = pattern_entry.match_and_capture(*b, current_value) {
current_value = v;
} else {
return None;
}
}
let fraction = Fraction::new(current_value as _, self.max_discrete_value() as _);
Some(fraction)
}
pub fn to_bytes(&self, variable_value: AbsoluteValue) -> Vec<u8> {
self.byte_iter(variable_value).collect()
}
pub fn byte_iter(
&self,
variable_value: AbsoluteValue,
) -> impl ExactSizeIterator<Item = u8> + '_ {
let discrete_value = match variable_value {
AbsoluteValue::Continuous(v) => v.to_discrete(self.max_discrete_value()),
AbsoluteValue::Discrete(f) => {
std::cmp::min(f.actual(), self.max_discrete_value() as u32) as u16
}
};
self.entries.iter().map(move |e| e.to_byte(discrete_value))
}
pub fn to_concrete_midi_event(
&self,
frame_offset: u32,
variable_value: AbsoluteValue,
) -> RawMidiEvent {
// TODO-medium Use RawMidiEvent::try_from_iter
let mut array = [0; RawMidiEvent::MAX_LENGTH];
let mut i = 0u32;
for byte in self
.byte_iter(variable_value)
.take(RawMidiEvent::MAX_LENGTH)
{
array[i as usize] = byte;
i += 1;
}
RawMidiEvent::new(frame_offset, i, array)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum RawMidiPatternEntry {
FixedByte(u8),
PotentiallyVariableByte(BitPattern),
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct BitPattern {
/// From most significant to least significant bit.
entries: [BitPatternEntry; 8],
}
impl BitPattern {
pub fn contains_variable_portions(&self) -> bool {
self.entries
.iter()
.any(|bpe| matches!(bpe, BitPatternEntry::VariableBit(_)))
}
fn max_variable_bit_index(&self) -> Option<u8> {
self.entries
.iter()
.filter_map(|bpe| bpe.variable_bit_index())
.max()
}
pub fn to_byte(self, discrete_value: u16) -> u8 {
let mut final_byte: u8 = 0;
for i in 0..8 {
use BitPatternEntry::*;
let final_bit = match self.entries[i] {
FixedBit(bit) => bit,
VariableBit(bit_index) => (discrete_value & (1 << bit_index) as u16) > 0,
};
if final_bit {
final_byte |= 1 << (7 - i);
}
}
final_byte
}
fn match_and_capture(&self, actual_byte: u8, current_value: u16) -> Option<u16> {
let mut new_value = current_value;
for i in 0..8 {
let actual_bit = (actual_byte >> (7 - i)) & 1 == 1;
use BitPatternEntry::*;
match self.entries[i] {
FixedBit(bit) => {
if bit != actual_bit {
return None;
}
}
VariableBit(bit_index) => {
if actual_bit {
new_value |= 1 << bit_index;
}
}
};
}
Some(new_value)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum BitPatternEntry {
FixedBit(bool),
/// The number represents the bit index starting from 0 where 0 represents the *least*
/// significant bit!.
VariableBit(u8),
}
impl Default for BitPatternEntry {
fn default() -> Self {
BitPatternEntry::FixedBit(false)
}
}
impl BitPatternEntry {
fn variable_bit_index(&self) -> Option<u8> {
use BitPatternEntry::*;
match self {
FixedBit(_) => None,
VariableBit(i) => Some(*i),
}
}
}
impl RawMidiPatternEntry {
fn is_fixed(&self) -> bool {
// TODO-low This could be implemented better by transforming potentially variable
// bytes that are not variable into fixed bytes in the first place!
self.byte_if_fixed().is_some()
}
fn byte_if_fixed(&self) -> Option<u8> {
use RawMidiPatternEntry::*;
match self {
FixedByte(b) => Some(*b),
PotentiallyVariableByte(p) => {
if p.contains_variable_portions() {
None
} else {
// Value parameter not important if pattern doesn't contain
// variable portions.
Some(p.to_byte(0))
}
}
}
}
fn match_and_capture(&self, actual_byte: u8, current_value: u16) -> Option<u16> {
use RawMidiPatternEntry::*;
match self {
FixedByte(b) => {
if actual_byte == *b {
Some(current_value)
} else {
None
}
}
PotentiallyVariableByte(pattern) => {
pattern.match_and_capture(actual_byte, current_value)
}
}
}
fn max_variable_bit_index(&self) -> Option<u8> {
use RawMidiPatternEntry::*;
match self {
FixedByte(_) => None,
PotentiallyVariableByte(bit_pattern) => bit_pattern.max_variable_bit_index(),
}
}
fn to_byte(self, discrete_value: u16) -> u8 {
use RawMidiPatternEntry::*;
match self {
FixedByte(byte) => byte,
PotentiallyVariableByte(bit_pattern) => bit_pattern.to_byte(discrete_value),
}
}
}
impl Display for RawMidiPattern {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let string_vec: Vec<_> = self.entries.iter().map(|e| e.to_string()).collect();
f.write_str(&string_vec.join(" "))
}
}
impl Display for RawMidiPatternEntry {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use RawMidiPatternEntry::*;
match self {
FixedByte(byte) => write!(f, "{:02X}", *byte),
PotentiallyVariableByte(pattern) => write!(f, "[{pattern}]"),
}
}
}
impl Display for BitPattern {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
for entry in &self.entries[..4] {
let _ = entry.fmt(f);
}
let _ = f.write_char(' ');
for entry in &self.entries[4..] {
let _ = entry.fmt(f);
}
Ok(())
}
}
impl Display for BitPatternEntry {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use BitPatternEntry::*;
match self {
FixedBit(bit) => write!(f, "{}", if *bit { '1' } else { '0' }),
VariableBit(bit_index) => write!(f, "{}", (97 + bit_index) as char),
}
}
}
impl FromStr for RawMidiPattern {
type Err = ParseRawMidiPatternError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lex: Lexer<RawMidiPatternToken> = RawMidiPatternToken::lexer(s);
use RawMidiPatternToken::*;
let entries: Result<Vec<_>, ParseRawMidiPatternError> = lex
.map(|token| {
let entry = match token? {
FixedByte(byte) => RawMidiPatternEntry::FixedByte(byte),
PotentiallyVariableByte(pattern) => {
RawMidiPatternEntry::PotentiallyVariableByte(pattern)
}
};
Ok(entry)
})
.collect();
let entries = entries.map_err(|_| "couldn't parse raw MIDI pattern")?;
Ok(RawMidiPattern::from_entries(entries))
}
}
#[derive(Debug, PartialEq, Logos)]
#[logos(skip r"[ \t\n\f]+")]
#[logos(error = ParseRawMidiPatternError)]
enum RawMidiPatternToken {
#[regex(r"\[[01abcdefghijklmnop ]*\]", parse_as_bit_pattern)]
PotentiallyVariableByte(BitPattern),
#[regex(r"[0-9a-fA-F][0-9a-fA-F]?", parse_as_byte)]
FixedByte(u8),
}
#[derive(Clone, PartialEq, Debug, Default, thiserror::Error)]
#[error("{msg}")]
pub struct ParseRawMidiPatternError {
msg: &'static str,
}
impl From<&'static str> for ParseRawMidiPatternError {
fn from(msg: &'static str) -> Self {
Self { msg }
}
}
impl From<ParseIntError> for ParseRawMidiPatternError {
fn from(_: ParseIntError) -> Self {
Self {
msg: "problem parsing fixed byte",
}
}
}
fn parse_as_byte(lex: &mut Lexer<RawMidiPatternToken>) -> Result<u8, core::num::ParseIntError> {
u8::from_str_radix(lex.slice(), 16)
}
fn parse_as_bit_pattern(lex: &mut Lexer<RawMidiPatternToken>) -> Result<BitPattern, &'static str> {
let mut entries: [BitPatternEntry; 8] = Default::default();
let slice: &str = lex.slice();
let mut i = 0;
for c in slice.chars() {
use BitPatternEntry::*;
let entry = match c {
'0' => FixedBit(false),
'1' => FixedBit(true),
'a'..='p' => VariableBit(c as u8 - 97),
_ => continue,
};
if i > 7 {
return Err("too many bits in bit pattern");
}
entries[i] = entry;
i += 1;
}
let pattern = BitPattern { entries };
Ok(pattern)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_variable_nibble() {
// Given
let pattern: RawMidiPattern = "F0 [0000 dcba] F7".parse().unwrap();
// When
// Then
assert_eq!(
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::MAX)),
vec![0xf0, 0x0f, 0xf7]
);
assert_eq!(
pattern.match_and_capture(&[0xf0, 0x0f, 0xf7]),
Some(Fraction::new(15, 15))
);
assert_eq!(
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::MIN)),
vec![0xf0, 0x00, 0xf7]
);
assert_eq!(
pattern.match_and_capture(&[0xf0, 0x00, 0xf7]),
Some(Fraction::new(0, 15))
);
assert_eq!(
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::new(0.5))),
vec![0xf0, 0x08, 0xf7]
);
assert_eq!(
pattern.match_and_capture(&[0xf0, 0x08, 0xf7]),
Some(Fraction::new(8, 15))
);
assert_eq!(&pattern.to_string(), "F0 [0000 dcba] F7");
assert_eq!(pattern.match_and_capture(&[0xf1, 0x0f, 0xf7]), None);
}
#[test]
fn one_variable_nibble_no_spaces() {
// Given
let pattern: RawMidiPattern = "F0[0000dcba]F7".parse().unwrap();
// When
// Then
assert_eq!(
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::MAX)),
vec![0xf0, 0x0f, 0xf7]
);
assert_eq!(
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::MIN)),
vec![0xf0, 0x00, 0xf7]
);
assert_eq!(
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::new(0.5))),
vec![0xf0, 0x08, 0xf7]
);
assert_eq!(&pattern.to_string(), "F0 [0000 dcba] F7");
}
#[test]
fn one_variable_nibble_variation() {
// Given
let pattern: RawMidiPattern = "F0[1111dcba]F7".parse().unwrap();
// When
// Then
assert_eq!(
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::MAX)),
vec![0xf0, 0xff, 0xf7]
);
assert_eq!(
pattern.match_and_capture(&[0xf0, 0x0ff, 0xf7]),
Some(Fraction::new(15, 15))
);
assert_eq!(
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::MIN)),
vec![0xf0, 0xf0, 0xf7]
);
assert_eq!(
pattern.match_and_capture(&[0xf0, 0x0f0, 0xf7]),
Some(Fraction::new(0, 15))
);
assert_eq!(
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::new(0.5))),
vec![0xf0, 0xf8, 0xf7]
);
assert_eq!(
pattern.match_and_capture(&[0xf0, 0x0f8, 0xf7]),
Some(Fraction::new(8, 15))
);
assert_eq!(&pattern.to_string(), "F0 [1111 dcba] F7");
}
#[test]
fn wrong_variable_pattern() {
let result = "F0[0000dcbaa]F7".parse::<RawMidiPattern>();
assert!(result.is_err());
}
#[test]
fn correct_resolution_1() {
// Given
let pattern: RawMidiPattern = "B0 00 [0nml kjih]".parse().unwrap();
// When
// Then
assert_eq!(pattern.resolution(), 14);
}
#[test]
fn correct_resolution_2() {
// Given
let pattern: RawMidiPattern = "B0 00 [0gfe dcba]".parse().unwrap();
// When
// Then
assert_eq!(pattern.resolution(), 7);
}
#[test]
fn fixed_pattern() {
// Given
let pattern: RawMidiPattern = "B0 00 F7".parse().unwrap();
// When
// Then
assert_eq!(pattern.resolution(), 0);
assert_eq!(pattern.max_discrete_value(), 0);
assert_eq!(pattern.match_and_capture(&[0xf0, 0x0f8, 0xf7]), None);
assert_eq!(
pattern.match_and_capture(&[0xb0, 0x00, 0xf7]),
Some(Fraction::new(0, 0))
);
}
#[test]
fn real_world_fixed_pattern() {
// Given
let pattern: RawMidiPattern = "F0 0 20 6B 7F 42 02 00 0 2F 7F F7".parse().unwrap();
// When
// Then
assert_eq!(pattern.resolution(), 0);
assert_eq!(pattern.max_discrete_value(), 0);
assert_eq!(pattern.match_and_capture(&[0xf0, 0x0f8, 0xf7]), None);
assert_eq!(
pattern.match_and_capture(&[
0xF0, 0x0, 0x20, 0x6B, 0x7F, 0x42, 0x2, 0x0, 0x0, 0x2F, 0x7F, 0xF6
]),
None
);
assert_eq!(
pattern.match_and_capture(&[
0xF0, 0x0, 0x20, 0x6B, 0x7F, 0x42, 0x2, 0x0, 0x0, 0x2F, 0x7F, 0xF7
]),
Some(Fraction::new(0, 0))
);
}
}
@@ -0,0 +1,5 @@
/// Context for source-related functions.
#[derive(Copy, Clone, Debug, Default)]
pub struct SourceContext<A> {
pub additional_script_input: A,
}
@@ -0,0 +1,16 @@
use crate::{FeedbackValue, MidiSourceScript, MidiSourceScriptOutcome};
use std::borrow::Cow;
pub struct TestMidiSourceScript;
impl MidiSourceScript<'_> for TestMidiSourceScript {
type AdditionalInput = ();
fn execute(
&self,
_input_value: FeedbackValue,
_additional_input: (),
) -> Result<MidiSourceScriptOutcome, Cow<'static, str>> {
unimplemented!()
}
}
@@ -0,0 +1,48 @@
use crate::{AbsoluteValue, ControlValue, UnitValue};
use approx::AbsDiffEq;
impl AbsDiffEq for UnitValue {
type Epsilon = f64;
fn default_epsilon() -> f64 {
f64::EPSILON
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
self.get().abs_diff_eq(&other.get(), epsilon)
}
}
impl AbsDiffEq for ControlValue {
type Epsilon = f64;
fn default_epsilon() -> f64 {
f64::EPSILON
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
match (self, other) {
(ControlValue::AbsoluteContinuous(v1), ControlValue::AbsoluteContinuous(v2)) => {
v1.abs_diff_eq(v2, epsilon)
}
_ => self == other,
}
}
}
impl AbsDiffEq for AbsoluteValue {
type Epsilon = f64;
fn default_epsilon() -> f64 {
f64::EPSILON
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
match (self, other) {
(AbsoluteValue::Continuous(v1), AbsoluteValue::Continuous(v2)) => {
v1.abs_diff_eq(v2, epsilon)
}
_ => self == other,
}
}
}