|
@@ -1,23 +1,38 @@
|
|
|
use pitch;
|
|
|
+use sample;
|
|
|
use std;
|
|
|
use Velocity;
|
|
|
|
|
|
+
|
|
|
+/// A type that maps frequncy and velocity ranges to audio samples.
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
-pub struct SampleMap<S> {
|
|
|
- pairs: Vec<Pair<S>>,
|
|
|
+pub struct Map<F> {
|
|
|
+ pairs: Vec<Pair<F>>,
|
|
|
}
|
|
|
|
|
|
/// Some slice of PCM samples that represents a single audio sample.
|
|
|
+///
|
|
|
+/// **Note:** The `sampler` crate currently assumes that the `Audio` you give it has the same
|
|
|
+/// format as the parameters with which audio is requested. We are hoping to enforce this using
|
|
|
+/// types with some changes to the `sample` crate.
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
-pub struct Audio<S> {
|
|
|
- data: std::sync::Arc<[S]>,
|
|
|
+pub struct Audio<F> {
|
|
|
+ pub data: std::sync::Arc<Box<[F]>>,
|
|
|
+}
|
|
|
+
|
|
|
+/// A performable `Sample` with some base playback Hz and Velocity.
|
|
|
+#[derive(Clone, Debug, PartialEq)]
|
|
|
+pub struct Sample<F> {
|
|
|
+ pub base_hz: pitch::Hz,
|
|
|
+ pub base_vel: Velocity,
|
|
|
+ pub audio: Audio<F>,
|
|
|
}
|
|
|
|
|
|
/// A range paired with a specific sample.
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
-struct Pair<S> {
|
|
|
+struct Pair<F> {
|
|
|
range: HzVelRange,
|
|
|
- audio: Audio<S>,
|
|
|
+ sample: Sample<F>,
|
|
|
}
|
|
|
|
|
|
/// A 2-dimensional space, represented as a frequency range and a velocity range.
|
|
@@ -34,43 +49,358 @@ pub struct Range<T> {
|
|
|
max: T,
|
|
|
}
|
|
|
|
|
|
-impl<T> Range<T> {
|
|
|
- /// Is the given `T` greater than or equal to the `min` and smaller than the `max`.
|
|
|
- pub fn is_over(&self, t: T) -> bool
|
|
|
- where T: PartialOrd,
|
|
|
+
|
|
|
+impl Range<pitch::Hz> {
|
|
|
+ /// Is the given hz greater than or equal to the `min` and smaller than the `max`.
|
|
|
+ pub fn is_over(&self, hz: pitch::Hz) -> bool {
|
|
|
+ self.min <= hz && hz < self.max
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Range<Velocity> {
|
|
|
+ /// Is the given velocity greater than or equal to the `min` and smaller than the `max`.
|
|
|
+ pub fn is_over(&self, vel: Velocity) -> bool {
|
|
|
+ self.min <= vel && vel <= self.max
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<F> Audio<F> {
|
|
|
+ /// Constructor for a new `Audio`.
|
|
|
+ pub fn new<I>(samples: I) -> Self
|
|
|
+ where I: Into<std::sync::Arc<Box<[F]>>>,
|
|
|
{
|
|
|
- self.min <= t && t < self.max
|
|
|
+ Audio { data: samples.into() }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<F> Sample<F> {
|
|
|
+
|
|
|
+ /// Constructor for a new `Sample` with the given base Hz and Velocity.
|
|
|
+ pub fn new(base_hz: pitch::Hz, base_vel: Velocity, audio: Audio<F>) -> Self {
|
|
|
+ Sample {
|
|
|
+ base_hz: base_hz,
|
|
|
+ base_vel: base_vel,
|
|
|
+ audio: audio,
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
}
|
|
|
|
|
|
-impl<S> SampleMap<S> {
|
|
|
+impl<F> Map<F>
|
|
|
+ where F: sample::Frame,
|
|
|
+{
|
|
|
|
|
|
- /// Construct a `SampleMap` from a series of mappings, starting from (-C2, 1.0).
|
|
|
+ /// Construct an empty `Map`.
|
|
|
+ pub fn empty() -> Self {
|
|
|
+ Map { pairs: vec![] }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Construct a `Map` from a series of mappings, starting from (-C2, 1.0).
|
|
|
pub fn from_sequential_mappings<I>(mappings: I) -> Self
|
|
|
- where I: IntoIterator<Item=(pitch::Hz, Velocity, Audio<S>)>,
|
|
|
+ where I: IntoIterator<Item=(pitch::Hz, Velocity, Sample<F>)>,
|
|
|
{
|
|
|
- let (mut last_hz, mut last_vel) = (pitch::Step(0.0).to_hz(), 1.0);
|
|
|
- let pairs = mappings.into_iter().map(|(hz, vel, audio)| {
|
|
|
+ const MIN_HZ: pitch::Hz = pitch::Hz(0.0);
|
|
|
+ let (mut last_hz, mut last_vel) = (MIN_HZ, 1.0);
|
|
|
+ let pairs = mappings.into_iter().map(|(hz, vel, sample)| {
|
|
|
let range = HzVelRange {
|
|
|
hz: Range { min: last_hz, max: hz },
|
|
|
vel: Range { min: last_vel, max: vel },
|
|
|
};
|
|
|
last_hz = hz;
|
|
|
last_vel = vel;
|
|
|
- Pair { range: range, audio: audio }
|
|
|
+ Pair { range: range, sample: sample }
|
|
|
}).collect();
|
|
|
- SampleMap { pairs: pairs }
|
|
|
+ Map { pairs: pairs }
|
|
|
}
|
|
|
|
|
|
- /// Inserts a range -> audio mapping into the SampleMap.
|
|
|
- pub fn insert(&mut self, range: HzVelRange, audio: Audio<S>) {
|
|
|
+ /// Creates a `Map` with a single sample mapped to the entire Hz and Velocity range.
|
|
|
+ pub fn from_single_sample(sample: Sample<F>) -> Self {
|
|
|
+ let range = HzVelRange {
|
|
|
+ hz: Range { min: pitch::Hz(0.0), max: pitch::Hz(std::f32::MAX) },
|
|
|
+ vel: Range { min: 0.0, max: 1.0 },
|
|
|
+ };
|
|
|
+ let pairs = vec![Pair { range: range, sample: sample }];
|
|
|
+ Map { pairs: pairs }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Inserts a range -> audio mapping into the Map.
|
|
|
+ pub fn insert(&mut self, range: HzVelRange, sample: Sample<F>) {
|
|
|
for i in 0..self.pairs.len() {
|
|
|
if self.pairs[i].range > range {
|
|
|
- self.pairs.insert(i, Pair { range: range, audio: audio });
|
|
|
+ self.pairs.insert(i, Pair { range: range, sample: sample });
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
- self.pairs.push(Pair { range: range, audio: audio });
|
|
|
+ self.pairs.push(Pair { range: range, sample: sample });
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Returns the `Audio` associated with the range within which the given hz and velocity exist.
|
|
|
+ ///
|
|
|
+ /// TODO: This would probably be quicker with some sort of specialised RangeMap.
|
|
|
+ pub fn sample(&self, hz: pitch::Hz, vel: Velocity) -> Option<Sample<F>>
|
|
|
+ where F: Clone,
|
|
|
+ {
|
|
|
+ for &Pair { ref range, ref sample } in &self.pairs {
|
|
|
+ if range.hz.is_over(hz) && range.vel.is_over(vel) {
|
|
|
+ return Some(sample.clone());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ None
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+mod wav {
|
|
|
+ use hound;
|
|
|
+ use map;
|
|
|
+ use pitch;
|
|
|
+ use sample;
|
|
|
+ use std;
|
|
|
+
|
|
|
+ impl<F> map::Sample<F>
|
|
|
+ where F: sample::Frame,
|
|
|
+ F::Sample: sample::Duplex<f64> + hound::Sample,
|
|
|
+ Box<[F::Sample]>: sample::ToBoxedFrameSlice<F>,
|
|
|
+ {
|
|
|
+
|
|
|
+ /// Loads a `Sample` from the `.wav` file at the given `path`.
|
|
|
+ ///
|
|
|
+ /// If the `.wav` file has a musical note in the file name, that note's playback frequency in
|
|
|
+ /// `hz` will be used as the `base_hz`.
|
|
|
+ ///
|
|
|
+ /// If a musical note cannot be determined automatically, a default `C1` will be used.
|
|
|
+ ///
|
|
|
+ /// The PCM data retrieved from the file will be re-sampled upon loading (rather than at
|
|
|
+ /// playback) to the given target sample rate for efficiency.
|
|
|
+ pub fn from_wav_file<P>(path: P, target_sample_hz: f64) -> Result<Self, hound::Error>
|
|
|
+ where P: AsRef<std::path::Path>,
|
|
|
+ {
|
|
|
+ use sample::Signal;
|
|
|
+
|
|
|
+ let path = path.as_ref();
|
|
|
+
|
|
|
+ let mut wav_reader = try!(hound::WavReader::open(path));
|
|
|
+ const DEFAULT_LETTER_OCTAVE: pitch::LetterOctave = pitch::LetterOctave(pitch::Letter::C, 1);
|
|
|
+ let base_letter_octave = read_base_letter_octave(path).unwrap_or(DEFAULT_LETTER_OCTAVE);
|
|
|
+
|
|
|
+ let base_hz = base_letter_octave.to_hz();
|
|
|
+ let base_vel = 1.0;
|
|
|
+
|
|
|
+ let audio = {
|
|
|
+ let spec = wav_reader.spec();
|
|
|
+ // TODO: Return an error instead of panic!ing! OR do some sort of frame /
|
|
|
+ // channel layout conversion.
|
|
|
+ assert!(spec.channels as usize == F::n_channels(),
|
|
|
+ "The number of channels in the audio file differs from the number of \
|
|
|
+ channels in the frame");
|
|
|
+
|
|
|
+ // Collect the samples in a loop so that we may handle any errors if necessary.
|
|
|
+ let mut samples: Vec<F::Sample> = Vec::new();
|
|
|
+ for sample in wav_reader.samples() {
|
|
|
+ samples.push(try!(sample));
|
|
|
+ }
|
|
|
+
|
|
|
+ let boxed_samples = samples.into_boxed_slice();
|
|
|
+ let boxed_frames: Box<[F]> = match sample::slice::to_boxed_frame_slice(boxed_samples) {
|
|
|
+ Some(slice) => slice,
|
|
|
+ // TODO: Return an error instead of panic!ing! OR do some sort of frame /
|
|
|
+ // channel layout conversion.
|
|
|
+ None => panic!("The number of samples produced from the wav file does not \
|
|
|
+ match the number of channels ({}) in the given `Frame` type",
|
|
|
+ F::n_channels()),
|
|
|
+ };
|
|
|
+
|
|
|
+ // Convert the sample rate to our target sample rate.
|
|
|
+ let frames: Vec<F> = boxed_frames.iter().cloned()
|
|
|
+ .from_hz_to_hz(spec.sample_rate as f64, target_sample_hz)
|
|
|
+ .collect();
|
|
|
+
|
|
|
+ map::Audio::new(frames.into_boxed_slice())
|
|
|
+ };
|
|
|
+
|
|
|
+ Ok(map::Sample::new(base_hz, base_vel, audio))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ impl<F> map::Map<F>
|
|
|
+ where F: sample::Frame,
|
|
|
+ F::Sample: sample::Duplex<f64> + hound::Sample,
|
|
|
+ Box<[F::Sample]>: sample::ToBoxedFrameSlice<F>,
|
|
|
+ {
|
|
|
+
|
|
|
+ /// Loads a `Map` from the given directory.
|
|
|
+ ///
|
|
|
+ /// All `.wav` files that can be successfully loaded will be loaded into the `Map`.
|
|
|
+ ///
|
|
|
+ /// If the `.wav` file has a musical note in the file name, that note's playback frequency in
|
|
|
+ /// `hz` will be used as the `base_hz`.
|
|
|
+ ///
|
|
|
+ /// For efficiency, all files will be re-sampled upon loading (rather than at playback) to the
|
|
|
+ /// given target sample rate.
|
|
|
+ pub fn from_wav_directory<P>(path: P, target_sample_hz: f64) -> Result<Self, hound::Error>
|
|
|
+ where P: AsRef<std::path::Path>,
|
|
|
+ {
|
|
|
+ use sample::Signal;
|
|
|
+ use std::cmp::Ordering;
|
|
|
+
|
|
|
+ let path = path.as_ref();
|
|
|
+
|
|
|
+ struct SampleReader {
|
|
|
+ base_letter_octave: Option<pitch::LetterOctave>,
|
|
|
+ wav_reader: hound::WavReader<std::io::BufReader<std::fs::File>>,
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut sample_readers: Vec<SampleReader> = Vec::new();
|
|
|
+
|
|
|
+ // Find all .wav files in the given directory and store them as `SampleReader`s.
|
|
|
+ for entry in try!(std::fs::read_dir(path)) {
|
|
|
+ let file_name = try!(entry).file_name();
|
|
|
+
|
|
|
+ // If the entry is a wave file, add it to our list.
|
|
|
+ if has_wav_ext(file_name.as_ref()) {
|
|
|
+ let wav_reader = try!(hound::WavReader::open(&file_name));
|
|
|
+ let sample_reader = SampleReader {
|
|
|
+ base_letter_octave: read_base_letter_octave(file_name.as_ref()),
|
|
|
+ wav_reader: wav_reader,
|
|
|
+ };
|
|
|
+ sample_readers.push(sample_reader);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Sort the readers by their base hz.
|
|
|
+ sample_readers.sort_by(|a, b| match (a.base_letter_octave, b.base_letter_octave) {
|
|
|
+ (Some(_), None) => Ordering::Less,
|
|
|
+ (None, Some(_)) => Ordering::Greater,
|
|
|
+ (Some(a), Some(b)) => a.cmp(&b),
|
|
|
+ (None, None) => Ordering::Equal,
|
|
|
+ });
|
|
|
+
|
|
|
+ const DEFAULT_LETTER_OCTAVE: pitch::LetterOctave =
|
|
|
+ pitch::LetterOctave(pitch::Letter::C, 1);
|
|
|
+ let mut maybe_last_step = None;
|
|
|
+
|
|
|
+ // We must imperatively collect the mappings so that we can handle any errors.
|
|
|
+ let mut mappings = Vec::with_capacity(sample_readers.len());
|
|
|
+ for SampleReader { base_letter_octave, mut wav_reader } in sample_readers {
|
|
|
+ let base_vel = 1.0;
|
|
|
+ let base_hz = match base_letter_octave {
|
|
|
+ Some(letter_octave) => {
|
|
|
+ maybe_last_step = Some(letter_octave.step());
|
|
|
+ letter_octave.to_hz()
|
|
|
+ },
|
|
|
+ None => {
|
|
|
+ let last_step = maybe_last_step.unwrap_or(DEFAULT_LETTER_OCTAVE.step());
|
|
|
+ let step = last_step + 1.0;
|
|
|
+ maybe_last_step = Some(step);
|
|
|
+ pitch::Step(step).to_hz()
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ let audio = {
|
|
|
+ let spec = wav_reader.spec();
|
|
|
+
|
|
|
+ // Collect the samples in a loop so that we may handle any errors if necessary.
|
|
|
+ let mut samples: Vec<F::Sample> = Vec::new();
|
|
|
+ for sample in wav_reader.samples() {
|
|
|
+ samples.push(try!(sample));
|
|
|
+ }
|
|
|
+
|
|
|
+ let boxed_samples = samples.into_boxed_slice();
|
|
|
+ let boxed_frames: Box<[F]> = match sample::slice::to_boxed_frame_slice(boxed_samples) {
|
|
|
+ Some(slice) => slice,
|
|
|
+ // TODO: Return an error instead of panic!ing! OR do some sort of frame /
|
|
|
+ // channel layout conversion.
|
|
|
+ None => panic!("The number of samples produced from the wav file does not \
|
|
|
+ match the number of channels ({}) in the given `Frame` type",
|
|
|
+ F::n_channels()),
|
|
|
+ };
|
|
|
+
|
|
|
+ // Convert the sample rate to our target sample rate.
|
|
|
+ let frames: Vec<F> = boxed_frames.iter().cloned()
|
|
|
+ .from_hz_to_hz(spec.sample_rate as f64, target_sample_hz)
|
|
|
+ .collect();
|
|
|
+
|
|
|
+ map::Audio::new(frames.into_boxed_slice())
|
|
|
+ };
|
|
|
+
|
|
|
+ let sample = map::Sample::new(base_hz, base_vel, audio);
|
|
|
+
|
|
|
+ // The `Hz` range that triggers this sample will span from the last sample's Hz (or
|
|
|
+ // the minimum if there is no last sample) to the following `to_hz` value.
|
|
|
+ //
|
|
|
+ // TODO: Investigate a nicer way of evenly spreading samples across the keyboard.
|
|
|
+ let to_hz = pitch::Step(base_hz.step() + 0.5).to_hz();
|
|
|
+ let to_vel = base_vel;
|
|
|
+ mappings.push((to_hz, to_vel, sample));
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(Self::from_sequential_mappings(mappings))
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ ///// Utility functions.
|
|
|
+
|
|
|
+
|
|
|
+ /// Determines whether the given `Path` leads to a wave file.
|
|
|
+ fn has_wav_ext(path: &std::path::Path) -> bool {
|
|
|
+ let ext = path.extension()
|
|
|
+ .and_then(|s| s.to_str())
|
|
|
+ .map_or("".into(), std::ascii::AsciiExt::to_ascii_lowercase);
|
|
|
+ match &ext[..] {
|
|
|
+ "wav" | "wave" => true,
|
|
|
+ _ => false,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Scans the given path for an indication of its pitch.
|
|
|
+ fn read_base_letter_octave(path: &std::path::Path) -> Option<pitch::LetterOctave> {
|
|
|
+ use pitch::Letter::*;
|
|
|
+ use std::ascii::AsciiExt;
|
|
|
+
|
|
|
+ let s = path.to_str().map_or("".into(), |s| s.to_ascii_lowercase());
|
|
|
+
|
|
|
+ // Check to see if the path contains a note for the given `letter` for any octave
|
|
|
+ // between -8 and 24. If so, return the `LetterOctave`.
|
|
|
+ let contains_letter = |letter: &str| -> Option<pitch::LetterOctave> {
|
|
|
+ for i in -8i8..24 {
|
|
|
+ let pattern = format!("{}{}", letter, i);
|
|
|
+ if s.contains(&pattern) {
|
|
|
+ let letter = match letter {
|
|
|
+ "c" => C,
|
|
|
+ "c#" | "csh" => Csh,
|
|
|
+ "d" => D,
|
|
|
+ "d#" | "dsh" => Dsh,
|
|
|
+ "e" => E,
|
|
|
+ "f" => F,
|
|
|
+ "f#" | "fsh" => Fsh,
|
|
|
+ "g" => G,
|
|
|
+ "g#" | "gsh" => Gsh,
|
|
|
+ "a" => A,
|
|
|
+ "a#" | "ash" => Ash,
|
|
|
+ "b" => B,
|
|
|
+ _ => unreachable!(),
|
|
|
+ };
|
|
|
+ return Some(pitch::LetterOctave(letter, i as pitch::Octave));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ None
|
|
|
+ };
|
|
|
+
|
|
|
+ let list = [
|
|
|
+ "c", "c#", "csh", "d", "d#", "dsh", "e", "f", "f#", "fsh", "g", "g#", "gsh",
|
|
|
+ "a", "a#", "ash", "b",
|
|
|
+ ];
|
|
|
+
|
|
|
+ for letter in &list[..] {
|
|
|
+ if let Some(letter_octave) = contains_letter(letter) {
|
|
|
+ return Some(letter_octave);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ None
|
|
|
}
|
|
|
|
|
|
}
|