wip: edn project format

This commit is contained in:
🪞👃🪞 2024-07-08 02:41:04 +03:00
parent 14d9116c7c
commit e2a842492e
12 changed files with 401 additions and 65 deletions

19
Cargo.lock generated
View file

@ -212,6 +212,15 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "clojure-reader"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fe72db90a90a91de4a9fbd79542538caa0445ebdebcd3112589cab4c1e0e10b"
dependencies = [
"ordered-float",
]
[[package]]
name = "cmake"
version = "0.1.50"
@ -680,6 +689,15 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "ordered-float"
version = "4.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ff2cf528c6c03d9ed653d6c4ce1dc0582dc4af309790ad92f07c1cd551b0be"
dependencies = [
"num-traits",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
@ -1032,6 +1050,7 @@ dependencies = [
"backtrace",
"better-panic",
"clap",
"clojure-reader",
"crossterm",
"fraction",
"jack",

View file

@ -23,3 +23,4 @@ atomic_float = "1.0.0"
fraction = "0.15.3"
rlsf = "0.2.1"
r8brain-rs = "0.3.5"
clojure-reader = "0.1.0"

View file

@ -0,0 +1,13 @@
TODO:
* Focus transport to set BPM/sync/quant with `.,`
* Sample browser
* Envelope
* Stretch sample to PPM
* Move clip/track/scene
* Fix next/prev clip
* needs_update
* Buffer sequencer
* Buffer chain view
* Device ports
* LV2 GUI

67
demos/project.edn Normal file
View file

@ -0,0 +1,67 @@
(track { :name "Drums" :gain +0.0 }
(sampler { :name "DrumKit1" :dir "/home/user/Lab/Music/pak" }
(sample { :midi 34 :name "808" :file "808.wav" })
(sample { :midi 35 :name "KC1" :file "kik.wav" })
(sample { :midi 36 :name "KC2" :file "kik2.wav" })
(sample { :midi 38 :name "SN1" :file "sna.wav" })
(sample { :midi 40 :name "SN2" :file "sna2.wav" })
(sample { :midi 42 :name "HH1" :file "chh.wav" })
(sample { :midi 44 :name "HH2" :file "chh2.wav" }))
(phrase { :name "4 kicks" :beats 4 :steps 16 }
(:00 (36 128))
(:04 (36 128))
(:08 (36 128))
(:12 (36 128)))
)
;(phrase {
;:name "5 kicks"
;:beats 4
;} (:00 (36 128))
;(:04 (36 128))
;(:08 (36 128))
;(:12 (36 128))
;(:14 (36 128)))
;(phrase {
;:name "D Beat"
;:beats 4
;} (:00 (:44 :100) (:34 :100) (:35 :100))
;(:02 (:42 :100) )
;(:04 (:42 :080) (:38 :100) )
;(:06 (:44 :120) )
;(:08 (:42 :100) (:34 :100) (:35 :100))
;(:10 (:42 :100) (:34 :100) (:35 :100))
;(:12 (:44 :100) (:40 :100) )
;(:13 (:44 :100) )
;(:14 (:44 :100) )
;(:15 (:42 :100) ))
;(phrase {
;:name "Garage"
;:beats 4
;} (:00 (44 100) (36 100) (35 100))
;(:01 (44 100))
;(:02 (44 100) (35 100))
;(:03 (44 100))
;(:04 (44 100) (40 100))
;(:06 (44 100))
;(:07 (44 100) (34 100))
;(:09 (44 100))
;(:10 (44 100))
;(:11 (35 100) (36 100))
;(:12 (44 100) (40 100))
;(:14 (44 100))))
;(track "Bass"
;(lv2-plugin "Odin2")
;(phrase { :name "Empty" :beats 4 })
;(phrase { :name "Empty" :beats 4 }))
;(track "Lead"
;(lv2-plugin "Odin2")
;(phrase { :name "Empty" :beats 4 })
;(phrase { :name "Empty" :beats 4 }))

View file

@ -34,13 +34,13 @@ handle!(App |self, e| {
});
const KEYMAP_FOCUS: &'static [KeyBinding<App>] = keymap!(App {
[Tab, NONE, "focus_next", "focus next area", focus_next],
[Tab, SHIFT, "focus_prev", "focus previous area", focus_prev],
[Esc, NONE, "focus_exit", "unfocus", |app: &mut App|{
[Tab, NONE, "focus_next", "focus next area", focus_next],
[Tab, SHIFT, "focus_prev", "focus previous area", focus_prev],
[Esc, NONE, "focus_exit", "unfocus", |app: &mut App|{
app.entered = false;
Ok(true)
}],
[Enter, NONE, "focus_enter", "activate item at cursor", |app: &mut App|{
[Enter, NONE, "focus_enter", "activate item at cursor", |app: &mut App|{
app.entered = true;
Ok(true)
}],
@ -50,8 +50,8 @@ const KEYMAP: &'static [KeyBinding<App>] = keymap!(App {
[F(1), NONE, "help_toggle", "toggle help", |_: &mut App| {Ok(true)}],
[Up, NONE, "focus_prev", "focus previous area", focus_prev],
[Down, NONE, "focus_next", "focus next area", focus_next],
[Up, NONE, "focus_prev", "focus previous area", focus_prev],
[Down, NONE, "focus_next", "focus next area", focus_next],
[Char(' '), NONE, "play_toggle", "play or pause", |app: &mut App| {
app.toggle_play()?;
@ -175,13 +175,52 @@ const KEYMAP_GRID: &'static [KeyBinding<App>] = keymap!(App {
true
}
)],
[Char('.'), NONE, "grid_increment", "increment item at cursor", clip_next],
[Char(','), NONE, "grid_decrement", "decrement item at cursor", clip_prev],
[Char('.'), NONE, "grid_increment", "set next clip at cursor", |app: &mut App| {
Ok(true)
}],
[Char(','), NONE, "grid_decrement", "set previous clip at cursor", |app: &mut App| {
Ok(true)
}],
[Char('`'), NONE, "grid_mode_switch", "switch the display mode", |app: &mut App| {
app.grid_mode = !app.seq_mode;
Ok(true)
}],
});
fn clip_next (_: &mut App) -> Usually<bool> { Ok(true) }
//fn clip_next (state: &mut Launcher) -> Usually<bool> {
//if state.cursor.0 >= 1 && state.cursor.1 >= 1 {
//let scene_id = state.cursor.1 - 1;
//let clip_id = state.cursor.0 - 1;
//let scene = &mut state.scenes[scene_id];
//scene.clips[clip_id] = match scene.clips[clip_id] {
//None => Some(0),
//Some(i) => if i >= state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1) {
//None
//} else {
//Some(i + 1)
//}
//};
//}
//Ok(true)
//}
fn clip_prev (_: &mut App) -> Usually<bool> { Ok(true) }
//fn clip_prev (state: &mut Launcher) -> Usually<bool> {
//if state.cursor.0 >= 1 && state.cursor.1 >= 1 {
//let scene_id = state.cursor.1 - 1;
//let clip_id = state.cursor.0 - 1;
//let scene = &mut state.scenes[scene_id];
//scene.clips[clip_id] = match scene.clips[clip_id] {
//None => Some(state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1)),
//Some(i) => if i == 0 {
//None
//} else {
//Some(i - 1)
//}
//};
//}
//Ok(true)
//}
const KEYMAP_SEQUENCER: &'static [KeyBinding<App>] = keymap!(App {
[Up, NONE, "seq_cursor_up", "move cursor up", |app: &mut App| {
@ -257,46 +296,11 @@ fn increment (app: &mut App) -> Usually<bool> {
Ok(false)
}
fn clip_next (_: &mut App) -> Usually<bool> { Ok(true) }
//fn clip_next (state: &mut Launcher) -> Usually<bool> {
//if state.cursor.0 >= 1 && state.cursor.1 >= 1 {
//let scene_id = state.cursor.1 - 1;
//let clip_id = state.cursor.0 - 1;
//let scene = &mut state.scenes[scene_id];
//scene.clips[clip_id] = match scene.clips[clip_id] {
//None => Some(0),
//Some(i) => if i >= state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1) {
//None
//} else {
//Some(i + 1)
//}
//};
//}
//Ok(true)
//}
fn decrement (app: &mut App) -> Usually<bool> {
Ok(false)
}
fn clip_prev (_: &mut App) -> Usually<bool> { Ok(true) }
//fn clip_prev (state: &mut Launcher) -> Usually<bool> {
//if state.cursor.0 >= 1 && state.cursor.1 >= 1 {
//let scene_id = state.cursor.1 - 1;
//let clip_id = state.cursor.0 - 1;
//let scene = &mut state.scenes[scene_id];
//scene.clips[clip_id] = match scene.clips[clip_id] {
//None => Some(state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1)),
//Some(i) => if i == 0 {
//None
//} else {
//Some(i - 1)
//}
//};
//}
//Ok(true)
//}
fn delete (app: &mut App) -> Usually<bool> {
match app.section {
AppSection::Grid => delete_track(app),

222
src/edn.rs Normal file
View file

@ -0,0 +1,222 @@
use crate::{core::*, model::*, App};
use clojure_reader::edn::Edn;
impl App {
pub fn load_edn (&mut self, mut src: &str) {
loop {
match clojure_reader::edn::read(src) {
Ok((edn, rest)) => {
self.load_edn_one(edn);
if rest.len() > 0 {
src = rest;
} else {
break
}
},
Err(e) => {
panic!("{e:?}");
}
}
}
}
fn load_edn_one <'e> (&mut self, edn: Edn<'e>) {
match edn {
Edn::List(items) => {
let head = items.get(0);
match head {
Some(Edn::Symbol("track")) => Track::load_edn(self, &items[1..]),
_ => panic!("unexpected edn: {head:?}")
}
},
_ => {
panic!("unexpected edn: {edn:?}");
}
};
}
}
impl Track {
fn load_edn <'a, 'e> (app: &'a mut App, items: &[Edn<'e>]) -> Usually<&'a mut Self> {
let ppq = app.timebase.ppq() as usize;
let mut name = String::new();
let mut gain = 0.0f64;
let mut devices: Vec<JackDevice> = vec![];
let mut phrases: Vec<Phrase> = vec![];
for edn in items[1..].iter() {
match edn {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key("name")) {
name = String::from(*n);
}
if let Some(Edn::Double(g)) = map.get(&Edn::Key("gain")) {
gain = f64::from(*g)
}
},
Edn::List(items) => match items.get(0) {
Some(Edn::Symbol("phrase")) => {
phrases.push(Phrase::load_edn(ppq, items)?)
},
Some(Edn::Symbol("sampler")) => {
devices.push(Sampler::load_edn(items)?)
},
Some(Edn::Symbol("lv2")) => {
devices.push(LV2Plugin::load_edn(items)?)
},
None => panic!("empty list track {name}"),
_ => panic!("unexpected in track {name}: {:?}", items.get(0).unwrap())
},
_ => {}
}
}
app.add_track_with_cb(Some(name.as_str()), move|_, track|{
for phrase in phrases {
track.phrases.push(phrase);
}
for device in devices {
track.add_device(device);
}
Ok(())
})
}
}
impl Phrase {
fn load_edn <'e> (ppq: usize, items: &[Edn<'e>]) -> Usually<Self> {
let mut name = String::new();
let mut beats = 0usize;
let mut steps = 0usize;
let mut data = BTreeMap::new();
for edn in items[1..].iter() {
match edn {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key("name")) {
name = String::from(*n);
}
if let Some(Edn::Int(b)) = map.get(&Edn::Key("beats")) {
beats = *b as usize;
}
if let Some(Edn::Int(s)) = map.get(&Edn::Key("steps")) {
steps = *s as usize;
}
},
Edn::List(items) => {
let time = (match items.get(0) {
Some(Edn::Symbol(text)) => text.parse::<f64>()?,
Some(Edn::Int(i)) => *i as f64,
Some(Edn::Double(f)) => f64::from(*f),
_ => panic!("unexpected in phrase {name}: {:?}", items.get(0)),
} * beats as f64 * ppq as f64 / steps as f64) as usize;
for edn in items[1..].iter() {
match edn {
Edn::List(items) => if let (
Some(Edn::Int(key)),
Some(Edn::Int(vel)),
) = (
items.get(0),
items.get(1),
) {
if !data.contains_key(&time) {
data.insert(time, vec![]);
}
data.get_mut(&time).unwrap().push(MidiMessage::NoteOn {
key: u7::from(*key as u8),
vel: u7::from(*vel as u8),
});
} else {
panic!("unexpected list in phrase {name}")
},
_ => panic!("unexpected in phrase {name}: {edn:?}")
}
}
},
_ => panic!("unexpected in phrase {name}: {edn:?}"),
}
}
Ok(Self::new(&name, beats * ppq, Some(data)))
}
}
impl Sampler {
fn load_edn <'e> (items: &[Edn<'e>]) -> Usually<JackDevice> {
let mut name = String::new();
let mut dir = String::new();
let mut samples = BTreeMap::new();
for edn in items[1..].iter() {
match edn {
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(items) => match items.get(0) {
Some(Edn::Symbol("sample")) => {
let (midi, sample) = Sample::load_edn(&items[1..])?;
if let Some(midi) = midi {
samples.insert(midi, sample);
} else {
panic!("sample without midi binding: {}", sample.name);
}
},
_ => panic!("unexpected in sampler {name}: {items:?}")
},
_ => panic!("unexpected in sampler {name}: {edn:?}")
}
}
Self::new(&name, Some(samples))
}
}
impl Sample {
fn load_edn <'e> (items: &[Edn<'e>]) -> Usually<(Option<u7>, Arc<Self>)> {
let mut name = String::new();
let mut file = String::new();
let mut midi = None;
let mut start = 0usize;
for edn in items[1..].iter() {
match edn {
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) = read_sample_data(&file)?;
Ok((midi, Self::new(&name, start, end, data)))
}
}
impl LV2Plugin {
fn load_edn <'e> (items: &[Edn<'e>]) -> Usually<JackDevice> {
let mut name = String::new();
let mut path = String::new();
for edn in items[1..].iter() {
match edn {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key("name")) {
name = String::from(*n);
}
if let Some(Edn::Str(p)) = map.get(&Edn::Key("path")) {
path = String::from(*p);
}
},
_ => panic!("unexpected in sample {name}"),
}
}
Plugin::lv2(&name, &path)
}
}

View file

@ -16,11 +16,13 @@ pub mod core;
pub mod model;
pub mod view;
pub mod jack;
pub mod edn;
use crate::{core::*, model::*};
/// Application entrypoint.
pub fn main () -> Usually<()> {
let mut app = App::default();
// Load config
@ -75,6 +77,8 @@ pub fn main () -> Usually<()> {
state.jack = Some(jack);
state.load_edn(include_str!("../demos/project.edn"));
state.add_track_with_cb(Some("Drums"), |_, track|{
track.add_device_with_cb(Sampler::new("Sampler", Some(BTreeMap::from([

View file

@ -9,7 +9,7 @@ pub mod track;
pub use self::phrase::Phrase;
pub use self::scene::Scene;
pub use self::track::Track;
pub use self::sampler::{Sampler, Sample};
pub use self::sampler::{Sampler, Sample, read_sample_data};
pub use self::mixer::Mixer;
pub use self::plugin::{Plugin, PluginKind, lv2::LV2Plugin};
@ -158,7 +158,7 @@ impl App {
pub fn add_track_with_cb (
&mut self,
name: Option<&str>,
init: impl Fn(&Client, &mut Track)->Usually<()>,
init: impl FnOnce(&Client, &mut Track)->Usually<()>,
) -> Usually<&mut Track> {
let name = name.ok_or_else(||format!("Track {}", self.tracks.len() + 1))?;
let mut track = Track::new(&name, self.client(), None, None)?;

View file

@ -114,22 +114,28 @@ impl Sampler {
/// Load sample from WAV and assign to MIDI note.
#[macro_export] macro_rules! sample {
($note:expr, $name:expr, $src:expr) => {
{
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);
}
(u7::from_int_lossy($note).into(), Sample::new($name, 0, end, data).into())
}
};
($note:expr, $name:expr, $src:expr) => {{
let (end, data) = read_sample_data($src)?;
(
u7::from_int_lossy($note).into(),
Sample::new($name, 0, end, data).into()
)
}};
}
pub fn read_sample_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 struct Sample {

View file

@ -48,7 +48,7 @@ render!(App |self, buf, area| {
Style::default().green()
} else {
Style::default().green().dim()
}).draw(buf, Rect { x, y, width, height: chain.height })
}).draw(buf, chain)
}
let phrase = self.draw_phrase(buf, Rect {
x, y, width, height: height - height / 3

View file

@ -131,7 +131,7 @@ impl<'a> SceneGridViewVertical<'a> {
(0 == self.cursor.0) && (index + 1 == self.cursor.1)
).bold());
"".blit(self.buf, x, y, style);
scene.name.blit(self.buf, x + 1, y, style);
scene.name.blit(self.buf, x + 2, y, style);
}
}

View file

@ -155,11 +155,11 @@ mod horizontal {
cell_bg_tick.set_style(bw);
let mut cell_a = Cell::default();
cell_a.set_char('');
cell_a.set_char('');
cell_a.set_style(wh);
let mut cell_b = Cell::default();
cell_b.set_char('');
cell_b.set_char('');
cell_b.set_style(wh);
let mut cell_ab = Cell::default();