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,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))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user