Skip to content

Commit

Permalink
Merge branch 'i16'
Browse files Browse the repository at this point in the history
  • Loading branch information
phip1611 committed May 12, 2024
2 parents 123eded + dbbebcb commit 4abdc07
Show file tree
Hide file tree
Showing 18 changed files with 519 additions and 234 deletions.
18 changes: 14 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
[package]
name = "beat-detector"
description = """
beat-detector is a `no_std`-compatible and alloc-free library written in Rust
for detecting beats in audio. It can be used for both post- and live detection.
beat-detector detects beats in live audio, but can also be used for post
analysis of audio data. It is a library written in Rust that is
`no_std`-compatible and doesn't need `alloc`.
"""
version = "0.2.0"
authors = ["Philipp Schuster <phip1611@gmail.com>"]
Expand Down Expand Up @@ -38,13 +39,17 @@ recording = ["std", "dep:cpal"]
name = "beat_detection_bench"
harness = false

[[bench]]
name = "general"
harness = false

[[example]]
name = "minimal-live-input"
required-features = [ "recording" ]
required-features = ["recording"]

[[example]]
name = "minimal-live-input-gui"
required-features = [ "recording" ]
required-features = ["recording"]

[dependencies]
# +++ NOSTD DEPENDENCIES +++
Expand All @@ -57,11 +62,16 @@ ringbuffer = "0.15.0"
# +++ STD DEPENDENCIES +++
cpal = { version = "0.15", optional = true }


[dev-dependencies]
assert2 = "0.3.14"
ctrlc = { version = "3.4", features = ["termination"] }
criterion = { version = "0.5", features = [] }
float-cmp = "0.9.0"
itertools = "0.12.1"
simple_logger = "5.0"
minifb = "0.25.0"
rand = "0.8.5"
wav = "1.0"

[profile.dev]
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Beat Detector - Audio Beat Detection Library Written In Rust

beat-detector detects beats in live audio, but can also be used for post
analysis of audio data. It is a library written in Rust that is
`no_std`-compatible and doesn't need `alloc`.

beat-detector was developed with typical sampling rates and bit depths in
mind, namely 44.1 kHz, 48.0 kHz, and 16 bit. Other input sources might work
as well.

# Performance / Latency
On a realistic workload each analysis step of my algorithm, i.e., on each new audio input, took 0.5ms on a Raspberry
Pi and 0.05ms on an Intel i5-10600K. The benchmark binary was build as optimized release build. Thus, this is the
Expand Down
62 changes: 28 additions & 34 deletions benches/beat_detection_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,56 +44,50 @@ mod samples {

/// Returns the mono samples of the holiday sample (long version)
/// together with the sampling rate.
pub fn holiday_long() -> (Vec<f32>, wav::Header) {
pub fn holiday_long() -> (Vec<i16>, wav::Header) {
read_wav_to_mono("res/holiday_lowpassed--long.wav")
}
}

/// Copy and paste from `test_utils.rs`.
mod helpers {
use beat_detector::util::{f32_sample_to_i16, stereo_to_mono};
use itertools::Itertools;
use std::fs::File;
use std::path::Path;
use wav::BitDepth;

fn i16_sample_to_f32_sample(val: i16) -> f32 {
if val == 0 {
0.0
} else {
val as f32 / i16::MAX as f32
}
}

/// Reads a WAV file to mono audio. Returns the samples as mono audio.
/// Additionally, it returns the sampling rate of the file.
pub fn read_wav_to_mono<T: AsRef<Path>>(file: T) -> (Vec<f32>, wav::Header) {
pub fn read_wav_to_mono<T: AsRef<Path>>(file: T) -> (Vec<i16>, wav::Header) {
let mut file = File::open(file).unwrap();
let (header, data) = wav::read(&mut file).unwrap();

// owning vector with original data in f32 format
let original_data_f32 = if data.is_sixteen() {
data.as_sixteen()
.unwrap()
.iter()
.map(|sample| i16_sample_to_f32_sample(*sample))
.collect()
} else if data.is_thirty_two_float() {
data.as_thirty_two_float().unwrap().clone()
} else {
panic!("unsupported format");
let data = match data {
BitDepth::Sixteen(samples) => samples,
BitDepth::ThirtyTwoFloat(samples) => samples
.into_iter()
.map(f32_sample_to_i16)
.map(Result::unwrap)
.collect::<Vec<_>>(),
_ => todo!("{data:?} not supported yet"),
};

assert!(
!original_data_f32.iter().any(|&x| libm::fabsf(x) > 1.0),
"float audio data must be in interval [-1, 1]."
);

if header.channel_count == 1 {
(original_data_f32, header)
(data, header)
} else if header.channel_count == 2 {
let mut mono_audio = Vec::new();
for sample in original_data_f32.chunks(2) {
let mono_sample = (sample[0] + sample[1]) / 2.0;
mono_audio.push(mono_sample);
}
(mono_audio, header)
let data = data
.into_iter()
.chunks(2)
.into_iter()
.map(|mut lr| {
let l = lr.next().unwrap();
let r = lr
.next()
.expect("should have an even number of LRLR samples");
stereo_to_mono(l, r)
})
.collect::<Vec<_>>();
(data, header)
} else {
panic!("unsupported format!");
}
Expand Down
93 changes: 93 additions & 0 deletions benches/general.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! Benchmarks a few general audio transformations relevant in the field of this
//! crate. Useful to run this on a host platform to see the roughly costs.
//!
//! To run bench these, run `$ cargo bench "convert samples"`

use beat_detector::util::{f32_sample_to_i16, i16_sample_to_f32, stereo_to_mono};
use criterion::{criterion_group, criterion_main, Criterion};
use itertools::Itertools;
use std::hint::black_box;

fn criterion_benchmark(c: &mut Criterion) {
let typical_sampling_rate = 44100;
let sample_count = typical_sampling_rate;
let mut samples_f32 = vec![0.0; sample_count];
samples_f32.fill_with(rand::random::<f32>);
let mut samples_i16 = vec![0; sample_count];
samples_i16.fill_with(rand::random::<i16>);

assert_eq!(samples_f32.len(), sample_count);
assert_eq!(samples_i16.len(), sample_count);

c.bench_function(
&format!("{sample_count} convert samples (i16 to f32)"),
|b| {
b.iter(|| {
let _res = black_box(
samples_i16
.iter()
.copied()
.map(|s| i16_sample_to_f32(black_box(s)))
.collect::<Vec<_>>(),
);
})
},
);

c.bench_function(
&format!("{sample_count} convert samples (i16 to f32 (just cast))"),
|b| {
b.iter(|| {
let _res = black_box(
samples_i16
.iter()
.copied()
.map(|s| black_box(s as f32))
.collect::<Vec<_>>(),
);
})
},
);

c.bench_function(
&format!("{sample_count} convert samples (f32 to i16)"),
|b| {
b.iter(|| {
let _res = black_box(
samples_f32
.iter()
.copied()
.map(|s| f32_sample_to_i16(black_box(s)).unwrap())
.collect::<Vec<_>>(),
);
})
},
);

c.bench_function(
&format!("{sample_count} convert samples (i16 stereo to mono)"),
|b| {
b.iter(|| {
let _res = black_box(
samples_i16
.iter()
.copied()
// We pretend the data is interleaved (LRLR pattern).
.chunks(2)
.into_iter()
.map(|mut lr| {
let l = lr.next().unwrap();
let r = lr
.next()
.expect("should have an even number of LRLR samples");
stereo_to_mono(black_box(l), black_box(r))
})
.collect::<Vec<_>>(),
);
})
},
);
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
8 changes: 6 additions & 2 deletions examples/_modules/example_utils.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#![allow(unused)]

use cpal::traits::{DeviceTrait, HostTrait};
use log::LevelFilter;
use std::io::Read;
use std::io::{Read, Write};
use std::process::exit;

pub fn init_logger() {
Expand Down Expand Up @@ -76,11 +78,13 @@ pub fn select_audio_device() -> cpal::Device {
}

print!("Type a number: ");
std::io::stdout().flush().unwrap();

let mut buf = [0];
std::io::stdin().read_exact(&mut buf).unwrap();
println!(); // newline
let buf = std::str::from_utf8(&buf).unwrap();
let choice = usize::from_str_radix(buf, 10).unwrap();
let choice = str::parse::<usize>(buf).unwrap();

// Remove element and take ownership.
devices.swap_remove(choice).1
Expand Down
25 changes: 25 additions & 0 deletions examples/cpal-info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use cpal::traits::DeviceTrait;

#[path = "_modules/example_utils.rs"]
mod example_utils;

/// Minimal example to explore the structure of the audio input samples we get
/// from cpal. This example does nothing with the beat detection library.
fn main() {
let input_device = example_utils::select_audio_device();
let supported_configs = input_device
.supported_input_configs()
.unwrap()
.collect::<Vec<_>>();
println!("Supported input configs:");
for cfg in supported_configs {
println!(
"channels: {:>2}, format: {format:>3}, min_rate: {:06?}, max_rate: {:06?}, buffer: {:?}",
cfg.channels(),
cfg.min_sample_rate(),
cfg.max_sample_rate(),
cfg.buffer_size(),
format = format!("{:?}", cfg.sample_format(),)
);
}
}
57 changes: 57 additions & 0 deletions examples/cpal-minimal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{BufferSize, StreamConfig};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

#[path = "_modules/example_utils.rs"]
mod example_utils;

/// Minimal example to explore the structure of the audio input samples we get
/// from cpal. This example does nothing with the beat detection library.
fn main() {
let host = cpal::default_host();
let dev = host.default_input_device().unwrap();
let x = dev.supported_input_configs().unwrap().collect::<Vec<_>>();
dbg!(x);
let cfg = dev.default_input_config().unwrap();
let cfg = StreamConfig {
channels: 1,
sample_rate: cfg.sample_rate(),
buffer_size: BufferSize::Default,
};

let mut max = i16::MIN;
let mut min = i16::MAX;

let stream = dev
.build_input_stream(
&cfg,
// cpal is powerful enough to let us specify the type of the
// samples, such as `&[i16]` or `&[f32]`. For i16, the value is
// between `i16::MIN..i16::MAX`, for f32, the value is between
// `-1.0..1.0`. Supported formats are in enum `SampleFormat`.
move |samples: &[i16], _info| {
for &sample in samples {
max = core::cmp::max(max, sample);
min = core::cmp::min(min, sample);
println!("{sample:>6}, max={max:>6}, min={min:>6}");
}
},
|e| eprintln!("error: {e:?}"),
None,
)
.unwrap();

let stop_recording = Arc::new(AtomicBool::new(false));
{
let stop_recording = stop_recording.clone();
ctrlc::set_handler(move || {
stop_recording.store(true, Ordering::SeqCst);
})
.unwrap();
}

stream.play().unwrap();
while !stop_recording.load(Ordering::SeqCst) {}
stream.pause().unwrap();
}
2 changes: 1 addition & 1 deletion examples/minimal-live-input-gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fn main() {
let handle = {
let rgb_buffer = rgb_buffer.clone();
recording::start_detector_thread(
move |info| {
move |_info| {
println!("found beat!");
let mut rgb_buffer_locked = rgb_buffer.lock().unwrap();
for xrgb_pxl in rgb_buffer_locked.iter_mut() {
Expand Down
3 changes: 3 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ pkgs.mkShell rec {
xorg.libXcursor
xorg.libX11

# benchmarks
gnuplot

# Development
nixpkgs-fmt
rustup
Expand Down
Loading

0 comments on commit 4abdc07

Please sign in to comment.