OSC messages for column beat tracking

This commit is contained in:
2025-06-22 18:52:57 +02:00
parent cfa2f1709e
commit b4f51548b3
16 changed files with 441 additions and 272 deletions

View File

@@ -118,14 +118,17 @@ impl<const ROWS: usize> Column<ROWS> {
chunk_factory: &mut impl ChunkFactory,
controllers: &ColumnControllers,
) -> Result<()> {
let len = self.len();
let old_len = self.len();
if self.idle() {
if let Some(beat_index) = timing.beat_in_buffer {
let idle_time = input_buffer.len() - beat_index as usize;
if len == 0 {
self.playback_position = self.frames_per_beat - idle_time;
// When starting from idle, always start at beat 0 (beginning of first beat)
// regardless of where in the buffer the beat occurs
if old_len == 0 {
self.playback_position = 0; // Start at beat 0 for first recording
} else {
self.playback_position = len - idle_time;
let idle_time = input_buffer.len() - beat_index as usize;
self.playback_position = old_len - idle_time;
}
}
}
@@ -147,13 +150,38 @@ impl<const ROWS: usize> Column<ROWS> {
}
}
let len = self.len();
if len > 0 {
self.playback_position = (self.playback_position + input_buffer.len()) % self.len();
} else {
self.playback_position =
(self.playback_position + input_buffer.len()) % self.frames_per_beat;
let new_len = self.len();
if old_len == 0 && new_len > 0 {
let beats = new_len / self.frames_per_beat;
controllers.send_column_beats_changed(beats)?;
}
// Update playback position
if new_len > 0 {
self.playback_position = (self.playback_position + input_buffer.len()) % new_len;
} else {
// During recording of first track, don't use modulo - let position grow
// so that beat calculation works correctly
self.playback_position += input_buffer.len();
}
// Send beat change message if a beat occurred in this buffer
if timing.beat_in_buffer.is_some() {
// Calculate which beat we're entering (after the beat that just occurred)
let current_beat = if new_len > 0 {
// When column has defined length, calculate beat within that loop
let column_beats = new_len / self.frames_per_beat;
(self.playback_position / self.frames_per_beat) % column_beats
} else {
// During recording of first track, calculate beat based on absolute position
// The beat that just occurred is the beat we're now in
self.playback_position / self.frames_per_beat
};
controllers.send_column_beat_changed(current_beat)?;
}
Ok(())
}
}

View File

@@ -65,6 +65,18 @@ impl<'a> ColumnControllers<'a> {
row,
}
}
pub fn send_column_beats_changed(&self, beats: usize) -> Result<()> {
self.osc.column_beats_changed(self.column, beats)
}
pub fn send_column_beat_changed(&self, beat: usize) -> Result<()> {
self.osc.column_beat_changed(self.column, beat)
}
pub fn column(&self) -> usize {
self.column
}
}
impl<'a> TrackControllers<'a> {

View File

@@ -84,10 +84,10 @@ async fn main() {
let state = persistence_manager.state();
// Calculate tempo from state and jack client
let tempo_bpm = (jack_client.sample_rate() as f32 * 60.0)
/ state.borrow().metronome.frames_per_beat as f32;
let tempo_bpm =
(jack_client.sample_rate() as f32 * 60.0) / state.borrow().metronome.frames_per_beat as f32;
let (mut osc, osc_controller) = Osc::new(&args.socket, COLS, ROWS, tempo_bpm)
let (mut osc, osc_controller) = Osc::<COLS, ROWS>::new(&args.socket, tempo_bpm)
.await
.expect("Could not create OSC server");
@@ -190,4 +190,4 @@ fn setup_jack() -> (jack::Client, JackPorts) {
};
(jack_client, ports)
}
}

View File

@@ -62,4 +62,22 @@ impl OscController {
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
}
}
pub fn column_beats_changed(&self, column: usize, beats: usize) -> Result<()> {
let message = osc::Message::ColumnBeatsChanged { column, beats };
match self.sender.try_send(message) {
Ok(true) => Ok(()),
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
}
}
pub fn column_beat_changed(&self, column: usize, beat: usize) -> Result<()> {
let message = osc::Message::ColumnBeatChanged { column, beat };
match self.sender.try_send(message) {
Ok(true) => Ok(()),
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
}
}
}

View File

@@ -3,21 +3,16 @@ use futures::SinkExt;
const CLIENT_BUFFER_SIZE: usize = 32;
pub struct Osc {
pub struct Osc<const COLS: usize, const ROWS: usize> {
receiver: kanal::AsyncReceiver<osc::Message>,
listener: osc_server::platform::PlatformListener,
broadcaster: tokio::sync::broadcast::Sender<osc::Message>,
shadow_state: osc::State,
shadow_state: osc::State<COLS, ROWS>,
}
impl Osc {
/// Create new OSC server and controller with configurable matrix size and tempo
pub async fn new(
socket_path: &str,
columns: usize,
rows: usize,
tempo: f32,
) -> Result<(Self, OscController)> {
impl<const COLS: usize, const ROWS: usize> Osc<COLS, ROWS> {
/// Create new OSC server and controller with configurable tempo
pub async fn new(socket_path: &str, tempo: f32) -> Result<(Self, OscController)> {
// Create platform listener (server)
let listener = osc_server::platform::create_listener(socket_path).await?;
@@ -32,7 +27,7 @@ impl Osc {
receiver,
listener,
broadcaster,
shadow_state: osc::State::new(columns, rows, tempo),
shadow_state: osc::State::new(tempo),
};
Ok((server, controller))
@@ -110,4 +105,4 @@ impl Osc {
Ok(())
}
}
}

View File

@@ -150,7 +150,9 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
}
}
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> jack::ProcessHandler for ProcessHandler<F, COLS, ROWS> {
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> jack::ProcessHandler
for ProcessHandler<F, COLS, ROWS>
{
fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
if let Err(e) = self.process_with_error_handling(ps) {
log::error!("Error processing audio: {}", e);
@@ -181,4 +183,4 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
Ok(())
}
}
}