per track volume
This commit is contained in:
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
|
||||
}
|
||||
}
|
||||
}
|
||||
121
osc/src/state.rs
Normal file
121
osc/src/state.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use strum::Display;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone, Debug, Display, PartialEq)]
|
||||
pub enum TrackState {
|
||||
Empty,
|
||||
Idle,
|
||||
Playing,
|
||||
Recording,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Track {
|
||||
pub state: TrackState,
|
||||
pub volume: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
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 cells: Vec<Vec<Track>>,
|
||||
pub columns: usize,
|
||||
pub rows: usize,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(columns: usize, rows: usize) -> Self {
|
||||
let cells = (0..columns)
|
||||
.map(|_| vec![Track { state: TrackState::Empty, volume: 1.0 }; rows])
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
selected_column: 0,
|
||||
selected_row: 0,
|
||||
cells,
|
||||
columns,
|
||||
rows,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: &Message) {
|
||||
match message {
|
||||
Message::TrackStateChanged { column, row, state } => {
|
||||
if *column < self.columns && *row < self.rows {
|
||||
self.cells[*column][*row].state = state.clone();
|
||||
}
|
||||
}
|
||||
Message::TrackVolumeChanged { column, row, volume } => {
|
||||
if *column < self.columns && *row < self.rows {
|
||||
self.cells[*column][*row].volume = *volume;
|
||||
}
|
||||
}
|
||||
Message::SelectedColumnChanged { column } => {
|
||||
if *column < self.columns {
|
||||
self.selected_column = *column;
|
||||
}
|
||||
}
|
||||
Message::SelectedRowChanged { row } => {
|
||||
if *row < self.rows {
|
||||
self.selected_row = *row;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_state_dump(&self) -> Vec<Message> {
|
||||
let mut messages = Vec::new();
|
||||
|
||||
// Send current selections
|
||||
messages.push(Message::SelectedColumnChanged {
|
||||
column: self.selected_column,
|
||||
});
|
||||
messages.push(Message::SelectedRowChanged {
|
||||
row: self.selected_row,
|
||||
});
|
||||
|
||||
// Send all cell states and volumes
|
||||
for (column, column_cells) in self.cells.iter().enumerate() {
|
||||
for (row, track) in column_cells.iter().enumerate() {
|
||||
messages.push(Message::TrackStateChanged {
|
||||
column,
|
||||
row,
|
||||
state: track.state.clone(),
|
||||
});
|
||||
messages.push(Message::TrackVolumeChanged {
|
||||
column,
|
||||
row,
|
||||
volume: track.volume,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user