Skip to content

Commit

Permalink
#1419 Fix MIDI clock tempo source
Browse files Browse the repository at this point in the history
  • Loading branch information
helgoboss committed Jan 19, 2025
1 parent 77203a7 commit 711e831
Show file tree
Hide file tree
Showing 8 changed files with 40 additions and 118 deletions.
14 changes: 12 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ rmp-serde = "1.1.1"
anyhow = "1.0.71"
thiserror = "1.0.45"
enum_dispatch = "0.3.6"
simple_moving_average = "1.0.2"
tinyvec = "1.6.0"
erased-serde = "0.4.2"
fragile = "2.0.0"
Expand Down
2 changes: 2 additions & 0 deletions main/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ pathdiff.workspace = true
# For fetching Helgobox remote config.
# Important to not use default features because we want to avoid the libssl dependency on Linux.
reqwest = { workspace = true, default-features = false, features = ["rustls-tls-no-provider"] }
# For MIDI clock processing
simple_moving_average.workspace = true

[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
# For speech source
Expand Down
3 changes: 0 additions & 3 deletions main/src/base/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ pub use scheduling::*;
mod property;
pub use property::*;

mod moving_average_calculator;
pub use moving_average_calculator::*;

pub mod notification;

pub mod eel;
Expand Down
38 changes: 0 additions & 38 deletions main/src/base/moving_average_calculator.rs

This file was deleted.

4 changes: 2 additions & 2 deletions main/src/domain/audio_hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ impl RealearnAudioHook {
} => {
for (_, p) in self.real_time_processors.iter() {
p.lock_recover()
.run_from_audio_hook_essential(block_props, might_be_rebirth);
.run_from_audio_hook_essential(might_be_rebirth);
}
for dev in Reaper::get().midi_input_devices() {
dev.with_midi_input(|mi| {
Expand Down Expand Up @@ -387,7 +387,7 @@ impl RealearnAudioHook {
// stop doing so synchronously if the plug-in is
// gone.
let mut guard = p.lock_recover();
guard.run_from_audio_hook_all(block_props, might_be_rebirth, start_of_block_timestamp);
guard.run_from_audio_hook_all(might_be_rebirth, start_of_block_timestamp);
if guard.control_is_globally_enabled() {
if let MidiControlInput::Device(dev_id) = guard.midi_control_input() {
midi_dev_id_is_used[dev_id.get() as usize] = true;
Expand Down
67 changes: 18 additions & 49 deletions main/src/domain/midi_clock_calculator.rs
Original file line number Diff line number Diff line change
@@ -1,67 +1,36 @@
use crate::base::MovingAverageCalculator;

use crate::domain::SampleOffset;
use crate::domain::ControlEventTimestamp;
use reaper_common_types::Bpm;
use reaper_medium::Hz;
use simple_moving_average::{SumTreeSMA, SMA};
use std::convert::TryInto;
use tracing::warn;

#[derive(Debug)]
pub struct MidiClockCalculator {
sample_rate: Hz,
sample_counter: u64,
previous_midi_clock_timestamp_in_samples: u64,
bpm_calculator: MovingAverageCalculator,
previous_timestamp: Option<ControlEventTimestamp>,
previous_bpm: Option<f64>,
moving_avg_calculator: SumTreeSMA<f64, f64, 10>,
}

impl Default for MidiClockCalculator {
fn default() -> Self {
Self {
sample_rate: Hz::new_panic(1.0),
sample_counter: 0,
previous_midi_clock_timestamp_in_samples: 0,
bpm_calculator: Default::default(),
previous_timestamp: None,
previous_bpm: None,
moving_avg_calculator: SumTreeSMA::new(),
}
}
}

impl MidiClockCalculator {
pub fn update_sample_rate(&mut self, sample_rate: Hz) {
self.sample_rate = sample_rate;
}

pub fn increase_sample_counter_by(&mut self, sample_count: u64) {
self.sample_counter += sample_count;
}

pub fn feed(&mut self, offset: SampleOffset) -> Option<Bpm> {
let timestamp_in_samples = self.sample_counter + offset.get();
let prev_timestamp = self.previous_midi_clock_timestamp_in_samples;
self.previous_midi_clock_timestamp_in_samples = timestamp_in_samples;

if prev_timestamp == 0 || timestamp_in_samples <= prev_timestamp {
return None;
}
let difference_in_samples = timestamp_in_samples - prev_timestamp;
let difference_in_secs = difference_in_samples as f64 / self.sample_rate.get();
let num_ticks_per_sec = 1.0 / difference_in_secs;
pub fn feed(&mut self, timestamp: ControlEventTimestamp) -> Option<Bpm> {
let prev_timestamp = self.previous_timestamp.replace(timestamp)?;
let duration_since_last = timestamp - prev_timestamp;
let num_ticks_per_sec = 1.0 / duration_since_last.as_secs_f64();
let num_beats_per_sec = num_ticks_per_sec / 24.0;
let num_beats_per_min = num_beats_per_sec * 60.0;
if num_beats_per_min > 300.0 {
return None;
}
self.bpm_calculator.feed(num_beats_per_min);
let moving_avg = match self.bpm_calculator.moving_average() {
None => return None,
Some(a) => a,
};
if self.bpm_calculator.value_count_so_far() % 24 == 0 {
moving_avg.try_into().ok()
} else {
None
}
}

pub fn current_sample_count(&self) -> u64 {
self.sample_counter
let new_bpm = num_beats_per_sec * 60.0;
self.moving_avg_calculator.add_sample(new_bpm);
let avg_bpm = self.moving_avg_calculator.get_average();
let avg_bpm: Bpm = avg_bpm.try_into().ok()?;
Some(avg_bpm)
}
}
29 changes: 5 additions & 24 deletions main/src/domain/real_time_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,10 @@ impl RealTimeProcessor {
/// This should be regularly called by audio hook in normal mode.
pub fn run_from_audio_hook_all(
&mut self,
block_props: AudioBlockProps,
might_be_rebirth: bool,
timestamp: ControlEventTimestamp,
) {
self.run_from_audio_hook_essential(block_props, might_be_rebirth);
self.run_from_audio_hook_essential(might_be_rebirth);
self.run_from_audio_hook_control_and_learn(timestamp);
}

Expand Down Expand Up @@ -194,14 +193,7 @@ impl RealTimeProcessor {
/// The rebirth parameter is `true` if this could be the first audio cycle after an "unplanned"
/// downtime of the audio device. It could also be just a downtime related to opening the
/// project itself, which we detect to some degree. See the code that reacts to this parameter.
pub fn run_from_audio_hook_essential(
&mut self,
block_props: AudioBlockProps,
might_be_rebirth: bool,
) {
// Increase MIDI clock calculator's sample counter
self.midi_clock_calculator
.increase_sample_counter_by(block_props.block_length as u64);
pub fn run_from_audio_hook_essential(&mut self, might_be_rebirth: bool) {
if might_be_rebirth {
self.request_full_sync_and_discard_tasks_if_successful();
}
Expand Down Expand Up @@ -285,17 +277,6 @@ impl RealTimeProcessor {
}
}
UpdateTargetsPartially(compartment, mut target_updates) => {
// Also log sample count in order to be sure about invocation order
// (timestamp is not accurate enough on e.g. selection changes).
// TODO-low We should use an own logger and always log the sample count
// automatically.
permit_alloc(|| {
debug!(
"Update target activations in {} at {} samples...",
compartment,
self.midi_clock_calculator.current_sample_count()
);
});
// Apply updates
for update in target_updates.iter_mut() {
if let Some(m) = self.mappings[compartment].get_mut(&update.id) {
Expand Down Expand Up @@ -340,7 +321,6 @@ impl RealTimeProcessor {
debug!("Updating sample rate");
});
self.sample_rate = sample_rate;
self.midi_clock_calculator.update_sample_rate(sample_rate);
}
StartLearnSource {
allow_virtual_sources,
Expand Down Expand Up @@ -580,10 +560,11 @@ impl RealTimeProcessor {
MatchOutcome::Unmatched
}
Timing => {
// Timing clock messages are treated special (calculates BPM).
// Timing clock messages are treated special (calculates BPM). Matching each tick with
// mappings would be a waste of resources. We know they come in very densely.
// This is control-only, we never learn it.
if self.control_is_globally_enabled {
if let Some(bpm) = self.midi_clock_calculator.feed(event.payload().offset()) {
if let Some(bpm) = self.midi_clock_calculator.feed(event.timestamp()) {
let source_value = MidiSourceValue::<RawShortMessage>::Tempo(bpm);
self.control_midi(
event.with_payload(MidiEvent::new(
Expand Down

0 comments on commit 711e831

Please sign in to comment.