Reorganize top-level directories with clearer naming convention
app -> app-desktop-macos, presets -> app-presets, server -> remote-server, daw-config-reaper -> osc-config-daw, plugin-reaper-realearn -> plugin-reaper-relearn. Updated run.py and presets.py path references accordingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
use crate::RgbColor;
|
||||
|
||||
// Initially taken from https://github.com/jamesmunns/launch-rs/blob/master/lib/src/color.rs
|
||||
pub fn find_closest_color_in_palette(color: RgbColor, palette: &[RgbColor]) -> u8 {
|
||||
let (red, green, blue) = (color.r(), color.g(), color.b());
|
||||
let mut ifurthest = 0usize;
|
||||
let mut furthest = 3 * 255_i32.pow(2) + 1;
|
||||
for (i, c) in palette.iter().enumerate() {
|
||||
if red == c.r() && green == c.g() && blue == c.b() {
|
||||
// Exact match
|
||||
return i as u8;
|
||||
}
|
||||
let distance = (red as i32 - c.r() as i32).pow(2)
|
||||
+ (green as i32 - c.g() as i32).pow(2)
|
||||
+ (blue as i32 - c.b() as i32).pow(2);
|
||||
if distance < furthest {
|
||||
furthest = distance;
|
||||
ifurthest = i;
|
||||
}
|
||||
}
|
||||
ifurthest as u8
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
//! Initially taken from https://github.com/jamesmunns/launch-rs/blob/master/lib/src/color.rs
|
||||
use crate::RgbColor;
|
||||
|
||||
/// http://launchpaddr.com/mk2palette/
|
||||
pub const COLOR_PALETTE: [RgbColor; 128] = [
|
||||
// 0..64
|
||||
RgbColor::new(0x00, 0x00, 0x00),
|
||||
RgbColor::new(0x1c, 0x1c, 0x1c),
|
||||
RgbColor::new(0x7c, 0x7c, 0x7c),
|
||||
RgbColor::new(0xfc, 0xfc, 0xfc),
|
||||
RgbColor::new(0xff, 0x4e, 0x48),
|
||||
RgbColor::new(0xfe, 0x0a, 0x00),
|
||||
RgbColor::new(0x5a, 0x00, 0x00),
|
||||
RgbColor::new(0x18, 0x00, 0x02),
|
||||
RgbColor::new(0xff, 0xbc, 0x63),
|
||||
RgbColor::new(0xff, 0x57, 0x00),
|
||||
RgbColor::new(0x5a, 0x1d, 0x00),
|
||||
RgbColor::new(0x24, 0x18, 0x02),
|
||||
RgbColor::new(0xfd, 0xfd, 0x21),
|
||||
RgbColor::new(0xfd, 0xfd, 0x00),
|
||||
RgbColor::new(0x58, 0x58, 0x00),
|
||||
RgbColor::new(0x18, 0x18, 0x00),
|
||||
RgbColor::new(0x81, 0xfd, 0x2b),
|
||||
RgbColor::new(0x40, 0xfd, 0x01),
|
||||
RgbColor::new(0x16, 0x58, 0x00),
|
||||
RgbColor::new(0x13, 0x28, 0x01),
|
||||
RgbColor::new(0x35, 0xfd, 0x2b),
|
||||
RgbColor::new(0x00, 0xfe, 0x00),
|
||||
RgbColor::new(0x00, 0x58, 0x01),
|
||||
RgbColor::new(0x00, 0x18, 0x00),
|
||||
RgbColor::new(0x35, 0xfc, 0x47),
|
||||
RgbColor::new(0x00, 0xfe, 0x00),
|
||||
RgbColor::new(0x00, 0x58, 0x01),
|
||||
RgbColor::new(0x00, 0x18, 0x00),
|
||||
RgbColor::new(0x32, 0xfd, 0x7f),
|
||||
RgbColor::new(0x00, 0xfd, 0x3a),
|
||||
RgbColor::new(0x01, 0x58, 0x14),
|
||||
RgbColor::new(0x00, 0x1c, 0x0e),
|
||||
RgbColor::new(0x2f, 0xfc, 0xb1),
|
||||
RgbColor::new(0x00, 0xfb, 0x91),
|
||||
RgbColor::new(0x01, 0x57, 0x32),
|
||||
RgbColor::new(0x01, 0x18, 0x10),
|
||||
RgbColor::new(0x39, 0xbe, 0xff),
|
||||
RgbColor::new(0x00, 0xa7, 0xff),
|
||||
RgbColor::new(0x01, 0x40, 0x51),
|
||||
RgbColor::new(0x00, 0x10, 0x18),
|
||||
RgbColor::new(0x41, 0x86, 0xff),
|
||||
RgbColor::new(0x00, 0x50, 0xff),
|
||||
RgbColor::new(0x01, 0x1a, 0x5a),
|
||||
RgbColor::new(0x01, 0x06, 0x19),
|
||||
RgbColor::new(0x47, 0x47, 0xff),
|
||||
RgbColor::new(0x00, 0x00, 0xfe),
|
||||
RgbColor::new(0x00, 0x00, 0x5a),
|
||||
RgbColor::new(0x00, 0x00, 0x18),
|
||||
RgbColor::new(0x83, 0x47, 0xff),
|
||||
RgbColor::new(0x50, 0x00, 0xff),
|
||||
RgbColor::new(0x16, 0x00, 0x67),
|
||||
RgbColor::new(0x0a, 0x00, 0x32),
|
||||
RgbColor::new(0xff, 0x48, 0xfe),
|
||||
RgbColor::new(0xff, 0x00, 0xfe),
|
||||
RgbColor::new(0x5a, 0x00, 0x5a),
|
||||
RgbColor::new(0x18, 0x00, 0x18),
|
||||
RgbColor::new(0xfb, 0x4e, 0x83),
|
||||
RgbColor::new(0xff, 0x07, 0x53),
|
||||
RgbColor::new(0x5a, 0x02, 0x1b),
|
||||
RgbColor::new(0x21, 0x01, 0x10),
|
||||
RgbColor::new(0xff, 0x19, 0x01),
|
||||
RgbColor::new(0x9a, 0x35, 0x00),
|
||||
RgbColor::new(0x7a, 0x51, 0x01),
|
||||
RgbColor::new(0x3e, 0x65, 0x00),
|
||||
// 64..128
|
||||
RgbColor::new(0x01, 0x38, 0x00),
|
||||
RgbColor::new(0x00, 0x54, 0x32),
|
||||
RgbColor::new(0x00, 0x53, 0x7f),
|
||||
RgbColor::new(0x00, 0x00, 0xfe),
|
||||
RgbColor::new(0x01, 0x44, 0x4d),
|
||||
RgbColor::new(0x1a, 0x00, 0xd1),
|
||||
RgbColor::new(0x7c, 0x7c, 0x7c),
|
||||
RgbColor::new(0x20, 0x20, 0x20),
|
||||
RgbColor::new(0xff, 0x0a, 0x00),
|
||||
RgbColor::new(0xba, 0xfd, 0x00),
|
||||
RgbColor::new(0xac, 0xec, 0x00),
|
||||
RgbColor::new(0x56, 0xfd, 0x00),
|
||||
RgbColor::new(0x00, 0x88, 0x00),
|
||||
RgbColor::new(0x01, 0xfc, 0x7b),
|
||||
RgbColor::new(0x00, 0xa7, 0xff),
|
||||
RgbColor::new(0x02, 0x1a, 0xff),
|
||||
RgbColor::new(0x35, 0x00, 0xff),
|
||||
RgbColor::new(0x78, 0x00, 0xff),
|
||||
RgbColor::new(0xb4, 0x17, 0x7e),
|
||||
RgbColor::new(0x41, 0x20, 0x00),
|
||||
RgbColor::new(0xff, 0x4a, 0x01),
|
||||
RgbColor::new(0x82, 0xe1, 0x00),
|
||||
RgbColor::new(0x66, 0xfd, 0x00),
|
||||
RgbColor::new(0x00, 0xfe, 0x00),
|
||||
RgbColor::new(0x00, 0xfe, 0x00),
|
||||
RgbColor::new(0x45, 0xfd, 0x61),
|
||||
RgbColor::new(0x01, 0xfb, 0xcb),
|
||||
RgbColor::new(0x50, 0x86, 0xff),
|
||||
RgbColor::new(0x27, 0x4d, 0xc8),
|
||||
RgbColor::new(0x84, 0x7a, 0xed),
|
||||
RgbColor::new(0xd3, 0x0c, 0xff),
|
||||
RgbColor::new(0xff, 0x06, 0x5a),
|
||||
RgbColor::new(0xff, 0x7d, 0x01),
|
||||
RgbColor::new(0xb8, 0xb1, 0x00),
|
||||
RgbColor::new(0x8a, 0xfd, 0x00),
|
||||
RgbColor::new(0x81, 0x5d, 0x00),
|
||||
RgbColor::new(0x3a, 0x28, 0x02),
|
||||
RgbColor::new(0x0d, 0x4c, 0x05),
|
||||
RgbColor::new(0x00, 0x50, 0x37),
|
||||
RgbColor::new(0x13, 0x14, 0x29),
|
||||
RgbColor::new(0x10, 0x1f, 0x5a),
|
||||
RgbColor::new(0x6a, 0x3c, 0x18),
|
||||
RgbColor::new(0xac, 0x04, 0x01),
|
||||
RgbColor::new(0xe1, 0x51, 0x36),
|
||||
RgbColor::new(0xdc, 0x69, 0x00),
|
||||
RgbColor::new(0xfe, 0xe1, 0x00),
|
||||
RgbColor::new(0x99, 0xe1, 0x01),
|
||||
RgbColor::new(0x60, 0xb5, 0x00),
|
||||
RgbColor::new(0x1b, 0x1c, 0x31),
|
||||
RgbColor::new(0xdc, 0xfd, 0x54),
|
||||
RgbColor::new(0x76, 0xfb, 0xb9),
|
||||
RgbColor::new(0x96, 0x98, 0xff),
|
||||
RgbColor::new(0x8b, 0x62, 0xff),
|
||||
RgbColor::new(0x40, 0x40, 0x40),
|
||||
RgbColor::new(0x74, 0x74, 0x74),
|
||||
RgbColor::new(0xde, 0xfc, 0xfc),
|
||||
RgbColor::new(0xa2, 0x04, 0x01),
|
||||
RgbColor::new(0x34, 0x01, 0x00),
|
||||
RgbColor::new(0x00, 0xd2, 0x01),
|
||||
RgbColor::new(0x00, 0x41, 0x01),
|
||||
RgbColor::new(0xb8, 0xb1, 0x00),
|
||||
RgbColor::new(0x3c, 0x30, 0x00),
|
||||
RgbColor::new(0xb4, 0x5d, 0x00),
|
||||
RgbColor::new(0x4c, 0x13, 0x00),
|
||||
];
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod launchpad;
|
||||
pub mod x_touch;
|
||||
@@ -0,0 +1,96 @@
|
||||
use crate::source::color_util::find_closest_color_in_palette;
|
||||
use crate::{MackieLcdScope, RgbColor};
|
||||
use base::hash_util::NonCryptoHashMap;
|
||||
|
||||
mod colors {
|
||||
use crate::RgbColor;
|
||||
|
||||
pub const BLANK: RgbColor = RgbColor::new(0, 0, 0);
|
||||
pub const RED: RgbColor = RgbColor::new(255, 0, 0);
|
||||
pub const GREEN: RgbColor = RgbColor::new(0, 255, 0);
|
||||
pub const YELLOW: RgbColor = RgbColor::new(255, 255, 0);
|
||||
pub const BLUE: RgbColor = RgbColor::new(0, 0, 255);
|
||||
pub const PURPLE: RgbColor = RgbColor::new(128, 0, 128);
|
||||
pub const CYAN: RgbColor = RgbColor::new(0, 255, 255);
|
||||
pub const WHITE: RgbColor = RgbColor::new(255, 255, 255);
|
||||
}
|
||||
|
||||
use colors::*;
|
||||
|
||||
const COLOR_PALETTE: [RgbColor; 8] = [BLANK, RED, GREEN, YELLOW, BLUE, PURPLE, CYAN, WHITE];
|
||||
|
||||
/// Global state for a particular Behringer X-Touch device.
|
||||
///
|
||||
/// It's used when choosing the X-Touch Mackie display MIDI source in order to determine if a
|
||||
/// sys-ex message needs to be sent to change the display color, and if yes, which one. We need
|
||||
/// global state here because, unfortunately, the color can only be changed for all displays
|
||||
/// (channels) at once. However, ReaLearn's color feedback design allows for defining the color
|
||||
/// in a very fine-granular way - as part of the feedback value (its "style"), and thus resides
|
||||
/// within the scope of a mapping.
|
||||
///
|
||||
/// We need to make sure that when changing the color for one display, that the colors of the other
|
||||
/// displays remain unchanged. This is impossible without having access to the current state of the
|
||||
/// other displays because there's no sys-ex to change the color of just one display.
|
||||
///
|
||||
/// One alternative would have been to somehow restructure ReaLearn's feedback design so that
|
||||
/// we always transfer batches of texts and colors ... but that wouldn't go well with the
|
||||
/// concept where one mapping can change something very small and specific (which makes ReaLearn so
|
||||
/// flexible and composable).
|
||||
///
|
||||
/// Another alternative would have been to make the feedback source value something more
|
||||
/// abstract than concrete MIDI messages and then creating the concrete MIDI message at a later
|
||||
/// stage when all information is available (probably in the struct that has access to the global
|
||||
/// source context state).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct XTouchMackieLcdState {
|
||||
state_by_extender: NonCryptoHashMap<u8, XTouchMackieExtenderLcdState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct XTouchMackieExtenderLcdState {
|
||||
color_index_by_channel: [Option<u8>; MackieLcdScope::CHANNEL_COUNT as usize],
|
||||
}
|
||||
|
||||
const EMPTY_COLOR_INDEX_BY_CHANNEL: XTouchMackieExtenderLcdState = XTouchMackieExtenderLcdState {
|
||||
color_index_by_channel: [None; MackieLcdScope::CHANNEL_COUNT as usize],
|
||||
};
|
||||
|
||||
impl XTouchMackieLcdState {
|
||||
/// Returns `true` if something has changed for the given extender.
|
||||
///
|
||||
/// In that case, the sys-ex should be sent again.
|
||||
pub fn notify_color_requested(
|
||||
&mut self,
|
||||
extender_index: u8,
|
||||
channel: u8,
|
||||
color_index: Option<u8>,
|
||||
) -> bool {
|
||||
let extender_state = self.state_by_extender.entry(extender_index).or_default();
|
||||
let previous_color_index = extender_state.color_index_by_channel[channel as usize];
|
||||
extender_state.color_index_by_channel[channel as usize] = color_index;
|
||||
color_index != previous_color_index
|
||||
}
|
||||
|
||||
/// Returns the sys-ex bytes for setting the colors for the given extender.
|
||||
pub fn sysex(&self, extender_index: u8) -> impl Iterator<Item = u8> + '_ {
|
||||
let start = [0xF0, 0x00, 0x00, 0x66, 0x14 + extender_index, 0x72];
|
||||
let extender_state = self
|
||||
.state_by_extender
|
||||
.get(&extender_index)
|
||||
.unwrap_or(&EMPTY_COLOR_INDEX_BY_CHANNEL);
|
||||
let color_indexes = extender_state
|
||||
.color_index_by_channel
|
||||
.iter()
|
||||
.map(|color_index| color_index.unwrap_or(X_TOUCH_DEFAULT_COLOR_INDEX));
|
||||
start
|
||||
.into_iter()
|
||||
.chain(color_indexes)
|
||||
.chain(std::iter::once(0xF7))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_x_touch_color_index_for_color(color: RgbColor) -> u8 {
|
||||
find_closest_color_in_palette(color, &COLOR_PALETTE)
|
||||
}
|
||||
|
||||
const X_TOUCH_DEFAULT_COLOR_INDEX: u8 = 0;
|
||||
@@ -0,0 +1,41 @@
|
||||
use crate::{FeedbackValue, PropValue};
|
||||
use base::hash_util::NonCryptoHashSet;
|
||||
use std::borrow::Cow;
|
||||
use std::error::Error;
|
||||
|
||||
// The lifetime 'a is necessary in case we want to parameterize the lifetime
|
||||
// of the additional input dynamically. An alternative would have been to
|
||||
// require the additional input type to be static and take it by reference.
|
||||
// But that would be less generic.
|
||||
pub trait FeedbackScript<'a> {
|
||||
type AdditionalInput: Default;
|
||||
|
||||
fn feedback(
|
||||
&self,
|
||||
input: FeedbackScriptInput,
|
||||
additional_input: Self::AdditionalInput,
|
||||
) -> Result<FeedbackScriptOutput, Cow<'static, str>>;
|
||||
|
||||
fn used_props(&self) -> Result<NonCryptoHashSet<String>, Box<dyn Error>>;
|
||||
}
|
||||
|
||||
pub trait PropProvider {
|
||||
fn get_prop_value(&self, key: &str) -> Option<PropValue>;
|
||||
}
|
||||
|
||||
impl<F> PropProvider for F
|
||||
where
|
||||
F: Fn(&str) -> Option<PropValue>,
|
||||
{
|
||||
fn get_prop_value(&self, key: &str) -> Option<PropValue> {
|
||||
(self)(key)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FeedbackScriptInput<'a> {
|
||||
pub prop_provider: &'a dyn PropProvider,
|
||||
}
|
||||
|
||||
pub struct FeedbackScriptOutput {
|
||||
pub feedback_value: FeedbackValue<'static>,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
use crate::{FeedbackValue, MidiSourceAddress, RawMidiEvents};
|
||||
use std::borrow::Cow;
|
||||
|
||||
// The lifetime 'a is necessary in case we want to parameterize the lifetime
|
||||
// of the additional input dynamically. An alternative would have been to
|
||||
// require the additional input type to be static and take it by reference.
|
||||
// But that would be less generic.
|
||||
pub trait MidiSourceScript<'a> {
|
||||
type AdditionalInput: Default;
|
||||
|
||||
/// Returns raw MIDI bytes.
|
||||
fn execute(
|
||||
&self,
|
||||
input_value: FeedbackValue,
|
||||
additional_input: Self::AdditionalInput,
|
||||
) -> Result<MidiSourceScriptOutcome, Cow<'static, str>>;
|
||||
}
|
||||
|
||||
pub struct MidiSourceScriptOutcome {
|
||||
pub address: Option<MidiSourceAddress>,
|
||||
pub events: RawMidiEvents,
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
use crate::{DisplaySpecAddress, MidiSourceAddress, PatternByte, UnitValue};
|
||||
use helgoboss_midi::{
|
||||
Channel, ControlChange14BitMessage, DataEntryByteOrder, ParameterNumberMessage, ShortMessage,
|
||||
ShortMessageFactory, StructuredShortMessage,
|
||||
};
|
||||
use reaper_common_types::Bpm;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub type RawMidiEvents = Vec<RawMidiEvent>;
|
||||
|
||||
/// Values produced when asking for feedback from MIDI sources.
|
||||
///
|
||||
/// At the moment, we always produce a final value and maybe a non-final one in addition, so this
|
||||
/// isn't an enum.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct PreliminaryMidiSourceFeedbackValue<'a, M: ShortMessage> {
|
||||
/// A concrete MIDI message.
|
||||
pub final_value: MidiSourceValue<'a, M>,
|
||||
/// Request to set the color of one particular XTouch channel display.
|
||||
///
|
||||
/// The XTouch doesn't provide a way to set the color for one particular channel, only one to
|
||||
/// set the colors of all channels at once. That means we need to keep the current color of
|
||||
/// each channel around as state, "integrate" these requests after collecting them from the
|
||||
/// sources and then build the final sys-ex message.
|
||||
pub x_touch_mackie_lcd_color_request: Option<XTouchMackieLcdColorRequest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||
pub struct XTouchMackieLcdColorRequest {
|
||||
pub extender_index: u8,
|
||||
pub channel: Option<u8>,
|
||||
pub color_index: Option<u8>,
|
||||
}
|
||||
|
||||
/// Incoming or outgoing value which might be used to control something or send feedback.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum MidiSourceValue<'a, M: ShortMessage> {
|
||||
// Feedback and control
|
||||
Plain(M),
|
||||
ParameterNumber(ParameterNumberMessage),
|
||||
ControlChange14Bit(ControlChange14BitMessage),
|
||||
/// We must take care not to allocate this in real-time thread!
|
||||
Raw {
|
||||
feedback_address_info: Option<RawFeedbackAddressInfo>,
|
||||
events: RawMidiEvents,
|
||||
},
|
||||
// Control-only
|
||||
Tempo(Bpm),
|
||||
// Control-only
|
||||
BorrowedSysEx(&'a [u8]),
|
||||
}
|
||||
|
||||
/// For being able to reconstructing the source address for feedback purposes (in particular,
|
||||
/// source takeover).
|
||||
///
|
||||
/// Also important for preventing duplicate feedback.
|
||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||
pub enum RawFeedbackAddressInfo {
|
||||
Raw {
|
||||
variable_range: Option<RangeInclusive<usize>>,
|
||||
},
|
||||
Display {
|
||||
spec: DisplaySpecAddress,
|
||||
},
|
||||
Custom(MidiSourceAddress),
|
||||
}
|
||||
|
||||
impl<M: ShortMessage> MidiSourceValue<'_, M> {
|
||||
pub fn single_raw(
|
||||
feedback_address_info: Option<RawFeedbackAddressInfo>,
|
||||
event: RawMidiEvent,
|
||||
) -> Self {
|
||||
Self::Raw {
|
||||
feedback_address_info,
|
||||
events: create_raw_midi_events_singleton(event),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_raw_midi_events_singleton(event: RawMidiEvent) -> RawMidiEvents {
|
||||
vec![event]
|
||||
}
|
||||
|
||||
impl<M: ShortMessage + ShortMessageFactory + Copy> MidiSourceValue<'_, M> {
|
||||
pub fn extract_feedback_address(&self) -> Option<MidiSourceAddress> {
|
||||
use MidiSourceValue::*;
|
||||
let res = match self {
|
||||
Plain(m) => {
|
||||
use StructuredShortMessage::*;
|
||||
match m.to_structured() {
|
||||
NoteOn {
|
||||
channel,
|
||||
key_number,
|
||||
..
|
||||
}
|
||||
| NoteOff {
|
||||
channel,
|
||||
key_number,
|
||||
..
|
||||
} => MidiSourceAddress::Note {
|
||||
channel,
|
||||
key_number,
|
||||
},
|
||||
PolyphonicKeyPressure {
|
||||
channel,
|
||||
key_number,
|
||||
..
|
||||
} => MidiSourceAddress::PolyphonicKeyPressure {
|
||||
channel,
|
||||
key_number,
|
||||
},
|
||||
ControlChange {
|
||||
channel,
|
||||
controller_number,
|
||||
..
|
||||
} => MidiSourceAddress::ControlChange {
|
||||
channel,
|
||||
controller_number,
|
||||
is_14_bit: false,
|
||||
},
|
||||
ProgramChange { channel, .. } => MidiSourceAddress::ProgramChange { channel },
|
||||
ChannelPressure { channel, .. } => {
|
||||
MidiSourceAddress::ChannelPressure { channel }
|
||||
}
|
||||
PitchBendChange { channel, .. } => {
|
||||
MidiSourceAddress::PitchBendChange { channel }
|
||||
}
|
||||
// No feedback supported for other types of MIDI messages
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
ParameterNumber(msg) => MidiSourceAddress::ParameterNumber {
|
||||
channel: msg.channel(),
|
||||
number: msg.number(),
|
||||
is_registered: msg.is_registered(),
|
||||
},
|
||||
ControlChange14Bit(msg) => MidiSourceAddress::ControlChange {
|
||||
channel: msg.channel(),
|
||||
controller_number: msg.msb_controller_number(),
|
||||
is_14_bit: true,
|
||||
},
|
||||
Raw {
|
||||
feedback_address_info,
|
||||
events,
|
||||
} => match feedback_address_info.as_ref()? {
|
||||
RawFeedbackAddressInfo::Raw { variable_range } => MidiSourceAddress::Raw {
|
||||
pattern: events
|
||||
.first()?
|
||||
.bytes()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, b)| {
|
||||
if let Some(vr) = variable_range {
|
||||
if vr.contains(&i) {
|
||||
PatternByte::Variable
|
||||
} else {
|
||||
PatternByte::Fixed(*b)
|
||||
}
|
||||
} else {
|
||||
PatternByte::Fixed(*b)
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
RawFeedbackAddressInfo::Display { spec } => {
|
||||
MidiSourceAddress::Display { spec: spec.clone() }
|
||||
}
|
||||
RawFeedbackAddressInfo::Custom(addr) => addr.clone(),
|
||||
},
|
||||
// No feedback
|
||||
Tempo(_) | BorrowedSysEx(_) => return None,
|
||||
};
|
||||
Some(res)
|
||||
}
|
||||
|
||||
pub fn channel(&self) -> Option<Channel> {
|
||||
use MidiSourceValue::*;
|
||||
match self {
|
||||
Plain(m) => m.channel(),
|
||||
ParameterNumber(m) => Some(m.channel()),
|
||||
ControlChange14Bit(m) => Some(m.channel()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Might allocate!
|
||||
///
|
||||
/// Not usable for producing feedback output that should participate in feedback relay
|
||||
/// (since BorrowedSysEx doesn't contain a feedback address).
|
||||
pub fn try_into_owned(self) -> Result<MidiSourceValue<'static, M>, &'static str> {
|
||||
use MidiSourceValue::*;
|
||||
let res = match self {
|
||||
Plain(v) => Plain(v),
|
||||
ParameterNumber(v) => ParameterNumber(v),
|
||||
ControlChange14Bit(v) => ControlChange14Bit(v),
|
||||
Tempo(v) => Tempo(v),
|
||||
Raw {
|
||||
feedback_address_info,
|
||||
events,
|
||||
} => Raw {
|
||||
feedback_address_info,
|
||||
events,
|
||||
},
|
||||
BorrowedSysEx(bytes) => {
|
||||
// Situations where we convert a borrowed message into an owned are not
|
||||
// situations in which we want to send a feedback value. So it's not bad that
|
||||
// we can't provide a feedback address here.
|
||||
let feedback_address_info = None;
|
||||
let event = RawMidiEvent::try_from_slice(0, bytes)?;
|
||||
MidiSourceValue::single_raw(feedback_address_info, event)
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn into_garbage(self) -> Option<RawMidiEvents> {
|
||||
use MidiSourceValue::*;
|
||||
match self {
|
||||
Raw { events, .. } => Some(events),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// For values that are best sent raw, e.g. sys-ex.
|
||||
pub fn to_raw(&self) -> Option<impl Iterator<Item = &RawMidiEvent>> {
|
||||
use MidiSourceValue::*;
|
||||
match self {
|
||||
Raw { events, .. } => Some(events.iter()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// For values that are best sent as short messages.
|
||||
pub fn to_short_messages(
|
||||
&self,
|
||||
nrpn_data_entry_byte_order: DataEntryByteOrder,
|
||||
) -> [Option<M>; 4] {
|
||||
use MidiSourceValue::*;
|
||||
match self {
|
||||
Plain(msg) => [Some(*msg), None, None, None],
|
||||
ParameterNumber(msg) => msg.to_short_messages(nrpn_data_entry_byte_order),
|
||||
ControlChange14Bit(msg) => {
|
||||
let inner_shorts = msg.to_short_messages();
|
||||
[Some(inner_shorts[0]), Some(inner_shorts[1]), None, None]
|
||||
}
|
||||
Tempo(_) | Raw { .. } | BorrowedSysEx(_) => [None; 4],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UnitValue> for Bpm {
|
||||
fn from(value: UnitValue) -> Self {
|
||||
let min = Bpm::ONE_BPM.get();
|
||||
let span = Bpm::NINE_HUNDRED_SIXTY_BPM.get() - min;
|
||||
Bpm::new_panic(min + value.get() * span)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bpm> for UnitValue {
|
||||
fn from(value: Bpm) -> Self {
|
||||
let min = Bpm::ONE_BPM.get();
|
||||
let span = Bpm::NINE_HUNDRED_SIXTY_BPM.get() - min;
|
||||
// At some point, we allowed BPM values higher than 960 BPM (it's just that REAPER doesn't take them).
|
||||
// That's why we clamp.
|
||||
UnitValue::new_clamped((value.get() - min) / span)
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw MIDI data which is compatible to both VST and REAPER MIDI data structures. The REAPER
|
||||
/// struct is more picky in that it needs offset and size directly in front of the raw data whereas
|
||||
/// the VST struct allows the data to be at a different address. That's why we need to follow the
|
||||
/// REAPER requirement.
|
||||
///
|
||||
/// Conforms to the LongMidiEvent in `reaper-medium` but the goal of `helgoboss-learn` is to be
|
||||
/// DAW-agnostic, so we have to recreate the lowest common denominator.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct RawMidiEvent {
|
||||
/// A MIDI frame offset.
|
||||
///
|
||||
/// This is a 1/1024000 of a second, *not* a sample frame!
|
||||
frame_offset: i32,
|
||||
size: i32,
|
||||
midi_message: [u8; RawMidiEvent::MAX_LENGTH],
|
||||
}
|
||||
|
||||
impl Default for RawMidiEvent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frame_offset: 0,
|
||||
size: 0,
|
||||
midi_message: [0; RawMidiEvent::MAX_LENGTH],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RawMidiEvent {
|
||||
pub const MAX_LENGTH: usize = 256;
|
||||
|
||||
pub const fn new(frame_offset: u32, size: u32, midi_message: [u8; Self::MAX_LENGTH]) -> Self {
|
||||
Self {
|
||||
frame_offset: frame_offset as _,
|
||||
size: size as _,
|
||||
midi_message,
|
||||
}
|
||||
}
|
||||
|
||||
/// If you already have a slice, use this. If you are just building something, `try_from_iter`
|
||||
/// is probably more efficient.
|
||||
pub fn try_from_slice(frame_offset: u32, midi_message: &[u8]) -> Result<Self, &'static str> {
|
||||
if midi_message.len() > Self::MAX_LENGTH {
|
||||
return Err("given MIDI message too long");
|
||||
}
|
||||
let mut array = [0; Self::MAX_LENGTH];
|
||||
// TODO-low I think copying from a slice is the only way to go, even we own a vec or array.
|
||||
// REAPER's struct layout requires us to put something in front of the vec, which is
|
||||
// not or at least not easily possible without copying.
|
||||
array[..midi_message.len()].copy_from_slice(midi_message);
|
||||
Ok(Self::new(frame_offset, midi_message.len() as _, array))
|
||||
}
|
||||
|
||||
pub fn try_from_iter<T: IntoIterator<Item = u8>>(
|
||||
frame_offset: u32,
|
||||
iter: T,
|
||||
) -> Result<Self, &'static str> {
|
||||
let mut array = [0; Self::MAX_LENGTH];
|
||||
let mut i = 0usize;
|
||||
for b in iter {
|
||||
if i == Self::MAX_LENGTH {
|
||||
return Err("given content too long");
|
||||
}
|
||||
let elem = unsafe { array.get_unchecked_mut(i) };
|
||||
*elem = b;
|
||||
i += 1;
|
||||
}
|
||||
Ok(Self::new(frame_offset, i as u32, array))
|
||||
}
|
||||
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.midi_message[..self.size as usize]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "reaper-low")]
|
||||
impl AsRef<reaper_low::raw::MIDI_event_t> for RawMidiEvent {
|
||||
fn as_ref(&self) -> &reaper_low::raw::MIDI_event_t {
|
||||
unsafe { &*(self as *const RawMidiEvent as *const reaper_low::raw::MIDI_event_t) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
mod midi_source_value;
|
||||
pub use midi_source_value::*;
|
||||
|
||||
mod midi_source;
|
||||
pub use midi_source::*;
|
||||
|
||||
mod osc_source;
|
||||
pub use osc_source::*;
|
||||
|
||||
mod raw_midi;
|
||||
pub use raw_midi::*;
|
||||
|
||||
mod midi_source_script;
|
||||
pub use midi_source_script::*;
|
||||
|
||||
mod feedback_script;
|
||||
pub use feedback_script::*;
|
||||
|
||||
mod source_context;
|
||||
pub use source_context::*;
|
||||
|
||||
mod color_util;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_util;
|
||||
|
||||
pub mod devices;
|
||||
@@ -0,0 +1,573 @@
|
||||
use crate::DetailedSourceCharacter::Trigger;
|
||||
use std::cmp;
|
||||
|
||||
use crate::{
|
||||
format_percentage_without_unit, parse_percentage_without_unit, AbsoluteValue, ControlValue,
|
||||
DetailedSourceCharacter, DiscreteIncrement, FeedbackValue, Fraction, Interval, RgbColor,
|
||||
SourceCharacter, UnitValue, UNIT_INTERVAL,
|
||||
};
|
||||
use derive_more::Display;
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use rosc::{OscColor, OscMessage, OscType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
use std::convert::TryInto;
|
||||
use strum::EnumIter;
|
||||
|
||||
/// With OSC it's easy: The source address is the address!
|
||||
pub type OscSourceAddress = String;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct OscSource {
|
||||
/// To filter out the correct messages.
|
||||
address_pattern: String,
|
||||
/// To process a value (not just trigger).
|
||||
arg_descriptor: Option<OscArgDescriptor>,
|
||||
/// If non-empty, these are used for mapping feedback data to arguments.
|
||||
feedback_args: Vec<OscFeedbackProp>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Debug,
|
||||
strum::EnumString,
|
||||
strum::Display,
|
||||
SerializeDisplay,
|
||||
DeserializeFromStr,
|
||||
)]
|
||||
pub enum OscFeedbackProp {
|
||||
// Floats
|
||||
#[strum(serialize = "value.float")]
|
||||
ValueAsFloat,
|
||||
// Doubles
|
||||
#[strum(serialize = "value.double")]
|
||||
ValueAsDouble,
|
||||
// Bools
|
||||
#[strum(serialize = "value.bool")]
|
||||
ValueAsBool,
|
||||
// Nil
|
||||
#[strum(serialize = "nil")]
|
||||
Nil,
|
||||
// Inf
|
||||
#[strum(serialize = "inf")]
|
||||
Inf,
|
||||
// Integers
|
||||
#[strum(serialize = "value.int")]
|
||||
ValueAsInt,
|
||||
// Strings
|
||||
#[strum(serialize = "value.string")]
|
||||
ValueAsString,
|
||||
// Longs
|
||||
#[strum(serialize = "value.long")]
|
||||
ValueAsLong,
|
||||
#[strum(serialize = "style.color.rrggbb")]
|
||||
ColorRrggbb,
|
||||
#[strum(serialize = "style.background_color.rrggbb")]
|
||||
BackgroundColorRrggbb,
|
||||
// Colors
|
||||
#[strum(serialize = "style.color")]
|
||||
Color,
|
||||
#[strum(serialize = "style.backround_color")]
|
||||
BackgroundColor,
|
||||
}
|
||||
|
||||
impl Default for OscFeedbackProp {
|
||||
fn default() -> Self {
|
||||
Self::Nil
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
pub struct OscArgDescriptor {
|
||||
/// To select the correct value.
|
||||
index: u32,
|
||||
/// To send the correct value type on feedback.
|
||||
type_tag: OscTypeTag,
|
||||
/// Interpret 1 values as increments and 0 values as decrements.
|
||||
is_relative: bool,
|
||||
/// Value range for all range types (double, float, int, long).
|
||||
value_range: Interval<f64>,
|
||||
}
|
||||
|
||||
impl OscArgDescriptor {
|
||||
pub fn new(
|
||||
index: u32,
|
||||
type_tag: OscTypeTag,
|
||||
is_relative: bool,
|
||||
value_range: Interval<f64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
index,
|
||||
type_tag,
|
||||
is_relative,
|
||||
value_range,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index(self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
|
||||
pub fn type_tag(self) -> OscTypeTag {
|
||||
self.type_tag
|
||||
}
|
||||
|
||||
pub fn is_relative(self) -> bool {
|
||||
self.is_relative
|
||||
}
|
||||
|
||||
pub fn value_range(&self) -> Interval<f64> {
|
||||
self.value_range
|
||||
}
|
||||
|
||||
pub fn from_msg(msg: &OscMessage, arg_index_hint: u32) -> Option<Self> {
|
||||
let desc = if let Some(hinted_arg) = msg.args.get(arg_index_hint as usize) {
|
||||
Self::from_arg(arg_index_hint, hinted_arg)
|
||||
} else {
|
||||
let first_arg = msg.args.first()?;
|
||||
Self::from_arg(0, first_arg)
|
||||
};
|
||||
Some(desc)
|
||||
}
|
||||
|
||||
pub fn to_concrete_args(self, value: FeedbackValue) -> Option<Vec<OscType>> {
|
||||
self.type_tag
|
||||
.to_concrete_args(self.index, value, self.value_range)
|
||||
}
|
||||
|
||||
fn from_arg(index: u32, arg: &OscType) -> Self {
|
||||
Self {
|
||||
index,
|
||||
type_tag: OscTypeTag::from_arg(arg),
|
||||
// Relative is the exception, so we reset it when learning.
|
||||
is_relative: false,
|
||||
value_range: match get_range_value(arg) {
|
||||
None => DEFAULT_OSC_ARG_VALUE_RANGE,
|
||||
Some(v) => Interval::new_auto(0.0, v),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const DEFAULT_OSC_ARG_VALUE_RANGE: Interval<f64> = UNIT_INTERVAL;
|
||||
|
||||
fn get_range_value(arg: &OscType) -> Option<f64> {
|
||||
use OscType::*;
|
||||
match arg {
|
||||
Int(v) => Some(*v as f64),
|
||||
Float(v) => Some(*v as f64),
|
||||
Long(v) => Some(*v as f64),
|
||||
Double(v) => Some(*v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
EnumIter,
|
||||
TryFromPrimitive,
|
||||
IntoPrimitive,
|
||||
Display,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[repr(usize)]
|
||||
// TODO-low Rename. This it not the tag, it's rather the OscType without value.
|
||||
pub enum OscTypeTag {
|
||||
#[display(fmt = "Float")]
|
||||
Float,
|
||||
#[display(fmt = "Double")]
|
||||
Double,
|
||||
#[display(fmt = "Bool (on/off)")]
|
||||
Bool,
|
||||
#[display(fmt = "Nil (trigger only)")]
|
||||
Nil,
|
||||
#[display(fmt = "Infinitum (trigger only)")]
|
||||
Inf,
|
||||
#[display(fmt = "Int")]
|
||||
Int,
|
||||
#[display(fmt = "String (feedback only)")]
|
||||
String,
|
||||
#[display(fmt = "Blob (ignored)")]
|
||||
Blob,
|
||||
#[display(fmt = "Time (ignored)")]
|
||||
Time,
|
||||
#[display(fmt = "Long")]
|
||||
Long,
|
||||
#[display(fmt = "Char (ignored)")]
|
||||
Char,
|
||||
#[display(fmt = "Color (feedback only)")]
|
||||
Color,
|
||||
#[display(fmt = "MIDI (ignored)")]
|
||||
Midi,
|
||||
#[display(fmt = "Array (ignored)")]
|
||||
Array,
|
||||
}
|
||||
|
||||
impl Default for OscTypeTag {
|
||||
fn default() -> Self {
|
||||
Self::Float
|
||||
}
|
||||
}
|
||||
|
||||
impl OscTypeTag {
|
||||
pub fn from_arg(arg: &OscType) -> Self {
|
||||
use OscType::*;
|
||||
match arg {
|
||||
Int(_) => Self::Int,
|
||||
Float(_) => Self::Float,
|
||||
String(_) => Self::String,
|
||||
Blob(_) => Self::Blob,
|
||||
Time(_) => Self::Time,
|
||||
Long(_) => Self::Long,
|
||||
Double(_) => Self::Double,
|
||||
Char(_) => Self::Char,
|
||||
Color(_) => Self::Color,
|
||||
Midi(_) => Self::Midi,
|
||||
Bool(_) => Self::Bool,
|
||||
Array(_) => Self::Array,
|
||||
Nil => Self::Nil,
|
||||
Inf => Self::Inf,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_concrete_args(
|
||||
self,
|
||||
index: u32,
|
||||
v: FeedbackValue,
|
||||
value_range: Interval<f64>,
|
||||
) -> Option<Vec<OscType>> {
|
||||
use OscTypeTag::*;
|
||||
let value = match self {
|
||||
Float => convert_feedback_prop_to_arg(OscFeedbackProp::ValueAsFloat, &v, value_range)?,
|
||||
Double => {
|
||||
convert_feedback_prop_to_arg(OscFeedbackProp::ValueAsDouble, &v, value_range)?
|
||||
}
|
||||
Bool => convert_feedback_prop_to_arg(OscFeedbackProp::ValueAsBool, &v, value_range)?,
|
||||
Nil => OscType::Nil,
|
||||
Inf => OscType::Inf,
|
||||
Int => convert_feedback_prop_to_arg(OscFeedbackProp::ValueAsInt, &v, value_range)?,
|
||||
String => {
|
||||
convert_feedback_prop_to_arg(OscFeedbackProp::ValueAsString, &v, value_range)?
|
||||
}
|
||||
Long => convert_feedback_prop_to_arg(OscFeedbackProp::ValueAsLong, &v, value_range)?,
|
||||
Color => convert_feedback_prop_to_arg(OscFeedbackProp::Color, &v, value_range)?,
|
||||
_ => return None,
|
||||
};
|
||||
// Send nil for all other elements
|
||||
let mut vec = vec![OscType::Nil; (index + 1) as usize];
|
||||
vec[index as usize] = value;
|
||||
Some(vec)
|
||||
}
|
||||
|
||||
pub fn supports_control(self) -> bool {
|
||||
use OscTypeTag::*;
|
||||
matches!(self, Float | Double | Bool | Nil | Inf | Int | Long)
|
||||
}
|
||||
|
||||
pub fn supports_feedback(self) -> bool {
|
||||
use OscTypeTag::*;
|
||||
matches!(
|
||||
self,
|
||||
Float | Double | Bool | Nil | Inf | Int | String | Long | Color
|
||||
)
|
||||
}
|
||||
|
||||
pub fn supports_value_range(self) -> bool {
|
||||
use OscTypeTag::*;
|
||||
matches!(self, Float | Double | Int | Long)
|
||||
}
|
||||
|
||||
pub fn is_discrete(self) -> bool {
|
||||
use OscTypeTag::*;
|
||||
matches!(self, Int | Long)
|
||||
}
|
||||
}
|
||||
|
||||
impl OscSource {
|
||||
pub fn feedback_address(&self) -> &OscSourceAddress {
|
||||
&self.address_pattern
|
||||
}
|
||||
|
||||
/// Checks if the given message is directed to the same address as the one of this source.
|
||||
///
|
||||
/// Used for:
|
||||
///
|
||||
/// - Source takeover (feedback)
|
||||
pub fn has_same_feedback_address_as_value(&self, value: &OscMessage) -> bool {
|
||||
self.address_pattern == value.addr
|
||||
}
|
||||
|
||||
/// Checks if this and the given source share the same address.
|
||||
///
|
||||
/// Used for:
|
||||
///
|
||||
/// - Feedback diffing
|
||||
pub fn has_same_feedback_address_as_source(&self, other: &Self) -> bool {
|
||||
self.address_pattern == other.address_pattern
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
address_pattern: String,
|
||||
arg_descriptor: Option<OscArgDescriptor>,
|
||||
feedback_args: Vec<OscFeedbackProp>,
|
||||
) -> Self {
|
||||
Self {
|
||||
address_pattern,
|
||||
arg_descriptor,
|
||||
feedback_args,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_source_value(msg: OscMessage, arg_index_hint: Option<u32>) -> OscSource {
|
||||
let arg_descriptor = OscArgDescriptor::from_msg(&msg, arg_index_hint.unwrap_or(0));
|
||||
OscSource::new(msg.addr, arg_descriptor, vec![])
|
||||
}
|
||||
|
||||
pub fn address_pattern(&self) -> &str {
|
||||
&self.address_pattern
|
||||
}
|
||||
|
||||
pub fn arg_descriptor(&self) -> Option<OscArgDescriptor> {
|
||||
self.arg_descriptor
|
||||
}
|
||||
|
||||
pub fn control(&self, msg: &OscMessage) -> Option<ControlValue> {
|
||||
let (absolute_value, is_relative) = {
|
||||
if msg.addr != self.address_pattern {
|
||||
return None;
|
||||
}
|
||||
if let Some(desc) = self.arg_descriptor {
|
||||
if let Some(arg) = msg.args.get(desc.index as usize) {
|
||||
use OscType::*;
|
||||
let v =
|
||||
match arg {
|
||||
Float(f) => AbsoluteValue::Continuous(
|
||||
map_continuous_from_range_to_unit(*f as f64, desc.value_range),
|
||||
),
|
||||
Double(d) => AbsoluteValue::Continuous(
|
||||
map_continuous_from_range_to_unit(*d, desc.value_range),
|
||||
),
|
||||
Bool(on) => AbsoluteValue::Continuous(if *on {
|
||||
UnitValue::MAX
|
||||
} else {
|
||||
UnitValue::MIN
|
||||
}),
|
||||
// Infinity/impulse or nil/null - act like a trigger.
|
||||
Inf | Nil => AbsoluteValue::Continuous(UnitValue::MAX),
|
||||
Int(i) => AbsoluteValue::Discrete(map_discrete_from_range_to_positive(
|
||||
*i,
|
||||
desc.value_range,
|
||||
)),
|
||||
Long(l) => {
|
||||
// TODO-low-discrete Maybe increase fraction integers to 64-bit? Right now
|
||||
// we don't really take advantage of fractions, so we emit continuous control
|
||||
// values as long as this doesn't change.
|
||||
AbsoluteValue::Continuous(map_continuous_from_range_to_unit(
|
||||
*l as f64,
|
||||
desc.value_range,
|
||||
))
|
||||
}
|
||||
String(_) | Blob(_) | Time(_) | Char(_) | Color(_) | Midi(_)
|
||||
| Array(_) => return None,
|
||||
};
|
||||
(v, desc.is_relative)
|
||||
} else {
|
||||
// Argument not found. Don't do anything.
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
// Source shall not look at any argument. Act like a trigger.
|
||||
(AbsoluteValue::Continuous(UnitValue::MAX), false)
|
||||
}
|
||||
};
|
||||
let control_value = if is_relative {
|
||||
let inc = if absolute_value.is_on() { 1 } else { -1 };
|
||||
ControlValue::RelativeDiscrete(DiscreteIncrement::new(inc))
|
||||
} else {
|
||||
ControlValue::from_absolute(absolute_value)
|
||||
};
|
||||
Some(control_value)
|
||||
}
|
||||
|
||||
pub fn format_control_value(&self, value: ControlValue) -> Result<String, &'static str> {
|
||||
let v = value.to_unit_value()?.get();
|
||||
Ok(format_percentage_without_unit(v))
|
||||
}
|
||||
|
||||
pub fn parse_control_value(&self, text: &str) -> Result<UnitValue, &'static str> {
|
||||
parse_percentage_without_unit(text)?.try_into()
|
||||
}
|
||||
|
||||
pub fn character(&self) -> SourceCharacter {
|
||||
use SourceCharacter::*;
|
||||
if let Some(desc) = self.arg_descriptor {
|
||||
use OscTypeTag::*;
|
||||
match desc.type_tag {
|
||||
Float | Double | Int | Long => RangeElement,
|
||||
Bool | Nil | Inf => MomentaryButton,
|
||||
_ => MomentaryButton,
|
||||
}
|
||||
} else {
|
||||
MomentaryButton
|
||||
}
|
||||
}
|
||||
|
||||
pub fn possible_detailed_characters(&self) -> Vec<DetailedSourceCharacter> {
|
||||
if let Some(desc) = self.arg_descriptor {
|
||||
if desc.is_relative {
|
||||
vec![DetailedSourceCharacter::Relative]
|
||||
} else {
|
||||
use OscTypeTag::*;
|
||||
match desc.type_tag {
|
||||
Float | Double | Int | Long => vec![
|
||||
DetailedSourceCharacter::RangeControl,
|
||||
DetailedSourceCharacter::MomentaryVelocitySensitiveButton,
|
||||
DetailedSourceCharacter::MomentaryOnOffButton,
|
||||
DetailedSourceCharacter::Trigger,
|
||||
],
|
||||
_ => vec![DetailedSourceCharacter::MomentaryOnOffButton, Trigger],
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vec![DetailedSourceCharacter::Trigger]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn feedback(&self, feedback_value: FeedbackValue) -> Option<OscMessage> {
|
||||
let msg = OscMessage {
|
||||
addr: self.address_pattern.clone(),
|
||||
args: if !self.feedback_args.is_empty() {
|
||||
// Explicit feedback args given.
|
||||
let value_range = self
|
||||
.arg_descriptor
|
||||
.map(|desc| desc.value_range)
|
||||
.unwrap_or(DEFAULT_OSC_ARG_VALUE_RANGE);
|
||||
self.feedback_args
|
||||
.iter()
|
||||
.map(|prop| {
|
||||
convert_feedback_prop_to_arg(*prop, &feedback_value, value_range)
|
||||
.unwrap_or(OscType::Nil)
|
||||
})
|
||||
.collect()
|
||||
} else if let Some(desc) = self.arg_descriptor {
|
||||
// No explicit feedback args given. Just derive from argument descriptor.
|
||||
desc.to_concrete_args(feedback_value)?
|
||||
} else {
|
||||
// No arguments shall be sent.
|
||||
vec![]
|
||||
},
|
||||
};
|
||||
Some(msg)
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_feedback_prop_to_arg(
|
||||
prop: OscFeedbackProp,
|
||||
v: &FeedbackValue,
|
||||
value_range: Interval<f64>,
|
||||
) -> Option<OscType> {
|
||||
use OscFeedbackProp::*;
|
||||
let arg = match prop {
|
||||
ValueAsFloat | ValueAsDouble | ValueAsLong => {
|
||||
let unit_value = v.to_numeric()?.value.to_unit_value();
|
||||
let range_value = map_continuous_from_unit_to_range(unit_value, value_range);
|
||||
match prop {
|
||||
ValueAsFloat => OscType::Float(range_value as _),
|
||||
ValueAsDouble => OscType::Double(range_value),
|
||||
ValueAsLong => OscType::Long(range_value.round() as i64),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
ValueAsBool => OscType::Bool(v.to_numeric()?.value.is_on()),
|
||||
Nil => OscType::Nil,
|
||||
Inf => OscType::Inf,
|
||||
ValueAsInt => {
|
||||
let range_value = match v.to_numeric()?.value {
|
||||
AbsoluteValue::Continuous(uv) => {
|
||||
map_continuous_from_unit_to_range(uv, value_range).round() as i32
|
||||
}
|
||||
AbsoluteValue::Discrete(f) => {
|
||||
map_discrete_from_positive_to_range(f.actual(), value_range)
|
||||
}
|
||||
};
|
||||
OscType::Int(range_value)
|
||||
}
|
||||
ValueAsString => OscType::String(v.to_textual().text.into_owned()),
|
||||
ColorRrggbb => convert_color_to_rrggbb_string_arg(v.color()),
|
||||
BackgroundColorRrggbb => convert_color_to_rrggbb_string_arg(v.background_color()),
|
||||
Color => convert_color_to_native_color_arg(v.color()),
|
||||
BackgroundColor => convert_color_to_native_color_arg(v.background_color()),
|
||||
};
|
||||
Some(arg)
|
||||
}
|
||||
|
||||
fn convert_color_to_rrggbb_string_arg(v: Option<RgbColor>) -> OscType {
|
||||
match v {
|
||||
// Nil is hopefully interpreted as "Default color".
|
||||
None => OscType::Nil,
|
||||
Some(c) => {
|
||||
let color_string = format!("{:02X}{:02X}{:02X}", c.r(), c.g(), c.b());
|
||||
OscType::String(color_string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_color_to_native_color_arg(v: Option<RgbColor>) -> OscType {
|
||||
match v {
|
||||
// Nil is hopefully interpreted as "Default color".
|
||||
None => OscType::Nil,
|
||||
Some(c) => OscType::Color(OscColor {
|
||||
red: c.r(),
|
||||
green: c.g(),
|
||||
blue: c.b(),
|
||||
alpha: 255,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_continuous_from_range_to_unit(x: f64, value_range: Interval<f64>) -> UnitValue {
|
||||
// y = (x - min) / span
|
||||
let y = (x - value_range.min_val()) / value_range.span();
|
||||
UnitValue::new_clamped(y)
|
||||
}
|
||||
|
||||
fn map_continuous_from_unit_to_range(y: UnitValue, value_range: Interval<f64>) -> f64 {
|
||||
// y = (x - min) / span
|
||||
// y * span = x - min
|
||||
// x = y * span + min
|
||||
y.get() * value_range.span() + value_range.min_val()
|
||||
}
|
||||
|
||||
fn map_discrete_from_range_to_positive(x: i32, value_range: Interval<f64>) -> Fraction {
|
||||
let rounded_range = round_value_range(value_range);
|
||||
Fraction::new(
|
||||
clamp_to_positive(x - rounded_range.min_val()),
|
||||
clamp_to_positive(rounded_range.span()),
|
||||
)
|
||||
}
|
||||
|
||||
fn map_discrete_from_positive_to_range(y: u32, value_range: Interval<f64>) -> i32 {
|
||||
let rounded_range = round_value_range(value_range);
|
||||
y as i32 + rounded_range.min_val()
|
||||
}
|
||||
|
||||
fn round_value_range(value_range: Interval<f64>) -> Interval<i32> {
|
||||
Interval::new(
|
||||
value_range.min_val().round() as i32,
|
||||
value_range.max_val().round() as i32,
|
||||
)
|
||||
}
|
||||
|
||||
fn clamp_to_positive(v: i32) -> u32 {
|
||||
cmp::max(0, v) as u32
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
use crate::{AbsoluteValue, Fraction, PatternByte, RawMidiEvent, UnitValue};
|
||||
use logos::{Lexer, Logos};
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter, Write};
|
||||
use std::num::ParseIntError;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
|
||||
pub struct RawMidiPattern {
|
||||
entries: Vec<RawMidiPatternEntry>,
|
||||
resolution: u8,
|
||||
}
|
||||
|
||||
impl RawMidiPattern {
|
||||
pub fn from_entries(entries: Vec<RawMidiPatternEntry>) -> Self {
|
||||
let max_variable_bit_index = entries
|
||||
.iter()
|
||||
.filter_map(|e| e.max_variable_bit_index())
|
||||
.max();
|
||||
Self {
|
||||
entries,
|
||||
resolution: if let Some(i) = max_variable_bit_index {
|
||||
i + 1
|
||||
} else {
|
||||
0
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fixed_from_slice(bytes: &[u8]) -> Self {
|
||||
let entries = bytes
|
||||
.iter()
|
||||
.map(|byte| RawMidiPatternEntry::FixedByte(*byte))
|
||||
.collect();
|
||||
Self {
|
||||
entries,
|
||||
resolution: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn variable_range(&self) -> Option<RangeInclusive<usize>> {
|
||||
let left = self.entries().iter().position(|e| !e.is_fixed())?;
|
||||
let right = self.entries().iter().rposition(|e| !e.is_fixed())?;
|
||||
Some(left..=right)
|
||||
}
|
||||
|
||||
pub fn to_pattern_bytes(&self) -> Vec<PatternByte> {
|
||||
self.entries()
|
||||
.iter()
|
||||
.map(|e| {
|
||||
if let Some(b) = e.byte_if_fixed() {
|
||||
PatternByte::Fixed(b)
|
||||
} else {
|
||||
PatternByte::Variable
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> &[RawMidiPatternEntry] {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
/// Resolution in bit (maximum 16 bit).
|
||||
///
|
||||
/// If no variable bytes, this returns 0.
|
||||
pub fn resolution(&self) -> u8 {
|
||||
self.resolution
|
||||
}
|
||||
|
||||
/// If no variable bytes, this returns 0.
|
||||
pub fn max_discrete_value(&self) -> u16 {
|
||||
(2u32.pow(self.resolution as _) - 1) as u16
|
||||
}
|
||||
|
||||
pub fn step_size(&self) -> Option<UnitValue> {
|
||||
let max = self.max_discrete_value();
|
||||
if max == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(UnitValue::new_clamped(1.0 / max as f64))
|
||||
}
|
||||
|
||||
/// If it matches and there are no variable bytes in the pattern, this returns
|
||||
/// `Some(Fraction(0, 0))`.
|
||||
pub fn match_and_capture(&self, bytes: &[u8]) -> Option<Fraction> {
|
||||
if bytes.len() != self.entries.len() {
|
||||
return None;
|
||||
}
|
||||
let mut current_value: u16 = 0;
|
||||
for (i, b) in bytes.iter().enumerate() {
|
||||
let pattern_entry = self.entries[i];
|
||||
if let Some(v) = pattern_entry.match_and_capture(*b, current_value) {
|
||||
current_value = v;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let fraction = Fraction::new(current_value as _, self.max_discrete_value() as _);
|
||||
Some(fraction)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self, variable_value: AbsoluteValue) -> Vec<u8> {
|
||||
self.byte_iter(variable_value).collect()
|
||||
}
|
||||
|
||||
pub fn byte_iter(
|
||||
&self,
|
||||
variable_value: AbsoluteValue,
|
||||
) -> impl ExactSizeIterator<Item = u8> + '_ {
|
||||
let discrete_value = match variable_value {
|
||||
AbsoluteValue::Continuous(v) => v.to_discrete(self.max_discrete_value()),
|
||||
AbsoluteValue::Discrete(f) => {
|
||||
std::cmp::min(f.actual(), self.max_discrete_value() as u32) as u16
|
||||
}
|
||||
};
|
||||
self.entries.iter().map(move |e| e.to_byte(discrete_value))
|
||||
}
|
||||
|
||||
pub fn to_concrete_midi_event(
|
||||
&self,
|
||||
frame_offset: u32,
|
||||
variable_value: AbsoluteValue,
|
||||
) -> RawMidiEvent {
|
||||
// TODO-medium Use RawMidiEvent::try_from_iter
|
||||
let mut array = [0; RawMidiEvent::MAX_LENGTH];
|
||||
let mut i = 0u32;
|
||||
for byte in self
|
||||
.byte_iter(variable_value)
|
||||
.take(RawMidiEvent::MAX_LENGTH)
|
||||
{
|
||||
array[i as usize] = byte;
|
||||
i += 1;
|
||||
}
|
||||
RawMidiEvent::new(frame_offset, i, array)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub enum RawMidiPatternEntry {
|
||||
FixedByte(u8),
|
||||
PotentiallyVariableByte(BitPattern),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub struct BitPattern {
|
||||
/// From most significant to least significant bit.
|
||||
entries: [BitPatternEntry; 8],
|
||||
}
|
||||
|
||||
impl BitPattern {
|
||||
pub fn contains_variable_portions(&self) -> bool {
|
||||
self.entries
|
||||
.iter()
|
||||
.any(|bpe| matches!(bpe, BitPatternEntry::VariableBit(_)))
|
||||
}
|
||||
|
||||
fn max_variable_bit_index(&self) -> Option<u8> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|bpe| bpe.variable_bit_index())
|
||||
.max()
|
||||
}
|
||||
|
||||
pub fn to_byte(self, discrete_value: u16) -> u8 {
|
||||
let mut final_byte: u8 = 0;
|
||||
for i in 0..8 {
|
||||
use BitPatternEntry::*;
|
||||
let final_bit = match self.entries[i] {
|
||||
FixedBit(bit) => bit,
|
||||
VariableBit(bit_index) => (discrete_value & (1 << bit_index) as u16) > 0,
|
||||
};
|
||||
if final_bit {
|
||||
final_byte |= 1 << (7 - i);
|
||||
}
|
||||
}
|
||||
final_byte
|
||||
}
|
||||
|
||||
fn match_and_capture(&self, actual_byte: u8, current_value: u16) -> Option<u16> {
|
||||
let mut new_value = current_value;
|
||||
for i in 0..8 {
|
||||
let actual_bit = (actual_byte >> (7 - i)) & 1 == 1;
|
||||
use BitPatternEntry::*;
|
||||
match self.entries[i] {
|
||||
FixedBit(bit) => {
|
||||
if bit != actual_bit {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
VariableBit(bit_index) => {
|
||||
if actual_bit {
|
||||
new_value |= 1 << bit_index;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Some(new_value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub enum BitPatternEntry {
|
||||
FixedBit(bool),
|
||||
/// The number represents the bit index starting from 0 where 0 represents the *least*
|
||||
/// significant bit!.
|
||||
VariableBit(u8),
|
||||
}
|
||||
|
||||
impl Default for BitPatternEntry {
|
||||
fn default() -> Self {
|
||||
BitPatternEntry::FixedBit(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl BitPatternEntry {
|
||||
fn variable_bit_index(&self) -> Option<u8> {
|
||||
use BitPatternEntry::*;
|
||||
match self {
|
||||
FixedBit(_) => None,
|
||||
VariableBit(i) => Some(*i),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RawMidiPatternEntry {
|
||||
fn is_fixed(&self) -> bool {
|
||||
// TODO-low This could be implemented better by transforming potentially variable
|
||||
// bytes that are not variable into fixed bytes in the first place!
|
||||
self.byte_if_fixed().is_some()
|
||||
}
|
||||
|
||||
fn byte_if_fixed(&self) -> Option<u8> {
|
||||
use RawMidiPatternEntry::*;
|
||||
match self {
|
||||
FixedByte(b) => Some(*b),
|
||||
PotentiallyVariableByte(p) => {
|
||||
if p.contains_variable_portions() {
|
||||
None
|
||||
} else {
|
||||
// Value parameter not important if pattern doesn't contain
|
||||
// variable portions.
|
||||
Some(p.to_byte(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn match_and_capture(&self, actual_byte: u8, current_value: u16) -> Option<u16> {
|
||||
use RawMidiPatternEntry::*;
|
||||
match self {
|
||||
FixedByte(b) => {
|
||||
if actual_byte == *b {
|
||||
Some(current_value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
PotentiallyVariableByte(pattern) => {
|
||||
pattern.match_and_capture(actual_byte, current_value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn max_variable_bit_index(&self) -> Option<u8> {
|
||||
use RawMidiPatternEntry::*;
|
||||
match self {
|
||||
FixedByte(_) => None,
|
||||
PotentiallyVariableByte(bit_pattern) => bit_pattern.max_variable_bit_index(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_byte(self, discrete_value: u16) -> u8 {
|
||||
use RawMidiPatternEntry::*;
|
||||
match self {
|
||||
FixedByte(byte) => byte,
|
||||
PotentiallyVariableByte(bit_pattern) => bit_pattern.to_byte(discrete_value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RawMidiPattern {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let string_vec: Vec<_> = self.entries.iter().map(|e| e.to_string()).collect();
|
||||
f.write_str(&string_vec.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RawMidiPatternEntry {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
use RawMidiPatternEntry::*;
|
||||
match self {
|
||||
FixedByte(byte) => write!(f, "{:02X}", *byte),
|
||||
PotentiallyVariableByte(pattern) => write!(f, "[{pattern}]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for BitPattern {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
for entry in &self.entries[..4] {
|
||||
let _ = entry.fmt(f);
|
||||
}
|
||||
let _ = f.write_char(' ');
|
||||
for entry in &self.entries[4..] {
|
||||
let _ = entry.fmt(f);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for BitPatternEntry {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
use BitPatternEntry::*;
|
||||
match self {
|
||||
FixedBit(bit) => write!(f, "{}", if *bit { '1' } else { '0' }),
|
||||
VariableBit(bit_index) => write!(f, "{}", (97 + bit_index) as char),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RawMidiPattern {
|
||||
type Err = ParseRawMidiPatternError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let lex: Lexer<RawMidiPatternToken> = RawMidiPatternToken::lexer(s);
|
||||
use RawMidiPatternToken::*;
|
||||
let entries: Result<Vec<_>, ParseRawMidiPatternError> = lex
|
||||
.map(|token| {
|
||||
let entry = match token? {
|
||||
FixedByte(byte) => RawMidiPatternEntry::FixedByte(byte),
|
||||
PotentiallyVariableByte(pattern) => {
|
||||
RawMidiPatternEntry::PotentiallyVariableByte(pattern)
|
||||
}
|
||||
};
|
||||
Ok(entry)
|
||||
})
|
||||
.collect();
|
||||
let entries = entries.map_err(|_| "couldn't parse raw MIDI pattern")?;
|
||||
Ok(RawMidiPattern::from_entries(entries))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Logos)]
|
||||
#[logos(skip r"[ \t\n\f]+")]
|
||||
#[logos(error = ParseRawMidiPatternError)]
|
||||
enum RawMidiPatternToken {
|
||||
#[regex(r"\[[01abcdefghijklmnop ]*\]", parse_as_bit_pattern)]
|
||||
PotentiallyVariableByte(BitPattern),
|
||||
#[regex(r"[0-9a-fA-F][0-9a-fA-F]?", parse_as_byte)]
|
||||
FixedByte(u8),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Default, thiserror::Error)]
|
||||
#[error("{msg}")]
|
||||
pub struct ParseRawMidiPatternError {
|
||||
msg: &'static str,
|
||||
}
|
||||
|
||||
impl From<&'static str> for ParseRawMidiPatternError {
|
||||
fn from(msg: &'static str) -> Self {
|
||||
Self { msg }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseIntError> for ParseRawMidiPatternError {
|
||||
fn from(_: ParseIntError) -> Self {
|
||||
Self {
|
||||
msg: "problem parsing fixed byte",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_as_byte(lex: &mut Lexer<RawMidiPatternToken>) -> Result<u8, core::num::ParseIntError> {
|
||||
u8::from_str_radix(lex.slice(), 16)
|
||||
}
|
||||
|
||||
fn parse_as_bit_pattern(lex: &mut Lexer<RawMidiPatternToken>) -> Result<BitPattern, &'static str> {
|
||||
let mut entries: [BitPatternEntry; 8] = Default::default();
|
||||
let slice: &str = lex.slice();
|
||||
let mut i = 0;
|
||||
for c in slice.chars() {
|
||||
use BitPatternEntry::*;
|
||||
let entry = match c {
|
||||
'0' => FixedBit(false),
|
||||
'1' => FixedBit(true),
|
||||
'a'..='p' => VariableBit(c as u8 - 97),
|
||||
_ => continue,
|
||||
};
|
||||
if i > 7 {
|
||||
return Err("too many bits in bit pattern");
|
||||
}
|
||||
entries[i] = entry;
|
||||
i += 1;
|
||||
}
|
||||
let pattern = BitPattern { entries };
|
||||
Ok(pattern)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn one_variable_nibble() {
|
||||
// Given
|
||||
let pattern: RawMidiPattern = "F0 [0000 dcba] F7".parse().unwrap();
|
||||
// When
|
||||
// Then
|
||||
assert_eq!(
|
||||
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::MAX)),
|
||||
vec![0xf0, 0x0f, 0xf7]
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.match_and_capture(&[0xf0, 0x0f, 0xf7]),
|
||||
Some(Fraction::new(15, 15))
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::MIN)),
|
||||
vec![0xf0, 0x00, 0xf7]
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.match_and_capture(&[0xf0, 0x00, 0xf7]),
|
||||
Some(Fraction::new(0, 15))
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::new(0.5))),
|
||||
vec![0xf0, 0x08, 0xf7]
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.match_and_capture(&[0xf0, 0x08, 0xf7]),
|
||||
Some(Fraction::new(8, 15))
|
||||
);
|
||||
assert_eq!(&pattern.to_string(), "F0 [0000 dcba] F7");
|
||||
assert_eq!(pattern.match_and_capture(&[0xf1, 0x0f, 0xf7]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_variable_nibble_no_spaces() {
|
||||
// Given
|
||||
let pattern: RawMidiPattern = "F0[0000dcba]F7".parse().unwrap();
|
||||
// When
|
||||
// Then
|
||||
assert_eq!(
|
||||
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::MAX)),
|
||||
vec![0xf0, 0x0f, 0xf7]
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::MIN)),
|
||||
vec![0xf0, 0x00, 0xf7]
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::new(0.5))),
|
||||
vec![0xf0, 0x08, 0xf7]
|
||||
);
|
||||
assert_eq!(&pattern.to_string(), "F0 [0000 dcba] F7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_variable_nibble_variation() {
|
||||
// Given
|
||||
let pattern: RawMidiPattern = "F0[1111dcba]F7".parse().unwrap();
|
||||
// When
|
||||
// Then
|
||||
assert_eq!(
|
||||
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::MAX)),
|
||||
vec![0xf0, 0xff, 0xf7]
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.match_and_capture(&[0xf0, 0x0ff, 0xf7]),
|
||||
Some(Fraction::new(15, 15))
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::MIN)),
|
||||
vec![0xf0, 0xf0, 0xf7]
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.match_and_capture(&[0xf0, 0x0f0, 0xf7]),
|
||||
Some(Fraction::new(0, 15))
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.to_bytes(AbsoluteValue::Continuous(UnitValue::new(0.5))),
|
||||
vec![0xf0, 0xf8, 0xf7]
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.match_and_capture(&[0xf0, 0x0f8, 0xf7]),
|
||||
Some(Fraction::new(8, 15))
|
||||
);
|
||||
assert_eq!(&pattern.to_string(), "F0 [1111 dcba] F7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_variable_pattern() {
|
||||
let result = "F0[0000dcbaa]F7".parse::<RawMidiPattern>();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn correct_resolution_1() {
|
||||
// Given
|
||||
let pattern: RawMidiPattern = "B0 00 [0nml kjih]".parse().unwrap();
|
||||
// When
|
||||
// Then
|
||||
assert_eq!(pattern.resolution(), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn correct_resolution_2() {
|
||||
// Given
|
||||
let pattern: RawMidiPattern = "B0 00 [0gfe dcba]".parse().unwrap();
|
||||
// When
|
||||
// Then
|
||||
assert_eq!(pattern.resolution(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_pattern() {
|
||||
// Given
|
||||
let pattern: RawMidiPattern = "B0 00 F7".parse().unwrap();
|
||||
// When
|
||||
// Then
|
||||
assert_eq!(pattern.resolution(), 0);
|
||||
assert_eq!(pattern.max_discrete_value(), 0);
|
||||
assert_eq!(pattern.match_and_capture(&[0xf0, 0x0f8, 0xf7]), None);
|
||||
assert_eq!(
|
||||
pattern.match_and_capture(&[0xb0, 0x00, 0xf7]),
|
||||
Some(Fraction::new(0, 0))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_world_fixed_pattern() {
|
||||
// Given
|
||||
let pattern: RawMidiPattern = "F0 0 20 6B 7F 42 02 00 0 2F 7F F7".parse().unwrap();
|
||||
// When
|
||||
// Then
|
||||
assert_eq!(pattern.resolution(), 0);
|
||||
assert_eq!(pattern.max_discrete_value(), 0);
|
||||
assert_eq!(pattern.match_and_capture(&[0xf0, 0x0f8, 0xf7]), None);
|
||||
assert_eq!(
|
||||
pattern.match_and_capture(&[
|
||||
0xF0, 0x0, 0x20, 0x6B, 0x7F, 0x42, 0x2, 0x0, 0x0, 0x2F, 0x7F, 0xF6
|
||||
]),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.match_and_capture(&[
|
||||
0xF0, 0x0, 0x20, 0x6B, 0x7F, 0x42, 0x2, 0x0, 0x0, 0x2F, 0x7F, 0xF7
|
||||
]),
|
||||
Some(Fraction::new(0, 0))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/// Context for source-related functions.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct SourceContext<A> {
|
||||
pub additional_script_input: A,
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use crate::{FeedbackValue, MidiSourceScript, MidiSourceScriptOutcome};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub struct TestMidiSourceScript;
|
||||
|
||||
impl MidiSourceScript<'_> for TestMidiSourceScript {
|
||||
type AdditionalInput = ();
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
_input_value: FeedbackValue,
|
||||
_additional_input: (),
|
||||
) -> Result<MidiSourceScriptOutcome, Cow<'static, str>> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user