Added tap tempo
This commit is contained in:
@@ -90,6 +90,14 @@ impl<const ROWS: usize> Column<ROWS> {
|
||||
self.tracks[row].set_consolidated_buffer(buffer)
|
||||
}
|
||||
|
||||
pub fn is_all_tracks_cleared(&self) -> bool {
|
||||
self.tracks.iter().all(|track| track.is_empty())
|
||||
}
|
||||
|
||||
pub fn set_frames_per_beat(&mut self, new_frames_per_beat: usize) {
|
||||
self.frames_per_beat = new_frames_per_beat;
|
||||
}
|
||||
|
||||
pub fn handle_xrun(
|
||||
&mut self,
|
||||
timing: &BufferTiming,
|
||||
|
||||
@@ -16,6 +16,7 @@ mod persistence_manager;
|
||||
mod post_record_handler;
|
||||
mod process_handler;
|
||||
mod state;
|
||||
mod tap_tempo;
|
||||
mod track;
|
||||
mod track_matrix;
|
||||
|
||||
@@ -47,6 +48,7 @@ use post_record_handler::PostRecordHandler;
|
||||
use post_record_handler::PostRecordResponse;
|
||||
use process_handler::ProcessHandler;
|
||||
use state::State;
|
||||
use tap_tempo::TapTempo;
|
||||
use track::Track;
|
||||
use track::TrackState;
|
||||
use track_matrix::TrackMatrix;
|
||||
|
||||
@@ -68,13 +68,22 @@ impl Metronome {
|
||||
Ok(timing)
|
||||
}
|
||||
|
||||
pub fn set_click_volume(&mut self, click_volume: f32, persistence: &mut PersistenceManagerController) -> Result<()> {
|
||||
pub fn set_click_volume(
|
||||
&mut self,
|
||||
click_volume: f32,
|
||||
persistence: &mut PersistenceManagerController,
|
||||
) -> Result<()> {
|
||||
let clamped = click_volume.clamp(0.0, 1.0);
|
||||
persistence.update_metronome_volume(clamped)?;
|
||||
self.click_volume = clamped;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_frames_per_beat(&mut self, frames_per_beat: usize) {
|
||||
self.frames_per_beat = frames_per_beat;
|
||||
self.frames_since_last_beat = self.frames_since_last_beat % self.frames_per_beat;
|
||||
}
|
||||
|
||||
fn process_ppqn_ticks(
|
||||
&mut self,
|
||||
ps: &jack::ProcessScope,
|
||||
|
||||
@@ -59,7 +59,7 @@ pub fn process_events<F: ChunkFactory, const COLS: usize, const ROWS: usize>(
|
||||
process_handler.handle_button_4()?;
|
||||
}
|
||||
24 if value_num > 0 => {
|
||||
process_handler.handle_button_5()?;
|
||||
process_handler.handle_button_5(ps)?;
|
||||
}
|
||||
25 if value_num > 0 => {
|
||||
process_handler.handle_button_6()?;
|
||||
|
||||
@@ -16,7 +16,10 @@ pub enum PersistenceUpdate {
|
||||
},
|
||||
UpdateMetronomeVolume {
|
||||
volume: f32,
|
||||
}
|
||||
},
|
||||
UpdateMetronomeFramesPerBeat {
|
||||
frames_per_beat: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl PersistenceManagerController {
|
||||
@@ -34,7 +37,16 @@ impl PersistenceManagerController {
|
||||
}
|
||||
|
||||
pub fn update_metronome_volume(&self, volume: f32) -> Result<()> {
|
||||
let update = PersistenceUpdate::UpdateMetronomeVolume { volume: volume };
|
||||
let update = PersistenceUpdate::UpdateMetronomeVolume { 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 fn update_metronome_frames_per_beat(&self, frames_per_beat: usize) -> Result<()> {
|
||||
let update = PersistenceUpdate::UpdateMetronomeFramesPerBeat { frames_per_beat };
|
||||
match self.sender.try_send(update) {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel full
|
||||
@@ -120,6 +132,12 @@ impl PersistenceManager {
|
||||
PersistenceUpdate::UpdateMetronomeVolume { volume } => {
|
||||
self.state
|
||||
.send_modify(|state| state.metronome.click_volume = volume);
|
||||
self.save_to_disk()?;
|
||||
}
|
||||
PersistenceUpdate::UpdateMetronomeFramesPerBeat { frames_per_beat } => {
|
||||
self.state
|
||||
.send_modify(|state| state.metronome.frames_per_beat = frames_per_beat);
|
||||
self.save_to_disk()?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct ProcessHandler<F: ChunkFactory, const COLS: usize, const ROWS: usize>
|
||||
selected_column: usize,
|
||||
last_volume_setting: f32,
|
||||
sample_rate: usize,
|
||||
tap_tempo: TapTempo,
|
||||
}
|
||||
|
||||
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, COLS, ROWS> {
|
||||
@@ -36,6 +37,7 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
|
||||
selected_column: 0,
|
||||
last_volume_setting: 1.0,
|
||||
sample_rate: client.sample_rate(),
|
||||
tap_tempo: TapTempo::new(client.sample_rate()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,7 +61,8 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
|
||||
}
|
||||
|
||||
pub fn handle_pedal_b(&mut self, value: u8) -> Result<()> {
|
||||
self.metronome.set_click_volume(value as f32 / 127.0, &mut self.persistence)
|
||||
self.metronome
|
||||
.set_click_volume(value as f32 / 127.0, &mut self.persistence)
|
||||
}
|
||||
|
||||
pub fn handle_button_1(&mut self) -> Result<()> {
|
||||
@@ -92,10 +95,6 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
|
||||
}
|
||||
|
||||
pub fn handle_button_4(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_button_5(&mut self) -> Result<()> {
|
||||
let controllers = TrackControllers::new(
|
||||
&self.post_record_controller,
|
||||
&self.osc,
|
||||
@@ -107,6 +106,27 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
|
||||
self.track_matrix.handle_clear_button(&controllers)
|
||||
}
|
||||
|
||||
pub fn handle_button_5(&mut self, ps: &jack::ProcessScope) -> Result<()> {
|
||||
if !self.track_matrix.is_all_tracks_cleared() {
|
||||
// Ignore tap if not all tracks are clear
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let current_frame_time = ps.last_frame_time();
|
||||
|
||||
let tempo_updated = self.tap_tempo.handle_tap(current_frame_time);
|
||||
|
||||
if let Some(frames_per_beat) = tempo_updated {
|
||||
// If tempo was updated, we need to update all columns with new frames_per_beat
|
||||
self.metronome.set_frames_per_beat(frames_per_beat);
|
||||
self.persistence
|
||||
.update_metronome_frames_per_beat(frames_per_beat)?;
|
||||
self.track_matrix.set_frames_per_beat(frames_per_beat);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_button_6(&mut self) -> Result<()> {
|
||||
self.selected_column = 0;
|
||||
self.osc.selected_column_changed(self.selected_column)?;
|
||||
|
||||
65
audio_engine/src/tap_tempo.rs
Normal file
65
audio_engine/src/tap_tempo.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
/// Collects tap timestamps and calculates tempo when enough taps are received.
|
||||
#[derive(Debug)]
|
||||
pub struct TapTempo {
|
||||
sample_rate: usize,
|
||||
tap_times: [u32; Self::MAX_TAPS],
|
||||
tap_count: usize,
|
||||
}
|
||||
|
||||
impl TapTempo {
|
||||
const MAX_TAPS: usize = 8;
|
||||
const MIN_TAPS: usize = 4;
|
||||
const MIN_INTERVAL_MS: usize = 100;
|
||||
const MAX_INTERVAL_MS: usize = 2000;
|
||||
|
||||
pub fn new(sample_rate: usize) -> Self {
|
||||
Self {
|
||||
sample_rate,
|
||||
tap_times: [0; Self::MAX_TAPS],
|
||||
tap_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_tap(&mut self, current_frame_time: u32) -> Option<usize> {
|
||||
self.record_tap(current_frame_time);
|
||||
self.calculate_tempo()
|
||||
}
|
||||
|
||||
fn record_tap(&mut self, frame_time: u32) {
|
||||
// Check if valid interval range
|
||||
if self.tap_count > 0 {
|
||||
let last_tap = self.tap_times[self.tap_count - 1];
|
||||
if frame_time - last_tap < (Self::MIN_INTERVAL_MS * self.sample_rate / 1000) as u32 {
|
||||
return;
|
||||
}
|
||||
if frame_time - last_tap > (Self::MAX_INTERVAL_MS * self.sample_rate / 1000) as u32 {
|
||||
self.tap_count = 0;
|
||||
}
|
||||
}
|
||||
// Add new entry
|
||||
if self.tap_count < Self::MAX_TAPS {
|
||||
self.tap_times[self.tap_count] = frame_time;
|
||||
self.tap_count += 1;
|
||||
} else {
|
||||
self.tap_times[0] = frame_time;
|
||||
self.tap_times.rotate_left(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_tempo(&self) -> Option<usize> {
|
||||
if self.tap_count < Self::MIN_TAPS {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut total_interval = 0u64;
|
||||
let interval_count = self.tap_count - 1;
|
||||
|
||||
for i in 1..self.tap_count {
|
||||
let interval = self.tap_times[i] - self.tap_times[i - 1];
|
||||
total_interval += interval as u64;
|
||||
}
|
||||
|
||||
let average_interval = (total_interval / interval_count as u64) as usize;
|
||||
Some(average_interval)
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,10 @@ impl Track {
|
||||
self.audio_data.set_consolidated_buffer(buffer)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
matches!(self.current_state, TrackState::Empty) && self.audio_data.is_empty()
|
||||
}
|
||||
|
||||
pub fn handle_xrun(
|
||||
&mut self,
|
||||
beat_in_missed: Option<u32>,
|
||||
|
||||
@@ -40,6 +40,18 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
|
||||
self.columns[controllers.column()].handle_volume_update(new_volume, controllers)
|
||||
}
|
||||
|
||||
pub fn is_all_tracks_cleared(&self) -> bool {
|
||||
self.columns
|
||||
.iter()
|
||||
.all(|column| column.is_all_tracks_cleared())
|
||||
}
|
||||
|
||||
pub fn set_frames_per_beat(&mut self, new_frames_per_beat: usize) {
|
||||
for column in &mut self.columns {
|
||||
column.set_frames_per_beat(new_frames_per_beat);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(
|
||||
&mut self,
|
||||
ps: &jack::ProcessScope,
|
||||
|
||||
Reference in New Issue
Block a user