From b29d3ecda2cf8feb3c2075fd77f018b31c907e4a Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 23 Jul 2024 17:35:15 +0300 Subject: [PATCH] wip: Sample::from_file, add Rubato --- Cargo.lock | 62 +++++ Cargo.toml | 4 + src/devices/sampler.rs | 370 +----------------------------- src/devices/sampler/add_sample.rs | 261 +++++++++++++++++++++ src/devices/sampler/sample.rs | 56 +++++ src/devices/sampler/voice.rs | 31 +++ 6 files changed, 415 insertions(+), 369 deletions(-) create mode 100644 src/devices/sampler/add_sample.rs create mode 100644 src/devices/sampler/sample.rs create mode 100644 src/devices/sampler/voice.rs diff --git a/Cargo.lock b/Cargo.lock index 675be55d..9f0ca113 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -898,6 +898,15 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -975,6 +984,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "realfft" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953d9f7e5cdd80963547b456251296efc2626ed4e3cbf36c869d9564e0220571" +dependencies = [ + "rustfft", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1014,12 +1032,39 @@ dependencies = [ "svgbobdoc", ] +[[package]] +name = "rubato" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d18b486e7d29a408ef3f825bc1327d8f87af091c987ca2f5b734625940e234" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustfft" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -1119,6 +1164,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + [[package]] name = "strsim" version = "0.11.1" @@ -1398,6 +1449,7 @@ dependencies = [ "r8brain-rs", "ratatui", "rlsf", + "rubato", "symphonia", "toml", "vst", @@ -1469,6 +1521,16 @@ dependencies = [ "winnow 0.6.13", ] +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index 1c24922c..e6698897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,5 +26,9 @@ rlsf = "0.2.1" r8brain-rs = "0.3.5" clojure-reader = "0.1.0" once_cell = "1.19.0" + symphonia = { version = "0.5.4", features = [ "all" ] } + dasp = { version = "0.11.0", features = [ "all" ] } + +rubato = "0.15.0" diff --git a/src/devices/sampler.rs b/src/devices/sampler.rs index df10a2fe..115213f7 100644 --- a/src/devices/sampler.rs +++ b/src/devices/sampler.rs @@ -2,12 +2,7 @@ use crate::{core::*, view::*, model::MODAL}; -use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL}; -use symphonia::core::errors::Error; -use symphonia::core::formats::FormatOptions; -use symphonia::core::io::MediaSourceStream; -use symphonia::core::meta::MetadataOptions; -use symphonia::core::probe::Hint; +submod! { add_sample sample voice } /// The sampler plugin plays sounds. pub struct Sampler { @@ -204,366 +199,3 @@ impl Sampler { } } } - -/// Load sample from WAV and assign to MIDI note. -#[macro_export] macro_rules! sample { - ($note:expr, $name:expr, $src:expr) => {{ - let (end, data) = read_sample_data($src)?; - ( - u7::from_int_lossy($note).into(), - Sample::new($name, 0, end, data).into() - ) - }}; -} - -/// Read WAV from file -pub fn read_sample_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) -} - -/// A sound sample. -pub struct Sample { - pub name: String, - pub start: usize, - pub end: usize, - pub channels: Vec>, -} - -impl Sample { - pub fn new ( - name: &str, start: usize, end: usize, channels: Vec> - ) -> Arc> { - Arc::new(RwLock::new(Self { name: name.to_string(), start, end, channels })) - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0 - } - } -} - -/// A currently playing instance of a sample. -pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after = self.after - 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position = self.position + 1; - return Some([ - sample.channels[0][position] * self.velocity, - sample.channels[0][position] * self.velocity, - ]) - } - None - } -} - -pub struct AddSampleModal { - exited: bool, - dir: PathBuf, - subdirs: Vec, - files: Vec, - cursor: usize, - offset: usize, - sample: Arc>, - search: Option, -} - -exit!(AddSampleModal); - -render!(AddSampleModal |self,buf,area|{ - make_dim(buf); - let area = center_box( - area, - 64.max(area.width.saturating_sub(8)), - 20.max(area.width.saturating_sub(8)), - ); - fill_fg(buf, area, Color::Reset); - fill_bg(buf, area, Nord::bg_lo(true, true)); - fill_char(buf, area, ' '); - format!("{}", &self.dir.to_string_lossy()) - .blit(buf, area.x+2, area.y+1, Some(Style::default().bold()))?; - "Select sample:" - .blit(buf, area.x+2, area.y+2, Some(Style::default().bold()))?; - for (i, (is_dir, name)) in self.subdirs.iter() - .map(|path|(true, path)) - .chain(self.files.iter().map(|path|(false, path))) - .enumerate() - .skip(self.offset) - { - if i >= area.height as usize - 4 { - break - } - let t = if is_dir { "" } else { "" }; - let line = format!("{t} {}", name.to_string_lossy()); - let line = &line[..line.len().min(area.width as usize - 4)]; - line.blit(buf, area.x + 2, area.y + 3 + i as u16, Some(if i == self.cursor { - Style::default().green() - } else { - Style::default().white() - }))?; - } - Lozenge(Style::default()).draw(buf, area) -}); - -handle!(AddSampleModal |self,e|{ - if handle_keymap(self, e, KEYMAP_ADD_SAMPLE)? { - return Ok(true) - } - Ok(true) -}); - -impl AddSampleModal { - fn new (sample: &Arc>) -> Usually { - let dir = std::env::current_dir()?; - let (subdirs, files) = scan(&dir)?; - Ok(Self { - exited: false, - dir, - subdirs, - files, - cursor: 0, - offset: 0, - sample: sample.clone(), - search: None - }) - } - fn rescan (&mut self) -> Usually<()> { - scan(&self.dir).map(|(subdirs, files)|{ - self.subdirs = subdirs; - self.files = files; - }) - } - fn prev (&mut self) { - self.cursor = self.cursor.saturating_sub(1); - } - fn next (&mut self) { - self.cursor = self.cursor + 1; - } - fn try_preview (&mut self) -> Usually<()> { - if let Some(path) = self.cursor_file() { - let src = std::fs::File::open(&path)?; - let mss = MediaSourceStream::new(Box::new(src), Default::default()); - let mut hint = Hint::new(); - if let Some(ext) = path.extension() { - hint.with_extension(&ext.to_string_lossy()); - } - let meta_opts: MetadataOptions = Default::default(); - let fmt_opts: FormatOptions = Default::default(); - if let Ok(mut probed) = symphonia::default::get_probe() - .format(&hint, mss, &fmt_opts, &meta_opts) - { - panic!("{:?}", probed.format.metadata()); - }; - } - Ok(()) - } - fn cursor_dir (&self) -> Option { - if self.cursor < self.subdirs.len() { - Some(self.dir.join(&self.subdirs[self.cursor])) - } else { - None - } - } - fn cursor_file (&self) -> Option { - if self.cursor < self.subdirs.len() { - return None - } - let index = self.cursor.saturating_sub(self.subdirs.len()); - if index < self.files.len() { - Some(self.dir.join(&self.files[index])) - } else { - None - } - } - fn pick (&mut self) -> Usually { - if self.cursor == 0 { - if let Some(parent) = self.dir.parent() { - self.dir = parent.into(); - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - } - if let Some(dir) = self.cursor_dir() { - self.dir = dir; - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - if let Some(path) = self.cursor_file() { - let (end, channels) = read_sample_data(&path.to_string_lossy())?; - let mut sample = self.sample.write().unwrap(); - sample.name = path.file_name().unwrap().to_string_lossy().into(); - sample.end = end; - sample.channels = channels; - return Ok(true) - } - return Ok(false) - } -} - -pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { - [Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ - modal.exit(); - Ok(true) - }], - [Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ - modal.prev(); - Ok(true) - }], - [Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ - modal.next(); - Ok(true) - }], - [Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ - if modal.pick()? { - modal.exit(); - } - Ok(true) - }], - [Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ - modal.try_preview()?; - Ok(true) - }] -}); - -fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - let (mut subdirs, mut files) = read_dir(dir)? - .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ - let entry = entry.expect("failed to read drectory entry"); - let meta = entry.metadata().expect("failed to read entry metadata"); - if meta.is_file() { - files.push(entry.file_name()); - } else if meta.is_dir() { - subdirs.push(entry.file_name()); - } - (subdirs, files) - }); - subdirs.sort(); - files.sort(); - Ok((subdirs, files)) -} - -fn load_sample () { - // Get the first command line argument. - let args: Vec = std::env::args().collect(); - let path = args.get(1).expect("file path not provided"); - - // Open the media source. - let src = std::fs::File::open(path).expect("failed to open media"); - - // Create the media source stream. - let mss = MediaSourceStream::new(Box::new(src), Default::default()); - - // Create a probe hint using the file's extension. [Optional] - let mut hint = Hint::new(); - hint.with_extension("mp3"); - - // Use the default options for metadata and format readers. - let meta_opts: MetadataOptions = Default::default(); - let fmt_opts: FormatOptions = Default::default(); - - // Probe the media source. - let probed = symphonia::default::get_probe() - .format(&hint, mss, &fmt_opts, &meta_opts) - .expect("unsupported format"); - - // Get the instantiated format reader. - let mut format = probed.format; - - // Find the first audio track with a known (decodeable) codec. - let track = format - .tracks() - .iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no supported audio tracks"); - - // Use the default options for the decoder. - let dec_opts: DecoderOptions = Default::default(); - - // Create a decoder for the track. - let mut decoder = symphonia::default::get_codecs() - .make(&track.codec_params, &dec_opts) - .expect("unsupported codec"); - - // Store the track identifier, it will be used to filter packets. - let track_id = track.id; - - // The decode loop. - loop { - // Get the next packet from the media format. - let packet = match format.next_packet() { - Ok(packet) => packet, - Err(Error::ResetRequired) => { - // The track list has been changed. Re-examine it and create a new set of decoders, - // then restart the decode loop. This is an advanced feature and it is not - // unreasonable to consider this "the end." As of v0.5.0, the only usage of this is - // for chained OGG physical streams. - unimplemented!(); - } - Err(err) => { - // A unrecoverable error occurred, halt decoding. - panic!("{}", err); - } - }; - - // Consume any new metadata that has been read since the last packet. - while !format.metadata().is_latest() { - // Pop the old head of the metadata queue. - format.metadata().pop(); - - // Consume the new metadata at the head of the metadata queue. - } - - // If the packet does not belong to the selected track, skip over it. - if packet.track_id() != track_id { - continue; - } - - // Decode the packet into audio samples. - match decoder.decode(&packet) { - Ok(_decoded) => { - // Consume the decoded audio samples (see below). - } - Err(Error::IoError(_)) => { - // The packet failed to decode due to an IO error, skip the packet. - continue; - } - Err(Error::DecodeError(_)) => { - // The packet failed to decode due to invalid data, skip the packet. - continue; - } - Err(err) => { - // An unrecoverable error occurred, halt decoding. - panic!("{}", err); - } - } - } -} diff --git a/src/devices/sampler/add_sample.rs b/src/devices/sampler/add_sample.rs new file mode 100644 index 00000000..24704f13 --- /dev/null +++ b/src/devices/sampler/add_sample.rs @@ -0,0 +1,261 @@ +use crate::{core::*, view::*}; +use super::*; + +use std::fs::File; +use symphonia::core::codecs::CODEC_TYPE_NULL; +use symphonia::core::errors::Error; +use symphonia::core::io::MediaSourceStream; +use symphonia::core::probe::Hint; +use symphonia::core::audio::SampleBuffer; +use symphonia::default::get_codecs; + +pub struct AddSampleModal { + exited: bool, + dir: PathBuf, + subdirs: Vec, + files: Vec, + cursor: usize, + offset: usize, + sample: Arc>, + _search: Option, +} + +exit!(AddSampleModal); + +render!(AddSampleModal |self,buf,area|{ + make_dim(buf); + let area = center_box( + area, + 64.max(area.width.saturating_sub(8)), + 20.max(area.width.saturating_sub(8)), + ); + fill_fg(buf, area, Color::Reset); + fill_bg(buf, area, Nord::bg_lo(true, true)); + fill_char(buf, area, ' '); + format!("{}", &self.dir.to_string_lossy()) + .blit(buf, area.x+2, area.y+1, Some(Style::default().bold()))?; + "Select sample:" + .blit(buf, area.x+2, area.y+2, Some(Style::default().bold()))?; + for (i, (is_dir, name)) in self.subdirs.iter() + .map(|path|(true, path)) + .chain(self.files.iter().map(|path|(false, path))) + .enumerate() + .skip(self.offset) + { + if i >= area.height as usize - 4 { + break + } + let t = if is_dir { "" } else { "" }; + let line = format!("{t} {}", name.to_string_lossy()); + let line = &line[..line.len().min(area.width as usize - 4)]; + line.blit(buf, area.x + 2, area.y + 3 + i as u16, Some(if i == self.cursor { + Style::default().green() + } else { + Style::default().white() + }))?; + } + Lozenge(Style::default()).draw(buf, area) +}); + +handle!(AddSampleModal |self,e|{ + if handle_keymap(self, e, KEYMAP_ADD_SAMPLE)? { + return Ok(true) + } + Ok(true) +}); + +impl AddSampleModal { + pub fn new (sample: &Arc>) -> Usually { + let dir = std::env::current_dir()?; + let (subdirs, files) = scan(&dir)?; + Ok(Self { + exited: false, + dir, + subdirs, + files, + cursor: 0, + offset: 0, + sample: sample.clone(), + _search: None + }) + } + fn rescan (&mut self) -> Usually<()> { + scan(&self.dir).map(|(subdirs, files)|{ + self.subdirs = subdirs; + self.files = files; + }) + } + fn prev (&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + fn next (&mut self) { + self.cursor = self.cursor + 1; + } + fn try_preview (&mut self) -> Usually<()> { + if let Some(path) = self.cursor_file() { + //load_sample(&path)?; + //let src = std::fs::File::open(&path)?; + //let mss = MediaSourceStream::new(Box::new(src), Default::default()); + //let mut hint = Hint::new(); + //if let Some(ext) = path.extension() { + //hint.with_extension(&ext.to_string_lossy()); + //} + //let meta_opts: MetadataOptions = Default::default(); + //let fmt_opts: FormatOptions = Default::default(); + //if let Ok(mut probed) = symphonia::default::get_probe() + //.format(&hint, mss, &fmt_opts, &meta_opts) + //{ + //panic!("{:?}", probed.format.metadata()); + //}; + } + Ok(()) + } + fn cursor_dir (&self) -> Option { + if self.cursor < self.subdirs.len() { + Some(self.dir.join(&self.subdirs[self.cursor])) + } else { + None + } + } + fn cursor_file (&self) -> Option { + if self.cursor < self.subdirs.len() { + return None + } + let index = self.cursor.saturating_sub(self.subdirs.len()); + if index < self.files.len() { + Some(self.dir.join(&self.files[index])) + } else { + None + } + } + fn pick (&mut self) -> Usually { + if self.cursor == 0 { + if let Some(parent) = self.dir.parent() { + self.dir = parent.into(); + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + } + if let Some(dir) = self.cursor_dir() { + self.dir = dir; + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + if let Some(path) = self.cursor_file() { + let (end, channels) = read_sample_data(&path.to_string_lossy())?; + let mut sample = self.sample.write().unwrap(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); + sample.end = end; + sample.channels = channels; + return Ok(true) + } + return Ok(false) + } +} + +pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { + [Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ + modal.exit(); + Ok(true) + }], + [Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ + modal.prev(); + Ok(true) + }], + [Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ + modal.next(); + Ok(true) + }], + [Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ + if modal.pick()? { + modal.exit(); + } + Ok(true) + }], + [Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ + modal.try_preview()?; + Ok(true) + }] +}); + +fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { + let (mut subdirs, mut files) = read_dir(dir)? + .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ + let entry = entry.expect("failed to read drectory entry"); + let meta = entry.metadata().expect("failed to read entry metadata"); + if meta.is_file() { + files.push(entry.file_name()); + } else if meta.is_dir() { + subdirs.push(entry.file_name()); + } + (subdirs, files) + }); + subdirs.sort(); + files.sort(); + Ok((subdirs, files)) +} + +impl Sample { + fn from_file (path: &PathBuf) -> Usually>> { + let mut sample = Self::default(); + sample.name = path.to_string_lossy().into(); + // Use file extension if present + let mut hint = Hint::new(); + if let Some(ext) = path.extension() { + hint.with_extension(&ext.to_string_lossy()); + } + let probed = symphonia::default::get_probe().format( + &hint, + MediaSourceStream::new( + Box::new(File::open(path)?), + Default::default(), + ), + &Default::default(), + &Default::default() + )?; + let mut format = probed.format; + let mut decoder = get_codecs().make( + &format.tracks().iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no tracks found") + .codec_params, + &Default::default() + )?; + loop { + match format.next_packet() { + Ok(packet) => { + // Decode a packet + let decoded = match decoder.decode(&packet) { + Ok(decoded) => decoded, + Err(err) => { return Err(err.into()); } + }; + // Determine sample rate + let spec = *decoded.spec(); + if let Some(rate) = sample.rate { + if rate != spec.rate as usize { + panic!("sample rate changed"); + } + } else { + sample.rate = Some(spec.rate as usize); + } + // Determine channel count + while sample.channels.len() < spec.channels.count() { + sample.channels.push(vec![]); + } + let mut samples = SampleBuffer::new(decoded.frames() as u64, spec); + samples.copy_interleaved_ref(decoded); + for frame in samples.samples().chunks(spec.channels.count()) { + for (chan, frame) in frame.iter().enumerate() { + sample.channels[chan].push(*frame) + } + } + }, + Err(Error::IoError(_)) => break decoder.last_decoded(), + Err(err) => return Err(err.into()), + }; + }; + Ok(Arc::new(RwLock::new(sample))) + } +} diff --git a/src/devices/sampler/sample.rs b/src/devices/sampler/sample.rs new file mode 100644 index 00000000..4116d869 --- /dev/null +++ b/src/devices/sampler/sample.rs @@ -0,0 +1,56 @@ +use crate::core::*; +use super::*; + +/// A sound sample. +#[derive(Default)] +pub struct Sample { + pub name: String, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, +} + +impl Sample { + pub fn new ( + name: &str, start: usize, end: usize, channels: Vec> + ) -> Arc> { + Arc::new(RwLock::new(Self { name: name.to_string(), start, end, channels, rate: None })) + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } +} + + +/// Load sample from WAV and assign to MIDI note. +#[macro_export] macro_rules! sample { + ($note:expr, $name:expr, $src:expr) => {{ + let (end, data) = read_sample_data($src)?; + ( + u7::from_int_lossy($note).into(), + Sample::new($name, 0, end, data).into() + ) + }}; +} + +/// Read WAV from file +pub fn read_sample_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) +} diff --git a/src/devices/sampler/voice.rs b/src/devices/sampler/voice.rs new file mode 100644 index 00000000..ac9e9dcf --- /dev/null +++ b/src/devices/sampler/voice.rs @@ -0,0 +1,31 @@ +use crate::core::*; +use super::*; + +/// A currently playing instance of a sample. +pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after = self.after - 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position = self.position + 1; + return Some([ + sample.channels[0][position] * self.velocity, + sample.channels[0][position] * self.velocity, + ]) + } + None + } +} +