Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,040 changes: 809 additions & 231 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ documentation = "http://docs.rs/rudiments"
edition = "2018"

[dependencies]
bitvec = "0.18.1"
clap = "3.0.0-beta.5"
bitvec = "1.0.1"
clap = { version = "4.5.38", features = ["derive"] }
hound = "3.4.0"
nom = "7"
rodio = "0.11.0"
rodio = "0.20.1"
thiserror = "1.0"
3 changes: 2 additions & 1 deletion assets/instrumentations/linndrum
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ tambourine tamb.wav
tom tom.wav
tom-high tomh.wav
tom-vhigh tomhh.wav
tom-low toml.wav
tom-low toml.wav
sine [internal sound]
7 changes: 4 additions & 3 deletions assets/patterns/standard
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
hi-hat |x-x-|x-x-|x-x-|x-x-| 0.25
snare |----|x---|----|x---|
kick |x---|----|x---|----|
hi-hat |x-x-|x-x-|x-x-|x-x-|x-x-|x-x-|x-x-|xxx-| 0.25
snare |----|x---|----|x---|----|x---|----|x---|
kick |x---|----|x---|----|x---|----|
sine |-A-B|-C-D|
200 changes: 83 additions & 117 deletions src/audio.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use rodio::{self, dynamic_mixer, Source};
use std::{collections::HashMap, fmt, io::BufReader, path::Path, thread, time::Duration};
use rodio::{OutputStream, dynamic_mixer, source::Source};
use std::{collections::HashMap, fmt, path::Path, thread, time::Duration};

use crate::{
error::{Error::*, Result},
instrumentation::{Instrumentation, SampleFile},
pattern::{Amplitude, Pattern, Steps, BEATS_PER_MEASURE, STEPS_PER_MEASURE},
instrumentation::{SampleFile, SampleSource},
pattern::Amplitude,
steps::Steps,
};

/// Number of playback channels.
Expand All @@ -30,148 +31,113 @@ impl fmt::Display for Tempo {
}
}

/// A type that represents the fully bound and reduced tracks of a pattern.
type Tracks = HashMap<SampleFile, (Steps, Amplitude)>;

/// Plays a pattern either once or repeatedly at the tempo given using samples
/// found in the given path.
pub fn play(
pattern: Pattern,
instrumentation: Instrumentation,
samples_path: &Path,
tempo: Tempo,
repeat: bool,
) -> Result<()> {
let (tracks, aggregate_steps) = bind_tracks(pattern, instrumentation);
let mix = mix_tracks(&tempo, tracks, samples_path)?;

if repeat {
play_repeat(&tempo, mix, aggregate_steps)
} else {
play_once(&tempo, mix)
impl Tempo {
/// Computes the duration of a step.
pub fn step_duration(&self, beats: usize) -> Duration {
Duration::from_secs_f32((beats as f32) * 15.0 / (self.0 as f32))
}
}

/// Binds a pattern's step sequences to audio files.
/// An sequences bound to the same audio file will be unioned.
/// The smallest amplitude for instruments bound to the same audio file will be used.
fn bind_tracks(pattern: Pattern, instrumentation: Instrumentation) -> (Tracks, Steps) {
let mut aggregate_steps = Steps::zeros();
let tracks = instrumentation
.into_iter()
.map(|(sample_file, instruments)| {
let simplified_steps = instruments.iter().fold(
(Steps::zeros(), Amplitude::max()),
|mut acc, instrument| {
if let Some((steps, amplitude)) = pattern.get(instrument) {
// update the aggregate step sequence
aggregate_steps.union(steps);

// update the track's step sequence and amplitude
acc.0.union(steps);
acc.1 = acc.1.min(amplitude);
}

acc
},
);

(sample_file, simplified_steps)
})
.collect();
/// Computes the duration to delay a mix with trailing silence when played on repeat.
/// This is necessary so that playback of the next iteration begins at the end
/// of the current iteration's measure instead of after its final non-silent step.
fn delay_pad_duration(&self) -> Duration {
self.step_duration(1).mul_f32(self.delay_factor()) * 1 as u32
}

(tracks, aggregate_steps)
/// Computes a factor necessary for delay-padding a mix played on repeat.
fn delay_factor(&self) -> f32 {
-1.0 / 120.0 * self.0 as f32 + 2.0
}
}

/// Mixes the tracks together using audio files found in the path given.
fn mix_tracks(
tempo: &Tempo,
tracks: Tracks,
samples_path: &Path,
) -> Result<Box<dyn Source<Item = i16> + Send>> {
let (controller, mixer) = dynamic_mixer::mixer(CHANNELS, SAMPLE_RATE);

for (sample_file, (steps, amplitude)) in tracks.iter() {
let sample_file_path = sample_file.with_parent(samples_path)?;
let file = std::fs::File::open(sample_file_path.path())?;
let source = rodio::Decoder::new(BufReader::new(file))?.buffered();

for (i, step) in steps.iter().enumerate() {
if !step {
continue;
pub struct Sources(HashMap<SampleSource, (Steps, Amplitude)>);

impl Sources {
/// Mixes the sources together using audio files found in the path given.
pub fn mix(
&self,
tempo: &Tempo,
) -> Result<Box<dyn Source<Item = i16> + Send>> {
let (controller, mixer) = dynamic_mixer::mixer(CHANNELS, SAMPLE_RATE);
for (sample_source, (steps, amplitude)) in self.0.iter() {
for (i, (step_vel, step_freq)) in steps.iter().enumerate() {
if 0 == *step_vel {
continue;
}
let amp_vel = *step_vel as f32 / 256.0 * amplitude.value();
let delay = tempo.step_duration(1) * (i as u32);
controller.add(sample_source.source(*step_freq).amplify(amp_vel).delay(delay));
}
let delay = step_duration(tempo) * (i as u32);
controller.add(source.clone().amplify(amplitude.value()).delay(delay));
}
Ok(Box::new(mixer))
}
}

Ok(Box::new(mixer))
/// A type that represents the fully bound and reduced tracks of a pattern.
pub struct Tracks(HashMap<SampleFile, (Steps, Amplitude)>);

impl Tracks {
/// Creates sources using audio files found in the path given.
pub fn sources(&self, samples_path: &Path) -> Result<Sources> {
let mut sample_map = HashMap::new();
for (sample_file, (steps, amplitude)) in self.0.iter() {
sample_map.insert(
SampleSource::from(samples_path, sample_file)?,
(steps.clone(), amplitude.clone())
);
}
Ok(Sources(sample_map))
}

pub fn from(hash_map: HashMap<SampleFile, (Steps, Amplitude)>) -> Tracks {
Tracks(hash_map)
}
}

/// Plays a mixed pattern repeatedly.
fn play_repeat(
pub fn play_repeat(
tempo: &Tempo,
source: Box<dyn Source<Item = i16> + Send>,
aggregate_steps: Steps,
beats: usize,
) -> Result<()> {
if let Some(device) = rodio::default_output_device() {
// compute the amount of trailing silence
let trailing_silence = aggregate_steps.trailing_silent_steps();

if let Ok((_stream, stream_handle)) = OutputStream::try_default() {
// play the pattern
rodio::play_raw(
&device,
if let Ok(()) = stream_handle.play_raw(
source
// forward pad with trailing silence
.delay(delay_pad_duration(&tempo, trailing_silence))
.delay(tempo.delay_pad_duration())
// trim to measure length
.take_duration(measure_duration(&tempo))
.take_duration(tempo.step_duration(beats))
.repeat_infinite()
.convert_samples(),
);

// sleep forever
thread::park();

Ok(())
) {
// sleep forever
thread::park();
Ok(())
} else {
Err(AudioDeviceError())
}
} else {
Err(AudioDeviceError())
}
}

/// Plays a mixed pattern once.
fn play_once(tempo: &Tempo, source: Box<dyn Source<Item = i16> + Send>) -> Result<()> {
if let Some(device) = rodio::default_output_device() {
pub fn play_once(
tempo: &Tempo,
source: Box<dyn Source<Item = i16> + Send>,
beats: usize
) -> Result<()> {
if let Ok((_stream, stream_handle)) = OutputStream::try_default() {
// play the pattern
rodio::play_raw(&device, source.convert_samples());

// sleep for the duration of a single measure
thread::sleep(measure_duration(tempo));

Ok(())
if let Ok(_) = stream_handle.play_raw(source.convert_samples()) {
// sleep for the duration of a single measure
thread::sleep(tempo.step_duration(beats));
Ok(())
} else {
Err(AudioDeviceError())
}
} else {
Err(AudioDeviceError())
}
}

/// Computes the duration of a measure.
fn measure_duration(tempo: &Tempo) -> Duration {
Duration::from_secs_f32(1.0 / (tempo.0 as f32 / 60.0 / BEATS_PER_MEASURE as f32))
}

/// Computes the duration of a step.
fn step_duration(tempo: &Tempo) -> Duration {
measure_duration(tempo) / STEPS_PER_MEASURE as u32
}

/// Computes the duration to delay a mix with trailing silence when played on repeat.
/// This is necessary so that playback of the next iteration begins at the end
/// of the current iteration's measure instead of after its final non-silent step.
fn delay_pad_duration(tempo: &Tempo, trailing_silent_steps: usize) -> Duration {
step_duration(tempo).mul_f32(delay_factor(tempo)) * trailing_silent_steps as u32
}

/// Computes a factor necessary for delay-padding a mix played on repeat.
fn delay_factor(tempo: &Tempo) -> f32 {
-1.0 / 120.0 * tempo.0 as f32 + 2.0
}
Loading