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:
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user