wip: Sample::from_file, add Rubato

This commit is contained in:
🪞👃🪞 2024-07-23 17:35:15 +03:00
parent d61f2e3c20
commit b29d3ecda2
6 changed files with 415 additions and 369 deletions

62
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<Vec<f32>>)> {
let mut channels: Vec<wavers::Samples<f32>> = vec![];
for channel in wavers::Wav::from_path(src)?.channels() {
channels.push(channel);
}
let mut end = 0;
let mut data: Vec<Vec<f32>> = 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<Vec<f32>>,
}
impl Sample {
pub fn new (
name: &str, start: usize, end: usize, channels: Vec<Vec<f32>>
) -> Arc<RwLock<Self>> {
Arc::new(RwLock::new(Self { name: name.to_string(), start, end, channels }))
}
pub fn play (sample: &Arc<RwLock<Self>>, 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<RwLock<Sample>>,
pub after: usize,
pub position: usize,
pub velocity: f32,
}
impl Iterator for Voice {
type Item = [f32;2];
fn next (&mut self) -> Option<Self::Item> {
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<OsString>,
files: Vec<OsString>,
cursor: usize,
offset: usize,
sample: Arc<RwLock<Sample>>,
search: Option<String>,
}
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<RwLock<Sample>>) -> Usually<Self> {
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<PathBuf> {
if self.cursor < self.subdirs.len() {
Some(self.dir.join(&self.subdirs[self.cursor]))
} else {
None
}
}
fn cursor_file (&self) -> Option<PathBuf> {
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<bool> {
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<AddSampleModal>] = 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<OsString>, Vec<OsString>)> {
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<String> = 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);
}
}
}
}

View file

@ -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<OsString>,
files: Vec<OsString>,
cursor: usize,
offset: usize,
sample: Arc<RwLock<Sample>>,
_search: Option<String>,
}
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<RwLock<Sample>>) -> Usually<Self> {
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<PathBuf> {
if self.cursor < self.subdirs.len() {
Some(self.dir.join(&self.subdirs[self.cursor]))
} else {
None
}
}
fn cursor_file (&self) -> Option<PathBuf> {
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<bool> {
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<AddSampleModal>] = 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<OsString>, Vec<OsString>)> {
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<Arc<RwLock<Self>>> {
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)))
}
}

View file

@ -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<Vec<f32>>,
pub rate: Option<usize>,
}
impl Sample {
pub fn new (
name: &str, start: usize, end: usize, channels: Vec<Vec<f32>>
) -> Arc<RwLock<Self>> {
Arc::new(RwLock::new(Self { name: name.to_string(), start, end, channels, rate: None }))
}
pub fn play (sample: &Arc<RwLock<Self>>, 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<Vec<f32>>)> {
let mut channels: Vec<wavers::Samples<f32>> = vec![];
for channel in wavers::Wav::from_path(src)?.channels() {
channels.push(channel);
}
let mut end = 0;
let mut data: Vec<Vec<f32>> = vec![];
for samples in channels.iter() {
let channel = Vec::from(samples.as_ref());
end = end.max(channel.len());
data.push(channel);
}
Ok((end, data))
}

View file

@ -0,0 +1,31 @@
use crate::core::*;
use super::*;
/// A currently playing instance of a sample.
pub struct Voice {
pub sample: Arc<RwLock<Sample>>,
pub after: usize,
pub position: usize,
pub velocity: f32,
}
impl Iterator for Voice {
type Item = [f32;2];
fn next (&mut self) -> Option<Self::Item> {
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
}
}