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
+41
View File
@@ -0,0 +1,41 @@
[package]
name = "helgobox-api"
version = "0.1.0"
authors = ["Benjamin Klum <benjamin.klum@helgoboss.org>"]
edition = "2021"
publish = false
[features]
default = []
[dependencies]
# For being able to use the API macro
helgobox-macros.workspace = true
reaper-low.workspace = true
serde.workspace = true
semver.workspace = true
serde_json.workspace = true
playtime-api.workspace = true
derive_more.workspace = true
strum.workspace = true
num_enum.workspace = true
enum-map.workspace = true
enumset = { workspace = true, features = ["serde", "alloc"] }
helgoboss-license-api.workspace = true
serde_with.workspace = true
[dev-dependencies]
# For testing Lua compatibility
mlua.workspace = true
# For generating Luau type definitions from our Rust APIs
syn = { workspace = true, features = ["full", "extra-traits"] }
# For generating Luau type definitions from our Rust APIs
darling.workspace = true
# For generating Luau type definitions from our Rust APIs
heck.workspace = true
# For formatting generated Luau Type definitions
stylua = { workspace = true, features = ["luau"] }
anyhow.workspace = true
[lints.clippy]
enum_glob_use = "deny"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,109 @@
use crate::bindings::luau::luau_converter::Hook;
use std::fs;
use std::path::PathBuf;
use stylua_lib::OutputVerification;
mod luau_converter;
/// The final code formatting causes error `has overflowed its stack` by default. You need to set
/// `RUST_MIN_STACK` environment variable (e.g. `RUST_MIN_STACK=104857600`) or execute the test in
/// release mode for this to work.
#[test]
pub fn export_luau() {
struct RealearnApiExportHook;
impl Hook for RealearnApiExportHook {
fn translate_crate_name(&self, rust_crate_ident: &str) -> Option<&'static str> {
match rust_crate_ident {
"playtime_api" => Some("playtime"),
_ => None,
}
}
}
export_luau_internal(
"realearn",
"Contains types and helper functions for building ReaLearn presets",
[
"src/persistence/compartment.rs",
"src/persistence/glue.rs",
"src/persistence/group.rs",
"src/persistence/mapping.rs",
"src/persistence/parameter.rs",
"src/persistence/source.rs",
"src/persistence/target.rs",
],
&RealearnApiExportHook,
["playtime"],
["../playtime-api/src/persistence/mod.rs"],
);
struct PlaytimeApiExportHook;
impl Hook for PlaytimeApiExportHook {
fn include_type(&self, simple_ident: &str) -> bool {
!matches!(
simple_ident,
"FlexibleMatrix"
| "PlaytimeApiError"
| "PlaytimePersistenceRoot"
| "RawEvenQuantization"
)
}
}
export_luau_internal(
"playtime",
"Contains types and helper functions for building Playtime presets",
["../playtime-api/src/persistence/mod.rs"],
&PlaytimeApiExportHook,
[],
[],
);
}
fn export_luau_internal<'a>(
name: &str,
description: &str,
src_files: impl IntoIterator<Item = &'a str>,
hook: &impl Hook,
requires: impl AsRef<[&'a str]>,
foreign_files: impl IntoIterator<Item = &'a str>,
) {
let rust_codes: Vec<_> = src_files
.into_iter()
.map(|src_file| {
let code = fs::read_to_string(src_file).unwrap();
let filtered_code: Vec<_> = code
.lines()
.filter(|line| !line.starts_with("//!"))
.collect();
filtered_code.join("\n")
})
.collect();
let merged_rust_code = rust_codes.join("\n\n");
let rust_file = parse_rust_code(&merged_rust_code);
let foreign_rust_files: Vec<_> = foreign_files
.into_iter()
.map(|path| {
let code = fs::read_to_string(path).unwrap();
parse_rust_code(&code)
})
.collect();
let luau_file = luau_converter::LuauFile::new(&rust_file, hook, &foreign_rust_files);
use std::fmt::Write;
let mut luau_code = "--!strict\n\n--- Attention: This file is generated from Rust code! Don't modify it directly!\n\n".to_string();
for req in requires.as_ref() {
writeln!(&mut luau_code, "local {req} = require(\"{req}\")").unwrap();
}
writeln!(&mut luau_code, "\n--- {description}").unwrap();
write!(&mut luau_code, "{luau_file}").unwrap();
let luau_code = stylua_lib::format_code(
&luau_code,
Default::default(),
None,
OutputVerification::Full,
)
.unwrap();
let dest_file = PathBuf::from(format!("../resources/api/luau/{name}.luau"));
fs::write(&dest_file, luau_code).unwrap();
}
fn parse_rust_code(code: &str) -> syn::File {
syn::parse_file(code).expect("unable to parse Rust file")
}
@@ -0,0 +1 @@
mod luau;
+9
View File
@@ -0,0 +1,9 @@
pub mod persistence;
pub mod runtime;
/// Bindings are generated as result of unit tests.
#[cfg(test)]
mod bindings;
mod util;
@@ -0,0 +1,41 @@
use crate::persistence::*;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
/// Complete content of a ReaLearn compartment, including mappings, groups, parameters etc.
#[derive(Default, Serialize, Deserialize)]
pub struct Compartment {
/// Settings of the default group in this compartment.
///
/// Group fields `id` and `name` will be ignored for the default group.
#[serde(skip_serializing_if = "Option::is_none")]
pub default_group: Option<Group>,
/// All parameters in this compartment
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Vec<Parameter>>,
/// All mapping groups in this compartment.
#[serde(skip_serializing_if = "Option::is_none")]
pub groups: Option<Vec<Group>>,
/// All mappings in this compartment.
#[serde(skip_serializing_if = "Option::is_none")]
pub mappings: Option<Vec<Mapping>>,
/// Lua code that will be compiled only once and can then be reused in various Lua scripts within mappings.
///
/// This code should return a value. This value will then be made available to the scripts. How exactly, depends
/// on the particular kind of script. In most cases, you want to return a table that contains functions, variables
/// and other stuff that you want to make available in your scripts.
#[serde(skip_serializing_if = "Option::is_none")]
pub common_lua: Option<String>,
/// Arbitrarily formed data in this compartment.
///
/// The first level is a key-value map where a key represents a sort of namespace. E.g. data that's relevant
/// for the application ReaLearn Companion has key "companion" and data relevant for the application Playtime has
/// the key "playtime". Everything nested below is application-specific.
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_data: Option<HashMap<String, serde_json::Value>>,
/// Can contain text notes, e.g. a helpful description of this compartment, instructions etc.
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub unknown_props: Option<BTreeMap<String, serde_json::Value>>,
}
@@ -0,0 +1,154 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct ControllerConfig {
/// All configured controllers.
#[serde(default)]
pub controllers: Vec<Controller>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Controller {
/// ID of the controller.
///
/// Should be unique on a particular machine and ideally globally unique (good for potential
/// merging scenarios).
pub id: String,
/// Descriptive name of the controller.
///
/// If one uses multiple controllers of the same kind, this should make clear which
/// particular controller instance we are talking about.
pub name: String,
/// If not enabled, no auto units will be created for that controller.
#[serde(default)]
pub enabled: bool,
/// Controller color.
///
/// Used e.g. for the control unit rectangle.
#[serde(skip_serializing_if = "Option::is_none")]
pub palette_color: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub connection: Option<ControllerConnection>,
/// Default controller preset to load whenever an auto unit with this controller is created.
///
/// ReaLearn has mechanisms to automatically identify and load a suitable controller preset
/// depending on which main preset is loaded. If it has to choose between multiple
/// candidates and no default controller preset is set, it will prefer a factory controller
/// preset. If a default controller preset is set and it satisfies the needs of the main preset,
/// it will use this one instead. It will also use the default controller preset if it can't
/// automatically identify the correct one.
#[serde(skip_serializing_if = "Option::is_none")]
pub default_controller_preset: Option<CompartmentPresetId>,
/// Default main preset to load whenever an auto unit with this controller is created.
// TODO-high-playtime-after-release The plan is to introduce an advanced mode where you don't just set a main preset but can define
// a decision table per controller. It's a list of rules. A rule is made from conditions
// (fixed number, typed, every condition optional, AND) and effects (fixed number, typed, optional).
// The default_main_preset would act as fallback, as last line in the list of rules, which doesn't
// define any additional condition.
// Possible conditions:
// - Playtime matrix (if at least one instance is active that has a Playtime matrix)
// - Active pot unit?
// Possible effects:
// - Main preset (optional)
// - Use auto-load in unit
// "Use auto-load in unit" uses the already existing global FX-to-preset links to do auto-load within
// the unit. FX-to-preset links already is very similar to a decision table. It's going to be a
// second global decision table, but a subordinate one, which acts on a single unit (not by adding/removing
// units). The main preset defined in the controller rule should act as fallback if none of the
// FX-to-preset links was effective. This should be implemented by the existing auto-load. Maybe by
// memorizing the preset that was active when "Auto-load depending on instance FX" was active.
// FX-to-preset links are not 100% a decision table already. Because their order doesn't matter.
// But we can turn it into one by sorting the list according to our current automatic ranking
// (a migration step). Making it a decision table would have the nice effect that the user has
// much influence and it's immediately clear why something happens, no implicit ranking. The list
// of conditions can be easily extended. E.g. we could not just react on what unit FX is
// active but also unit track and so on. In any case, we should add a "Controller" condition, so
// that one e.g. can load a different main preset depending on which FX is focused AND which controller
// is connected.
#[serde(skip_serializing_if = "Option::is_none")]
pub default_main_preset: Option<CompartmentPresetId>,
}
/// The way a controller is connected to ReaLearn.
///
/// Protocol-specific.
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum ControllerConnection {
Midi(MidiControllerConnection),
Osc(OscControllerConnection),
}
/// A connection via MIDI.
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct MidiControllerConnection {
/// The expected response to a MIDI device inquiry.
///
/// Example: "F0 7E 00 06 02 00 20 6B 02 00 04 02 0E 02 01 01 F7"
///
/// Can be used by ReaLearn to verify whether the device connected to a port is the correct one.
#[serde(skip_serializing_if = "Option::is_none")]
pub identity_response: Option<String>,
/// The MIDI input port to which this controller is usually connected on this machine.
#[serde(skip_serializing_if = "Option::is_none")]
pub input_port: Option<MidiInputPort>,
/// The MIDI output port to which this controller is usually connected on this machine.
#[serde(skip_serializing_if = "Option::is_none")]
pub output_port: Option<MidiOutputPort>,
}
/// A connection via OSC.
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct OscControllerConnection {
#[serde(skip_serializing_if = "Option::is_none")]
pub osc_device_id: Option<OscDeviceId>,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct MidiInputPort(u32);
impl MidiInputPort {
pub fn new(raw: u32) -> Self {
Self(raw)
}
pub fn get(&self) -> u32 {
self.0
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct MidiOutputPort(u32);
impl MidiOutputPort {
pub fn new(raw: u32) -> Self {
Self(raw)
}
pub fn get(&self) -> u32 {
self.0
}
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct OscDeviceId(String);
impl OscDeviceId {
pub fn get(&self) -> &str {
&self.0
}
}
/// ID of a controller or main preset (which one depends on the context).
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct CompartmentPresetId(String);
impl CompartmentPresetId {
pub fn new(raw: String) -> Self {
Self(raw)
}
pub fn get(&self) -> &str {
&self.0
}
}
@@ -0,0 +1,252 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(PartialEq, Default, Serialize, Deserialize)]
pub struct Glue {
//region Relevant for control and feedback
#[serde(skip_serializing_if = "Option::is_none")]
pub absolute_mode: Option<AbsoluteMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_interval: Option<Interval<f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_interval: Option<Interval<f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reverse: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub out_of_range_behavior: Option<OutOfRangeBehavior>,
//endregion
//region Relevant for control only (might change in future)
#[serde(skip_serializing_if = "Option::is_none")]
pub target_value_sequence: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub round_target_value: Option<bool>,
//endregion
//region Relevant for control only (guaranteed)
#[serde(skip_serializing_if = "Option::is_none")]
pub wrap: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jump_interval: Option<Interval<f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub takeover_mode: Option<TakeoverMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub control_transformation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_size_interval: Option<Interval<f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_factor_interval: Option<Interval<i32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub button_filter: Option<ButtonFilter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encoder_filter: Option<EncoderFilter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relative_mode: Option<RelativeMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub interaction: Option<Interaction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fire_mode: Option<FireMode>,
//endregion
//region Relevant for feedback only (guaranteed)
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback: Option<Feedback>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_value_table: Option<FeedbackValueTable>,
//endregion
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum FeedbackValueTable {
FromTextToDiscrete(DiscreteFeedbackValueTableContent),
FromTextToContinuous(ContinuousFeedbackValueTableContent),
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct DiscreteFeedbackValueTableContent {
pub value: HashMap<String, u32>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct ContinuousFeedbackValueTableContent {
pub value: HashMap<String, f64>,
}
#[derive(Copy, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
pub enum AbsoluteMode {
#[default]
Normal,
IncrementalButton,
ToggleButton,
MakeRelative,
PerformanceControl,
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum RelativeMode {
Normal,
MakeAbsolute,
}
impl Default for RelativeMode {
fn default() -> Self {
Self::Normal
}
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum FireMode {
Normal(NormalFireMode),
AfterTimeout(AfterTimeoutFireMode),
AfterTimeoutKeepFiring(AfterTimeoutKeepFiringFireMode),
OnSinglePress(OnSinglePressFireMode),
OnDoublePress,
}
impl Default for FireMode {
fn default() -> Self {
Self::Normal(Default::default())
}
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct NormalFireMode {
#[serde(skip_serializing_if = "Option::is_none")]
pub press_duration_interval: Option<Interval<u32>>,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct AfterTimeoutFireMode {
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct AfterTimeoutKeepFiringFireMode {
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
pub rate: Option<u32>,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct OnSinglePressFireMode {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_duration: Option<u32>,
}
#[derive(Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum VirtualColor {
Rgb(RgbColor),
Prop(PropColor),
}
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct RgbColor(pub u8, pub u8, pub u8);
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PropColor {
pub prop: String,
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum OutOfRangeBehavior {
MinOrMax,
Min,
Ignore,
}
impl Default for OutOfRangeBehavior {
fn default() -> Self {
Self::MinOrMax
}
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum TakeoverMode {
Off,
PickUpTolerant,
PickUp,
LongTimeNoSee,
Parallel,
CatchUp,
}
impl Default for TakeoverMode {
fn default() -> Self {
Self::Off
}
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum ButtonFilter {
PressOnly,
ReleaseOnly,
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum EncoderFilter {
IncrementOnly,
DecrementOnly,
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum Interaction {
SameControl,
SameTargetValue,
InverseControl,
InverseTargetValue,
InverseTargetValueOnOnly,
InverseTargetValueOffOnly,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct FeedbackCommons {
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<VirtualColor>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background_color: Option<VirtualColor>,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum Feedback {
Numeric(NumericFeedback),
Text(TextFeedback),
Dynamic(DynamicFeedback),
}
impl Default for Feedback {
fn default() -> Self {
Self::Numeric(NumericFeedback::default())
}
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct NumericFeedback {
#[serde(flatten)]
pub commons: FeedbackCommons,
#[serde(skip_serializing_if = "Option::is_none")]
pub transformation: Option<String>,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct TextFeedback {
#[serde(flatten)]
pub commons: FeedbackCommons,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_expression: Option<String>,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct DynamicFeedback {
#[serde(flatten)]
pub commons: FeedbackCommons,
#[serde(skip_serializing_if = "Option::is_none")]
pub script: Option<String>,
}
#[derive(Copy, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct Interval<T>(pub T, pub T);
@@ -0,0 +1,22 @@
use crate::persistence::*;
use serde::{Deserialize, Serialize};
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct Group {
/// An optional ID that you can assign to this group in order to refer
/// to it from somewhere else.
///
/// This ID should be unique within all groups in the same compartment.
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub control_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub activation_condition: Option<ActivationCondition>,
}
@@ -0,0 +1,51 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct InstanceSettings {
pub control: InstanceControlSettings,
}
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct InstanceControlSettings {
/// Whether auto units will be created for all controllers that have a main preset set.
pub global_control_enabled: bool,
// Local overrides of controller settings.
//
// If global control is enabled, each override will alter the behavior of the corresponding
// controller.
//
// If global control is disabled, each override which has a main preset set will enable
// that specific controller. This way you can selectively enable controllers, either with
// the global default preset or with your own one.
//
// TODO-high-playtime-after-release Controller overrides are not yet implemented because about doubts.
// What if the user deletes the controller? Then all project/instance that have
// an override of that controller will reference a now gone controller. Consequently, the
// overrides will not work anymore. Ideas:
// 1. Memorize the original controller data as part of the override and update it whenever
// the global controller changes. Then we can use that data if the controller is gone.
// => the project will still work but it will be disconnected.
// 2. Don't actually use the global controller anymore once there's an override ... that's like
// disconnecting immediately.
// 3. GOOD SOLUTION FOR NOW Don't provide the possibility for overrides. Force user to create
// a ReaLearn setup that is completely self-containing (the other extreme instead of something
// in-between), including the preset content.
// 4. Is the controller role idea better after all? I don't think so. Yes, it allows a bit
// more instance-specific tuning of global control without depending on particular
// controllers. However, the kind of tuning that it allows is far from exhaustive. Also,
// it's opinionated (clip/daw roles) and has other issues (being harder to grasp and
// awkward when it comes to all-in-one controllers that do both clip/DAW control).
// 5. INTERESTING If all we need is the possibility to disable e.g. global DAW control for a
// specific instance, we could simply let the main preset declare which usage role
// it implements (e.g. the "DAW control" role) and allow the instance to switch on/off
// roles - which will cause the main preset to be loaded or not.
// pub controller_overrides: Vec<ControllerOverride>,
}
// #[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
// pub struct ControllerOverride {
// /// ID of the controller which should be overridden.
// pub controller_id: String,
// /// If this is `None`, the controller default main preset will be used.
// pub main_preset: Option<CompartmentPresetId>,
// }
@@ -0,0 +1,190 @@
use super::*;
use derive_more::Display;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Default, Serialize, Deserialize)]
pub struct Mapping {
/// An optional ID that you can assign to this mapping in order to refer
/// to it from somewhere else.
///
/// This ID should be unique within all mappings in the compartment.
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visible_in_projection: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub control_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub activation_condition: Option<ActivationCondition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_activate: Option<LifecycleHook>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_deactivate: Option<LifecycleHook>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<Source>,
#[serde(skip_serializing_if = "Option::is_none")]
pub glue: Option<Glue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<Target>,
#[serde(skip_serializing_if = "Option::is_none")]
pub success_audio_feedback: Option<SuccessAudioFeedback>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unprocessed: Option<serde_json::Map<String, serde_json::Value>>,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct LifecycleHook {
#[serde(skip_serializing_if = "Option::is_none")]
pub send_midi_feedback: Option<Vec<SendMidiFeedbackAction>>,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum SendMidiFeedbackAction {
Raw { message: RawMidiMessage },
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RawMidiMessage {
HexString(String),
ByteArray(Vec<u8>),
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum SuccessAudioFeedback {
Simple,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum ActivationCondition {
Modifier(ModifierActivationCondition),
Bank(BankActivationCondition),
Eel(EelActivationCondition),
Expression(ExpressionActivationCondition),
TargetValue(TargetValueActivationCondition),
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct ModifierActivationCondition {
pub modifiers: Option<Vec<ModifierState>>,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
pub struct ModifierState {
pub parameter: ParamRef,
pub on: bool,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
pub struct BankActivationCondition {
pub parameter: ParamRef,
pub bank_index: u32,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
pub struct EelActivationCondition {
pub condition: String,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
pub struct ExpressionActivationCondition {
pub condition: String,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
pub struct TargetValueActivationCondition {
#[serde(skip_serializing_if = "Option::is_none")]
pub mapping: Option<String>,
pub condition: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ParamRef {
Index(u32),
Key(String),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum VirtualControlElementId {
Indexed(u32),
Named(String),
}
#[derive(
Copy,
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
Hash,
Debug,
Default,
Serialize,
Deserialize,
Display,
strum::EnumIter,
TryFromPrimitive,
IntoPrimitive,
)]
#[repr(usize)]
pub enum VirtualControlElementCharacter {
/// A control element that can represent more than 2 states.
#[default]
#[serde(alias = "multi")]
Multi,
/// A control element that can represent at a maximum 2 states.
#[serde(alias = "button")]
Button,
}
#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct OscArgument {
#[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "kind")]
pub arg_kind: Option<OscArgKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value_range: Option<Interval<f64>>,
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum OscArgKind {
Float,
Double,
Bool,
Nil,
Inf,
Int,
String,
Blob,
Time,
Long,
Char,
Color,
Midi,
Array,
}
impl Default for OscArgKind {
fn default() -> Self {
Self::Float
}
}
@@ -0,0 +1,118 @@
mod compartment;
mod controller;
mod glue;
mod group;
mod instance;
mod mapping;
mod parameter;
mod preset;
mod root;
mod session;
mod source;
mod target;
pub use compartment::*;
pub use controller::*;
pub use glue::*;
pub use group::*;
pub use instance::*;
pub use mapping::*;
pub use parameter::*;
pub use preset::*;
pub use root::*;
pub use session::*;
pub use source::*;
pub use target::*;
use semver::Version;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Envelope<T> {
#[serde(default)]
pub version: Option<Version>,
pub value: T,
}
impl<T> Envelope<T> {
pub fn new(version: Option<Version>, value: T) -> Self {
Self { version, value }
}
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum ApiObject {
/// A Playtime matrix.
ClipMatrix(Envelope<Box<Option<playtime_api::persistence::FlexibleMatrix>>>),
/// Main compartment.
MainCompartment(Envelope<Box<Compartment>>),
/// Controller compartment.
ControllerCompartment(Envelope<Box<Compartment>>),
/// A flat list of mappings.
Mappings(Envelope<Vec<Mapping>>),
/// A single mapping.
Mapping(Envelope<Box<Mapping>>),
}
impl ApiObject {
pub fn into_mappings(self) -> Option<Envelope<Vec<Mapping>>> {
match self {
ApiObject::Mappings(Envelope {
value: mappings,
version,
}) => Some(Envelope::new(version, mappings)),
ApiObject::Mapping(Envelope { value: m, version }) => {
Some(Envelope::new(version, vec![*m]))
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn example_to_json() {
let mapping = Mapping {
id: Some("volume".to_string()),
name: Some("Volume".to_string()),
tags: Some(vec!["mix".to_string(), "master".to_string()]),
group: Some("faders".to_string()),
visible_in_projection: Some(true),
enabled: Some(true),
control_enabled: Some(true),
feedback_enabled: Some(true),
activation_condition: None,
source: Some(Source::MidiControlChangeValue(
MidiControlChangeValueSource {
feedback_behavior: Some(FeedbackBehavior::Normal),
channel: Some(0),
controller_number: Some(64),
character: Some(SourceCharacter::Button),
fourteen_bit: Some(false),
},
)),
glue: Some(Glue {
source_interval: Some(Interval(0.3, 0.7)),
..Default::default()
}),
target: None,
..Default::default()
};
serde_json::to_string_pretty(&mapping).unwrap();
// std::fs::write("src/schema/test/example.json", json).unwrap();
}
#[test]
fn example_from_lua() {
use mlua::{Lua, LuaSerdeExt};
let lua = Lua::new();
let value = lua.load(include_str!("test/example.lua")).eval().unwrap();
let mapping: Mapping = lua.from_value(value).unwrap();
serde_json::to_string_pretty(&mapping).unwrap();
// std::fs::write("src/schema/test/example_from_lua.json", json).unwrap();
}
}
@@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
use std::num::NonZeroU32;
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct Parameter {
pub index: u32,
/// An optional ID that you can assign to this parameter in order to refer
/// to it from somewhere else.
///
/// This ID should be unique within all parameters in the same compartment.
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value_count: Option<NonZeroU32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value_labels: Option<Vec<String>>,
}
@@ -0,0 +1,248 @@
use crate::util::deserialize_null_default;
use semver::Version;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::collections::HashSet;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use strum::{Display, EnumString};
/// Meta data that is common to both main and controller presets.
///
/// Preset meta data is everything that is loaded right at startup in order to be able to
/// display a list of preset, do certain validations etc. It doesn't include the preset
/// content which is necessary to actually use the preset (e.g. it doesn't include the mappings).
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct CommonPresetMetaData {
/// Display name of the preset.
pub name: String,
/// The ReaLearn version for which this preset was built.
///
/// This can effect the way the preset is loaded, e.g. it can lead to different interpretation
/// or migration of properties. So care should be taken to set this correctly!
///
/// If `None`, it's assumed that it was built for a very old version (< 1.12.0-pre18) that
/// didn't have the versioning concept yet.
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
#[serde(alias = "version")]
pub realearn_version: Option<Version>,
/// Author of the preset.
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
pub author: Option<String>,
/// Preset description (prose).
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
pub description: Option<String>,
/// Preset setup instructions (prose).
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
pub setup_instructions: Option<String>,
/// Device manufacturer.
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
pub device_manufacturer: Option<String>,
/// Original name of the device.
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
pub device_name: Option<String>,
}
/// Metadata that is specific to controller presets.
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct ControllerPresetMetaData {
/// MIDI identity compatibility pattern.
///
/// Will be used for auto-adding controllers and for finding the correct controller preset when calculating auto
/// units.
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
pub midi_identity_pattern: Option<String>,
/// Possible MIDI identity compatibility patterns.
///
/// Will be used for auto-adding controllers and for finding the correct controller preset when calculating auto
/// units.
///
/// It should only be provided if the device in question doesn't reply to device queries or if it exposes
/// multiple ports which all respond with the same device identity and only one of the ports is the correct one.
/// Example: APC Key 25 mk2, which exposes a "Control" and a "Keys" port.
///
/// It's a list because names often differ between operating systems. ReaLearn will match any in the list.
#[serde(default)]
pub midi_output_port_patterns: Vec<MidiPortPattern>,
/// Provided virtual control schemes.
///
/// Will be used for finding the correct controller preset when calculating auto units.
///
/// The order matters! It directly influences the choice of the best-suited main presets. In particular,
/// schemes that are more specific to this particular controller (e.g. "novation/launchpad-mk3") should come first.
/// Generic schemes (e.g. "grid") should come last. When auto-picking a main preset, matches of more specific
/// schemes will be favored over less specific ones.
#[serde(default)]
pub provided_schemes: Vec<VirtualControlSchemeId>,
}
#[derive(Clone, Eq, PartialEq, Debug, SerializeDisplay, DeserializeFromStr)]
pub struct MidiPortPattern {
pub scope: Option<MidiPortPatternScope>,
pub name_pattern: String,
}
impl MidiPortPattern {
pub fn scope_matches(&self) -> bool {
let Some(scope) = self.scope else {
return true;
};
match scope {
MidiPortPatternScope::Windows => cfg!(windows),
MidiPortPatternScope::MacOs => cfg!(target_os = "macos"),
MidiPortPatternScope::Linux => cfg!(target_os = "linux"),
}
}
}
impl FromStr for MidiPortPattern {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((scope_string, name_pattern)) = s.split_once(':') {
if let Ok(scope) = MidiPortPatternScope::from_str(scope_string) {
// MIDI port pattern with scope restriction
let pattern = Self {
scope: Some(scope),
name_pattern: name_pattern.to_string(),
};
return Ok(pattern);
}
}
// MIDI port pattern without scope restriction
let pattern = Self {
scope: None,
name_pattern: s.to_string(),
};
Ok(pattern)
}
}
impl Display for MidiPortPattern {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(s) = self.scope {
write!(f, "{s}:")?;
}
self.name_pattern.fmt(f)?;
Ok(())
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, EnumString)]
#[strum(serialize_all = "lowercase")]
pub enum MidiPortPatternScope {
Windows,
MacOs,
Linux,
}
/// Metadata that is specific to main presets.
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct MainPresetMetaData {
/// Used virtual control schemes.
///
/// Will be used for finding the correct controller preset when calculating auto units.
#[serde(default)]
pub used_schemes: HashSet<VirtualControlSchemeId>,
/// A set of features that a Helgobox instance needs to provide for the preset to make sense.
///
/// See [instance_features].
///
/// Will be used for determining whether an auto unit should be created for a specific instance
/// or not. Example: If the required feature is "playtime" and a controller is configured with
/// this main preset but the instance doesn't contain a Playtime matrix, this instance will
/// not load the main preset.
#[serde(default)]
pub required_features: HashSet<String>,
}
impl MainPresetMetaData {
pub fn requires_playtime(&self) -> bool {
self.required_features.contains(instance_features::PLAYTIME)
}
/// Higher specificity means that the main preset uses a scheme provided by the controller that's more specific to
/// that particular controller (and therefore better suited).
///
/// When picking the "best" main preset for a given controller preset, this is the first criteria taken into
/// account, if there are two competing main preset candidates.
///
/// Given a controller preset that provides the schemes [bla, foo].
/// If main preset A uses schemes [bla] and main preset B [foo], we want main preset A to win because
/// "bla" comes first in the controller preset's list of provided schemes, meaning that "bla" is the
/// more specific scheme.
///
/// An example where this matters in practice:
/// - Controller preset "Launchpad Pro mk3 - Live mode" provides schemes [novation/launchpad-pro-mk3/live, grid]
/// - Main preset "Generic grid controller - Playtime" uses schemes [grid]
/// - Main preset "Launchpad Pro mk3 - Playtime" uses schemes [novation/launchpad-pro-mk3/live]
///
/// Without that rule, it could easily happen that "Generic grid controller - Playtime" will be picked. Bad!
///
/// Returns `None` if no scheme matches.
pub fn calc_scheme_specificity(
&self,
provided_schemes: &[VirtualControlSchemeId],
) -> Option<u8> {
let lowest_matching_index = self
.used_schemes
.iter()
.filter_map(|used_scheme| provided_schemes.iter().position(|s| s == used_scheme))
.min()?;
Some((provided_schemes.len() - lowest_matching_index) as u8)
}
/// Higher coverage means that the main preset uses more schemes provided by the controller.
///
/// When picking the "best" main preset for a given controller preset, this is the second criteria taken into
/// account, if the specificity of two main preset candidates is the same.
pub fn calc_scheme_coverage(&self, provided_schemes: &[VirtualControlSchemeId]) -> u8 {
self.used_schemes
.iter()
.filter(|used_scheme| provided_schemes.contains(used_scheme))
.count() as u8
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
pub struct VirtualControlSchemeId(String);
impl VirtualControlSchemeId {
pub fn get(&self) -> &str {
&self.0
}
}
/// Known instance features.
pub mod instance_features {
/// Instance owns a Playtime matrix.
pub const PLAYTIME: &str = "playtime";
}
@@ -0,0 +1,6 @@
use crate::persistence::session::Session;
/// Only used for JSON schema generation.
pub struct RealearnPersistenceRoot {
_session: Session,
}
@@ -0,0 +1,20 @@
use crate::persistence::*;
/// Only used for JSON schema generation at the moment.
pub struct Session {
_main_compartment: Option<Compartment>,
_clip_matrix: Option<playtime_api::persistence::Matrix>,
_mapping_snapshots: Vec<MappingSnapshot>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct MappingSnapshot {
pub id: String,
pub mappings: Vec<MappingInSnapshot>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct MappingInSnapshot {
pub id: String,
pub target_value: TargetValue,
}
@@ -0,0 +1,394 @@
use crate::persistence::{OscArgument, VirtualControlElementCharacter, VirtualControlElementId};
use derive_more::Display;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
use strum::EnumIter;
#[derive(PartialEq, Default, Serialize, Deserialize)]
#[serde(tag = "kind")]
#[allow(clippy::enum_variant_names)]
pub enum Source {
// None
#[default]
None,
// REAPER
MidiDeviceChanges,
RealearnInstanceStart,
RealearnCompartmentLoaded,
Timer(TimerSource),
RealearnParameter(RealearnParameterSource),
Speech,
// MIDI
MidiNoteVelocity(MidiNoteVelocitySource),
MidiNoteKeyNumber(MidiNoteKeyNumberSource),
MidiPolyphonicKeyPressureAmount(MidiPolyphonicKeyPressureAmountSource),
MidiControlChangeValue(MidiControlChangeValueSource),
MidiProgramChangeNumber(MidiProgramChangeNumberSource),
MidiSpecificProgramChange(MidiSpecificProgramChangeSource),
MidiChannelPressureAmount(MidiChannelPressureAmountSource),
MidiPitchBendChangeValue(MidiPitchBendChangeValueSource),
MidiParameterNumberValue(MidiParameterNumberValueSource),
MidiClockTempo,
MidiClockTransport(MidiClockTransportSource),
MidiRaw(MidiRawSource),
MidiScript(MidiScriptSource),
MackieLcd(MackieLcdSource),
XTouchMackieLcd(XTouchMackieLcdSource),
MackieSevenSegmentDisplay(MackieSevenSegmentDisplaySource),
SlKeyboardDisplay(SlKeyboardDisplaySource),
SiniConE24Display(SiniConE24DisplaySource),
LaunchpadProScrollingTextDisplay,
// OSC
Osc(OscSource),
// Keyboard
Key(KeySource),
// StreamDeck
StreamDeck(StreamDeckSource),
// Virtual
Virtual(VirtualSource),
}
// Only makes sense for sources that support both control *and* feedback.
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum FeedbackBehavior {
Normal,
SendFeedbackAfterControl,
PreventEchoFeedback,
}
impl Default for FeedbackBehavior {
fn default() -> Self {
Self::Normal
}
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiNoteVelocitySource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_number: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiNoteKeyNumberSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiPolyphonicKeyPressureAmountSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_number: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiControlChangeValueSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub controller_number: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character: Option<SourceCharacter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fourteen_bit: Option<bool>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiProgramChangeNumberSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiSpecificProgramChangeSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub program_number: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiChannelPressureAmountSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiPitchBendChangeValueSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiParameterNumberValueSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub number: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fourteen_bit: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registered: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character: Option<SourceCharacter>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiClockTransportSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<MidiClockTransportMessage>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiRawSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character: Option<SourceCharacter>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiScriptSource {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "kind")]
pub script_kind: Option<MidiScriptKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script: Option<String>,
}
/// Kind of a MIDI script
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Default,
Serialize,
Deserialize,
EnumIter,
TryFromPrimitive,
IntoPrimitive,
Display,
)]
#[repr(usize)]
pub enum MidiScriptKind {
#[default]
#[serde(alias = "eel")]
#[display(fmt = "EEL")]
Eel,
#[serde(alias = "lua")]
#[display(fmt = "Lua")]
Lua,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Serialize, Deserialize)]
pub enum SourceCharacter {
#[default]
Range,
Button,
// 127 = decrement; 0 = none; 1 = increment
Relative1,
// 63 = decrement; 64 = none; 65 = increment
Relative2,
// 65 = decrement; 0 = none; 1 = increment
Relative3,
StatefulButton,
}
#[derive(Copy, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
pub enum MidiClockTransportMessage {
#[default]
Start,
Continue,
Stop,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MackieLcdSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub extender_index: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct XTouchMackieLcdSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub extender_index: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct SlKeyboardDisplaySource {
#[serde(skip_serializing_if = "Option::is_none")]
pub section: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u8>,
}
#[derive(Copy, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct MackieSevenSegmentDisplaySource {
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<MackieSevenSegmentDisplayScope>,
}
#[derive(Copy, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
pub enum MackieSevenSegmentDisplayScope {
All,
#[default]
Assignment,
Tc,
TcHoursBars,
TcMinutesBeats,
TcSecondsSub,
TcFramesTicks,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct SiniConE24DisplaySource {
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_index: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub item_index: Option<u8>,
}
#[derive(Default, PartialEq, Serialize, Deserialize)]
pub struct OscSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub argument: Option<OscArgument>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relative: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_arguments: Option<Vec<String>>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct RealearnParameterSource {
pub parameter_index: u32,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct TimerSource {
pub duration: u64,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct KeySource {
#[serde(skip_serializing_if = "Option::is_none")]
pub keystroke: Option<Keystroke>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct StreamDeckSource {
pub button_index: u32,
#[serde(default)]
pub button_design: StreamDeckButtonDesign,
}
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonDesign {
#[serde(default)]
pub background: StreamDeckButtonBackground,
#[serde(default)]
pub foreground: StreamDeckButtonForeground,
#[serde(default)]
pub static_text: String,
}
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum StreamDeckButtonForeground {
#[default]
None,
FadingColor(StreamDeckButtonFadingColorForeground),
FadingImage(StreamDeckButtonFadingImageForeground),
SlidingImage(StreamDeckButtonSlidingImageForeground),
FullBar(StreamDeckButtonFullBarForeground),
Knob(StreamDeckButtonKnobForeground),
}
#[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum StreamDeckButtonBackground {
Color(StreamDeckButtonColorBackground),
Image(StreamDeckButtonImageBackground),
}
impl Default for StreamDeckButtonBackground {
fn default() -> Self {
Self::Color(StreamDeckButtonColorBackground::default())
}
}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonFadingImageForeground {
pub path: String,
}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonSlidingImageForeground {
pub path: String,
}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonFadingColorForeground {}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonFullBarForeground {}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonKnobForeground {}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonImageBackground {
pub path: String,
}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonColorBackground {}
#[derive(Copy, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct Keystroke {
pub modifiers: u8,
pub key: u16,
}
#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct VirtualSource {
pub id: VirtualControlElementId,
#[serde(skip_serializing_if = "Option::is_none")]
pub character: Option<VirtualControlElementCharacter>,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
return {
name = "Pedal to Delay",
source = {
kind = "MidiControlChangeValue",
channel = 0,
controller_number = 64,
character = "Button",
fourteen_bit = false,
},
glue = {
target_interval = {0, 0.53},
jump_interval = {0, 0.53},
step_size_interval = {0.01, 0.01},
step_factor_interval = {1, 1},
},
target = {
kind = "FxParameterValue",
parameter = {
address = "ById",
fx = {
address = "ById",
chain = {
address = "Track",
},
id = "22FD4FC0-A4DD-4E6F-BCB3-38F242B557B2",
},
index = 23,
},
},
}
@@ -0,0 +1,28 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum GlobalInfoEvent {
Generic(GenericGlobalInfoEvent),
AutoAddedController(AutoAddedControllerEvent),
PlaytimeActivationSucceeded,
PlaytimeActivationFailed,
}
impl GlobalInfoEvent {
pub fn generic(message: impl Into<String>) -> Self {
Self::Generic(GenericGlobalInfoEvent {
message: message.into(),
})
}
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct GenericGlobalInfoEvent {
pub message: String,
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct AutoAddedControllerEvent {
pub controller_id: String,
}
@@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum InstanceInfoEvent {
Generic(GenericInstanceInfoEvent),
/// If attempting to MIDI-learn but the track is either not armed or the input monitoring mode
/// is not suitable.
MidiLearnFromFxInputButTrackNotArmed,
MidiLearnFromFxInputButTrackHasAudioInput,
}
impl InstanceInfoEvent {
pub fn generic(message: impl Into<String>) -> Self {
Self::Generic(GenericInstanceInfoEvent {
message: message.into(),
})
}
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct GenericInstanceInfoEvent {
pub message: String,
}
@@ -0,0 +1,13 @@
use helgoboss_license_api::persistence::LicenseData;
use serde::Serialize;
#[derive(Clone, Eq, PartialEq, Debug, Serialize)]
pub struct LicenseInfo {
pub licenses: Vec<ValidatedLicense>,
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize)]
pub struct ValidatedLicense {
pub license: LicenseData,
pub valid: bool,
}
@@ -0,0 +1,14 @@
mod preset;
pub use preset::*;
mod global_info_event;
pub use global_info_event::*;
mod instance_info_event;
pub use instance_info_event::*;
mod reaper;
pub use reaper::*;
mod licensing;
pub use licensing::*;
@@ -0,0 +1,16 @@
use crate::persistence::{CommonPresetMetaData, ControllerPresetMetaData, MainPresetMetaData};
use serde::Serialize;
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize)]
pub struct MainPreset {
pub id: String,
pub common: CommonPresetMetaData,
pub specific: MainPresetMetaData,
}
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize)]
pub struct ControllerPreset {
pub id: String,
pub common: CommonPresetMetaData,
pub specific: ControllerPresetMetaData,
}
@@ -0,0 +1,14 @@
#![allow(non_snake_case)]
use helgobox_macros::reaper_api;
reaper_api![
HelgoboxApi, HelgoboxApiPointers, HelgoboxApiSession, register_helgobox_api
{
/// Finds the first Helgobox instance in the given project.
///
/// If the given project is `null`, it will look in the current project.
///
/// Returns the instance ID or -1 if none exists.
HB_FindFirstHelgoboxInstanceInProject(project: *mut reaper_low::raw::ReaProject) -> std::ffi::c_int;
}
];
+15
View File
@@ -0,0 +1,15 @@
use serde::{Deserialize, Deserializer};
/// Makes sure that JSON `null` is treated the same as omitting a property.
///
/// Use as `#[serde(deserialize_with = "deserialize_null_default")]`.
///
/// See https://github.com/serde-rs/serde/issues/1098#issuecomment-760711617.
pub fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}