use crate::*; pub trait HasSampler { fn sampler (&self) -> &Option; fn sampler_mut (&mut self) -> &mut Option; fn sample_index (&self) -> usize; fn view_sample <'a> (&'a self, compact: bool) -> impl Content + 'a { self.sampler().as_ref().map(|sampler|Max::y( if compact { 0u16 } else { 5 }.into(), Fill::x(sampler.viewer(self.sample_index())) )) } fn view_sampler <'a> (&'a self, compact: bool, editor: &Option) -> impl Content + 'a { self.sampler().as_ref().map(|sampler|Fixed::x( if compact { 4u16 } else { 40 }.into(), Push::y( if compact { 1u16 } else { 0 }.into(), editor.as_ref().map(|e|Fill::y(sampler.list(compact, e))) ) )) } } #[macro_export] macro_rules! has_sampler { (|$self:ident:$Struct:ty| { sampler = $e0:expr; index = $e1:expr; }) => { impl HasSampler for $Struct { fn sampler (&$self) -> &Option { &$e0 } fn sampler_mut (&mut $self) -> &mut Option { &mut $e0 } fn sample_index (&$self) -> usize { $e1 } } } } /// The sampler device plays sounds in response to MIDI notes. #[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: Option>, pub audio_ins: Vec>, pub input_meter: Vec, pub audio_outs: Vec>, pub buffer: Vec>, pub output_gain: f32 } /// 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() ) }}; } /// 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 Default for Sampler { fn default () -> Self { Self { midi_in: None, audio_ins: vec![], input_meter: vec![0.0;2], audio_outs: vec![], jack: Default::default(), name: "tek_sampler".to_string(), mapped: [const { None };128], unmapped: vec![], voices: Arc::new(RwLock::new(vec![])), buffer: vec![vec![0.0;16384];2], output_gain: 1., recording: None, } } } 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: Some(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])?, ], audio_outs: vec![ JackPort::::new(jack, &format!("{name}/L"), audio_to[0])?, JackPort::::new(jack, &format!("{name}/R"), audio_to[1])?, ], ..Default::default() }) } 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 } } } 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 */ } _ => {} } } } 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; if let Some(ref midi_in) = midi_in { 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_atom!("sampler" => |jack: &Arc>, args| -> crate::Sampler { //let mut name = String::new(); //let mut dir = String::new(); //let mut samples = BTreeMap::new(); //atom!(atom in args { //Atom::Map(map) => { //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { //name = String::from(*n); //} //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":dir")) { //dir = String::from(*n); //} //}, //Atom::List(args) => match args.first() { //Some(Atom::Symbol("sample")) => { //let (midi, sample) = MidiSample::from_atom((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}: {atom:?}") //}); //Self::new(jack, &name) //}); //from_atom!("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; //atom!(atom in args { //Atom::Map(map) => { //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { //name = String::from(*n); //} //if let Some(Atom::Str(f)) = map.get(&Atom::Key(":file")) { //file = String::from(*f); //} //if let Some(Atom::Int(i)) = map.get(&Atom::Key(":start")) { //start = *i as usize; //} //if let Some(Atom::Int(m)) = map.get(&Atom::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 //})))) //}); 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 } } 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) //}] //}); pub enum SamplerMode { // Load sample from path Import(usize, FileBrowser), } //handle!(TuiIn: |self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input.event())); #[derive(Clone, Debug)] pub enum SamplerTuiCommand { Import(FileBrowserCommand), Select(usize), Sample(SamplerCommand), } atom_command!(SamplerTuiCommand: |state: SamplerTui| { ("select" [i: usize] Self::Select(i.expect("no index"))) ("import" [,..a] if let Some(command) = FileBrowserCommand::try_from_expr(state, a) { Self::Import(command) } else { return None }) ("sample" [,..a] if let Some(command) = SamplerCommand::try_from_expr(&state.state, a) { Self::Sample(command) } else { return None }) }); provide!(usize: |self: SamplerTui| {}); provide!(PathBuf: |self: SamplerTui| {}); provide!(Arc: |self: SamplerTui| {}); atom_command!(FileBrowserCommand: |state: SamplerTui| { ("begin" [] Self::Begin) ("cancel" [] Self::Cancel) ("confirm" [] Self::Confirm) ("select" [i: usize] Self::Select(i.expect("no index"))) ("chdir" [p: PathBuf] Self::Chdir(p.expect("no path"))) ("filter" [f: Arc] Self::Filter(f.expect("no filter"))) }); #[derive(Clone, Debug)] pub enum SamplerCommand { RecordBegin(u7), RecordCancel, RecordFinish, SetSample(u7, Option>>), SetStart(u7, usize), SetGain(u7, f32), NoteOn(u7, u7), NoteOff(u7), } atom_command!(SamplerCommand: |state: Sampler| { ("record/begin" [i: u7] Self::RecordBegin(i.expect("no index"))) ("record/cancel" [] Self::RecordCancel) ("record/finish" [] Self::RecordFinish) ("set/sample" [i: u7, s: Option>>] Self::SetSample(i.expect("no index"), s.expect("no sampler"))) ("set/start" [i: u7, s: usize] Self::SetStart(i.expect("no index"), s.expect("no start"))) ("set/gain" [i: u7, g: f32] Self::SetGain(i.expect("no index"), g.expect("no garin"))) ("note/on" [p: u7, v: u7] Self::NoteOn(p.expect("no pitch"), v.expect("no velocity"))) ("note/off" [p: u7] Self::NoteOff(p.expect("no pitch"))) }); provide!(u7: |self: Sampler| {}); provide!(Option>>: |self: Sampler| {}); provide!(usize: |self: Sampler| {}); provide!(f32: |self: Sampler| {}); //input_to_command!(FileBrowserCommand: |state:SamplerTui, input: Event|match input { _ => return None }); command!(|self: FileBrowserCommand,state:SamplerTui|match self { _ => todo!() }); //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_pos().overflowing_add(1).0.min(127)), //kpat!(KeyCode::Down) => Self::Select(state.note_pos().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_pos(); state.set_note_pos(index); Some(Self::Select(old)) }, Self::Sample(cmd) => cmd.execute(&mut state.state)?.map(Self::Sample), _ => todo!("{self:?}") }); command!(|self: SamplerCommand, state: Sampler|match self { Self::RecordBegin(index) => { state.begin_recording(index.as_int() as usize); None }, Self::RecordCancel => { state.cancel_recording(); None }, Self::RecordFinish => { state.finish_recording(); None }, 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)) }, _ => todo!("{self:?}") }); 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 } } content!(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(true, 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_pos(), height: self.size.h(), }), ))), )))) }); struct SamplesTui { color: ItemPalette, note_hi: usize, note_pt: usize, height: usize, } render!(TuiOut: |self: SamplesTui, to| { let x = to.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)); to.blit(&" (no sample) ", x, to.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_pos (&self) -> usize { self.note_pt.load(Relaxed) } fn set_note_pos (&self, x: usize) { self.note_pt.store(x, Relaxed); } } impl Sampler { const EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; pub fn list <'a> (&'a self, compact: bool, editor: &MidiEditor) -> impl Content + 'a { let note_lo = editor.note_lo().load(Relaxed); let note_pt = editor.note_pos(); let note_hi = editor.note_hi(); Outer(true, 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 self.mapped[note].is_some() { fg = TuiTheme::g(224); bg = Color::Rgb(0, if note == note_pt { 96 } else { 64 }, 0); } if let Some((index, _)) = self.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) } } offset(Tui::fg_bg(fg, bg, format!("{note:3} {}", self.list_item(note, compact)))) })) } pub fn list_item (&self, note: usize, compact: bool) -> String { if compact { String::default() } else if let Some(sample) = &self.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)") } } pub fn viewer (&self, note_pt: usize) -> impl Content { let sample = if let Some((_, sample)) = &self.recording { Some(sample.clone()) } else if let Some(sample) = &self.mapped[note_pt] { Some(sample.clone()) } else { None }; let min_db = -40.0; RenderThunk::new(move|to: &mut TuiOut|{ let [x, y, width, height] = to.area(); let area = Rect { x, y, width, height }; let (x_bounds, y_bounds, lines): ([f64;2], [f64;2], Vec) = if let Some(sample) = &sample { 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 fn status (&self, index: usize) -> impl Content { Tui::bold(true, Tui::fg(TuiTheme::g(224), self.mapped[index].as_ref().map(|sample|format!( "Sample {}-{}", sample.read().unwrap().start, sample.read().unwrap().end, )).unwrap_or_else(||"No sample".to_string()))) } }