OSC messages for column beat tracking
This commit is contained in:
@@ -6,6 +6,7 @@ use error::AddressParseResult;
|
||||
pub use error::Error;
|
||||
pub use error::Result;
|
||||
pub use message::Message;
|
||||
pub use state::Column;
|
||||
pub use state::State;
|
||||
pub use state::Track;
|
||||
pub use state::TrackState;
|
||||
|
||||
@@ -24,6 +24,14 @@ pub enum Message {
|
||||
Tempo {
|
||||
bpm: f32,
|
||||
},
|
||||
ColumnBeatsChanged {
|
||||
column: usize,
|
||||
beats: usize,
|
||||
},
|
||||
ColumnBeatChanged {
|
||||
column: usize,
|
||||
beat: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Message {
|
||||
@@ -37,7 +45,10 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_to_state(&self, state: &mut State) {
|
||||
pub fn apply_to_state<const COLS: usize, const ROWS: usize>(
|
||||
&self,
|
||||
state: &mut State<COLS, ROWS>,
|
||||
) {
|
||||
match self {
|
||||
Message::TrackStateChanged {
|
||||
column,
|
||||
@@ -65,6 +76,12 @@ impl Message {
|
||||
Message::Tempo { bpm } => {
|
||||
state.set_tempo(*bpm);
|
||||
}
|
||||
Message::ColumnBeatsChanged { column, beats } => {
|
||||
state.set_column_beats(*column, *beats);
|
||||
}
|
||||
Message::ColumnBeatChanged { column, beat } => {
|
||||
state.set_column_beat(*column, *beat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +100,7 @@ impl Message {
|
||||
|
||||
if let Some(rosc::OscType::String(state_str)) = args.first() {
|
||||
let state = state_str.parse::<TrackState>().unwrap_or(TrackState::Empty);
|
||||
log::trace!("TrackStateChanged: column={column}, row={row}, state={state}");
|
||||
log::debug!("TrackStateChanged: column={column}, row={row}, state={state}");
|
||||
return Ok(Some(Message::TrackStateChanged { column, row, state }));
|
||||
}
|
||||
} else if addr.starts_with("/looper/cell/") && addr.ends_with("/volume") {
|
||||
@@ -97,23 +114,53 @@ impl Message {
|
||||
let row: usize = parts[4].parse::<usize>().address_part_result("row")? - 1; // Convert to 0-based
|
||||
|
||||
if let Some(rosc::OscType::Float(volume)) = args.first() {
|
||||
log::trace!("TrackVolumeChanged: column={column}, row={row}, volume={volume}");
|
||||
log::debug!("TrackVolumeChanged: column={column}, row={row}, volume={volume}");
|
||||
return Ok(Some(Message::TrackVolumeChanged {
|
||||
column,
|
||||
row,
|
||||
volume: *volume,
|
||||
}));
|
||||
}
|
||||
} else if addr.starts_with("/looper/column/") && addr.ends_with("/beats") {
|
||||
// Parse: /looper/column/{column}/beats
|
||||
let parts: Vec<&str> = addr.split('/').collect();
|
||||
if parts.len() != 5 {
|
||||
return Err(Error::AddressParseError(addr));
|
||||
}
|
||||
|
||||
let column: usize = parts[3].parse::<usize>().address_part_result("column")? - 1; // Convert to 0-based
|
||||
|
||||
if let Some(rosc::OscType::Int(beats)) = args.first() {
|
||||
log::debug!("ColumnBeatsChanged: column={column}, beats={beats}");
|
||||
return Ok(Some(Message::ColumnBeatsChanged {
|
||||
column,
|
||||
beats: *beats as usize,
|
||||
}));
|
||||
}
|
||||
} else if addr.starts_with("/looper/column/") && addr.ends_with("/beat") {
|
||||
// Parse: /looper/column/{column}/beat
|
||||
let parts: Vec<&str> = addr.split('/').collect();
|
||||
if parts.len() != 5 {
|
||||
return Err(Error::AddressParseError(addr));
|
||||
}
|
||||
|
||||
let column: usize = parts[3].parse::<usize>().address_part_result("column")? - 1; // Convert to 0-based
|
||||
|
||||
if let Some(rosc::OscType::Int(beat)) = args.first() {
|
||||
let beat = (*beat as usize).saturating_sub(1); // Convert to 0-based
|
||||
log::debug!("ColumnBeatChanged: column={column}, beat={beat}");
|
||||
return Ok(Some(Message::ColumnBeatChanged { column, beat }));
|
||||
}
|
||||
} else if addr == "/looper/selected/column" {
|
||||
if let Some(rosc::OscType::Int(column_1based)) = args.first() {
|
||||
log::trace!("SelectedColumnChanged: column={column_1based}");
|
||||
let column = (*column_1based as usize).saturating_sub(1); // Convert to 0-based
|
||||
log::debug!("SelectedColumnChanged: column={column}");
|
||||
return Ok(Some(Message::SelectedColumnChanged { column }));
|
||||
}
|
||||
} else if addr == "/looper/selected/row" {
|
||||
if let Some(rosc::OscType::Int(row_1based)) = args.first() {
|
||||
log::trace!("SelectedRowChanged: row={row_1based}");
|
||||
let row = (*row_1based as usize).saturating_sub(1); // Convert to 0-based
|
||||
log::debug!("SelectedRowChanged: row={row}");
|
||||
return Ok(Some(Message::SelectedRowChanged { row }));
|
||||
}
|
||||
} else if addr == "/looper/metronome/position" {
|
||||
@@ -125,7 +172,7 @@ impl Message {
|
||||
}
|
||||
} else if addr == "/looper/tempo" {
|
||||
if let Some(rosc::OscType::Float(bpm)) = args.first() {
|
||||
log::trace!("Tempo: bpm={bpm}");
|
||||
log::debug!("Tempo: bpm={bpm}");
|
||||
return Ok(Some(Message::Tempo { bpm: *bpm }));
|
||||
}
|
||||
}
|
||||
@@ -187,6 +234,22 @@ impl Message {
|
||||
args: vec![rosc::OscType::Float(bpm)],
|
||||
})
|
||||
}
|
||||
Message::ColumnBeatsChanged { column, beats } => {
|
||||
let address = format!("/looper/column/{}/beats", column + 1); // 1-based indexing
|
||||
|
||||
rosc::OscPacket::Message(rosc::OscMessage {
|
||||
addr: address,
|
||||
args: vec![rosc::OscType::Int(beats as i32)],
|
||||
})
|
||||
}
|
||||
Message::ColumnBeatChanged { column, beat } => {
|
||||
let address = format!("/looper/column/{}/beat", column + 1); // 1-based indexing
|
||||
|
||||
rosc::OscPacket::Message(rosc::OscMessage {
|
||||
addr: address,
|
||||
args: vec![rosc::OscType::Int((beat + 1) as i32)], // Convert to 1-based
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
115
osc/src/state.rs
115
osc/src/state.rs
@@ -15,37 +15,41 @@ pub struct Track {
|
||||
}
|
||||
|
||||
#[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,
|
||||
pub metronome_position: f32, // 0.0 - 1.0 position within current beat
|
||||
pub metronome_timestamp: std::time::Instant, // When position was last updated
|
||||
pub tempo: f32, // BPM
|
||||
pub struct Column<const ROWS: usize> {
|
||||
pub tracks: [Track; ROWS],
|
||||
pub beat_count: usize, // total beats in column (0 = no beats set)
|
||||
pub current_beat: usize, // current beat position (0-based internally)
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(columns: usize, rows: usize, tempo: f32) -> Self {
|
||||
let cells = (0..columns)
|
||||
.map(|_| {
|
||||
vec![
|
||||
Track {
|
||||
state: TrackState::Empty,
|
||||
volume: 1.0
|
||||
};
|
||||
rows
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State<const COLS: usize, const ROWS: usize> {
|
||||
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 columns: [Column<ROWS>; COLS],
|
||||
pub metronome_position: f32, // 0.0 - 1.0 position within current beat
|
||||
pub metronome_timestamp: std::time::Instant, // When position was last updated
|
||||
pub tempo: f32, // BPM
|
||||
}
|
||||
|
||||
impl<const ROWS: usize> Column<ROWS> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tracks: std::array::from_fn(|_| Track {
|
||||
state: TrackState::Empty,
|
||||
volume: 1.0,
|
||||
}),
|
||||
beat_count: 0,
|
||||
current_beat: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const COLS: usize, const ROWS: usize> State<COLS, ROWS> {
|
||||
pub fn new(tempo: f32) -> Self {
|
||||
Self {
|
||||
selected_column: 0,
|
||||
selected_row: 0,
|
||||
cells,
|
||||
columns,
|
||||
rows,
|
||||
columns: std::array::from_fn(|_| Column::new()),
|
||||
metronome_position: 0.0,
|
||||
metronome_timestamp: std::time::Instant::now(),
|
||||
tempo,
|
||||
@@ -55,8 +59,8 @@ impl State {
|
||||
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();
|
||||
if *column < COLS && *row < ROWS {
|
||||
self.columns[*column].tracks[*row].state = state.clone();
|
||||
}
|
||||
}
|
||||
Message::TrackVolumeChanged {
|
||||
@@ -64,17 +68,17 @@ impl State {
|
||||
row,
|
||||
volume,
|
||||
} => {
|
||||
if *column < self.columns && *row < self.rows {
|
||||
self.cells[*column][*row].volume = *volume;
|
||||
if *column < COLS && *row < ROWS {
|
||||
self.columns[*column].tracks[*row].volume = *volume;
|
||||
}
|
||||
}
|
||||
Message::SelectedColumnChanged { column } => {
|
||||
if *column < self.columns {
|
||||
if *column < COLS {
|
||||
self.selected_column = *column;
|
||||
}
|
||||
}
|
||||
Message::SelectedRowChanged { row } => {
|
||||
if *row < self.rows {
|
||||
if *row < ROWS {
|
||||
self.selected_row = *row;
|
||||
}
|
||||
}
|
||||
@@ -84,6 +88,12 @@ impl State {
|
||||
Message::Tempo { bpm } => {
|
||||
self.set_tempo(*bpm);
|
||||
}
|
||||
Message::ColumnBeatsChanged { column, beats } => {
|
||||
self.set_column_beats(*column, *beats);
|
||||
}
|
||||
Message::ColumnBeatChanged { column, beat } => {
|
||||
self.set_column_beat(*column, *beat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +111,20 @@ impl State {
|
||||
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() {
|
||||
// Send column beat information and cell states/volumes
|
||||
for (column, column_data) in self.columns.iter().enumerate() {
|
||||
// Send column beat info
|
||||
messages.push(Message::ColumnBeatsChanged {
|
||||
column,
|
||||
beats: column_data.beat_count,
|
||||
});
|
||||
messages.push(Message::ColumnBeatChanged {
|
||||
column,
|
||||
beat: column_data.current_beat,
|
||||
});
|
||||
|
||||
// Send all cell states and volumes for this column
|
||||
for (row, track) in column_data.tracks.iter().enumerate() {
|
||||
messages.push(Message::TrackStateChanged {
|
||||
column,
|
||||
row,
|
||||
@@ -121,26 +142,26 @@ impl State {
|
||||
}
|
||||
|
||||
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;
|
||||
if column < COLS && row < ROWS {
|
||||
self.columns[column].tracks[row].state = state;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_selected_column(&mut self, column: usize) {
|
||||
if column < self.columns {
|
||||
if column < COLS {
|
||||
self.selected_column = column;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_selected_row(&mut self, row: usize) {
|
||||
if row < self.rows {
|
||||
if row < 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;
|
||||
if column < COLS && row < ROWS {
|
||||
self.columns[column].tracks[row].volume = volume;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,4 +173,16 @@ impl State {
|
||||
pub fn set_tempo(&mut self, bpm: f32) {
|
||||
self.tempo = bpm;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_column_beats(&mut self, column: usize, beats: usize) {
|
||||
if column < COLS {
|
||||
self.columns[column].beat_count = beats;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_column_beat(&mut self, column: usize, beat: usize) {
|
||||
if column < COLS {
|
||||
self.columns[column].current_beat = beat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user