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
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "realearn-csi"
version = "0.1.0"
authors = ["Benjamin Klum <benjamin.klum@helgoboss.org>"]
edition = "2021"
publish = false
[dependencies]
nom.workspace = true
helgoboss-midi.workspace = true
base.workspace = true
helgobox-api.workspace = true
derive_more.workspace = true
[lints.clippy]
enum_glob_use = "deny"
+837
View File
@@ -0,0 +1,837 @@
use base::hash_util::NonCryptoHashSet;
use derive_more::Display;
use helgoboss_midi::{RawShortMessage, ShortMessage, StructuredShortMessage, U14, U7};
use helgobox_api::persistence::{
ApiObject, ButtonFilter, Compartment, Envelope, Glue, Interval, MackieLcdSource,
MackieSevenSegmentDisplayScope, MackieSevenSegmentDisplaySource, Mapping,
MidiChannelPressureAmountSource, MidiControlChangeValueSource, MidiNoteVelocitySource,
MidiPitchBendChangeValueSource, MidiPolyphonicKeyPressureAmountSource,
MidiProgramChangeNumberSource, MidiRawSource, Source, SourceCharacter, Target,
VirtualControlElementCharacter, VirtualControlElementId, VirtualTarget,
};
use std::error::Error;
use std::fmt::{Display, Formatter};
mod parser;
mod schema;
pub use schema::*;
pub enum CsiObject {
Widgets(Vec<Widget>),
}
type CsiResult<T> = Result<T, Box<dyn Error>>;
pub fn deserialize_csi_object_from_csi(text: &str) -> Result<CsiObject, Box<dyn Error>> {
let widgets = parser::mst_file_content(text)?;
Ok(CsiObject::Widgets(widgets))
}
#[derive(Default)]
pub struct Annotator {
context_stack: Vec<String>,
annotations: Vec<Annotation>,
}
impl Annotator {
pub fn new() -> Self {
Self::default()
}
pub fn with_context<R>(&mut self, context: String, f: impl FnOnce(&mut Annotator) -> R) -> R {
self.context_stack.push(context);
let result = f(self);
self.context_stack.pop();
result
}
pub fn info(&mut self, message: impl Into<String>) {
self.annotate(message, AnnotationLevel::Info);
}
pub fn warn(&mut self, message: impl Into<String>) {
self.annotate(message, AnnotationLevel::Warn);
}
fn annotate(&mut self, message: impl Into<String>, level: AnnotationLevel) {
let annotation = Annotation {
context_stack: self.context_stack.clone(),
message: message.into(),
level,
};
self.annotations.push(annotation);
}
pub fn build_result<T>(self, value: T) -> AnnotatedResult<T> {
AnnotatedResult {
value,
annotations: self.annotations,
}
}
}
#[derive(Display)]
enum AnnotationLevel {
#[display(fmt = "INFO")]
Info,
#[display(fmt = "WARN")]
Warn,
}
pub struct Annotation {
context_stack: Vec<String>,
level: AnnotationLevel,
message: String,
}
impl Display for Annotation {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
let context_expression = self.context_stack.join(" => ");
write!(f, "{} {}: {}", self.level, context_expression, self.message)
}
}
pub struct AnnotatedResult<T> {
pub value: T,
pub annotations: Vec<Annotation>,
}
impl<T> AnnotatedResult<T> {
pub fn without_annotations(value: T) -> Self {
Self {
value,
annotations: vec![],
}
}
}
impl CsiObject {
pub fn try_into_api_object(self) -> Result<AnnotatedResult<ApiObject>, Box<dyn Error>> {
let mut annotator = Annotator::new();
use CsiObject as O;
let api_object = match self {
O::Widgets(widgets) => {
let results: Vec<_> = widgets
.into_iter()
.filter_map(|w| {
annotator.with_context(format!("Widget \"{}\"", w.name), |annotator| {
match convert_widget(w, annotator) {
Ok(res) => Some(res),
Err(e) => {
annotator.warn(e.to_string());
None
}
}
})
})
.collect();
let has_duplicate_widget_ids = {
let id_set: NonCryptoHashSet<_> =
results.iter().map(|r| r.widget_id.clone()).collect();
results.len() != id_set.len()
};
if has_duplicate_widget_ids {
annotator.warn("Duplicate widget IDs were produced because of truncation. This will most likely lead to problems! Please shorten the affected widget names.")
}
let mappings = results.into_iter().flat_map(|r| r.mappings).collect();
let compartment = Compartment {
mappings: Some(mappings),
..Default::default()
};
ApiObject::ControllerCompartment(Envelope {
version: None,
value: Box::new(compartment),
})
}
};
Ok(annotator.build_result(api_object))
}
}
struct WidgetConvResult {
widget_id: String,
mappings: Vec<Mapping>,
}
fn convert_widget(widget: Widget, annotator: &mut Annotator) -> CsiResult<WidgetConvResult> {
let widget_name = widget.name;
let widget_id = convert_widget_name_to_id(&widget_name, annotator)?;
let mappings = widget
.capabilities
.into_iter()
.flat_map(|c| {
annotator.with_context(format!("Capability \"{c}\""), |annotator| {
convert_capability_to_mappings(&widget_name, &widget_id, c, annotator)
.unwrap_or_else(|e| {
annotator.info(e.to_string());
vec![]
})
})
})
.collect();
let res = WidgetConvResult {
widget_id,
mappings,
};
Ok(res)
}
fn convert_capability_to_mappings(
widget_name: &str,
widget_id: &str,
capability: Capability,
annotator: &mut Annotator,
) -> CsiResult<Vec<Mapping>> {
let base_mapping = Mapping {
id: Some(format!("{widget_id}-{capability}")),
name: Some(format!("{widget_name} - {capability}")),
..Default::default()
};
let target_character = if capability.is_virtual_button() {
VirtualControlElementCharacter::Button
} else {
VirtualControlElementCharacter::Multi
};
let mappings = match capability {
Capability::Press { press, release } => {
let press_res = convert_max_short_msg_to_source(MsgConvInput {
msg: press,
character: SourceCharacter::Button,
press_only: release.is_none(),
fourteen_bit: false,
})?;
// If press-only and we have a value that's neither MAX or MIN, it means we want to a
// message with this particular value ONLY. In this case it's best to create a raw
// MIDI message source.
if let Some(release) = release {
let release_res = convert_max_short_msg_to_source(MsgConvInput {
msg: release,
character: SourceCharacter::Button,
press_only: false,
fourteen_bit: false,
})?;
if release_res.source != press_res.source {
annotator.warn("Press and release messages differ not just in value but also in type or channel. This is very uncommon and might be a mistake or shortcoming of the widget definition. In general, ReaLearn supports such exotic cases but the CSI-to-ReaLearn conversion not yet. If you really need it, open an issue at GitHub.")
}
}
let mapping = Mapping {
feedback_enabled: Some(false),
source: Some(press_res.source),
glue: {
let g = Glue {
button_filter: if release.is_some() {
None
} else {
Some(ButtonFilter::PressOnly)
},
reverse: Some(press_res.reverse_if_button_like),
..Default::default()
};
Some(g)
},
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::FbTwoState { on, off } => {
let on_res = convert_max_short_msg_to_source(MsgConvInput {
msg: on,
character: SourceCharacter::Button,
press_only: false,
fourteen_bit: false,
})?;
let off_res = convert_max_short_msg_to_source(MsgConvInput {
msg: off,
character: SourceCharacter::Button,
press_only: false,
fourteen_bit: false,
})?;
if off_res.source != on_res.source {
annotator.warn("On and off messages differ not just in value but also in type or channel. This is very uncommon and might be a mistake or shortcoming of the widget definition. In general, ReaLearn supports such exotic cases but the CSI-to-ReaLearn conversion for this case has not been implemented. If you really need it, open an issue at GitHub.")
}
let mapping = Mapping {
control_enabled: Some(false),
source: Some(on_res.source),
glue: {
let g = Glue {
reverse: Some(on_res.reverse_if_button_like),
..Default::default()
};
Some(g)
},
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::Encoder {
main,
accelerations,
} => {
let acc_conv_res = convert_accelerations(accelerations, annotator)?;
let main_res = convert_max_short_msg_to_source(MsgConvInput {
msg: main,
character: acc_conv_res.character,
press_only: false,
fourteen_bit: false,
})?;
let mapping = Mapping {
feedback_enabled: Some(false),
source: Some(main_res.source),
glue: {
let g = Glue {
step_factor_interval: Some(acc_conv_res.step_factor_interval),
..Default::default()
};
Some(g)
},
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::FbEncoder { max } => {
let max_res = convert_max_short_msg_to_source(MsgConvInput {
msg: max,
character: SourceCharacter::Relative1,
press_only: false,
fourteen_bit: false,
})?;
let mapping = Mapping {
control_enabled: Some(false),
source: Some(max_res.source),
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::Toggle { on } => {
let on_res = convert_max_short_msg_to_source(MsgConvInput {
msg: on,
character: SourceCharacter::Button,
press_only: false,
fourteen_bit: false,
})?;
let mapping = Mapping {
feedback_enabled: Some(false),
source: Some(on_res.source),
glue: {
let g = Glue {
reverse: Some(on_res.reverse_if_button_like),
..Default::default()
};
Some(g)
},
// TODO-medium Mmh, there's also a separate mapping for that. What's the point of
// "Toggle" then? Maybe it's just a duplicate in the X-Touch mst file. Check!
target: virtual_target(
extended_control_element_id(widget_id, "push")?,
target_character,
),
..base_mapping
};
vec![mapping]
}
Capability::Touch { touch, release } => {
let touch_res = convert_max_short_msg_to_source(MsgConvInput {
msg: touch,
character: SourceCharacter::Button,
press_only: false,
fourteen_bit: false,
})?;
let release_res = convert_max_short_msg_to_source(MsgConvInput {
msg: release,
character: SourceCharacter::Button,
press_only: false,
fourteen_bit: false,
})?;
if release_res.source != touch_res.source {
annotator.warn("Touch and release messages differ not just in value but also in type or channel. This is very uncommon and might be a mistake or shortcoming of the widget definition. In general, ReaLearn supports such exotic cases but the CSI-to-ReaLearn conversion for this case has not been implemented. If you really need it, open an issue at GitHub.")
}
let mapping = Mapping {
feedback_enabled: Some(false),
source: Some(touch_res.source),
glue: {
let g = Glue {
reverse: Some(touch_res.reverse_if_button_like),
..Default::default()
};
Some(g)
},
target: virtual_target(
extended_control_element_id(widget_id, "touch")?,
target_character,
),
..base_mapping
};
vec![mapping]
}
Capability::Fader14Bit { max } => {
let max_res = convert_max_short_msg_to_source(MsgConvInput {
msg: max,
character: SourceCharacter::Range,
press_only: false,
fourteen_bit: true,
})?;
let mapping = Mapping {
feedback_enabled: Some(false),
source: Some(max_res.source),
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::FbFader14Bit { max } => {
let max_res = convert_max_short_msg_to_source(MsgConvInput {
msg: max,
character: SourceCharacter::Range,
press_only: false,
fourteen_bit: true,
})?;
let mapping = Mapping {
control_enabled: Some(false),
source: Some(max_res.source),
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::FbMcuVuMeter { index } => {
let source = Source::MidiRaw(MidiRawSource {
feedback_behavior: None,
pattern: Some(format!("D0 [{index:04b} dcba]")),
character: Some(SourceCharacter::Range),
});
let mapping = Mapping {
control_enabled: Some(false),
source: Some(source),
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::FbMcuTimeDisplay => {
let source = Source::MackieSevenSegmentDisplay(MackieSevenSegmentDisplaySource {
scope: Some(MackieSevenSegmentDisplayScope::Tc),
});
let mapping = Mapping {
control_enabled: Some(false),
source: Some(source),
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::FbMcuDisplayLower { index } => {
let mapping = create_mackie_lcd_mapping(base_mapping, widget_id.to_owned(), index, 1);
vec![mapping]
}
Capability::FbMcuDisplayUpper { index } => {
let mapping = create_mackie_lcd_mapping(base_mapping, widget_id.to_owned(), index, 0);
vec![mapping]
}
Capability::Unknown(_) => {
annotator.warn("Unknown capability. If this is a valid CSI capability, please create a ReaLearn issue at GitHub.");
vec![]
}
};
Ok(mappings)
}
fn extended_control_element_id(base: &str, extension: &str) -> CsiResult<String> {
let res = format!("{base}/{extension}");
if res.len() > MAX_CONTROL_ELEMENT_ID_LENGTH {
return Err(format!("{res} is an invalid control element ID because it's too long. Please shorten the corresponding widget name.").into());
}
Ok(res)
}
fn virtual_target(id: String, character: VirtualControlElementCharacter) -> Option<Target> {
let t = VirtualTarget {
id: VirtualControlElementId::Named(id),
character: Some(character),
learnable: None,
};
Some(Target::Virtual(t))
}
struct AccelerationConvResult {
character: SourceCharacter,
step_factor_interval: Interval<i32>,
}
fn convert_accelerations(
accelerations: Option<Accelerations>,
annotator: &mut Annotator,
) -> CsiResult<AccelerationConvResult> {
let accelerations = if let Some(acc) = accelerations {
acc
} else {
let res = AccelerationConvResult {
character: SourceCharacter::Relative3,
step_factor_interval: Interval(1, 1),
};
return Ok(res);
};
let native_decrements = NativeAcceleration::from_acceleration(accelerations.decrements)
.map_err(|_| "No acceleration values provided for counter-clockwise encoder movement")?;
let native_increments = NativeAcceleration::from_acceleration(accelerations.increments)
.map_err(|_| "No acceleration values provided for clockwise encoder movement")?;
let neutral_accelerations = neutralize_accelerations(native_decrements, native_increments)?;
let res = AccelerationConvResult {
character: neutral_accelerations.character,
step_factor_interval: Interval(1, neutral_accelerations.max_acceleration()),
};
let dec_diff = neutral_accelerations.decrements.diff();
let inc_diff = neutral_accelerations.increments.diff();
if dec_diff.is_non_continuous() || inc_diff.is_non_continuous() {
annotator.warn(
"Non-continuous acceleration profile detected. Encoder acceleration behavior might be slightly different in ReaLearn.",
);
}
if dec_diff != inc_diff {
annotator.warn("Clockwise acceleration profile differs from counter-clockwise acceleration profile. In general supported by ReaLearn but not yet supported by the CSI-to-ReaLearn conversion. That means the acceleration behavior might be slightly different in ReaLearn.");
}
Ok(res)
}
const MAX_CONTROL_ELEMENT_ID_LENGTH: usize = 16;
fn convert_widget_name_to_id(name: &str, annotator: &mut Annotator) -> CsiResult<String> {
let id = name
.chars()
.filter(|ch| ch.is_ascii_alphanumeric() || ch.is_ascii_punctuation())
.take(MAX_CONTROL_ELEMENT_ID_LENGTH)
.collect();
if name.chars().count() > MAX_CONTROL_ELEMENT_ID_LENGTH {
annotator.info(format!("ReaLearn doesn't allow for virtual control element IDs longer than 16 characters, therefore the widget name \"{name}\" was truncated to the ID \"{id}\"."));
}
Ok(id)
}
struct MsgConvOutput {
source: Source,
reverse_if_button_like: bool,
}
struct MsgConvInput {
msg: RawShortMessage,
character: SourceCharacter,
press_only: bool,
fourteen_bit: bool,
}
impl MsgConvInput {
fn should_produce_raw_midi_source_7_bit(&self, value: U7) -> bool {
self.press_only && (1..U7::MAX.get()).contains(&value.get())
}
fn should_produce_raw_midi_source_14_bit(&self, value: U14) -> bool {
self.press_only && (1..U14::MAX.get()).contains(&value.get())
}
}
fn convert_max_short_msg_to_source(input: MsgConvInput) -> CsiResult<MsgConvOutput> {
use StructuredShortMessage as M;
let res = match input.msg.to_structured() {
M::NoteOn {
channel,
key_number,
velocity,
} => {
if input.should_produce_raw_midi_source_7_bit(velocity) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiNoteVelocity(MidiNoteVelocitySource {
feedback_behavior: None,
channel: Some(channel.get()),
key_number: Some(key_number.get()),
}),
reverse_if_button_like: velocity == U7::MIN,
}
}
}
M::NoteOff {
channel,
key_number,
velocity,
} => {
if input.should_produce_raw_midi_source_7_bit(velocity) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiNoteVelocity(MidiNoteVelocitySource {
feedback_behavior: None,
channel: Some(channel.get()),
key_number: Some(key_number.get()),
}),
reverse_if_button_like: true,
}
}
}
M::PolyphonicKeyPressure {
channel,
key_number,
pressure_amount,
} => {
if input.should_produce_raw_midi_source_7_bit(pressure_amount) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiPolyphonicKeyPressureAmount(
MidiPolyphonicKeyPressureAmountSource {
feedback_behavior: None,
channel: Some(channel.get()),
key_number: Some(key_number.get()),
},
),
reverse_if_button_like: pressure_amount == U7::MIN,
}
}
}
M::ControlChange {
channel,
controller_number,
control_value,
} => {
if input.should_produce_raw_midi_source_7_bit(control_value) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiControlChangeValue(MidiControlChangeValueSource {
feedback_behavior: None,
channel: Some(channel.get()),
controller_number: Some(controller_number.get()),
character: Some(input.character),
fourteen_bit: Some(input.fourteen_bit),
}),
reverse_if_button_like: control_value == U7::MIN,
}
}
}
M::ProgramChange {
channel,
program_number,
} => {
if input.should_produce_raw_midi_source_7_bit(program_number) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiProgramChangeNumber(MidiProgramChangeNumberSource {
feedback_behavior: None,
channel: Some(channel.get()),
}),
reverse_if_button_like: program_number == U7::MIN,
}
}
}
M::ChannelPressure {
channel,
pressure_amount,
} => {
if input.should_produce_raw_midi_source_7_bit(pressure_amount) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiChannelPressureAmount(MidiChannelPressureAmountSource {
feedback_behavior: None,
channel: Some(channel.get()),
}),
reverse_if_button_like: pressure_amount == U7::MIN,
}
}
}
M::PitchBendChange {
channel,
pitch_bend_value,
} => {
if input.should_produce_raw_midi_source_14_bit(pitch_bend_value) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiPitchBendChangeValue(MidiPitchBendChangeValueSource {
feedback_behavior: None,
channel: Some(channel.get()),
}),
reverse_if_button_like: pitch_bend_value == U14::MIN,
}
}
}
_ => {
return Err(format!("Message {:?} not handled in source conversion", input.msg).into())
}
};
Ok(res)
}
fn convert_short_msg_to_raw_midi_source(input: MsgConvInput) -> MsgConvOutput {
MsgConvOutput {
source: Source::MidiRaw(MidiRawSource {
feedback_behavior: None,
pattern: Some(convert_to_raw_midi_pattern(input.msg)),
character: Some(input.character),
}),
reverse_if_button_like: false,
}
}
fn convert_to_raw_midi_pattern(msg: RawShortMessage) -> String {
let (status_byte, data_byte_1, data_byte_2) = msg.to_bytes();
format!(
"{:02X} {:02X} {:02X}",
status_byte,
data_byte_1.get(),
data_byte_2.get()
)
}
fn create_mackie_lcd_mapping(
base_mapping: Mapping,
widget_id: String,
index: u8,
line: u8,
) -> Mapping {
let source = Source::MackieLcd(MackieLcdSource {
extender_index: None,
channel: Some(index),
line: Some(line),
});
Mapping {
control_enabled: Some(false),
source: Some(source),
target: virtual_target(widget_id, VirtualControlElementCharacter::Multi),
..base_mapping
}
}
struct NeutralAccelerations {
character: SourceCharacter,
/// This should contain values > 1 where each value contains the decrement amount.
decrements: NeutralAcceleration,
/// This should contain values > 1 where each value contains the increment amount.
increments: NeutralAcceleration,
}
impl NeutralAccelerations {
pub fn max_acceleration(&self) -> i32 {
std::cmp::max(
self.decrements.0.iter().max().copied().unwrap_or(0),
self.increments.0.iter().max().copied().unwrap_or(0),
)
}
}
struct NativeAcceleration(Vec<u8>);
impl NativeAcceleration {
pub fn from_acceleration(acc: Acceleration) -> Result<Self, &'static str> {
let vec = match acc {
Acceleration::Sequence(s) => s,
Acceleration::Range(r) => r.collect(),
};
if vec.is_empty() {
return Err("no acceleration values provided");
}
Ok(Self(vec))
}
pub fn first(&self) -> u8 {
*self.0.first().expect("impossible")
}
pub fn neutralize(self, crementor: i32) -> NeutralAcceleration {
let vec = self
.0
.into_iter()
.map(|b| (b as i32 + crementor).abs())
.collect();
NeutralAcceleration(vec)
}
}
struct NeutralAcceleration(Vec<i32>);
impl NeutralAcceleration {
pub fn diff(&self) -> AccelerationDiff {
let vec = self
.0
.iter()
.copied()
.zip(self.0.iter().copied().skip(1))
.map(|(prev, next)| next - prev)
.collect();
AccelerationDiff(vec)
}
}
#[derive(PartialEq)]
struct AccelerationDiff(Vec<i32>);
impl AccelerationDiff {
pub fn is_non_continuous(&self) -> bool {
self.0.iter().any(|d| *d != 1)
}
}
fn neutralize_accelerations(
decrements: NativeAcceleration,
increments: NativeAcceleration,
) -> CsiResult<NeutralAccelerations> {
let (character, decrementor, incrementor) = match (decrements.first(), increments.first()) {
(121..=127, 1..=7) => (SourceCharacter::Relative1, -128, 0),
(57..=63, 65..=71) => (SourceCharacter::Relative2, -64, -64),
(65..=71, 1..=7) => (SourceCharacter::Relative3, -64, 0),
_ => return Err("Unsupported relative encoder type".into()),
};
let neutralized_acc = NeutralAccelerations {
character,
decrements: decrements.neutralize(decrementor),
increments: increments.neutralize(incrementor),
};
Ok(neutralized_acc)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn neutralize_accelerations_relative_3() {
// Given
let decrements = NativeAcceleration(vec![0x41, 0x42, 0x43]);
let increments = NativeAcceleration(vec![0x01, 0x02, 0x03]);
// When
let neutralized = neutralize_accelerations(decrements, increments).unwrap();
// Then
assert_eq!(neutralized.character, SourceCharacter::Relative3);
assert_eq!(neutralized.decrements.0, vec![1, 2, 3]);
assert_eq!(neutralized.increments.0, vec![1, 2, 3]);
}
#[test]
fn neutralize_accelerations_relative_1() {
// Given
let decrements = NativeAcceleration(vec![0x7f, 0x7e, 0x7c, 0x7a]);
let increments = NativeAcceleration(vec![0x01, 0x04, 0x07]);
// When
let neutralized = neutralize_accelerations(decrements, increments).unwrap();
// Then
assert_eq!(neutralized.character, SourceCharacter::Relative1);
assert_eq!(neutralized.decrements.0, vec![1, 2, 4, 6]);
assert_eq!(neutralized.increments.0, vec![1, 4, 7]);
}
#[test]
fn neutral_diff() {
// Given
let increments = NeutralAcceleration(vec![0x01, 0x04, 0x07]);
// When
let diff = increments.diff();
// Then
assert_eq!(diff.0, vec![3, 3]);
}
}
+457
View File
@@ -0,0 +1,457 @@
use crate::schema::{Acceleration, Accelerations, Capability, Widget};
use helgoboss_midi::{RawShortMessage, ShortMessageFactory};
use nom::branch::alt;
use nom::bytes::complete::{tag, take_while1, take_while_m_n};
use nom::character::complete::{multispace0, not_line_ending, space0, space1};
use nom::combinator::{all_consuming, map, map_res, opt, verify};
use nom::error::ParseError;
use nom::multi::{separated_list0, separated_list1};
use nom::sequence::{preceded, separated_pair};
use nom::{character::complete::char, sequence::delimited, sequence::tuple, Err, IResult, Parser};
use std::convert::TryInto;
type Res<'a, T> = IResult<&'a str, T>;
pub fn mst_file_content(input: &str) -> Result<Vec<Widget>, String> {
let non_comment_lines: Vec<_> = input
.lines()
.filter(|l| !l.trim_start().starts_with('/'))
.collect();
let input_without_comments = non_comment_lines.join("\n");
let (_, widgets) = all_consuming(widgets)(&input_without_comments).map_err(|e| {
let short_err = match e {
Err::Error(e) => Err::Error(nom::error::Error::new(&e.input[0..30], e.code)),
e => e,
};
short_err.to_string()
})?;
Ok(widgets)
}
fn widgets(input: &str) -> Res<Vec<Widget>> {
delimited(
multispace0,
separated_list0(space_with_at_least_one_line_ending, widget),
multispace0,
)(input)
}
fn widget(input: &str) -> Res<Widget> {
map(
tuple((
widget_begin,
space_with_at_least_one_line_ending,
widget_capabilities,
space_with_at_least_one_line_ending,
tag("WidgetEnd"),
)),
|(name, _, capabilities, _, _)| Widget {
name: name.to_owned(),
capabilities,
},
)(input)
}
fn widget_begin(input: &str) -> Res<&str> {
preceded(
tuple((tag("Widget"), space1)),
take_while1(|ch: char| ch.is_alphanumeric() || matches!(ch, '-' | '_')),
)(input)
}
fn widget_capabilities(input: &str) -> Res<Vec<Capability>> {
separated_list0(space_with_at_least_one_line_ending, capability)(input)
}
fn capability(input: &str) -> Res<Capability> {
alt((
capability_press,
capability_fb_two_state,
capability_encoder,
capability_fb_encoder,
capability_toggle,
capability_fader_14_bit,
capability_fb_fader_14_bit,
capability_touch,
capability_fb_mcu_display_upper,
capability_fb_mcu_display_lower,
capability_fb_mcu_vu_meter,
capability_fb_mcu_time_display,
capability_unknown,
))(input)
}
fn capability_press(input: &str) -> Res<Capability> {
map(util::capability_msg_opt_msg("Press"), |(press, release)| {
Capability::Press { press, release }
})(input)
}
fn capability_fb_two_state(input: &str) -> Res<Capability> {
map(util::capability_msg_msg("FB_TwoState"), |(on, off)| {
Capability::FbTwoState { on, off }
})(input)
}
fn capability_fb_encoder(input: &str) -> Res<Capability> {
map(util::capability_msg("FB_Encoder"), |max| {
Capability::FbEncoder { max }
})(input)
}
fn capability_toggle(input: &str) -> Res<Capability> {
map(util::capability_msg("Toggle"), |on| Capability::Toggle {
on,
})(input)
}
fn capability_fader_14_bit(input: &str) -> Res<Capability> {
map(util::capability_msg("Fader14Bit"), |max| {
Capability::Fader14Bit { max }
})(input)
}
fn capability_fb_fader_14_bit(input: &str) -> Res<Capability> {
map(util::capability_msg("FB_Fader14Bit"), |max| {
Capability::FbFader14Bit { max }
})(input)
}
fn capability_touch(input: &str) -> Res<Capability> {
map(util::capability_msg_msg("Touch"), |(on, off)| {
Capability::Touch {
touch: on,
release: off,
}
})(input)
}
fn capability_fb_mcu_display_upper(input: &str) -> Res<Capability> {
map(util::capability_index("FB_MCUDisplayUpper"), |index| {
Capability::FbMcuDisplayUpper { index }
})(input)
}
fn capability_fb_mcu_display_lower(input: &str) -> Res<Capability> {
map(util::capability_index("FB_MCUDisplayLower"), |index| {
Capability::FbMcuDisplayLower { index }
})(input)
}
fn capability_fb_mcu_vu_meter(input: &str) -> Res<Capability> {
map(util::capability_index("FB_MCUVUMeter"), |index| {
Capability::FbMcuVuMeter { index }
})(input)
}
fn capability_fb_mcu_time_display(input: &str) -> Res<Capability> {
map(util::capability_empty("FB_MCUTimeDisplay"), |_| {
Capability::FbMcuTimeDisplay
})(input)
}
fn capability_encoder(input: &str) -> Res<Capability> {
map(
tuple((
preceded(tuple((tag("Encoder"), space1)), short_midi_msg),
opt(preceded(space1, accelerations)),
)),
|(main, accelerations)| Capability::Encoder {
main,
accelerations,
},
)(input)
}
fn capability_unknown(input: &str) -> Res<Capability> {
map(
verify(not_line_ending, |s: &str| s != "WidgetEnd"),
|line: &str| Capability::Unknown(line.to_owned()),
)(input)
}
fn short_midi_msg(input: &str) -> Res<RawShortMessage> {
map_res(
tuple((hex_byte, space1, hex_byte, space1, hex_byte)),
|(b1, _, b2, _, b3)| {
RawShortMessage::from_bytes((
b1,
b2.try_into().map_err(|_| "data byte 1 too high")?,
b3.try_into().map_err(|_| "data byte 2 too high")?,
))
.map_err(|_| "invalid short message")
},
)(input)
}
fn accelerations(input: &str) -> Res<Accelerations> {
map(
delimited(
ws(char('[')),
tuple((
parameterized_acceleration('<'),
parameterized_acceleration('>'),
)),
ws(char(']')),
),
|(decrements, increments)| Accelerations {
increments,
decrements,
},
)(input)
}
fn parameterized_acceleration<'a>(
letter: char,
) -> impl FnMut(&'a str) -> IResult<&'a str, Acceleration> {
preceded(ws(char(letter)), acceleration)
}
fn acceleration(input: &str) -> Res<Acceleration> {
alt((acceleration_range, acceleration_sequence))(input)
}
fn acceleration_sequence(input: &str) -> Res<Acceleration> {
map(separated_list1(space1, hex_byte), |values| {
Acceleration::Sequence(values)
})(input)
}
fn acceleration_range(input: &str) -> Res<Acceleration> {
map(
separated_pair(hex_byte, char('-'), hex_byte),
|(min, max)| Acceleration::Range(min..=max),
)(input)
}
fn hex_byte(input: &str) -> Res<u8> {
map_res(take_while_m_n(2, 2, util::is_hex_digit), util::from_hex)(input)
}
fn space_with_at_least_one_line_ending(input: &str) -> Res<&str> {
verify(multispace0, |s: &str| s.contains(&['\r', '\n'][..]))(input)
}
/// Surrounded by optional whitespace (no line endings).
fn ws<'a, O, E>(p: impl Parser<&'a str, O, E>) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
where
E: ParseError<&'a str>,
{
delimited(space0, p, space0)
}
mod util {
use super::*;
use nom::character::complete::digit1;
use nom::combinator::value;
pub fn is_hex_digit(c: char) -> bool {
c.is_ascii_hexdigit()
}
pub fn from_hex(input: &str) -> Result<u8, std::num::ParseIntError> {
u8::from_str_radix(input, 16)
}
pub fn capability_msg_opt_msg<'a>(
name: &'static str,
) -> impl FnMut(&'a str) -> Res<'a, (RawShortMessage, Option<RawShortMessage>)> {
preceded(
tag(name),
tuple((
preceded(space1, short_midi_msg),
opt(preceded(space1, short_midi_msg)),
)),
)
}
pub fn capability_msg_msg<'a>(
name: &'static str,
) -> impl FnMut(&'a str) -> Res<'a, (RawShortMessage, RawShortMessage)> {
preceded(
tag(name),
tuple((
preceded(space1, short_midi_msg),
preceded(space1, short_midi_msg),
)),
)
}
pub fn capability_index<'a>(name: &'static str) -> impl FnMut(&'a str) -> Res<'a, u8> {
map_res(preceded(tuple((tag(name), space1)), digit1), |s: &str| {
s.parse::<u8>()
})
}
pub fn capability_empty<'a>(name: &'static str) -> impl FnMut(&'a str) -> Res<'a, ()> {
value((), tag(name))
}
pub fn capability_msg<'a>(
name: &'static str,
) -> impl FnMut(&'a str) -> Res<'a, RawShortMessage> {
preceded(tag(name), preceded(space1, short_midi_msg))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{Acceleration, Widget};
use helgoboss_midi::test_util::u7;
use helgoboss_midi::ShortMessageFactory;
#[test]
fn parse_widgets() {
let mst_content = include_str!("test_data/test.mst");
let (_, widgets) = widgets(mst_content).unwrap();
assert_eq!(widgets.len(), 146);
for w in widgets {
for c in w.capabilities {
assert!(!c.is_unknown());
}
}
}
#[test]
fn parse_widget() {
assert_eq!(
widget(
"\
Widget RecordArm1
Press 90 00 7f 90 00 00
FB_TwoState 90 00 7f 90 00 00
Weird eu 898 dqwun wd08 . ---
WidgetEnd"
),
Ok((
"",
Widget {
name: "RecordArm1".to_owned(),
capabilities: vec![
Capability::Press {
press: short(0x90, 0x00, 0x7f),
release: Some(short(0x90, 0x00, 0x00)),
},
Capability::FbTwoState {
on: short(0x90, 0x00, 0x7f),
off: short(0x90, 0x00, 0x00),
},
Capability::Unknown("Weird eu 898 dqwun wd08 . ---".to_owned())
]
}
))
);
}
#[test]
fn parse_widget_ugly_formatting() {
assert_eq!(
widget(
"\
Widget RecordArm1
Press 90 00 7f 90 00 00
FB_TwoState 90 00 7f 90 00 00
Weird eu 898 dqwun wd08 . ---
WidgetEnd"
),
Ok((
"",
Widget {
name: "RecordArm1".to_owned(),
capabilities: vec![
Capability::Press {
press: short(0x90, 0x00, 0x7f),
release: Some(short(0x90, 0x00, 0x00)),
},
Capability::FbTwoState {
on: short(0x90, 0x00, 0x7f),
off: short(0x90, 0x00, 0x00),
},
Capability::Unknown("Weird eu 898 dqwun wd08 . ---".to_owned())
]
}
))
);
}
#[test]
fn parse_press_capability_without_release() {
assert_eq!(
capability("Press 90 28 7f"),
Ok((
"",
Capability::Press {
press: short(0x90, 0x28, 0x7f),
release: None,
}
))
);
}
#[test]
fn parse_press_capability_with_release() {
assert_eq!(
capability("Press 90 28 7f 90 28 00"),
Ok((
"",
Capability::Press {
press: short(0x90, 0x28, 0x7f),
release: Some(short(0x90, 0x28, 0x00)),
}
))
);
}
#[test]
fn parse_fb_two_state_capability() {
assert_eq!(
capability("FB_TwoState 90 00 7f 90 00 00"),
Ok((
"",
Capability::FbTwoState {
on: short(0x90, 0x00, 0x7f),
off: short(0x90, 0x00, 0x00),
}
))
);
}
#[test]
fn parse_encoder_capability_with_range() {
assert_eq!(
capability("Encoder b0 10 7f [ < 41-48 > 01-08 ]"),
Ok((
"",
Capability::Encoder {
main: short(0xb0, 0x10, 0x7f),
accelerations: Some(Accelerations {
decrements: Acceleration::Range(0x41..=0x48),
increments: Acceleration::Range(0x01..=0x08)
})
}
))
);
}
#[test]
fn parse_short_midi_msg() {
assert_eq!(
short_midi_msg("90 28 7f"),
Ok(("", short(0x90, 0x28, 0x7f)))
);
}
#[test]
fn single_hex_byte() {
assert_eq!(hex_byte("90"), Ok(("", 0x90)));
}
fn short(status_byte: u8, data_byte_1: u8, data_byte_2: u8) -> RawShortMessage {
RawShortMessage::from_bytes((status_byte, u7(data_byte_1), u7(data_byte_2))).unwrap()
}
}
+74
View File
@@ -0,0 +1,74 @@
use derive_more::Display;
use helgoboss_midi::RawShortMessage;
use std::ops::RangeInclusive;
#[derive(Eq, PartialEq, Debug)]
pub struct Widget {
pub name: String,
pub capabilities: Vec<Capability>,
}
#[derive(Eq, PartialEq, Debug, Display)]
pub enum Capability {
#[display(fmt = "Press")]
Press {
press: RawShortMessage,
release: Option<RawShortMessage>,
},
#[display(fmt = "FB_TwoState")]
FbTwoState {
on: RawShortMessage,
off: RawShortMessage,
},
#[display(fmt = "Encoder")]
Encoder {
main: RawShortMessage,
accelerations: Option<Accelerations>,
},
#[display(fmt = "FB_Encoder")]
FbEncoder { max: RawShortMessage },
#[display(fmt = "Toggle")]
Toggle { on: RawShortMessage },
#[display(fmt = "Fader14Bit")]
Fader14Bit { max: RawShortMessage },
#[display(fmt = "FB_Fader14Bit")]
FbFader14Bit { max: RawShortMessage },
#[display(fmt = "Touch")]
Touch {
touch: RawShortMessage,
release: RawShortMessage,
},
#[display(fmt = "FB_MCUDisplayLower")]
FbMcuDisplayLower { index: u8 },
#[display(fmt = "FB_MCUDisplayUpper")]
FbMcuDisplayUpper { index: u8 },
#[display(fmt = "FB_MCUTimeDisplay")]
FbMcuTimeDisplay,
#[display(fmt = "FB_MCUVUMeter")]
FbMcuVuMeter { index: u8 },
#[display(fmt = "{_0}")]
Unknown(String),
}
impl Capability {
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown(_))
}
pub fn is_virtual_button(&self) -> bool {
use Capability as C;
matches!(self, C::Press { .. } | C::Toggle { .. } | C::Touch { .. })
}
}
#[derive(Eq, PartialEq, Debug)]
pub struct Accelerations {
pub decrements: Acceleration,
pub increments: Acceleration,
}
#[derive(Eq, PartialEq, Debug)]
pub enum Acceleration {
Sequence(Vec<u8>),
Range(RangeInclusive<u8>),
}
@@ -0,0 +1,711 @@
Widget RecordArm1
Press 90 00 7f 90 00 00
FB_TwoState 90 00 7f 90 00 00
WidgetEnd
Widget RecordArm2
Press 90 01 7f 90 01 00
FB_TwoState 90 01 7f 90 01 00
WidgetEnd
Widget RecordArm3
Press 90 02 7f 90 02 00
FB_TwoState 90 02 7f 90 02 00
WidgetEnd
Widget RecordArm4
Press 90 03 7f 90 03 00
FB_TwoState 90 03 7f 90 03 00
WidgetEnd
Widget RecordArm5
Press 90 04 7f 90 04 00
FB_TwoState 90 04 7f 90 04 00
WidgetEnd
Widget RecordArm6
Press 90 05 7f 90 05 00
FB_TwoState 90 05 7f 90 05 00
WidgetEnd
Widget RecordArm7
Press 90 06 7f 90 06 00
FB_TwoState 90 06 7f 90 06 00
WidgetEnd
Widget RecordArm8
Press 90 07 7f 90 07 00
FB_TwoState 90 07 7f 90 07 00
WidgetEnd
Widget Solo1
Press 90 08 7f 90 08 00
FB_TwoState 90 08 7f 90 08 00
WidgetEnd
Widget Solo2
Press 90 09 7f 90 09 00
FB_TwoState 90 09 7f 90 09 00
WidgetEnd
Widget Solo3
Press 90 0a 7f 90 0a 00
FB_TwoState 90 0a 7f 90 0a 00
WidgetEnd
Widget Solo4
Press 90 0b 7f 90 0b 00
FB_TwoState 90 0b 7f 90 0b 00
WidgetEnd
Widget Solo5
Press 90 0c 7f 90 0c 00
FB_TwoState 90 0c 7f 90 0c 00
WidgetEnd
Widget Solo6
Press 90 0d 7f 90 0d 00
FB_TwoState 90 0d 7f 90 0d 00
WidgetEnd
Widget Solo7
Press 90 0e 7f 90 0e 00
FB_TwoState 90 0e 7f 90 0e 00
WidgetEnd
Widget Solo8
Press 90 0f 7f 90 0f 00
FB_TwoState 90 0f 7f 90 0f 00
WidgetEnd
Widget Mute1
Press 90 10 7f 90 10 00
FB_TwoState 90 10 7f 90 10 00
WidgetEnd
Widget Mute2
Press 90 11 7f 90 11 00
FB_TwoState 90 11 7f 90 11 00
WidgetEnd
Widget Mute3
Press 90 12 7f 90 12 00
FB_TwoState 90 12 7f 90 12 00
WidgetEnd
Widget Mute4
Press 90 13 7f 90 13 00
FB_TwoState 90 13 7f 90 13 00
WidgetEnd
Widget Mute5
Press 90 14 7f 90 14 00
FB_TwoState 90 14 7f 90 14 00
WidgetEnd
Widget Mute6
Press 90 15 7f 90 15 00
FB_TwoState 90 15 7f 90 15 00
WidgetEnd
Widget Mute7
Press 90 16 7f 90 16 00
FB_TwoState 90 16 7f 90 16 00
WidgetEnd
Widget Mute8
Press 90 17 7f 90 17 00
FB_TwoState 90 17 7f 90 17 00
WidgetEnd
Widget Select1
Press 90 18 7f 90 18 00
FB_TwoState 90 18 7f 90 18 00
WidgetEnd
Widget Select2
Press 90 19 7f 90 19 00
FB_TwoState 90 19 7f 90 19 00
WidgetEnd
Widget Select3
Press 90 1a 7f 90 1a 00
FB_TwoState 90 1a 7f 90 1a 00
WidgetEnd
Widget Select4
Press 90 1b 7f 90 1b 00
FB_TwoState 90 1b 7f 90 1b 00
WidgetEnd
Widget Select5
Press 90 1c 7f 90 1c 00
FB_TwoState 90 1c 7f 90 1c 00
WidgetEnd
Widget Select6
Press 90 1d 7f 90 1d 00
FB_TwoState 90 1d 7f 90 1d 00
WidgetEnd
Widget Select7
Press 90 1e 7f 90 1e 00
FB_TwoState 90 1e 7f 90 1e 00
WidgetEnd
Widget Select8
Press 90 1f 7f 90 1f 00
FB_TwoState 90 1f 7f 90 1f 00
WidgetEnd
Widget RotaryPush1
Press 90 20 7f 90 20 00
WidgetEnd
Widget RotaryPush2
Press 90 21 7f 90 21 00
WidgetEnd
Widget RotaryPush3
Press 90 22 7f 90 22 00
WidgetEnd
Widget RotaryPush4
Press 90 23 7f 90 23 00
WidgetEnd
Widget RotaryPush5
Press 90 24 7f 90 24 00
WidgetEnd
Widget RotaryPush6
Press 90 25 7f 90 25 00
WidgetEnd
Widget RotaryPush7
Press 90 26 7f 90 26 00
WidgetEnd
Widget RotaryPush8
Press 90 27 7f 90 27 00
WidgetEnd
Widget Rotary1
Encoder b0 10 7f [ < 41-48 > 01-08 ]
FB_Encoder b0 10 7f
Toggle 90 20 7f
WidgetEnd
Widget Rotary2
Encoder b0 11 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 11 7f
Toggle 90 21 7f
WidgetEnd
Widget Rotary3
Encoder b0 12 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 12 7f
Toggle 90 22 7f
WidgetEnd
Widget Rotary4
Encoder b0 13 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 13 7f
Toggle 90 23 7f
WidgetEnd
Widget Rotary5
Encoder b0 14 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 14 7f
Toggle 90 24 7f
WidgetEnd
Widget Rotary6
Encoder b0 15 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 15 7f
Toggle 90 25 7f
WidgetEnd
Widget Rotary7
Encoder b0 16 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 16 7f
Toggle 90 26 7f
WidgetEnd
Widget Rotary8
Encoder b0 17 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 17 7f
Toggle 90 27 7f
WidgetEnd
Widget Track
Press 90 28 7f 90 28 00
FB_TwoState 90 28 7f 90 28 00
WidgetEnd
Widget Send
Press 90 29 7f 90 29 00
FB_TwoState 90 29 7f 90 29 00
WidgetEnd
Widget Pan
Press 90 2a 7f 90 2a 00
FB_TwoState 90 2a 7f 90 2a 00
WidgetEnd
Widget Plugin
Press 90 2b 7f 90 2b 00
FB_TwoState 90 2b 7f 90 2b 00
WidgetEnd
Widget EQ
Press 90 2c 7f 90 2c 00
FB_TwoState 90 2c 7f 90 2c 00
WidgetEnd
Widget Instrument
Press 90 2d 7f 90 2d 00
FB_TwoState 90 2d 7f 90 2d 00
WidgetEnd
Widget BankLeft
Press 90 2e 7f 90 2e 00
FB_TwoState 90 2e 7f 90 2e 00
WidgetEnd
Widget BankRight
Press 90 2f 7f 90 2f 00
FB_TwoState 90 2f 7f 90 2f 00
WidgetEnd
Widget ChannelLeft
Press 90 30 7f 90 30 00
FB_TwoState 90 30 7f 90 30 00
WidgetEnd
Widget ChannelRight
Press 90 31 7f 90 31 00
FB_TwoState 90 31 7f 90 31 00
WidgetEnd
Widget Flip
Press 90 32 7f 90 32 00
FB_TwoState 90 32 7f 90 32 00
WidgetEnd
Widget GlobalView
Press 90 33 7f 90 33 00
FB_TwoState 90 33 7f 90 33 00
WidgetEnd
Widget BPM-Time
Press 90 35 7f 90 35 00
FB_TwoState 90 35 7f 90 35 00
WidgetEnd
Widget nameValue
Press 90 34 7f
WidgetEnd
Widget F1
Press 90 36 7f 90 36 00
FB_TwoState 90 36 7f 90 36 00
WidgetEnd
Widget F2
Press 90 37 7f 90 37 00
FB_TwoState 90 37 7f 90 37 00
WidgetEnd
Widget F3
Press 90 38 7f 90 38 00
FB_TwoState 90 38 7f 90 38 00
WidgetEnd
Widget F4
Press 90 39 7f 90 39 00
FB_TwoState 90 39 7f 90 39 00
WidgetEnd
Widget F5
Press 90 3a 7f 90 3a 00
FB_TwoState 90 3a 7f 90 3a 00
WidgetEnd
Widget F6
Press 90 3b 7f 90 3b 00
FB_TwoState 90 3b 7f 90 3b 00
WidgetEnd
Widget F7
Press 90 3c 7f 90 3c 00
FB_TwoState 90 3c 7f 90 3c 00
WidgetEnd
Widget F8
Press 90 3d 7f 90 3d 00
FB_TwoState 90 3d 7f 90 3d 00
WidgetEnd
Widget MidiTracks
Press 90 3e 7f 90 3e 00
FB_TwoState 90 3e 7f 90 3e 00
WidgetEnd
Widget Inputs
Press 90 3f 7f 90 3f 00
FB_TwoState 90 3f 7f 90 3f 00
WidgetEnd
Widget AudioTracks
Press 90 40 7f 90 40 00
FB_TwoState 90 40 7f 90 40 00
WidgetEnd
Widget AudioInstrument
Press 90 41 7f 90 41 00
FB_TwoState 90 41 7f 90 41 00
WidgetEnd
Widget Aux
Press 90 42 7f 90 42 00
FB_TwoState 90 42 7f 90 42 00
WidgetEnd
Widget Busses
Press 90 43 7f 90 43 00
FB_TwoState 90 43 7f 90 43 00
WidgetEnd
Widget Outputs
Press 90 44 7f 90 44 00
FB_TwoState 90 44 7f 90 44 00
WidgetEnd
Widget User
Press 90 45 7f 90 45 00
FB_TwoState 90 45 7f 90 45 00
WidgetEnd
Widget Shift
Press 90 46 7f 90 46 00
FB_TwoState 90 46 7f 90 46 00
WidgetEnd
Widget Option
Press 90 47 7f 90 47 00
FB_TwoState 90 47 7f 90 47 00
WidgetEnd
Widget Control
Press 90 48 7f 90 48 00
FB_TwoState 90 48 7f 90 48 00
WidgetEnd
Widget Alt
Press 90 49 7f 90 49 00
FB_TwoState 90 49 7f 90 49 00
WidgetEnd
Widget Read
Press 90 4a 7f 90 4a 00
FB_TwoState 90 4a 7f 90 4a 00
WidgetEnd
Widget Write
Press 90 4b 7f 90 4b 00
FB_TwoState 90 4b 7f 90 4b 00
WidgetEnd
Widget Trim
Press 90 4c 7f 90 4c 00
FB_TwoState 90 4c 7f 90 4c 00
WidgetEnd
Widget Touch
Press 90 4d 7f 90 4d 00
FB_TwoState 90 4d 7f 90 4d 00
WidgetEnd
Widget Latch
Press 90 4e 7f 90 4e 00
FB_TwoState 90 4e 7f 90 4e 00
WidgetEnd
Widget Group
Press 90 4f 7f 90 4f 00
FB_TwoState 90 4f 7f 90 4f 00
WidgetEnd
Widget Save
Press 90 50 7f 90 50 00
FB_TwoState 90 50 7f 90 50 00
WidgetEnd
Widget Undo
Press 90 51 7f 90 51 00
FB_TwoState 90 51 7f 90 51 00
WidgetEnd
Widget Cancel
Press 90 52 7f 90 52 00
FB_TwoState 90 52 7f 90 52 00
WidgetEnd
Widget Enter
Press 90 53 7f 90 53 00
FB_TwoState 90 53 7f 90 53 00
WidgetEnd
Widget Marker
Press 90 54 7f 90 54 00
FB_TwoState 90 54 7f 90 54 00
WidgetEnd
Widget Nudge
Press 90 55 7f 90 55 00
FB_TwoState 90 55 7f 90 55 00
WidgetEnd
Widget Cycle
Press 90 56 7f 90 56 00
FB_TwoState 90 56 7f 90 56 00
WidgetEnd
Widget Drop
Press 90 57 7f 90 57 00
FB_TwoState 90 57 7f 90 57 00
WidgetEnd
Widget Replace
Press 90 58 7f 90 58 00
FB_TwoState 90 58 7f 90 58 00
WidgetEnd
Widget Click
Press 90 59 7f 90 59 00
FB_TwoState 90 59 7f 90 59 00
WidgetEnd
Widget Solo
Press 90 5a 7f 90 5a 00
FB_TwoState 90 5a 7f 90 5a 00
WidgetEnd
Widget Rewind
Press 90 5b 7f 90 5b 00
FB_TwoState 90 5b 7f 90 5b 00
WidgetEnd
Widget FastForward
Press 90 5c 7f 90 5c 00
FB_TwoState 90 5c 7f 90 5c 00
WidgetEnd
Widget Stop
Press 90 5d 7f 90 5d 00
FB_TwoState 90 5d 7f 90 5d 00
WidgetEnd
Widget Play
Press 90 5e 7f 90 5e 00
FB_TwoState 90 5e 7f 90 5e 00
WidgetEnd
Widget Record
Press 90 5f 7f 90 5f 00
FB_TwoState 90 5f 7f 90 5f 00
WidgetEnd
Widget Up
Press 90 60 7f 90 60 00
FB_TwoState 90 60 7f 90 60 00
WidgetEnd
Widget Down
Press 90 61 7f 90 61 00
FB_TwoState 90 61 7f 90 61 00
WidgetEnd
Widget Left
Press 90 62 7f 90 62 00
FB_TwoState 90 62 7f 90 62 00
WidgetEnd
Widget Right
Press 90 63 7f 90 63 00
FB_TwoState 90 63 7f 90 63 00
WidgetEnd
Widget Zoom
Press 90 64 7f 90 64 00
FB_TwoState 90 64 7f 90 64 00
WidgetEnd
Widget Scrub
Press 90 65 7f 90 65 00
FB_TwoState 90 65 7f 90 65 00
WidgetEnd
Widget Fader1
Fader14Bit e0 7f 7f
FB_Fader14Bit e0 7f 7f
Touch 90 68 7f 90 68 00
WidgetEnd
Widget Fader2
Fader14Bit e1 7f 7f
FB_Fader14Bit e1 7f 7f
Touch 90 69 7f 90 69 00
WidgetEnd
Widget Fader3
Fader14Bit e2 7f 7f
FB_Fader14Bit e2 7f 7f
Touch 90 6a 7f 90 6a 00
WidgetEnd
Widget Fader4
Fader14Bit e3 7f 7f
FB_Fader14Bit e3 7f 7f
Touch 90 6b 7f 90 6b 00
WidgetEnd
Widget Fader5
Fader14Bit e4 7f 7f
FB_Fader14Bit e4 7f 7f
Touch 90 6c 7f 90 6c 00
WidgetEnd
Widget Fader6
Fader14Bit e5 7f 7f
FB_Fader14Bit e5 7f 7f
Touch 90 6d 7f 90 6d 00
WidgetEnd
Widget Fader7
Fader14Bit e6 7f 7f
FB_Fader14Bit e6 7f 7f
Touch 90 6e 7f 90 6e 00
WidgetEnd
Widget Fader8
Fader14Bit e7 7f 7f
FB_Fader14Bit e7 7f 7f
Touch 90 6f 7f 90 6f 00
WidgetEnd
Widget MasterFader
Fader14Bit e8 7f 7f
FB_Fader14Bit e8 7f 7f
Touch e8 7f 7f e8 7f 00
WidgetEnd
Widget DisplayLower1
FB_MCUDisplayLower 0
WidgetEnd
Widget DisplayLower2
FB_MCUDisplayLower 1
WidgetEnd
Widget DisplayLower3
FB_MCUDisplayLower 2
WidgetEnd
Widget DisplayLower4
FB_MCUDisplayLower 3
WidgetEnd
Widget DisplayLower5
FB_MCUDisplayLower 4
WidgetEnd
Widget DisplayLower6
FB_MCUDisplayLower 5
WidgetEnd
Widget DisplayLower7
FB_MCUDisplayLower 6
WidgetEnd
Widget DisplayLower8
FB_MCUDisplayLower 7
WidgetEnd
Widget DisplayUpper1
FB_MCUDisplayUpper 0
WidgetEnd
Widget DisplayUpper2
FB_MCUDisplayUpper 1
WidgetEnd
Widget DisplayUpper3
FB_MCUDisplayUpper 2
WidgetEnd
Widget DisplayUpper4
FB_MCUDisplayUpper 3
WidgetEnd
Widget DisplayUpper5
FB_MCUDisplayUpper 4
WidgetEnd
Widget DisplayUpper6
FB_MCUDisplayUpper 5
WidgetEnd
Widget DisplayUpper7
FB_MCUDisplayUpper 6
WidgetEnd
Widget DisplayUpper8
FB_MCUDisplayUpper 7
WidgetEnd
Widget TimeDisplay
FB_MCUTimeDisplay
WidgetEnd
Widget VUMeter1
FB_MCUVUMeter 0
WidgetEnd
Widget VUMeter2
FB_MCUVUMeter 1
WidgetEnd
Widget VUMeter3
FB_MCUVUMeter 2
WidgetEnd
Widget VUMeter4
FB_MCUVUMeter 3
WidgetEnd
Widget VUMeter5
FB_MCUVUMeter 4
WidgetEnd
Widget VUMeter6
FB_MCUVUMeter 5
WidgetEnd
Widget VUMeter7
FB_MCUVUMeter 6
WidgetEnd
Widget VUMeter8
FB_MCUVUMeter 7
WidgetEnd
Widget JogWheelRotaryCW1
Press b0 3c 01
WidgetEnd
Widget JogWheelRotaryCCW1
Press b0 3c 41
WidgetEnd