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:
Paul Lipscomb
2026-07-15 17:51:05 -04:00
parent e58f06d9fa
commit 7ecc718f5d
2256 changed files with 11 additions and 5 deletions
@@ -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!()
}
}