per track volume

This commit is contained in:
2025-06-21 11:00:04 +02:00
parent 70b2f92a8d
commit 7b7ebb8c0e
29 changed files with 4989 additions and 309 deletions

2
osc/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
Cargo.lock

10
osc/Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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;
}
}
}