//! ``` //! let sample = Sample::new("test", 0, 0, vec![]); //! ``` use crate::*; /// The sampler device plays sounds in response to MIDI notes. #[derive(Debug)] pub struct Sampler { /// Name of sampler. pub name: Arc, /// Device color. pub color: ItemTheme, /// Audio input ports. Samples get recorded here. #[cfg(feature = "port")] pub audio_ins: Vec, /// Audio input meters. #[cfg(feature = "meter")] pub input_meters: Vec, /// Sample currently being recorded. pub recording: Option<(usize, Option>>)>, /// Recording buffer. pub buffer: Vec>, /// Samples mapped to MIDI notes. pub mapped: [Option>>;128], /// Samples that are not mapped to MIDI notes. pub unmapped: Vec>>, /// Sample currently being edited. pub editing: Option>>, /// MIDI input port. Triggers sample playback. #[cfg(feature = "port")] pub midi_in: MidiInput, /// Collection of currently playing instances of samples. pub voices: Arc>>, /// Audio output ports. Voices get played here. #[cfg(feature = "port")] pub audio_outs: Vec, /// Audio output meters. #[cfg(feature = "meter")] pub output_meters: Vec, /// How to mix the voices. pub mixing_mode: MixingMode, /// How to meter the inputs and outputs. pub metering_mode: MeteringMode, /// Fixed gain applied to all output. pub output_gain: f32, /// Currently active modal, if any. pub mode: Option, /// Size of rendered sampler. pub size: Measure, /// Lowest note displayed. pub note_lo: AtomicUsize, /// Currently selected note. pub note_pt: AtomicUsize, /// Selected note as row/col. pub cursor: (AtomicUsize, AtomicUsize), } impl Sampler { pub fn new ( jack: &Jack<'static>, name: impl AsRef, #[cfg(feature = "port")] midi_from: &[Connect], #[cfg(feature = "port")] audio_from: &[&[Connect];2], #[cfg(feature = "port")] audio_to: &[&[Connect];2], ) -> Usually { let name = name.as_ref(); Ok(Self { name: name.into(), #[cfg(feature = "port")] midi_in: MidiInput::new(jack, &format!("M/{name}"), midi_from)?, #[cfg(feature = "port")] audio_ins: vec![ AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, ], #[cfg(feature = "port")] audio_outs: vec![ AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, ], input_meters: vec![0.0;2], output_meters: vec![0.0;2], mapped: [const { None };128], unmapped: vec![], voices: Arc::new(RwLock::new(vec![])), buffer: vec![vec![0.0;16384];2], output_gain: 1., recording: None, mode: None, editing: None, size: Default::default(), note_lo: 0.into(), note_pt: 0.into(), cursor: (0.into(), 0.into()), color: Default::default(), mixing_mode: Default::default(), metering_mode: Default::default(), }) } /// Value of cursor pub fn cursor (&self) -> (usize, usize) { (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) } } impl NoteRange for Sampler { fn note_lo (&self) -> &AtomicUsize { &self.note_lo } fn note_axis (&self) -> &AtomicUsize { &self.size.y } } impl NotePoint for Sampler { fn note_len (&self) -> &AtomicUsize { unreachable!(); } fn get_note_len (&self) -> usize { 0 } fn set_note_len (&self, x: usize) -> usize { 0 /*TODO?*/ } fn note_pos (&self) -> &AtomicUsize { &self.note_pt } fn get_note_pos (&self) -> usize { self.note_pt.load(Relaxed) } fn set_note_pos (&self, x: usize) -> usize { let old = self.note_pt.swap(x, Relaxed); self.cursor.0.store(x % 8, Relaxed); self.cursor.1.store(x / 8, Relaxed); old } } /// 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, pub color: ItemTheme, } impl Sample { pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { Self { name: name.as_ref().into(), start, end, channels, rate: None, gain: 1.0, color: ItemTheme::random(), } } pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { Voice { sample: sample.clone(), after, position: sample.read().unwrap().start, velocity: velocity.as_int() as f32 / 127.0, } } } /// A currently playing instance of a sample. #[derive(Default, Debug, Clone)] pub struct Voice { pub sample: Arc>, pub after: usize, pub position: usize, pub velocity: f32, } #[derive(Debug)] pub enum SamplerMode { // Load sample from path Import(usize, Browse), } impl Sample { /// 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 type MidiSample = (Option, Arc>); impl Sampler { /// Create [Voice]s from [Sample]s in response to MIDI input. pub fn process_midi_in (&mut self, scope: &ProcessScope) { let Sampler { midi_in, mapped, voices, .. } = self; for RawMidi { time, bytes } in midi_in.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 } _ => {} } } } } } impl Sample { 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 */ } _ => {} } } } // TODO: //for port in midi_in.iter() { //for event in port.iter() { //match event { //(time, Ok(LiveEvent::Midi {message, ..})) => match message { //MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => { //editor.set_note_pos(key.as_int() as usize); //}, //MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = ( //self.editor.as_ref(), //self.sampler.as_ref(), //) => { //// TODO: give sampler its own cursor //if let Some(sample) = &sampler.mapped[editor.note_pos()] { //sample.write().unwrap().handle_cc(*controller, *value) //} //} //_ =>{} //}, //_ =>{} //} //} //} audio!(|self: Sampler, _client, scope|{ self.process_midi_in(scope); self.process_audio_out(scope); self.process_audio_in(scope); Control::Continue }); impl Sampler { pub fn process_audio_in (&mut self, scope: &ProcessScope) { self.reset_input_meters(); if self.recording.is_some() { self.record_into(scope); } else { self.update_input_meters(scope); } } /// Make sure that input meter count corresponds to input channel count fn reset_input_meters (&mut self) { let channels = self.audio_ins.len(); if self.input_meters.len() != channels { self.input_meters = vec![f32::MIN;channels]; } } /// Record from inputs to sample fn record_into (&mut self, scope: &ProcessScope) { if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { let mut sample = sample.write().unwrap(); if sample.channels.len() != self.audio_ins.len() { panic!("channel count mismatch"); } let samples_with_meters = self.audio_ins.iter() .zip(self.input_meters.iter_mut()) .zip(sample.channels.iter_mut()); let mut length = 0; for ((input, meter), channel) in samples_with_meters { let slice = input.port().as_slice(scope); length = length.max(slice.len()); *meter = to_rms(slice); channel.extend_from_slice(slice); } sample.end += length; } else { panic!("tried to record into the void") } } /// Update input meters fn update_input_meters (&mut self, scope: &ProcessScope) { for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { let slice = input.port().as_slice(scope); *meter = to_rms(slice); } } /// Make sure that output meter count corresponds to input channel count fn reset_output_meters (&mut self) { let channels = self.audio_outs.len(); if self.output_meters.len() != channels { self.output_meters = vec![f32::MIN;channels]; } } /// Mix all currently playing samples into the output. pub fn process_audio_out (&mut self, scope: &ProcessScope) { self.clear_output_buffer(); self.populate_output_buffer(scope.n_frames() as usize); self.write_output_buffer(scope); } /// Zero the output buffer. fn clear_output_buffer (&mut self) { for buffer in self.buffer.iter_mut() { buffer.fill(0.0); } } /// Write playing voices to output buffer fn populate_output_buffer (&mut self, frames: usize) { let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; let channel_count = buffer.len(); match mixing_mode { MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) }), MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) }), } } /// Write output buffer to output ports. fn write_output_buffer (&mut self, scope: &ProcessScope) { let Sampler { audio_outs, buffer, .. } = self; for (i, port) in audio_outs.iter_mut().enumerate() { let buffer = &buffer[i]; for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { *value = *buffer.get(i).unwrap_or(&0.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 } } def_command!(SamplerCommand: |sampler: Sampler| { RecordToggle { slot: usize } => { let slot = *slot; let recording = sampler.recording.as_ref().map(|x|x.0); let _ = Self::RecordFinish.execute(sampler)?; // autoslice: continue recording at next slot if recording != Some(slot) { Self::RecordBegin { slot }.execute(sampler) } else { Ok(None) } }, RecordBegin { slot: usize } => { let slot = *slot; sampler.recording = Some(( slot, Some(Arc::new(RwLock::new(Sample::new( "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] )))) )); Ok(None) }, RecordFinish => { let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ std::mem::swap(sample, &mut sampler.mapped[*index]); sample }); // TODO: undo Ok(None) }, RecordCancel => { sampler.recording = None; Ok(None) }, PlaySample { slot: usize } => { let slot = *slot; if let Some(ref sample) = sampler.mapped[slot] { sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); } Ok(None) }, StopSample { slot: usize } => { let slot = *slot; todo!(); Ok(None) }, }); def_command!(FileBrowserCommand: |sampler: Sampler|{ //("begin" [] Some(Self::Begin)) //("cancel" [] Some(Self::Cancel)) //("confirm" [] Some(Self::Confirm)) //("select" [i: usize] Some(Self::Select(i.expect("no index")))) //("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) //("filter" [f: Arc] Some(Self::Filter(f.expect("no filter"))))) }); impl Sampler { fn sample_selected (&self) -> usize { (self.get_note_pos() as u8).into() } fn sample_selected_pitch (&self) -> u7 { (self.get_note_pos() as u8).into() } } 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)) } impl Draw for AddSampleModal { fn draw (&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 Sampler { pub fn view_grid (&self) -> impl Content + use<'_> { //let cells_x = 8u16; //let cells_y = 8u16; //let cell_width = 10u16; //let cell_height = 2u16; //let width = cells_x * cell_width; //let height = cells_y * cell_height; //let cols = Map::east( //cell_width, //move||0..cells_x, //move|x, _|Map::south( //cell_height, //move||0..cells_y, //move|y, _|self.view_grid_cell("........", x, y, cell_width, cell_height) //) //); //cols //Thunk::new(|to: &mut TuiOut|{ //}) "TODO" } pub fn view_grid_cell <'a> ( &'a self, name: &'a str, x: u16, y: u16, w: u16, h: u16 ) -> impl Content + use<'a> { let cursor = self.cursor(); let hi_fg = Color::Rgb(64, 64, 64); let hi_bg = if y == 0 { Color::Reset } else { Color::Rgb(64, 64, 64) /*prev*/ }; let tx_fg = if let Some((index, _)) = self.recording && index % 8 == x as usize && index / 8 == y as usize { Color::Rgb(255, 64, 0) } else { Color::Rgb(255, 255, 255) }; let tx_bg = if x as usize == cursor.0 && y as usize == cursor.1 { Color::Rgb(96, 96, 96) } else { Color::Rgb(64, 64, 64) }; let lo_fg = Color::Rgb(64, 64, 64); let lo_bg = if y == 7 { Color::Reset } else { tx_bg }; Fixed::XY(w, h, Bsp::s( Fixed::Y(1, Tui::fg_bg(hi_fg, hi_bg, RepeatH(Phat::<()>::LO))), Bsp::n( Fixed::Y(1, Tui::fg_bg(lo_fg, lo_bg, RepeatH(Phat::<()>::HI))), Fill::X(Fixed::Y(1, Tui::fg_bg(tx_fg, tx_bg, name))), ), )) } const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; pub fn view_list <'a, T: NotePoint + NoteRange> ( &'a self, compact: bool, editor: &T ) -> impl Content + 'a { let note_lo = editor.get_note_lo(); let note_pt = editor.get_note_pos(); let note_hi = editor.get_note_hi(); Fixed::X(if compact { 4 } else { 12 }, Map::south( 1, move||(note_lo..=note_hi).rev(), move|note, _index| { //let offset = |a|Push::y(i as u16, Align::n(Fixed::Y(1, Fill::X(a)))); let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; let mut fg = Tui::g(160); if let Some(mapped) = &self.mapped[note] { let sample = mapped.read().unwrap(); fg = if note == note_pt { sample.color.lightest.rgb } else { Tui::g(224) }; bg = if note == note_pt { sample.color.light.rgb } else { sample.color.base.rgb }; } 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) } } Tui::fg_bg(fg, bg, format!("{note:3} {}", self.view_list_item(note, compact))) })) } pub fn view_list_item (&self, note: usize, compact: bool) -> String { if compact { String::default() } else { draw_list_item(&self.mapped[note]) } } pub fn view_sample (&self, note_pt: usize) -> impl Content + use<'_> { Outer(true, Style::default().fg(Tui::g(96))) .enclose(Fill::XY(draw_viewer(if let Some((_, Some(sample))) = &self.recording { Some(sample) } else if let Some(sample) = &self.mapped[note_pt] { Some(sample) } else { None }))) } pub fn view_sample_info (&self, note_pt: usize) -> impl Content + use<'_> { Fill::X(Fixed::Y(1, draw_info(if let Some((_, Some(sample))) = &self.recording { Some(sample) } else if let Some(sample) = &self.mapped[note_pt] { Some(sample) } else { None }))) } pub fn view_sample_status (&self, note_pt: usize) -> impl Content + use<'_> { Fixed::X(20, draw_info_v(if let Some((_, Some(sample))) = &self.recording { Some(sample) } else if let Some(sample) = &self.mapped[note_pt] { Some(sample) } else { None })) } pub fn view_status (&self, index: usize) -> impl Content { draw_status(self.mapped[index].as_ref()) } pub fn view_meters_input (&self) -> impl Content + use<'_> { draw_meters(&self.input_meters) } pub fn view_meters_output (&self) -> impl Content + use<'_> { draw_meters(&self.output_meters) } } fn draw_meters (meters: &[f32]) -> impl Content + use<'_> { Tui::bg(Black, Fixed::X(2, Map::east(1, ||meters.iter(), |value, _index|{ Fill::Y(RmsMeter(*value)) }))) } fn draw_list_item (sample: &Option>>) -> String { if let Some(sample) = sample { let sample = sample.read().unwrap(); format!("{:8}", sample.name) //format!("{:8} {:3} {:6}-{:6}/{:6}", //sample.name, //sample.gain, //sample.start, //sample.end, //sample.channels[0].len() //) } else { String::from("........") } } fn draw_viewer (sample: Option<&Arc>>) -> impl Content + use<'_> { let min_db = -64.0; Thunk::new(move|to: &mut TuiOut|{ let [x, y, width, height] = to.area(); let area = Rect { x, y, width, height }; 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.; } Canvas::default() .x_bounds([sample.start as f64, sample.end as f64]) .y_bounds([min_db, 0.]) .paint(|ctx| { for line in lines.iter() { ctx.draw(line); } //FIXME: proportions //let text = "press record to finish sampling"; //ctx.print( //(width - text.len() as u16) as f64 / 2.0, //height as f64 / 2.0, //text.red() //); }).render(area, &mut to.buffer); } else { Canvas::default() .x_bounds([0.0, width as f64]) .y_bounds([0.0, height as f64]) .paint(|_ctx| { //let text = "press record to begin sampling"; //ctx.print( //(width - text.len() as u16) as f64 / 2.0, //height as f64 / 2.0, //text.red() //); }) .render(area, &mut to.buffer); } }) } fn draw_info (sample: Option<&Arc>>) -> impl Content + use<'_> { When::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; to.place(&row!( FieldH(theme, "Name", format!("{:<10}", sample.name.clone())), FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())), FieldH(theme, "Start", format!("{:<8}", sample.start)), FieldH(theme, "End", format!("{:<8}", sample.end)), FieldH(theme, "Trans", "0"), FieldH(theme, "Gain", format!("{}", sample.gain)), )) })) } fn draw_info_v (sample: Option<&Arc>>) -> impl Content + use<'_> { Either::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; to.place(&Fixed::X(20, col!( Fill::X(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))), Fill::X(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))), Fill::X(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))), Fill::X(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))), Fill::X(Align::w(FieldH(theme, "Trans ", "0"))), Fill::X(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))), ))) }), Thunk::new(|to: &mut TuiOut|to.place(&Tui::fg(Red, col!( Tui::bold(true, "× No sample."), "[r] record", "[Shift-F9] import", ))))) } fn draw_status (sample: Option<&Arc>>) -> impl Content { Tui::bold(true, Tui::fg(Tui::g(224), sample .map(|sample|{ let sample = sample.read().unwrap(); format!("Sample {}-{}", sample.start, sample.end) }) .unwrap_or_else(||"No sample".to_string()))) } 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) } //fn file_browser_filter (&self) -> Arc { //todo!() //} //fn file_browser_path (&self) -> PathBuf { //todo!(); //} ///// Immutable reference to sample at cursor. //fn sample_selected (&self) -> Option>> { //for (i, sample) in self.mapped.iter().enumerate() { //if i == self.cursor().0 { //return sample.as_ref() //} //} //for (i, sample) in self.unmapped.iter().enumerate() { //if i + self.mapped.len() == self.cursor().0 { //return Some(sample) //} //} //None //} //fn sample_gain (&self) -> f32 { //todo!() //} //fn sample_above () -> usize { //self.note_pos().min(119) + 8 //} //fn sample_below () -> usize { //self.note_pos().max(8) - 8 //} //fn sample_to_left () -> usize { //self.note_pos().min(126) + 1 //} //fn sample_to_right () -> usize { //self.note_pos().max(1) - 1 //} //fn selected_pitch () -> u7 { //(self.note_pos() as u8).into() // TODO //} //select (&self, state: &mut Sampler, i: usize) -> Option { //Self::Select(state.set_note_pos(i)) //} ///// Assign sample to slot //set (&self, slot: u7, sample: Option>>) -> Option { //let i = slot.as_int() as usize; //let old = self.mapped[i].clone(); //self.mapped[i] = sample; //Some(Self::Set(old)) //} //set_start (&self, state: &mut Sampler, slot: u7, frame: usize) -> Option { //todo!() //} //set_gain (&self, state: &mut Sampler, slot: u7, g: f32) -> Option { //todo!() //} //note_on (&self, state: &mut Sampler, slot: u7, v: u7) -> Option { //todo!() //} //note_off (&self, state: &mut Sampler, slot: u7) -> Option { //todo!() //} //set_sample (&self, state: &mut Sampler, slot: u7, s: Option>>) -> Option { //Some(Self::SetSample(p, state.set_sample(p, s))) //} //import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option { //match c { //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 //}, //_ => { //println!("\n\rtodo: import: filebrowser: {c:?}"); //None //} //} //} ////(Select [i: usize] Some(Self::Select(state.set_note_pos(i)))) ////(RecordBegin [p: u7] cmd!(state.begin_recording(p.as_int() as usize))) ////(RecordCancel [] cmd!(state.cancel_recording())) ////(RecordFinish [] cmd!(state.finish_recording())) ////(SetStart [p: u7, frame: usize] cmd_todo!("\n\rtodo: {self:?}")) ////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}")) ////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}")) ////(NoteOff [p: u7] cmd_todo!("\n\rtodo: {self:?}")) ////(SetSample [p: u7, s: Option>>] Some(Self::SetSample(p, state.set_sample(p, s)))) ////(Import [c: FileBrowserCommand] match c { ////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 ////}, ////_ => { ////println!("\n\rtodo: import: filebrowser: {c:?}"); ////None ////} ////}))); ////("import" [,..a] ////FileBrowserCommand::try_from_expr(state, a).map(Self::Import)) ////("select" [i: usize] ////Some(Self::Select(i.expect("no index")))) ////("record/begin" [i: u7] ////Some(Self::RecordBegin(i.expect("no index")))) ////("record/cancel" [] ////Some(Self::RecordCancel)) ////("record/finish" [] ////Some(Self::RecordFinish)) ////("set/sample" [i: u7, s: Option>>] ////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler")))) ////("set/start" [i: u7, s: usize] ////Some(Self::SetStart(i.expect("no index"), s.expect("no start")))) ////("set/gain" [i: u7, g: f32] ////Some(Self::SetGain(i.expect("no index"), g.expect("no gain")))) ////("note/on" [p: u7, v: u7] ////Some(Self::NoteOn(p.expect("no slot"), v.expect("no velocity")))) ////("note/off" [p: u7] ////Some(Self::NoteOff(p.expect("no slot"))))));