Added track volume control to audio engine
This commit is contained in:
parent
b68e3af28a
commit
70b2f92a8d
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
|
config/
|
||||||
collect.txt
|
collect.txt
|
||||||
@ -10,8 +10,8 @@ pub struct Args {
|
|||||||
#[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 file
|
/// Path to configuration dir
|
||||||
#[arg(short = 'c', long = "config-file", env = "CONFIG_FILE", default_value = default_config_path())]
|
#[arg(short = 'c', long = "config-path", env = "CONFIG_PATH", default_value = default_config_path())]
|
||||||
pub config_file: PathBuf,
|
pub config_file: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,8 +26,11 @@ impl Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_config_path() -> String {
|
fn default_config_path() -> String {
|
||||||
|
/*
|
||||||
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||||
path.push(".fcb_looper");
|
path.push(".fcb_looper");
|
||||||
path.push("state.json");
|
path.push("state.json");
|
||||||
path.to_string_lossy().to_string()
|
path.to_string_lossy().to_string()
|
||||||
|
*/
|
||||||
|
"../config".to_string()
|
||||||
}
|
}
|
||||||
@ -37,9 +37,14 @@ impl<const ROWS: usize> Column<ROWS> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_record_button(&mut self, row: usize) -> Result<()> {
|
pub fn handle_record_button(
|
||||||
|
&mut self,
|
||||||
|
last_volume_setting: f32,
|
||||||
|
controllers: &TrackControllers,
|
||||||
|
) -> Result<()> {
|
||||||
let len = self.len();
|
let len = self.len();
|
||||||
let track = &mut self.tracks[row];
|
let track = &mut self.tracks[controllers.row()];
|
||||||
|
|
||||||
if track.is_recording() {
|
if track.is_recording() {
|
||||||
if len > 0 {
|
if len > 0 {
|
||||||
track.clear();
|
track.clear();
|
||||||
@ -49,71 +54,76 @@ impl<const ROWS: usize> Column<ROWS> {
|
|||||||
} else {
|
} else {
|
||||||
if len > 0 {
|
if len > 0 {
|
||||||
let sync_offset = len - self.playback_position;
|
let sync_offset = len - self.playback_position;
|
||||||
track.record_auto_stop(len, sync_offset);
|
track.record_auto_stop(len, sync_offset, last_volume_setting);
|
||||||
} else {
|
} else {
|
||||||
track.record();
|
track.record(last_volume_setting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_play_button(&mut self, row: usize) -> Result<()> {
|
pub fn handle_play_button(
|
||||||
let track = &mut self.tracks[row];
|
&mut self,
|
||||||
if track.len() > 0 && track.is_idle() {
|
controllers: &TrackControllers,
|
||||||
|
) -> Result<()> {
|
||||||
|
let track = &mut self.tracks[controllers.row()];
|
||||||
|
if track.len() > 0 && track.is_idle() {
|
||||||
track.play();
|
track.play();
|
||||||
} else if ! track.is_idle() {
|
} else if !track.is_idle() {
|
||||||
track.stop();
|
track.stop();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_clear_button(&mut self, row: usize) -> Result<()> {
|
pub fn handle_clear_button(
|
||||||
let track = &mut self.tracks[row];
|
&mut self,
|
||||||
|
controllers: &TrackControllers,
|
||||||
|
) -> Result<()> {
|
||||||
|
let track = &mut self.tracks[controllers.row()];
|
||||||
track.clear();
|
track.clear();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_volume_update(
|
||||||
|
&mut self,
|
||||||
|
new_volume: f32,
|
||||||
|
controllers: &TrackControllers,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.tracks[controllers.row()].set_volume(new_volume, controllers)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_consolidated_buffer(&mut self, row: usize, buffer: Box<[f32]>) -> Result<()> {
|
pub fn set_consolidated_buffer(&mut self, row: usize, buffer: Box<[f32]>) -> Result<()> {
|
||||||
self.tracks[row].set_consolidated_buffer(buffer)
|
self.tracks[row].set_consolidated_buffer(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_xrun<PRC, OC>(
|
pub fn handle_xrun(
|
||||||
&mut self,
|
&mut self,
|
||||||
timing: &BufferTiming,
|
timing: &BufferTiming,
|
||||||
chunk_factory: &mut impl ChunkFactory,
|
chunk_factory: &mut impl ChunkFactory,
|
||||||
post_record_controller: PRC,
|
controllers: &ColumnControllers,
|
||||||
osc_controller: OC,
|
) -> Result<()> {
|
||||||
) -> Result<()>
|
|
||||||
where
|
|
||||||
PRC: Fn(usize, Arc<AudioChunk>, usize) -> Result<()>,
|
|
||||||
OC: Fn(usize, TrackState) -> Result<()>,
|
|
||||||
{
|
|
||||||
for (row, track) in self.tracks.iter_mut().enumerate() {
|
for (row, track) in self.tracks.iter_mut().enumerate() {
|
||||||
|
let track_controllers = controllers.to_track_controllers(row);
|
||||||
|
|
||||||
track.handle_xrun(
|
track.handle_xrun(
|
||||||
timing.beat_in_missed,
|
timing.beat_in_missed,
|
||||||
timing.missed_frames,
|
timing.missed_frames,
|
||||||
chunk_factory,
|
chunk_factory,
|
||||||
|chunk, sync_offset| post_record_controller(row, chunk, sync_offset),
|
&track_controllers,
|
||||||
|state| osc_controller(row, state),
|
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process<PRC, OC>(
|
pub fn process(
|
||||||
&mut self,
|
&mut self,
|
||||||
timing: &BufferTiming,
|
timing: &BufferTiming,
|
||||||
input_buffer: &[f32],
|
input_buffer: &[f32],
|
||||||
output_buffer: &mut [f32],
|
output_buffer: &mut [f32],
|
||||||
scratch_pad: &mut [f32],
|
scratch_pad: &mut [f32],
|
||||||
chunk_factory: &mut impl ChunkFactory,
|
chunk_factory: &mut impl ChunkFactory,
|
||||||
post_record_controller: PRC,
|
controllers: &ColumnControllers,
|
||||||
osc_controller: OC,
|
) -> Result<()> {
|
||||||
) -> Result<()>
|
|
||||||
where
|
|
||||||
PRC: Fn(usize, Arc<AudioChunk>, usize) -> Result<()>,
|
|
||||||
OC: Fn(usize, TrackState) -> Result<()>,
|
|
||||||
{
|
|
||||||
let len = self.len();
|
let len = self.len();
|
||||||
if self.idle() {
|
if self.idle() {
|
||||||
if let Some(beat_index) = timing.beat_in_buffer {
|
if let Some(beat_index) = timing.beat_in_buffer {
|
||||||
@ -125,20 +135,24 @@ impl<const ROWS: usize> Column<ROWS> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (row, track) in self.tracks.iter_mut().enumerate() {
|
for (row, track) in self.tracks.iter_mut().enumerate() {
|
||||||
|
let track_controllers = controllers.to_track_controllers(row);
|
||||||
|
|
||||||
track.process(
|
track.process(
|
||||||
self.playback_position,
|
self.playback_position,
|
||||||
timing.beat_in_buffer,
|
timing.beat_in_buffer,
|
||||||
input_buffer,
|
input_buffer,
|
||||||
scratch_pad,
|
scratch_pad,
|
||||||
chunk_factory,
|
chunk_factory,
|
||||||
|chunk, sync_offset| post_record_controller(row, chunk, sync_offset),
|
&track_controllers,
|
||||||
|state| osc_controller(row, state),
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for (output_val, scratch_pad_val) in output_buffer.iter_mut().zip(scratch_pad.iter()) {
|
for (output_val, scratch_pad_val) in output_buffer.iter_mut().zip(scratch_pad.iter()) {
|
||||||
*output_val += *scratch_pad_val;
|
*output_val += *scratch_pad_val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let len = self.len();
|
let len = self.len();
|
||||||
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();
|
||||||
|
|||||||
118
audio_engine/src/controllers.rs
Normal file
118
audio_engine/src/controllers.rs
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub struct MatrixControllers<'a> {
|
||||||
|
post_record: &'a PostRecordController,
|
||||||
|
osc: &'a OscController,
|
||||||
|
persistence: &'a PersistenceManagerController,
|
||||||
|
sample_rate: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ColumnControllers<'a> {
|
||||||
|
post_record: &'a PostRecordController,
|
||||||
|
osc: &'a OscController,
|
||||||
|
persistence: &'a PersistenceManagerController,
|
||||||
|
sample_rate: usize,
|
||||||
|
column: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TrackControllers<'a> {
|
||||||
|
post_record: &'a PostRecordController,
|
||||||
|
osc: &'a OscController,
|
||||||
|
persistence: &'a PersistenceManagerController,
|
||||||
|
sample_rate: usize,
|
||||||
|
column: usize,
|
||||||
|
row: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MatrixControllers<'a> {
|
||||||
|
pub fn new(
|
||||||
|
post_record: &'a PostRecordController,
|
||||||
|
osc: &'a OscController,
|
||||||
|
persistence: &'a PersistenceManagerController,
|
||||||
|
sample_rate: usize,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
post_record,
|
||||||
|
osc,
|
||||||
|
persistence,
|
||||||
|
sample_rate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_column_controllers(&self, column: usize) -> ColumnControllers<'a> {
|
||||||
|
ColumnControllers {
|
||||||
|
post_record: self.post_record,
|
||||||
|
osc: self.osc,
|
||||||
|
persistence: self.persistence,
|
||||||
|
sample_rate: self.sample_rate,
|
||||||
|
column,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_recv_post_record_response(&self) -> Option<PostRecordResponse> {
|
||||||
|
self.post_record.try_recv_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ColumnControllers<'a> {
|
||||||
|
pub fn to_track_controllers(&self, row: usize) -> TrackControllers<'a> {
|
||||||
|
TrackControllers {
|
||||||
|
post_record: self.post_record,
|
||||||
|
osc: self.osc,
|
||||||
|
persistence: self.persistence,
|
||||||
|
sample_rate: self.sample_rate,
|
||||||
|
column: self.column,
|
||||||
|
row,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TrackControllers<'a> {
|
||||||
|
pub fn new(
|
||||||
|
post_record: &'a PostRecordController,
|
||||||
|
osc: &'a OscController,
|
||||||
|
persistence: &'a PersistenceManagerController,
|
||||||
|
sample_rate: usize,
|
||||||
|
column: usize,
|
||||||
|
row: usize,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
post_record,
|
||||||
|
osc,
|
||||||
|
persistence,
|
||||||
|
sample_rate,
|
||||||
|
column,
|
||||||
|
row,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<()> {
|
||||||
|
self.osc.track_state_changed(self.column, self.row, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_volume_update(&self, volume: f32) -> Result<()> {
|
||||||
|
self.osc.track_volume_changed(self.column, self.row, volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_track_volume(&self, volume: f32) -> Result<()> {
|
||||||
|
self.persistence.update_track_volume(self.column, self.row, volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn column(&self) -> usize {
|
||||||
|
self.column
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn row(&self) -> usize {
|
||||||
|
self.row
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ mod beep;
|
|||||||
mod chunk_factory;
|
mod chunk_factory;
|
||||||
mod column;
|
mod column;
|
||||||
mod connection_manager;
|
mod connection_manager;
|
||||||
|
mod controllers;
|
||||||
mod looper_error;
|
mod looper_error;
|
||||||
mod metronome;
|
mod metronome;
|
||||||
mod midi;
|
mod midi;
|
||||||
@ -28,6 +29,9 @@ use beep::generate_beep;
|
|||||||
use chunk_factory::ChunkFactory;
|
use chunk_factory::ChunkFactory;
|
||||||
use column::Column;
|
use column::Column;
|
||||||
use connection_manager::ConnectionManager;
|
use connection_manager::ConnectionManager;
|
||||||
|
use controllers::ColumnControllers;
|
||||||
|
use controllers::MatrixControllers;
|
||||||
|
use controllers::TrackControllers;
|
||||||
use looper_error::LooperError;
|
use looper_error::LooperError;
|
||||||
use looper_error::Result;
|
use looper_error::Result;
|
||||||
use metronome::BufferTiming;
|
use metronome::BufferTiming;
|
||||||
@ -37,8 +41,10 @@ use notification_handler::NotificationHandler;
|
|||||||
use osc::OscController;
|
use osc::OscController;
|
||||||
use osc::Osc;
|
use osc::Osc;
|
||||||
use persistence_manager::PersistenceManager;
|
use persistence_manager::PersistenceManager;
|
||||||
|
use persistence_manager::PersistenceManagerController;
|
||||||
use post_record_handler::PostRecordController;
|
use post_record_handler::PostRecordController;
|
||||||
use post_record_handler::PostRecordHandler;
|
use post_record_handler::PostRecordHandler;
|
||||||
|
use post_record_handler::PostRecordResponse;
|
||||||
use process_handler::ProcessHandler;
|
use process_handler::ProcessHandler;
|
||||||
use state::State;
|
use state::State;
|
||||||
use track::Track;
|
use track::Track;
|
||||||
@ -71,29 +77,28 @@ async fn main() {
|
|||||||
let notification_handler = NotificationHandler::new();
|
let notification_handler = NotificationHandler::new();
|
||||||
let mut notification_channel = notification_handler.subscribe();
|
let mut notification_channel = notification_handler.subscribe();
|
||||||
|
|
||||||
let (mut persistence_manager, state_watch) =
|
let (mut persistence_manager, persistence_controller) =
|
||||||
PersistenceManager::new(&args, notification_handler.subscribe());
|
PersistenceManager::new(&args, notification_handler.subscribe());
|
||||||
|
let state = persistence_manager.state();
|
||||||
// Load state values for metronome configuration
|
|
||||||
let initial_state = state_watch.borrow().clone();
|
|
||||||
|
|
||||||
// Create post-record handler and get controller for ProcessHandler
|
// Create post-record handler and get controller for ProcessHandler
|
||||||
let (mut post_record_handler, post_record_controller) =
|
let (mut post_record_handler, post_record_controller) =
|
||||||
PostRecordHandler::new().expect("Could not create post-record handler");
|
PostRecordHandler::new(&args).expect("Could not create post-record handler");
|
||||||
|
|
||||||
let process_handler = ProcessHandler::new(
|
let process_handler = ProcessHandler::new(
|
||||||
&jack_client,
|
&jack_client,
|
||||||
ports,
|
ports,
|
||||||
allocator,
|
allocator,
|
||||||
beep_samples,
|
beep_samples,
|
||||||
&initial_state,
|
&state.borrow(),
|
||||||
osc_controller,
|
|
||||||
post_record_controller,
|
post_record_controller,
|
||||||
|
osc_controller,
|
||||||
|
persistence_controller,
|
||||||
)
|
)
|
||||||
.expect("Could not create process handler");
|
.expect("Could not create process handler");
|
||||||
|
|
||||||
let mut connection_manager = ConnectionManager::new(
|
let mut connection_manager = ConnectionManager::new(
|
||||||
state_watch,
|
persistence_manager.state(),
|
||||||
notification_handler.subscribe(),
|
notification_handler.subscribe(),
|
||||||
jack_client.name().to_string(),
|
jack_client.name().to_string(),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -34,48 +34,53 @@ pub fn process_events<F: ChunkFactory>(
|
|||||||
Ok(message) => {
|
Ok(message) => {
|
||||||
match message {
|
match message {
|
||||||
wmidi::MidiMessage::ControlChange(_, controller, value) => {
|
wmidi::MidiMessage::ControlChange(_, controller, value) => {
|
||||||
// Only process button presses (value > 0)
|
let controller_num = u8::from(controller);
|
||||||
if u8::from(value) > 0 {
|
let value_num = u8::from(value);
|
||||||
match u8::from(controller) {
|
|
||||||
20 => {
|
match controller_num {
|
||||||
process_handler.handle_button_1()?;
|
// Expression Pedal A - Track Volume
|
||||||
}
|
1 => {
|
||||||
21 => {
|
process_handler.handle_pedal_a(value_num)?;
|
||||||
process_handler.handle_button_2()?;
|
}
|
||||||
}
|
// Button mappings (only process button presses - value > 0)
|
||||||
22 => {
|
20 if value_num > 0 => {
|
||||||
process_handler.handle_button_3()?;
|
process_handler.handle_button_1()?;
|
||||||
}
|
}
|
||||||
23 => {
|
21 if value_num > 0 => {
|
||||||
process_handler.handle_button_4()?;
|
process_handler.handle_button_2()?;
|
||||||
}
|
}
|
||||||
24 => {
|
22 if value_num > 0 => {
|
||||||
process_handler.handle_button_5()?;
|
process_handler.handle_button_3()?;
|
||||||
}
|
}
|
||||||
25 => {
|
23 if value_num > 0 => {
|
||||||
process_handler.handle_button_6()?;
|
process_handler.handle_button_4()?;
|
||||||
}
|
}
|
||||||
26 => {
|
24 if value_num > 0 => {
|
||||||
process_handler.handle_button_7()?;
|
process_handler.handle_button_5()?;
|
||||||
}
|
}
|
||||||
27 => {
|
25 if value_num > 0 => {
|
||||||
process_handler.handle_button_8()?;
|
process_handler.handle_button_6()?;
|
||||||
}
|
}
|
||||||
28 => {
|
26 if value_num > 0 => {
|
||||||
process_handler.handle_button_9()?;
|
process_handler.handle_button_7()?;
|
||||||
}
|
}
|
||||||
29 => {
|
27 if value_num > 0 => {
|
||||||
process_handler.handle_button_10()?;
|
process_handler.handle_button_8()?;
|
||||||
}
|
}
|
||||||
30 => {
|
28 if value_num > 0 => {
|
||||||
process_handler.handle_button_up()?;
|
process_handler.handle_button_9()?;
|
||||||
}
|
}
|
||||||
31 => {
|
29 if value_num > 0 => {
|
||||||
process_handler.handle_button_down()?;
|
process_handler.handle_button_10()?;
|
||||||
}
|
}
|
||||||
_ => {
|
30 if value_num > 0 => {
|
||||||
// Other CC messages - ignore
|
process_handler.handle_button_up()?;
|
||||||
}
|
}
|
||||||
|
31 if value_num > 0 => {
|
||||||
|
process_handler.handle_button_down()?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Other CC messages - ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,20 @@ impl OscController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn track_volume_changed(
|
||||||
|
&self,
|
||||||
|
column: usize,
|
||||||
|
row: usize,
|
||||||
|
volume: f32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let message = OscMessage::TrackVolumeChanged { column, row, volume };
|
||||||
|
match self.sender.try_send(message) {
|
||||||
|
Ok(true) => Ok(()),
|
||||||
|
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||||
|
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selected_column_changed(&self, column: usize) -> Result<()> {
|
pub fn selected_column_changed(&self, column: usize) -> Result<()> {
|
||||||
let message = OscMessage::SelectedColumnChanged { column };
|
let message = OscMessage::SelectedColumnChanged { column };
|
||||||
match self.sender.try_send(message) {
|
match self.sender.try_send(message) {
|
||||||
|
|||||||
@ -7,6 +7,11 @@ pub(crate) enum OscMessage {
|
|||||||
row: usize,
|
row: usize,
|
||||||
state: TrackState,
|
state: TrackState,
|
||||||
},
|
},
|
||||||
|
TrackVolumeChanged {
|
||||||
|
column: usize,
|
||||||
|
row: usize,
|
||||||
|
volume: f32,
|
||||||
|
},
|
||||||
SelectedColumnChanged {
|
SelectedColumnChanged {
|
||||||
column: usize,
|
column: usize,
|
||||||
},
|
},
|
||||||
@ -15,11 +20,17 @@ pub(crate) enum OscMessage {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct Track {
|
||||||
|
pub state: TrackState,
|
||||||
|
pub volume: f32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ShadowState {
|
pub(crate) struct ShadowState {
|
||||||
pub selected_column: usize, // 0-based (converted to 1-based for OSC)
|
pub selected_column: usize, // 0-based (converted to 1-based for OSC)
|
||||||
pub selected_row: usize, // 0-based (converted to 1-based for OSC)
|
pub selected_row: usize, // 0-based (converted to 1-based for OSC)
|
||||||
pub cells: Vec<Vec<TrackState>>,
|
pub cells: Vec<Vec<Track>>,
|
||||||
pub columns: usize,
|
pub columns: usize,
|
||||||
pub rows: usize,
|
pub rows: usize,
|
||||||
}
|
}
|
||||||
@ -27,7 +38,7 @@ pub(crate) struct ShadowState {
|
|||||||
impl ShadowState {
|
impl ShadowState {
|
||||||
pub fn new(columns: usize, rows: usize) -> Self {
|
pub fn new(columns: usize, rows: usize) -> Self {
|
||||||
let cells = (0..columns)
|
let cells = (0..columns)
|
||||||
.map(|_| vec![TrackState::Empty; rows])
|
.map(|_| vec![Track { state: TrackState::Empty, volume: 1.0 }; rows])
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@ -43,7 +54,12 @@ impl ShadowState {
|
|||||||
match message {
|
match message {
|
||||||
OscMessage::TrackStateChanged { column, row, state } => {
|
OscMessage::TrackStateChanged { column, row, state } => {
|
||||||
if *column < self.columns && *row < self.rows {
|
if *column < self.columns && *row < self.rows {
|
||||||
self.cells[*column][*row] = state.clone();
|
self.cells[*column][*row].state = state.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OscMessage::TrackVolumeChanged { column, row, volume } => {
|
||||||
|
if *column < self.columns && *row < self.rows {
|
||||||
|
self.cells[*column][*row].volume = *volume;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OscMessage::SelectedColumnChanged { column } => {
|
OscMessage::SelectedColumnChanged { column } => {
|
||||||
@ -70,13 +86,18 @@ impl ShadowState {
|
|||||||
row: self.selected_row,
|
row: self.selected_row,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send all cell states
|
// Send all cell states and volumes
|
||||||
for (column, column_cells) in self.cells.iter().enumerate() {
|
for (column, column_cells) in self.cells.iter().enumerate() {
|
||||||
for (row, cell_state) in column_cells.iter().enumerate() {
|
for (row, track) in column_cells.iter().enumerate() {
|
||||||
messages.push(OscMessage::TrackStateChanged {
|
messages.push(OscMessage::TrackStateChanged {
|
||||||
column,
|
column,
|
||||||
row,
|
row,
|
||||||
state: cell_state.clone(),
|
state: track.state.clone(),
|
||||||
|
});
|
||||||
|
messages.push(OscMessage::TrackVolumeChanged {
|
||||||
|
column,
|
||||||
|
row,
|
||||||
|
volume: track.volume,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -119,6 +119,14 @@ impl Osc {
|
|||||||
args: vec![rosc::OscType::String(osc_state)],
|
args: vec![rosc::OscType::String(osc_state)],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
OscMessage::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)],
|
||||||
|
}))
|
||||||
|
}
|
||||||
OscMessage::SelectedColumnChanged { column } => {
|
OscMessage::SelectedColumnChanged { column } => {
|
||||||
let address = "/looper/selected/column".to_string();
|
let address = "/looper/selected/column".to_string();
|
||||||
|
|
||||||
@ -142,7 +150,8 @@ impl Osc {
|
|||||||
match state {
|
match state {
|
||||||
crate::TrackState::Empty => "empty".to_string(),
|
crate::TrackState::Empty => "empty".to_string(),
|
||||||
crate::TrackState::Idle => "ready".to_string(),
|
crate::TrackState::Idle => "ready".to_string(),
|
||||||
crate::TrackState::Recording | crate::TrackState::RecordingAutoStop { .. } => "recording".to_string(),
|
crate::TrackState::Recording { .. } => "recording".to_string(),
|
||||||
|
crate::TrackState::RecordingAutoStop { .. } => "recording".to_string(),
|
||||||
crate::TrackState::Playing => "playing".to_string(),
|
crate::TrackState::Playing => "playing".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,31 @@ use crate::*;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tokio::sync::{broadcast, watch};
|
use tokio::sync::{broadcast, watch};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PersistenceManagerController {
|
||||||
|
sender: kanal::Sender<PersistenceUpdate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PersistenceUpdate {
|
||||||
|
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 };
|
||||||
|
match self.sender.try_send(update) {
|
||||||
|
Ok(true) => Ok(()),
|
||||||
|
Ok(false) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel full
|
||||||
|
Err(_) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PersistenceManager {
|
pub struct PersistenceManager {
|
||||||
state: State,
|
state: watch::Sender<State>,
|
||||||
state_tx: watch::Sender<State>,
|
|
||||||
notification_rx: broadcast::Receiver<JackNotification>,
|
notification_rx: broadcast::Receiver<JackNotification>,
|
||||||
|
update_rx: kanal::AsyncReceiver<PersistenceUpdate>,
|
||||||
state_file_path: PathBuf,
|
state_file_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -13,33 +34,65 @@ impl PersistenceManager {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
args: &Args,
|
args: &Args,
|
||||||
notification_rx: broadcast::Receiver<JackNotification>,
|
notification_rx: broadcast::Receiver<JackNotification>,
|
||||||
) -> (Self, watch::Receiver<State>) {
|
) -> (Self, PersistenceManagerController) {
|
||||||
let state_file_path = args.config_file.clone();
|
let state_file_path = args.config_file.join("state.json");
|
||||||
if let Some(parent) = state_file_path.parent() {
|
std::fs::create_dir_all(&args.config_file).ok(); // Create directory if it doesn't exist
|
||||||
std::fs::create_dir_all(parent).ok(); // Create directory if it doesn't exist
|
|
||||||
}
|
|
||||||
let initial_state = Self::load_from_disk(&state_file_path).unwrap_or_default();
|
let initial_state = Self::load_from_disk(&state_file_path).unwrap_or_default();
|
||||||
let (state_tx, state_rx) = watch::channel(initial_state.clone());
|
let (state, _) = watch::channel(initial_state);
|
||||||
|
|
||||||
|
let (update_tx, update_rx) = kanal::bounded(64);
|
||||||
|
let update_rx = update_rx.to_async();
|
||||||
|
let controller = PersistenceManagerController {
|
||||||
|
sender: update_tx,
|
||||||
|
};
|
||||||
|
|
||||||
let manager = Self {
|
let manager = Self {
|
||||||
state: initial_state,
|
state,
|
||||||
state_tx,
|
|
||||||
notification_rx,
|
notification_rx,
|
||||||
|
update_rx,
|
||||||
state_file_path,
|
state_file_path,
|
||||||
};
|
};
|
||||||
|
|
||||||
(manager, state_rx)
|
(manager, controller)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state(&self) -> watch::Receiver<State> {
|
||||||
|
self.state.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(&mut self) -> Result<()> {
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
while let Ok(notification) = self.notification_rx.recv().await {
|
loop {
|
||||||
if let JackNotification::PortConnect {
|
tokio::select! {
|
||||||
port_a,
|
notification = self.notification_rx.recv() => {
|
||||||
port_b,
|
match notification {
|
||||||
connected,
|
Ok(JackNotification::PortConnect { port_a, port_b, connected }) => {
|
||||||
} = notification
|
self.handle_port_connection(port_a, port_b, connected)?;
|
||||||
{
|
}
|
||||||
self.handle_port_connection(port_a, port_b, connected)?;
|
Ok(_) => {}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle state updates
|
||||||
|
update = self.update_rx.recv() => {
|
||||||
|
match update {
|
||||||
|
Ok(update) => {
|
||||||
|
self.handle_state_update(update)?;
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
self.save_to_disk()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,37 +117,36 @@ impl PersistenceManager {
|
|||||||
|
|
||||||
// Update the connections state
|
// Update the connections state
|
||||||
let our_port_name = our_port.strip_prefix("looper:").unwrap_or(&our_port);
|
let our_port_name = our_port.strip_prefix("looper:").unwrap_or(&our_port);
|
||||||
let connections = &mut self.state.connections;
|
self.state.send_if_modified(|state| {
|
||||||
|
let connections = &mut state.connections;
|
||||||
|
|
||||||
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,
|
||||||
"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,
|
||||||
_ => {
|
_ => {
|
||||||
log::warn!("Unknown port: {}", our_port_name);
|
log::warn!("Unknown port: {}", our_port_name);
|
||||||
return Ok(());
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if connected {
|
||||||
|
// Add connection if not already present
|
||||||
|
if !port_list.contains(&external_port) {
|
||||||
|
port_list.push(external_port.clone());
|
||||||
|
log::info!("Added connection: {} -> {}", our_port, external_port);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove connection
|
||||||
|
port_list.retain(|p| p != &external_port);
|
||||||
|
log::info!("Removed connection: {} -> {}", our_port, external_port);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
if connected {
|
|
||||||
// Add connection if not already present
|
|
||||||
if !port_list.contains(&external_port) {
|
|
||||||
port_list.push(external_port.clone());
|
|
||||||
log::info!("Added connection: {} -> {}", our_port, external_port);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Remove connection
|
|
||||||
port_list.retain(|p| p != &external_port);
|
|
||||||
log::info!("Removed connection: {} -> {}", our_port, external_port);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast state change
|
|
||||||
if let Err(e) = self.state_tx.send(self.state.clone()) {
|
|
||||||
log::error!("Failed to broadcast state change: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to disk
|
|
||||||
self.save_to_disk()?;
|
self.save_to_disk()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -116,7 +168,7 @@ impl PersistenceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn save_to_disk(&self) -> Result<()> {
|
fn save_to_disk(&self) -> Result<()> {
|
||||||
let json = serde_json::to_string_pretty(&self.state)
|
let json = serde_json::to_string_pretty(&*self.state.borrow())
|
||||||
.map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?;
|
.map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?;
|
||||||
|
|
||||||
std::fs::write(&self.state_file_path, json)
|
std::fs::write(&self.state_file_path, json)
|
||||||
|
|||||||
@ -69,7 +69,7 @@ pub struct PostRecordHandler {
|
|||||||
|
|
||||||
impl PostRecordHandler {
|
impl PostRecordHandler {
|
||||||
/// Create new handler and return RT controller
|
/// Create new handler and return RT controller
|
||||||
pub fn new() -> Result<(Self, PostRecordController)> {
|
pub fn new(args: &Args) -> Result<(Self, PostRecordController)> {
|
||||||
// Create channels for bidirectional communication
|
// Create channels for bidirectional communication
|
||||||
let (request_sender, request_receiver) = kanal::bounded(16);
|
let (request_sender, request_receiver) = kanal::bounded(16);
|
||||||
let (response_sender, response_receiver) = kanal::bounded(16);
|
let (response_sender, response_receiver) = kanal::bounded(16);
|
||||||
@ -85,7 +85,7 @@ impl PostRecordHandler {
|
|||||||
let handler = Self {
|
let handler = Self {
|
||||||
request_receiver,
|
request_receiver,
|
||||||
response_sender,
|
response_sender,
|
||||||
directory: Self::create_directory()?,
|
directory: args.config_file.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((handler, controller))
|
Ok((handler, controller))
|
||||||
@ -219,18 +219,6 @@ impl PostRecordHandler {
|
|||||||
.map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?
|
.map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create save directory and return path
|
|
||||||
fn create_directory() -> Result<PathBuf> {
|
|
||||||
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
|
||||||
path.push(".fcb_looper");
|
|
||||||
|
|
||||||
std::fs::create_dir_all(&path)
|
|
||||||
.map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?;
|
|
||||||
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get file path for track recording
|
|
||||||
fn get_file_path(&self, row: usize) -> PathBuf {
|
fn get_file_path(&self, row: usize) -> PathBuf {
|
||||||
self.directory.join(format!("row_{row}.wav"))
|
self.directory.join(format!("row_{row}.wav"))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,16 @@ const COLS: usize = 5;
|
|||||||
const ROWS: usize = 5;
|
const ROWS: usize = 5;
|
||||||
|
|
||||||
pub struct ProcessHandler<F: ChunkFactory> {
|
pub struct ProcessHandler<F: ChunkFactory> {
|
||||||
|
post_record_controller: PostRecordController,
|
||||||
osc: OscController,
|
osc: OscController,
|
||||||
|
persistence: PersistenceManagerController,
|
||||||
ports: JackPorts,
|
ports: JackPorts,
|
||||||
metronome: Metronome,
|
metronome: Metronome,
|
||||||
track_matrix: TrackMatrix<F, COLS, ROWS>,
|
track_matrix: TrackMatrix<F, COLS, ROWS>,
|
||||||
selected_row: usize,
|
selected_row: usize,
|
||||||
selected_column: usize,
|
selected_column: usize,
|
||||||
|
last_volume_setting: f32,
|
||||||
|
sample_rate: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F: ChunkFactory> ProcessHandler<F> {
|
impl<F: ChunkFactory> ProcessHandler<F> {
|
||||||
@ -19,22 +23,26 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
chunk_factory: F,
|
chunk_factory: F,
|
||||||
beep_samples: Arc<AudioChunk>,
|
beep_samples: Arc<AudioChunk>,
|
||||||
state: &State,
|
state: &State,
|
||||||
osc: OscController,
|
|
||||||
post_record_controller: PostRecordController,
|
post_record_controller: PostRecordController,
|
||||||
|
osc: OscController,
|
||||||
|
persistence: PersistenceManagerController,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let track_matrix = TrackMatrix::new(
|
let track_matrix = TrackMatrix::new(
|
||||||
client,
|
client,
|
||||||
chunk_factory,
|
chunk_factory,
|
||||||
state,
|
state,
|
||||||
post_record_controller,
|
|
||||||
)?;
|
)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
post_record_controller,
|
||||||
osc,
|
osc,
|
||||||
|
persistence,
|
||||||
ports,
|
ports,
|
||||||
metronome: Metronome::new(beep_samples, state),
|
metronome: Metronome::new(beep_samples, state),
|
||||||
track_matrix,
|
track_matrix,
|
||||||
selected_row: 0,
|
selected_row: 0,
|
||||||
selected_column: 0,
|
selected_column: 0,
|
||||||
|
last_volume_setting: 1.0,
|
||||||
|
sample_rate: client.sample_rate(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,12 +50,50 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
&self.ports
|
&self.ports
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_pedal_a(&mut self, value: u8) -> Result<()> {
|
||||||
|
self.last_volume_setting = (value as f32) / 127.0;
|
||||||
|
|
||||||
|
let controllers = TrackControllers::new(
|
||||||
|
&self.post_record_controller,
|
||||||
|
&self.osc,
|
||||||
|
&self.persistence,
|
||||||
|
self.sample_rate,
|
||||||
|
self.selected_column,
|
||||||
|
self.selected_row,
|
||||||
|
);
|
||||||
|
self.track_matrix.handle_volume_update(
|
||||||
|
self.last_volume_setting,
|
||||||
|
&controllers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_button_1(&mut self) -> Result<()> {
|
pub fn handle_button_1(&mut self) -> Result<()> {
|
||||||
self.track_matrix.handle_record_button(self.selected_column, self.selected_row)
|
let controllers = TrackControllers::new(
|
||||||
|
&self.post_record_controller,
|
||||||
|
&self.osc,
|
||||||
|
&self.persistence,
|
||||||
|
self.sample_rate,
|
||||||
|
self.selected_column,
|
||||||
|
self.selected_row,
|
||||||
|
);
|
||||||
|
self.track_matrix.handle_record_button(
|
||||||
|
self.last_volume_setting,
|
||||||
|
&controllers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_button_2(&mut self) -> Result<()> {
|
pub fn handle_button_2(&mut self) -> Result<()> {
|
||||||
self.track_matrix.handle_play_button(self.selected_column, self.selected_row)
|
let controllers = TrackControllers::new(
|
||||||
|
&self.post_record_controller,
|
||||||
|
&self.osc,
|
||||||
|
&self.persistence,
|
||||||
|
self.sample_rate,
|
||||||
|
self.selected_column,
|
||||||
|
self.selected_row,
|
||||||
|
);
|
||||||
|
self.track_matrix.handle_play_button(
|
||||||
|
&controllers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_button_3(&mut self) -> Result<()> {
|
pub fn handle_button_3(&mut self) -> Result<()> {
|
||||||
@ -59,7 +105,17 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_button_5(&mut self) -> Result<()> {
|
pub fn handle_button_5(&mut self) -> Result<()> {
|
||||||
self.track_matrix.handle_clear_button(self.selected_column, self.selected_row)
|
let controllers = TrackControllers::new(
|
||||||
|
&self.post_record_controller,
|
||||||
|
&self.osc,
|
||||||
|
&self.persistence,
|
||||||
|
self.sample_rate,
|
||||||
|
self.selected_column,
|
||||||
|
self.selected_row,
|
||||||
|
);
|
||||||
|
self.track_matrix.handle_clear_button(
|
||||||
|
&controllers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_button_6(&mut self) -> Result<()> {
|
pub fn handle_button_6(&mut self) -> Result<()> {
|
||||||
@ -110,8 +166,8 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
||||||
fn process(&mut self, client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
|
fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
|
||||||
if let Err(e) = self.process_with_error_handling(client, ps) {
|
if let Err(e) = self.process_with_error_handling(ps) {
|
||||||
log::error!("Error processing audio: {}", e);
|
log::error!("Error processing audio: {}", e);
|
||||||
jack::Control::Quit
|
jack::Control::Quit
|
||||||
} else {
|
} else {
|
||||||
@ -123,7 +179,6 @@ 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,
|
&mut self,
|
||||||
client: &jack::Client,
|
|
||||||
ps: &jack::ProcessScope,
|
ps: &jack::ProcessScope,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Process metronome and get beat timing information
|
// Process metronome and get beat timing information
|
||||||
@ -133,12 +188,17 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
midi::process_events(self, ps)?;
|
midi::process_events(self, ps)?;
|
||||||
|
|
||||||
// Process audio
|
// Process audio
|
||||||
|
let controllers = MatrixControllers::new(
|
||||||
|
&self.post_record_controller,
|
||||||
|
&self.osc,
|
||||||
|
&self.persistence,
|
||||||
|
self.sample_rate,
|
||||||
|
);
|
||||||
self.track_matrix.process(
|
self.track_matrix.process(
|
||||||
client,
|
|
||||||
ps,
|
ps,
|
||||||
&mut self.ports,
|
&mut self.ports,
|
||||||
&timing,
|
&timing,
|
||||||
&mut self.osc,
|
&controllers,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub connections: ConnectionState,
|
pub connections: ConnectionState,
|
||||||
pub metronome: MetronomeState,
|
pub metronome: MetronomeState,
|
||||||
|
pub track_volumes: TrackVolumes,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@ -20,6 +22,35 @@ pub struct MetronomeState {
|
|||||||
pub click_volume: f32, // 0.0 to 1.0
|
pub click_volume: f32, // 0.0 to 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TrackVolumes {
|
||||||
|
volumes: HashMap<String, f32>, // "col_1_row_1" → volume (1-based naming)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackVolumes {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
volumes: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_volume(&mut self, column: usize, row: usize, volume: f32) {
|
||||||
|
let key = Self::make_key(column, row);
|
||||||
|
let clamped_volume = volume.clamp(0.0, 1.0);
|
||||||
|
self.volumes.insert(key, clamped_volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_key(column: usize, row: usize) -> String {
|
||||||
|
format!("col_{}_row_{}", column + 1, row + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TrackVolumes {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for State {
|
impl Default for State {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -33,6 +64,7 @@ impl Default for State {
|
|||||||
frames_per_beat: 96000, // 120 BPM at 192kHz sample rate
|
frames_per_beat: 96000, // 120 BPM at 192kHz sample rate
|
||||||
click_volume: 0.5, // Default 50% volume
|
click_volume: 0.5, // Default 50% volume
|
||||||
},
|
},
|
||||||
|
track_volumes: TrackVolumes::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -12,8 +12,11 @@ pub enum TrackState {
|
|||||||
Empty,
|
Empty,
|
||||||
Idle,
|
Idle,
|
||||||
Playing,
|
Playing,
|
||||||
Recording,
|
Recording {
|
||||||
|
volume: f32,
|
||||||
|
},
|
||||||
RecordingAutoStop {
|
RecordingAutoStop {
|
||||||
|
volume: f32,
|
||||||
target_samples: usize,
|
target_samples: usize,
|
||||||
sync_offset: usize,
|
sync_offset: usize,
|
||||||
},
|
},
|
||||||
@ -32,7 +35,7 @@ impl Track {
|
|||||||
pub fn is_recording(&self) -> bool {
|
pub fn is_recording(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.current_state,
|
self.current_state,
|
||||||
TrackState::Recording | TrackState::RecordingAutoStop { .. }
|
TrackState::Recording { .. } | TrackState::RecordingAutoStop { .. }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,20 +51,40 @@ impl Track {
|
|||||||
self.audio_data.len()
|
self.audio_data.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn volume(&self) -> f32 {
|
pub fn set_volume(
|
||||||
self.volume
|
&mut self,
|
||||||
|
new_volume: f32,
|
||||||
|
controllers: &TrackControllers,
|
||||||
|
) -> Result<()> {
|
||||||
|
let new_volume = new_volume.clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
match &mut self.current_state {
|
||||||
|
TrackState::Recording { volume } => {
|
||||||
|
*volume = new_volume; // Update recording volume only
|
||||||
|
controllers.send_volume_update(new_volume)?;
|
||||||
|
}
|
||||||
|
TrackState::RecordingAutoStop { volume, .. } => {
|
||||||
|
*volume = new_volume; // Update recording volume only
|
||||||
|
controllers.send_volume_update(new_volume)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.volume = new_volume; // Update committed volume
|
||||||
|
controllers.update_track_volume(new_volume)?; // Update State
|
||||||
|
controllers.send_volume_update(new_volume)?; // Update GUI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_volume(&mut self, volume: f32) {
|
pub fn record(&mut self, last_volume_setting: f32) {
|
||||||
self.volume = volume.clamp(0.0, 1.0);
|
self.next_state = TrackState::Recording {
|
||||||
|
volume: last_volume_setting,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record(&mut self) {
|
pub fn record_auto_stop(&mut self, target_samples: usize, sync_offset: usize, last_volume_setting: f32) {
|
||||||
self.next_state = TrackState::Recording;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_auto_stop(&mut self, target_samples: usize, sync_offset: usize) {
|
|
||||||
self.next_state = TrackState::RecordingAutoStop {
|
self.next_state = TrackState::RecordingAutoStop {
|
||||||
|
volume: last_volume_setting,
|
||||||
target_samples,
|
target_samples,
|
||||||
sync_offset,
|
sync_offset,
|
||||||
};
|
};
|
||||||
@ -83,18 +106,13 @@ impl Track {
|
|||||||
self.audio_data.set_consolidated_buffer(buffer)
|
self.audio_data.set_consolidated_buffer(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_xrun<PRC, OC>(
|
pub fn handle_xrun(
|
||||||
&mut self,
|
&mut self,
|
||||||
beat_in_missed: Option<u32>,
|
beat_in_missed: Option<u32>,
|
||||||
missed_frames: usize,
|
missed_frames: usize,
|
||||||
chunk_factory: &mut impl ChunkFactory,
|
chunk_factory: &mut impl ChunkFactory,
|
||||||
post_record_controller: PRC,
|
controllers: &TrackControllers,
|
||||||
osc_controller: OC,
|
) -> Result<()> {
|
||||||
) -> Result<()>
|
|
||||||
where
|
|
||||||
PRC: Fn(Arc<AudioChunk>, usize) -> Result<()>,
|
|
||||||
OC: Fn(TrackState) -> Result<()>,
|
|
||||||
{
|
|
||||||
match beat_in_missed {
|
match beat_in_missed {
|
||||||
None => {
|
None => {
|
||||||
if self.is_recording() {
|
if self.is_recording() {
|
||||||
@ -111,7 +129,7 @@ impl Track {
|
|||||||
.append_silence(beat_offset as _, chunk_factory)?;
|
.append_silence(beat_offset as _, chunk_factory)?;
|
||||||
}
|
}
|
||||||
// Apply state transition at beat boundary
|
// Apply state transition at beat boundary
|
||||||
self.apply_state_transition(chunk_factory, post_record_controller, osc_controller)?;
|
self.apply_state_transition(chunk_factory, controllers)?;
|
||||||
// Insert silence after beat with new state
|
// Insert silence after beat with new state
|
||||||
let frames_after_beat = missed_frames - beat_offset as usize;
|
let frames_after_beat = missed_frames - beat_offset as usize;
|
||||||
if frames_after_beat > 0 && self.is_recording() {
|
if frames_after_beat > 0 && self.is_recording() {
|
||||||
@ -124,20 +142,15 @@ impl Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Audio processing
|
/// Audio processing
|
||||||
pub fn process<PRC, OC>(
|
pub fn process(
|
||||||
&mut self,
|
&mut self,
|
||||||
playback_position: usize,
|
playback_position: usize,
|
||||||
beat_in_buffer: Option<u32>,
|
beat_in_buffer: Option<u32>,
|
||||||
input_buffer: &[f32],
|
input_buffer: &[f32],
|
||||||
output_buffer: &mut [f32],
|
output_buffer: &mut [f32],
|
||||||
chunk_factory: &mut impl ChunkFactory,
|
chunk_factory: &mut impl ChunkFactory,
|
||||||
post_record_controller: PRC,
|
controllers: &TrackControllers,
|
||||||
osc_controller: OC,
|
) -> Result<()> {
|
||||||
) -> Result<()>
|
|
||||||
where
|
|
||||||
PRC: Fn(Arc<AudioChunk>, usize) -> Result<()>,
|
|
||||||
OC: Fn(TrackState) -> Result<()>,
|
|
||||||
{
|
|
||||||
match beat_in_buffer {
|
match beat_in_buffer {
|
||||||
None => {
|
None => {
|
||||||
// No beat in this buffer - process entire buffer with current state
|
// No beat in this buffer - process entire buffer with current state
|
||||||
@ -160,7 +173,7 @@ impl Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply state transition at beat boundary
|
// Apply state transition at beat boundary
|
||||||
self.apply_state_transition(chunk_factory, post_record_controller, osc_controller)?;
|
self.apply_state_transition(chunk_factory, controllers)?;
|
||||||
|
|
||||||
// Process samples after beat with new current state
|
// Process samples after beat with new current state
|
||||||
if (beat_index_in_buffer as usize) < output_buffer.len() {
|
if (beat_index_in_buffer as usize) < output_buffer.len() {
|
||||||
@ -198,7 +211,7 @@ impl Track {
|
|||||||
TrackState::Empty | TrackState::Idle => {
|
TrackState::Empty | TrackState::Idle => {
|
||||||
output_buffer.fill(0.0);
|
output_buffer.fill(0.0);
|
||||||
}
|
}
|
||||||
TrackState::Recording => {
|
TrackState::Recording { .. } => {
|
||||||
self.audio_data
|
self.audio_data
|
||||||
.append_samples(input_buffer, chunk_factory)?;
|
.append_samples(input_buffer, chunk_factory)?;
|
||||||
output_buffer.fill(0.0);
|
output_buffer.fill(0.0);
|
||||||
@ -239,16 +252,11 @@ impl Track {
|
|||||||
|
|
||||||
/// Apply state transition from next_state to current_state
|
/// Apply state transition from next_state to current_state
|
||||||
/// Returns true if track should be consolidated and saved
|
/// Returns true if track should be consolidated and saved
|
||||||
fn apply_state_transition<PRC, OC>(
|
fn apply_state_transition(
|
||||||
&mut self,
|
&mut self,
|
||||||
chunk_factory: &mut impl ChunkFactory,
|
chunk_factory: &mut impl ChunkFactory,
|
||||||
post_record_controller: PRC,
|
controllers: &TrackControllers,
|
||||||
osc_controller: OC,
|
) -> Result<()> {
|
||||||
) -> Result<()>
|
|
||||||
where
|
|
||||||
PRC: Fn(Arc<AudioChunk>, usize) -> Result<()>,
|
|
||||||
OC: Fn(TrackState) -> Result<()>,
|
|
||||||
{
|
|
||||||
// Check for auto-stop recording completion and transition to playing if no other state transition
|
// Check for auto-stop recording completion and transition to playing if no other state transition
|
||||||
if self.current_state == self.next_state {
|
if self.current_state == self.next_state {
|
||||||
if let TrackState::RecordingAutoStop { target_samples, .. } = &self.current_state {
|
if let TrackState::RecordingAutoStop { target_samples, .. } = &self.current_state {
|
||||||
@ -260,11 +268,16 @@ impl Track {
|
|||||||
|
|
||||||
// remember recording before transition
|
// remember recording before transition
|
||||||
let was_recording = self.is_recording();
|
let was_recording = self.is_recording();
|
||||||
|
let recording_volume = match &self.current_state {
|
||||||
|
TrackState::Recording { volume } => Some(*volume),
|
||||||
|
TrackState::RecordingAutoStop { volume, .. } => Some(*volume),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
// Handle transitions that require setup
|
// Handle transitions that require setup
|
||||||
match (&self.current_state, &self.next_state) {
|
match (&self.current_state, &self.next_state) {
|
||||||
(current_state, TrackState::Recording)
|
(current_state, TrackState::Recording { .. })
|
||||||
if !matches!(current_state, TrackState::Recording) =>
|
if !matches!(current_state, TrackState::Recording { .. }) =>
|
||||||
{
|
{
|
||||||
// Starting manual recording - clear previous data and create new unconsolidated data
|
// Starting manual recording - clear previous data and create new unconsolidated data
|
||||||
self.audio_data = AudioData::new_unconsolidated(chunk_factory, 0)?;
|
self.audio_data = AudioData::new_unconsolidated(chunk_factory, 0)?;
|
||||||
@ -293,15 +306,25 @@ impl Track {
|
|||||||
|
|
||||||
// Apply the state transition
|
// Apply the state transition
|
||||||
if self.current_state != self.next_state {
|
if self.current_state != self.next_state {
|
||||||
osc_controller(self.next_state.clone())?;
|
controllers.send_state_update(self.next_state.clone())?;
|
||||||
}
|
}
|
||||||
self.current_state = self.next_state.clone();
|
self.current_state = self.next_state.clone();
|
||||||
|
|
||||||
// Handle post-record processing
|
// Handle post-record processing and volume updates
|
||||||
if was_recording && !self.is_recording() {
|
if was_recording && !self.is_recording() {
|
||||||
|
// Recording accepted - commit recording volume to track volume and update state
|
||||||
|
if let Some(recording_vol) = recording_volume {
|
||||||
|
self.volume = recording_vol;
|
||||||
|
controllers.update_track_volume(recording_vol)?;
|
||||||
|
}
|
||||||
|
|
||||||
let (chunk, sync_offset) = self.audio_data.get_chunk_for_processing()?;
|
let (chunk, sync_offset) = self.audio_data.get_chunk_for_processing()?;
|
||||||
post_record_controller(chunk, sync_offset)?;
|
controllers.send_post_record_request(chunk, sync_offset)?;
|
||||||
|
} else if was_recording && self.current_state == TrackState::Empty {
|
||||||
|
// Recording cancelled - send OSC with committed volume (Track.volume unchanged)
|
||||||
|
controllers.send_volume_update(self.volume)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,7 +2,6 @@ use crate::*;
|
|||||||
|
|
||||||
pub struct TrackMatrix<F: ChunkFactory, const COLS: usize, const ROWS: usize> {
|
pub struct TrackMatrix<F: ChunkFactory, const COLS: usize, const ROWS: usize> {
|
||||||
chunk_factory: F,
|
chunk_factory: F,
|
||||||
post_record_controller: PostRecordController,
|
|
||||||
columns: [Column<ROWS>; COLS],
|
columns: [Column<ROWS>; COLS],
|
||||||
scratch_pad: Box<[f32]>,
|
scratch_pad: Box<[f32]>,
|
||||||
}
|
}
|
||||||
@ -12,39 +11,60 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
|
|||||||
client: &jack::Client,
|
client: &jack::Client,
|
||||||
chunk_factory: F,
|
chunk_factory: F,
|
||||||
state: &State,
|
state: &State,
|
||||||
post_record_controller: PostRecordController,
|
|
||||||
) -> Result<Self> {
|
) -> 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,
|
||||||
post_record_controller,
|
|
||||||
columns,
|
columns,
|
||||||
scratch_pad: vec![0.0; client.buffer_size() as usize].into_boxed_slice(),
|
scratch_pad: vec![0.0; client.buffer_size() as usize].into_boxed_slice(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_record_button(&mut self, selected_column: usize, selected_row: usize) -> Result<()> {
|
pub fn handle_record_button(
|
||||||
self.columns[selected_column].handle_record_button(selected_row)
|
&mut self,
|
||||||
|
last_volume_setting: f32,
|
||||||
|
controllers: &TrackControllers,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.columns[controllers.column()].handle_record_button(
|
||||||
|
last_volume_setting,
|
||||||
|
controllers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_play_button(&mut self, selected_column: usize, selected_row: usize) -> Result<()> {
|
pub fn handle_play_button(
|
||||||
self.columns[selected_column].handle_play_button(selected_row)
|
&mut self,
|
||||||
|
controllers: &TrackControllers,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.columns[controllers.column()].handle_play_button(controllers)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_clear_button(&mut self, selected_column: usize, selected_row: usize) -> Result<()> {
|
pub fn handle_clear_button(
|
||||||
self.columns[selected_column].handle_clear_button(selected_row)
|
&mut self,
|
||||||
|
controllers: &TrackControllers,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.columns[controllers.column()].handle_clear_button(controllers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_volume_update(
|
||||||
|
&mut self,
|
||||||
|
new_volume: f32,
|
||||||
|
controllers: &TrackControllers,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.columns[controllers.column()].handle_volume_update(
|
||||||
|
new_volume,
|
||||||
|
controllers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process(
|
pub fn process(
|
||||||
&mut self,
|
&mut self,
|
||||||
client: &jack::Client,
|
|
||||||
ps: &jack::ProcessScope,
|
ps: &jack::ProcessScope,
|
||||||
ports: &mut JackPorts,
|
ports: &mut JackPorts,
|
||||||
timing: &BufferTiming,
|
timing: &BufferTiming,
|
||||||
osc_controller: &mut OscController,
|
controllers: &MatrixControllers,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Check for consolidation response
|
// Check for consolidation response
|
||||||
if let Some(response) = self.post_record_controller.try_recv_response() {
|
if let Some(response) = controllers.try_recv_post_record_response() {
|
||||||
self.columns[response.column]
|
self.columns[response.column]
|
||||||
.set_consolidated_buffer(response.row, response.consolidated_buffer)?;
|
.set_consolidated_buffer(response.row, response.consolidated_buffer)?;
|
||||||
}
|
}
|
||||||
@ -52,21 +72,12 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
|
|||||||
// Handle xruns
|
// Handle xruns
|
||||||
if timing.missed_frames > 0 {
|
if timing.missed_frames > 0 {
|
||||||
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);
|
||||||
|
|
||||||
column.handle_xrun(
|
column.handle_xrun(
|
||||||
&timing,
|
&timing,
|
||||||
&mut self.chunk_factory,
|
&mut self.chunk_factory,
|
||||||
|row, chunk, sync_offset| {
|
&controllers,
|
||||||
self.post_record_controller.send_request(
|
|
||||||
i,
|
|
||||||
row,
|
|
||||||
chunk,
|
|
||||||
sync_offset,
|
|
||||||
client.sample_rate(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|row, state| {
|
|
||||||
osc_controller.track_state_changed(i, row, state)
|
|
||||||
},
|
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,24 +88,15 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
|
|||||||
output_buffer.fill(0.0);
|
output_buffer.fill(0.0);
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
column.process(
|
column.process(
|
||||||
&timing,
|
&timing,
|
||||||
input_buffer,
|
input_buffer,
|
||||||
output_buffer,
|
output_buffer,
|
||||||
&mut self.scratch_pad,
|
&mut self.scratch_pad,
|
||||||
&mut self.chunk_factory,
|
&mut self.chunk_factory,
|
||||||
|row, chunk, sync_offset| {
|
&controllers,
|
||||||
self.post_record_controller.send_request(
|
|
||||||
i,
|
|
||||||
row,
|
|
||||||
chunk,
|
|
||||||
sync_offset,
|
|
||||||
client.sample_rate(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|row, state| {
|
|
||||||
osc_controller.track_state_changed(i, row, state)
|
|
||||||
},
|
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user