diff --git a/src/app.rs b/src/app.rs index b7820826..1bf483e7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,5 @@ use crate::*; +pub mod audio; pub use self::audio::*; pub mod bind; pub use self::bind::*; pub mod cli; pub use self::cli::*; pub mod config; pub use self::config::*; @@ -123,57 +124,6 @@ impl App { }) } - pub fn jack_process ( - &mut self, client: &Client, scope: &ProcessScope - ) -> Control { - let t0 = self.perf.get_t0(); - self.clock().update_from_scope(scope).unwrap(); - let midi_in = self.project.midi_input_collect(scope); - if let Some(editor) = &self.editor() { - let mut pitch: Option = None; - for port in midi_in.iter() { - for event in port.iter() { - if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) - = event - { - pitch = Some(key.clone()); - } - } - } - if let Some(pitch) = pitch { - editor.set_note_pos(pitch.as_int() as usize); - } - } - let result = self.project.process_tracks(client, scope); - self.perf.update_from_jack_scope(t0, scope); - result - } - - pub fn jack_event ( - &mut self, event: JackEvent - ) { - use JackEvent::*; - match event { - SampleRate(sr) => { self.clock().timebase.sr.set(sr as f64); }, - PortRegistration(_id, true) => { - //let port = self.jack().port_by_id(id); - //println!("\rport add: {id} {port:?}"); - //println!("\rport add: {id}"); - }, - PortRegistration(_id, false) => { - /*println!("\rport del: {id}")*/ - }, - PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, - PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, - ClientRegistration(_id, true) => {}, - ClientRegistration(_id, false) => {}, - ThreadInit => {}, - XRun => {}, - GraphReorder => {}, - _ => { panic!("{event:?}"); } - } - } - /// Update memoized render of clock values. /// ``` /// tek::App::default().update_clock(); @@ -389,8 +339,6 @@ impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); impl_has_clips!( |self: App|self.pool.clips); -impl_audio!(App: tek_jack_process, tek_jack_event); - namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); @@ -564,13 +512,14 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua }))) }.draw(to), - Some(":sessions") => h_exact(Some(6), w_min(30, thunk(|to: &mut Tui|{ + Some(":sessions") => h_exact(Some(6), w_min(Some(30), thunk(|to: &mut Tui|{ let fg = Rgb(224, 192, 128); for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; y_push((2 * index) as u16, &h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to); } + Ok(()) }))).draw(to), Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(), diff --git a/src/app/audio.rs b/src/app/audio.rs new file mode 100644 index 00000000..c5eee668 --- /dev/null +++ b/src/app/audio.rs @@ -0,0 +1,50 @@ +use crate::*; + +impl_audio!(App: tek_jack_process, tek_jack_event); + +fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control { + let t0 = state.perf.get_t0(); + state.clock().update_from_scope(scope).unwrap(); + let midi_in = state.project.midi_input_collect(scope); + if let Some(editor) = &state.editor() { + let mut pitch: Option = None; + for port in midi_in.iter() { + for event in port.iter() { + if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) + = event + { + pitch = Some(key.clone()); + } + } + } + if let Some(pitch) = pitch { + editor.set_note_pos(pitch.as_int() as usize); + } + } + let result = state.project.process_tracks(client, scope); + state.perf.update_from_jack_scope(t0, scope); + result +} + +fn tek_jack_event (state: &mut App, event: JackEvent) { + use JackEvent::*; + match event { + SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); }, + PortRegistration(_id, true) => { + //let port = self.jack().port_by_id(id); + //println!("\rport add: {id} {port:?}"); + //println!("\rport add: {id}"); + }, + PortRegistration(_id, false) => { + /*println!("\rport del: {id}")*/ + }, + PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, + PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, + ClientRegistration(_id, true) => {}, + ClientRegistration(_id, false) => {}, + ThreadInit => {}, + XRun => {}, + GraphReorder => {}, + _ => { panic!("{event:?}"); } + } +} diff --git a/src/device.rs b/src/device.rs index d271ec7c..614a8735 100644 --- a/src/device.rs +++ b/src/device.rs @@ -109,7 +109,7 @@ pub mod menu; pub use self::menu::*; pub mod meter; pub use self::meter::*; pub mod mix; pub use self::mix::*; pub mod port; pub use self::port::*; -pub mod sample; pub use self::sample::*; +pub mod sampler; pub use self::sampler::*; pub mod sequence; pub use self::sequence::*; #[cfg(feature = "plugin")] pub mod plugin; diff --git a/src/device/browse.rs b/src/device/browse.rs index a10f486f..4f0e444a 100644 --- a/src/device/browse.rs +++ b/src/device/browse.rs @@ -1,4 +1,4 @@ -use crate::{*, clock::*, sequence::*, sample::*}; +use crate::{*, clock::*, sequence::*, sampler::*}; def_command!(FileBrowserCommand: |sampler: Sampler|{ //("begin" [] Some(Self::Begin)) @@ -261,6 +261,7 @@ impl<'a> PoolView<'a> { })))) } } + impl ClipLength { fn tui (&self) -> impl Draw { use ClipLengthFocus::*; @@ -311,6 +312,7 @@ impl Browse { fn _todo_stub_usize (&self) -> usize { todo!() } fn _todo_stub_arc_str (&self) -> Arc { todo!() } } + impl Browse { fn tui (&self) -> impl Draw { let item = |entry, _index|w_full(origin_w(entry)); @@ -324,6 +326,7 @@ impl Browse { iter_south_fixed(1, iterate, item) } } + impl<'a> Iterator for EntriesIterator<'a, Tui> { type Item = impl Draw; fn next (&mut self) -> Option { @@ -341,6 +344,7 @@ impl<'a> Iterator for EntriesIterator<'a, Tui> { } } } + impl PartialEq for BrowseTarget { fn eq (&self, other: &Self) -> bool { match self { diff --git a/src/device/editor.rs b/src/device/editor.rs index 2ab03f36..943c8514 100644 --- a/src/device/editor.rs +++ b/src/device/editor.rs @@ -16,14 +16,15 @@ use crate::*; /// let _ = editor.edit_status(); /// ``` pub struct MidiEditor { - /// Size of editor on screen - pub size: Size, /// View mode and state of editor pub mode: PianoHorizontal, + /// Size of editor on screen + pub size: Size, } impl_default!(MidiEditor: Self { - size: [0, 0].into(), mode: PianoHorizontal::new(None) + mode: PianoHorizontal::new(None), + size: Size(0.into(), 0.into()), }); impl MidiEditor { diff --git a/src/device/editor/piano.rs b/src/device/editor/piano.rs index d1c6fbd4..d0b4d8f4 100644 --- a/src/device/editor/piano.rs +++ b/src/device/editor/piano.rs @@ -32,10 +32,10 @@ impl Draw for PianoHorizontal { impl PianoHorizontal { pub fn new (clip: Option<&Arc>>) -> Self { - let size: Size = [0, 0].into(); + let size = Size::default(); let mut range = MidiSelection::from((12, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); + range.time_axis = size.0.clone(); + range.note_axis = size.1.clone(); let piano = Self { keys_width: 5, size, diff --git a/src/device/sample.rs b/src/device/sampler.rs similarity index 57% rename from src/device/sample.rs rename to src/device/sampler.rs index b9581142..61abcf40 100644 --- a/src/device/sample.rs +++ b/src/device/sampler.rs @@ -1,51 +1,9 @@ use crate::{*, device::*, browse::*, mix::*}; -def_command!(SamplerCommand: |sampler: Sampler| { - RecordToggle { slot: usize } => { - let slot = *slot; - let recording = sampler.recording.as_ref().map(|x|x.0); - let _ = Self::RecordFinish.act(sampler)?; - // autoslice: continue recording at next slot - if recording != Some(slot) { - Self::RecordBegin { slot }.act(sampler) - } else { - Ok(None) - } - }, - RecordBegin { slot: usize } => { - let slot = *slot; - sampler.recording = Some(( - slot, - Some(Arc::new(RwLock::new(Sample::new( - "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] - )))) - )); - Ok(None) - }, - RecordFinish => { - let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ - std::mem::swap(sample, &mut sampler.samples.0[*index]); - sample - }); // TODO: undo - Ok(None) - }, - RecordCancel => { - sampler.recording = None; - Ok(None) - }, - PlaySample { slot: usize } => { - let slot = *slot; - if let Some(ref sample) = sampler.samples.0[slot] { - sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); - } - Ok(None) - }, - StopSample { slot: usize } => { - let _slot = *slot; - todo!(); - //Ok(None) - }, -}); +mod voice; pub use self::voice::*; +mod sample; pub use self::sample::*; +mod sample_add; pub use self::sample_add::*; +mod sample_kit; pub use self::sample_kit::*; /// Plays [Voice]s from [Sample]s. /// @@ -97,51 +55,17 @@ def_command!(SamplerCommand: |sampler: Sampler| { #[cfg(feature = "meter")] pub output_meters: Vec, } -/// Collection of samples, one per slot, fixed number of slots. -/// -/// History: Separated to cleanly implement [Default]. -/// -/// ``` -/// let samples = tek::SampleKit([None, None, None, None]); -/// ``` -#[derive(Debug)] pub struct SampleKit( - pub [Option>>;N] -); +impl_audio!(Sampler: sampler_jack_process); -/// A sound cut. -/// -/// ``` -/// let sample = tek::Sample::default(); -/// let sample = tek::Sample::new("test", 0, 0, vec![]); -/// ``` -#[derive(Default, Debug)] pub struct Sample { - pub name: Arc, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: f32, - pub color: ItemTheme, -} - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -#[derive(Default, Debug)] pub struct SampleAdd { - pub exited: bool, - pub dir: PathBuf, - pub subdirs: Vec, - pub files: Vec, - pub cursor: usize, - pub offset: usize, - pub sample: Arc>, - pub voices: Arc>>, - pub _search: Option, +pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control { + if let Some(midi_in) = &state.midi_in { + for midi in midi_in.port().iter(scope) { + sampler_midi_in(&state.samples, &state.voices, midi) + } + } + state.process_audio_out(scope); + state.process_audio_in(scope); + Control::Continue } #[derive(Debug)] pub enum SamplerMode { @@ -152,28 +76,6 @@ def_command!(SamplerCommand: |sampler: Sampler| { pub type MidiSample = (Option, Arc>); -impl Default for SampleKit { - fn default () -> Self { Self([const { None }; N]) } -} -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - 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 += 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity * sample.gain, - sample.channels[0][position] * self.velocity * sample.gain, - ]) - } - None - } -} impl NoteRange for Sampler { fn note_lo (&self) -> &AtomicUsize { &self.note_lo @@ -182,6 +84,7 @@ impl NoteRange for Sampler { &self.size.y } } + impl NotePoint for Sampler { fn note_len (&self) -> &AtomicUsize { unreachable!(); @@ -205,6 +108,7 @@ impl NotePoint for Sampler { old } } + impl Sampler { pub fn new ( jack: &Jack<'static>, @@ -331,252 +235,6 @@ impl Sampler { } } } -impl SampleAdd { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } - pub fn new ( - sample: &Arc>, - voices: &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(), - voices: voices.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() { - if let Ok(sample) = Sample::from_file(&path) { - *self.sample.write().unwrap() = sample; - self.voices.write().unwrap().push( - Sample::play(&self.sample, 0, &u7::from(100u8)) - ); - } - //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) - } -} -impl SampleKit { - pub fn get (&self, index: usize) -> &Option>> { - if index < self.0.len() { - &self.0[index] - } else { - &None - } - } -} -impl Sample { - pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { - Self { - name: name.as_ref().into(), - start, - end, - channels, - rate: None, - gain: 1.0, - color: ItemTheme::random(), - } - } - 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, - } - } - pub fn handle_cc (&mut self, controller: u7, value: u7) { - let percentage = value.as_int() as f64 / 127.; - match controller.as_int() { - 20 => { - self.start = (percentage * self.end as f64) as usize; - }, - 21 => { - let length = self.channels[0].len(); - self.end = length.min( - self.start + (percentage * (length as f64 - self.start as f64)) as usize - ); - }, - 22 => { /*attack*/ }, - 23 => { /*decay*/ }, - 24 => { - self.gain = percentage as f32 * 2.0; - }, - 26 => { /* pan */ } - 25 => { /* pitch */ } - _ => {} - } - } - /// Read WAV from file - pub fn read_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)) - } - pub fn from_file (path: &PathBuf) -> Usually { - let name = path.file_name().unwrap().to_string_lossy().into(); - let mut sample = Self { name, ..Default::default() }; - // 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 params = &format.tracks().iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no tracks found") - .codec_params; - let mut decoder = get_codecs().make(params, &Default::default())?; - loop { - match format.next_packet() { - Ok(packet) => sample.decode_packet(&mut decoder, packet)?, - Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), - Err(err) => return Err(err.into()), - }; - }; - sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); - Ok(sample) - } - fn decode_packet ( - &mut self, decoder: &mut Box, packet: Packet - ) -> Usually<()> { - // Decode a packet - let decoded = decoder - .decode(&packet) - .map_err(|e|Box::::from(e))?; - // Determine sample rate - let spec = *decoded.spec(); - if let Some(rate) = self.rate { - if rate != spec.rate as usize { - panic!("sample rate changed"); - } - } else { - self.rate = Some(spec.rate as usize); - } - // Determine channel count - while self.channels.len() < spec.channels.count() { - self.channels.push(vec![]); - } - // Load sample - let mut samples = SampleBuffer::new( - decoded.frames() as u64, - spec - ); - if samples.capacity() > 0 { - samples.copy_interleaved_ref(decoded); - for frame in samples.samples().chunks(spec.channels.count()) { - for (chan, frame) in frame.iter().enumerate() { - self.channels[chan].push(*frame) - } - } - } - Ok(()) - } -} -impl Draw for SampleAdd { - fn draw (self, _to: &mut Tui) -> Usually> { - todo!() - } -} fn draw_list_item (sample: &Option>>) -> String { if let Some(sample) = sample { @@ -652,18 +310,6 @@ fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_ }) } -impl_audio!(Sampler: sampler_jack_process); -pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control { - if let Some(midi_in) = &state.midi_in { - for midi in midi_in.port().iter(scope) { - sampler_midi_in(&state.samples, &state.voices, midi) - } - } - state.process_audio_out(scope); - state.process_audio_in(scope); - Control::Continue -} - /// Create [Voice]s from [Sample]s in response to MIDI input. fn sampler_midi_in ( samples: &SampleKit<128>, voices: &Arc>>, RawMidi { time, bytes }: RawMidi @@ -704,3 +350,50 @@ fn draw_sample ( fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { todo!(); } + +def_command!(SamplerCommand: |sampler: Sampler| { + RecordToggle { slot: usize } => { + let slot = *slot; + let recording = sampler.recording.as_ref().map(|x|x.0); + let _ = Self::RecordFinish.act(sampler)?; + // autoslice: continue recording at next slot + if recording != Some(slot) { + Self::RecordBegin { slot }.act(sampler) + } else { + Ok(None) + } + }, + RecordBegin { slot: usize } => { + let slot = *slot; + sampler.recording = Some(( + slot, + Some(Arc::new(RwLock::new(Sample::new( + "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] + )))) + )); + Ok(None) + }, + RecordFinish => { + let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ + std::mem::swap(sample, &mut sampler.samples.0[*index]); + sample + }); // TODO: undo + Ok(None) + }, + RecordCancel => { + sampler.recording = None; + Ok(None) + }, + PlaySample { slot: usize } => { + let slot = *slot; + if let Some(ref sample) = sampler.samples.0[slot] { + sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); + } + Ok(None) + }, + StopSample { slot: usize } => { + let _slot = *slot; + todo!(); + //Ok(None) + }, +}); diff --git a/src/device/sampler/sample.rs b/src/device/sampler/sample.rs new file mode 100644 index 00000000..06caf01f --- /dev/null +++ b/src/device/sampler/sample.rs @@ -0,0 +1,144 @@ +use crate::*; + +/// A sound cut. +/// +/// ``` +/// let sample = tek::Sample::default(); +/// let sample = tek::Sample::new("test", 0, 0, vec![]); +/// ``` +#[derive(Default, Debug)] pub struct Sample { + pub name: Arc, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: f32, + pub color: ItemTheme, +} + +impl Sample { + pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { + Self { + name: name.as_ref().into(), + start, + end, + channels, + rate: None, + gain: 1.0, + color: ItemTheme::random(), + } + } + 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, + } + } + pub fn handle_cc (&mut self, controller: u7, value: u7) { + let percentage = value.as_int() as f64 / 127.; + match controller.as_int() { + 20 => { + self.start = (percentage * self.end as f64) as usize; + }, + 21 => { + let length = self.channels[0].len(); + self.end = length.min( + self.start + (percentage * (length as f64 - self.start as f64)) as usize + ); + }, + 22 => { /*attack*/ }, + 23 => { /*decay*/ }, + 24 => { + self.gain = percentage as f32 * 2.0; + }, + 26 => { /* pan */ } + 25 => { /* pitch */ } + _ => {} + } + } + /// Read WAV from file + pub fn read_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)) + } + pub fn from_file (path: &PathBuf) -> Usually { + let name = path.file_name().unwrap().to_string_lossy().into(); + let mut sample = Self { name, ..Default::default() }; + // 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 params = &format.tracks().iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no tracks found") + .codec_params; + let mut decoder = get_codecs().make(params, &Default::default())?; + loop { + match format.next_packet() { + Ok(packet) => sample.decode_packet(&mut decoder, packet)?, + Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), + Err(err) => return Err(err.into()), + }; + }; + sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); + Ok(sample) + } + fn decode_packet ( + &mut self, decoder: &mut Box, packet: Packet + ) -> Usually<()> { + // Decode a packet + let decoded = decoder + .decode(&packet) + .map_err(|e|Box::::from(e))?; + // Determine sample rate + let spec = *decoded.spec(); + if let Some(rate) = self.rate { + if rate != spec.rate as usize { + panic!("sample rate changed"); + } + } else { + self.rate = Some(spec.rate as usize); + } + // Determine channel count + while self.channels.len() < spec.channels.count() { + self.channels.push(vec![]); + } + // Load sample + let mut samples = SampleBuffer::new( + decoded.frames() as u64, + spec + ); + if samples.capacity() > 0 { + samples.copy_interleaved_ref(decoded); + for frame in samples.samples().chunks(spec.channels.count()) { + for (chan, frame) in frame.iter().enumerate() { + self.channels[chan].push(*frame) + } + } + } + Ok(()) + } +} diff --git a/src/device/sampler/sample_add.rs b/src/device/sampler/sample_add.rs new file mode 100644 index 00000000..996e4b04 --- /dev/null +++ b/src/device/sampler/sample_add.rs @@ -0,0 +1,126 @@ +use crate::{*, device::sampler::*}; + +#[derive(Default, Debug)] pub struct SampleAdd { + pub exited: bool, + pub dir: PathBuf, + pub subdirs: Vec, + pub files: Vec, + pub cursor: usize, + pub offset: usize, + pub sample: Arc>, + pub voices: Arc>>, + pub _search: Option, +} + +impl SampleAdd { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } + pub fn new ( + sample: &Arc>, + voices: &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(), + voices: voices.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() { + if let Ok(sample) = Sample::from_file(&path) { + *self.sample.write().unwrap() = sample; + self.voices.write().unwrap().push( + Sample::play(&self.sample, 0, &u7::from(100u8)) + ); + } + //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) + } +} + +impl Draw for SampleAdd { + fn draw (self, _to: &mut Tui) -> Usually> { + todo!() + } +} diff --git a/src/device/sampler/sample_kit.rs b/src/device/sampler/sample_kit.rs new file mode 100644 index 00000000..382ce678 --- /dev/null +++ b/src/device/sampler/sample_kit.rs @@ -0,0 +1,26 @@ +use crate::*; + +/// Collection of samples, one per slot, fixed number of slots. +/// +/// History: Separated to cleanly implement [Default]. +/// +/// ``` +/// let samples = tek::SampleKit([None, None, None, None]); +/// ``` +#[derive(Debug)] pub struct SampleKit ( + pub [Option>>;N] +); + +impl Default for SampleKit { + fn default () -> Self { Self([const { None }; N]) } +} + +impl SampleKit { + pub fn get (&self, index: usize) -> &Option>> { + if index < self.0.len() { + &self.0[index] + } else { + &None + } + } +} diff --git a/src/device/sampler/voice.rs b/src/device/sampler/voice.rs new file mode 100644 index 00000000..632e5202 --- /dev/null +++ b/src/device/sampler/voice.rs @@ -0,0 +1,29 @@ +use crate::*; + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] 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 -= 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position += 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity * sample.gain, + sample.channels[0][position] * self.velocity * sample.gain, + ]) + } + None + } +}