/looper/metronome/position in audio_engine

This commit is contained in:
Geens 2025-06-21 19:52:46 +02:00
parent 470089ae5b
commit 89645db1d9
21 changed files with 334 additions and 213 deletions

View File

@ -1,13 +1,18 @@
use crate::*;
use clap::Parser;
use std::path::PathBuf;
use crate::*;
#[derive(Parser)]
#[command(name = "audio_engine")]
#[command(version, about = "FCB1010 Looper Pedal Audio Engine")]
pub struct Args {
/// Path to Unix socket or named pipe name
#[arg(short = 's', long = "socket", env = "SOCKET", default_value = "fcb_looper.sock")]
#[arg(
short = 's',
long = "socket",
env = "SOCKET",
default_value = "fcb_looper.sock"
)]
pub socket: String,
/// Path to configuration dir

View File

@ -62,10 +62,7 @@ impl<const ROWS: usize> Column<ROWS> {
Ok(())
}
pub fn handle_play_button(
&mut self,
controllers: &TrackControllers,
) -> Result<()> {
pub fn handle_play_button(&mut self, controllers: &TrackControllers) -> Result<()> {
let track = &mut self.tracks[controllers.row()];
if track.len() > 0 && track.is_idle() {
track.play();
@ -75,10 +72,7 @@ impl<const ROWS: usize> Column<ROWS> {
Ok(())
}
pub fn handle_clear_button(
&mut self,
controllers: &TrackControllers,
) -> Result<()> {
pub fn handle_clear_button(&mut self, controllers: &TrackControllers) -> Result<()> {
let track = &mut self.tracks[controllers.row()];
track.clear();
Ok(())
@ -157,7 +151,8 @@ impl<const ROWS: usize> Column<ROWS> {
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;
self.playback_position =
(self.playback_position + input_buffer.len()) % self.frames_per_beat;
}
Ok(())
}

View File

@ -51,6 +51,16 @@ impl ConnectionManager {
}
}
for external_port in &state.connections.midi_out {
let our_port = format!("{}:midi_out", self.jack_client_name);
let result = self
.jack_client
.connect_ports_by_name(&our_port, external_port);
if let Ok(_) = result {
log::info!("Connected {} -> {}", our_port, external_port);
}
}
for external_port in &state.connections.audio_in {
let our_port = format!("{}:audio_in", self.jack_client_name);
let result = self

View File

@ -86,14 +86,13 @@ impl<'a> TrackControllers<'a> {
}
}
pub fn send_post_record_request(&self, chunk: Arc<AudioChunk>, sync_offset: usize) -> Result<()> {
self.post_record.send_request(
self.column,
self.row,
chunk,
sync_offset,
self.sample_rate,
)
pub fn send_post_record_request(
&self,
chunk: Arc<AudioChunk>,
sync_offset: usize,
) -> Result<()> {
self.post_record
.send_request(self.column, self.row, chunk, sync_offset, self.sample_rate)
}
pub fn send_state_update(&self, state: TrackState) -> Result<()> {
@ -105,7 +104,8 @@ impl<'a> TrackControllers<'a> {
}
pub fn update_track_volume(&self, volume: f32) -> Result<()> {
self.persistence.update_track_volume(self.column, self.row, volume)
self.persistence
.update_track_volume(self.column, self.row, volume)
}
pub fn column(&self) -> usize {

View File

@ -26,6 +26,9 @@ pub enum LooperError {
#[error("Irrecoverable XRUN")]
Xrun(&'static std::panic::Location<'static>),
#[error("Failed to write midi message")]
Midi(&'static std::panic::Location<'static>),
}
pub type Result<T> = std::result::Result<T, LooperError>;

View File

@ -38,8 +38,8 @@ use metronome::BufferTiming;
use metronome::Metronome;
use notification_handler::JackNotification;
use notification_handler::NotificationHandler;
use osc_server::OscController;
use osc_server::Osc;
use osc_server::OscController;
use persistence_manager::PersistenceManager;
use persistence_manager::PersistenceManagerController;
use post_record_handler::PostRecordController;
@ -56,6 +56,7 @@ pub struct JackPorts {
pub audio_out: jack::Port<jack::AudioOut>,
pub click_track_out: jack::Port<jack::AudioOut>,
pub midi_in: jack::Port<jack::MidiIn>,
pub midi_out: jack::Port<jack::MidiOut>,
}
#[tokio::main]
@ -65,7 +66,9 @@ async fn main() {
.init()
.expect("Could not initialize logger");
let (mut osc, osc_controller) = Osc::new(&args.socket).await.expect("Could not create OSC server");
let (mut osc, osc_controller) = Osc::new(&args.socket)
.await
.expect("Could not create OSC server");
let (jack_client, ports) = setup_jack();
@ -167,12 +170,16 @@ fn setup_jack() -> (jack::Client, JackPorts) {
let midi_in = jack_client
.register_port("midi_in", jack::MidiIn::default())
.expect("Could not create midi_in port");
let midi_out = jack_client
.register_port("midi_out", jack::MidiOut::default())
.expect("Could not create midi_out port");
let ports = JackPorts {
audio_in,
audio_out,
click_track_out,
midi_in,
midi_out,
};
(jack_client, ports)

View File

@ -39,6 +39,7 @@ impl Metronome {
&mut self,
ps: &jack::ProcessScope,
ports: &mut JackPorts,
osc_controller: &OscController,
) -> Result<BufferTiming> {
let buffer_size = ps.n_frames();
let current_frame_time = ps.last_frame_time();
@ -51,17 +52,90 @@ impl Metronome {
self.render_click(buffer_size, &timing, click_output);
// Process PPQN ticks for this buffer
self.process_ppqn_ticks(ps, ports, osc_controller)?;
Ok(timing)
}
fn process_ppqn_ticks(
&mut self,
ps: &jack::ProcessScope,
ports: &mut JackPorts,
osc_controller: &OscController,
) -> Result<()> {
let buffer_size = ps.n_frames() as usize;
// Calculate the starting position for this buffer
let start_position = (self.frames_since_last_beat + self.frames_per_beat - buffer_size)
% self.frames_per_beat;
// Process PPQN ticks in the current buffer only
self.send_buffer_ppqn_ticks(ps, ports, osc_controller, start_position, buffer_size)?;
Ok(())
}
fn send_buffer_ppqn_ticks(
&self,
ps: &jack::ProcessScope,
ports: &mut JackPorts,
osc_controller: &OscController,
buffer_start_position: usize,
buffer_size: usize,
) -> Result<()> {
let frames_per_ppqn_tick = self.frames_per_beat / 24;
let mut current_position = buffer_start_position;
let mut frames_processed = 0;
// Get MIDI output writer
let mut midi_writer = ports.midi_out.writer(ps);
while frames_processed < buffer_size {
let frames_to_next_ppqn =
frames_per_ppqn_tick - (current_position % frames_per_ppqn_tick);
let frames_remaining_in_buffer = buffer_size - frames_processed;
if frames_remaining_in_buffer >= frames_to_next_ppqn {
// We hit a PPQN tick within this buffer
let tick_frame_offset = frames_processed + frames_to_next_ppqn;
// Ensure we don't hit the buffer boundary (JACK expects [0, buffer_size))
if tick_frame_offset >= buffer_size {
break;
}
current_position = (current_position + frames_to_next_ppqn) % self.frames_per_beat;
frames_processed = tick_frame_offset;
// Send sample-accurate MIDI clock
let midi_clock_message = [0xF8]; // MIDI Clock byte
midi_writer
.write(&jack::RawMidi {
time: tick_frame_offset as u32,
bytes: &midi_clock_message,
})
.map_err(|_| LooperError::Midi(std::panic::Location::caller()))?;
// Send OSC position message
let ppqn_position = (current_position as f32) / (self.frames_per_beat as f32);
osc_controller.metronome_position_changed(ppqn_position)?;
} else {
break;
}
}
Ok(())
}
fn render_click(&mut self, buffer_size: u32, timing: &BufferTiming, click_output: &mut [f32]) {
let click_length = self.click_samples.sample_count;
// Calculate our position at the START of this buffer (before calculate_timing updated it)
// We need to go back by: buffer_size + any missed samples
let total_advancement = buffer_size as usize + timing.missed_frames;
let start_position =
(self.frames_since_last_beat + self.frames_per_beat - total_advancement)
let start_position = (self.frames_since_last_beat + self.frames_per_beat
- total_advancement)
% self.frames_per_beat;
if let Some(beat_offset) = timing.beat_in_buffer {
@ -139,14 +213,15 @@ impl Metronome {
let missed = current_frame_time.wrapping_sub(expected) as usize;
// Check if we missed multiple beats
let total_samples = self.frames_since_last_beat + missed as usize + buffer_size as usize;
let total_samples =
self.frames_since_last_beat + missed as usize + buffer_size as usize;
if total_samples >= 2 * self.frames_per_beat {
return Err(LooperError::Xrun(std::panic::Location::caller()));
}
// Check if a beat occurred in the missed section
let beat_in_missed = if self.frames_since_last_beat + missed as usize >= self.frames_per_beat
{
let beat_in_missed =
if self.frames_since_last_beat + missed as usize >= self.frames_per_beat {
Some((self.frames_per_beat - self.frames_since_last_beat) as u32)
} else {
None
@ -172,7 +247,8 @@ impl Metronome {
// Update state - advance by total samples (missed + buffer)
self.frames_since_last_beat =
(self.frames_since_last_beat + missed_frames + buffer_size as usize) % self.frames_per_beat;
(self.frames_since_last_beat + missed_frames + buffer_size as usize)
% self.frames_per_beat;
self.last_frame_time = Some(current_frame_time);
Ok(BufferTiming {

View File

@ -10,13 +10,12 @@ impl OscController {
Self { sender }
}
pub fn track_state_changed(
&self,
column: usize,
row: usize,
state: TrackState,
) -> Result<()> {
let message = osc::Message::TrackStateChanged { column, row, state: state.into() };
pub fn track_state_changed(&self, column: usize, row: usize, state: TrackState) -> Result<()> {
let message = osc::Message::TrackStateChanged {
column,
row,
state: state.into(),
};
match self.sender.try_send(message) {
Ok(true) => Ok(()),
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
@ -24,13 +23,12 @@ impl OscController {
}
}
pub fn track_volume_changed(
&self,
column: usize,
row: usize,
volume: f32,
) -> Result<()> {
let message = osc::Message::TrackVolumeChanged { column, row, volume };
pub fn track_volume_changed(&self, column: usize, row: usize, volume: f32) -> Result<()> {
let message = osc::Message::TrackVolumeChanged {
column,
row,
volume,
};
match self.sender.try_send(message) {
Ok(true) => Ok(()),
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
@ -55,4 +53,13 @@ impl OscController {
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
}
}
pub fn metronome_position_changed(&self, position: f32) -> Result<()> {
let message = osc::Message::MetronomePosition { position };
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

@ -9,7 +9,10 @@ pub(crate) struct PlatformListener {
impl PlatformListener {
pub async fn accept(&mut self) -> Result<PlatformStream> {
let (stream, _) = self.listener.accept().await
let (stream, _) = self
.listener
.accept()
.await
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?;
Ok(stream)
}

View File

@ -16,7 +16,9 @@ impl PlatformListener {
}
let server = self.current_server.take().unwrap();
server.connect().await
server
.connect()
.await
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?;
Ok(server)

View File

@ -65,7 +65,10 @@ impl Osc {
let state_dump = self.shadow_state.create_state_dump();
tokio::spawn(async move {
let mut framed = tokio_util::codec::Framed::new(stream, tokio_util::codec::LengthDelimitedCodec::new());
let mut framed = tokio_util::codec::Framed::new(
stream,
tokio_util::codec::LengthDelimitedCodec::new(),
);
// Send current state dump immediately
for msg in state_dump {
@ -84,10 +87,13 @@ impl Osc {
}
async fn send_osc_message(
framed: &mut tokio_util::codec::Framed<osc_server::platform::PlatformStream, tokio_util::codec::LengthDelimitedCodec>,
framed: &mut tokio_util::codec::Framed<
osc_server::platform::PlatformStream,
tokio_util::codec::LengthDelimitedCodec,
>,
message: osc::Message,
) -> Result<()> {
let osc_packet = Self::message_to_osc_packet(message)?;
let osc_packet = message.to_osc_packet();
let osc_bytes = rosc::encoder::encode(&osc_packet)
.map_err(|_| crate::LooperError::StateSave(std::panic::Location::caller()))?;
@ -99,41 +105,4 @@ impl Osc {
Ok(())
}
fn message_to_osc_packet(message: osc::Message) -> Result<rosc::OscPacket> {
match message {
osc::Message::TrackStateChanged { column, row, state } => {
let address = format!("/looper/cell/{}/{}/state", column + 1, row + 1); // 1-based indexing
Ok(rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::String(state.to_string())],
}))
}
osc::Message::TrackVolumeChanged { column, row, volume } => {
let address = format!("/looper/cell/{}/{}/volume", column + 1, row + 1); // 1-based indexing
Ok(rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::Float(volume)],
}))
}
osc::Message::SelectedColumnChanged { column } => {
let address = "/looper/selected/column".to_string();
Ok(rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::Int((column + 1) as i32)], // 1-based indexing
}))
}
osc::Message::SelectedRowChanged { row } => {
let address = "/looper/selected/row".to_string();
Ok(rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::Int((row + 1) as i32)], // 1-based indexing
}))
}
}
}
}

View File

@ -9,12 +9,20 @@ pub struct PersistenceManagerController {
#[derive(Debug)]
pub enum PersistenceUpdate {
UpdateTrackVolume { column: usize, row: usize, volume: f32 },
UpdateTrackVolume {
column: usize,
row: usize,
volume: f32,
},
}
impl PersistenceManagerController {
pub fn update_track_volume(&self, column: usize, row: usize, volume: f32) -> Result<()> {
let update = PersistenceUpdate::UpdateTrackVolume { column, row, volume };
let update = PersistenceUpdate::UpdateTrackVolume {
column,
row,
volume,
};
match self.sender.try_send(update) {
Ok(true) => Ok(()),
Ok(false) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel full
@ -42,9 +50,7 @@ impl PersistenceManager {
let (update_tx, update_rx) = kanal::bounded(64);
let update_rx = update_rx.to_async();
let controller = PersistenceManagerController {
sender: update_tx,
};
let controller = PersistenceManagerController { sender: update_tx };
let manager = Self {
state,
@ -90,8 +96,13 @@ impl PersistenceManager {
fn handle_state_update(&mut self, update: PersistenceUpdate) -> Result<()> {
match update {
PersistenceUpdate::UpdateTrackVolume { column, row, volume } => {
self.state.send_modify(|state| state.track_volumes.set_volume(column, row, volume));
PersistenceUpdate::UpdateTrackVolume {
column,
row,
volume,
} => {
self.state
.send_modify(|state| state.track_volumes.set_volume(column, row, volume));
self.save_to_disk()?;
}
}
@ -122,6 +133,7 @@ impl PersistenceManager {
let port_list = match our_port_name {
"midi_in" => &mut connections.midi_in,
"midi_out" => &mut connections.midi_out,
"audio_in" => &mut connections.audio_in,
"audio_out" => &mut connections.audio_out,
"click_track" => &mut connections.click_track_out,

View File

@ -27,11 +27,7 @@ impl<F: ChunkFactory> ProcessHandler<F> {
osc: OscController,
persistence: PersistenceManagerController,
) -> Result<Self> {
let track_matrix = TrackMatrix::new(
client,
chunk_factory,
state,
)?;
let track_matrix = TrackMatrix::new(client, chunk_factory, state)?;
Ok(Self {
post_record_controller,
osc,
@ -61,10 +57,8 @@ impl<F: ChunkFactory> ProcessHandler<F> {
self.selected_column,
self.selected_row,
);
self.track_matrix.handle_volume_update(
self.last_volume_setting,
&controllers,
)
self.track_matrix
.handle_volume_update(self.last_volume_setting, &controllers)
}
pub fn handle_button_1(&mut self) -> Result<()> {
@ -76,10 +70,8 @@ impl<F: ChunkFactory> ProcessHandler<F> {
self.selected_column,
self.selected_row,
);
self.track_matrix.handle_record_button(
self.last_volume_setting,
&controllers,
)
self.track_matrix
.handle_record_button(self.last_volume_setting, &controllers)
}
pub fn handle_button_2(&mut self) -> Result<()> {
@ -91,9 +83,7 @@ impl<F: ChunkFactory> ProcessHandler<F> {
self.selected_column,
self.selected_row,
);
self.track_matrix.handle_play_button(
&controllers,
)
self.track_matrix.handle_play_button(&controllers)
}
pub fn handle_button_3(&mut self) -> Result<()> {
@ -113,9 +103,7 @@ impl<F: ChunkFactory> ProcessHandler<F> {
self.selected_column,
self.selected_row,
);
self.track_matrix.handle_clear_button(
&controllers,
)
self.track_matrix.handle_clear_button(&controllers)
}
pub fn handle_button_6(&mut self) -> Result<()> {
@ -177,12 +165,9 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
}
impl<F: ChunkFactory> ProcessHandler<F> {
fn process_with_error_handling(
&mut self,
ps: &jack::ProcessScope,
) -> Result<()> {
fn process_with_error_handling(&mut self, ps: &jack::ProcessScope) -> Result<()> {
// Process metronome and get beat timing information
let timing = self.metronome.process(ps, &mut self.ports)?;
let timing = self.metronome.process(ps, &mut self.ports, &self.osc)?;
// Process MIDI
midi::process_events(self, ps)?;
@ -194,12 +179,8 @@ impl<F: ChunkFactory> ProcessHandler<F> {
&self.persistence,
self.sample_rate,
);
self.track_matrix.process(
ps,
&mut self.ports,
&timing,
&controllers,
)?;
self.track_matrix
.process(ps, &mut self.ports, &timing, &controllers)?;
Ok(())
}

View File

@ -11,6 +11,7 @@ pub struct State {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionState {
pub midi_in: Vec<String>,
pub midi_out: Vec<String>,
pub audio_in: Vec<String>,
pub audio_out: Vec<String>,
pub click_track_out: Vec<String>,
@ -56,6 +57,7 @@ impl Default for State {
Self {
connections: ConnectionState {
midi_in: Vec::new(),
midi_out: Vec::new(),
audio_in: Vec::new(),
audio_out: Vec::new(),
click_track_out: Vec::new(),

View File

@ -51,11 +51,7 @@ impl Track {
self.audio_data.len()
}
pub fn set_volume(
&mut self,
new_volume: f32,
controllers: &TrackControllers,
) -> Result<()> {
pub fn set_volume(&mut self, new_volume: f32, controllers: &TrackControllers) -> Result<()> {
let new_volume = new_volume.clamp(0.0, 1.0);
match &mut self.current_state {
@ -82,7 +78,12 @@ impl Track {
};
}
pub fn record_auto_stop(&mut self, target_samples: usize, sync_offset: usize, last_volume_setting: f32) {
pub fn record_auto_stop(
&mut self,
target_samples: usize,
sync_offset: usize,
last_volume_setting: f32,
) {
self.next_state = TrackState::RecordingAutoStop {
volume: last_volume_setting,
target_samples,

View File

@ -7,11 +7,7 @@ pub struct TrackMatrix<F: ChunkFactory, const COLS: usize, const ROWS: usize> {
}
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS, ROWS> {
pub fn new(
client: &jack::Client,
chunk_factory: F,
state: &State,
) -> Result<Self> {
pub fn new(client: &jack::Client, chunk_factory: F, state: &State) -> Result<Self> {
let columns = std::array::from_fn(|_| Column::new(state.metronome.frames_per_beat));
Ok(Self {
chunk_factory,
@ -25,23 +21,14 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
last_volume_setting: f32,
controllers: &TrackControllers,
) -> Result<()> {
self.columns[controllers.column()].handle_record_button(
last_volume_setting,
controllers,
)
self.columns[controllers.column()].handle_record_button(last_volume_setting, controllers)
}
pub fn handle_play_button(
&mut self,
controllers: &TrackControllers,
) -> Result<()> {
pub fn handle_play_button(&mut self, controllers: &TrackControllers) -> Result<()> {
self.columns[controllers.column()].handle_play_button(controllers)
}
pub fn handle_clear_button(
&mut self,
controllers: &TrackControllers,
) -> Result<()> {
pub fn handle_clear_button(&mut self, controllers: &TrackControllers) -> Result<()> {
self.columns[controllers.column()].handle_clear_button(controllers)
}
@ -50,10 +37,7 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
new_volume: f32,
controllers: &TrackControllers,
) -> Result<()> {
self.columns[controllers.column()].handle_volume_update(
new_volume,
controllers,
)
self.columns[controllers.column()].handle_volume_update(new_volume, controllers)
}
pub fn process(
@ -74,11 +58,7 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
for (i, column) in &mut self.columns.iter_mut().enumerate() {
let controllers = controllers.to_column_controllers(i);
column.handle_xrun(
&timing,
&mut self.chunk_factory,
&controllers,
)?;
column.handle_xrun(&timing, &mut self.chunk_factory, &controllers)?;
}
}

View File

@ -18,6 +18,9 @@ pub enum Message {
SelectedRowChanged {
row: usize,
},
MetronomePosition {
position: f32, // 0.0 - 1.0 position within current beat
},
}
impl Message {
@ -53,6 +56,9 @@ impl Message {
} => {
state.set_track_volume(*column, *row, *volume);
}
Message::MetronomePosition { position } => {
state.set_metronome_position(*position);
}
}
}
@ -100,9 +106,59 @@ impl Message {
let row = (*row_1based as usize).saturating_sub(1); // Convert to 0-based
return Ok(Some(Message::SelectedRowChanged { row }));
}
} else if addr == "/looper/metronome/position" {
if let Some(rosc::OscType::Float(position)) = args.first() {
return Ok(Some(Message::MetronomePosition {
position: *position
}));
}
}
// Unknown or unsupported message
Ok(None)
}
pub fn to_osc_packet(self) -> rosc::OscPacket {
match self {
Message::TrackStateChanged { column, row, state } => {
let address = format!("/looper/cell/{}/{}/state", column + 1, row + 1); // 1-based indexing
rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::String(state.to_string())],
})
}
Message::TrackVolumeChanged { column, row, volume } => {
let address = format!("/looper/cell/{}/{}/volume", column + 1, row + 1); // 1-based indexing
rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::Float(volume)],
})
}
Message::SelectedColumnChanged { column } => {
let address = "/looper/selected/column".to_string();
rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::Int((column + 1) as i32)], // 1-based indexing
})
}
Message::SelectedRowChanged { row } => {
let address = "/looper/selected/row".to_string();
rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::Int((row + 1) as i32)], // 1-based indexing
})
}
Message::MetronomePosition { position } => {
let address = "/looper/metronome/position".to_string();
rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::Float(position)],
})
}
}
}
}

View File

@ -21,6 +21,8 @@ pub struct State {
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
}
impl State {
@ -35,6 +37,8 @@ impl State {
cells,
columns,
rows,
metronome_position: 0.0,
metronome_timestamp: std::time::Instant::now(),
}
}
@ -60,6 +64,9 @@ impl State {
self.selected_row = *row;
}
}
Message::MetronomePosition { position } => {
self.set_metronome_position(*position);
}
}
}
@ -116,4 +123,9 @@ impl State {
self.cells[column][row].volume = volume;
}
}
pub fn set_metronome_position(&mut self, position: f32) {
self.metronome_position = position;
self.metronome_timestamp = std::time::Instant::now();
}
}