mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-07-17 07:46:57 +02:00
restruct: 25e
This commit is contained in:
parent
c737c7d839
commit
3bbe52093e
11 changed files with 454 additions and 432 deletions
57
src/app.rs
57
src/app.rs
|
|
@ -1,4 +1,5 @@
|
|||
use crate::*;
|
||||
pub mod audio; pub use self::audio::*;
|
||||
pub mod bind; pub use self::bind::*;
|
||||
pub mod cli; pub use self::cli::*;
|
||||
pub mod config; pub use self::config::*;
|
||||
|
|
@ -123,57 +124,6 @@ impl App {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn jack_process (
|
||||
&mut self, client: &Client, scope: &ProcessScope
|
||||
) -> Control {
|
||||
let t0 = self.perf.get_t0();
|
||||
self.clock().update_from_scope(scope).unwrap();
|
||||
let midi_in = self.project.midi_input_collect(scope);
|
||||
if let Some(editor) = &self.editor() {
|
||||
let mut pitch: Option<u7> = None;
|
||||
for port in midi_in.iter() {
|
||||
for event in port.iter() {
|
||||
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
|
||||
= event
|
||||
{
|
||||
pitch = Some(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(pitch) = pitch {
|
||||
editor.set_note_pos(pitch.as_int() as usize);
|
||||
}
|
||||
}
|
||||
let result = self.project.process_tracks(client, scope);
|
||||
self.perf.update_from_jack_scope(t0, scope);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn jack_event (
|
||||
&mut self, event: JackEvent
|
||||
) {
|
||||
use JackEvent::*;
|
||||
match event {
|
||||
SampleRate(sr) => { self.clock().timebase.sr.set(sr as f64); },
|
||||
PortRegistration(_id, true) => {
|
||||
//let port = self.jack().port_by_id(id);
|
||||
//println!("\rport add: {id} {port:?}");
|
||||
//println!("\rport add: {id}");
|
||||
},
|
||||
PortRegistration(_id, false) => {
|
||||
/*println!("\rport del: {id}")*/
|
||||
},
|
||||
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
|
||||
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
|
||||
ClientRegistration(_id, true) => {},
|
||||
ClientRegistration(_id, false) => {},
|
||||
ThreadInit => {},
|
||||
XRun => {},
|
||||
GraphReorder => {},
|
||||
_ => { panic!("{event:?}"); }
|
||||
}
|
||||
}
|
||||
|
||||
/// Update memoized render of clock values.
|
||||
/// ```
|
||||
/// tek::App::default().update_clock();
|
||||
|
|
@ -389,8 +339,6 @@ impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
|
|||
|
||||
impl_has_clips!( |self: App|self.pool.clips);
|
||||
|
||||
impl_audio!(App: tek_jack_process, tek_jack_event);
|
||||
|
||||
namespace!(App: Arc<str> { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); });
|
||||
|
||||
namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); });
|
||||
|
|
@ -564,13 +512,14 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua
|
|||
})))
|
||||
}.draw(to),
|
||||
|
||||
Some(":sessions") => h_exact(Some(6), w_min(30, thunk(|to: &mut Tui|{
|
||||
Some(":sessions") => h_exact(Some(6), w_min(Some(30), thunk(|to: &mut Tui|{
|
||||
let fg = Rgb(224, 192, 128);
|
||||
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
||||
let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
|
||||
y_push((2 * index) as u16,
|
||||
&h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to);
|
||||
}
|
||||
Ok(())
|
||||
}))).draw(to),
|
||||
|
||||
Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(),
|
||||
|
|
|
|||
50
src/app/audio.rs
Normal file
50
src/app/audio.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use crate::*;
|
||||
|
||||
impl_audio!(App: tek_jack_process, tek_jack_event);
|
||||
|
||||
fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control {
|
||||
let t0 = state.perf.get_t0();
|
||||
state.clock().update_from_scope(scope).unwrap();
|
||||
let midi_in = state.project.midi_input_collect(scope);
|
||||
if let Some(editor) = &state.editor() {
|
||||
let mut pitch: Option<u7> = None;
|
||||
for port in midi_in.iter() {
|
||||
for event in port.iter() {
|
||||
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
|
||||
= event
|
||||
{
|
||||
pitch = Some(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(pitch) = pitch {
|
||||
editor.set_note_pos(pitch.as_int() as usize);
|
||||
}
|
||||
}
|
||||
let result = state.project.process_tracks(client, scope);
|
||||
state.perf.update_from_jack_scope(t0, scope);
|
||||
result
|
||||
}
|
||||
|
||||
fn tek_jack_event (state: &mut App, event: JackEvent) {
|
||||
use JackEvent::*;
|
||||
match event {
|
||||
SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); },
|
||||
PortRegistration(_id, true) => {
|
||||
//let port = self.jack().port_by_id(id);
|
||||
//println!("\rport add: {id} {port:?}");
|
||||
//println!("\rport add: {id}");
|
||||
},
|
||||
PortRegistration(_id, false) => {
|
||||
/*println!("\rport del: {id}")*/
|
||||
},
|
||||
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
|
||||
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
|
||||
ClientRegistration(_id, true) => {},
|
||||
ClientRegistration(_id, false) => {},
|
||||
ThreadInit => {},
|
||||
XRun => {},
|
||||
GraphReorder => {},
|
||||
_ => { panic!("{event:?}"); }
|
||||
}
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ pub mod menu; pub use self::menu::*;
|
|||
pub mod meter; pub use self::meter::*;
|
||||
pub mod mix; pub use self::mix::*;
|
||||
pub mod port; pub use self::port::*;
|
||||
pub mod sample; pub use self::sample::*;
|
||||
pub mod sampler; pub use self::sampler::*;
|
||||
pub mod sequence; pub use self::sequence::*;
|
||||
|
||||
#[cfg(feature = "plugin")] pub mod plugin;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{*, clock::*, sequence::*, sample::*};
|
||||
use crate::{*, clock::*, sequence::*, sampler::*};
|
||||
|
||||
def_command!(FileBrowserCommand: |sampler: Sampler|{
|
||||
//("begin" [] Some(Self::Begin))
|
||||
|
|
@ -261,6 +261,7 @@ impl<'a> PoolView<'a> {
|
|||
}))))
|
||||
}
|
||||
}
|
||||
|
||||
impl ClipLength {
|
||||
fn tui (&self) -> impl Draw<Tui> {
|
||||
use ClipLengthFocus::*;
|
||||
|
|
@ -311,6 +312,7 @@ impl Browse {
|
|||
fn _todo_stub_usize (&self) -> usize { todo!() }
|
||||
fn _todo_stub_arc_str (&self) -> Arc<str> { todo!() }
|
||||
}
|
||||
|
||||
impl Browse {
|
||||
fn tui (&self) -> impl Draw<Tui> {
|
||||
let item = |entry, _index|w_full(origin_w(entry));
|
||||
|
|
@ -324,6 +326,7 @@ impl Browse {
|
|||
iter_south_fixed(1, iterate, item)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for EntriesIterator<'a, Tui> {
|
||||
type Item = impl Draw<Tui>;
|
||||
fn next (&mut self) -> Option<Self::Item> {
|
||||
|
|
@ -341,6 +344,7 @@ impl<'a> Iterator for EntriesIterator<'a, Tui> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for BrowseTarget {
|
||||
fn eq (&self, other: &Self) -> bool {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -16,14 +16,15 @@ use crate::*;
|
|||
/// let _ = editor.edit_status();
|
||||
/// ```
|
||||
pub struct MidiEditor {
|
||||
/// Size of editor on screen
|
||||
pub size: Size,
|
||||
/// View mode and state of editor
|
||||
pub mode: PianoHorizontal,
|
||||
/// Size of editor on screen
|
||||
pub size: Size,
|
||||
}
|
||||
|
||||
impl_default!(MidiEditor: Self {
|
||||
size: [0, 0].into(), mode: PianoHorizontal::new(None)
|
||||
mode: PianoHorizontal::new(None),
|
||||
size: Size(0.into(), 0.into()),
|
||||
});
|
||||
|
||||
impl MidiEditor {
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ impl Draw<Tui> for PianoHorizontal {
|
|||
impl PianoHorizontal {
|
||||
|
||||
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
|
||||
let size: Size = [0, 0].into();
|
||||
let size = Size::default();
|
||||
let mut range = MidiSelection::from((12, true));
|
||||
range.time_axis = size.x.clone();
|
||||
range.note_axis = size.y.clone();
|
||||
range.time_axis = size.0.clone();
|
||||
range.note_axis = size.1.clone();
|
||||
let piano = Self {
|
||||
keys_width: 5,
|
||||
size,
|
||||
|
|
|
|||
|
|
@ -1,51 +1,9 @@
|
|||
use crate::{*, device::*, browse::*, mix::*};
|
||||
|
||||
def_command!(SamplerCommand: |sampler: Sampler| {
|
||||
RecordToggle { slot: usize } => {
|
||||
let slot = *slot;
|
||||
let recording = sampler.recording.as_ref().map(|x|x.0);
|
||||
let _ = Self::RecordFinish.act(sampler)?;
|
||||
// autoslice: continue recording at next slot
|
||||
if recording != Some(slot) {
|
||||
Self::RecordBegin { slot }.act(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.samples.0[*index]);
|
||||
sample
|
||||
}); // TODO: undo
|
||||
Ok(None)
|
||||
},
|
||||
RecordCancel => {
|
||||
sampler.recording = None;
|
||||
Ok(None)
|
||||
},
|
||||
PlaySample { slot: usize } => {
|
||||
let slot = *slot;
|
||||
if let Some(ref sample) = sampler.samples.0[slot] {
|
||||
sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
|
||||
}
|
||||
Ok(None)
|
||||
},
|
||||
StopSample { slot: usize } => {
|
||||
let _slot = *slot;
|
||||
todo!();
|
||||
//Ok(None)
|
||||
},
|
||||
});
|
||||
mod voice; pub use self::voice::*;
|
||||
mod sample; pub use self::sample::*;
|
||||
mod sample_add; pub use self::sample_add::*;
|
||||
mod sample_kit; pub use self::sample_kit::*;
|
||||
|
||||
/// Plays [Voice]s from [Sample]s.
|
||||
///
|
||||
|
|
@ -97,51 +55,17 @@ def_command!(SamplerCommand: |sampler: Sampler| {
|
|||
#[cfg(feature = "meter")] pub output_meters: Vec<f32>,
|
||||
}
|
||||
|
||||
/// Collection of samples, one per slot, fixed number of slots.
|
||||
///
|
||||
/// History: Separated to cleanly implement [Default].
|
||||
///
|
||||
/// ```
|
||||
/// let samples = tek::SampleKit([None, None, None, None]);
|
||||
/// ```
|
||||
#[derive(Debug)] pub struct SampleKit<const N: usize>(
|
||||
pub [Option<Arc<RwLock<Sample>>>;N]
|
||||
);
|
||||
impl_audio!(Sampler: sampler_jack_process);
|
||||
|
||||
/// A sound cut.
|
||||
///
|
||||
/// ```
|
||||
/// let sample = tek::Sample::default();
|
||||
/// let sample = tek::Sample::new("test", 0, 0, vec![]);
|
||||
/// ```
|
||||
#[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,
|
||||
}
|
||||
|
||||
/// 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(Default, Debug)] pub struct SampleAdd {
|
||||
pub exited: bool,
|
||||
pub dir: PathBuf,
|
||||
pub subdirs: Vec<OsString>,
|
||||
pub files: Vec<OsString>,
|
||||
pub cursor: usize,
|
||||
pub offset: usize,
|
||||
pub sample: Arc<RwLock<Sample>>,
|
||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||
pub _search: Option<String>,
|
||||
pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control {
|
||||
if let Some(midi_in) = &state.midi_in {
|
||||
for midi in midi_in.port().iter(scope) {
|
||||
sampler_midi_in(&state.samples, &state.voices, midi)
|
||||
}
|
||||
}
|
||||
state.process_audio_out(scope);
|
||||
state.process_audio_in(scope);
|
||||
Control::Continue
|
||||
}
|
||||
|
||||
#[derive(Debug)] pub enum SamplerMode {
|
||||
|
|
@ -152,28 +76,6 @@ def_command!(SamplerCommand: |sampler: Sampler| {
|
|||
pub type MidiSample =
|
||||
(Option<u7>, Arc<RwLock<crate::Sample>>);
|
||||
|
||||
impl<const N: usize> Default for SampleKit<N> {
|
||||
fn default () -> Self { Self([const { None }; N]) }
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
impl NoteRange for Sampler {
|
||||
fn note_lo (&self) -> &AtomicUsize {
|
||||
&self.note_lo
|
||||
|
|
@ -182,6 +84,7 @@ impl NoteRange for Sampler {
|
|||
&self.size.y
|
||||
}
|
||||
}
|
||||
|
||||
impl NotePoint for Sampler {
|
||||
fn note_len (&self) -> &AtomicUsize {
|
||||
unreachable!();
|
||||
|
|
@ -205,6 +108,7 @@ impl NotePoint for Sampler {
|
|||
old
|
||||
}
|
||||
}
|
||||
|
||||
impl Sampler {
|
||||
pub fn new (
|
||||
jack: &Jack<'static>,
|
||||
|
|
@ -331,252 +235,6 @@ impl Sampler {
|
|||
}
|
||||
}
|
||||
}
|
||||
impl SampleAdd {
|
||||
fn exited (&self) -> bool {
|
||||
self.exited
|
||||
}
|
||||
fn exit (&mut self) {
|
||||
self.exited = true
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
impl<const N: usize> SampleKit<N> {
|
||||
pub fn get (&self, index: usize) -> &Option<Arc<RwLock<Sample>>> {
|
||||
if index < self.0.len() {
|
||||
&self.0[index]
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
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 */ }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// 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(())
|
||||
}
|
||||
}
|
||||
impl Draw<Tui> for SampleAdd {
|
||||
fn draw (self, _to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String {
|
||||
if let Some(sample) = sample {
|
||||
|
|
@ -652,18 +310,6 @@ fn draw_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_
|
|||
})
|
||||
}
|
||||
|
||||
impl_audio!(Sampler: sampler_jack_process);
|
||||
pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control {
|
||||
if let Some(midi_in) = &state.midi_in {
|
||||
for midi in midi_in.port().iter(scope) {
|
||||
sampler_midi_in(&state.samples, &state.voices, midi)
|
||||
}
|
||||
}
|
||||
state.process_audio_out(scope);
|
||||
state.process_audio_in(scope);
|
||||
Control::Continue
|
||||
}
|
||||
|
||||
/// Create [Voice]s from [Sample]s in response to MIDI input.
|
||||
fn sampler_midi_in (
|
||||
samples: &SampleKit<128>, voices: &Arc<RwLock<Vec<Voice>>>, RawMidi { time, bytes }: RawMidi
|
||||
|
|
@ -704,3 +350,50 @@ fn draw_sample (
|
|||
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||
todo!();
|
||||
}
|
||||
|
||||
def_command!(SamplerCommand: |sampler: Sampler| {
|
||||
RecordToggle { slot: usize } => {
|
||||
let slot = *slot;
|
||||
let recording = sampler.recording.as_ref().map(|x|x.0);
|
||||
let _ = Self::RecordFinish.act(sampler)?;
|
||||
// autoslice: continue recording at next slot
|
||||
if recording != Some(slot) {
|
||||
Self::RecordBegin { slot }.act(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.samples.0[*index]);
|
||||
sample
|
||||
}); // TODO: undo
|
||||
Ok(None)
|
||||
},
|
||||
RecordCancel => {
|
||||
sampler.recording = None;
|
||||
Ok(None)
|
||||
},
|
||||
PlaySample { slot: usize } => {
|
||||
let slot = *slot;
|
||||
if let Some(ref sample) = sampler.samples.0[slot] {
|
||||
sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
|
||||
}
|
||||
Ok(None)
|
||||
},
|
||||
StopSample { slot: usize } => {
|
||||
let _slot = *slot;
|
||||
todo!();
|
||||
//Ok(None)
|
||||
},
|
||||
});
|
||||
144
src/device/sampler/sample.rs
Normal file
144
src/device/sampler/sample.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
use crate::*;
|
||||
|
||||
/// A sound cut.
|
||||
///
|
||||
/// ```
|
||||
/// let sample = tek::Sample::default();
|
||||
/// let sample = tek::Sample::new("test", 0, 0, vec![]);
|
||||
/// ```
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
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 */ }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// 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(())
|
||||
}
|
||||
}
|
||||
126
src/device/sampler/sample_add.rs
Normal file
126
src/device/sampler/sample_add.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
use crate::{*, device::sampler::*};
|
||||
|
||||
#[derive(Default, Debug)] pub struct SampleAdd {
|
||||
pub exited: bool,
|
||||
pub dir: PathBuf,
|
||||
pub subdirs: Vec<OsString>,
|
||||
pub files: Vec<OsString>,
|
||||
pub cursor: usize,
|
||||
pub offset: usize,
|
||||
pub sample: Arc<RwLock<Sample>>,
|
||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||
pub _search: Option<String>,
|
||||
}
|
||||
|
||||
impl SampleAdd {
|
||||
fn exited (&self) -> bool {
|
||||
self.exited
|
||||
}
|
||||
fn exit (&mut self) {
|
||||
self.exited = true
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl Draw<Tui> for SampleAdd {
|
||||
fn draw (self, _to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
26
src/device/sampler/sample_kit.rs
Normal file
26
src/device/sampler/sample_kit.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use crate::*;
|
||||
|
||||
/// Collection of samples, one per slot, fixed number of slots.
|
||||
///
|
||||
/// History: Separated to cleanly implement [Default].
|
||||
///
|
||||
/// ```
|
||||
/// let samples = tek::SampleKit([None, None, None, None]);
|
||||
/// ```
|
||||
#[derive(Debug)] pub struct SampleKit <const N: usize> (
|
||||
pub [Option<Arc<RwLock<Sample>>>;N]
|
||||
);
|
||||
|
||||
impl<const N: usize> Default for SampleKit<N> {
|
||||
fn default () -> Self { Self([const { None }; N]) }
|
||||
}
|
||||
|
||||
impl<const N: usize> SampleKit<N> {
|
||||
pub fn get (&self, index: usize) -> &Option<Arc<RwLock<Sample>>> {
|
||||
if index < self.0.len() {
|
||||
&self.0[index]
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/device/sampler/voice.rs
Normal file
29
src/device/sampler/voice.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use crate::*;
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue