/looper/metronome/position in audio_engine
This commit is contained in:
parent
470089ae5b
commit
89645db1d9
@ -1,13 +1,18 @@
|
|||||||
|
use crate::*;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "audio_engine")]
|
#[command(name = "audio_engine")]
|
||||||
#[command(version, about = "FCB1010 Looper Pedal Audio Engine")]
|
#[command(version, about = "FCB1010 Looper Pedal Audio Engine")]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
/// Path to Unix socket or named pipe name
|
/// 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,
|
pub socket: String,
|
||||||
|
|
||||||
/// Path to configuration dir
|
/// Path to configuration dir
|
||||||
|
|||||||
@ -62,10 +62,7 @@ impl<const ROWS: usize> Column<ROWS> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_play_button(
|
pub fn handle_play_button(&mut self, controllers: &TrackControllers) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
controllers: &TrackControllers,
|
|
||||||
) -> Result<()> {
|
|
||||||
let track = &mut self.tracks[controllers.row()];
|
let track = &mut self.tracks[controllers.row()];
|
||||||
if track.len() > 0 && track.is_idle() {
|
if track.len() > 0 && track.is_idle() {
|
||||||
track.play();
|
track.play();
|
||||||
@ -75,10 +72,7 @@ impl<const ROWS: usize> Column<ROWS> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_clear_button(
|
pub fn handle_clear_button(&mut self, controllers: &TrackControllers) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
controllers: &TrackControllers,
|
|
||||||
) -> Result<()> {
|
|
||||||
let track = &mut self.tracks[controllers.row()];
|
let track = &mut self.tracks[controllers.row()];
|
||||||
track.clear();
|
track.clear();
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -157,7 +151,8 @@ impl<const ROWS: usize> Column<ROWS> {
|
|||||||
if len > 0 {
|
if len > 0 {
|
||||||
self.playback_position = (self.playback_position + input_buffer.len()) % self.len();
|
self.playback_position = (self.playback_position + input_buffer.len()) % self.len();
|
||||||
} else {
|
} 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
for external_port in &state.connections.audio_in {
|
||||||
let our_port = format!("{}:audio_in", self.jack_client_name);
|
let our_port = format!("{}:audio_in", self.jack_client_name);
|
||||||
let result = self
|
let result = self
|
||||||
|
|||||||
@ -86,14 +86,13 @@ impl<'a> TrackControllers<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_post_record_request(&self, chunk: Arc<AudioChunk>, sync_offset: usize) -> Result<()> {
|
pub fn send_post_record_request(
|
||||||
self.post_record.send_request(
|
&self,
|
||||||
self.column,
|
chunk: Arc<AudioChunk>,
|
||||||
self.row,
|
sync_offset: usize,
|
||||||
chunk,
|
) -> Result<()> {
|
||||||
sync_offset,
|
self.post_record
|
||||||
self.sample_rate,
|
.send_request(self.column, self.row, chunk, sync_offset, self.sample_rate)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_state_update(&self, state: TrackState) -> Result<()> {
|
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<()> {
|
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 {
|
pub fn column(&self) -> usize {
|
||||||
|
|||||||
@ -26,6 +26,9 @@ pub enum LooperError {
|
|||||||
|
|
||||||
#[error("Irrecoverable XRUN")]
|
#[error("Irrecoverable XRUN")]
|
||||||
Xrun(&'static std::panic::Location<'static>),
|
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>;
|
pub type Result<T> = std::result::Result<T, LooperError>;
|
||||||
|
|||||||
@ -38,8 +38,8 @@ use metronome::BufferTiming;
|
|||||||
use metronome::Metronome;
|
use metronome::Metronome;
|
||||||
use notification_handler::JackNotification;
|
use notification_handler::JackNotification;
|
||||||
use notification_handler::NotificationHandler;
|
use notification_handler::NotificationHandler;
|
||||||
use osc_server::OscController;
|
|
||||||
use osc_server::Osc;
|
use osc_server::Osc;
|
||||||
|
use osc_server::OscController;
|
||||||
use persistence_manager::PersistenceManager;
|
use persistence_manager::PersistenceManager;
|
||||||
use persistence_manager::PersistenceManagerController;
|
use persistence_manager::PersistenceManagerController;
|
||||||
use post_record_handler::PostRecordController;
|
use post_record_handler::PostRecordController;
|
||||||
@ -56,6 +56,7 @@ pub struct JackPorts {
|
|||||||
pub audio_out: jack::Port<jack::AudioOut>,
|
pub audio_out: jack::Port<jack::AudioOut>,
|
||||||
pub click_track_out: jack::Port<jack::AudioOut>,
|
pub click_track_out: jack::Port<jack::AudioOut>,
|
||||||
pub midi_in: jack::Port<jack::MidiIn>,
|
pub midi_in: jack::Port<jack::MidiIn>,
|
||||||
|
pub midi_out: jack::Port<jack::MidiOut>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@ -65,7 +66,9 @@ async fn main() {
|
|||||||
.init()
|
.init()
|
||||||
.expect("Could not initialize logger");
|
.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();
|
let (jack_client, ports) = setup_jack();
|
||||||
|
|
||||||
@ -167,12 +170,16 @@ fn setup_jack() -> (jack::Client, JackPorts) {
|
|||||||
let midi_in = jack_client
|
let midi_in = jack_client
|
||||||
.register_port("midi_in", jack::MidiIn::default())
|
.register_port("midi_in", jack::MidiIn::default())
|
||||||
.expect("Could not create midi_in port");
|
.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 {
|
let ports = JackPorts {
|
||||||
audio_in,
|
audio_in,
|
||||||
audio_out,
|
audio_out,
|
||||||
click_track_out,
|
click_track_out,
|
||||||
midi_in,
|
midi_in,
|
||||||
|
midi_out,
|
||||||
};
|
};
|
||||||
|
|
||||||
(jack_client, ports)
|
(jack_client, ports)
|
||||||
|
|||||||
@ -8,7 +8,7 @@ pub struct Metronome {
|
|||||||
// Timing state
|
// Timing state
|
||||||
frames_per_beat: usize,
|
frames_per_beat: usize,
|
||||||
frames_since_last_beat: usize, // Where we are in the current beat cycle
|
frames_since_last_beat: usize, // Where we are in the current beat cycle
|
||||||
last_frame_time: Option<u32>, // For xrun detection
|
last_frame_time: Option<u32>, // For xrun detection
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@ -39,6 +39,7 @@ impl Metronome {
|
|||||||
&mut self,
|
&mut self,
|
||||||
ps: &jack::ProcessScope,
|
ps: &jack::ProcessScope,
|
||||||
ports: &mut JackPorts,
|
ports: &mut JackPorts,
|
||||||
|
osc_controller: &OscController,
|
||||||
) -> Result<BufferTiming> {
|
) -> Result<BufferTiming> {
|
||||||
let buffer_size = ps.n_frames();
|
let buffer_size = ps.n_frames();
|
||||||
let current_frame_time = ps.last_frame_time();
|
let current_frame_time = ps.last_frame_time();
|
||||||
@ -51,17 +52,90 @@ impl Metronome {
|
|||||||
|
|
||||||
self.render_click(buffer_size, &timing, click_output);
|
self.render_click(buffer_size, &timing, click_output);
|
||||||
|
|
||||||
|
// Process PPQN ticks for this buffer
|
||||||
|
self.process_ppqn_ticks(ps, ports, osc_controller)?;
|
||||||
|
|
||||||
Ok(timing)
|
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]) {
|
fn render_click(&mut self, buffer_size: u32, timing: &BufferTiming, click_output: &mut [f32]) {
|
||||||
let click_length = self.click_samples.sample_count;
|
let click_length = self.click_samples.sample_count;
|
||||||
|
|
||||||
// Calculate our position at the START of this buffer (before calculate_timing updated it)
|
// 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
|
// We need to go back by: buffer_size + any missed samples
|
||||||
let total_advancement = buffer_size as usize + timing.missed_frames;
|
let total_advancement = buffer_size as usize + timing.missed_frames;
|
||||||
let start_position =
|
let start_position = (self.frames_since_last_beat + self.frames_per_beat
|
||||||
(self.frames_since_last_beat + self.frames_per_beat - total_advancement)
|
- total_advancement)
|
||||||
% self.frames_per_beat;
|
% self.frames_per_beat;
|
||||||
|
|
||||||
if let Some(beat_offset) = timing.beat_in_buffer {
|
if let Some(beat_offset) = timing.beat_in_buffer {
|
||||||
@ -139,18 +213,19 @@ impl Metronome {
|
|||||||
let missed = current_frame_time.wrapping_sub(expected) as usize;
|
let missed = current_frame_time.wrapping_sub(expected) as usize;
|
||||||
|
|
||||||
// Check if we missed multiple beats
|
// 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 {
|
if total_samples >= 2 * self.frames_per_beat {
|
||||||
return Err(LooperError::Xrun(std::panic::Location::caller()));
|
return Err(LooperError::Xrun(std::panic::Location::caller()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a beat occurred in the missed section
|
// 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)
|
Some((self.frames_per_beat - self.frames_since_last_beat) as u32)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
(missed, beat_in_missed)
|
(missed, beat_in_missed)
|
||||||
} else {
|
} else {
|
||||||
@ -172,7 +247,8 @@ impl Metronome {
|
|||||||
|
|
||||||
// Update state - advance by total samples (missed + buffer)
|
// Update state - advance by total samples (missed + buffer)
|
||||||
self.frames_since_last_beat =
|
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);
|
self.last_frame_time = Some(current_frame_time);
|
||||||
|
|
||||||
Ok(BufferTiming {
|
Ok(BufferTiming {
|
||||||
|
|||||||
@ -10,31 +10,29 @@ impl OscController {
|
|||||||
Self { sender }
|
Self { sender }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn track_state_changed(
|
pub fn track_state_changed(&self, column: usize, row: usize, state: TrackState) -> Result<()> {
|
||||||
&self,
|
let message = osc::Message::TrackStateChanged {
|
||||||
column: usize,
|
column,
|
||||||
row: usize,
|
row,
|
||||||
state: TrackState,
|
state: state.into(),
|
||||||
) -> Result<()> {
|
};
|
||||||
let message = osc::Message::TrackStateChanged { column, row, state: state.into() };
|
|
||||||
match self.sender.try_send(message) {
|
match self.sender.try_send(message) {
|
||||||
Ok(true) => Ok(()),
|
Ok(true) => Ok(()),
|
||||||
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||||
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn track_volume_changed(
|
pub fn track_volume_changed(&self, column: usize, row: usize, volume: f32) -> Result<()> {
|
||||||
&self,
|
let message = osc::Message::TrackVolumeChanged {
|
||||||
column: usize,
|
column,
|
||||||
row: usize,
|
row,
|
||||||
volume: f32,
|
volume,
|
||||||
) -> Result<()> {
|
};
|
||||||
let message = osc::Message::TrackVolumeChanged { column, row, volume };
|
|
||||||
match self.sender.try_send(message) {
|
match self.sender.try_send(message) {
|
||||||
Ok(true) => Ok(()),
|
Ok(true) => Ok(()),
|
||||||
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||||
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +41,7 @@ impl OscController {
|
|||||||
match self.sender.try_send(message) {
|
match self.sender.try_send(message) {
|
||||||
Ok(true) => Ok(()),
|
Ok(true) => Ok(()),
|
||||||
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||||
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +50,16 @@ impl OscController {
|
|||||||
match self.sender.try_send(message) {
|
match self.sender.try_send(message) {
|
||||||
Ok(true) => Ok(()),
|
Ok(true) => Ok(()),
|
||||||
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||||
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9,7 +9,10 @@ pub(crate) struct PlatformListener {
|
|||||||
|
|
||||||
impl PlatformListener {
|
impl PlatformListener {
|
||||||
pub async fn accept(&mut self) -> Result<PlatformStream> {
|
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()))?;
|
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?;
|
||||||
Ok(stream)
|
Ok(stream)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,9 @@ impl PlatformListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let server = self.current_server.take().unwrap();
|
let server = self.current_server.take().unwrap();
|
||||||
server.connect().await
|
server
|
||||||
|
.connect()
|
||||||
|
.await
|
||||||
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?;
|
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?;
|
||||||
|
|
||||||
Ok(server)
|
Ok(server)
|
||||||
|
|||||||
@ -65,7 +65,10 @@ impl Osc {
|
|||||||
let state_dump = self.shadow_state.create_state_dump();
|
let state_dump = self.shadow_state.create_state_dump();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
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
|
// Send current state dump immediately
|
||||||
for msg in state_dump {
|
for msg in state_dump {
|
||||||
@ -84,10 +87,13 @@ impl Osc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn send_osc_message(
|
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,
|
message: osc::Message,
|
||||||
) -> Result<()> {
|
) -> 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)
|
let osc_bytes = rosc::encoder::encode(&osc_packet)
|
||||||
.map_err(|_| crate::LooperError::StateSave(std::panic::Location::caller()))?;
|
.map_err(|_| crate::LooperError::StateSave(std::panic::Location::caller()))?;
|
||||||
@ -99,41 +105,4 @@ impl Osc {
|
|||||||
|
|
||||||
Ok(())
|
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
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -9,12 +9,20 @@ pub struct PersistenceManagerController {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PersistenceUpdate {
|
pub enum PersistenceUpdate {
|
||||||
UpdateTrackVolume { column: usize, row: usize, volume: f32 },
|
UpdateTrackVolume {
|
||||||
|
column: usize,
|
||||||
|
row: usize,
|
||||||
|
volume: f32,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersistenceManagerController {
|
impl PersistenceManagerController {
|
||||||
pub fn update_track_volume(&self, column: usize, row: usize, volume: f32) -> Result<()> {
|
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) {
|
match self.sender.try_send(update) {
|
||||||
Ok(true) => Ok(()),
|
Ok(true) => Ok(()),
|
||||||
Ok(false) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel full
|
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_tx, update_rx) = kanal::bounded(64);
|
||||||
let update_rx = update_rx.to_async();
|
let update_rx = update_rx.to_async();
|
||||||
let controller = PersistenceManagerController {
|
let controller = PersistenceManagerController { sender: update_tx };
|
||||||
sender: update_tx,
|
|
||||||
};
|
|
||||||
|
|
||||||
let manager = Self {
|
let manager = Self {
|
||||||
state,
|
state,
|
||||||
@ -90,8 +96,13 @@ impl PersistenceManager {
|
|||||||
|
|
||||||
fn handle_state_update(&mut self, update: PersistenceUpdate) -> Result<()> {
|
fn handle_state_update(&mut self, update: PersistenceUpdate) -> Result<()> {
|
||||||
match update {
|
match update {
|
||||||
PersistenceUpdate::UpdateTrackVolume { column, row, volume } => {
|
PersistenceUpdate::UpdateTrackVolume {
|
||||||
self.state.send_modify(|state| state.track_volumes.set_volume(column, row, volume));
|
column,
|
||||||
|
row,
|
||||||
|
volume,
|
||||||
|
} => {
|
||||||
|
self.state
|
||||||
|
.send_modify(|state| state.track_volumes.set_volume(column, row, volume));
|
||||||
self.save_to_disk()?;
|
self.save_to_disk()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,6 +133,7 @@ impl PersistenceManager {
|
|||||||
|
|
||||||
let port_list = match our_port_name {
|
let port_list = match our_port_name {
|
||||||
"midi_in" => &mut connections.midi_in,
|
"midi_in" => &mut connections.midi_in,
|
||||||
|
"midi_out" => &mut connections.midi_out,
|
||||||
"audio_in" => &mut connections.audio_in,
|
"audio_in" => &mut connections.audio_in,
|
||||||
"audio_out" => &mut connections.audio_out,
|
"audio_out" => &mut connections.audio_out,
|
||||||
"click_track" => &mut connections.click_track_out,
|
"click_track" => &mut connections.click_track_out,
|
||||||
|
|||||||
@ -27,11 +27,7 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
osc: OscController,
|
osc: OscController,
|
||||||
persistence: PersistenceManagerController,
|
persistence: PersistenceManagerController,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let track_matrix = TrackMatrix::new(
|
let track_matrix = TrackMatrix::new(client, chunk_factory, state)?;
|
||||||
client,
|
|
||||||
chunk_factory,
|
|
||||||
state,
|
|
||||||
)?;
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
post_record_controller,
|
post_record_controller,
|
||||||
osc,
|
osc,
|
||||||
@ -61,10 +57,8 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
self.selected_column,
|
self.selected_column,
|
||||||
self.selected_row,
|
self.selected_row,
|
||||||
);
|
);
|
||||||
self.track_matrix.handle_volume_update(
|
self.track_matrix
|
||||||
self.last_volume_setting,
|
.handle_volume_update(self.last_volume_setting, &controllers)
|
||||||
&controllers,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_button_1(&mut self) -> Result<()> {
|
pub fn handle_button_1(&mut self) -> Result<()> {
|
||||||
@ -76,10 +70,8 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
self.selected_column,
|
self.selected_column,
|
||||||
self.selected_row,
|
self.selected_row,
|
||||||
);
|
);
|
||||||
self.track_matrix.handle_record_button(
|
self.track_matrix
|
||||||
self.last_volume_setting,
|
.handle_record_button(self.last_volume_setting, &controllers)
|
||||||
&controllers,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_button_2(&mut self) -> Result<()> {
|
pub fn handle_button_2(&mut self) -> Result<()> {
|
||||||
@ -91,9 +83,7 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
self.selected_column,
|
self.selected_column,
|
||||||
self.selected_row,
|
self.selected_row,
|
||||||
);
|
);
|
||||||
self.track_matrix.handle_play_button(
|
self.track_matrix.handle_play_button(&controllers)
|
||||||
&controllers,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_button_3(&mut self) -> Result<()> {
|
pub fn handle_button_3(&mut self) -> Result<()> {
|
||||||
@ -113,9 +103,7 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
self.selected_column,
|
self.selected_column,
|
||||||
self.selected_row,
|
self.selected_row,
|
||||||
);
|
);
|
||||||
self.track_matrix.handle_clear_button(
|
self.track_matrix.handle_clear_button(&controllers)
|
||||||
&controllers,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_button_6(&mut self) -> Result<()> {
|
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> {
|
impl<F: ChunkFactory> ProcessHandler<F> {
|
||||||
fn process_with_error_handling(
|
fn process_with_error_handling(&mut self, ps: &jack::ProcessScope) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
ps: &jack::ProcessScope,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Process metronome and get beat timing information
|
// 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
|
// Process MIDI
|
||||||
midi::process_events(self, ps)?;
|
midi::process_events(self, ps)?;
|
||||||
@ -194,12 +179,8 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
&self.persistence,
|
&self.persistence,
|
||||||
self.sample_rate,
|
self.sample_rate,
|
||||||
);
|
);
|
||||||
self.track_matrix.process(
|
self.track_matrix
|
||||||
ps,
|
.process(ps, &mut self.ports, &timing, &controllers)?;
|
||||||
&mut self.ports,
|
|
||||||
&timing,
|
|
||||||
&controllers,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ pub struct State {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ConnectionState {
|
pub struct ConnectionState {
|
||||||
pub midi_in: Vec<String>,
|
pub midi_in: Vec<String>,
|
||||||
|
pub midi_out: Vec<String>,
|
||||||
pub audio_in: Vec<String>,
|
pub audio_in: Vec<String>,
|
||||||
pub audio_out: Vec<String>,
|
pub audio_out: Vec<String>,
|
||||||
pub click_track_out: Vec<String>,
|
pub click_track_out: Vec<String>,
|
||||||
@ -56,6 +57,7 @@ impl Default for State {
|
|||||||
Self {
|
Self {
|
||||||
connections: ConnectionState {
|
connections: ConnectionState {
|
||||||
midi_in: Vec::new(),
|
midi_in: Vec::new(),
|
||||||
|
midi_out: Vec::new(),
|
||||||
audio_in: Vec::new(),
|
audio_in: Vec::new(),
|
||||||
audio_out: Vec::new(),
|
audio_out: Vec::new(),
|
||||||
click_track_out: Vec::new(),
|
click_track_out: Vec::new(),
|
||||||
|
|||||||
@ -44,33 +44,29 @@ impl Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_idle(&self) -> bool {
|
pub fn is_idle(&self) -> bool {
|
||||||
! self.is_recording() && ! self.is_playing()
|
!self.is_recording() && !self.is_playing()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.audio_data.len()
|
self.audio_data.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_volume(
|
pub fn set_volume(&mut self, new_volume: f32, controllers: &TrackControllers) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
new_volume: f32,
|
|
||||||
controllers: &TrackControllers,
|
|
||||||
) -> Result<()> {
|
|
||||||
let new_volume = new_volume.clamp(0.0, 1.0);
|
let new_volume = new_volume.clamp(0.0, 1.0);
|
||||||
|
|
||||||
match &mut self.current_state {
|
match &mut self.current_state {
|
||||||
TrackState::Recording { volume } => {
|
TrackState::Recording { volume } => {
|
||||||
*volume = new_volume; // Update recording volume only
|
*volume = new_volume; // Update recording volume only
|
||||||
controllers.send_volume_update(new_volume)?;
|
controllers.send_volume_update(new_volume)?;
|
||||||
}
|
}
|
||||||
TrackState::RecordingAutoStop { volume, .. } => {
|
TrackState::RecordingAutoStop { volume, .. } => {
|
||||||
*volume = new_volume; // Update recording volume only
|
*volume = new_volume; // Update recording volume only
|
||||||
controllers.send_volume_update(new_volume)?;
|
controllers.send_volume_update(new_volume)?;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
self.volume = new_volume; // Update committed volume
|
self.volume = new_volume; // Update committed volume
|
||||||
controllers.update_track_volume(new_volume)?; // Update State
|
controllers.update_track_volume(new_volume)?; // Update State
|
||||||
controllers.send_volume_update(new_volume)?; // Update GUI
|
controllers.send_volume_update(new_volume)?; // Update GUI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -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 {
|
self.next_state = TrackState::RecordingAutoStop {
|
||||||
volume: last_volume_setting,
|
volume: last_volume_setting,
|
||||||
target_samples,
|
target_samples,
|
||||||
|
|||||||
@ -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> {
|
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS, ROWS> {
|
||||||
pub fn new(
|
pub fn new(client: &jack::Client, chunk_factory: F, state: &State) -> Result<Self> {
|
||||||
client: &jack::Client,
|
|
||||||
chunk_factory: F,
|
|
||||||
state: &State,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let columns = std::array::from_fn(|_| Column::new(state.metronome.frames_per_beat));
|
let columns = std::array::from_fn(|_| Column::new(state.metronome.frames_per_beat));
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
chunk_factory,
|
chunk_factory,
|
||||||
@ -25,23 +21,14 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
|
|||||||
last_volume_setting: f32,
|
last_volume_setting: f32,
|
||||||
controllers: &TrackControllers,
|
controllers: &TrackControllers,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.columns[controllers.column()].handle_record_button(
|
self.columns[controllers.column()].handle_record_button(last_volume_setting, controllers)
|
||||||
last_volume_setting,
|
|
||||||
controllers,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_play_button(
|
pub fn handle_play_button(&mut self, controllers: &TrackControllers) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
controllers: &TrackControllers,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.columns[controllers.column()].handle_play_button(controllers)
|
self.columns[controllers.column()].handle_play_button(controllers)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_clear_button(
|
pub fn handle_clear_button(&mut self, controllers: &TrackControllers) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
controllers: &TrackControllers,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.columns[controllers.column()].handle_clear_button(controllers)
|
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,
|
new_volume: f32,
|
||||||
controllers: &TrackControllers,
|
controllers: &TrackControllers,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.columns[controllers.column()].handle_volume_update(
|
self.columns[controllers.column()].handle_volume_update(new_volume, controllers)
|
||||||
new_volume,
|
|
||||||
controllers,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process(
|
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() {
|
for (i, column) in &mut self.columns.iter_mut().enumerate() {
|
||||||
let controllers = controllers.to_column_controllers(i);
|
let controllers = controllers.to_column_controllers(i);
|
||||||
|
|
||||||
column.handle_xrun(
|
column.handle_xrun(&timing, &mut self.chunk_factory, &controllers)?;
|
||||||
&timing,
|
|
||||||
&mut self.chunk_factory,
|
|
||||||
&controllers,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,9 @@ pub enum Message {
|
|||||||
SelectedRowChanged {
|
SelectedRowChanged {
|
||||||
row: usize,
|
row: usize,
|
||||||
},
|
},
|
||||||
|
MetronomePosition {
|
||||||
|
position: f32, // 0.0 - 1.0 position within current beat
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
@ -53,6 +56,9 @@ impl Message {
|
|||||||
} => {
|
} => {
|
||||||
state.set_track_volume(*column, *row, *volume);
|
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
|
let row = (*row_1based as usize).saturating_sub(1); // Convert to 0-based
|
||||||
return Ok(Some(Message::SelectedRowChanged { row }));
|
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
|
// Unknown or unsupported message
|
||||||
Ok(None)
|
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)],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -21,6 +21,8 @@ pub struct State {
|
|||||||
pub cells: Vec<Vec<Track>>,
|
pub cells: Vec<Vec<Track>>,
|
||||||
pub columns: usize,
|
pub columns: usize,
|
||||||
pub rows: 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 {
|
impl State {
|
||||||
@ -35,6 +37,8 @@ impl State {
|
|||||||
cells,
|
cells,
|
||||||
columns,
|
columns,
|
||||||
rows,
|
rows,
|
||||||
|
metronome_position: 0.0,
|
||||||
|
metronome_timestamp: std::time::Instant::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +64,9 @@ impl State {
|
|||||||
self.selected_row = *row;
|
self.selected_row = *row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::MetronomePosition { position } => {
|
||||||
|
self.set_metronome_position(*position);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,4 +123,9 @@ impl State {
|
|||||||
self.cells[column][row].volume = volume;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user