From d61f2e3c20e0d1186cb617d873a6929eaec6754a Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 23 Jul 2024 00:45:00 +0300 Subject: [PATCH] add symphony and dasp for sample preview --- Cargo.lock | 337 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/devices/arranger.rs | 4 +- src/devices/sampler.rs | 194 ++++++++++++++++++++--- src/model.rs | 4 +- 5 files changed, 517 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2433687..675be55d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,6 +84,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "atomic_float" version = "1.0.0" @@ -317,6 +323,125 @@ dependencies = [ "winapi", ] +[[package]] +name = "dasp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7381b67da416b639690ac77c73b86a7b5e64a29e31d1f75fb3b1102301ef355a" +dependencies = [ + "dasp_envelope", + "dasp_frame", + "dasp_interpolate", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", + "dasp_signal", + "dasp_slice", + "dasp_window", +] + +[[package]] +name = "dasp_envelope" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec617ce7016f101a87fe85ed44180839744265fae73bb4aa43e7ece1b7668b6" +dependencies = [ + "dasp_frame", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", +] + +[[package]] +name = "dasp_frame" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" +dependencies = [ + "dasp_sample", +] + +[[package]] +name = "dasp_interpolate" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc975a6563bb7ca7ec0a6c784ead49983a21c24835b0bc96eea11ee407c7486" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_sample", +] + +[[package]] +name = "dasp_peak" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf88559d79c21f3d8523d91250c397f9a15b5fc72fbb3f87fdb0a37b79915bf" +dependencies = [ + "dasp_frame", + "dasp_sample", +] + +[[package]] +name = "dasp_ring_buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d79e19b89618a543c4adec9c5a347fe378a19041699b3278e616e387511ea1" + +[[package]] +name = "dasp_rms" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c5dcb30b7e5014486e2822537ea2beae50b19722ffe2ed7549ab03774575aa" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_sample", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "dasp_signal" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa1ab7d01689c6ed4eae3d38fe1cea08cba761573fbd2d592528d55b421077e7" +dependencies = [ + "dasp_envelope", + "dasp_frame", + "dasp_interpolate", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", + "dasp_window", +] + +[[package]] +name = "dasp_slice" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1c7335d58e7baedafa516cb361360ff38d6f4d3f9d9d5ee2a2fc8e27178fa1" +dependencies = [ + "dasp_frame", + "dasp_sample", +] + +[[package]] +name = "dasp_window" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ded7b88821d2ce4e8b842c9f1c86ac911891ab89443cc1de750cae764c5076" +dependencies = [ + "dasp_sample", +] + [[package]] name = "either" version = "1.12.0" @@ -329,12 +454,27 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + [[package]] name = "fraction" version = "0.15.3" @@ -1020,6 +1160,201 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-adpcm", + "symphonia-codec-alac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-caf", + "symphonia-format-isomp4", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-caf" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e43c99c696a388295a29fe71b133079f5d8b18041cf734c5459c35ad9097af50" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "1.0.109" @@ -1052,6 +1387,7 @@ dependencies = [ "clap", "clojure-reader", "crossterm", + "dasp", "fraction", "jack", "livi", @@ -1062,6 +1398,7 @@ dependencies = [ "r8brain-rs", "ratatui", "rlsf", + "symphonia", "toml", "vst", "wavers", diff --git a/Cargo.toml b/Cargo.toml index 874c26d9..1c24922c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,5 @@ 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" ] } diff --git a/src/devices/arranger.rs b/src/devices/arranger.rs index 478d0fd5..538c2557 100644 --- a/src/devices/arranger.rs +++ b/src/devices/arranger.rs @@ -143,7 +143,7 @@ mod draw_vertical { pub fn draw_compact (state: &Arranger, buf: &mut Buffer, area: Rect) -> Usually { let track_cols = track_clip_name_lengths(state.tracks.as_slice()); - let scene_rows = (0..=state.scenes.len()).map(|i|(96, 96*(i+1))).collect::>(); + let scene_rows = (0..=state.scenes.len()+3).map(|i|(96, 96*i)).collect::>(); draw(state, buf, area, track_cols.as_slice(), scene_rows.as_slice()) } @@ -301,7 +301,7 @@ mod draw_vertical { if y > height { break } - let h = (pulses / 96) as u16; + let h = 1.max((pulses / 96) as u16); let area = Rect { x: area.x, y, width: area.width, height: h.min(area.height - y) }; scene_row(state, buf, area, scene, track_cols, offset)?; y = y + h diff --git a/src/devices/sampler.rs b/src/devices/sampler.rs index 963c292f..df10a2fe 100644 --- a/src/devices/sampler.rs +++ b/src/devices/sampler.rs @@ -2,6 +2,13 @@ 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; + /// The sampler plugin plays sounds. pub struct Sampler { pub name: String, @@ -292,7 +299,11 @@ exit!(AddSampleModal); render!(AddSampleModal |self,buf,area|{ make_dim(buf); - let area = center_box(area, 64, 20); + 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, ' '); @@ -306,13 +317,17 @@ render!(AddSampleModal |self,buf,area|{ .enumerate() .skip(self.offset) { + if i >= area.height as usize - 4 { + break + } let t = if is_dir { "" } else { "" }; - format!("{t} {}", name.to_string_lossy()) - .blit(buf, area.x + 2, area.y + 3 + i as u16, Some(if i == self.cursor { - Style::default().green() - } else { - Style::default().white() - }))?; + 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) }); @@ -351,6 +366,42 @@ impl AddSampleModal { 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() { @@ -360,18 +411,16 @@ impl AddSampleModal { return Ok(false) } } - if self.cursor < self.subdirs.len() { - self.dir = self.dir.join(&self.subdirs[self.cursor]); + if let Some(dir) = self.cursor_dir() { + self.dir = dir; self.rescan()?; self.cursor = 0; return Ok(false) } - if (self.cursor - self.subdirs.len()) < self.files.len() { - let file = &self.files[self.cursor - self.subdirs.len()]; - let path = self.dir.join(file); + 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 = file.to_string_lossy().into(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); sample.end = end; sample.channels = channels; return Ok(true) @@ -381,30 +430,33 @@ impl AddSampleModal { } pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { - [Esc, NONE, "add_sample_close", "close help dialog", |modal: &mut AddSampleModal|{ + [Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ modal.exit(); Ok(true) }], - [Up, NONE, "add_sample_prev", "select previous entry", |modal: &mut AddSampleModal|{ + [Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ modal.prev(); Ok(true) }], - [Down, NONE, "add_sample_next", "select next entry", |modal: &mut AddSampleModal|{ + [Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ modal.next(); Ok(true) }], - [Enter, NONE, "add_sample_enter", "activate selected entry", |modal: &mut AddSampleModal|{ + [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)> { - Ok(read_dir(dir)?.fold( - (vec!["..".into()], vec![]), - |(mut subdirs, mut files), entry|{ + 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() { @@ -413,5 +465,105 @@ fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { 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/model.rs b/src/model.rs index 5bb68d61..794eaa2b 100644 --- a/src/model.rs +++ b/src/model.rs @@ -108,7 +108,9 @@ impl App { .collect(); Ok(self) } - pub fn activate (mut self, init: Option>)->Usually<()>>) -> Usually>> { + pub fn activate ( + mut self, init: Option>)->Usually<()>> + ) -> Usually>> { let jack = self.jack.take().expect("no jack client"); let app = Arc::new(RwLock::new(self)); app.write().unwrap().jack = Some(jack.activate(&app.clone(), |state, client, scope|{