/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 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
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user