diff --git a/crates/tek/src/groovebox.rs b/crates/tek/src/groovebox.rs index a04774c2..3a1a24f4 100644 --- a/crates/tek/src/groovebox.rs +++ b/crates/tek/src/groovebox.rs @@ -12,7 +12,9 @@ pub struct GrooveboxTui { from_jack!(|jack|GrooveboxTui { let mut sequencer = SequencerTui::try_from(jack)?; - sequencer.status = false; + sequencer.status = false; + sequencer.transport = false; + sequencer.selectors = false; let midi_in_1 = jack.read().unwrap().register_port("in1", MidiIn::default())?; let midi_out = jack.read().unwrap().register_port("out", MidiOut::default())?; let midi_in_2 = jack.read().unwrap().register_port("in2", MidiIn::default())?; @@ -35,17 +37,31 @@ pub enum GrooveboxFocus { } audio!(|self:GrooveboxTui,_client,_process|Control::Continue); - -render!(|self:GrooveboxTui|Fill::wh(Bsp::n( - Fixed::h(2, GrooveboxStatus::from(self)), - Fill::h(lay!([ - Fill::h(&self.size), - Fill::h(Bsp::s( - Tui::min_y(20, &self.sequencer), - Tui::min_y(20, &self.sampler), - )) - ])), -))); +has_clock!(|self:GrooveboxTui|&self.sequencer.clock); +render!(|self:GrooveboxTui|Fill::wh(lay!([ + &self.size, + Fill::wh(Align::s(Fixed::h(2, GrooveboxStatus::from(self)))), + Tui::shrink_y(2, col!([ + Fixed::h(2, row!([ + Fixed::wh(5, 2, PlayPause(self.clock().is_rolling())), + Fixed::h(2, TransportView::from((self, self.sequencer.player.play_phrase().as_ref().map(|(_,p)| + p.as_ref().map(|p|p.read().unwrap().color) + ).flatten().clone(), true))), + ])), + Tui::push_x(20, Fixed::h(1, row!([ + PhraseSelector::play_phrase(&self.sequencer.player), + PhraseSelector::next_phrase(&self.sequencer.player), + ]))), + row!([ + Tui::pull_y(1, Tui::shrink_y(0, Fill::h(Fixed::w(20, &self.sampler)))), + Fill::wh(&self.sequencer), + ]), + ])) +]))); + //Bsp::n( + //Fill::wh(lay!([ + //])), +//))); pub enum GrooveboxCommand { Sequencer(SequencerCommand), diff --git a/crates/tek/src/sampler.rs b/crates/tek/src/sampler.rs index ba68e5db..0caa6a45 100644 --- a/crates/tek/src/sampler.rs +++ b/crates/tek/src/sampler.rs @@ -1,156 +1,5 @@ use crate::{*, tui::piano_h::PianoHorizontalKeys}; -/// The sampler plugin plays sounds. -#[derive(Debug)] -pub struct Sampler { - pub jack: Arc>, - pub name: String, - pub mapped: BTreeMap>>, - pub unmapped: Vec>>, - pub voices: Arc>>, - pub midi_in: Port, - pub audio_outs: Vec>, - pub buffer: Vec>, - pub output_gain: f32 -} - -/// A sound sample. -#[derive(Default, Debug)] -pub struct Sample { - pub name: String, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, -} - -/// 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, -} - -/// 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() - ) - }}; -} - -impl Sampler { - - /// Create [Voice]s from [Sample]s in response to MIDI input. - pub fn process_midi_in (&mut self, scope: &ProcessScope) { - let Sampler { midi_in, mapped, voices, .. } = self; - for RawMidi { time, bytes } in midi_in.iter(scope) { - if let LiveEvent::Midi { - message: MidiMessage::NoteOn { ref key, ref vel }, .. - } = LiveEvent::parse(bytes).unwrap() { - if let Some(sample) = mapped.get(key) { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); - } - } - } - } - - /// Zero the output buffer. - pub fn clear_output_buffer (&mut self) { - for buffer in self.buffer.iter_mut() { - buffer.fill(0.0); - } - } - - /// Mix all currently playing samples into the output. - pub fn process_audio_out (&mut self, scope: &ProcessScope) { - let Sampler { ref mut buffer, voices, output_gain, .. } = self; - let channel_count = buffer.len(); - voices.write().unwrap().retain_mut(|voice|{ - for index in 0..scope.n_frames() as usize { - if let Some(frame) = voice.next() { - for (channel, sample) in frame.iter().enumerate() { - // Averaging mixer: - //self.buffer[channel % channel_count][index] = ( - //(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0 - //); - buffer[channel % channel_count][index] += sample * *output_gain; - } - } else { - return false - } - } - true - }); - } - - /// Write output buffer to output ports. - pub fn write_output_buffer (&mut self, scope: &ProcessScope) { - let Sampler { ref mut audio_outs, buffer, .. } = self; - for (i, port) in audio_outs.iter_mut().enumerate() { - let buffer = &buffer[i]; - for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() { - *value = *buffer.get(i).unwrap_or(&0.0); - } - } - } - -} - -impl Sample { - pub fn new (name: &str, start: usize, end: usize, channels: Vec>) -> Self { - 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, - } - } - /// 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)) - } -} - -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.channels[0][position] * self.velocity, - ]) - } - None - } -} - use KeyCode::Char; use std::fs::File; use symphonia::{ @@ -165,6 +14,39 @@ use symphonia::{ default::get_codecs, }; +pub mod sample; +pub(crate) use self::sample::*; +pub use self::sample::Sample; + +pub mod voice; +pub(crate) use self::voice::*; +pub use self::voice::Voice; + +pub mod sampler_control; +pub(crate) use self::sampler_control::*; + +pub mod sampler_audio; +pub(crate) use self::sampler_audio::*; + +pub mod sampler_keys; +pub(crate) use self::sampler_keys::*; + +pub mod sample_import; +pub(crate) use self::sample_import::*; + +/// The sampler plugin plays sounds. +#[derive(Debug)] +pub struct Sampler { + pub jack: Arc>, + pub name: String, + pub mapped: BTreeMap>>, + pub unmapped: Vec>>, + pub voices: Arc>>, + pub midi_in: Port, + pub audio_outs: Vec>, + pub buffer: Vec>, + pub output_gain: f32 +} pub struct SamplerTui { pub state: Sampler, pub cursor: (usize, usize), @@ -177,7 +59,22 @@ pub struct SamplerTui { pub note_pt: AtomicUsize, color: ItemPalette } - +impl SamplerTui { + /// Immutable reference to sample at cursor. + pub fn sample (&self) -> Option<&Arc>> { + for (i, sample) in self.state.mapped.values().enumerate() { + if i == self.cursor.0 { + return Some(sample) + } + } + for (i, sample) in self.state.unmapped.iter().enumerate() { + if i + self.state.mapped.len() == self.cursor.0 { + return Some(sample) + } + } + None + } +} from_jack!(|jack|SamplerTui{ let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?; let audio_outs = vec![ @@ -205,7 +102,6 @@ from_jack!(|jack|SamplerTui{ }, } }); - render!(|self: SamplerTui|{ let keys_width = 5; let keys = move||SamplerKeys(self); @@ -236,7 +132,6 @@ render!(|self: SamplerTui|{ ))), )))) }); - impl NoteRange for SamplerTui { fn note_lo (&self) -> &AtomicUsize { &self.note_lo } fn note_axis (&self) -> &AtomicUsize { &self.size.y } @@ -248,419 +143,7 @@ impl NotePoint for SamplerTui { fn set_note_point (&self, x: usize) { self.note_pt.store(x, Relaxed); } } -struct SamplerKeys<'a>(&'a SamplerTui); -has_color!(|self: SamplerKeys<'a>|self.0.color.base); -render!(|self: SamplerKeys<'a>|render(|to|Ok(render_keys_v(to, self)))); -impl<'a> NoteRange for SamplerKeys<'a> { - fn note_lo (&self) -> &AtomicUsize { &self.0.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.0.size.y } -} -impl<'a> NotePoint for SamplerKeys<'a> { - fn note_len (&self) -> usize {0/*TODO*/} - fn set_note_len (&self, x: usize) {} - fn note_point (&self) -> usize { self.0.note_point() } - fn set_note_point (&self, x: usize) { self.0.set_note_point(x); } -} - pub enum SamplerMode { // Load sample from path Import(usize, FileBrowser), } -handle!(|self:SamplerTui,input|SamplerCommand::execute_with_state(self, input)); -pub enum SamplerCommand { - Import(FileBrowserCommand), - SelectNote(usize), - SelectField(usize), - SetName(String), - SetNote(u7, Arc>), - SetGain(f32), - NoteOn(u7, u7), - NoteOff(u7) -} -input_to_command!(SamplerCommand: |state: SamplerTui, input|match state.mode { - Some(SamplerMode::Import(..)) => Self::Import( - FileBrowserCommand::input_to_command(state, input)? - ), - _ => match input.event() { - // load sample - key_pat!(Shift-Char('L')) => { - Self::Import(FileBrowserCommand::Begin) - }, - key_pat!(KeyCode::Up) => { - Self::SelectNote(state.note_point().overflowing_add(1).0.min(127)) - }, - key_pat!(KeyCode::Down) => { - Self::SelectNote(state.note_point().overflowing_sub(1).0.min(127)) - }, - _ => return None - } - //key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { - //voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); - //}, - //key_pat!(KeyCode::Char('a')) => { - //let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - //self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - //unmapped.push(sample); - //}, - //key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { - //self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - //}, - //key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { - //self.editing = Some(sample.clone()); - //}, - //_ => { - //return Ok(None) - //} - //} -}); -input_to_command!(FileBrowserCommand:|state:SamplerTui,input|match input { - _ => return None -}); -command!(|self:SamplerCommand,state:SamplerTui|match self { - Self::Import(FileBrowserCommand::Begin) => { - let voices = &state.state.voices; - let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); - None - }, - Self::SelectNote(index) => { - let old = state.note_point(); - state.set_note_point(index); - Some(Self::SelectNote(old)) - }, - _ => todo!() -}); -command!(|self:FileBrowserCommand,state:SamplerTui|match self { - _ => todo!() -}); -impl SamplerTui { - /// Immutable reference to sample at cursor. - pub fn sample (&self) -> Option<&Arc>> { - for (i, sample) in self.state.mapped.values().enumerate() { - if i == self.cursor.0 { - return Some(sample) - } - } - for (i, sample) in self.state.unmapped.iter().enumerate() { - if i + self.state.mapped.len() == self.cursor.0 { - return Some(sample) - } - } - None - } -} - -audio!(|self: SamplerTui, _client, scope|{ - self.state.process_midi_in(scope); - self.state.clear_output_buffer(); - self.state.process_audio_out(scope); - self.state.write_output_buffer(scope); - Control::Continue -}); - -pub struct AddSampleModal { - exited: bool, - dir: PathBuf, - subdirs: Vec, - files: Vec, - cursor: usize, - offset: usize, - sample: Arc>, - voices: Arc>>, - _search: Option, -} - -impl Exit for AddSampleModal { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } -} - -impl AddSampleModal { - 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) - } -} - -fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { - todo!(); -} - -fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - let (mut subdirs, mut files) = std::fs::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 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(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(()) - } -} - -fn draw_sample ( - to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool -) -> Usually { - let style = if focus { Style::default().green() } else { Style::default() }; - if focus { - to.blit(&"🬴", x+1, y, Some(style.bold())); - } - let label1 = format!("{:3} {:12}", - note.map(|n|n.to_string()).unwrap_or(String::default()), - sample.name); - let label2 = format!("{:>6} {:>6} +0.0", - sample.start, - sample.end); - to.blit(&label1, x+2, y, Some(style.bold())); - to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); - Ok(label1.len() + label2.len() + 4) -} - -impl Render for AddSampleModal { - fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - todo!() - //Align::Center(()).layout(to) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - todo!() - //let area = to.area(); - //to.make_dim(); - //let area = center_box( - //area, - //64.max(area.w().saturating_sub(8)), - //20.max(area.w().saturating_sub(8)), - //); - //to.fill_fg(area, Color::Reset); - //to.fill_bg(area, Nord::bg_lo(true, true)); - //to.fill_char(area, ' '); - //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; - //to.blit(&"Select sample:", 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.h() 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.w() as usize - 4)]; - //to.blit(&line, 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(to) - } -} - -//impl Handle for AddSampleModal { - //fn handle (&mut self, from: &TuiInput) -> Perhaps { - //if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { - //return Ok(Some(true)) - //} - //Ok(Some(true)) - //} -//} - -//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) - //}] -//}); diff --git a/crates/tek/src/sampler/sample.rs b/crates/tek/src/sampler/sample.rs new file mode 100644 index 00000000..adfc9fa0 --- /dev/null +++ b/crates/tek/src/sampler/sample.rs @@ -0,0 +1,120 @@ +use crate::*; +use super::*; + +/// A sound sample. +#[derive(Default, Debug)] +pub struct Sample { + pub name: String, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, +} + +/// 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() + ) + }}; +} + +impl Sample { + pub fn new (name: &str, start: usize, end: usize, channels: Vec>) -> Self { + 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, + } + } + /// 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/crates/tek/src/sampler/sample_import.rs b/crates/tek/src/sampler/sample_import.rs new file mode 100644 index 00000000..e14609ff --- /dev/null +++ b/crates/tek/src/sampler/sample_import.rs @@ -0,0 +1,246 @@ +use crate::*; +use super::*; + +input_to_command!(FileBrowserCommand:|state:SamplerTui,input|match input { + _ => return None +}); + +command!(|self:FileBrowserCommand,state:SamplerTui|match self { + _ => todo!() +}); + +pub struct AddSampleModal { + exited: bool, + dir: PathBuf, + subdirs: Vec, + files: Vec, + cursor: usize, + offset: usize, + sample: Arc>, + voices: Arc>>, + _search: Option, +} + +impl Exit for AddSampleModal { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } +} + +impl AddSampleModal { + 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) + } +} + +fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { + todo!(); +} + +fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { + let (mut subdirs, mut files) = std::fs::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 draw_sample ( + to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool +) -> Usually { + let style = if focus { Style::default().green() } else { Style::default() }; + if focus { + to.blit(&"🬴", x+1, y, Some(style.bold())); + } + let label1 = format!("{:3} {:12}", + note.map(|n|n.to_string()).unwrap_or(String::default()), + sample.name); + let label2 = format!("{:>6} {:>6} +0.0", + sample.start, + sample.end); + to.blit(&label1, x+2, y, Some(style.bold())); + to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); + Ok(label1.len() + label2.len() + 4) +} + +impl Render for AddSampleModal { + fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + todo!() + //Align::Center(()).layout(to) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + todo!() + //let area = to.area(); + //to.make_dim(); + //let area = center_box( + //area, + //64.max(area.w().saturating_sub(8)), + //20.max(area.w().saturating_sub(8)), + //); + //to.fill_fg(area, Color::Reset); + //to.fill_bg(area, Nord::bg_lo(true, true)); + //to.fill_char(area, ' '); + //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; + //to.blit(&"Select sample:", 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.h() 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.w() as usize - 4)]; + //to.blit(&line, 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(to) + } +} + +//impl Handle for AddSampleModal { + //fn handle (&mut self, from: &TuiInput) -> Perhaps { + //if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { + //return Ok(Some(true)) + //} + //Ok(Some(true)) + //} +//} + +//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) + //}] +//}); diff --git a/crates/tek/src/sampler/sampler_audio.rs b/crates/tek/src/sampler/sampler_audio.rs new file mode 100644 index 00000000..60efa41f --- /dev/null +++ b/crates/tek/src/sampler/sampler_audio.rs @@ -0,0 +1,67 @@ +use crate::*; + +audio!(|self: SamplerTui, _client, scope|{ + self.state.process_midi_in(scope); + self.state.clear_output_buffer(); + self.state.process_audio_out(scope); + self.state.write_output_buffer(scope); + Control::Continue +}); + +impl Sampler { + + /// Create [Voice]s from [Sample]s in response to MIDI input. + pub fn process_midi_in (&mut self, scope: &ProcessScope) { + let Sampler { midi_in, mapped, voices, .. } = self; + for RawMidi { time, bytes } in midi_in.iter(scope) { + if let LiveEvent::Midi { + message: MidiMessage::NoteOn { ref key, ref vel }, .. + } = LiveEvent::parse(bytes).unwrap() { + if let Some(sample) = mapped.get(key) { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + } + } + } + + /// Zero the output buffer. + pub fn clear_output_buffer (&mut self) { + for buffer in self.buffer.iter_mut() { + buffer.fill(0.0); + } + } + + /// Mix all currently playing samples into the output. + pub fn process_audio_out (&mut self, scope: &ProcessScope) { + let Sampler { ref mut buffer, voices, output_gain, .. } = self; + let channel_count = buffer.len(); + voices.write().unwrap().retain_mut(|voice|{ + for index in 0..scope.n_frames() as usize { + if let Some(frame) = voice.next() { + for (channel, sample) in frame.iter().enumerate() { + // Averaging mixer: + //self.buffer[channel % channel_count][index] = ( + //(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0 + //); + buffer[channel % channel_count][index] += sample * *output_gain; + } + } else { + return false + } + } + true + }); + } + + /// Write output buffer to output ports. + pub fn write_output_buffer (&mut self, scope: &ProcessScope) { + let Sampler { ref mut audio_outs, buffer, .. } = self; + for (i, port) in audio_outs.iter_mut().enumerate() { + let buffer = &buffer[i]; + for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() { + *value = *buffer.get(i).unwrap_or(&0.0); + } + } + } + +} diff --git a/crates/tek/src/sampler/sampler_control.rs b/crates/tek/src/sampler/sampler_control.rs new file mode 100644 index 00000000..0a8d9c63 --- /dev/null +++ b/crates/tek/src/sampler/sampler_control.rs @@ -0,0 +1,66 @@ +use crate::*; +use KeyCode::Char; + +handle!(|self:SamplerTui,input|SamplerCommand::execute_with_state(self, input)); + +pub enum SamplerCommand { + Import(FileBrowserCommand), + SelectNote(usize), + SelectField(usize), + SetName(String), + SetNote(u7, Arc>), + SetGain(f32), + NoteOn(u7, u7), + NoteOff(u7) +} + +input_to_command!(SamplerCommand: |state: SamplerTui, input|match state.mode { + Some(SamplerMode::Import(..)) => Self::Import( + FileBrowserCommand::input_to_command(state, input)? + ), + _ => match input.event() { + // load sample + key_pat!(Shift-Char('L')) => { + Self::Import(FileBrowserCommand::Begin) + }, + key_pat!(KeyCode::Up) => { + Self::SelectNote(state.note_point().overflowing_add(1).0.min(127)) + }, + key_pat!(KeyCode::Down) => { + Self::SelectNote(state.note_point().overflowing_sub(1).0.min(127)) + }, + _ => return None + } + //key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { + //voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); + //}, + //key_pat!(KeyCode::Char('a')) => { + //let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + //self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + //unmapped.push(sample); + //}, + //key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { + //self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + //}, + //key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { + //self.editing = Some(sample.clone()); + //}, + //_ => { + //return Ok(None) + //} + //} +}); +command!(|self:SamplerCommand,state:SamplerTui|match self { + Self::Import(FileBrowserCommand::Begin) => { + let voices = &state.state.voices; + let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); + None + }, + Self::SelectNote(index) => { + let old = state.note_point(); + state.set_note_point(index); + Some(Self::SelectNote(old)) + }, + _ => todo!() +}); diff --git a/crates/tek/src/sampler/sampler_keys.rs b/crates/tek/src/sampler/sampler_keys.rs new file mode 100644 index 00000000..92fad051 --- /dev/null +++ b/crates/tek/src/sampler/sampler_keys.rs @@ -0,0 +1,15 @@ +use crate::*; + +pub struct SamplerKeys<'a>(pub &'a SamplerTui); +has_color!(|self: SamplerKeys<'a>|self.0.color.base); +render!(|self: SamplerKeys<'a>|render(|to|Ok(render_keys_v(to, self)))); +impl<'a> NoteRange for SamplerKeys<'a> { + fn note_lo (&self) -> &AtomicUsize { &self.0.note_lo } + fn note_axis (&self) -> &AtomicUsize { &self.0.size.y } +} +impl<'a> NotePoint for SamplerKeys<'a> { + fn note_len (&self) -> usize {0/*TODO*/} + fn set_note_len (&self, x: usize) {} + fn note_point (&self) -> usize { self.0.note_point() } + fn set_note_point (&self, x: usize) { self.0.set_note_point(x); } +} diff --git a/crates/tek/src/sampler/voice.rs b/crates/tek/src/sampler/voice.rs new file mode 100644 index 00000000..738f51aa --- /dev/null +++ b/crates/tek/src/sampler/voice.rs @@ -0,0 +1,30 @@ +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.channels[0][position] * self.velocity, + ]) + } + None + } +} diff --git a/crates/tek/src/sequencer.rs b/crates/tek/src/sequencer.rs index 2613ffdc..7b24dc5d 100644 --- a/crates/tek/src/sequencer.rs +++ b/crates/tek/src/sequencer.rs @@ -7,15 +7,17 @@ use PhrasePoolCommand::*; /// Root view for standalone `tek_sequencer`. pub struct SequencerTui { _jack: Arc>, - pub clock: ClockModel, - pub phrases: PoolModel, - pub player: MidiPlayer, - pub editor: MidiEditorModel, - pub size: Measure, - pub status: bool, - pub note_buf: Vec, - pub midi_buf: Vec>>, - pub perf: PerfModel, + pub transport: bool, + pub selectors: bool, + pub clock: ClockModel, + pub phrases: PoolModel, + pub player: MidiPlayer, + pub editor: MidiEditorModel, + pub size: Measure, + pub status: bool, + pub note_buf: Vec, + pub midi_buf: Vec>>, + pub perf: PerfModel, } from_jack!(|jack|SequencerTui { let clock = ClockModel::from(jack); @@ -25,6 +27,8 @@ from_jack!(|jack|SequencerTui { ))); Self { _jack: jack.clone(), + transport: true, + selectors: true, phrases: PoolModel::from(&phrase), editor: MidiEditorModel::from(&phrase), player: MidiPlayer::from((&clock, &phrase)), @@ -40,7 +44,7 @@ render!(|self: SequencerTui|{ let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; let pool_w = if self.phrases.visible { phrase_w } else { 0 }; - let pool = Fill::h(Align::e(PoolView(&self.phrases))); + let pool = Tui::pull_y(1, Fill::h(Align::e(PoolView(&self.phrases)))); let with_pool = move|x|Tui::split_w(false, pool_w, pool, x); let status = SequencerStatus::from(self); let with_status = |x|Tui::split_n(false, if self.status { 2 } else { 0 }, status, x); @@ -50,13 +54,14 @@ render!(|self: SequencerTui|{ let color = self.player.play_phrase().as_ref().map(|(_,p)| p.as_ref().map(|p|p.read().unwrap().color) ).flatten().clone(); - let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); - let transport = Fixed::h(2, TransportView::from((self, color, true))); - let toolbar = row!([play, transport]); + let toolbar = row!([ + Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())), + Fixed::h(2, TransportView::from((self, color, true))), + ]).when(self.transport); let play_queue = row!([ PhraseSelector::play_phrase(&self.player), PhraseSelector::next_phrase(&self.player), - ]); + ]).when(self.selectors);; Tui::min_y(15, with_size(with_status(col!([ toolbar, play_queue, editor, ])))) }); audio!(|self:SequencerTui, client, scope|{ diff --git a/crates/tek/src/space.rs b/crates/tek/src/space.rs index ec0cbd7e..7b6a624b 100644 --- a/crates/tek/src/space.rs +++ b/crates/tek/src/space.rs @@ -9,7 +9,7 @@ use std::fmt::{Display, Debug}; pub(crate) mod align; pub(crate) mod bsp; -pub(crate) mod cond; +pub(crate) mod cond; pub(crate) use cond::*; pub(crate) mod fill; pub(crate) mod fixed; pub(crate) use fixed::*; pub(crate) mod inset_outset; pub(crate) use inset_outset::*; diff --git a/crates/tek/src/status/status_arranger.rs b/crates/tek/src/status/status_arranger.rs index a8f80422..751fb030 100644 --- a/crates/tek/src/status/status_arranger.rs +++ b/crates/tek/src/status/status_arranger.rs @@ -6,7 +6,6 @@ pub struct ArrangerStatus { pub(crate) width: usize, pub(crate) cpu: Option, pub(crate) size: String, - pub(crate) res: String, pub(crate) playing: bool, } from!(|state:&ArrangerTui|ArrangerStatus = { @@ -19,7 +18,6 @@ from!(|state:&ArrangerTui|ArrangerStatus = { playing: state.clock.is_rolling(), cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), size: format!("{}x{}│", width, state.size.h()), - res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), } }); render!(|self: ArrangerStatus|Fixed::h(2, lay!([ @@ -49,6 +47,6 @@ impl ArrangerStatus { ])) } fn stats (&self) -> impl Render + use<'_> { - row!([&self.cpu, &self.res, &self.size]) + row!([&self.cpu, &self.size]) } } diff --git a/crates/tek/src/status/status_groovebox.rs b/crates/tek/src/status/status_groovebox.rs index 65d77a2d..e92e99cf 100644 --- a/crates/tek/src/status/status_groovebox.rs +++ b/crates/tek/src/status/status_groovebox.rs @@ -6,7 +6,6 @@ pub struct GrooveboxStatus { pub(crate) width: usize, pub(crate) cpu: Option, pub(crate) size: String, - pub(crate) res: String, pub(crate) playing: bool, } from!(|state:&GrooveboxTui|GrooveboxStatus = { @@ -19,7 +18,6 @@ from!(|state:&GrooveboxTui|GrooveboxStatus = { playing: state.sequencer.clock.is_rolling(), cpu: state.sequencer.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), size: format!("{}x{}│", width, state.size.h()), - res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), } }); render!(|self: GrooveboxStatus|Fixed::h(2, lay!([ @@ -47,6 +45,6 @@ impl GrooveboxStatus { ])) } fn stats (&self) -> impl Render + use<'_> { - row!([&self.cpu, &self.res, &self.size]) + row!([&self.cpu, &self.size]) } } diff --git a/crates/tek/src/status/status_sequencer.rs b/crates/tek/src/status/status_sequencer.rs index 91257cad..9bf9c020 100644 --- a/crates/tek/src/status/status_sequencer.rs +++ b/crates/tek/src/status/status_sequencer.rs @@ -6,7 +6,6 @@ pub struct SequencerStatus { pub(crate) width: usize, pub(crate) cpu: Option, pub(crate) size: String, - pub(crate) res: String, pub(crate) playing: bool, } from!(|state:&SequencerTui|SequencerStatus = { @@ -19,7 +18,6 @@ from!(|state:&SequencerTui|SequencerStatus = { playing: state.clock.is_rolling(), cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), size: format!("{}x{}│", width, state.size.h()), - res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), } }); render!(|self: SequencerStatus|Fixed::h(2, lay!([ @@ -47,6 +45,6 @@ impl SequencerStatus { ])) } fn stats (&self) -> impl Render + use<'_> { - row!([&self.cpu, &self.res, &self.size]) + row!([&self.cpu, &self.size]) } }