per track volume
This commit is contained in:
parent
70b2f92a8d
commit
7b7ebb8c0e
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
config/
|
|
||||||
collect.txt
|
collect.txt
|
||||||
|
config/
|
||||||
|
target/
|
||||||
4606
Cargo.lock
generated
Normal file
4606
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "3"
|
||||||
|
members = [
|
||||||
|
"audio_engine",
|
||||||
|
"gui",
|
||||||
|
"osc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
bytes = "1.0"
|
||||||
|
futures = "0.3"
|
||||||
|
log = "0.4"
|
||||||
|
osc = { path = "osc" }
|
||||||
|
rosc = "0.10"
|
||||||
|
thiserror = "1"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-util = { version = "0.7", features = ["codec"] }
|
||||||
9
audio_engine/Cargo.lock
generated
9
audio_engine/Cargo.lock
generated
@ -79,6 +79,7 @@ dependencies = [
|
|||||||
"jack",
|
"jack",
|
||||||
"kanal",
|
"kanal",
|
||||||
"log",
|
"log",
|
||||||
|
"osc",
|
||||||
"rosc",
|
"rosc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -519,6 +520,14 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "osc"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"rosc",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.4"
|
version = "0.12.4"
|
||||||
|
|||||||
@ -4,19 +4,21 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytes = "1.0"
|
|
||||||
clap = { version = "4", features = ["derive", "env", "string"] }
|
clap = { version = "4", features = ["derive", "env", "string"] }
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
futures = "0.3"
|
|
||||||
hound = "3.5"
|
hound = "3.5"
|
||||||
jack = "0.13"
|
jack = "0.13"
|
||||||
kanal = "0.1"
|
kanal = "0.1"
|
||||||
log = "0.4"
|
|
||||||
rosc = "0.10"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
simple_logger = "5"
|
simple_logger = "5"
|
||||||
thiserror = "1"
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
tokio-util = { version = "0.7", features = ["codec"] }
|
|
||||||
wmidi = "4"
|
wmidi = "4"
|
||||||
|
|
||||||
|
bytes.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
osc.workspace = true
|
||||||
|
rosc.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
tokio-util.workspace = true
|
||||||
@ -11,7 +11,7 @@ mod looper_error;
|
|||||||
mod metronome;
|
mod metronome;
|
||||||
mod midi;
|
mod midi;
|
||||||
mod notification_handler;
|
mod notification_handler;
|
||||||
mod osc;
|
mod osc_server;
|
||||||
mod persistence_manager;
|
mod persistence_manager;
|
||||||
mod post_record_handler;
|
mod post_record_handler;
|
||||||
mod process_handler;
|
mod process_handler;
|
||||||
@ -38,8 +38,8 @@ use metronome::BufferTiming;
|
|||||||
use metronome::Metronome;
|
use metronome::Metronome;
|
||||||
use notification_handler::JackNotification;
|
use notification_handler::JackNotification;
|
||||||
use notification_handler::NotificationHandler;
|
use notification_handler::NotificationHandler;
|
||||||
use osc::OscController;
|
use osc_server::OscController;
|
||||||
use osc::Osc;
|
use osc_server::Osc;
|
||||||
use persistence_manager::PersistenceManager;
|
use persistence_manager::PersistenceManager;
|
||||||
use persistence_manager::PersistenceManagerController;
|
use persistence_manager::PersistenceManagerController;
|
||||||
use post_record_handler::PostRecordController;
|
use post_record_handler::PostRecordController;
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
use crate::{LooperError, Result, TrackState};
|
use crate::*;
|
||||||
use super::message::OscMessage;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct OscController {
|
pub struct OscController {
|
||||||
sender: kanal::Sender<OscMessage>,
|
sender: kanal::Sender<osc::Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OscController {
|
impl OscController {
|
||||||
pub(crate) fn new(sender: kanal::Sender<OscMessage>) -> Self {
|
pub(crate) fn new(sender: kanal::Sender<osc::Message>) -> Self {
|
||||||
Self { sender }
|
Self { sender }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +16,7 @@ impl OscController {
|
|||||||
row: usize,
|
row: usize,
|
||||||
state: TrackState,
|
state: TrackState,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let message = OscMessage::TrackStateChanged { column, row, state };
|
let message = osc::Message::TrackStateChanged { column, row, state: state.into() };
|
||||||
match self.sender.try_send(message) {
|
match self.sender.try_send(message) {
|
||||||
Ok(true) => Ok(()),
|
Ok(true) => Ok(()),
|
||||||
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||||
@ -31,7 +30,7 @@ impl OscController {
|
|||||||
row: usize,
|
row: usize,
|
||||||
volume: f32,
|
volume: f32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let message = OscMessage::TrackVolumeChanged { column, row, volume };
|
let message = osc::Message::TrackVolumeChanged { column, row, volume };
|
||||||
match self.sender.try_send(message) {
|
match self.sender.try_send(message) {
|
||||||
Ok(true) => Ok(()),
|
Ok(true) => Ok(()),
|
||||||
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||||
@ -40,7 +39,7 @@ impl OscController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected_column_changed(&self, column: usize) -> Result<()> {
|
pub fn selected_column_changed(&self, column: usize) -> Result<()> {
|
||||||
let message = OscMessage::SelectedColumnChanged { column };
|
let message = osc::Message::SelectedColumnChanged { column };
|
||||||
match self.sender.try_send(message) {
|
match self.sender.try_send(message) {
|
||||||
Ok(true) => Ok(()),
|
Ok(true) => Ok(()),
|
||||||
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||||
@ -49,7 +48,7 @@ impl OscController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected_row_changed(&self, row: usize) -> Result<()> {
|
pub fn selected_row_changed(&self, row: usize) -> Result<()> {
|
||||||
let message = OscMessage::SelectedRowChanged { row };
|
let message = osc::Message::SelectedRowChanged { row };
|
||||||
match self.sender.try_send(message) {
|
match self.sender.try_send(message) {
|
||||||
Ok(true) => Ok(()),
|
Ok(true) => Ok(()),
|
||||||
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||||
@ -1,5 +1,4 @@
|
|||||||
mod controller;
|
mod controller;
|
||||||
mod message;
|
|
||||||
mod platform;
|
mod platform;
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
@ -1,33 +1,25 @@
|
|||||||
use crate::Result;
|
use crate::*;
|
||||||
use super::{
|
|
||||||
controller::OscController,
|
|
||||||
message::{OscMessage, ShadowState},
|
|
||||||
platform::{create_listener, PlatformListener, PlatformStream},
|
|
||||||
};
|
|
||||||
use bytes::Bytes;
|
|
||||||
use futures::SinkExt;
|
use futures::SinkExt;
|
||||||
use tokio::sync::broadcast;
|
|
||||||
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
|
||||||
|
|
||||||
const CLIENT_BUFFER_SIZE: usize = 32;
|
const CLIENT_BUFFER_SIZE: usize = 32;
|
||||||
|
|
||||||
pub struct Osc {
|
pub struct Osc {
|
||||||
receiver: kanal::AsyncReceiver<OscMessage>,
|
receiver: kanal::AsyncReceiver<osc::Message>,
|
||||||
listener: PlatformListener,
|
listener: osc_server::platform::PlatformListener,
|
||||||
broadcaster: broadcast::Sender<OscMessage>,
|
broadcaster: tokio::sync::broadcast::Sender<osc::Message>,
|
||||||
shadow_state: ShadowState,
|
shadow_state: osc::State,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Osc {
|
impl Osc {
|
||||||
/// Create new OSC server and controller with same interface as before
|
/// Create new OSC server and controller with same interface as before
|
||||||
pub async fn new(socket_path: &str) -> Result<(Self, OscController)> {
|
pub async fn new(socket_path: &str) -> Result<(Self, OscController)> {
|
||||||
// Create platform listener (server)
|
// Create platform listener (server)
|
||||||
let listener = create_listener(socket_path).await?;
|
let listener = osc_server::platform::create_listener(socket_path).await?;
|
||||||
|
|
||||||
// Create communication channels
|
// Create communication channels
|
||||||
let (sender, receiver) = kanal::bounded(64);
|
let (sender, receiver) = kanal::bounded(64);
|
||||||
let receiver = receiver.to_async();
|
let receiver = receiver.to_async();
|
||||||
let (broadcaster, _) = broadcast::channel(CLIENT_BUFFER_SIZE);
|
let (broadcaster, _) = tokio::sync::broadcast::channel(CLIENT_BUFFER_SIZE);
|
||||||
|
|
||||||
let controller = OscController::new(sender);
|
let controller = OscController::new(sender);
|
||||||
|
|
||||||
@ -35,7 +27,7 @@ impl Osc {
|
|||||||
receiver,
|
receiver,
|
||||||
listener,
|
listener,
|
||||||
broadcaster,
|
broadcaster,
|
||||||
shadow_state: ShadowState::new(5, 5), // Default 5x5 matrix
|
shadow_state: osc::State::new(5, 5), // Default 5x5 matrix
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((server, controller))
|
Ok((server, controller))
|
||||||
@ -68,12 +60,12 @@ impl Osc {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_client_task(&self, stream: PlatformStream) {
|
fn spawn_client_task(&self, stream: osc_server::platform::PlatformStream) {
|
||||||
let mut receiver = self.broadcaster.subscribe();
|
let mut receiver = self.broadcaster.subscribe();
|
||||||
let state_dump = self.shadow_state.create_state_dump();
|
let state_dump = self.shadow_state.create_state_dump();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut framed = Framed::new(stream, LengthDelimitedCodec::new());
|
let mut framed = tokio_util::codec::Framed::new(stream, tokio_util::codec::LengthDelimitedCodec::new());
|
||||||
|
|
||||||
// Send current state dump immediately
|
// Send current state dump immediately
|
||||||
for msg in state_dump {
|
for msg in state_dump {
|
||||||
@ -92,8 +84,8 @@ impl Osc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn send_osc_message(
|
async fn send_osc_message(
|
||||||
framed: &mut Framed<PlatformStream, LengthDelimitedCodec>,
|
framed: &mut tokio_util::codec::Framed<osc_server::platform::PlatformStream, tokio_util::codec::LengthDelimitedCodec>,
|
||||||
message: OscMessage,
|
message: osc::Message,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let osc_packet = Self::message_to_osc_packet(message)?;
|
let osc_packet = Self::message_to_osc_packet(message)?;
|
||||||
|
|
||||||
@ -101,25 +93,24 @@ impl Osc {
|
|||||||
.map_err(|_| crate::LooperError::StateSave(std::panic::Location::caller()))?;
|
.map_err(|_| crate::LooperError::StateSave(std::panic::Location::caller()))?;
|
||||||
|
|
||||||
framed
|
framed
|
||||||
.send(Bytes::from(osc_bytes))
|
.send(bytes::Bytes::from(osc_bytes))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| crate::LooperError::StateSave(std::panic::Location::caller()))?;
|
.map_err(|_| crate::LooperError::StateSave(std::panic::Location::caller()))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn message_to_osc_packet(message: OscMessage) -> Result<rosc::OscPacket> {
|
fn message_to_osc_packet(message: osc::Message) -> Result<rosc::OscPacket> {
|
||||||
match message {
|
match message {
|
||||||
OscMessage::TrackStateChanged { column, row, state } => {
|
osc::Message::TrackStateChanged { column, row, state } => {
|
||||||
let osc_state = Self::track_state_to_osc_string(state);
|
|
||||||
let address = format!("/looper/cell/{}/{}/state", column + 1, row + 1); // 1-based indexing
|
let address = format!("/looper/cell/{}/{}/state", column + 1, row + 1); // 1-based indexing
|
||||||
|
|
||||||
Ok(rosc::OscPacket::Message(rosc::OscMessage {
|
Ok(rosc::OscPacket::Message(rosc::OscMessage {
|
||||||
addr: address,
|
addr: address,
|
||||||
args: vec![rosc::OscType::String(osc_state)],
|
args: vec![rosc::OscType::String(state.to_string())],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
OscMessage::TrackVolumeChanged { column, row, volume } => {
|
osc::Message::TrackVolumeChanged { column, row, volume } => {
|
||||||
let address = format!("/looper/cell/{}/{}/volume", column + 1, row + 1); // 1-based indexing
|
let address = format!("/looper/cell/{}/{}/volume", column + 1, row + 1); // 1-based indexing
|
||||||
|
|
||||||
Ok(rosc::OscPacket::Message(rosc::OscMessage {
|
Ok(rosc::OscPacket::Message(rosc::OscMessage {
|
||||||
@ -127,7 +118,7 @@ impl Osc {
|
|||||||
args: vec![rosc::OscType::Float(volume)],
|
args: vec![rosc::OscType::Float(volume)],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
OscMessage::SelectedColumnChanged { column } => {
|
osc::Message::SelectedColumnChanged { column } => {
|
||||||
let address = "/looper/selected/column".to_string();
|
let address = "/looper/selected/column".to_string();
|
||||||
|
|
||||||
Ok(rosc::OscPacket::Message(rosc::OscMessage {
|
Ok(rosc::OscPacket::Message(rosc::OscMessage {
|
||||||
@ -135,7 +126,7 @@ impl Osc {
|
|||||||
args: vec![rosc::OscType::Int((column + 1) as i32)], // 1-based indexing
|
args: vec![rosc::OscType::Int((column + 1) as i32)], // 1-based indexing
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
OscMessage::SelectedRowChanged { row } => {
|
osc::Message::SelectedRowChanged { row } => {
|
||||||
let address = "/looper/selected/row".to_string();
|
let address = "/looper/selected/row".to_string();
|
||||||
|
|
||||||
Ok(rosc::OscPacket::Message(rosc::OscMessage {
|
Ok(rosc::OscPacket::Message(rosc::OscMessage {
|
||||||
@ -145,14 +136,4 @@ impl Osc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn track_state_to_osc_string(state: crate::TrackState) -> String {
|
|
||||||
match state {
|
|
||||||
crate::TrackState::Empty => "empty".to_string(),
|
|
||||||
crate::TrackState::Idle => "ready".to_string(),
|
|
||||||
crate::TrackState::Recording { .. } => "recording".to_string(),
|
|
||||||
crate::TrackState::RecordingAutoStop { .. } => "recording".to_string(),
|
|
||||||
crate::TrackState::Playing => "playing".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -328,3 +328,15 @@ impl Track {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Into<osc::TrackState> for TrackState {
|
||||||
|
fn into(self) -> osc::TrackState {
|
||||||
|
match self {
|
||||||
|
TrackState::Empty => osc::TrackState::Empty,
|
||||||
|
TrackState::Idle => osc::TrackState::Idle,
|
||||||
|
TrackState::Recording { .. } => osc::TrackState::Recording,
|
||||||
|
TrackState::RecordingAutoStop { .. } => osc::TrackState::Recording,
|
||||||
|
TrackState::Playing => osc::TrackState::Playing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
gui/Cargo.lock
generated
9
gui/Cargo.lock
generated
@ -1097,6 +1097,7 @@ dependencies = [
|
|||||||
"egui",
|
"egui",
|
||||||
"futures",
|
"futures",
|
||||||
"log",
|
"log",
|
||||||
|
"osc",
|
||||||
"rosc",
|
"rosc",
|
||||||
"simple_logger",
|
"simple_logger",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -2309,6 +2310,14 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "osc"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"rosc",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owned_ttf_parser"
|
name = "owned_ttf_parser"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fcb-looper-gui"
|
name = "gui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bytes = "1.0"
|
|
||||||
eframe = { version = "0.29", default-features = true, features = [ "default_fonts", "glow" ] }
|
eframe = { version = "0.29", default-features = true, features = [ "default_fonts", "glow" ] }
|
||||||
egui = "0.29"
|
egui = "0.29"
|
||||||
futures = "0.3"
|
|
||||||
log = "0.4"
|
|
||||||
rosc = "0.10"
|
|
||||||
simple_logger = "5"
|
simple_logger = "5"
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
tokio-util = { version = "0.7", features = ["codec"] }
|
bytes.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
osc.workspace = true
|
||||||
|
rosc.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
tokio-util.workspace = true
|
||||||
@ -1,72 +0,0 @@
|
|||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum TrackState {
|
|
||||||
Empty,
|
|
||||||
Ready,
|
|
||||||
Recording,
|
|
||||||
Playing,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TrackState {
|
|
||||||
pub fn display_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
TrackState::Empty => "---",
|
|
||||||
TrackState::Ready => "READY",
|
|
||||||
TrackState::Recording => "REC",
|
|
||||||
TrackState::Playing => "PLAY",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct ColumnState {
|
|
||||||
pub track_states: Vec<TrackState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColumnState {
|
|
||||||
pub fn new(rows: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
track_states: vec![TrackState::Empty; rows],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct DisplayState {
|
|
||||||
pub selected_column: usize, // 0-based internally
|
|
||||||
pub selected_row: usize, // 0-based internally
|
|
||||||
pub column_states: Vec<ColumnState>,
|
|
||||||
pub columns: usize,
|
|
||||||
pub rows: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisplayState {
|
|
||||||
pub fn new(columns: usize, rows: usize) -> Self {
|
|
||||||
let column_states = (0..columns).map(|_| ColumnState::new(rows)).collect();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
selected_column: 0,
|
|
||||||
selected_row: 0,
|
|
||||||
column_states,
|
|
||||||
columns,
|
|
||||||
rows,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_track_state(&mut self, column: usize, row: usize, state: TrackState) {
|
|
||||||
if column < self.columns && row < self.rows {
|
|
||||||
self.column_states[column].track_states[row] = state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_selected_column(&mut self, column: usize) {
|
|
||||||
if column < self.columns {
|
|
||||||
self.selected_column = column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_selected_row(&mut self, row: usize) {
|
|
||||||
if row < self.rows {
|
|
||||||
self.selected_row = row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +1,23 @@
|
|||||||
mod display_state;
|
mod osc_client;
|
||||||
mod osc;
|
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use display_state::ColumnState;
|
|
||||||
use display_state::DisplayState;
|
trait DisplayStr {
|
||||||
use display_state::TrackState;
|
fn display_str(&self) -> &'static str;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayStr for osc::TrackState {
|
||||||
|
fn display_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
osc::TrackState::Empty => "---",
|
||||||
|
osc::TrackState::Idle => "READY",
|
||||||
|
osc::TrackState::Recording => "REC",
|
||||||
|
osc::TrackState::Playing => "PLAY",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
@ -17,12 +29,12 @@ async fn main() -> Result<()> {
|
|||||||
// Logger
|
// Logger
|
||||||
simple_logger::SimpleLogger::new()
|
simple_logger::SimpleLogger::new()
|
||||||
.with_level(log::LevelFilter::Error)
|
.with_level(log::LevelFilter::Error)
|
||||||
.with_module_level("fcb_looper_gui::osc::client", log::LevelFilter::Off)
|
.with_module_level("gui::osc_client", log::LevelFilter::Off)
|
||||||
.init()
|
.init()
|
||||||
.expect("Could not initialize logger");
|
.expect("Could not initialize logger");
|
||||||
|
|
||||||
// Create OSC client
|
// Create OSC client
|
||||||
let osc_client = osc::Client::new(socket_path, columns, rows)?;
|
let osc_client = osc_client::Client::new(socket_path, columns, rows)?;
|
||||||
let state_receiver = osc_client.state_receiver();
|
let state_receiver = osc_client.state_receiver();
|
||||||
|
|
||||||
// Spawn OSC receiver thread on tokio runtime
|
// Spawn OSC receiver thread on tokio runtime
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
use crate::display_state::{DisplayState, TrackState};
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use rosc::{OscMessage, OscPacket, OscType};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum StateUpdate {
|
|
||||||
TrackStateChanged {
|
|
||||||
column: usize, // 0-based
|
|
||||||
row: usize, // 0-based
|
|
||||||
state: TrackState,
|
|
||||||
},
|
|
||||||
SelectedColumnChanged {
|
|
||||||
column: usize, // 0-based
|
|
||||||
},
|
|
||||||
SelectedRowChanged {
|
|
||||||
row: usize, // 0-based
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StateUpdate {
|
|
||||||
pub fn from_osc_packet(packet: OscPacket) -> Result<Option<Self>> {
|
|
||||||
match packet {
|
|
||||||
OscPacket::Message(msg) => Self::from_osc_message(msg),
|
|
||||||
OscPacket::Bundle(_) => {
|
|
||||||
// For now, we don't handle bundles
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_to_state(&self, state: &mut DisplayState) {
|
|
||||||
match self {
|
|
||||||
StateUpdate::TrackStateChanged {
|
|
||||||
column,
|
|
||||||
row,
|
|
||||||
state: track_state,
|
|
||||||
} => {
|
|
||||||
state.update_track_state(*column, *row, track_state.clone());
|
|
||||||
}
|
|
||||||
StateUpdate::SelectedColumnChanged { column } => {
|
|
||||||
state.update_selected_column(*column);
|
|
||||||
}
|
|
||||||
StateUpdate::SelectedRowChanged { row } => {
|
|
||||||
state.update_selected_row(*row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_osc_message(msg: OscMessage) -> Result<Option<Self>> {
|
|
||||||
let OscMessage { addr, args } = msg;
|
|
||||||
|
|
||||||
if addr.starts_with("/looper/cell/") && addr.ends_with("/state") {
|
|
||||||
// Parse: /looper/cell/{column}/{row}/state
|
|
||||||
let parts: Vec<&str> = addr.split('/').collect();
|
|
||||||
if parts.len() != 6 {
|
|
||||||
return Err(anyhow!("Invalid cell state address format: {}", addr));
|
|
||||||
}
|
|
||||||
|
|
||||||
let column: usize = parts[3].parse::<usize>()? - 1; // Convert to 0-based
|
|
||||||
let row: usize = parts[4].parse::<usize>()? - 1; // Convert to 0-based
|
|
||||||
|
|
||||||
if let Some(OscType::String(state_str)) = args.first() {
|
|
||||||
let state = Self::track_state_from_osc_string(state_str);
|
|
||||||
return Ok(Some(StateUpdate::TrackStateChanged { column, row, state }));
|
|
||||||
}
|
|
||||||
} else if addr == "/looper/selected/column" {
|
|
||||||
if let Some(OscType::Int(column_1based)) = args.first() {
|
|
||||||
let column = (*column_1based as usize).saturating_sub(1); // Convert to 0-based
|
|
||||||
return Ok(Some(StateUpdate::SelectedColumnChanged { column }));
|
|
||||||
}
|
|
||||||
} else if addr == "/looper/selected/row" {
|
|
||||||
if let Some(OscType::Int(row_1based)) = args.first() {
|
|
||||||
let row = (*row_1based as usize).saturating_sub(1); // Convert to 0-based
|
|
||||||
return Ok(Some(StateUpdate::SelectedRowChanged { row }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown or unsupported message
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn track_state_from_osc_string(s: &str) -> TrackState {
|
|
||||||
match s {
|
|
||||||
"empty" => TrackState::Empty,
|
|
||||||
"ready" => TrackState::Ready,
|
|
||||||
"recording" => TrackState::Recording,
|
|
||||||
"playing" => TrackState::Playing,
|
|
||||||
_ => TrackState::Empty, // Default fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
mod client;
|
|
||||||
mod message;
|
|
||||||
|
|
||||||
pub use client::Client;
|
|
||||||
@ -1,10 +1,8 @@
|
|||||||
use anyhow::{anyhow, Result};
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
||||||
|
|
||||||
use super::message::*;
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@ -19,24 +17,30 @@ type PlatformStream = NamedPipeClient;
|
|||||||
|
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
socket_path: String,
|
socket_path: String,
|
||||||
state_sender: watch::Sender<DisplayState>,
|
state_sender: watch::Sender<osc::State>,
|
||||||
state_receiver: watch::Receiver<DisplayState>,
|
state_receiver: watch::Receiver<osc::State>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn new(socket_path: &str, columns: usize, rows: usize) -> Result<Self> {
|
pub fn new(socket_path: &str, columns: usize, rows: usize) -> Result<Self> {
|
||||||
let initial_state = DisplayState::new(columns, rows);
|
let initial_state = osc::State::new(columns, rows);
|
||||||
// Test data
|
// Test data
|
||||||
let initial_state = {
|
let initial_state = {
|
||||||
let mut initial_state = initial_state;
|
let mut initial_state = initial_state;
|
||||||
initial_state.selected_column = 2;
|
initial_state.selected_column = 2;
|
||||||
initial_state.selected_row = 3;
|
initial_state.selected_row = 3;
|
||||||
initial_state.column_states[0].track_states[0] = TrackState::Playing;
|
initial_state.cells[0][0].state = osc::TrackState::Playing;
|
||||||
initial_state.column_states[0].track_states[1] = TrackState::Ready;
|
initial_state.cells[0][0].volume = 0.3;
|
||||||
initial_state.column_states[1].track_states[0] = TrackState::Ready;
|
initial_state.cells[0][1].state = osc::TrackState::Idle;
|
||||||
initial_state.column_states[1].track_states[1] = TrackState::Playing;
|
initial_state.cells[0][1].volume = 1.0;
|
||||||
initial_state.column_states[2].track_states[0] = TrackState::Ready;
|
initial_state.cells[1][0].state = osc::TrackState::Idle;
|
||||||
initial_state.column_states[2].track_states[3] = TrackState::Recording;
|
initial_state.cells[1][0].volume = 0.9;
|
||||||
|
initial_state.cells[1][1].state = osc::TrackState::Playing;
|
||||||
|
initial_state.cells[1][1].volume = 0.8;
|
||||||
|
initial_state.cells[2][0].state = osc::TrackState::Idle;
|
||||||
|
initial_state.cells[2][0].volume = 0.7;
|
||||||
|
initial_state.cells[2][3].state = osc::TrackState::Recording;
|
||||||
|
initial_state.cells[2][3].volume = 0.6;
|
||||||
|
|
||||||
initial_state
|
initial_state
|
||||||
};
|
};
|
||||||
@ -49,7 +53,7 @@ impl Client {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state_receiver(&self) -> watch::Receiver<DisplayState> {
|
pub fn state_receiver(&self) -> watch::Receiver<osc::State> {
|
||||||
self.state_receiver.clone()
|
self.state_receiver.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,11 +86,11 @@ impl Client {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_osc_data(&self, data: Bytes, current_state: &mut DisplayState) -> Result<()> {
|
async fn handle_osc_data(&self, data: Bytes, current_state: &mut osc::State) -> Result<()> {
|
||||||
let (_, osc_packet) = rosc::decoder::decode_udp(&data)
|
let (_, osc_packet) = rosc::decoder::decode_udp(&data)
|
||||||
.map_err(|e| anyhow!("Failed to decode OSC packet: {}", e))?;
|
.map_err(|e| anyhow!("Failed to decode OSC packet: {}", e))?;
|
||||||
|
|
||||||
if let Some(update) = StateUpdate::from_osc_packet(osc_packet)? {
|
if let Some(update) = osc::Message::from_osc_packet(osc_packet)? {
|
||||||
update.apply_to_state(current_state);
|
update.apply_to_state(current_state);
|
||||||
|
|
||||||
// Send updated state through watch channel
|
// Send updated state through watch channel
|
||||||
@ -1,11 +1,9 @@
|
|||||||
use crate::*;
|
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
state_receiver: tokio::sync::watch::Receiver<DisplayState>,
|
state_receiver: tokio::sync::watch::Receiver<osc::State>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(state_receiver: tokio::sync::watch::Receiver<DisplayState>) -> Self {
|
pub fn new(state_receiver: tokio::sync::watch::Receiver<osc::State>) -> Self {
|
||||||
Self { state_receiver }
|
Self { state_receiver }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -20,7 +18,7 @@ impl eframe::App for App {
|
|||||||
});
|
});
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
ui.columns(state.columns, |ui| {
|
ui.columns(state.columns, |ui| {
|
||||||
for (i, column) in state.column_states.iter().enumerate() {
|
for (i, column) in state.cells.iter().enumerate() {
|
||||||
let selected_row = if i == state.selected_column {
|
let selected_row = if i == state.selected_column {
|
||||||
Some(state.selected_row)
|
Some(state.selected_row)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
use super::ui_rows_ext::UiRowsExt;
|
use super::ui_rows_ext::UiRowsExt;
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
pub struct Column<'a> {
|
pub struct Column<'a> {
|
||||||
state: &'a ColumnState,
|
state: &'a Vec<osc::Track>,
|
||||||
selected_row: Option<usize>,
|
selected_row: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Column<'a> {
|
impl<'a> Column<'a> {
|
||||||
pub fn new(state: &'a ColumnState, selected_row: Option<usize>) -> Self {
|
pub fn new(state: &'a Vec<osc::Track>, selected_row: Option<usize>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state,
|
state,
|
||||||
selected_row,
|
selected_row,
|
||||||
@ -17,8 +15,8 @@ impl<'a> Column<'a> {
|
|||||||
|
|
||||||
impl<'a> egui::Widget for Column<'a> {
|
impl<'a> egui::Widget for Column<'a> {
|
||||||
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
||||||
ui.rows(self.state.track_states.len(), |ui| {
|
ui.rows(self.state.len(), |ui| {
|
||||||
for (i, track) in self.state.track_states.iter().enumerate() {
|
for (i, track) in self.state.iter().enumerate() {
|
||||||
ui[i].add(super::Track::new(track, Some(i) == self.selected_row));
|
ui[i].add(super::Track::new(track, Some(i) == self.selected_row));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,31 +1,31 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub struct Track<'a> {
|
pub struct Track<'a> {
|
||||||
state: &'a TrackState,
|
state: &'a osc::Track,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Track<'a> {
|
impl<'a> Track<'a> {
|
||||||
pub fn new(state: &'a TrackState, selected: bool) -> Self {
|
pub fn new(state: &'a osc::Track, selected: bool) -> Self {
|
||||||
Self { state, selected }
|
Self { state, selected }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_colors(&self) -> (egui::Color32, egui::Color32) {
|
fn get_colors(&self) -> (egui::Color32, egui::Color32) {
|
||||||
// Returns (background_color, text_color)
|
// Returns (background_color, text_color)
|
||||||
let (dark, bright) = match self.state {
|
let (dark, bright) = match self.state.state {
|
||||||
TrackState::Empty => (
|
osc::TrackState::Empty => (
|
||||||
egui::Color32::from_rgb(26, 26, 26), // #1a1a1a
|
egui::Color32::from_rgb(26, 26, 26), // #1a1a1a
|
||||||
egui::Color32::from_rgb(102, 102, 102), // #666666
|
egui::Color32::from_rgb(102, 102, 102), // #666666
|
||||||
),
|
),
|
||||||
TrackState::Ready => (
|
osc::TrackState::Idle => (
|
||||||
egui::Color32::from_rgb(10, 48, 32), // #0a3020
|
egui::Color32::from_rgb(10, 48, 32), // #0a3020
|
||||||
egui::Color32::from_rgb(0, 170, 102), // #00aa66
|
egui::Color32::from_rgb(0, 170, 102), // #00aa66
|
||||||
),
|
),
|
||||||
TrackState::Playing => (
|
osc::TrackState::Playing => (
|
||||||
egui::Color32::from_rgb(0, 42, 64), // #002a40
|
egui::Color32::from_rgb(0, 42, 64), // #002a40
|
||||||
egui::Color32::from_rgb(0, 136, 204), // #0088cc
|
egui::Color32::from_rgb(0, 136, 204), // #0088cc
|
||||||
),
|
),
|
||||||
TrackState::Recording => (
|
osc::TrackState::Recording => (
|
||||||
egui::Color32::from_rgb(68, 10, 10), // #440a0a
|
egui::Color32::from_rgb(68, 10, 10), // #440a0a
|
||||||
egui::Color32::from_rgb(204, 68, 68), // #cc4444
|
egui::Color32::from_rgb(204, 68, 68), // #cc4444
|
||||||
),
|
),
|
||||||
@ -47,7 +47,7 @@ impl<'a> egui::Widget for Track<'a> {
|
|||||||
|
|
||||||
let (bg_color, fg_color) = self.get_colors();
|
let (bg_color, fg_color) = self.get_colors();
|
||||||
|
|
||||||
egui::Frame::none()
|
let frame_response = egui::Frame::none()
|
||||||
.fill(bg_color)
|
.fill(bg_color)
|
||||||
.rounding(egui::Rounding::same(4.0 * sf))
|
.rounding(egui::Rounding::same(4.0 * sf))
|
||||||
.stroke(egui::Stroke::new(2.5 * sf, fg_color))
|
.stroke(egui::Stroke::new(2.5 * sf, fg_color))
|
||||||
@ -55,12 +55,55 @@ impl<'a> egui::Widget for Track<'a> {
|
|||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.centered_and_justified(|ui| {
|
ui.centered_and_justified(|ui| {
|
||||||
ui.add(egui::Label::new(
|
ui.add(egui::Label::new(
|
||||||
egui::RichText::new(self.state.display_str())
|
egui::RichText::new(self.state.state.display_str())
|
||||||
.font(font_id)
|
.font(font_id)
|
||||||
.color(fg_color),
|
.color(fg_color),
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
.response
|
|
||||||
|
// Draw volume bar overlay at the bottom
|
||||||
|
let volume_bar_height = 2.0 * sf;
|
||||||
|
let margin = 4.0 * sf;
|
||||||
|
let frame_rect = frame_response.response.rect;
|
||||||
|
|
||||||
|
// Calculate volume bar position (bottom of the frame, inside the margins)
|
||||||
|
let inner_rect = frame_rect.shrink(5.0 * sf); // Match the outer_margin
|
||||||
|
let volume_bar_rect = egui::Rect::from_min_max(
|
||||||
|
egui::pos2(
|
||||||
|
inner_rect.min.x + margin,
|
||||||
|
inner_rect.max.y - volume_bar_height - margin
|
||||||
|
),
|
||||||
|
egui::pos2(
|
||||||
|
inner_rect.max.x - margin,
|
||||||
|
inner_rect.max.y - margin
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw volume bar background
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
volume_bar_rect,
|
||||||
|
egui::Rounding::same(1.0 * sf),
|
||||||
|
egui::Color32::from_rgb(51, 51, 51) // #333333
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw volume bar fill (only if volume > 0)
|
||||||
|
if self.state.volume > 0.0 {
|
||||||
|
let fill_width = volume_bar_rect.width() * self.state.volume;
|
||||||
|
if fill_width > 0.0 {
|
||||||
|
let fill_rect = egui::Rect::from_min_size(
|
||||||
|
volume_bar_rect.min,
|
||||||
|
egui::vec2(fill_width, volume_bar_rect.height())
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
fill_rect,
|
||||||
|
egui::Rounding::same(1.0 * sf),
|
||||||
|
egui::Color32::from_rgb(0, 255, 136) // #00ff88
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame_response.response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2
osc/.gitignore
vendored
Normal file
2
osc/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
10
osc/Cargo.toml
Normal file
10
osc/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "osc"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
strum = { version = "0.23", features = ["derive"] }
|
||||||
|
|
||||||
|
rosc.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
17
osc/src/error.rs
Normal file
17
osc/src/error.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Failed to parse osc address")]
|
||||||
|
AddressParseError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AddressParseResult<T> {
|
||||||
|
fn address_part_result(self, context: &'static str) -> Result<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <T> AddressParseResult <T> for std::result::Result<T, std::num::ParseIntError> {
|
||||||
|
fn address_part_result(self, context: &'static str) -> Result<T> {
|
||||||
|
self.map_err(|e| Error::AddressParseError(format!("failed to parse number in address part {}: {}", context, e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
11
osc/src/lib.rs
Normal file
11
osc/src/lib.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
mod error;
|
||||||
|
mod message;
|
||||||
|
mod state;
|
||||||
|
|
||||||
|
use error::AddressParseResult;
|
||||||
|
pub use error::Error;
|
||||||
|
pub use error::Result;
|
||||||
|
pub use message::Message;
|
||||||
|
pub use state::State;
|
||||||
|
pub use state::Track;
|
||||||
|
pub use state::TrackState;
|
||||||
101
osc/src/message.rs
Normal file
101
osc/src/message.rs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Message {
|
||||||
|
TrackStateChanged {
|
||||||
|
column: usize,
|
||||||
|
row: usize,
|
||||||
|
state: TrackState,
|
||||||
|
},
|
||||||
|
TrackVolumeChanged {
|
||||||
|
column: usize,
|
||||||
|
row: usize,
|
||||||
|
volume: f32,
|
||||||
|
},
|
||||||
|
SelectedColumnChanged {
|
||||||
|
column: usize,
|
||||||
|
},
|
||||||
|
SelectedRowChanged {
|
||||||
|
row: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
pub fn from_osc_packet(packet: rosc::OscPacket) -> Result<Option<Self>> {
|
||||||
|
match packet {
|
||||||
|
rosc::OscPacket::Message(msg) => Self::from_osc_message(msg),
|
||||||
|
rosc::OscPacket::Bundle(_) => {
|
||||||
|
// For now, we don't handle bundles
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_to_state(&self, state: &mut State) {
|
||||||
|
match self {
|
||||||
|
Message::TrackStateChanged {
|
||||||
|
column,
|
||||||
|
row,
|
||||||
|
state: track_state,
|
||||||
|
} => {
|
||||||
|
state.set_track_state(*column, *row, track_state.clone());
|
||||||
|
}
|
||||||
|
Message::SelectedColumnChanged { column } => {
|
||||||
|
state.set_selected_column(*column);
|
||||||
|
}
|
||||||
|
Message::SelectedRowChanged { row } => {
|
||||||
|
state.set_selected_row(*row);
|
||||||
|
}
|
||||||
|
Message::TrackVolumeChanged {
|
||||||
|
column,
|
||||||
|
row,
|
||||||
|
volume,
|
||||||
|
} => {
|
||||||
|
state.set_track_volume(*column, *row, *volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_osc_message(msg: rosc::OscMessage) -> Result<Option<Self>> {
|
||||||
|
let rosc::OscMessage { addr, args } = msg;
|
||||||
|
|
||||||
|
if addr.starts_with("/looper/cell/") && addr.ends_with("/state") {
|
||||||
|
// Parse: /looper/cell/{column}/{row}/state
|
||||||
|
let parts: Vec<&str> = addr.split('/').collect();
|
||||||
|
if parts.len() != 6 {
|
||||||
|
return Err(Error::AddressParseError(addr));
|
||||||
|
}
|
||||||
|
|
||||||
|
let column: usize = parts[3].parse::<usize>().address_part_result("column")? - 1; // Convert to 0-based
|
||||||
|
let row: usize = parts[4].parse::<usize>().address_part_result("row")? - 1; // Convert to 0-based
|
||||||
|
|
||||||
|
if let Some(rosc::OscType::String(state_str)) = args.first() {
|
||||||
|
let state = Self::track_state_from_osc_string(state_str);
|
||||||
|
return Ok(Some(Message::TrackStateChanged { column, row, state }));
|
||||||
|
}
|
||||||
|
} else if addr == "/looper/selected/column" {
|
||||||
|
if let Some(rosc::OscType::Int(column_1based)) = args.first() {
|
||||||
|
let column = (*column_1based as usize).saturating_sub(1); // Convert to 0-based
|
||||||
|
return Ok(Some(Message::SelectedColumnChanged { column }));
|
||||||
|
}
|
||||||
|
} else if addr == "/looper/selected/row" {
|
||||||
|
if let Some(rosc::OscType::Int(row_1based)) = args.first() {
|
||||||
|
let row = (*row_1based as usize).saturating_sub(1); // Convert to 0-based
|
||||||
|
return Ok(Some(Message::SelectedRowChanged { row }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown or unsupported message
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track_state_from_osc_string(s: &str) -> TrackState {
|
||||||
|
match s {
|
||||||
|
"empty" => TrackState::Empty,
|
||||||
|
"idle" => TrackState::Idle,
|
||||||
|
"recording" => TrackState::Recording,
|
||||||
|
"playing" => TrackState::Playing,
|
||||||
|
_ => TrackState::Empty, // Default fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,33 +1,23 @@
|
|||||||
use crate::TrackState;
|
use strum::Display;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
use crate::*;
|
||||||
pub(crate) enum OscMessage {
|
|
||||||
TrackStateChanged {
|
#[derive(Clone, Debug, Display, PartialEq)]
|
||||||
column: usize,
|
pub enum TrackState {
|
||||||
row: usize,
|
Empty,
|
||||||
state: TrackState,
|
Idle,
|
||||||
},
|
Playing,
|
||||||
TrackVolumeChanged {
|
Recording,
|
||||||
column: usize,
|
|
||||||
row: usize,
|
|
||||||
volume: f32,
|
|
||||||
},
|
|
||||||
SelectedColumnChanged {
|
|
||||||
column: usize,
|
|
||||||
},
|
|
||||||
SelectedRowChanged {
|
|
||||||
row: usize,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct Track {
|
pub struct Track {
|
||||||
pub state: TrackState,
|
pub state: TrackState,
|
||||||
pub volume: f32,
|
pub volume: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ShadowState {
|
pub struct State {
|
||||||
pub selected_column: usize, // 0-based (converted to 1-based for OSC)
|
pub selected_column: usize, // 0-based (converted to 1-based for OSC)
|
||||||
pub selected_row: usize, // 0-based (converted to 1-based for OSC)
|
pub selected_row: usize, // 0-based (converted to 1-based for OSC)
|
||||||
pub cells: Vec<Vec<Track>>,
|
pub cells: Vec<Vec<Track>>,
|
||||||
@ -35,7 +25,7 @@ pub(crate) struct ShadowState {
|
|||||||
pub rows: usize,
|
pub rows: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShadowState {
|
impl State {
|
||||||
pub fn new(columns: usize, rows: usize) -> Self {
|
pub fn new(columns: usize, rows: usize) -> Self {
|
||||||
let cells = (0..columns)
|
let cells = (0..columns)
|
||||||
.map(|_| vec![Track { state: TrackState::Empty, volume: 1.0 }; rows])
|
.map(|_| vec![Track { state: TrackState::Empty, volume: 1.0 }; rows])
|
||||||
@ -50,24 +40,24 @@ impl ShadowState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, message: &OscMessage) {
|
pub fn update(&mut self, message: &Message) {
|
||||||
match message {
|
match message {
|
||||||
OscMessage::TrackStateChanged { column, row, state } => {
|
Message::TrackStateChanged { column, row, state } => {
|
||||||
if *column < self.columns && *row < self.rows {
|
if *column < self.columns && *row < self.rows {
|
||||||
self.cells[*column][*row].state = state.clone();
|
self.cells[*column][*row].state = state.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OscMessage::TrackVolumeChanged { column, row, volume } => {
|
Message::TrackVolumeChanged { column, row, volume } => {
|
||||||
if *column < self.columns && *row < self.rows {
|
if *column < self.columns && *row < self.rows {
|
||||||
self.cells[*column][*row].volume = *volume;
|
self.cells[*column][*row].volume = *volume;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OscMessage::SelectedColumnChanged { column } => {
|
Message::SelectedColumnChanged { column } => {
|
||||||
if *column < self.columns {
|
if *column < self.columns {
|
||||||
self.selected_column = *column;
|
self.selected_column = *column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OscMessage::SelectedRowChanged { row } => {
|
Message::SelectedRowChanged { row } => {
|
||||||
if *row < self.rows {
|
if *row < self.rows {
|
||||||
self.selected_row = *row;
|
self.selected_row = *row;
|
||||||
}
|
}
|
||||||
@ -75,26 +65,26 @@ impl ShadowState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_state_dump(&self) -> Vec<OscMessage> {
|
pub fn create_state_dump(&self) -> Vec<Message> {
|
||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
|
|
||||||
// Send current selections
|
// Send current selections
|
||||||
messages.push(OscMessage::SelectedColumnChanged {
|
messages.push(Message::SelectedColumnChanged {
|
||||||
column: self.selected_column,
|
column: self.selected_column,
|
||||||
});
|
});
|
||||||
messages.push(OscMessage::SelectedRowChanged {
|
messages.push(Message::SelectedRowChanged {
|
||||||
row: self.selected_row,
|
row: self.selected_row,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send all cell states and volumes
|
// Send all cell states and volumes
|
||||||
for (column, column_cells) in self.cells.iter().enumerate() {
|
for (column, column_cells) in self.cells.iter().enumerate() {
|
||||||
for (row, track) in column_cells.iter().enumerate() {
|
for (row, track) in column_cells.iter().enumerate() {
|
||||||
messages.push(OscMessage::TrackStateChanged {
|
messages.push(Message::TrackStateChanged {
|
||||||
column,
|
column,
|
||||||
row,
|
row,
|
||||||
state: track.state.clone(),
|
state: track.state.clone(),
|
||||||
});
|
});
|
||||||
messages.push(OscMessage::TrackVolumeChanged {
|
messages.push(Message::TrackVolumeChanged {
|
||||||
column,
|
column,
|
||||||
row,
|
row,
|
||||||
volume: track.volume,
|
volume: track.volume,
|
||||||
@ -104,4 +94,28 @@ impl ShadowState {
|
|||||||
|
|
||||||
messages
|
messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_track_state(&mut self, column: usize, row: usize, state: TrackState) {
|
||||||
|
if column < self.columns && row < self.rows {
|
||||||
|
self.cells[column][row].state = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_selected_column(&mut self, column: usize) {
|
||||||
|
if column < self.columns {
|
||||||
|
self.selected_column = column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_selected_row(&mut self, row: usize) {
|
||||||
|
if row < self.rows {
|
||||||
|
self.selected_row = row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_track_volume(&mut self, column: usize, row: usize, volume: f32) {
|
||||||
|
if column < self.columns && row < self.rows {
|
||||||
|
self.cells[column][row].volume = volume;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user