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,40 @@
|
||||
[package]
|
||||
name = "playtime-api"
|
||||
version = "0.1.0"
|
||||
authors = ["Benjamin Klum <benjamin.klum@helgoboss.org>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
# For reusing common sound-related types that conveniently happen to be compatible with REAPER
|
||||
reaper-common-types.workspace = true
|
||||
# For exposing a runtime API within REAPER
|
||||
reaper-low.workspace = true
|
||||
# For being able to use the API macro
|
||||
helgobox-macros.workspace = true
|
||||
# For generating random IDs
|
||||
nanoid.workspace = true
|
||||
# For easier Display impl
|
||||
derive_more.workspace = true
|
||||
# For encoding/decoding a signed matrix value
|
||||
rmp-serde.workspace = true
|
||||
# For encoding/decoding a signed matrix value
|
||||
base64.workspace = true
|
||||
# For proper error types
|
||||
thiserror.workspace = true
|
||||
# For date/time persistence
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
# For better error handling
|
||||
anyhow.workspace = true
|
||||
# For capturing and reporting unknown properties
|
||||
serde_json.workspace = true
|
||||
# For primitive enums
|
||||
strum.workspace = true
|
||||
# For primitive enums
|
||||
num_enum.workspace = true
|
||||
# For UTF-8 paths
|
||||
camino = { workspace = true, features = ["serde1"] }
|
||||
|
||||
[lints.clippy]
|
||||
enum_glob_use = "deny"
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod persistence;
|
||||
pub mod runtime;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
||||
use crate::persistence::{
|
||||
MatrixSequenceColumnMessage, MatrixSequenceEvent, MatrixSequenceMessage,
|
||||
MatrixSequenceRowMessage, MatrixSequenceSlotMessage, MatrixSequenceStartSlotMessage,
|
||||
};
|
||||
use serde::de::{Error, SeqAccess, Visitor};
|
||||
use serde::ser::SerializeTuple;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt::Formatter;
|
||||
|
||||
/// A very compact representation of an event.
|
||||
///
|
||||
/// The compactness is achieved using tuple serialization.
|
||||
///
|
||||
/// Pro:
|
||||
///
|
||||
/// - If used with a CSV serializer, the outcome can be made look as "noise-less" as
|
||||
/// REAPER's MIDI sequence format (newline to separate events, space delimiter, unquoted strings).
|
||||
/// - If used with JSON/Lua serializer, the outcome is valid JSON/Lua while still being compact.
|
||||
/// No need for string embedding (which looks especially bad in JSON due to newline escaping).
|
||||
/// - If used with a binary serializer (e.g. bincode or msgpack), one can achieve a *really* compact
|
||||
/// serialization that also tops REAPER's MIDI sequence format. That will come in handy with
|
||||
/// large undo histories or storage within RPP (in RPPs, we are base64-encoded, so the
|
||||
/// human-readable-text advantage is not present anyway).
|
||||
/// - TODO-high-ms3 Especially the last point could be desirable for MIDI sequences as well.
|
||||
/// Use serde for them, too!
|
||||
///
|
||||
/// Contra:
|
||||
///
|
||||
/// - Not self-describing. However: If embedded in some `Serialize` wrapper that evaluates
|
||||
/// `is_human_readable`, one could switch between this non-descriptive serialization style and a
|
||||
/// (derived) descriptive serialization style. So we can have both if we want.
|
||||
impl Serialize for MatrixSequenceEvent {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
use MatrixSequenceMessage as M;
|
||||
match self.message {
|
||||
M::PanicMatrix => {
|
||||
let mut seq = serializer.serialize_tuple(2)?;
|
||||
seq.serialize_element(&self.pulse_diff)?;
|
||||
seq.serialize_element(&0u8)?;
|
||||
seq.end()
|
||||
}
|
||||
M::StopMatrix => {
|
||||
let mut seq = serializer.serialize_tuple(2)?;
|
||||
seq.serialize_element(&self.pulse_diff)?;
|
||||
seq.serialize_element(&1u8)?;
|
||||
seq.end()
|
||||
}
|
||||
M::PanicColumn(m) => {
|
||||
let mut seq = serializer.serialize_tuple(3)?;
|
||||
seq.serialize_element(&self.pulse_diff)?;
|
||||
seq.serialize_element(&2u8)?;
|
||||
seq.serialize_element(&m.index)?;
|
||||
seq.end()
|
||||
}
|
||||
M::StopColumn(m) => {
|
||||
let mut seq = serializer.serialize_tuple(3)?;
|
||||
seq.serialize_element(&self.pulse_diff)?;
|
||||
seq.serialize_element(&3u8)?;
|
||||
seq.serialize_element(&m.index)?;
|
||||
seq.end()
|
||||
}
|
||||
M::StartScene(m) => {
|
||||
let mut seq = serializer.serialize_tuple(3)?;
|
||||
seq.serialize_element(&self.pulse_diff)?;
|
||||
seq.serialize_element(&4u8)?;
|
||||
seq.serialize_element(&m.index)?;
|
||||
seq.end()
|
||||
}
|
||||
M::PanicSlot(m) => {
|
||||
let mut seq = serializer.serialize_tuple(4)?;
|
||||
seq.serialize_element(&self.pulse_diff)?;
|
||||
seq.serialize_element(&5u8)?;
|
||||
seq.serialize_element(&m.column_index)?;
|
||||
seq.serialize_element(&m.row_index)?;
|
||||
seq.end()
|
||||
}
|
||||
M::StartSlot(m) => {
|
||||
let mut seq = serializer.serialize_tuple(5)?;
|
||||
seq.serialize_element(&self.pulse_diff)?;
|
||||
seq.serialize_element(&6u8)?;
|
||||
seq.serialize_element(&m.column_index)?;
|
||||
seq.serialize_element(&m.row_index)?;
|
||||
seq.serialize_element(&m.velocity)?;
|
||||
seq.end()
|
||||
}
|
||||
|
||||
M::StopSlot(m) => {
|
||||
let mut seq = serializer.serialize_tuple(4)?;
|
||||
seq.serialize_element(&self.pulse_diff)?;
|
||||
seq.serialize_element(&7u8)?;
|
||||
seq.serialize_element(&m.column_index)?;
|
||||
seq.serialize_element(&m.row_index)?;
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MatrixSequenceEventVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for MatrixSequenceEventVisitor {
|
||||
type Value = MatrixSequenceEvent;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
||||
write!(formatter, "a tuple")
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
use MatrixSequenceMessage as M;
|
||||
macro_rules! col(() => {
|
||||
MatrixSequenceColumnMessage {
|
||||
index: seq
|
||||
.next_element()?
|
||||
.ok_or(Error::custom("expected column index"))?
|
||||
}
|
||||
});
|
||||
macro_rules! row(() => {
|
||||
MatrixSequenceRowMessage {
|
||||
index: seq
|
||||
.next_element()?
|
||||
.ok_or(Error::custom("expected row index"))?
|
||||
}
|
||||
});
|
||||
macro_rules! slot(() => {
|
||||
MatrixSequenceSlotMessage {
|
||||
column_index: seq
|
||||
.next_element()?
|
||||
.ok_or(Error::custom("expected slot column index"))?,
|
||||
row_index: seq
|
||||
.next_element()?
|
||||
.ok_or(Error::custom("expected slot row index"))?,
|
||||
}
|
||||
});
|
||||
let pulse_diff: u32 = seq
|
||||
.next_element()?
|
||||
.ok_or(Error::custom("expected pulse diff"))?;
|
||||
let msg_type: u8 = seq
|
||||
.next_element()?
|
||||
.ok_or(Error::custom("expected message type"))?;
|
||||
let message = match msg_type {
|
||||
0 => M::PanicMatrix,
|
||||
1 => M::StopMatrix,
|
||||
2 => M::PanicColumn(col!()),
|
||||
3 => M::StopColumn(col!()),
|
||||
4 => M::StartScene(row!()),
|
||||
5 => M::PanicSlot(slot!()),
|
||||
6 => M::StartSlot(MatrixSequenceStartSlotMessage {
|
||||
column_index: seq
|
||||
.next_element()?
|
||||
.ok_or(Error::custom("expected slot column index"))?,
|
||||
row_index: seq
|
||||
.next_element()?
|
||||
.ok_or(Error::custom("expected slot row index"))?,
|
||||
// Full velocity by default
|
||||
velocity: seq.next_element()?.unwrap_or(1.0),
|
||||
}),
|
||||
7 => M::StopSlot(slot!()),
|
||||
_ => return Err(Error::custom(format!("unknown message type {msg_type}"))),
|
||||
};
|
||||
let event = MatrixSequenceEvent {
|
||||
pulse_diff,
|
||||
message,
|
||||
};
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for MatrixSequenceEvent {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_seq(MatrixSequenceEventVisitor)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Usually we use Protocol Buffers for the runtime app API but there are a few things that are
|
||||
//! not performance-critical and better expressed in a Rust-first manner.
|
||||
use crate::persistence::{ColumnAddress, RowAddress, SlotAddress};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// We don't really need a tagged enum here but it's an easy way to transmit the event as a
|
||||
// JSON object (vs. just a string) ... which is better for some clients. Plus, we might want
|
||||
// to deliver some additional payloads in the future.
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum InfoEvent {
|
||||
Generic(GenericInfoEvent),
|
||||
RecordedMatrixSequence,
|
||||
DiscardedMatrixSequenceBecauseEmpty,
|
||||
RemovedMatrixSequence,
|
||||
WroteMatrixSequenceToArrangement,
|
||||
}
|
||||
|
||||
impl InfoEvent {
|
||||
/// Creates an info event with a generic message. This is displayed as toast in the app.
|
||||
pub fn generic(message: impl Into<String>) -> Self {
|
||||
Self::Generic(GenericInfoEvent {
|
||||
message: message.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates an info event that would ideally be treated like a warning on the app side.
|
||||
pub fn warning(message: impl Into<String>) -> Self {
|
||||
// In the future, this could set a special error flag, so that it could be displayed
|
||||
// in a different way in the app.
|
||||
Self::generic(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
|
||||
pub struct GenericInfoEvent {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||
pub struct SimpleMappingContainer {
|
||||
pub mappings: Vec<SimpleMapping>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||
pub struct SimpleMapping {
|
||||
pub source: SimpleSource,
|
||||
pub target: SimpleMappingTarget,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum SimpleSource {
|
||||
Note(NoteSource),
|
||||
MoreComplicated,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
|
||||
pub struct NoteSource {
|
||||
pub channel: u8,
|
||||
pub number: u8,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum SimpleMappingTarget {
|
||||
TriggerMatrix,
|
||||
TriggerColumn(ColumnAddress),
|
||||
TriggerRow(RowAddress),
|
||||
TriggerSlot(SlotAddress),
|
||||
SmartRecord,
|
||||
EnterSilenceModeOrPlayIgnited,
|
||||
SequencerRecordOnOffState,
|
||||
SequencerPlayOnOffState,
|
||||
TapTempo,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
|
||||
pub struct CellAddress {
|
||||
pub column_index: Option<usize>,
|
||||
pub row_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl CellAddress {
|
||||
pub fn new(column_index: Option<usize>, row_index: Option<usize>) -> Self {
|
||||
Self {
|
||||
column_index,
|
||||
row_index,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matrix() -> Self {
|
||||
Self::new(None, None)
|
||||
}
|
||||
|
||||
pub fn column(column_index: usize) -> Self {
|
||||
Self {
|
||||
column_index: Some(column_index),
|
||||
row_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn row(row_index: usize) -> Self {
|
||||
Self {
|
||||
column_index: None,
|
||||
row_index: Some(row_index),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slot(column_index: usize, row_index: usize) -> Self {
|
||||
Self {
|
||||
column_index: Some(column_index),
|
||||
row_index: Some(row_index),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_slot_address(&self) -> Option<SlotAddress> {
|
||||
Some(SlotAddress::new(self.column_index?, self.row_index?))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use crate::persistence::SlotAddress;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Default, Serialize)]
|
||||
pub struct ControlUnitConfig {
|
||||
#[serde(default)]
|
||||
pub control_units: Vec<ControlUnit>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default, Serialize)]
|
||||
pub struct ControlUnitId(u32);
|
||||
|
||||
impl ControlUnitId {
|
||||
pub fn new(raw: u32) -> Self {
|
||||
Self(raw)
|
||||
}
|
||||
|
||||
pub fn get(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A control unit represents a controller connected to Playtime.
|
||||
///
|
||||
/// While definitely a part of the Playtime domain, control units are **not** managed/persisted by
|
||||
/// Playtime. That's the responsibility of the software that integrates Playtime and provides the
|
||||
/// controller integration (in our case Helgobox with ReaLearn).
|
||||
#[derive(Clone, PartialEq, Debug, Serialize)]
|
||||
pub struct ControlUnit {
|
||||
/// Uniquely identifies the control unit at runtime.
|
||||
///
|
||||
/// In our case (Helgobox/ReaLearn), it's equal to the ReaLearn unit ID.
|
||||
pub id: ControlUnitId,
|
||||
/// A display name which should indicate what connected device we are talking about.
|
||||
pub name: String,
|
||||
/// Color in which the control unit should be visualized in the Playtime matrix.
|
||||
///
|
||||
/// In our case (Helgobox/ReaLearn), the color is in most usage scenarios dictated by the
|
||||
/// global controller definition but should also work without using the "global controller"
|
||||
/// feature (by setting `custom_data.playtime.control_unit.palette_color` in the main
|
||||
/// compartment data).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub palette_color: Option<u32>,
|
||||
/// The top-left column/row which this control unit controls.
|
||||
///
|
||||
/// This will change as the controller scrolls. So it must be changeable by ReaLearn's targets.
|
||||
pub top_left_corner: SlotAddress,
|
||||
/// Both column and row count are fixed.
|
||||
///
|
||||
/// It should be dictated by the ReaLearn main compartment as it's a decision of the main preset
|
||||
/// which area of the controller's grid will be used for slot control.
|
||||
pub column_count: u32,
|
||||
pub row_count: u32,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
mod reaper;
|
||||
pub use reaper::*;
|
||||
|
||||
mod app;
|
||||
pub use app::*;
|
||||
|
||||
mod control_unit;
|
||||
pub use control_unit::*;
|
||||
@@ -0,0 +1,23 @@
|
||||
#![allow(non_snake_case)]
|
||||
use helgobox_macros::reaper_api;
|
||||
|
||||
reaper_api![
|
||||
PlaytimeApi, PlaytimeApiPointers, PlaytimeApiSession, register_playtime_api
|
||||
{
|
||||
/// Finds the first Helgobox instance in the given project that contains a Playtime matrix.
|
||||
///
|
||||
/// If the given project is `null`, it will look in the current project.
|
||||
///
|
||||
/// Returns the instance ID or -1 if none exists.
|
||||
HB_FindFirstPlaytimeHelgoboxInstanceInProject(project: *mut reaper_low::raw::ReaProject) -> std::ffi::c_int;
|
||||
|
||||
/// Creates a new Playtime matrix in the given Helgobox instance.
|
||||
HB_CreateClipMatrix(instance_id: std::ffi::c_int);
|
||||
|
||||
/// Shows or hides the app for the given Helgobox instance and makes sure that the app displays
|
||||
/// Playtime.
|
||||
///
|
||||
/// If necessary, this will also start the app and create a Playtime matrix for the given instance.
|
||||
HB_ShowOrHidePlaytime(instance_id: std::ffi::c_int);
|
||||
}
|
||||
];
|
||||
Reference in New Issue
Block a user