From 532b648a9e8cdc33c1a8ba22b6e827ef1167fe60 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 11 Jan 2025 19:06:27 +0100 Subject: [PATCH] extract sampler crate --- Cargo.lock | 15 +- Cargo.toml | 1 + rust-jack | 2 +- sampler/Cargo.toml | 13 + sampler/src/lib.rs | 37 ++ sampler/src/sampler.rs | 720 +++++++++++++++++++++++++++++ sampler/src/sampler_tui.rs | 191 ++++++++ tek/Cargo.toml | 11 +- tek/src/arranger.rs | 13 +- tek/src/groovebox.rs | 2 +- tek/src/lib.rs | 3 +- tek/src/meter.rs | 26 -- tek/src/mixer.rs | 25 + tek/src/sampler.rs | 265 ----------- tek/src/sampler/sample.rs | 143 ------ tek/src/sampler/sample_import.rs | 242 ---------- tek/src/sampler/sample_list.rs | 50 -- tek/src/sampler/sample_viewer.rs | 68 --- tek/src/sampler/sampler_command.rs | 71 --- tek/src/sampler/sampler_status.rs | 9 - tek/src/sampler/sampler_tui.rs | 91 ---- tek/src/sampler/voice.rs | 30 -- 22 files changed, 1016 insertions(+), 1012 deletions(-) create mode 100644 sampler/Cargo.toml create mode 100644 sampler/src/lib.rs create mode 100644 sampler/src/sampler.rs create mode 100644 sampler/src/sampler_tui.rs delete mode 100644 tek/src/meter.rs delete mode 100644 tek/src/sampler.rs delete mode 100644 tek/src/sampler/sample.rs delete mode 100644 tek/src/sampler/sample_import.rs delete mode 100644 tek/src/sampler/sample_list.rs delete mode 100644 tek/src/sampler/sample_viewer.rs delete mode 100644 tek/src/sampler/sampler_command.rs delete mode 100644 tek/src/sampler/sampler_status.rs delete mode 100644 tek/src/sampler/sampler_tui.rs delete mode 100644 tek/src/sampler/voice.rs diff --git a/Cargo.lock b/Cargo.lock index 55d23eb8..f12bbd2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1417,13 +1417,12 @@ dependencies = [ "livi", "palette", "rand", - "symphonia", "tek_jack", "tek_midi", + "tek_sampler", "tek_time", "tek_tui", "toml", - "wavers", ] [[package]] @@ -1475,6 +1474,18 @@ dependencies = [ "tek_tui", ] +[[package]] +name = "tek_sampler" +version = "0.2.0" +dependencies = [ + "symphonia", + "tek_jack", + "tek_midi", + "tek_time", + "tek_tui", + "wavers", +] + [[package]] name = "tek_time" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 7c7fcf4c..fa0a504b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "./jack", "./midi", "./output", + "./sampler", "./tek", "./time", "./tui" diff --git a/rust-jack b/rust-jack index d0978895..a13c1c4d 160000 --- a/rust-jack +++ b/rust-jack @@ -1 +1 @@ -Subproject commit d09788959fe1cc937b27e8bfb2b695f84b406885 +Subproject commit a13c1c4d20343e574787a703eaeea7aeda63b084 diff --git a/sampler/Cargo.toml b/sampler/Cargo.toml new file mode 100644 index 00000000..bc3532b4 --- /dev/null +++ b/sampler/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tek_sampler" +edition = "2021" +version = "0.2.0" + +[dependencies] +tek_tui = { path = "../tui" } +tek_jack = { path = "../jack" } +tek_time = { path = "../time" } +tek_midi = { path = "../midi" } + +symphonia = { version = "0.5.4", features = [ "all" ] } +wavers = "1.4.3" diff --git a/sampler/src/lib.rs b/sampler/src/lib.rs new file mode 100644 index 00000000..68b5bf7f --- /dev/null +++ b/sampler/src/lib.rs @@ -0,0 +1,37 @@ +mod sampler; pub use self::sampler::*; +mod sampler_tui; pub use self::sampler_tui::*; + +pub(crate) use ::tek_tui::{ + *, + tek_output::*, + tek_input::*, + tek_edn::*, + ratatui::prelude::*, + crossterm::event::*, +}; +pub(crate) use ::tek_jack::{*, jack::*}; +pub(crate) use ::tek_midi::{*, midly::{*, live::*, num::*}}; + +pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}}; +pub(crate) use std::fs::File; +pub(crate) use std::ops::Deref; +pub(crate) use std::path::PathBuf; +pub(crate) use std::error::Error; +pub(crate) use std::ffi::OsString; +pub(crate) use KeyCode::Char; +pub(crate) use symphonia::{ + core::{ + formats::Packet, + codecs::{Decoder, CODEC_TYPE_NULL}, + errors::Error as SymphoniaError, + io::MediaSourceStream, + probe::Hint, + audio::SampleBuffer, + }, + default::get_codecs, +}; +pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Points, Line}}}; + +#[cfg(test)] #[test] fn test_sampler () { + // TODO! +} diff --git a/sampler/src/sampler.rs b/sampler/src/sampler.rs new file mode 100644 index 00000000..d8cf1241 --- /dev/null +++ b/sampler/src/sampler.rs @@ -0,0 +1,720 @@ +use crate::*; + +/// The sampler plugin plays sounds. +#[derive(Debug)] +pub struct Sampler { + pub jack: Arc>, + pub name: String, + pub mapped: [Option>>;128], + pub recording: Option<(usize, Arc>)>, + pub unmapped: Vec>>, + pub voices: Arc>>, + pub midi_in: JackPort, + pub audio_ins: Vec>, + pub input_meter: Vec, + pub audio_outs: Vec>, + pub buffer: Vec>, + pub output_gain: f32 +} +impl Sampler { + pub fn new ( + jack: &Arc>, + name: impl AsRef, + midi_from: &[PortConnection], + audio_from: &[&[PortConnection];2], + audio_to: &[&[PortConnection];2], + ) -> Usually { + let name = name.as_ref(); + Ok(Self { + midi_in: JackPort::::new(jack, format!("M/{name}"), midi_from)?, + audio_ins: vec![ + JackPort::::new(jack, &format!("L/{name}"), audio_from[0])?, + JackPort::::new(jack, &format!("R/{name}"), audio_from[1])?, + ], + input_meter: vec![0.0;2], + audio_outs: vec![ + JackPort::::new(jack, &format!("{name}/L"), audio_to[0])?, + JackPort::::new(jack, &format!("{name}/R"), audio_to[1])?, + ], + jack: jack.clone(), + name: name.into(), + mapped: [const { None };128], + unmapped: vec![], + voices: Arc::new(RwLock::new(vec![])), + buffer: vec![vec![0.0;16384];2], + output_gain: 1., + recording: None, + }) + } + pub fn cancel_recording (&mut self) { + self.recording = None; + } + pub fn begin_recording (&mut self, index: usize) { + self.recording = Some(( + index, + Arc::new(RwLock::new(Sample::new("Sample", 0, 0, vec![vec![];self.audio_ins.len()]))) + )); + } + pub fn finish_recording (&mut self) -> Option>> { + let recording = self.recording.take(); + if let Some((index, sample)) = recording { + let old = self.mapped[index].clone(); + self.mapped[index] = Some(sample); + old + } else { + None + } + } +} +audio!(|self: SamplerTui, client, scope|{ + SamplerAudio(&mut self.state).process(client, scope) +}); +pub struct SamplerAudio<'a>(pub &'a mut Sampler); +audio!(|self: SamplerAudio<'a>, _client, scope|{ + self.0.process_midi_in(scope); + self.0.clear_output_buffer(); + self.0.process_audio_out(scope); + self.0.write_output_buffer(scope); + self.0.process_audio_in(scope); + Control::Continue +}); +impl Sampler { + pub fn process_audio_in (&mut self, scope: &ProcessScope) { + let Sampler { audio_ins, input_meter, recording, .. } = self; + if audio_ins.len() != input_meter.len() { + *input_meter = vec![0.0;audio_ins.len()]; + } + if let Some((_, sample)) = recording { + let mut sample = sample.write().unwrap(); + if sample.channels.len() != audio_ins.len() { + panic!("channel count mismatch"); + } + let iterator = audio_ins.iter().zip(input_meter).zip(sample.channels.iter_mut()); + let mut length = 0; + for ((input, meter), channel) in iterator { + let slice = input.port.as_slice(scope); + length = length.max(slice.len()); + let total: f32 = slice.iter().map(|x|x.abs()).sum(); + let count = slice.len() as f32; + *meter = 10. * (total / count).log10(); + channel.extend_from_slice(slice); + } + sample.end += length; + } else { + for (input, meter) in audio_ins.iter().zip(input_meter) { + let slice = input.port.as_slice(scope); + let total: f32 = slice.iter().map(|x|x.abs()).sum(); + let count = slice.len() as f32; + *meter = 10. * (total / count).log10(); + } + } + } + /// 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.port.iter(scope) { + if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { + match message { + MidiMessage::NoteOn { ref key, ref vel } => { + if let Some(ref sample) = mapped[key.as_int() as usize] { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + }, + MidiMessage::Controller { controller, value } => { + // TODO + } + _ => {} + } + } + } + } + /// 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.port.as_mut_slice(scope).iter_mut().enumerate() { + *value = *buffer.get(i).unwrap_or(&0.0); + } + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +type MidiSample = (Option, Arc>); + +from_edn!("sampler" => |jack: &Arc>, args| -> crate::Sampler { + 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.first() { + Some(Edn::Symbol("sample")) => { + let (midi, sample) = MidiSample::from_edn((jack, &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(jack, &name) +}); + +from_edn!("sample" => |(_jack, dir): (&Arc>, &str), args| -> MidiSample { + 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) = Sample::read_data(&format!("{dir}/{file}"))?; + Ok((midi, Arc::new(RwLock::new(crate::Sample { + name, + start, + end, + channels: data, + rate: None, + gain: 1.0 + })))) +}); + +/// A sound sample. +#[derive(Default, Debug)] +pub struct Sample { + pub name: Arc, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: 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 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 } + } + 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(()) + } + 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 */ } + _ => {} + } + } +} + +/// 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 + } +} + +input_to_command!(FileBrowserCommand: |state:SamplerTui, input: Event|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 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 TuiOut, 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 Content for AddSampleModal { + fn render (&self, to: &mut TuiOut) { + 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: &TuiIn) -> 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) + //}] +//}); + + +handle!(TuiIn: |self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input.event())); + +pub enum SamplerTuiCommand { + Import(FileBrowserCommand), + Select(usize), + Sample(SamplerCommand), +} + +pub enum SamplerCommand { + RecordBegin(u7), + RecordCancel, + RecordFinish, + SetSample(u7, Option>>), + SetStart(u7, usize), + SetGain(f32), + NoteOn(u7, u7), + NoteOff(u7), +} + +input_to_command!(SamplerTuiCommand: |state: SamplerTui, input: Event|match state.mode{ + Some(SamplerMode::Import(..)) => Self::Import( + FileBrowserCommand::input_to_command(state, input)? + ), + _ => match input { + // load sample + kpat!(Shift-Char('L')) => Self::Import(FileBrowserCommand::Begin), + kpat!(KeyCode::Up) => Self::Select(state.note_point().overflowing_add(1).0.min(127)), + kpat!(KeyCode::Down) => Self::Select(state.note_point().overflowing_sub(1).0.min(127)), + _ => return None + } +}); + +command!(|self: SamplerTuiCommand, 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::Select(index) => { + let old = state.note_point(); + state.set_note_point(index); + Some(Self::Select(old)) + }, + Self::Sample(cmd) => cmd.execute(&mut state.state)?.map(Self::Sample), + _ => todo!() +}); + +command!(|self: SamplerCommand, state: Sampler|match self { + Self::SetSample(index, sample) => { + let i = index.as_int() as usize; + let old = state.mapped[i].clone(); + state.mapped[i] = sample; + Some(Self::SetSample(index, old)) + }, + Self::RecordBegin(index) => { + state.begin_recording(index.as_int() as usize); + None + }, + Self::RecordCancel => { + state.cancel_recording(); + None + }, + Self::RecordFinish => { + state.finish_recording(); + None + }, + _ => todo!() +}); + +pub enum SamplerMode { + // Load sample from path + Import(usize, FileBrowser), +} diff --git a/sampler/src/sampler_tui.rs b/sampler/src/sampler_tui.rs new file mode 100644 index 00000000..ca569c70 --- /dev/null +++ b/sampler/src/sampler_tui.rs @@ -0,0 +1,191 @@ +use crate::*; +pub struct SamplerTui { + pub state: Sampler, + pub cursor: (usize, usize), + pub editing: Option>>, + pub mode: Option, + /// Size of actual notes area + pub size: Measure, + /// Lowest note displayed + pub note_lo: AtomicUsize, + pub note_pt: AtomicUsize, + pub color: ItemPalette +} +impl SamplerTui { + /// Immutable reference to sample at cursor. + pub fn sample (&self) -> Option<&Arc>> { + for (i, sample) in self.state.mapped.iter().enumerate() { + if i == self.cursor.0 { + return sample.as_ref() + } + } + for (i, sample) in self.state.unmapped.iter().enumerate() { + if i + self.state.mapped.len() == self.cursor.0 { + return Some(sample) + } + } + None + } +} +render!(TuiOut: (self: SamplerTui) => { + let keys_width = 5; + let keys = move||"";//SamplerKeys(self); + let fg = self.color.base.rgb; + let bg = self.color.darkest.rgb; + let border = Fill::xy(Outer(Style::default().fg(fg).bg(bg))); + let with_border = |x|lay!(border, Fill::xy(x)); + let with_size = |x|lay!(self.size.clone(), x); + Tui::bg(bg, Fill::xy(with_border(Bsp::s( + Tui::fg(self.color.light.rgb, Tui::bold(true, &"Sampler")), + with_size(Shrink::y(1, Bsp::e( + Fixed::x(keys_width, keys()), + Fill::xy(SamplesTui { + color: self.color, + note_hi: self.note_hi(), + note_pt: self.note_point(), + height: self.size.h(), + }), + ))), + )))) +}); +struct SamplesTui { + color: ItemPalette, + note_hi: usize, + note_pt: usize, + height: usize, +} +render!(TuiOut: |self: SamplesTui, render|{ + let x = render.area.x(); + let bg_base = self.color.darkest.rgb; + let bg_selected = self.color.darker.rgb; + let style_empty = Style::default().fg(self.color.base.rgb); + let style_full = Style::default().fg(self.color.lighter.rgb); + for y in 0..self.height { + let note = self.note_hi - y as usize; + let bg = if note == self.note_pt { bg_selected } else { bg_base }; + let style = Some(style_empty.bg(bg)); + render.blit(&" (no sample) ", x, render.area.y() + y as u16, style); + } +}); +impl NoteRange for SamplerTui { + fn note_lo (&self) -> &AtomicUsize { &self.note_lo } + fn note_axis (&self) -> &AtomicUsize { &self.size.y } +} +impl NotePoint for SamplerTui { + fn note_len (&self) -> usize {0/*TODO*/} + fn set_note_len (&self, x: usize) {} + fn note_point (&self) -> usize { self.note_pt.load(Relaxed) } + fn set_note_point (&self, x: usize) { self.note_pt.store(x, Relaxed); } +} +pub struct SampleList<'a> { + compact: bool, + sampler: &'a Sampler, + editor: &'a MidiEditor +} +impl<'a> SampleList<'a> { + pub fn new (compact: bool, sampler: &'a Sampler, editor: &'a MidiEditor) -> Self { + Self { compact, sampler, editor } + } +} +render!(TuiOut: (self: SampleList<'a>) => { + let Self { compact, sampler, editor } = self; + let note_lo = editor.note_lo().load(Relaxed); + let note_pt = editor.note_point(); + let note_hi = editor.note_hi(); + Outer(Style::default().fg(TuiTheme::g(96))).enclose(Map::new(move||(note_lo..=note_hi).rev(), move|note, i| { + let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); + let mut bg = if note == note_pt { TuiTheme::g(64) } else { Color::Reset }; + let mut fg = TuiTheme::g(160); + if sampler.mapped[note].is_some() { + fg = TuiTheme::g(224); + bg = Color::Rgb(0, if note == note_pt { 96 } else { 64 }, 0); + } + if let Some((index, _)) = sampler.recording { + if note == index { + bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; + fg = Color::Rgb(224,64,32) + } + } + let label = if *compact { + String::default() + } else if let Some(sample) = &sampler.mapped[note] { + let sample = sample.read().unwrap(); + format!("{:8} {:3} {:6}-{:6}/{:6}", + sample.name, + sample.gain, + sample.start, + sample.end, + sample.channels[0].len() + ) + } else { + String::from("(none)") + }; + offset(Tui::fg_bg(fg, bg, format!("{note:3} {}", label))) + })) +}); +impl Sampler { + pub fn viewer (&self, note_pt: usize) -> impl Content { + if let Some((_, sample)) = &self.recording { + SampleViewer(Some(sample.clone())) + } else if let Some(sample) = &self.mapped[note_pt] { + SampleViewer(Some(sample.clone())) + } else { + SampleViewer(None) + } + } +} +const EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; +pub struct SampleViewer(pub Option>>); +render!(TuiOut: |self: SampleViewer, to|{ + let [x, y, width, height] = to.area(); + let area = Rect { x, y, width, height }; + let min_db = -40.0; + let (x_bounds, y_bounds, lines): ([f64;2], [f64;2], Vec) = + if let Some(sample) = &self.0 { + let sample = sample.read().unwrap(); + let start = sample.start as f64; + let end = sample.end as f64; + let length = end - start; + let step = length / width as f64; + let mut t = start; + let mut lines = vec![]; + while t < end { + let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; + let total: f32 = chunk.iter().map(|x|x.abs()).sum(); + let count = chunk.len() as f32; + let meter = 10. * (total / count).log10(); + let x = t as f64; + let y = meter as f64; + lines.push(Line::new(x, min_db, x, y, Color::Green)); + t += step / 2.; + } + ( + [sample.start as f64, sample.end as f64], + [min_db, 0.], + lines + ) + } else { + ( + [0.0, width as f64], + [0.0, height as f64], + vec![ + Line::new(0.0, 0.0, width as f64, height as f64, Color::Red), + Line::new(width as f64, 0.0, 0.0, height as f64, Color::Red), + ] + ) + }; + + Canvas::default() + .x_bounds(x_bounds) + .y_bounds(y_bounds) + .paint(|ctx| { for line in lines.iter() { ctx.draw(line) } }) + .render(area, &mut to.buffer); +}); + +pub struct SamplerStatus<'a>(pub &'a Sampler, pub usize); + +render!(TuiOut: (self: SamplerStatus<'a>) => Tui::bold(true, Tui::fg(TuiTheme::g(224), self.0.mapped[self.1].as_ref().map(|sample|format!( + "Sample {}-{}", + sample.read().unwrap().start, + sample.read().unwrap().end, +)).unwrap_or_else(||"No sample".to_string())))); diff --git a/tek/Cargo.toml b/tek/Cargo.toml index 3c593ae8..8dc7134b 100644 --- a/tek/Cargo.toml +++ b/tek/Cargo.toml @@ -4,18 +4,17 @@ edition = "2021" version = "0.2.0" [dependencies] -tek_tui = { path = "../tui" } -tek_jack = { path = "../jack" } -tek_time = { path = "../time" } -tek_midi = { path = "../midi" } +tek_tui = { path = "../tui" } +tek_jack = { path = "../jack" } +tek_time = { path = "../time" } +tek_midi = { path = "../midi" } +tek_sampler = { path = "../sampler" } backtrace = "0.3.72" livi = "0.7.4" palette = { version = "0.7.6", features = [ "random" ] } rand = "0.8.5" -symphonia = { version = "0.5.4", features = [ "all" ] } toml = "0.8.12" -wavers = "1.4.3" #once_cell = "1.19.0" #no_deadlocks = "1.3.2" #suil-rs = { path = "../suil" } diff --git a/tek/src/arranger.rs b/tek/src/arranger.rs index 5c3406bc..a5adfe07 100644 --- a/tek/src/arranger.rs +++ b/tek/src/arranger.rs @@ -212,12 +212,15 @@ impl Arranger { fn track_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { let iter = ||self.tracks_with_sizes(); (move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { - let name = Push::x(1, &track.name); - let color = track.color(); - let fg = color.lightest.rgb; - let bg = color.base.rgb; + let name = Push::x(1, &track.name); + let color = track.color(); + let fg = color.lightest.rgb; + let bg = color.base.rgb; + let active = self.selected.track() == Some(i); + let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) }; + let border = Style::default().fg(bfg).bg(bg); Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16, - Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name)))) + Outer(border).enclose(Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name))))) )) })).boxed()).into() } diff --git a/tek/src/groovebox.rs b/tek/src/groovebox.rs index 48a0a6a9..ca424941 100644 --- a/tek/src/groovebox.rs +++ b/tek/src/groovebox.rs @@ -81,7 +81,7 @@ impl Groovebox { Max::y(sample_h, Fill::xy( Bsp::a( Fill::x(Align::w(Fixed::y(1, SamplerStatus(&self.sampler, note_pt)))), - SampleViewer::from_sampler(&self.sampler, note_pt)))) + self.sampler.viewer(note_pt)))) } fn pool_view (&self) -> impl Content + use<'_> { let w = self.size.w(); diff --git a/tek/src/lib.rs b/tek/src/lib.rs index afded3f9..1c04d4f9 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -12,16 +12,15 @@ pub type Perhaps = std::result::Result, Box>; pub mod arranger; pub use self::arranger::*; pub mod groovebox; pub use self::groovebox::*; -pub mod meter; pub use self::meter::*; pub mod mixer; pub use self::mixer::*; pub mod plugin; pub use self::plugin::*; pub mod pool; pub use self::pool::*; -pub mod sampler; pub use self::sampler::*; pub mod sequencer; pub use self::sequencer::*; pub use ::tek_time; pub use ::tek_time::*; pub use ::tek_jack; pub use ::tek_jack::{*, jack::{*, contrib::*}}; pub use ::tek_midi; pub use ::tek_midi::{*, midly::{*, num::*, live::*}}; +pub use ::tek_sampler::{self, *}; pub use ::tek_tui::{ *, tek_edn::*, diff --git a/tek/src/meter.rs b/tek/src/meter.rs deleted file mode 100644 index 70fc27bd..00000000 --- a/tek/src/meter.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::*; - -pub struct Meter<'a>(pub &'a str, pub f32); -render!(TuiOut: (self: Meter<'a>) => col!( - Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)), - Fixed::xy(if self.1 >= 0.0 { 13 } - else if self.1 >= -1.0 { 12 } - else if self.1 >= -2.0 { 11 } - else if self.1 >= -3.0 { 10 } - else if self.1 >= -4.0 { 9 } - else if self.1 >= -6.0 { 8 } - else if self.1 >= -9.0 { 7 } - else if self.1 >= -12.0 { 6 } - else if self.1 >= -15.0 { 5 } - else if self.1 >= -20.0 { 4 } - else if self.1 >= -25.0 { 3 } - else if self.1 >= -30.0 { 2 } - else if self.1 >= -40.0 { 1 } - else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red } - else if self.1 >= -3.0 { Color::Yellow } - else { Color::Green }, ())))); - -pub struct Meters<'a>(pub &'a[f32]); -render!(TuiOut: (self: Meters<'a>) => col!( - format!("L/{:>+9.3}", self.0[0]), - format!("R/{:>+9.3}", self.0[1]))); diff --git a/tek/src/mixer.rs b/tek/src/mixer.rs index bae4b4a2..e86b5b80 100644 --- a/tek/src/mixer.rs +++ b/tek/src/mixer.rs @@ -1,5 +1,30 @@ use crate::*; +pub struct Meter<'a>(pub &'a str, pub f32); +render!(TuiOut: (self: Meter<'a>) => col!( + Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)), + Fixed::xy(if self.1 >= 0.0 { 13 } + else if self.1 >= -1.0 { 12 } + else if self.1 >= -2.0 { 11 } + else if self.1 >= -3.0 { 10 } + else if self.1 >= -4.0 { 9 } + else if self.1 >= -6.0 { 8 } + else if self.1 >= -9.0 { 7 } + else if self.1 >= -12.0 { 6 } + else if self.1 >= -15.0 { 5 } + else if self.1 >= -20.0 { 4 } + else if self.1 >= -25.0 { 3 } + else if self.1 >= -30.0 { 2 } + else if self.1 >= -40.0 { 1 } + else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red } + else if self.1 >= -3.0 { Color::Yellow } + else { Color::Green }, ())))); + +pub struct Meters<'a>(pub &'a[f32]); +render!(TuiOut: (self: Meters<'a>) => col!( + format!("L/{:>+9.3}", self.0[0]), + format!("R/{:>+9.3}", self.0[1]))); + #[derive(Debug)] pub struct Mixer { /// JACK client handle (needs to not be dropped for standalone mode to work). diff --git a/tek/src/sampler.rs b/tek/src/sampler.rs deleted file mode 100644 index f4fa229a..00000000 --- a/tek/src/sampler.rs +++ /dev/null @@ -1,265 +0,0 @@ -mod sample; pub use self::sample::*; -mod sample_import; pub use self::sample_import::*; -mod sample_list; pub use self::sample_list::*; -mod sample_viewer; pub use self::sample_viewer::*; -mod sampler_command; pub use self::sampler_command::*; -mod sampler_status; pub use self::sampler_status::*; -mod sampler_tui; pub use self::sampler_tui::*; -mod voice; pub use self::voice::*; -use crate::*; -use KeyCode::Char; -use std::fs::File; -use symphonia::{ - core::{ - formats::Packet, - codecs::{Decoder, CODEC_TYPE_NULL}, - errors::Error, - io::MediaSourceStream, - probe::Hint, - audio::SampleBuffer, - }, - default::get_codecs, -}; - -/// The sampler plugin plays sounds. -#[derive(Debug)] -pub struct Sampler { - pub jack: Arc>, - pub name: String, - pub mapped: [Option>>;128], - pub recording: Option<(usize, Arc>)>, - pub unmapped: Vec>>, - pub voices: Arc>>, - pub midi_in: JackPort, - pub audio_ins: Vec>, - pub input_meter: Vec, - pub audio_outs: Vec>, - pub buffer: Vec>, - pub output_gain: f32 -} -impl Sampler { - pub fn new ( - jack: &Arc>, - name: impl AsRef, - midi_from: &[PortConnection], - audio_from: &[&[PortConnection];2], - audio_to: &[&[PortConnection];2], - ) -> Usually { - let name = name.as_ref(); - Ok(Self { - midi_in: JackPort::::new(jack, format!("M/{name}"), midi_from)?, - audio_ins: vec![ - JackPort::::new(jack, &format!("L/{name}"), audio_from[0])?, - JackPort::::new(jack, &format!("R/{name}"), audio_from[1])?, - ], - input_meter: vec![0.0;2], - audio_outs: vec![ - JackPort::::new(jack, &format!("{name}/L"), audio_to[0])?, - JackPort::::new(jack, &format!("{name}/R"), audio_to[1])?, - ], - jack: jack.clone(), - name: name.into(), - mapped: [const { None };128], - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - buffer: vec![vec![0.0;16384];2], - output_gain: 1., - recording: None, - }) - } - pub fn cancel_recording (&mut self) { - self.recording = None; - } - pub fn begin_recording (&mut self, index: usize) { - self.recording = Some(( - index, - Arc::new(RwLock::new(Sample::new("Sample", 0, 0, vec![vec![];self.audio_ins.len()]))) - )); - } - pub fn finish_recording (&mut self) -> Option>> { - let recording = self.recording.take(); - if let Some((index, sample)) = recording { - let old = self.mapped[index].clone(); - self.mapped[index] = Some(sample); - old - } else { - None - } - } -} - -audio!(|self: SamplerTui, client, scope|{ - SamplerAudio(&mut self.state).process(client, scope) -}); - -pub struct SamplerAudio<'a>(pub &'a mut Sampler); - -audio!(|self: SamplerAudio<'a>, _client, scope|{ - self.0.process_midi_in(scope); - self.0.clear_output_buffer(); - self.0.process_audio_out(scope); - self.0.write_output_buffer(scope); - self.0.process_audio_in(scope); - Control::Continue -}); - -impl Sampler { - - pub fn process_audio_in (&mut self, scope: &ProcessScope) { - let Sampler { audio_ins, input_meter, recording, .. } = self; - if audio_ins.len() != input_meter.len() { - *input_meter = vec![0.0;audio_ins.len()]; - } - if let Some((_, sample)) = recording { - let mut sample = sample.write().unwrap(); - if sample.channels.len() != audio_ins.len() { - panic!("channel count mismatch"); - } - let iterator = audio_ins.iter().zip(input_meter).zip(sample.channels.iter_mut()); - let mut length = 0; - for ((input, meter), channel) in iterator { - let slice = input.port.as_slice(scope); - length = length.max(slice.len()); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); - channel.extend_from_slice(slice); - } - sample.end += length; - } else { - for (input, meter) in audio_ins.iter().zip(input_meter) { - let slice = input.port.as_slice(scope); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); - } - } - } - - /// 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.port.iter(scope) { - if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { - match message { - MidiMessage::NoteOn { ref key, ref vel } => { - if let Some(ref sample) = mapped[key.as_int() as usize] { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); - } - }, - MidiMessage::Controller { controller, value } => { - // TODO - } - _ => {} - } - } - } - } - - /// 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.port.as_mut_slice(scope).iter_mut().enumerate() { - *value = *buffer.get(i).unwrap_or(&0.0); - } - } - } - -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -type MidiSample = (Option, Arc>); - -from_edn!("sampler" => |jack: &Arc>, args| -> crate::Sampler { - 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.first() { - Some(Edn::Symbol("sample")) => { - let (midi, sample) = MidiSample::from_edn((jack, &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(jack, &name) -}); - -from_edn!("sample" => |(_jack, dir): (&Arc>, &str), args| -> MidiSample { - 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) = Sample::read_data(&format!("{dir}/{file}"))?; - Ok((midi, Arc::new(RwLock::new(crate::Sample { - name, - start, - end, - channels: data, - rate: None, - gain: 1.0 - })))) -}); diff --git a/tek/src/sampler/sample.rs b/tek/src/sampler/sample.rs deleted file mode 100644 index c0b9103e..00000000 --- a/tek/src/sampler/sample.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::*; -use super::*; - -/// A sound sample. -#[derive(Default, Debug)] -pub struct Sample { - pub name: Arc, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: 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 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 } - } - 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(()) - } - 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 */ } - _ => {} - } - } -} diff --git a/tek/src/sampler/sample_import.rs b/tek/src/sampler/sample_import.rs deleted file mode 100644 index 6ddc5ef9..00000000 --- a/tek/src/sampler/sample_import.rs +++ /dev/null @@ -1,242 +0,0 @@ -use crate::*; -use super::*; - -input_to_command!(FileBrowserCommand: |state:SamplerTui, input: Event|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 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 TuiOut, 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 Content for AddSampleModal { - fn render (&self, to: &mut TuiOut) { - 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: &TuiIn) -> 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/tek/src/sampler/sample_list.rs b/tek/src/sampler/sample_list.rs deleted file mode 100644 index 51227409..00000000 --- a/tek/src/sampler/sample_list.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::*; - -pub struct SampleList<'a> { - compact: bool, - sampler: &'a Sampler, - editor: &'a MidiEditor -} - -impl<'a> SampleList<'a> { - pub fn new (compact: bool, sampler: &'a Sampler, editor: &'a MidiEditor) -> Self { - Self { compact, sampler, editor } - } -} - -render!(TuiOut: (self: SampleList<'a>) => { - let Self { compact, sampler, editor } = self; - let note_lo = editor.note_lo().load(Relaxed); - let note_pt = editor.note_point(); - let note_hi = editor.note_hi(); - Outer(Style::default().fg(TuiTheme::g(96))).enclose(Map::new(move||(note_lo..=note_hi).rev(), move|note, i| { - let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); - let mut bg = if note == note_pt { TuiTheme::g(64) } else { Color::Reset }; - let mut fg = TuiTheme::g(160); - if sampler.mapped[note].is_some() { - fg = TuiTheme::g(224); - bg = Color::Rgb(0, if note == note_pt { 96 } else { 64 }, 0); - } - if let Some((index, _)) = sampler.recording { - if note == index { - bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; - fg = Color::Rgb(224,64,32) - } - } - let label = if *compact { - String::default() - } else if let Some(sample) = &sampler.mapped[note] { - let sample = sample.read().unwrap(); - format!("{:8} {:3} {:6}-{:6}/{:6}", - sample.name, - sample.gain, - sample.start, - sample.end, - sample.channels[0].len() - ) - } else { - String::from("(none)") - }; - offset(Tui::fg_bg(fg, bg, format!("{note:3} {}", label))) - })) -}); diff --git a/tek/src/sampler/sample_viewer.rs b/tek/src/sampler/sample_viewer.rs deleted file mode 100644 index d4d82dcd..00000000 --- a/tek/src/sampler/sample_viewer.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::*; -use std::ops::Deref; -use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Points, Line}}}; - -const EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; - -pub struct SampleViewer(pub Option>>); - -impl SampleViewer { - pub fn from_sampler (sampler: &Sampler, note_pt: usize) -> Self { - if let Some((_, sample)) = &sampler.recording { - SampleViewer(Some(sample.clone())) - } else if let Some(sample) = &sampler.mapped[note_pt] { - SampleViewer(Some(sample.clone())) - } else { - SampleViewer(None) - } - } -} - -render!(TuiOut: |self: SampleViewer, to|{ - - let [x, y, width, height] = to.area(); - - let area = Rect { x, y, width, height }; - let min_db = -40.0; - - let (x_bounds, y_bounds, lines): ([f64;2], [f64;2], Vec) = - if let Some(sample) = &self.0 { - let sample = sample.read().unwrap(); - let start = sample.start as f64; - let end = sample.end as f64; - let length = end - start; - let step = length / width as f64; - let mut t = start; - let mut lines = vec![]; - while t < end { - let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; - let total: f32 = chunk.iter().map(|x|x.abs()).sum(); - let count = chunk.len() as f32; - let meter = 10. * (total / count).log10(); - let x = t as f64; - let y = meter as f64; - lines.push(Line::new(x, min_db, x, y, Color::Green)); - t += step / 2.; - } - ( - [sample.start as f64, sample.end as f64], - [min_db, 0.], - lines - ) - } else { - ( - [0.0, width as f64], - [0.0, height as f64], - vec![ - Line::new(0.0, 0.0, width as f64, height as f64, Color::Red), - Line::new(width as f64, 0.0, 0.0, height as f64, Color::Red), - ] - ) - }; - - Canvas::default() - .x_bounds(x_bounds) - .y_bounds(y_bounds) - .paint(|ctx| { for line in lines.iter() { ctx.draw(line) } }) - .render(area, &mut to.buffer); -}); diff --git a/tek/src/sampler/sampler_command.rs b/tek/src/sampler/sampler_command.rs deleted file mode 100644 index 5bd81f0f..00000000 --- a/tek/src/sampler/sampler_command.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::*; - -handle!(TuiIn: |self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input.event())); - -pub enum SamplerTuiCommand { - Import(FileBrowserCommand), - Select(usize), - Sample(SamplerCommand), -} - -pub enum SamplerCommand { - RecordBegin(u7), - RecordCancel, - RecordFinish, - SetSample(u7, Option>>), - SetStart(u7, usize), - SetGain(f32), - NoteOn(u7, u7), - NoteOff(u7), -} - -input_to_command!(SamplerTuiCommand: |state: SamplerTui, input: Event|match state.mode{ - Some(SamplerMode::Import(..)) => Self::Import( - FileBrowserCommand::input_to_command(state, input)? - ), - _ => match input { - // load sample - kpat!(Shift-Char('L')) => Self::Import(FileBrowserCommand::Begin), - kpat!(KeyCode::Up) => Self::Select(state.note_point().overflowing_add(1).0.min(127)), - kpat!(KeyCode::Down) => Self::Select(state.note_point().overflowing_sub(1).0.min(127)), - _ => return None - } -}); - -command!(|self: SamplerTuiCommand, 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::Select(index) => { - let old = state.note_point(); - state.set_note_point(index); - Some(Self::Select(old)) - }, - Self::Sample(cmd) => cmd.execute(&mut state.state)?.map(Self::Sample), - _ => todo!() -}); - -command!(|self: SamplerCommand, state: Sampler|match self { - Self::SetSample(index, sample) => { - let i = index.as_int() as usize; - let old = state.mapped[i].clone(); - state.mapped[i] = sample; - Some(Self::SetSample(index, old)) - }, - Self::RecordBegin(index) => { - state.begin_recording(index.as_int() as usize); - None - }, - Self::RecordCancel => { - state.cancel_recording(); - None - }, - Self::RecordFinish => { - state.finish_recording(); - None - }, - _ => todo!() -}); diff --git a/tek/src/sampler/sampler_status.rs b/tek/src/sampler/sampler_status.rs deleted file mode 100644 index 051ae6f5..00000000 --- a/tek/src/sampler/sampler_status.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::*; - -pub struct SamplerStatus<'a>(pub &'a Sampler, pub usize); - -render!(TuiOut: (self: SamplerStatus<'a>) => Tui::bold(true, Tui::fg(TuiTheme::g(224), self.0.mapped[self.1].as_ref().map(|sample|format!( - "Sample {}-{}", - sample.read().unwrap().start, - sample.read().unwrap().end, -)).unwrap_or_else(||"No sample".to_string())))); diff --git a/tek/src/sampler/sampler_tui.rs b/tek/src/sampler/sampler_tui.rs deleted file mode 100644 index c343b910..00000000 --- a/tek/src/sampler/sampler_tui.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::*; -use KeyCode::Char; - -pub struct SamplerTui { - pub state: Sampler, - pub cursor: (usize, usize), - pub editing: Option>>, - pub mode: Option, - /// Size of actual notes area - pub size: Measure, - /// Lowest note displayed - pub note_lo: AtomicUsize, - pub note_pt: AtomicUsize, - pub color: ItemPalette -} - -impl SamplerTui { - /// Immutable reference to sample at cursor. - pub fn sample (&self) -> Option<&Arc>> { - for (i, sample) in self.state.mapped.iter().enumerate() { - if i == self.cursor.0 { - return sample.as_ref() - } - } - for (i, sample) in self.state.unmapped.iter().enumerate() { - if i + self.state.mapped.len() == self.cursor.0 { - return Some(sample) - } - } - None - } -} - -render!(TuiOut: (self: SamplerTui) => { - let keys_width = 5; - let keys = move||"";//SamplerKeys(self); - let fg = self.color.base.rgb; - let bg = self.color.darkest.rgb; - let border = Fill::xy(Outer(Style::default().fg(fg).bg(bg))); - let with_border = |x|lay!(border, Fill::xy(x)); - let with_size = |x|lay!(self.size.clone(), x); - Tui::bg(bg, Fill::xy(with_border(Bsp::s( - Tui::fg(self.color.light.rgb, Tui::bold(true, &"Sampler")), - with_size(Shrink::y(1, Bsp::e( - Fixed::x(keys_width, keys()), - Fill::xy(SamplesTui { - color: self.color, - note_hi: self.note_hi(), - note_pt: self.note_point(), - height: self.size.h(), - }), - ))), - )))) -}); - -struct SamplesTui { - color: ItemPalette, - note_hi: usize, - note_pt: usize, - height: usize, -} -render!(TuiOut: |self: SamplesTui, render|{ - let x = render.area.x(); - let bg_base = self.color.darkest.rgb; - let bg_selected = self.color.darker.rgb; - let style_empty = Style::default().fg(self.color.base.rgb); - let style_full = Style::default().fg(self.color.lighter.rgb); - for y in 0..self.height { - let note = self.note_hi - y as usize; - let bg = if note == self.note_pt { bg_selected } else { bg_base }; - let style = Some(style_empty.bg(bg)); - render.blit(&" (no sample) ", x, render.area.y() + y as u16, style); - } -}); - -impl NoteRange for SamplerTui { - fn note_lo (&self) -> &AtomicUsize { &self.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.size.y } -} - -impl NotePoint for SamplerTui { - fn note_len (&self) -> usize {0/*TODO*/} - fn set_note_len (&self, x: usize) {} - fn note_point (&self) -> usize { self.note_pt.load(Relaxed) } - fn set_note_point (&self, x: usize) { self.note_pt.store(x, Relaxed); } -} - -pub enum SamplerMode { - // Load sample from path - Import(usize, FileBrowser), -} diff --git a/tek/src/sampler/voice.rs b/tek/src/sampler/voice.rs deleted file mode 100644 index be4d8918..00000000 --- a/tek/src/sampler/voice.rs +++ /dev/null @@ -1,30 +0,0 @@ -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 - } -}