extract edn; build out more groovebox

This commit is contained in:
🪞👃🪞 2024-12-18 12:46:42 +01:00
parent 950dbdfe8e
commit 8cf42aff0b
10 changed files with 231 additions and 205 deletions

View file

@ -1,18 +1,5 @@
use crate::*;
pub struct SamplerAudio {
model: Arc<RwLock<Sampler>>
}
/// 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,
}
/// The sampler plugin plays sounds.
#[derive(Debug)]
pub struct Sampler {
@ -37,6 +24,15 @@ pub struct Sample {
pub rate: Option<usize>,
}
/// 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,
}
/// Load sample from WAV and assign to MIDI note.
#[macro_export] macro_rules! sample {
($note:expr, $name:expr, $src:expr) => {{
@ -49,66 +45,10 @@ pub struct Sample {
}
impl Sampler {
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Self> {
let mut name = String::new();
let mut dir = String::new();
let mut samples = BTreeMap::new();
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) {
dir = String::from(*n);
}
},
Edn::List(args) => match args.get(0) {
Some(Edn::Symbol("sample")) => {
let (midi, sample) = Sample::from_edn(jack, &dir, &args[1..])?;
if let Some(midi) = midi {
samples.insert(midi, sample);
} else {
panic!("sample without midi binding: {}", sample.read().unwrap().name);
}
},
_ => panic!("unexpected in sampler {name}: {args:?}")
},
_ => panic!("unexpected in sampler {name}: {edn:?}")
});
let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?;
Ok(Sampler {
jack: jack.clone(),
name: name.into(),
mapped: samples,
unmapped: Default::default(),
voices: Default::default(),
buffer: Default::default(),
midi_in: midi_in,
audio_outs: vec![],
output_gain: 0.
})
}
}
impl From<&Arc<RwLock<Sampler>>> for SamplerAudio {
fn from (model: &Arc<RwLock<Sampler>>) -> Self {
Self { model: model.clone() }
}
}
audio!(|self: SamplerAudio, _client, scope|{
self.process_midi_in(scope);
self.clear_output_buffer();
self.process_audio_out(scope);
self.write_output_buffer(scope);
Control::Continue
});
impl SamplerAudio {
/// 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.model.read().unwrap();
let Sampler { midi_in, mapped, voices, .. } = self;
for RawMidi { time, bytes } in midi_in.iter(scope) {
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
if let MidiMessage::NoteOn { ref key, ref vel } = message {
@ -122,14 +62,14 @@ impl SamplerAudio {
/// Zero the output buffer.
pub fn clear_output_buffer (&mut self) {
for buffer in self.model.write().unwrap().buffer.iter_mut() {
for buffer in self.buffer.iter_mut() {
buffer.fill(0.0);
}
}
/// Mix all currently playing samples into the output.
pub fn process_audio_out (&mut self, scope: &ProcessScope) {
let Sampler { ref mut buffer, voices, output_gain, .. } = &mut*self.model.write().unwrap();
let Sampler { ref mut buffer, voices, output_gain, .. } = self;
let channel_count = buffer.len();
voices.write().unwrap().retain_mut(|voice|{
for index in 0..scope.n_frames() as usize {
@ -151,7 +91,7 @@ impl SamplerAudio {
/// Write output buffer to output ports.
pub fn write_output_buffer (&mut self, scope: &ProcessScope) {
let Sampler { ref mut audio_outs, buffer, .. } = &mut*self.model.write().unwrap();
let Sampler { ref mut audio_outs, buffer, .. } = self;
for (i, port) in audio_outs.iter_mut().enumerate() {
let buffer = &buffer[i];
for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() {
@ -162,7 +102,6 @@ impl SamplerAudio {
}
impl Sample {
pub fn new (name: &str, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
Self { name: name.to_string(), start, end, channels, rate: None }
@ -175,38 +114,6 @@ impl Sample {
velocity: velocity.as_int() as f32 / 127.0,
}
}
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option<u7>, Arc<RwLock<Self>>)> {
let mut name = String::new();
let mut file = String::new();
let mut midi = None;
let mut start = 0usize;
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) {
file = String::from(*f);
}
if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) {
start = *i as usize;
}
if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) {
midi = Some(u7::from(*m as u8));
}
},
_ => panic!("unexpected in sample {name}"),
});
let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?;
Ok((midi, Arc::new(RwLock::new(Self {
name: name.into(),
start,
end,
channels: data,
rate: None
}))))
}
/// Read WAV from file
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
let mut channels: Vec<wavers::Samples<f32>> = vec![];

View file

@ -94,34 +94,3 @@ pub trait ArrangerSceneApi: Sized {
}
}
//impl ArrangerScene {
////TODO
////pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
////let mut name = None;
////let mut clips = vec![];
////edn!(edn in args {
////Edn::Map(map) => {
////let key = map.get(&Edn::Key(":name"));
////if let Some(Edn::Str(n)) = key {
////name = Some(*n);
////} else {
////panic!("unexpected key in scene '{name:?}': {key:?}")
////}
////},
////Edn::Symbol("_") => {
////clips.push(None);
////},
////Edn::Int(i) => {
////clips.push(Some(*i as usize));
////},
////_ => panic!("unexpected in scene '{name:?}': {edn:?}")
////});
////Ok(ArrangerScene {
////name: Arc::new(name.unwrap_or("").to_string().into()),
////color: ItemColor::random(),
////clips,
////})
////}
//}

View file

@ -1,7 +1,6 @@
pub(crate) mod audio; pub(crate) use audio::*;
pub(crate) mod color; pub(crate) use color::*;
pub(crate) mod command; pub(crate) use command::*;
pub(crate) mod edn; pub(crate) use edn::*;
pub(crate) mod engine; pub(crate) use engine::*;
pub(crate) mod focus; pub(crate) use focus::*;
pub(crate) mod input; pub(crate) use input::*;

View file

@ -1,15 +0,0 @@
pub use clojure_reader::edn::Edn;
//pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError};
/// EDN parsing helper.
#[macro_export] macro_rules! edn {
($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
match $edn { $($pat => $expr),* }
};
($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
for $edn in $args {
edn!($edn { $($pat => $expr),* })
}
};
}

View file

@ -76,8 +76,12 @@ pub trait Area<N: Coordinate>: Copy {
#[inline] fn lrtb (&self) -> [N;4] { [self.x(), self.x2(), self.y(), self.y2()] }
#[inline] fn push_x (&self, x: N) -> [N;4] { [self.x() + x, self.y(), self.w(), self.h()] }
#[inline] fn push_y (&self, y: N) -> [N;4] { [self.x(), self.y() + y, self.w(), self.h()] }
#[inline] fn shrink_x (&self, x: N) -> [N;4] { [self.x(), self.y(), self.w() - x, self.h()] }
#[inline] fn shrink_y (&self, y: N) -> [N;4] { [self.x(), self.y(), self.w(), self.h() - y] }
#[inline] fn shrink_x (&self, x: N) -> [N;4] {
[self.x(), self.y(), self.w().minus(x), self.h()]
}
#[inline] fn shrink_y (&self, y: N) -> [N;4] {
[self.x(), self.y(), self.w(), self.h().minus(y)]
}
#[inline] fn set_w (&self, w: N) -> [N;4] { [self.x(), self.y(), w, self.h()] }
#[inline] fn set_h (&self, h: N) -> [N;4] { [self.x(), self.y(), self.w(), h] }
#[inline] fn clip_h (&self, h: N) -> [N;4] {

123
crates/tek/src/edn.rs Normal file
View file

@ -0,0 +1,123 @@
use crate::*;
pub use clojure_reader::edn::Edn;
//pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError};
/// EDN parsing helper.
#[macro_export] macro_rules! edn {
($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
match $edn { $($pat => $expr),* }
};
($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
for $edn in $args {
edn!($edn { $($pat => $expr),* })
}
};
}
impl crate::Sampler {
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Self> {
let mut name = String::new();
let mut dir = String::new();
let mut samples = BTreeMap::new();
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) {
dir = String::from(*n);
}
},
Edn::List(args) => match args.get(0) {
Some(Edn::Symbol("sample")) => {
let (midi, sample) = Sample::from_edn(jack, &dir, &args[1..])?;
if let Some(midi) = midi {
samples.insert(midi, sample);
} else {
panic!("sample without midi binding: {}", sample.read().unwrap().name);
}
},
_ => panic!("unexpected in sampler {name}: {args:?}")
},
_ => panic!("unexpected in sampler {name}: {edn:?}")
});
let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?;
Ok(Sampler {
jack: jack.clone(),
name: name.into(),
mapped: samples,
unmapped: Default::default(),
voices: Default::default(),
buffer: Default::default(),
midi_in: midi_in,
audio_outs: vec![],
output_gain: 0.
})
}
}
impl crate::Sample {
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option<u7>, Arc<RwLock<Self>>)> {
let mut name = String::new();
let mut file = String::new();
let mut midi = None;
let mut start = 0usize;
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) {
file = String::from(*f);
}
if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) {
start = *i as usize;
}
if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) {
midi = Some(u7::from(*m as u8));
}
},
_ => panic!("unexpected in sample {name}"),
});
let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?;
Ok((midi, Arc::new(RwLock::new(Self {
name: name.into(),
start,
end,
channels: data,
rate: None
}))))
}
}
//impl ArrangerScene {
////TODO
////pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
////let mut name = None;
////let mut clips = vec![];
////edn!(edn in args {
////Edn::Map(map) => {
////let key = map.get(&Edn::Key(":name"));
////if let Some(Edn::Str(n)) = key {
////name = Some(*n);
////} else {
////panic!("unexpected key in scene '{name:?}': {key:?}")
////}
////},
////Edn::Symbol("_") => {
////clips.push(None);
////},
////Edn::Int(i) => {
////clips.push(Some(*i as usize));
////},
////_ => panic!("unexpected in scene '{name:?}': {edn:?}")
////});
////Ok(ArrangerScene {
////name: Arc::new(name.unwrap_or("").to_string().into()),
////color: ItemColor::random(),
////clips,
////})
////}
//}

View file

@ -31,6 +31,9 @@ pub(crate) use api::*;
pub mod tui;
pub(crate) use tui::*;
pub mod edn;
pub(crate) use edn::*;
testmod! { test }
pub(crate) use clap::{self, Parser};

View file

@ -10,7 +10,8 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for GrooveboxTui {
Ok(Self {
sequencer,
sampler: SamplerTui::try_from(jack)?,
split: 16
split: 16,
focus: GrooveboxFocus::Sequencer,
})
}
}
@ -19,24 +20,39 @@ pub struct GrooveboxTui {
pub sequencer: SequencerTui,
pub sampler: SamplerTui,
pub split: u16,
pub focus: GrooveboxFocus
}
pub enum GrooveboxFocus {
Sequencer,
Sampler
}
audio!(|self:GrooveboxTui,_client,_process|Control::Continue);
render!(<Tui>|self:GrooveboxTui|Bsp::n(
Fixed::h(2, SequencerStatusBar::from(&self.sequencer)),
Fill::h(Bsp::s(&self.sequencer, &self.sampler)),
));
pub enum GrooveboxCommand {
Sequencer(SequencerCommand),
Sampler(SamplerCommand),
}
render!(<Tui>|self:GrooveboxTui|Bsp::n(
Fixed::h(2, SequencerStatusBar::from(&self.sequencer)),
Bsp::s(Fixed::h(self.split, &self.sequencer), &self.sampler),
));
audio!(|self:GrooveboxTui,_client,_process|Control::Continue);
handle!(<Tui>|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input));
input_to_command!(GrooveboxCommand: <Tui>|state:GrooveboxTui,input|match input.event() {
// load sample
key_pat!(Char('l')) => GrooveboxCommand::Sampler(SamplerCommand::Import),
_ => GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?),
key_pat!(Char('l')) => GrooveboxCommand::Sampler(SamplerCommand::Import(FileBrowserCommand::Begin)),
_ => match state.focus {
GrooveboxFocus::Sequencer =>
GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?),
GrooveboxFocus::Sampler =>
GrooveboxCommand::Sampler(SamplerCommand::input_to_command(&state.sampler, input)?),
}
});
command!(|self:GrooveboxCommand,state:GrooveboxTui|match self {
GrooveboxCommand::Sequencer(command) =>
command.execute(&mut state.sequencer)?.map(GrooveboxCommand::Sequencer),

View file

@ -9,18 +9,32 @@ use symphonia::core::audio::SampleBuffer;
use symphonia::default::get_codecs;
pub enum SamplerCommand {
Import
Import(FileBrowserCommand),
SetName(String),
SetNote(u7, Arc<RwLock<Sample>>),
SetGain(f32),
NoteOn(u7, u7),
NoteOff(u7)
}
input_to_command!(SamplerCommand: <Tui>|state:SamplerTui,input|match input.event() {
input_to_command!(SamplerCommand:<Tui>|state:SamplerTui,input|match state.mode {
Some(SamplerMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?),
_ => return None
});
input_to_command!(FileBrowserCommand:<Tui>|state:SamplerTui,input|match input {
_ => return None
});
command!(|self:SamplerCommand,state:SamplerTui|match self {
SamplerCommand::Import => {
SamplerCommand::Import(FileBrowserCommand::Begin) => {
let voices = &state.state.voices;
let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?));
None
},
_ => todo!()
});
command!(|self:FileBrowserCommand,state:SamplerTui|match self {
_ => todo!()
});
impl TryFrom<&Arc<RwLock<JackClient>>> for SamplerTui {
type Error = Box<dyn std::error::Error>;
@ -31,10 +45,9 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for SamplerTui {
jack.read().unwrap().client().register_port("outR", AudioOut::default())?,
];
Ok(Self {
focus: SamplerFocus::_TODO,
cursor: (0, 0),
editing: None,
modal: Default::default(),
mode: None,
state: Sampler {
jack: jack.clone(),
name: "Sampler".into(),
@ -51,47 +64,54 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for SamplerTui {
}
pub struct SamplerTui {
pub focus: SamplerFocus,
pub state: Sampler,
pub cursor: (usize, usize),
pub editing: Option<Arc<RwLock<Sample>>>,
pub modal: Arc<Mutex<Option<Box<dyn Exit + Send>>>>,
pub mode: Option<SamplerMode>,
}
/// Sections that may be focused
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum SamplerFocus {
_TODO
enum SamplerMode {
// Load sample from path
Import(usize, FileBrowser),
}
audio!(|self: SamplerTui, _client, _scope|Control::Continue);
render!(<Tui>|self: SamplerTui|{
Fill::wh(lay!([
Fill::wh(render(|to|{ // border
let [x, y, w, h] = to.area();
let green = Some(Style::default().fg(Color::Green));
to.blit(&"🭚", x, y, green);
to.blit(&"🭥", x + w.saturating_sub(1), y, green);
to.blit(&"🬿", x, y + h.saturating_sub(1), green);
to.blit(&"🭊", x + w.saturating_sub(1), y + h.saturating_sub(1), green);
Ok(())
})),
col!([
Tui::push_x(2, row!([
Tui::bold(true, "Sampler"), "|Voices: ",
&format!("{}", self.state.voices.read().unwrap().len()),
])),
Tui::either(self.state.unmapped.len() > 0,
col!((index, sample) in self.state.unmapped.iter().enumerate() =>
{ &format!("| Unmapped #{index}") }), "· No unmapped samples."),
Tui::either(self.state.mapped.len() > 0,
col!((index, sample) in self.state.unmapped.iter().enumerate() =>
{ &format!("| Mapped #{index}") }), "· No mapped samples."),
])
]))
audio!(|self: SamplerTui, _client, scope|{
self.state.process_midi_in(scope);
self.state.clear_output_buffer();
self.state.process_audio_out(scope);
self.state.write_output_buffer(scope);
Control::Continue
});
render!(<Tui>|self: SamplerTui|Tui::min_y(10, Fill::wh(lay!([
Fill::wh(render(|to|{ // border
let [x, y, w, h] = to.area();
let green = Some(Style::default().fg(Color::Green));
to.blit(&"🭚", x, y, green);
to.blit(&"🭥", x + w.saturating_sub(1), y, green);
to.blit(&"🬿", x, y + h.saturating_sub(1), green);
to.blit(&"🭊", x + w.saturating_sub(1), y + h.saturating_sub(1), green);
Ok(())
})),
col!(|add|{
add(&Tui::push_x(2, row!([
Tui::bold(true, "Sampler"), "|Voices: ",
&format!("{}", self.state.voices.read().unwrap().len()),
])))?;
if let Some(SamplerMode::Import(_, browser)) = self.mode.as_ref() {
add(&browser)
} else {
add(&row!([
" ",
col!(note in 35..=42 => {
&format!("{note:>3} ---------------- +0.0dB")
}),
]))
}
}),
]))));
handle!(<Tui>|self:SamplerTui,from|{
let cursor = &mut self.cursor;
@ -112,11 +132,11 @@ handle!(<Tui>|self:SamplerTui,from|{
},
key_pat!(KeyCode::Char('a')) => {
let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
*self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?));
self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?));
unmapped.push(sample);
},
key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() {
*self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?));
self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?));
},
key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() {
self.editing = Some(sample.clone());

View file

@ -154,7 +154,7 @@ render!(<Tui>|self: SequencerTui|{
PhraseSelector::play_phrase(&self.player),
PhraseSelector::next_phrase(&self.player),
]), transport]);
with_size(with_status(col!([ toolbar, editor, ])))
Tui::min_y(15, with_size(with_status(col!([ toolbar, editor, ]))))
});
audio!(|self:SequencerTui, client, scope|{
// Start profiling cycle