Files
virtual-controller/plugin-reaper-relearn/main/lib/helgoboss-learn/src/source/raw_midi.rs
T
Paul Lipscomb 7ecc718f5d 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>
2026-07-15 17:51:05 -04:00

555 lines
16 KiB
Rust

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))
);
}
}