Skip to content

Commit

Permalink
XXX WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
phip1611 committed Apr 19, 2024
1 parent 5e5a873 commit 0144f60
Show file tree
Hide file tree
Showing 8 changed files with 364 additions and 54 deletions.
69 changes: 48 additions & 21 deletions src/audio_history.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
use crate::envelope_iterator::ENVELOPE_MIN_DURATION_MS;
use core::cmp::Ordering;
use core::time::Duration;
use ringbuffer::{ConstGenericRingBuffer, RingBuffer};

/// Default buffer size for [`AudioHistory`]. For a typical (and to be expected)
/// sampling frequency of 44100Hz, this corresponds to 1000ms which is more than
/// enough to detect beats that are typically at 60Hz, with a period duration
/// of 20ms (or 20Hz at 50ms). This results in roughly 35kib of stack usage
/// for the audio buffer.
///
/// TODO I plan to reduce this to 500, 300, or even 200ms to reduce memory usage.
/// However, during development, a large ubffer size is necessary to tune the
/// algorithm in a way that it reliably works for data analysis when the full
/// sample is in memory. If that works reliably, I can test the library in a
/// "data streaming approach" and discover beats one by one. However, for now,
/// I keep it very basic and easy.
pub const DEFAULT_BUFFER_SIZE: usize = 44100;
const SAFE_MIN_DURATION_MS: usize = (ENVELOPE_MIN_DURATION_MS as f64 * 2.5) as usize;

/// Based on the de-facto default sampling rate of 44100 Hz / 44.1 kHz.
const DEFAULT_SAMPLES_PER_SECOND: usize = 44100;
const MS_PER_SECOND: usize = 1000;

/// Default buffer size for [`AudioHistory`]. The size is a trade-off between
/// memory efficiency and effectiveness in detecting envelops properly.
pub const DEFAULT_BUFFER_SIZE: usize =
(SAFE_MIN_DURATION_MS * DEFAULT_SAMPLES_PER_SECOND) / MS_PER_SECOND;

/// Sample info with time context.
#[derive(Copy, Clone, Debug)]
Expand Down Expand Up @@ -87,6 +85,7 @@ impl AudioHistory {

/// Update the audio history with fresh samples. The audio samples are
/// expected to be in mono channel, i.e., no stereo interleaving
/// TODO: Update consume iterator and consume enum: Interleaved or Mono
pub fn update(&mut self, mono_samples: &[f32]) {
if mono_samples.len() >= self.audio_buffer.capacity() {
log::warn!(
Expand Down Expand Up @@ -183,9 +182,18 @@ impl AudioHistory {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::{sample_1_test_data, single_beat_test_data};
use crate::test_util::{sample_1_test_data};
use std::prelude::v1::Vec;

#[test]
fn buffer_len_sane() {
let sampling_rate = 1.0 / DEFAULT_SAMPLES_PER_SECOND as f32;
let duration = Duration::from_secs_f32(sampling_rate * DEFAULT_BUFFER_SIZE as f32);
dbg!(duration);
assert!(duration.as_millis() > 10);
assert!(duration.as_millis() <= 1000);
}

#[test]
fn audio_duration_is_updated_properly() {
let mut hist = AudioHistory::new(2.0);
Expand Down Expand Up @@ -277,18 +285,37 @@ mod tests {
let mut hist = AudioHistory::new(1.0);

hist.update(&[0.0]);
assert_eq!(hist.index_to_sample_info(0).duration_behind, Duration::from_secs(0));
assert_eq!(
hist.index_to_sample_info(0).duration_behind,
Duration::from_secs(0)
);
hist.update(&[0.0]);
assert_eq!(hist.index_to_sample_info(0).duration_behind, Duration::from_secs(1));
assert_eq!(hist.index_to_sample_info(1).duration_behind, Duration::from_secs(0));

assert_eq!(
hist.index_to_sample_info(0).duration_behind,
Duration::from_secs(1)
);
assert_eq!(
hist.index_to_sample_info(1).duration_behind,
Duration::from_secs(0)
);

hist.update(&[0.0].repeat(hist.data().capacity() * 2));

let sample = hist.index_to_sample_info(0);
assert_eq!(hist.index_to_sample_info(0).duration_behind, Duration::from_secs_f32((DEFAULT_BUFFER_SIZE - 1) as f32));
assert_eq!(hist.index_to_sample_info(DEFAULT_BUFFER_SIZE - 10).duration_behind, Duration::from_secs_f32(9.0));
assert_eq!(hist.index_to_sample_info(DEFAULT_BUFFER_SIZE - 1).duration_behind, Duration::from_secs(0));
assert_eq!(
hist.index_to_sample_info(0).duration_behind,
Duration::from_secs_f32((DEFAULT_BUFFER_SIZE - 1) as f32)
);
assert_eq!(
hist.index_to_sample_info(DEFAULT_BUFFER_SIZE - 10)
.duration_behind,
Duration::from_secs_f32(9.0)
);
assert_eq!(
hist.index_to_sample_info(DEFAULT_BUFFER_SIZE - 1)
.duration_behind,
Duration::from_secs(0)
);
}

#[test]
Expand Down
23 changes: 23 additions & 0 deletions src/audio_input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
pub enum AudioInput<'a> {
/// The audio input stream only consists of mono samples.
Mono(&'a [f32]),
/// The audio input streams consists of interleaved samples following a
/// LRLRLR scheme. This is typically the case for stereo channel audio.
InterleavedLR(&'a [f32]),
}

impl<'a> AudioInput<'a> {
pub fn iter(&self) -> impl Iterator<Item = f32> {
match self {
AudioInput::Mono(samples) => samples.iter(),
AudioInput::InterleavedLR(samples) => samples.chunks(2).map(|lr| (lr[0] + lr[1]) / 2.0)
}
}
}

#[cfg(test)]
mod tests {

fn stereo_to:

}
13 changes: 9 additions & 4 deletions src/envelope_iterator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ const ENVELOPE_MIN_VALUE: f32 = 0.2;
/// The factor by that a peak must be higher than the begin and end so that we
/// in fact found an envelope that represents a beat.
const ENVELOPE_PEAK_RATIO: f32 = 2.5;
const ENVELOPE_MIN_DURATION: Duration = Duration::from_millis(100);

pub(crate) const ENVELOPE_MIN_DURATION_MS: u64 = 160;

/// Minimum realistic duration of an envelope. This value is the result of
/// analyzing some waveforms in Audacity. Specifically, this results from an
/// envelope of two beats very close to each other.
const ENVELOPE_MIN_DURATION: Duration = Duration::from_millis(ENVELOPE_MIN_DURATION_MS);

/// Iterates the envelopes of an audio signal. An envelope is the set of
/// vibrations(? - german: Schwingungen) that characterize a beat. Its waveform
Expand Down Expand Up @@ -164,7 +170,6 @@ impl PartialEq for EnvelopeInfo {
mod tests {
use super::*;
use crate::test_util::*;
use std::collections::{BTreeMap, BTreeSet};
use std::vec::Vec;

#[test]
Expand Down Expand Up @@ -244,12 +249,12 @@ mod tests {
let mut hist = AudioHistory::new(header.sampling_rate as f32);

// 5ms at 44.1kHz - realistic value for an audio input device
let mut begin_index = None;
/*let mut begin_index = None;
for chunk in vec.chunks(250) {
hist.update(chunk);
let iter = EnvelopeIterator::new(&hist, begin_index);
let maybe_envelop = iter.next();
}
}*/
/*.flat_map(|chunks| {
hist.update(chunks);
let iter = EnvelopeIterator::new(&hist, None);
Expand Down
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,14 @@ SOFTWARE.
extern crate std;

mod audio_history;
mod root_iterator;
mod envelope_iterator;
mod max_min_iterator;
mod root_iterator;

// Todo export function to combine large audio stream with my code lololo

mod sample;
/// PRIVATE. For tests and helper binaries.
#[cfg(test)]
mod test_util;

mod audio_input;
2 changes: 1 addition & 1 deletion src/max_min_iterator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ mod tests {
// I checked in Audacity whether the values returned by the code
// make sense. Then, they became the reference for the test.
[
(0, -0.07258828),
(0, -0.07258828), // TODO should not have zero here?!
(71, 0.5884732),
(323, -0.7123936),
(608, 0.599353),
Expand Down
35 changes: 10 additions & 25 deletions src/root_iterator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,40 +51,25 @@ mod tests {
use crate::test_util::*;
use std::vec::Vec;

#[test]
fn find_roots_simple() {
let history = simple_test_data();
let iter = RootIterator::new(&history, None);
#[rustfmt::skip]
assert_eq!(
iter.map(|info| (info.value, info.index)).collect::<Vec<_>>(),
[
(-0.5, 5),
(0.5, 9),
(-0.5, 13),
]
);
}

#[test]
fn find_roots_in_real_sample() {
let history = single_beat_excerpt_test_data();
let iter = RootIterator::new(&history, None);
#[rustfmt::skip]
assert_eq!(
iter.map(|info| (info.timestamp.as_secs_f32(), info.value)).collect::<Vec<_>>(),
iter.map(|info| (info.total_index, info.value)).collect::<Vec<_>>(),
// I checked in Audacity whether the values returned by the code
// make sense. Then, they became the reference for the test.
[
(0.000181406, -0.002929777),
(0.004444445, 0.005798517),
(0.009818594, -0.004074221),
(0.017823128, 0.0008850368),
(0.025170067, -0.0017700735),
(0.032743763, 0.004379406),
(0.04111111, -0.0031891842),
(0.04918367, 0.0013428144),
(0.05732426, -0.0026245918)
(8, -0.002929777),
(196, 0.005798517),
(433, -0.004074221),
(786, 0.0008850368),
(1110, -0.0017700735),
(1444, 0.004379406),
(1813, -0.0031891842),
(2169, 0.0013428144),
(2528, -0.0026245918)
]
);
}
Expand Down
Loading

0 comments on commit 0144f60

Please sign in to comment.