use crate::*; /// The sampler plugin plays sounds. pub struct Sampler { _engine: PhantomData, pub jack: Arc>, pub name: String, pub cursor: (usize, usize), pub editing: Option>>, pub mapped: BTreeMap>>, pub unmapped: Vec>>, pub voices: Arc>>, pub ports: JackPorts, pub buffer: Vec>, pub modal: Arc>>>, pub output_gain: f32 } impl Sampler { pub fn from_edn <'e> (args: &[Edn<'e>]) -> Usually> { let mut name = String::new(); let mut dir = String::new(); let mut samples = BTreeMap::new(); edn!(edn in args { Edn::Map(map) => { if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { name = String::from(*n); } if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) { dir = String::from(*n); } }, Edn::List(args) => match args.get(0) { Some(Edn::Symbol("sample")) => { let (midi, sample) = Sample::from_edn(&dir, &args[1..])?; if let Some(midi) = midi { samples.insert(midi, sample); } else { panic!("sample without midi binding: {}", sample.read().unwrap().name); } }, _ => panic!("unexpected in sampler {name}: {args:?}") }, _ => panic!("unexpected in sampler {name}: {edn:?}") }); Self::new(&name, Some(samples)) } pub fn new ( name: &str, mapped: Option>>> ) -> Usually> { Jack::new(name)? .midi_in("midi") .audio_in("recL") .audio_in("recR") .audio_out("outL") .audio_out("outR") .run(|ports|Box::new(Self { _engine: Default::default(), name: name.into(), cursor: (0, 0), editing: None, mapped: mapped.unwrap_or_else(||BTreeMap::new()), unmapped: vec![], voices: Arc::new(RwLock::new(vec![])), ports, buffer: vec![vec![0.0;16384];2], output_gain: 0.5, modal: Default::default() })) } /// Immutable reference to sample at cursor. pub fn sample (&self) -> Option<&Arc>> { for (i, sample) in self.mapped.values().enumerate() { if i == self.cursor.0 { return Some(sample) } } for (i, sample) in self.unmapped.iter().enumerate() { if i + self.mapped.len() == self.cursor.0 { return Some(sample) } } None } /// Create [Voice]s from [Sample]s in response to MIDI input. fn process_midi_in (&mut self, scope: &ProcessScope) { for RawMidi { time, bytes } in self.ports.midi_ins.get("midi").unwrap().iter(scope) { if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { if let MidiMessage::NoteOn { ref key, ref vel } = message { if let Some(sample) = self.mapped.get(key) { self.voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); } } } } } /// Zero the output buffer. 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. fn process_audio_out (&mut self, scope: &ProcessScope) { let channel_count = self.buffer.len(); self.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 //); self.buffer[channel % channel_count][index] += sample * self.output_gain; } } else { return false } } return true }); } /// Write output buffer to output ports. fn write_output_buffer (&mut self, scope: &ProcessScope) { for (i, port) in self.ports.audio_outs.values_mut().enumerate() { let buffer = &self.buffer[i]; for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() { *value = *buffer.get(i).unwrap_or(&0.0); } } } } impl Audio for Sampler { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { self.process_midi_in(scope); self.clear_output_buffer(); self.process_audio_out(scope); self.write_output_buffer(scope); Control::Continue } } impl Handle for Sampler { fn handle (&mut self, from: &TuiInput) -> Perhaps { match from.event() { key!(KeyCode::Up) => { self.cursor.0 = if self.cursor.0 == 0 { self.mapped.len() + self.unmapped.len() - 1 } else { self.cursor.0 - 1 }; Ok(Some(true)) }, key!(KeyCode::Down) => { self.cursor.0 = (self.cursor.0 + 1) % (self.mapped.len() + self.unmapped.len()); Ok(Some(true)) }, key!(KeyCode::Char('p')) => { if let Some(sample) = self.sample() { self.voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); } Ok(Some(true)) }, key!(KeyCode::Char('a')) => { let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?)); self.unmapped.push(sample); Ok(Some(true)) }, key!(KeyCode::Char('r')) => { if let Some(sample) = self.sample() { *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?)); } Ok(Some(true)) }, key!(KeyCode::Enter) => { if let Some(sample) = self.sample() { self.editing = Some(sample.clone()); } Ok(Some(true)) } _ => Ok(None) } } } impl Widget for Sampler { type Engine = Tui; fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { todo!() } fn render (&self, to: &mut TuiOutput) -> Usually<()> { tui_render_sampler(self, to) } } pub fn tui_render_sampler (sampler: &Sampler, to: &mut TuiOutput) -> Usually<()> { let [x, y, _, height] = to.area(); let style = Style::default().gray(); let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len()); to.blit(&title, x+1, y, Some(style.white().bold().not_dim())); let mut width = title.len() + 2; let mut y1 = 1; let mut j = 0; for (note, sample) in sampler.mapped.iter() .map(|(note, sample)|(Some(note), sample)) .chain(sampler.unmapped.iter().map(|sample|(None, sample))) { if y1 >= height { break } let active = j == sampler.cursor.0; width = width.max( draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)? ); y1 = y1 + 1; j = j + 1; } let height = ((2 + y1) as u16).min(height); //Ok(Some([x, y, (width as u16).min(to.area().w()), height])) 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) } /// A sound sample. #[derive(Default, Debug)] pub struct Sample { pub name: String, pub start: usize, pub end: usize, pub channels: Vec>, pub rate: Option, } impl Sample { pub fn from_edn <'e> (dir: &str, args: &[Edn<'e>]) -> Usually<(Option, Arc>)> { let mut name = String::new(); let mut file = String::new(); let mut midi = None; let mut start = 0usize; edn!(edn in args { Edn::Map(map) => { if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { name = String::from(*n); } if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { file = String::from(*f); } if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { start = *i as usize; } if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { midi = Some(u7::from(*m as u8)); } }, _ => panic!("unexpected in sample {name}"), }); let (end, data) = read_sample_data(&format!("{dir}/{file}"))?; Ok((midi, Arc::new(RwLock::new(Self::new(&name, start, end, data))))) } 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, } } } /// Load sample from WAV and assign to MIDI note. #[macro_export] macro_rules! sample { ($note:expr, $name:expr, $src:expr) => {{ let (end, data) = read_sample_data($src)?; ( u7::from_int_lossy($note).into(), Sample::new($name, 0, end, data).into() ) }}; } /// Read WAV from file pub fn read_sample_data (src: &str) -> Usually<(usize, Vec>)> { let mut channels: Vec> = vec![]; for channel in wavers::Wav::from_path(src)?.channels() { channels.push(channel); } let mut end = 0; let mut data: Vec> = vec![]; for samples in channels.iter() { let channel = Vec::from(samples.as_ref()); end = end.max(channel.len()); data.push(channel); } Ok((end, data)) } use std::fs::File; use symphonia::core::codecs::CODEC_TYPE_NULL; use symphonia::core::errors::Error; use symphonia::core::io::MediaSourceStream; use symphonia::core::probe::Hint; use symphonia::core::audio::SampleBuffer; use symphonia::default::get_codecs; pub struct AddSampleModal { exited: bool, dir: PathBuf, subdirs: Vec, files: Vec, cursor: usize, offset: usize, sample: Arc>, voices: Arc>>, _search: Option, } impl Exit for AddSampleModal { fn exited (&self) -> bool { self.exited } fn exit (&mut self) { self.exited = true } } impl Widget for AddSampleModal { type Engine = Tui; fn layout (&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)) } } 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) } } pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { [Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ modal.exit(); Ok(true) }], [Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ modal.prev(); Ok(true) }], [Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ modal.next(); Ok(true) }], [Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ if modal.pick()? { modal.exit(); } Ok(true) }], [Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ modal.try_preview()?; Ok(true) }] }); fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { let (mut subdirs, mut files) = read_dir(dir)? .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ let entry = entry.expect("failed to read drectory entry"); let meta = entry.metadata().expect("failed to read entry metadata"); if meta.is_file() { files.push(entry.file_name()); } else if meta.is_dir() { subdirs.push(entry.file_name()); } (subdirs, files) }); subdirs.sort(); files.sort(); Ok((subdirs, files)) } impl Sample { fn from_file (path: &PathBuf) -> Usually { let mut sample = Self::default(); sample.name = path.file_name().unwrap().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![]); } // 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() { sample.channels[chan].push(*frame) } } } }, 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) } }