mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
1116 lines
39 KiB
Rust
1116 lines
39 KiB
Rust
//! ```
|
||
//! 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<str>,
|
||
/// Device color.
|
||
pub color: ItemTheme,
|
||
/// Audio input ports. Samples get recorded here.
|
||
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
||
/// Audio input meters.
|
||
#[cfg(feature = "meter")] pub input_meters: Vec<f32>,
|
||
/// Sample currently being recorded.
|
||
pub recording: Option<(usize, Option<Arc<RwLock<Sample>>>)>,
|
||
/// Recording buffer.
|
||
pub buffer: Vec<Vec<f32>>,
|
||
/// Samples mapped to MIDI notes.
|
||
pub mapped: [Option<Arc<RwLock<Sample>>>;128],
|
||
/// Samples that are not mapped to MIDI notes.
|
||
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
||
/// Sample currently being edited.
|
||
pub editing: Option<Arc<RwLock<Sample>>>,
|
||
/// MIDI input port. Triggers sample playback.
|
||
#[cfg(feature = "port")] pub midi_in: MidiInput,
|
||
/// Collection of currently playing instances of samples.
|
||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||
/// Audio output ports. Voices get played here.
|
||
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
||
/// Audio output meters.
|
||
#[cfg(feature = "meter")] pub output_meters: Vec<f32>,
|
||
/// 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<SamplerMode>,
|
||
/// Size of rendered sampler.
|
||
pub size: Measure<TuiOut>,
|
||
/// 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<str>,
|
||
#[cfg(feature = "port")] midi_from: &[Connect],
|
||
#[cfg(feature = "port")] audio_from: &[&[Connect];2],
|
||
#[cfg(feature = "port")] audio_to: &[&[Connect];2],
|
||
) -> Usually<Self> {
|
||
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<str>,
|
||
pub start: usize,
|
||
pub end: usize,
|
||
pub channels: Vec<Vec<f32>>,
|
||
pub rate: Option<usize>,
|
||
pub gain: f32,
|
||
pub color: ItemTheme,
|
||
}
|
||
|
||
impl Sample {
|
||
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
|
||
Self {
|
||
name: name.as_ref().into(),
|
||
start,
|
||
end,
|
||
channels,
|
||
rate: None,
|
||
gain: 1.0,
|
||
color: ItemTheme::random(),
|
||
}
|
||
}
|
||
pub fn play (sample: &Arc<RwLock<Self>>, 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<RwLock<Sample>>,
|
||
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<Vec<f32>>)> {
|
||
let mut channels: Vec<wavers::Samples<f32>> = vec![];
|
||
for channel in wavers::Wav::from_path(src)?.channels() {
|
||
channels.push(channel);
|
||
}
|
||
let mut end = 0;
|
||
let mut data: Vec<Vec<f32>> = 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<Self> {
|
||
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<dyn Decoder>, packet: Packet
|
||
) -> Usually<()> {
|
||
// Decode a packet
|
||
let decoded = decoder
|
||
.decode(&packet)
|
||
.map_err(|e|Box::<dyn std::error::Error>::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<u7>, Arc<RwLock<crate::Sample>>);
|
||
|
||
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<Self::Item> {
|
||
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<str>] 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<OsString>,
|
||
files: Vec<OsString>,
|
||
cursor: usize,
|
||
offset: usize,
|
||
sample: Arc<RwLock<Sample>>,
|
||
voices: Arc<RwLock<Vec<Voice>>>,
|
||
_search: Option<String>,
|
||
}
|
||
|
||
impl AddSampleModal {
|
||
fn exited (&self) -> bool {
|
||
self.exited
|
||
}
|
||
fn exit (&mut self) {
|
||
self.exited = true
|
||
}
|
||
}
|
||
|
||
impl AddSampleModal {
|
||
pub fn new (
|
||
sample: &Arc<RwLock<Sample>>,
|
||
voices: &Arc<RwLock<Vec<Voice>>>
|
||
) -> Usually<Self> {
|
||
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<PathBuf> {
|
||
if self.cursor < self.subdirs.len() {
|
||
Some(self.dir.join(&self.subdirs[self.cursor]))
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
fn cursor_file (&self) -> Option<PathBuf> {
|
||
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<bool> {
|
||
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<Vec<f32>>)> {
|
||
todo!();
|
||
}
|
||
|
||
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||
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<TuiOut> 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<TuiOut> + 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<TuiOut> + 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<TuiOut> + '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<TuiOut> + 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<TuiOut> + 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<TuiOut> + 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<TuiOut> {
|
||
draw_status(self.mapped[index].as_ref())
|
||
}
|
||
|
||
pub fn view_meters_input (&self) -> impl Content<TuiOut> + use<'_> {
|
||
draw_meters(&self.input_meters)
|
||
}
|
||
|
||
pub fn view_meters_output (&self) -> impl Content<TuiOut> + use<'_> {
|
||
draw_meters(&self.output_meters)
|
||
}
|
||
}
|
||
|
||
fn draw_meters (meters: &[f32]) -> impl Content<TuiOut> + use<'_> {
|
||
Tui::bg(Black, Fixed::X(2, Map::east(1, ||meters.iter(), |value, _index|{
|
||
Fill::Y(RmsMeter(*value))
|
||
})))
|
||
}
|
||
|
||
fn draw_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> 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<RwLock<Sample>>>) -> impl Content<TuiOut> + 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<RwLock<Sample>>>) -> impl Content<TuiOut> + 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<RwLock<Sample>>>) -> impl Content<TuiOut> + 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<RwLock<Sample>>>) -> impl Content<TuiOut> {
|
||
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<usize> {
|
||
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<str> {
|
||
//todo!()
|
||
//}
|
||
//fn file_browser_path (&self) -> PathBuf {
|
||
//todo!();
|
||
//}
|
||
///// Immutable reference to sample at cursor.
|
||
//fn sample_selected (&self) -> Option<Arc<RwLock<Sample>>> {
|
||
//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> {
|
||
//Self::Select(state.set_note_pos(i))
|
||
//}
|
||
///// Assign sample to slot
|
||
//set (&self, slot: u7, sample: Option<Arc<RwLock<Sample>>>) -> Option<Self> {
|
||
//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<Self> {
|
||
//todo!()
|
||
//}
|
||
//set_gain (&self, state: &mut Sampler, slot: u7, g: f32) -> Option<Self> {
|
||
//todo!()
|
||
//}
|
||
//note_on (&self, state: &mut Sampler, slot: u7, v: u7) -> Option<Self> {
|
||
//todo!()
|
||
//}
|
||
//note_off (&self, state: &mut Sampler, slot: u7) -> Option<Self> {
|
||
//todo!()
|
||
//}
|
||
//set_sample (&self, state: &mut Sampler, slot: u7, s: Option<Arc<RwLock<Sample>>>) -> Option<Self> {
|
||
//Some(Self::SetSample(p, state.set_sample(p, s)))
|
||
//}
|
||
//import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option<Self> {
|
||
//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<Arc<RwLock<Sample>>>] 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<Arc<RwLock<Sample>>>]
|
||
////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"))))));
|