mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-01-12 02:56:42 +01:00
big ass refactor (rip client)
This commit is contained in:
parent
94c1f83ef2
commit
8c3cf53c67
56 changed files with 2232 additions and 1891 deletions
102
src/config.rs
102
src/config.rs
|
|
@ -1,29 +1,85 @@
|
|||
use crate::core::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{File, create_dir_all};
|
||||
|
||||
const CONFIG_FILE_NAME: &'static str = "tek.toml";
|
||||
const PROJECT_FILE_NAME: &'static str = "project.toml";
|
||||
|
||||
pub fn create_dirs (xdg: µxdg::XdgApp) -> Result<(), Box<dyn Error>> {
|
||||
use std::{path::Path,fs::{File,create_dir_all}};
|
||||
let config_dir = xdg.app_config()?;
|
||||
if !Path::new(&config_dir).exists() {
|
||||
println!("Creating {config_dir:?}");
|
||||
create_dir_all(&config_dir)?;
|
||||
}
|
||||
let config_path = config_dir.join(CONFIG_FILE_NAME);
|
||||
if !Path::new(&config_path).exists() {
|
||||
println!("Creating {config_path:?}");
|
||||
File::create_new(&config_path)?;
|
||||
}
|
||||
let data_dir = xdg.app_data()?;
|
||||
if !Path::new(&data_dir).exists() {
|
||||
println!("Creating {data_dir:?}");
|
||||
create_dir_all(&data_dir)?;
|
||||
}
|
||||
let project_path = data_dir.join(PROJECT_FILE_NAME);
|
||||
if !Path::new(&project_path).exists() {
|
||||
println!("Creating {project_path:?}");
|
||||
File::create_new(&project_path)?;
|
||||
}
|
||||
Ok(())
|
||||
pub struct AppPaths {
|
||||
config_dir: PathBuf,
|
||||
config_file: PathBuf,
|
||||
data_dir: PathBuf,
|
||||
project_file: PathBuf,
|
||||
}
|
||||
impl AppPaths {
|
||||
pub fn new (xdg: &XdgApp) -> Usually<Self> {
|
||||
let config_dir = PathBuf::from(xdg.app_config()?);
|
||||
let config_file = PathBuf::from(config_dir.join(CONFIG_FILE_NAME));
|
||||
let data_dir = PathBuf::from(xdg.app_data()?);
|
||||
let project_file = PathBuf::from(data_dir.join(PROJECT_FILE_NAME));
|
||||
Ok(Self { config_dir, config_file, data_dir, project_file })
|
||||
}
|
||||
pub fn should_create (&self) -> bool {
|
||||
for path in [
|
||||
&self.config_dir,
|
||||
&self.config_file,
|
||||
&self.data_dir,
|
||||
&self.project_file,
|
||||
].iter() {
|
||||
if !Path::new(path).exists() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
pub fn create (&self) -> Usually<()> {
|
||||
for dir in [&self.config_dir, &self.data_dir].iter() {
|
||||
if !Path::new(dir).exists() {
|
||||
create_dir_all(&dir)?;
|
||||
}
|
||||
}
|
||||
for file in [&self.config_file, &self.project_file].iter() {
|
||||
if !Path::new(file).exists() {
|
||||
File::create_new(&file)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SetupModal(pub Option<Arc<XdgApp>>);
|
||||
|
||||
render!(SetupModal |self, buf, area| {
|
||||
let lines = [
|
||||
(" ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" Welcome to TEK. ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" Press ENTER to create the ", Style::default().white().on_black().not_dim()),
|
||||
(" following directories: ", Style::default().white().on_black().not_dim()),
|
||||
(" ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" Configuration directory: ", Style::default().white().on_black().not_dim()),
|
||||
(" ~/.config/tek ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" ", Style::default().white().on_black().not_dim()),
|
||||
(" Data directory: ", Style::default().white().on_black().not_dim()),
|
||||
(" ~/.local/share/tek ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" ", Style::default().white().on_black().not_dim()),
|
||||
];
|
||||
let width = lines[0].0.len() as u16;
|
||||
let x = area.x + (area.width - width) / 2;
|
||||
for (i, (line, style)) in lines.iter().enumerate() {
|
||||
line.blit(buf, x, area.y + area.height / 2 - (lines.len() / 2) as u16 + i as u16, Some(*style));
|
||||
}
|
||||
Ok(area)
|
||||
});
|
||||
handle!(SetupModal |self, e| {
|
||||
if let AppEvent::Input(::crossterm::event::Event::Key(KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
..
|
||||
})) = e {
|
||||
AppPaths::new(&self.0.as_ref().unwrap())?.create()?;
|
||||
self.0 = None;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
});
|
||||
|
|
|
|||
9
src/control.rs
Normal file
9
src/control.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
pub mod chain;
|
||||
pub mod focus;
|
||||
pub mod launcher;
|
||||
pub mod mixer;
|
||||
pub mod plugin;
|
||||
pub mod sampler;
|
||||
pub mod sequencer;
|
||||
|
||||
pub use self::focus::*;
|
||||
72
src/control/chain.rs
Normal file
72
src/control/chain.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
use crate::{core::*, model::*};
|
||||
use super::focus::*;
|
||||
|
||||
pub fn handle (state: &mut Chain, event: &AppEvent) -> Usually<bool> {
|
||||
Ok(handle_focus(state, event, keymap!(Chain {
|
||||
[Up, NONE, "focus_up", "focus row above",
|
||||
|s: &mut Chain|s.handle_focus(&FocusEvent::Backward)],
|
||||
[Down, NONE, "focus_down", "focus row below",
|
||||
|s: &mut Chain|s.handle_focus(&FocusEvent::Forward)],
|
||||
[Enter, NONE, "focus_down", "focus row below",
|
||||
|s: &mut Chain|s.handle_focus(&FocusEvent::Inward)],
|
||||
[Esc, NONE, "focus_down", "focus row below",
|
||||
|s: &mut Chain|s.handle_focus(&FocusEvent::Outward)],
|
||||
}))? || handle_keymap(state, event, keymap!(Chain {
|
||||
[Char('a'), NONE, "add_device", "add a device", add_device]
|
||||
}))?)
|
||||
}
|
||||
|
||||
fn add_device (state: &mut Chain) -> Usually<bool> {
|
||||
state.adding = true;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
impl Focus for Chain {
|
||||
fn unfocus (&mut self) {
|
||||
self.focused = false
|
||||
}
|
||||
fn focused (&self) -> Option<&Box<dyn Device>> {
|
||||
match self.focused {
|
||||
true => self.items.get(self.focus),
|
||||
false => None
|
||||
}
|
||||
}
|
||||
fn focused_mut (&mut self) -> Option<&mut Box<dyn Device>> {
|
||||
match self.focused {
|
||||
true => self.items.get_mut(self.focus),
|
||||
false => None
|
||||
}
|
||||
}
|
||||
fn handle_focus (&mut self, event: &FocusEvent) -> Usually<bool> {
|
||||
Ok(match event {
|
||||
FocusEvent::Backward => {
|
||||
if self.focus == 0 {
|
||||
self.focus = self.items.len();
|
||||
}
|
||||
self.focus = self.focus - 1;
|
||||
true
|
||||
},
|
||||
FocusEvent::Forward => {
|
||||
self.focus = self.focus + 1;
|
||||
if self.focus >= self.items.len() {
|
||||
self.focus = 0;
|
||||
}
|
||||
true
|
||||
},
|
||||
FocusEvent::Inward => {
|
||||
self.focused = true;
|
||||
self.items[self.focus].handle(&AppEvent::Focus)?;
|
||||
true
|
||||
},
|
||||
FocusEvent::Outward => {
|
||||
if self.focused {
|
||||
self.focused = false;
|
||||
self.items[self.focus].handle(&AppEvent::Blur)?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
use crate::core::*;
|
||||
use super::*;
|
||||
use crate::{core::*, view::*};
|
||||
|
||||
pub trait Focus {
|
||||
fn unfocus (&mut self);
|
||||
|
|
@ -187,3 +186,4 @@ impl Focus for FocusRow {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,25 +1,22 @@
|
|||
use crate::core::*;
|
||||
use super::*;
|
||||
use crate::{core::*,model::*};
|
||||
|
||||
pub fn handle (state: &mut Launcher, event: &AppEvent) -> Usually<bool> {
|
||||
//if let Some(ref modal) = state.modal {
|
||||
//if modal.handle_with_state(state, event)? {
|
||||
//return Ok(true)
|
||||
//}
|
||||
//}
|
||||
Ok(handle_keymap(state, event, KEYMAP)? || match state.view {
|
||||
LauncherView::Tracks => handle_keymap(state, event, KEYMAP_TRACKS)?,
|
||||
LauncherView::Sequencer => {
|
||||
LauncherMode::Tracks => {
|
||||
handle_keymap(state, event, KEYMAP_TRACKS)?
|
||||
},
|
||||
LauncherMode::Sequencer => {
|
||||
let i = state.col().saturating_sub(1);
|
||||
if let Some(track) = state.tracks.get_mut(i) {
|
||||
crate::device::sequencer::handle(&mut *track.sequencer.state(), event)?
|
||||
crate::control::sequencer::handle(&mut track.sequencer, event)?
|
||||
} else {
|
||||
true
|
||||
}
|
||||
},
|
||||
LauncherView::Chains => {
|
||||
LauncherMode::Chains => {
|
||||
let i = state.col().saturating_sub(1);
|
||||
if let Some(track) = state.tracks.get_mut(i) {
|
||||
crate::device::chain::handle(&mut *track.chain.state(), event)?
|
||||
crate::control::chain::handle(&mut track.chain, event)?
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
|
@ -50,35 +47,38 @@ pub const KEYMAP_TRACKS: &'static [KeyBinding<Launcher>] = keymap!(Launcher {
|
|||
[Enter, NONE, "activate", "activate item at cursor", activate],
|
||||
});
|
||||
fn duplicate (_: &mut Launcher) -> Usually<bool> {
|
||||
unimplemented!();
|
||||
Ok(true)
|
||||
}
|
||||
fn activate (state: &mut Launcher) -> Usually<bool> {
|
||||
if let (
|
||||
Some((_scene_id, scene)),
|
||||
Some((track_id, track)),
|
||||
) = (state.scene(), state.track()) {
|
||||
// Launch clip
|
||||
if let Some(phrase_id) = scene.clips.get(track_id) {
|
||||
track.sequencer.state().sequence = *phrase_id;
|
||||
}
|
||||
if state.playing == TransportState::Stopped {
|
||||
state.transport.start()?;
|
||||
state.playing = TransportState::Starting;
|
||||
}
|
||||
} else if let Some((_scene_id, scene)) = state.scene() {
|
||||
// Launch scene
|
||||
for (track_id, track) in state.tracks.iter().enumerate() {
|
||||
if let Some(phrase_id) = scene.clips.get(track_id) {
|
||||
track.sequencer.state().sequence = *phrase_id;
|
||||
}
|
||||
}
|
||||
if state.playing == TransportState::Stopped {
|
||||
state.transport.start()?;
|
||||
state.playing = TransportState::Starting;
|
||||
}
|
||||
} else if let Some((_track_id, _track)) = state.track() {
|
||||
// Rename track?
|
||||
}
|
||||
|
||||
fn activate (_: &mut Launcher) -> Usually<bool> {
|
||||
unimplemented!();
|
||||
//if let (
|
||||
//Some((_scene_id, scene)),
|
||||
//Some((track_id, track)),
|
||||
//) = (state.scene_mut(), state.track_mut()) {
|
||||
//// Launch clip
|
||||
//if let Some(phrase_id) = scene.clips.get(track_id) {
|
||||
//track.sequencer.sequence = *phrase_id;
|
||||
//}
|
||||
//if state.playing == TransportState::Stopped {
|
||||
//state.transport.start()?;
|
||||
//state.playing = TransportState::Starting;
|
||||
//}
|
||||
//} else if let Some((_scene_id, scene)) = state.scene() {
|
||||
//// Launch scene
|
||||
//for (track_id, track) in state.tracks.iter().enumerate() {
|
||||
//if let Some(phrase_id) = scene.clips.get(track_id) {
|
||||
//track.sequencer.sequence = *phrase_id;
|
||||
//}
|
||||
//}
|
||||
//if state.playing == TransportState::Stopped {
|
||||
//state.transport.start()?;
|
||||
//state.playing = TransportState::Starting;
|
||||
//}
|
||||
//} else if let Some((_track_id, _track)) = state.track() {
|
||||
//// Rename track?
|
||||
//}
|
||||
|
||||
//let track = state.active_track().unwrap();
|
||||
//let scene = state.active_scene();
|
||||
|
|
@ -104,15 +104,18 @@ fn activate (state: &mut Launcher) -> Usually<bool> {
|
|||
//}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn rename (_: &mut Launcher) -> Usually<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn add_track (state: &mut Launcher) -> Usually<bool> {
|
||||
let name = format!("Track {}", state.tracks.len() + 1);
|
||||
state.tracks.push(Track::new(&name, &state.timebase, None, None)?);
|
||||
state.cursor.0 = state.tracks.len();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn delete_track (state: &mut Launcher) -> Usually<bool> {
|
||||
if state.tracks.len() > 0 && state.cursor.0 >= 1 {
|
||||
state.tracks.remove(state.cursor.0 - 1);
|
||||
|
|
@ -120,44 +123,50 @@ fn delete_track (state: &mut Launcher) -> Usually<bool> {
|
|||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn cursor_up (state: &mut Launcher) -> Usually<bool> {
|
||||
state.dec_row();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn cursor_down (state: &mut Launcher) -> Usually<bool> {
|
||||
state.inc_row();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn cursor_left (state: &mut Launcher) -> Usually<bool> {
|
||||
state.dec_col();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn cursor_right (state: &mut Launcher) -> Usually<bool> {
|
||||
state.inc_col();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn toggle_help (state: &mut Launcher) -> Usually<bool> {
|
||||
state.show_help = !state.show_help;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn focus_next (state: &mut Launcher) -> Usually<bool> {
|
||||
match state.view {
|
||||
LauncherView::Tracks => { state.view = LauncherView::Sequencer; },
|
||||
LauncherView::Sequencer => { state.view = LauncherView::Chains; },
|
||||
LauncherView::Chains => { state.view = LauncherView::Tracks; },
|
||||
_ => {},
|
||||
LauncherMode::Tracks => { state.view = LauncherMode::Sequencer; },
|
||||
LauncherMode::Sequencer => { state.view = LauncherMode::Chains; },
|
||||
LauncherMode::Chains => { state.view = LauncherMode::Tracks; },
|
||||
};
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn focus_prev (state: &mut Launcher) -> Usually<bool> {
|
||||
match state.view {
|
||||
LauncherView::Tracks => { state.view = LauncherView::Chains; },
|
||||
LauncherView::Chains => { state.view = LauncherView::Sequencer; },
|
||||
LauncherView::Sequencer => { state.view = LauncherView::Tracks; },
|
||||
_ => {},
|
||||
LauncherMode::Tracks => { state.view = LauncherMode::Chains; },
|
||||
LauncherMode::Chains => { state.view = LauncherMode::Sequencer; },
|
||||
LauncherMode::Sequencer => { state.view = LauncherMode::Tracks; },
|
||||
};
|
||||
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;
|
||||
|
|
@ -165,7 +174,7 @@ fn clip_next (state: &mut Launcher) -> Usually<bool> {
|
|||
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.state().phrases.len().saturating_sub(1) {
|
||||
Some(i) => if i >= state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1) {
|
||||
None
|
||||
} else {
|
||||
Some(i + 1)
|
||||
|
|
@ -174,13 +183,14 @@ fn clip_next (state: &mut Launcher) -> 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.state().phrases.len().saturating_sub(1)),
|
||||
None => Some(state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1)),
|
||||
Some(i) => if i == 0 {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -190,6 +200,7 @@ fn clip_prev (state: &mut Launcher) -> Usually<bool> {
|
|||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn play_toggle (s: &mut Launcher) -> Usually<bool> {
|
||||
s.playing = match s.playing {
|
||||
TransportState::Stopped => {
|
||||
|
|
@ -207,15 +218,19 @@ fn play_toggle (s: &mut Launcher) -> Usually<bool> {
|
|||
//fn play_start (_: &mut Launcher) -> Usually<bool> {
|
||||
//unimplemented!()
|
||||
//}
|
||||
|
||||
fn record_toggle (s: &mut Launcher) -> Usually<bool> {
|
||||
s.sequencer().map(|mut s|s.recording = !s.recording);
|
||||
s.sequencer_mut().map(|s|s.recording = !s.recording);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn overdub_toggle (s: &mut Launcher) -> Usually<bool> {
|
||||
s.sequencer().map(|mut s|s.overdub = !s.overdub);
|
||||
s.sequencer_mut().map(|s|s.overdub = !s.overdub);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn monitor_toggle (s: &mut Launcher) -> Usually<bool> {
|
||||
s.sequencer().map(|mut s|s.monitoring = !s.monitoring);
|
||||
s.sequencer_mut().map(|s|s.monitoring = !s.monitoring);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
59
src/control/mixer.rs
Normal file
59
src/control/mixer.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use crate::{core::*, model::*};
|
||||
|
||||
pub fn handle (state: &mut Mixer, event: &AppEvent) -> Usually<bool> {
|
||||
if let AppEvent::Input(crossterm::event::Event::Key(event)) = event {
|
||||
|
||||
match event.code {
|
||||
//KeyCode::Char('c') => {
|
||||
//if event.modifiers == KeyModifiers::CONTROL {
|
||||
//state.exit();
|
||||
//}
|
||||
//},
|
||||
KeyCode::Down => {
|
||||
state.selected_track = (state.selected_track + 1) % state.tracks.len();
|
||||
println!("{}", state.selected_track);
|
||||
return Ok(true)
|
||||
},
|
||||
KeyCode::Up => {
|
||||
if state.selected_track == 0 {
|
||||
state.selected_track = state.tracks.len() - 1;
|
||||
} else {
|
||||
state.selected_track = state.selected_track - 1;
|
||||
}
|
||||
println!("{}", state.selected_track);
|
||||
return Ok(true)
|
||||
},
|
||||
KeyCode::Left => {
|
||||
if state.selected_column == 0 {
|
||||
state.selected_column = 6
|
||||
} else {
|
||||
state.selected_column = state.selected_column - 1;
|
||||
}
|
||||
return Ok(true)
|
||||
},
|
||||
KeyCode::Right => {
|
||||
if state.selected_column == 6 {
|
||||
state.selected_column = 0
|
||||
} else {
|
||||
state.selected_column = state.selected_column + 1;
|
||||
}
|
||||
return Ok(true)
|
||||
},
|
||||
_ => {
|
||||
println!("\n{event:?}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// - Meters: propagate clipping:
|
||||
// - If one stage clips, all stages after it are marked red
|
||||
// - If one track clips, all tracks that feed from it are marked red?
|
||||
|
||||
pub const ACTIONS: [(&'static str, &'static str);2] = [
|
||||
("+/-", "Adjust"),
|
||||
("Ins/Del", "Add/remove track"),
|
||||
];
|
||||
69
src/control/plugin.rs
Normal file
69
src/control/plugin.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
use crate::{core::*, model::*};
|
||||
|
||||
pub fn handle (s: &mut Plugin, event: &AppEvent) -> Usually<bool> {
|
||||
handle_keymap(s, event, keymap!(Plugin {
|
||||
|
||||
[Up, NONE, "cursor_up", "move cursor up",
|
||||
|s: &mut Plugin|{
|
||||
if s.selected > 0 {
|
||||
s.selected = s.selected - 1
|
||||
} else {
|
||||
s.selected = match &s.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
||||
_ => 0
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}],
|
||||
|
||||
[Down, NONE, "cursor_down", "move cursor down",
|
||||
|s: &mut Plugin|{
|
||||
s.selected = s.selected + 1;
|
||||
match &s.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => {
|
||||
if s.selected >= port_list.len() {
|
||||
s.selected = 0;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(true)
|
||||
}],
|
||||
|
||||
[Char(','), NONE, "decrement", "decrement value",
|
||||
|s: &mut Plugin|{
|
||||
match s.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||
let index = port_list[s.selected].index;
|
||||
if let Some(value) = instance.control_input(index) {
|
||||
instance.set_control_input(index, value - 0.01);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(true)
|
||||
}],
|
||||
|
||||
[Char('.'), NONE, "increment", "increment value",
|
||||
|s: &mut Plugin|{
|
||||
match s.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||
let index = port_list[s.selected].index;
|
||||
if let Some(value) = instance.control_input(index) {
|
||||
instance.set_control_input(index, value + 0.01);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(true)
|
||||
}],
|
||||
|
||||
[Char('m'), NONE, "toggle_midi_map", "toggle midi map mode",
|
||||
|s: &mut Plugin|{
|
||||
s.mapping = !s.mapping;
|
||||
Ok(true)
|
||||
}]
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
45
src/control/sampler.rs
Normal file
45
src/control/sampler.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use crate::{core::*, model::*};
|
||||
|
||||
pub fn handle (state: &mut Sampler, event: &AppEvent) -> Usually<bool> {
|
||||
Ok(handle_keymap(state, event, KEYMAP)?)
|
||||
}
|
||||
|
||||
pub const KEYMAP: &'static [KeyBinding<Sampler>] = keymap!(Sampler {
|
||||
[Up, NONE, "cursor_up", "move cursor up", cursor_up],
|
||||
[Down, NONE, "cursor_down", "move cursor down", cursor_down],
|
||||
[Char('t'), NONE, "trigger", "play current sample", trigger],
|
||||
[Enter, NONE, "select", "select item under cursor", select],
|
||||
});
|
||||
|
||||
fn cursor_up (state: &mut Sampler) -> Usually<bool> {
|
||||
state.cursor.0 = if state.cursor.0 == 0 {
|
||||
state.samples.len() - 1
|
||||
} else {
|
||||
state.cursor.0 - 1
|
||||
};
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn cursor_down (state: &mut Sampler) -> Usually<bool> {
|
||||
state.cursor.0 = (state.cursor.0 + 1) % state.samples.len();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn trigger (state: &mut Sampler) -> Usually<bool> {
|
||||
for (i, sample) in state.samples.values().enumerate() {
|
||||
if i == state.cursor.0 {
|
||||
state.voices.push(sample.play(0))
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn select (state: &mut Sampler) -> Usually<bool> {
|
||||
for (i, _sample) in state.samples.values().enumerate() {
|
||||
if i == state.cursor.0 {
|
||||
//state.voices.push(sample.play(0))
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
use crate::core::*;
|
||||
use super::*;
|
||||
pub fn handle (s: &mut Sequencer, event: &AppEvent) -> Usually<bool> {
|
||||
handle_keymap(s, event, KEYMAP)
|
||||
use crate::{core::*, model::*};
|
||||
|
||||
pub fn handle (state: &mut Sequencer, event: &AppEvent) -> Usually<bool> {
|
||||
handle_keymap(state, event, KEYMAP)
|
||||
}
|
||||
|
||||
pub const KEYMAP: &'static [KeyBinding<Sequencer>] = keymap!(Sequencer {
|
||||
[Up, NONE, "cursor_up", "move cursor up", cursor_up],
|
||||
[Down, NONE, "cursor_down", "move cursor down", cursor_down],
|
||||
|
|
@ -37,15 +38,18 @@ pub const KEYMAP: &'static [KeyBinding<Sequencer>] = keymap!(Sequencer {
|
|||
[Char('7'), NONE, "seq_7", "Phrase 7", focus_seq(6)],
|
||||
[Char('8'), NONE, "seq_8", "Phrase 8", focus_seq(7)],
|
||||
});
|
||||
|
||||
const fn focus_seq (i: usize) -> impl Fn(&mut Sequencer)->Usually<bool> {
|
||||
move |s: &mut Sequencer| {
|
||||
s.sequence = Some(i);
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn nop (_: &mut Sequencer) -> Usually<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn note_add (s: &mut Sequencer) -> Usually<bool> {
|
||||
if s.sequence.is_none() {
|
||||
return Ok(false)
|
||||
|
|
@ -70,70 +74,82 @@ fn note_add (s: &mut Sequencer) -> Usually<bool> {
|
|||
};
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn note_del (_: &mut Sequencer) -> Usually<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn time_cursor_inc (s: &mut Sequencer) -> Usually<bool> {
|
||||
s.time_cursor = s.time_cursor + 1;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn time_cursor_dec (s: &mut Sequencer) -> Usually<bool> {
|
||||
s.time_cursor = s.time_cursor.saturating_sub(1);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn note_cursor_inc (s: &mut Sequencer) -> Usually<bool> {
|
||||
s.note_cursor = s.note_cursor + 1;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn note_cursor_dec (s: &mut Sequencer) -> Usually<bool> {
|
||||
s.note_cursor = s.note_cursor.saturating_sub(1);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn cursor_up (s: &mut Sequencer) -> Usually<bool> {
|
||||
match s.view {
|
||||
SequencerView::Vertical => time_cursor_dec(s),
|
||||
SequencerView::Horizontal => note_cursor_dec(s),
|
||||
SequencerMode::Vertical => time_cursor_dec(s),
|
||||
SequencerMode::Horizontal => note_cursor_dec(s),
|
||||
_ => Ok(false)
|
||||
}?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn cursor_down (s: &mut Sequencer) -> Usually<bool> {
|
||||
match s.view {
|
||||
SequencerView::Vertical => time_cursor_inc(s),
|
||||
SequencerView::Horizontal => note_cursor_inc(s),
|
||||
SequencerMode::Vertical => time_cursor_inc(s),
|
||||
SequencerMode::Horizontal => note_cursor_inc(s),
|
||||
_ => Ok(false)
|
||||
}?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn cursor_left (s: &mut Sequencer) -> Usually<bool> {
|
||||
match s.view {
|
||||
SequencerView::Vertical => note_cursor_dec(s),
|
||||
SequencerView::Horizontal => time_cursor_dec(s),
|
||||
SequencerMode::Vertical => note_cursor_dec(s),
|
||||
SequencerMode::Horizontal => time_cursor_dec(s),
|
||||
_ => Ok(false)
|
||||
}?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn cursor_right (s: &mut Sequencer) -> Usually<bool> {
|
||||
match s.view {
|
||||
SequencerView::Vertical => note_cursor_inc(s),
|
||||
SequencerView::Horizontal => time_cursor_inc(s),
|
||||
SequencerMode::Vertical => note_cursor_inc(s),
|
||||
SequencerMode::Horizontal => time_cursor_inc(s),
|
||||
_ => Ok(false)
|
||||
}?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn cursor_duration_inc (_: &mut Sequencer) -> Usually<bool> {
|
||||
//s.cursor.2 = s.cursor.2 + 1
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn cursor_duration_dec (_: &mut Sequencer) -> Usually<bool> {
|
||||
//if s.cursor.2 > 0 { s.cursor.2 = s.cursor.2 - 1 }
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn mode_next (s: &mut Sequencer) -> Usually<bool> {
|
||||
s.view = s.view.next();
|
||||
Ok(true)
|
||||
}
|
||||
impl SequencerView {
|
||||
impl SequencerMode {
|
||||
fn next (&self) -> Self {
|
||||
match self {
|
||||
Self::Horizontal => Self::Vertical,
|
||||
|
|
@ -143,12 +159,14 @@ impl SequencerView {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_and_rewind (s: &mut Sequencer) -> Usually<bool> {
|
||||
s.transport.stop()?;
|
||||
s.transport.locate(0)?;
|
||||
s.playing = TransportState::Stopped;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn toggle_play (s: &mut Sequencer) -> Usually<bool> {
|
||||
s.playing = match s.playing {
|
||||
TransportState::Stopped => {
|
||||
|
|
@ -163,24 +181,29 @@ fn toggle_play (s: &mut Sequencer) -> Usually<bool> {
|
|||
};
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn toggle_record (s: &mut Sequencer) -> Usually<bool> {
|
||||
s.recording = !s.recording;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn toggle_overdub (s: &mut Sequencer) -> Usually<bool> {
|
||||
s.overdub = !s.overdub;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn toggle_monitor (s: &mut Sequencer) -> Usually<bool> {
|
||||
s.monitoring = !s.monitoring;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn quantize_next (s: &mut Sequencer) -> Usually<bool> {
|
||||
if s.time_zoom < 64 {
|
||||
s.time_zoom = s.time_zoom * 2;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn quantize_prev (s: &mut Sequencer) -> Usually<bool> {
|
||||
if s.time_zoom > 1 {
|
||||
s.time_zoom = s.time_zoom / 2;
|
||||
|
|
@ -192,6 +215,7 @@ fn zoom_in (s: &mut Sequencer) -> Usually<bool> {
|
|||
s.time_zoom = s.time_zoom / 2;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn zoom_out (s: &mut Sequencer) -> Usually<bool> {
|
||||
s.time_zoom = s.time_zoom * 2;
|
||||
Ok(true)
|
||||
|
|
@ -6,6 +6,8 @@ pub use std::collections::BTreeMap;
|
|||
pub use std::sync::{Arc, Mutex, MutexGuard};
|
||||
pub use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize};
|
||||
pub use std::sync::mpsc::{channel, Sender, Receiver};
|
||||
|
||||
pub use microxdg::XdgApp;
|
||||
pub use ratatui::prelude::*;
|
||||
pub use midly::{MidiMessage, live::LiveEvent, num::u7};
|
||||
pub use crossterm::{ExecutableCommand, QueueableCommand};
|
||||
|
|
@ -13,8 +15,15 @@ pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
|
|||
|
||||
macro_rules! submod { ($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* }; }
|
||||
|
||||
submod!( device handle jack keymap port render run time );
|
||||
submod!( device handle jack keymap midi port render run time );
|
||||
|
||||
pub type Usually<T> = Result<T, Box<dyn Error>>;
|
||||
|
||||
pub use crate::{key, keymap};
|
||||
// Reexport macros:
|
||||
pub use crate::{
|
||||
render,
|
||||
handle,
|
||||
process,
|
||||
keymap,
|
||||
key
|
||||
};
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
use crate::core::*;
|
||||
|
||||
pub trait Component: Render + Handle {}
|
||||
|
||||
impl<T: Render + Handle> Component for T {}
|
||||
|
||||
/// A UI component that may have presence on the JACK grap.
|
||||
pub trait Device: Render + Handle + PortList + Send + Sync {
|
||||
fn boxed (self) -> Box<dyn Device> where Self: Sized + 'static {
|
||||
|
|
@ -25,7 +29,7 @@ impl<T> Handle for DynamicDevice<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> Render for DynamicDevice<T> {
|
||||
impl<T: Send> Render for DynamicDevice<T> {
|
||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
self.render.lock().unwrap()(&*self.state.lock().unwrap(), buf, area)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,26 @@ pub trait Handle {
|
|||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! handle {
|
||||
($T:ty) => {
|
||||
impl Handle for $T {}
|
||||
};
|
||||
($T:ty |$self:ident, $e:ident|$block:tt) => {
|
||||
impl Handle for $T {
|
||||
fn handle (&mut $self, $e: &AppEvent) -> Usually<bool> {
|
||||
$block
|
||||
}
|
||||
}
|
||||
};
|
||||
($T:ty = $handle:path) => {
|
||||
impl Handle for $T {
|
||||
fn handle (&mut self, e: &AppEvent) -> Usually<bool> {
|
||||
$handle(self, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppEvent {
|
||||
/// Terminal input
|
||||
|
|
|
|||
|
|
@ -1,7 +1,51 @@
|
|||
use crate::core::*;
|
||||
|
||||
pub fn jack_run <T> (name: &str, app: &Arc<Mutex<T>>) -> Usually<DynamicAsyncClient>
|
||||
where T: Handle + Process + Send + 'static
|
||||
{
|
||||
let options = ClientOptions::NO_START_SERVER;
|
||||
let (client, _status) = Client::new(name, options)?;
|
||||
Ok(client.activate_async(
|
||||
Notifications(Box::new({
|
||||
let _app = app.clone();
|
||||
move|_event|{
|
||||
// FIXME: this deadlocks
|
||||
//app.lock().unwrap().handle(&event).unwrap();
|
||||
}
|
||||
}) as Box<dyn Fn(AppEvent) + Send + Sync>),
|
||||
ClosureProcessHandler::new(Box::new({
|
||||
let app = app.clone();
|
||||
move|c: &Client, s: &ProcessScope|{
|
||||
app.lock().unwrap().process(c, s)
|
||||
}
|
||||
}) as BoxedProcessHandler)
|
||||
)?)
|
||||
}
|
||||
|
||||
pub trait Process {
|
||||
fn process (&mut self, c: &Client, s: &ProcessScope) -> Control;
|
||||
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! process {
|
||||
($T:ty) => {
|
||||
impl Process for $T {}
|
||||
};
|
||||
($T:ty |$self:ident, $c:ident, $s:ident|$block:tt) => {
|
||||
impl Process for $T {
|
||||
fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control {
|
||||
$block
|
||||
}
|
||||
}
|
||||
};
|
||||
($T:ty = $process:path) => {
|
||||
impl Process for $T {
|
||||
fn process (&mut self, c: &Client, s: &ProcessScope) -> Control {
|
||||
$process(self, c, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type DynamicAsyncClient =
|
||||
|
|
@ -16,23 +60,6 @@ pub type DynamicProcessHandler =
|
|||
pub type BoxedProcessHandler =
|
||||
Box<dyn FnMut(&Client, &ProcessScope)-> Control + Send>;
|
||||
|
||||
pub fn jack_run <T> (name: &str, app: &Arc<Mutex<T>>) -> Usually<DynamicAsyncClient>
|
||||
where T: Handle + Process + Send + 'static
|
||||
{
|
||||
let options = ClientOptions::NO_START_SERVER;
|
||||
let (client, _status) = Client::new(name, options)?;
|
||||
Ok(client.activate_async(
|
||||
Notifications(Box::new({
|
||||
let app = app.clone();
|
||||
move|event|{app.lock().unwrap().handle(&event).unwrap();}
|
||||
}) as Box<dyn Fn(AppEvent) + Send + Sync>),
|
||||
ClosureProcessHandler::new(Box::new({
|
||||
let app = app.clone();
|
||||
move|c: &Client, s: &ProcessScope|{app.lock().unwrap().process(c, s)}
|
||||
}) as BoxedProcessHandler)
|
||||
)?)
|
||||
}
|
||||
|
||||
pub use ::jack::{
|
||||
AsyncClient,
|
||||
AudioIn,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
use crate::core::*;
|
||||
|
||||
pub type PhraseData =
|
||||
BTreeMap<usize, Vec<MidiMessage>>;
|
||||
|
||||
pub type MIDIMessage =
|
||||
Vec<u8>;
|
||||
|
||||
pub type MIDIChunk =
|
||||
[Option<Vec<MIDIMessage>>];
|
||||
|
||||
pub const KEY_WHITE: Style = Style {
|
||||
fg: Some(Color::Gray),
|
||||
bg: None,
|
||||
|
|
@ -24,3 +33,4 @@ pub const KEY_STYLE: [Style;12] = [
|
|||
pub const KEYS_VERTICAL: [&'static str; 6] = [
|
||||
"▄", "▄", "█", "▀", "▀", "▀",
|
||||
];
|
||||
|
||||
|
|
@ -2,34 +2,38 @@ use crate::core::*;
|
|||
use ratatui::widgets::WidgetRef;
|
||||
|
||||
/// Trait for things that render to the display.
|
||||
pub trait Render {
|
||||
pub trait Render: Send {
|
||||
// Render something to an area of the buffer.
|
||||
// Returns area used by component.
|
||||
// This is insufficient but for the most basic dynamic layout algorithms.
|
||||
fn render (&self, _b: &mut Buffer, _a: Rect) -> Usually<Rect> {
|
||||
Ok(Rect { x: 0, y: 0, width: 0, height: 0 })
|
||||
}
|
||||
fn min_width (&self) -> u16 {
|
||||
0
|
||||
}
|
||||
fn max_width (&self) -> u16 {
|
||||
u16::MAX
|
||||
}
|
||||
fn min_height (&self) -> u16 {
|
||||
0
|
||||
}
|
||||
fn max_height (&self) -> u16 {
|
||||
u16::MAX
|
||||
}
|
||||
|
||||
//fn boxed (self) -> Box<dyn Render> where Self: Sized + 'static {
|
||||
//Box::new(self)
|
||||
//}
|
||||
}
|
||||
|
||||
impl<T: Fn(&mut Buffer, Rect) -> Usually<Rect>> Render for T {
|
||||
#[macro_export] macro_rules! render {
|
||||
($T:ty) => {
|
||||
impl Render for $T {}
|
||||
};
|
||||
($T:ty |$self:ident, $buf:ident, $area:ident|$block:tt) => {
|
||||
impl Render for $T {
|
||||
fn render (&$self, $buf: &mut Buffer, $area: Rect) -> Usually<Rect> {
|
||||
$block
|
||||
}
|
||||
}
|
||||
};
|
||||
($T:ty = $render:path) => {
|
||||
impl Render for $T {
|
||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
$render(self, buf, area)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Fn(&mut Buffer, Rect) -> Usually<Rect> + Send> Render for T {
|
||||
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
||||
(*self).render(b, a)
|
||||
(*self)(b, a)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
114
src/core/run.rs
114
src/core/run.rs
|
|
@ -1,28 +1,77 @@
|
|||
use crate::core::*;
|
||||
use better_panic::{Settings, Verbosity};
|
||||
use crossterm::terminal::{
|
||||
EnterAlternateScreen, LeaveAlternateScreen,
|
||||
enable_raw_mode, disable_raw_mode
|
||||
};
|
||||
|
||||
impl<T: Render + Handle + Send + Sized + 'static> Run for T {}
|
||||
|
||||
pub trait Run: Render + Handle + Send + Sized + 'static {
|
||||
fn run (self, callback: Option<impl FnOnce(Arc<Mutex<Self>>)->Usually<()>>) -> Usually<()> {
|
||||
let device = Arc::new(Mutex::new(self));
|
||||
fn run (self, init: Option<impl FnOnce(Arc<Mutex<Self>>)->Usually<()>>) -> Usually<()> {
|
||||
let app = Arc::new(Mutex::new(self));
|
||||
let exited = Arc::new(AtomicBool::new(false));
|
||||
let _input_thread = input_thread(&exited, &device);
|
||||
let _input_thread = input_thread(&exited, &app);
|
||||
terminal_setup()?;
|
||||
panic_hook_setup();
|
||||
let main_thread = main_thread(&exited, &device)?;
|
||||
if let Some(callback) = callback {
|
||||
callback(device);
|
||||
let main_thread = main_thread(&exited, &app)?;
|
||||
if let Some(init) = init {
|
||||
init(app)?;
|
||||
}
|
||||
main_thread.join();
|
||||
main_thread.join().unwrap();
|
||||
terminal_teardown()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Render + Handle + Send + Sized + 'static> Run for T {}
|
||||
/// Set up panic hook
|
||||
pub fn panic_hook_setup () {
|
||||
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
||||
std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo|{
|
||||
stdout().execute(LeaveAlternateScreen).unwrap();
|
||||
disable_raw_mode().unwrap();
|
||||
better_panic_handler(info);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Set up terminal
|
||||
pub fn terminal_setup () -> Usually<()> {
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
/// Cleanup
|
||||
pub fn terminal_teardown () -> Usually<()> {
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main thread render loop
|
||||
pub fn main_thread (
|
||||
exited: &Arc<AtomicBool>,
|
||||
device: &Arc<Mutex<impl Render + Send + 'static>>
|
||||
) -> Usually<JoinHandle<()>> {
|
||||
let exited = exited.clone();
|
||||
let device = device.clone();
|
||||
let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
let sleep = std::time::Duration::from_millis(16);
|
||||
Ok(spawn(move || loop {
|
||||
|
||||
terminal.draw(|frame|{
|
||||
let area = frame.size();
|
||||
let buffer = frame.buffer_mut();
|
||||
device.lock().unwrap().render(buffer, area).expect("Failed to render content.");
|
||||
}).expect("Failed to render frame");
|
||||
|
||||
if exited.fetch_and(true, Ordering::Relaxed) {
|
||||
break
|
||||
}
|
||||
|
||||
std::thread::sleep(sleep);
|
||||
|
||||
}))
|
||||
}
|
||||
/// Spawn thread that listens for user input
|
||||
pub fn input_thread (
|
||||
exited: &Arc<AtomicBool>,
|
||||
|
|
@ -54,52 +103,3 @@ pub fn input_thread (
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Set up terminal
|
||||
pub fn terminal_setup () -> Usually<()> {
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
/// Set up panic hook
|
||||
pub fn panic_hook_setup () {
|
||||
let better_panic_handler = better_panic::Settings::auto()
|
||||
.verbosity(better_panic::Verbosity::Full)
|
||||
.create_panic_handler();
|
||||
std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo|{
|
||||
stdout().execute(LeaveAlternateScreen).unwrap();
|
||||
crossterm::terminal::disable_raw_mode().unwrap();
|
||||
better_panic_handler(info);
|
||||
}));
|
||||
}
|
||||
/// Main thread render loop
|
||||
pub fn main_thread (
|
||||
exited: &Arc<AtomicBool>,
|
||||
device: &Arc<Mutex<impl Render + Send + 'static>>
|
||||
) -> Usually<JoinHandle<()>> {
|
||||
let exited = exited.clone();
|
||||
let device = device.clone();
|
||||
let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
let sleep = std::time::Duration::from_millis(16);
|
||||
Ok(spawn(move || loop {
|
||||
|
||||
terminal.draw(|frame|{
|
||||
let area = frame.size();
|
||||
let buffer = frame.buffer_mut();
|
||||
device.lock().unwrap().render(buffer, area).expect("Failed to render content.");
|
||||
}).expect("Failed to render frame");
|
||||
|
||||
if exited.fetch_and(true, Ordering::Relaxed) {
|
||||
break
|
||||
}
|
||||
|
||||
std::thread::sleep(sleep);
|
||||
|
||||
}))
|
||||
}
|
||||
/// Cleanup
|
||||
pub fn terminal_teardown () -> Usually<()> {
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
use crate::core::*;
|
||||
|
||||
#[derive(Default)]
|
||||
use atomic_float::AtomicF64;
|
||||
#[derive(Debug)]
|
||||
pub struct Timebase {
|
||||
/// Frames per second
|
||||
pub rate: ::atomic_float::AtomicF64,
|
||||
pub rate: AtomicF64,
|
||||
/// Beats per minute
|
||||
pub bpm: ::atomic_float::AtomicF64,
|
||||
pub bpm: AtomicF64,
|
||||
/// Ticks per beat
|
||||
pub ppq: ::atomic_float::AtomicF64,
|
||||
pub ppq: AtomicF64,
|
||||
}
|
||||
impl Default for Timebase {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
rate: 48000f64.into(),
|
||||
bpm: 125f64.into(),
|
||||
ppq: 96f64.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Timebase {
|
||||
pub fn new (rate: f64, bpm: f64, ppq: f64) -> Self {
|
||||
Self { rate: rate.into(), bpm: bpm.into(), ppq: ppq.into() }
|
||||
|
|
@ -146,7 +154,6 @@ impl Timebase {
|
|||
ticks
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)] mod test {
|
||||
use super::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,281 +0,0 @@
|
|||
use crate::core::*;
|
||||
|
||||
mod lv2; pub use lv2::*;
|
||||
mod vst2;
|
||||
mod vst3;
|
||||
|
||||
pub struct Plugin {
|
||||
name: String,
|
||||
path: Option<String>,
|
||||
plugin: Option<PluginKind>,
|
||||
selected: usize,
|
||||
mapping: bool,
|
||||
midi_ins: Vec<Port<MidiIn>>,
|
||||
midi_outs: Vec<Port<MidiOut>>,
|
||||
audio_ins: Vec<Port<AudioIn>>,
|
||||
audio_outs: Vec<Port<AudioOut>>,
|
||||
}
|
||||
|
||||
enum PluginKind {
|
||||
LV2(LV2Plugin),
|
||||
VST2 {
|
||||
instance: ::vst::host::PluginInstance
|
||||
},
|
||||
VST3,
|
||||
}
|
||||
|
||||
const HELM: &'static str = "file:///nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lv2/helm.lv2";
|
||||
|
||||
impl Plugin {
|
||||
/// Load a LV2 plugin.
|
||||
pub fn lv2 (name: &str, path: &str) -> Usually<DynamicDevice<Self>> {
|
||||
let host = Self::new(name)?;
|
||||
let plugin = LV2Plugin::new(path)?;
|
||||
let mut state = host.state();
|
||||
let client = host.client.as_ref().unwrap().as_client();
|
||||
let (midi_ins, midi_outs, audio_ins, audio_outs) = (
|
||||
plugin.plugin.port_counts().atom_sequence_inputs,
|
||||
plugin.plugin.port_counts().atom_sequence_outputs,
|
||||
plugin.plugin.port_counts().audio_inputs,
|
||||
plugin.plugin.port_counts().audio_outputs,
|
||||
);
|
||||
state.midi_ins = {
|
||||
let mut ports = vec![];
|
||||
for i in 0..midi_ins {
|
||||
ports.push(client.register_port(&format!("midi-in-{i}"), MidiIn::default())?)
|
||||
}
|
||||
ports
|
||||
};
|
||||
state.midi_outs = {
|
||||
let mut ports = vec![];
|
||||
for i in 0..midi_outs {
|
||||
ports.push(client.register_port(&format!("midi-out-{i}"), MidiOut::default())?)
|
||||
}
|
||||
ports
|
||||
};
|
||||
state.audio_ins = {
|
||||
let mut ports = vec![];
|
||||
for i in 0..audio_ins {
|
||||
ports.push(client.register_port(&format!("audio-in-{i}"), AudioIn::default())?)
|
||||
}
|
||||
ports
|
||||
};
|
||||
state.audio_outs = {
|
||||
let mut ports = vec![];
|
||||
for i in 0..audio_outs {
|
||||
ports.push(client.register_port(&format!("audio-out-{i}"), AudioOut::default())?)
|
||||
}
|
||||
ports
|
||||
};
|
||||
state.plugin = Some(PluginKind::LV2(plugin));
|
||||
state.path = Some(String::from(path));
|
||||
std::mem::drop(state);
|
||||
Ok(host)
|
||||
}
|
||||
pub fn new (name: &str) -> Usually<DynamicDevice<Self>> {
|
||||
DynamicDevice::new(render, handle, Self::process, Self {
|
||||
name: name.into(),
|
||||
path: None,
|
||||
plugin: None,
|
||||
selected: 0,
|
||||
mapping: false,
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
audio_ins: vec![],
|
||||
audio_outs: vec![],
|
||||
}).activate(Client::new(name, ClientOptions::NO_START_SERVER)?.0)
|
||||
}
|
||||
pub fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
match self.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin { features, ref mut instance, .. })) => {
|
||||
let urid = features.midi_urid();
|
||||
let mut inputs = vec![];
|
||||
for port in self.midi_ins.iter() {
|
||||
let mut atom = ::livi::event::LV2AtomSequence::new(
|
||||
&features,
|
||||
scope.n_frames() as usize
|
||||
);
|
||||
for event in port.iter(scope) {
|
||||
match event.bytes.len() {
|
||||
3 => atom.push_midi_event::<3>(
|
||||
event.time as i64,
|
||||
urid,
|
||||
&event.bytes[0..3]
|
||||
).unwrap(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
inputs.push(atom);
|
||||
}
|
||||
let mut outputs = vec![];
|
||||
for _ in self.midi_outs.iter() {
|
||||
outputs.push(::livi::event::LV2AtomSequence::new(
|
||||
&features,
|
||||
scope.n_frames() as usize
|
||||
));
|
||||
}
|
||||
let ports = ::livi::EmptyPortConnections::new()
|
||||
.with_atom_sequence_inputs(
|
||||
inputs.iter()
|
||||
)
|
||||
.with_atom_sequence_outputs(
|
||||
outputs.iter_mut()
|
||||
)
|
||||
.with_audio_inputs(
|
||||
self.audio_ins.iter().map(|o|o.as_slice(scope))
|
||||
)
|
||||
.with_audio_outputs(
|
||||
self.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))
|
||||
);
|
||||
unsafe {
|
||||
instance.run(scope.n_frames() as usize, ports).unwrap()
|
||||
};
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl PortList for Plugin {
|
||||
fn audio_ins (&self) -> Usually<Vec<String>> {
|
||||
let mut ports = vec![];
|
||||
for port in self.audio_ins.iter() {
|
||||
ports.push(port.name()?);
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
fn audio_outs (&self) -> Usually<Vec<String>> {
|
||||
let mut ports = vec![];
|
||||
for port in self.audio_outs.iter() {
|
||||
ports.push(port.name()?);
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
fn midi_ins (&self) -> Usually<Vec<String>> {
|
||||
let mut ports = vec![];
|
||||
for port in self.midi_ins.iter() {
|
||||
ports.push(port.name()?);
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
fn midi_outs (&self) -> Usually<Vec<String>> {
|
||||
let mut ports = vec![];
|
||||
for port in self.midi_outs.iter() {
|
||||
ports.push(port.name()?);
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render (state: &Plugin, buf: &mut Buffer, area: Rect)
|
||||
-> Usually<Rect>
|
||||
{
|
||||
let Rect { x, y, height, .. } = area;
|
||||
let mut width = 20u16;
|
||||
match &state.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => {
|
||||
let start = state.selected.saturating_sub((height as usize / 2).saturating_sub(1));
|
||||
let end = start + height as usize - 2;
|
||||
//draw_box(buf, Rect { x, y, width, height });
|
||||
for i in start..end {
|
||||
if let Some(port) = port_list.get(i) {
|
||||
let value = if let Some(value) = instance.control_input(port.index) {
|
||||
value
|
||||
} else {
|
||||
port.default_value
|
||||
};
|
||||
//let label = &format!("C·· M·· {:25} = {value:.03}", port.name);
|
||||
let label = &format!("{:25} = {value:.03}", port.name);
|
||||
width = width.max(label.len() as u16 + 4);
|
||||
label.blit(buf, x + 2, y + 1 + i as u16 - start as u16, if i == state.selected {
|
||||
Some(Style::default().green())
|
||||
} else {
|
||||
None
|
||||
});
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
draw_header(state, buf, area.x, area.y, width)?;
|
||||
Ok(Rect { width, ..area })
|
||||
}
|
||||
|
||||
fn draw_header (state: &Plugin, buf: &mut Buffer, x: u16, y: u16, w: u16) -> Usually<Rect> {
|
||||
let style = Style::default().gray();
|
||||
let label1 = format!(" {}", state.name);
|
||||
label1.blit(buf, x + 1, y, Some(style.white().bold()));
|
||||
let label2 = format!("{}…", &HELM[..(w as usize - 10).min(HELM.len())]);
|
||||
label2.blit(buf, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
|
||||
Ok(Rect { x, y, width: w, height: 1 })
|
||||
}
|
||||
|
||||
pub fn handle (s: &mut Plugin, event: &AppEvent) -> Usually<bool> {
|
||||
handle_keymap(s, event, keymap!(Plugin {
|
||||
|
||||
[Up, NONE, "cursor_up", "move cursor up",
|
||||
|s: &mut Plugin|{
|
||||
if s.selected > 0 {
|
||||
s.selected = s.selected - 1
|
||||
} else {
|
||||
s.selected = match &s.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
||||
_ => 0
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}],
|
||||
|
||||
[Down, NONE, "cursor_down", "move cursor down",
|
||||
|s: &mut Plugin|{
|
||||
s.selected = s.selected + 1;
|
||||
match &s.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => {
|
||||
if s.selected >= port_list.len() {
|
||||
s.selected = 0;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(true)
|
||||
}],
|
||||
|
||||
[Char(','), NONE, "decrement", "decrement value",
|
||||
|s: &mut Plugin|{
|
||||
match s.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||
let index = port_list[s.selected].index;
|
||||
if let Some(value) = instance.control_input(index) {
|
||||
instance.set_control_input(index, value - 0.01);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(true)
|
||||
}],
|
||||
|
||||
[Char('.'), NONE, "increment", "increment value",
|
||||
|s: &mut Plugin|{
|
||||
match s.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||
let index = port_list[s.selected].index;
|
||||
if let Some(value) = instance.control_input(index) {
|
||||
instance.set_control_input(index, value + 0.01);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(true)
|
||||
}],
|
||||
|
||||
[Char('m'), NONE, "toggle_midi_map", "toggle midi map mode",
|
||||
|s: &mut Plugin|{
|
||||
s.mapping = !s.mapping;
|
||||
Ok(true)
|
||||
}]
|
||||
|
||||
}))
|
||||
}
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
use crate::core::*;
|
||||
|
||||
pub struct Voice {
|
||||
sample: Arc<Sample>,
|
||||
after: usize,
|
||||
position: usize,
|
||||
}
|
||||
impl Voice {
|
||||
fn chunk (&mut self, mut frames: usize) -> Option<Vec<Vec<f32>>> {
|
||||
// Create output buffer for each channel
|
||||
let mut chunk = vec![vec![];self.sample.channels.len()];
|
||||
// If it's not time to play yet, count down
|
||||
if self.after >= frames {
|
||||
self.after = self.after - frames;
|
||||
return Some(chunk)
|
||||
}
|
||||
// If the voice will start playing within the current buffer,
|
||||
// subtract the remaining number of wait frames.
|
||||
if self.after > 0 && self.after < frames {
|
||||
chunk = vec![vec![0.0;self.after];self.sample.channels.len()];
|
||||
frames = frames - self.after;
|
||||
self.after = 0;
|
||||
}
|
||||
if self.position < self.sample.end {
|
||||
let start = self.position.min(self.sample.end);
|
||||
let end = (self.position + frames).min(self.sample.end);
|
||||
for (i, channel) in self.sample.channels.iter().enumerate() {
|
||||
chunk[i].extend_from_slice(&channel[start..end]);
|
||||
};
|
||||
self.position = self.position + frames;
|
||||
Some(chunk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Sample {
|
||||
pub name: String,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub channels: Vec<Vec<f32>>,
|
||||
}
|
||||
impl Sample {
|
||||
pub fn new (name: &str, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Arc<Self> {
|
||||
Arc::new(Self { name: name.to_string(), start, end, channels })
|
||||
}
|
||||
fn play (self: &Arc<Self>, after: usize) -> Voice {
|
||||
Voice { sample: self.clone(), after, position: self.start }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Sampler {
|
||||
name: String,
|
||||
cursor: (usize, usize),
|
||||
samples: BTreeMap<u7, Arc<Sample>>,
|
||||
voices: Vec<Voice>,
|
||||
midi_in: Port<MidiIn>,
|
||||
audio_ins: Vec<Port<AudioIn>>,
|
||||
audio_outs: Vec<Port<AudioOut>>,
|
||||
}
|
||||
|
||||
impl Sampler {
|
||||
pub fn new (
|
||||
name: &str,
|
||||
samples: Option<BTreeMap<u7, Arc<Sample>>>,
|
||||
) -> Result<DynamicDevice<Self>, Box<dyn Error>> {
|
||||
let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||
DynamicDevice::new(render, handle, Self::process, Self {
|
||||
name: name.into(),
|
||||
cursor: (0, 0),
|
||||
samples: samples.unwrap_or(BTreeMap::new()),
|
||||
voices: vec![],
|
||||
midi_in: client.register_port("midi", ::jack::MidiIn::default())?,
|
||||
audio_ins: vec![
|
||||
client.register_port("recL", ::jack::AudioIn::default())?,
|
||||
client.register_port("recR", ::jack::AudioIn::default())?,
|
||||
],
|
||||
audio_outs: vec![
|
||||
client.register_port("outL", ::jack::AudioOut::default())?,
|
||||
client.register_port("outR", ::jack::AudioOut::default())?,
|
||||
],
|
||||
}).activate(client)
|
||||
}
|
||||
|
||||
pub fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
// Output buffer: this will be copied to the audio outs.
|
||||
let channel_count = self.audio_outs.len();
|
||||
let mut mixed = vec![vec![0.0;scope.n_frames() as usize];channel_count];
|
||||
// Process MIDI input to add new voices.
|
||||
for RawMidi { time, bytes } in self.midi_in.iter(scope) {
|
||||
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
|
||||
if let MidiMessage::NoteOn { ref key, .. } = message {
|
||||
if let Some(sample) = self.samples.get(key) {
|
||||
self.voices.push(sample.play(time as usize));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Emit next chunk of each currently playing voice,
|
||||
// dropping voices that have reached their ends.
|
||||
let mut voices = vec![];
|
||||
std::mem::swap(&mut voices, &mut self.voices);
|
||||
loop {
|
||||
if voices.len() < 1 {
|
||||
break
|
||||
}
|
||||
let mut voice = voices.swap_remove(0);
|
||||
if let Some(chunk) = voice.chunk(scope.n_frames() as usize) {
|
||||
for (i, channel) in chunk.iter().enumerate() {
|
||||
let buffer = &mut mixed[i % channel_count];
|
||||
for (i, sample) in channel.iter().enumerate() {
|
||||
buffer[i] += sample;
|
||||
}
|
||||
}
|
||||
self.voices.push(voice);
|
||||
}
|
||||
}
|
||||
// Write output buffer to output ports.
|
||||
for (i, port) in self.audio_outs.iter_mut().enumerate() {
|
||||
let buffer = &mixed[i];
|
||||
for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() {
|
||||
*value = *buffer.get(i).unwrap_or(&0.0);
|
||||
}
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
|
||||
fn load_sample (&mut self, _path: &str) {}
|
||||
}
|
||||
|
||||
impl PortList for Sampler {
|
||||
fn midi_ins (&self) -> Usually<Vec<String>> {
|
||||
Ok(vec![self.midi_in.name()?])
|
||||
}
|
||||
fn audio_ins (&self) -> Usually<Vec<String>> {
|
||||
let mut ports = vec![];
|
||||
for port in self.audio_ins.iter() {
|
||||
ports.push(port.name()?);
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
fn audio_outs (&self) -> Usually<Vec<String>> {
|
||||
let mut ports = vec![];
|
||||
for port in self.audio_outs.iter() {
|
||||
ports.push(port.name()?);
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render (state: &Sampler, buf: &mut Buffer, Rect { x, y, height, .. }: Rect)
|
||||
-> Usually<Rect>
|
||||
{
|
||||
let style = Style::default().gray();
|
||||
let title = format!(" {} ({})", state.name, state.voices.len());
|
||||
title.blit(buf, x+1, y, Some(style.white().bold().not_dim()));
|
||||
let mut width = title.len() + 2;
|
||||
for (i, (note, sample)) in state.samples.iter().enumerate() {
|
||||
let style = if i == state.cursor.0 {
|
||||
Style::default().green()
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
let i = i as u16;
|
||||
let y1 = y+1+i;
|
||||
if y1 >= y + height {
|
||||
break
|
||||
}
|
||||
if i as usize == state.cursor.0 {
|
||||
"⯈".blit(buf, x+1, y1, Some(style.bold()));
|
||||
}
|
||||
let label1 = format!("{note:3} {:8}", sample.name);
|
||||
let label2 = format!("{:>6} {:>6}", sample.start, sample.end);
|
||||
label1.blit(buf, x+2, y1, Some(style.bold()));
|
||||
label2.blit(buf, x+3+label1.len()as u16, y1, Some(style));
|
||||
width = width.max(label1.len() + label2.len() + 4);
|
||||
}
|
||||
let height = ((1 + state.samples.len()) as u16).min(height);
|
||||
Ok(Rect { x, y, width: width as u16, height })
|
||||
}
|
||||
|
||||
//fn render_table (
|
||||
//state: &mut Sampler,
|
||||
//stdout: &mut Stdout,
|
||||
//offset: (u16, u16),
|
||||
//) -> Result<(), Box<dyn Error>> {
|
||||
//let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row);
|
||||
//stdout.queue(move_to(0, 3))?.queue(
|
||||
//Print(" Name Rate Trigger Route")
|
||||
//)?;
|
||||
//for (i, sample) in state.samples.lock().unwrap().iter().enumerate() {
|
||||
//let row = 4 + i as u16;
|
||||
//for (j, (column, field)) in [
|
||||
//(0, format!(" {:7} ", sample.name)),
|
||||
//(9, format!(" {:.1}Hz ", sample.rate)),
|
||||
//(18, format!(" MIDI C{} {} ", sample.trigger.0, sample.trigger.1)),
|
||||
//(33, format!(" {:.1}dB -> Output ", sample.gain)),
|
||||
//(50, format!(" {} ", sample.playing.unwrap_or(0))),
|
||||
//].into_iter().enumerate() {
|
||||
//stdout.queue(move_to(column, row))?;
|
||||
//if state.selected_sample == i && state.selected_column == j {
|
||||
//stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?;
|
||||
//} else {
|
||||
//stdout.queue(PrintStyledContent(field.to_string().bold()))?;
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//Ok(())
|
||||
//}
|
||||
|
||||
//fn render_meters (
|
||||
//state: &mut Sampler,
|
||||
//stdout: &mut Stdout,
|
||||
//offset: (u16, u16),
|
||||
//) -> Result<(), Box<dyn Error>> {
|
||||
//let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row);
|
||||
//for (i, sample) in state.samples.lock().iter().enumerate() {
|
||||
//let row = 4 + i as u16;
|
||||
//stdout.queue(move_to(32, row))?.queue(
|
||||
//PrintStyledContent("▁".green())
|
||||
//)?;
|
||||
//}
|
||||
//Ok(())
|
||||
//}
|
||||
|
||||
pub fn handle (state: &mut Sampler, event: &AppEvent) -> Usually<bool> {
|
||||
Ok(handle_keymap(state, event, KEYMAP)?)
|
||||
}
|
||||
pub const KEYMAP: &'static [KeyBinding<Sampler>] = keymap!(Sampler {
|
||||
[Up, NONE, "cursor_up", "move cursor up", cursor_up],
|
||||
[Down, NONE, "cursor_down", "move cursor down", cursor_down],
|
||||
[Char('t'), NONE, "trigger", "play current sample", trigger],
|
||||
[Enter, NONE, "select", "select item under cursor", select],
|
||||
});
|
||||
fn cursor_up (state: &mut Sampler) -> Usually<bool> {
|
||||
state.cursor.0 = if state.cursor.0 == 0 {
|
||||
state.samples.len() - 1
|
||||
} else {
|
||||
state.cursor.0 - 1
|
||||
};
|
||||
Ok(true)
|
||||
}
|
||||
fn cursor_down (state: &mut Sampler) -> Usually<bool> {
|
||||
state.cursor.0 = (state.cursor.0 + 1) % state.samples.len();
|
||||
Ok(true)
|
||||
}
|
||||
fn trigger (state: &mut Sampler) -> Usually<bool> {
|
||||
for (i, sample) in state.samples.values().enumerate() {
|
||||
if i == state.cursor.0 {
|
||||
state.voices.push(sample.play(0))
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
fn select (state: &mut Sampler) -> Usually<bool> {
|
||||
for (i, _sample) in state.samples.values().enumerate() {
|
||||
if i == state.cursor.0 {
|
||||
//state.voices.push(sample.play(0))
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
use crate::core::*;
|
||||
use crate::layout::*;
|
||||
use crate::device::*;
|
||||
mod grid; pub use self::grid::*;
|
||||
mod handle; pub use self::handle::*;
|
||||
mod scene; pub use self::scene::*;
|
||||
pub struct Launcher {
|
||||
name: String,
|
||||
timebase: Arc<Timebase>,
|
||||
transport: ::jack::Transport,
|
||||
playing: TransportState,
|
||||
monitoring: bool,
|
||||
recording: bool,
|
||||
overdub: bool,
|
||||
current_frame: usize,
|
||||
cursor: (usize, usize),
|
||||
pub tracks: Vec<Track>,
|
||||
scenes: Vec<Scene>,
|
||||
show_help: bool,
|
||||
view: LauncherView,
|
||||
modal: Option<Box<dyn Modal<Self>>>,
|
||||
}
|
||||
impl Launcher {
|
||||
pub fn new (
|
||||
name: &str,
|
||||
timebase: &Arc<Timebase>,
|
||||
tracks: Option<Vec<Track>>,
|
||||
scenes: Option<Vec<Scene>>
|
||||
) -> Result<DynamicDevice<Self>, Box<dyn Error>> {
|
||||
let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||
let transport = client.transport();
|
||||
let ppq = timebase.ppq() as usize;
|
||||
DynamicDevice::new(render, handle, process, Self {
|
||||
name: name.into(),
|
||||
view: LauncherView::Chains,
|
||||
playing: transport.query_state()?,
|
||||
transport,
|
||||
timebase: timebase.clone(),
|
||||
monitoring: true,
|
||||
recording: false,
|
||||
overdub: true,
|
||||
cursor: (2, 2),
|
||||
current_frame: 0,
|
||||
scenes: scenes.unwrap_or_else(||vec![Scene::new(&"Scene 1", &[None])]),
|
||||
tracks: if let Some(tracks) = tracks { tracks } else { vec![
|
||||
Track::new("Track 1", &timebase, None, Some(vec![
|
||||
Phrase::new("MIDI Clip 1", ppq * 4, Some(BTreeMap::from([
|
||||
( ppq * 0, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ),
|
||||
( ppq * 1, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ),
|
||||
( ppq * 2, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ),
|
||||
( ppq * 3, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ),
|
||||
])))
|
||||
]))?,
|
||||
] },
|
||||
show_help: true,
|
||||
modal: None
|
||||
}).activate(client)
|
||||
}
|
||||
fn cols (&self) -> usize {
|
||||
(self.tracks.len() + 2) as usize
|
||||
}
|
||||
fn col (&self) -> usize {
|
||||
self.cursor.0 as usize
|
||||
}
|
||||
fn dec_col (&mut self) {
|
||||
self.cursor.0 = if self.cursor.0 > 0 {
|
||||
self.cursor.0 - 1
|
||||
} else {
|
||||
(self.cols() - 1) as usize
|
||||
}
|
||||
}
|
||||
fn inc_col (&mut self) {
|
||||
self.cursor.0 = if self.cursor.0 >= self.cols() - 1 {
|
||||
0
|
||||
} else {
|
||||
self.cursor.0 + 1
|
||||
}
|
||||
}
|
||||
fn rows (&self) -> usize {
|
||||
(self.scenes.len() + 2) as usize
|
||||
}
|
||||
fn row (&self) -> usize {
|
||||
self.cursor.1 as usize
|
||||
}
|
||||
fn dec_row (&mut self) {
|
||||
self.cursor.1 = if self.cursor.1 > 0 {
|
||||
self.cursor.1 - 1
|
||||
} else {
|
||||
self.rows() - 1
|
||||
}
|
||||
}
|
||||
fn inc_row (&mut self) {
|
||||
self.cursor.1 = if self.cursor.1 >= self.rows() - 1 {
|
||||
0
|
||||
} else {
|
||||
self.cursor.1 + 1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track <'a> (&'a self) -> Option<(usize, &'a Track)> {
|
||||
match self.col() { 0 => None, _ => {
|
||||
let id = self.col() as usize - 1;
|
||||
self.tracks.get(id).map(|t|(id, t))
|
||||
} }
|
||||
}
|
||||
pub fn scene <'a> (&'a self) -> Option<(usize, &'a Scene)> {
|
||||
match self.row() { 0 => None, _ => {
|
||||
let id = self.row() as usize - 1;
|
||||
self.scenes.get(id).map(|t|(id, t))
|
||||
} }
|
||||
}
|
||||
pub fn sequencer <'a> (&'a self) -> Option<MutexGuard<Sequencer>> {
|
||||
Some(self.track()?.1.sequencer.state())
|
||||
}
|
||||
pub fn chain <'a> (&'a self) -> Option<MutexGuard<Chain>> {
|
||||
Some(self.track()?.1.chain.state())
|
||||
}
|
||||
pub fn phrase_id (&self) -> Option<usize> {
|
||||
let (track_id, _) = self.track()?;
|
||||
let (_, scene) = self.scene()?;
|
||||
*scene.clips.get(track_id)?
|
||||
}
|
||||
}
|
||||
impl DynamicDevice<Launcher> {
|
||||
pub fn connect (&self, midi_in: &str, audio_outs: &[&str]) -> Usually<&Self> {
|
||||
{
|
||||
let state = &self.state();
|
||||
let (client, _status) = Client::new(
|
||||
&format!("{}-init", &state.name), ClientOptions::NO_START_SERVER
|
||||
)?;
|
||||
let midi_ins = client.ports(Some(midi_in), None, PortFlags::IS_OUTPUT);
|
||||
let audio_outs: Vec<Vec<String>> = audio_outs.iter()
|
||||
.map(|pattern|client.ports(Some(pattern), None, PortFlags::IS_INPUT))
|
||||
.collect();
|
||||
for (i, sequencer) in state.tracks.iter().enumerate() {
|
||||
for sequencer_midi_in in sequencer.midi_ins()?.iter() {
|
||||
for midi_in in midi_ins.iter() {
|
||||
client.connect_ports_by_name(&midi_in, &sequencer_midi_in)?;
|
||||
}
|
||||
}
|
||||
let chain: &DynamicDevice<Chain> = &state.tracks[i].chain;
|
||||
for port in sequencer.midi_outs()?.iter() {
|
||||
for midi_in in chain.midi_ins()?.iter() {
|
||||
client.connect_ports_by_name(&port, &midi_in)?;
|
||||
}
|
||||
}
|
||||
for (j, port) in chain.audio_outs()?.iter().enumerate() {
|
||||
for audio_out in audio_outs[j % audio_outs.len()].iter() {
|
||||
client.connect_ports_by_name(&port, &audio_out)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
impl PortList for Launcher {}
|
||||
pub fn process (state: &mut Launcher, _: &Client, _: &ProcessScope) -> Control {
|
||||
let transport = state.transport.query().unwrap();
|
||||
state.playing = transport.state;
|
||||
state.current_frame = transport.pos.frame() as usize;
|
||||
Control::Continue
|
||||
}
|
||||
pub fn render (state: &Launcher, buf: &mut Buffer, mut area: Rect) -> Usually<Rect> {
|
||||
//area.width = 80; // DOS mode
|
||||
//area.height = 25;
|
||||
let Rect { x, mut y, width, height } = area;
|
||||
|
||||
y = y + crate::device::Transport {
|
||||
timebase: &state.timebase,
|
||||
playing: state.playing,
|
||||
record: state.sequencer().map(|s|s.recording).unwrap_or(false),
|
||||
overdub: state.sequencer().map(|s|s.overdub).unwrap_or(false),
|
||||
monitor: state.sequencer().map(|s|s.monitoring).unwrap_or(false),
|
||||
frame: state.current_frame
|
||||
}.render(buf, area)?.height;
|
||||
|
||||
y = y + crate::device::launcher::SceneGrid {
|
||||
buf,
|
||||
area: Rect { x, y, width, height },
|
||||
name: &state.name,
|
||||
focused: state.view.is_tracks(),
|
||||
scenes: &state.scenes,
|
||||
tracks: &state.tracks,
|
||||
cursor: &state.cursor
|
||||
}.draw()?.height;
|
||||
|
||||
y = y + draw_section_chains(
|
||||
state,
|
||||
buf,
|
||||
Rect { x, y, width, height: height/3 }
|
||||
)?.height;
|
||||
|
||||
y = y + draw_section_sequencer(
|
||||
state,
|
||||
buf,
|
||||
Rect { x, y, width, height: height - y }
|
||||
)?.height;
|
||||
|
||||
area.height = y;
|
||||
if state.show_help {
|
||||
let style = Some(Style::default().bold().white().not_dim().on_black().italic());
|
||||
let hide = "[Tab] Mode [Arrows] Move [.,] Value [F1] Toggle help ";
|
||||
hide.blit(buf, x + (width - hide.len() as u16) / 2, height - 1, style);
|
||||
}
|
||||
|
||||
if let Some(ref modal) = state.modal {
|
||||
for cell in buf.content.iter_mut() {
|
||||
cell.fg = ::ratatui::style::Color::Gray;
|
||||
cell.modifier = ::ratatui::style::Modifier::DIM;
|
||||
}
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
fn draw_section_sequencer (state: &Launcher, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let Rect { x, y, width, height } = area;
|
||||
let style = Some(Style::default().green().dim());
|
||||
let view = &state.view;
|
||||
match view {
|
||||
LauncherView::Sequencer => {
|
||||
lozenge_left(buf, x, y, height, style);
|
||||
lozenge_right(buf, x + width - 1, y, height, style);
|
||||
},
|
||||
_ => {},
|
||||
};
|
||||
let track = state.track();
|
||||
if track.is_none() {
|
||||
return Ok(area);
|
||||
}
|
||||
let track = track.unwrap().1;
|
||||
let sequencer = track.sequencer.state();
|
||||
crate::device::sequencer::horizontal::draw(
|
||||
buf,
|
||||
area,
|
||||
match state.phrase_id().map(|id|sequencer.phrases.get(id)) {
|
||||
Some(Some(phrase)) => Some(phrase),
|
||||
_ => None
|
||||
},
|
||||
state.timebase.ppq() as usize,
|
||||
sequencer.time_cursor,
|
||||
sequencer.time_start,
|
||||
sequencer.time_zoom,
|
||||
sequencer.note_cursor,
|
||||
sequencer.note_start,
|
||||
Some(match view {
|
||||
LauncherView::Sequencer => Style::default().green().not_dim(),
|
||||
_ => Style::default().green().dim(),
|
||||
})
|
||||
)
|
||||
}
|
||||
fn draw_section_chains (state: &Launcher, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let style = Some(Style::default().green().dim());
|
||||
if state.view.is_chains() {
|
||||
let Rect { x, y, width, height} = area;
|
||||
lozenge_left(buf, x, y, height, style);
|
||||
lozenge_right(buf, x + width - 1, y, height, style);
|
||||
}
|
||||
let chain = state.chain();
|
||||
let _ = if let Some(chain) = &chain {
|
||||
let (_, plugins) = crate::device::chain::draw_as_row(
|
||||
&*chain, buf, area, style
|
||||
)?;
|
||||
plugins
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
Ok(area)
|
||||
}
|
||||
pub enum LauncherView {
|
||||
Tracks,
|
||||
Sequencer,
|
||||
Chains
|
||||
}
|
||||
impl LauncherView {
|
||||
fn is_chains (&self) -> bool {
|
||||
match self { Self::Chains => true, _ => false }
|
||||
}
|
||||
fn is_tracks (&self) -> bool {
|
||||
match self { Self::Tracks => true, _ => false }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
use crate::core::*;
|
||||
use crate::layout::*;
|
||||
|
||||
pub struct Mixer {
|
||||
name: String,
|
||||
tracks: Vec<Track>,
|
||||
selected_track: usize,
|
||||
selected_column: usize,
|
||||
}
|
||||
|
||||
impl Mixer {
|
||||
pub fn new (name: &str) -> Result<DynamicDevice<Self>, Box<dyn Error>> {
|
||||
let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||
Ok(DynamicDevice::new(render, handle, process, Self {
|
||||
name: name.into(),
|
||||
selected_column: 0,
|
||||
selected_track: 1,
|
||||
tracks: vec![
|
||||
Track::new(&client, 1, "Mono 1")?,
|
||||
Track::new(&client, 1, "Mono 2")?,
|
||||
Track::new(&client, 2, "Stereo 1")?,
|
||||
Track::new(&client, 2, "Stereo 2")?,
|
||||
Track::new(&client, 2, "Stereo 3")?,
|
||||
Track::new(&client, 2, "Bus 1")?,
|
||||
Track::new(&client, 2, "Bus 2")?,
|
||||
Track::new(&client, 2, "Mix")?,
|
||||
],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process (
|
||||
_: &mut Mixer,
|
||||
_: &Client,
|
||||
_: &ProcessScope
|
||||
) -> Control {
|
||||
Control::Continue
|
||||
}
|
||||
|
||||
pub fn render (state: &Mixer, buf: &mut Buffer, mut area: Rect)
|
||||
-> Usually<Rect>
|
||||
{
|
||||
if area.height < 2 {
|
||||
return Ok(area)
|
||||
}
|
||||
area.x = area.width.saturating_sub(80) / 2;
|
||||
area.width = area.width.min(80);
|
||||
area.height = state.tracks.len() as u16 + 2;
|
||||
draw_box(buf, area);
|
||||
let x = area.x + 1;
|
||||
let y = area.y + 1;
|
||||
let _h = area.height - 2;
|
||||
for (i, track) in state.tracks.iter().enumerate() {
|
||||
//buf.set_string(
|
||||
//x, y + index as u16,
|
||||
//&track.name, Style::default().bold().not_dim()
|
||||
//);
|
||||
for (j, (column, field)) in [
|
||||
(0, format!(" {:10} ", track.name)),
|
||||
(12, format!(" {:.1}dB ", track.gain)),
|
||||
(22, format!(" [ ] ")),
|
||||
(30, format!(" C ")),
|
||||
(35, format!(" {:.1}dB ", track.level)),
|
||||
(45, format!(" [ ] ")),
|
||||
(51, format!(" {:7} ", track.route)),
|
||||
].into_iter().enumerate() {
|
||||
buf.set_string(
|
||||
x + column as u16,
|
||||
y + i as u16,
|
||||
field,
|
||||
if state.selected_track == i && state.selected_column == j {
|
||||
Style::default().white().bold().not_dim()
|
||||
} else {
|
||||
Style::default().not_dim()
|
||||
}
|
||||
);
|
||||
//stdout.queue(move_to(column, row))?;
|
||||
//if state.selected_track == i && state.selected_column == j {
|
||||
//stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?;
|
||||
//} else {
|
||||
//stdout.queue(PrintStyledContent(field.to_string().bold()))?;
|
||||
//}
|
||||
//fn render_meters (
|
||||
//state: &mut Mixer,
|
||||
//stdout: &mut Stdout,
|
||||
//offset: Rect
|
||||
//) -> Result<(), Box<dyn Error>> {
|
||||
//let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row);
|
||||
//for (i, track) in state.tracks.iter().enumerate() {
|
||||
//let row = (i + 1) as u16;
|
||||
//stdout
|
||||
//.queue(move_to(10, row))?.queue(PrintStyledContent("▁".green()))?
|
||||
//.queue(move_to(20, row))?.queue(PrintStyledContent("▁".green()))?
|
||||
//.queue(move_to(28, row))?.queue(PrintStyledContent("▁".green()))?
|
||||
//.queue(move_to(43, row))?.queue(PrintStyledContent("▁".green()))?;
|
||||
//}
|
||||
//Ok(())
|
||||
//}
|
||||
}
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
|
||||
pub fn handle (state: &mut Mixer, event: &AppEvent) -> Usually<bool> {
|
||||
if let AppEvent::Input(crossterm::event::Event::Key(event)) = event {
|
||||
|
||||
match event.code {
|
||||
//KeyCode::Char('c') => {
|
||||
//if event.modifiers == KeyModifiers::CONTROL {
|
||||
//state.exit();
|
||||
//}
|
||||
//},
|
||||
KeyCode::Down => {
|
||||
state.selected_track = (state.selected_track + 1) % state.tracks.len();
|
||||
println!("{}", state.selected_track);
|
||||
return Ok(true)
|
||||
},
|
||||
KeyCode::Up => {
|
||||
if state.selected_track == 0 {
|
||||
state.selected_track = state.tracks.len() - 1;
|
||||
} else {
|
||||
state.selected_track = state.selected_track - 1;
|
||||
}
|
||||
println!("{}", state.selected_track);
|
||||
return Ok(true)
|
||||
},
|
||||
KeyCode::Left => {
|
||||
if state.selected_column == 0 {
|
||||
state.selected_column = 6
|
||||
} else {
|
||||
state.selected_column = state.selected_column - 1;
|
||||
}
|
||||
return Ok(true)
|
||||
},
|
||||
KeyCode::Right => {
|
||||
if state.selected_column == 6 {
|
||||
state.selected_column = 0
|
||||
} else {
|
||||
state.selected_column = state.selected_column + 1;
|
||||
}
|
||||
return Ok(true)
|
||||
},
|
||||
_ => {
|
||||
println!("\n{event:?}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// - Meters: propagate clipping:
|
||||
// - If one stage clips, all stages after it are marked red
|
||||
// - If one track clips, all tracks that feed from it are marked red?
|
||||
|
||||
pub const ACTIONS: [(&'static str, &'static str);2] = [
|
||||
("+/-", "Adjust"),
|
||||
("Ins/Del", "Add/remove track"),
|
||||
];
|
||||
|
||||
pub struct Track {
|
||||
name: String,
|
||||
channels: u8,
|
||||
input_ports: Vec<Port<AudioIn>>,
|
||||
pre_gain_meter: f64,
|
||||
gain: f64,
|
||||
insert_ports: Vec<Port<AudioOut>>,
|
||||
return_ports: Vec<Port<AudioIn>>,
|
||||
post_gain_meter: f64,
|
||||
post_insert_meter: f64,
|
||||
level: f64,
|
||||
pan: f64,
|
||||
output_ports: Vec<Port<AudioOut>>,
|
||||
post_fader_meter: f64,
|
||||
route: String,
|
||||
}
|
||||
|
||||
impl Track {
|
||||
pub fn new (jack: &Client, channels: u8, name: &str) -> Result<Self, Box<dyn Error>> {
|
||||
let mut input_ports = vec![];
|
||||
let mut insert_ports = vec![];
|
||||
let mut return_ports = vec![];
|
||||
let mut output_ports = vec![];
|
||||
for channel in 1..=channels {
|
||||
input_ports.push(jack.register_port(&format!("{name} [input {channel}]"), AudioIn::default())?);
|
||||
output_ports.push(jack.register_port(&format!("{name} [out {channel}]"), AudioOut::default())?);
|
||||
let insert_port = jack.register_port(&format!("{name} [pre {channel}]"), AudioOut::default())?;
|
||||
let return_port = jack.register_port(&format!("{name} [insert {channel}]"), AudioIn::default())?;
|
||||
jack.connect_ports(&insert_port, &return_port)?;
|
||||
insert_ports.push(insert_port);
|
||||
return_ports.push(return_port);
|
||||
}
|
||||
Ok(Self {
|
||||
name: name.into(),
|
||||
channels,
|
||||
input_ports,
|
||||
pre_gain_meter: 0.0,
|
||||
gain: 0.0,
|
||||
post_gain_meter: 0.0,
|
||||
insert_ports,
|
||||
return_ports,
|
||||
post_insert_meter: 0.0,
|
||||
level: 0.0,
|
||||
pan: 0.0,
|
||||
post_fader_meter: 0.0,
|
||||
route: "---".into(),
|
||||
output_ports,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//impl<W: Write> Input<TUI<W>, bool> for Mixer {
|
||||
//fn handle (&mut self, engine: &mut TUI<W>) -> Result<Option<bool>> {
|
||||
//Ok(None)
|
||||
//}
|
||||
//
|
||||
|
||||
//impl<W: Write> Output<TUI<W>, [u16;2]> for Mixer {
|
||||
//fn render (&self, envine: &mut TUI<W>) -> Result<Option<[u16;2]>> {
|
||||
|
||||
//let tracks_table = Columns::new()
|
||||
//.add(titles)
|
||||
//.add(input_meters)
|
||||
//.add(gains)
|
||||
//.add(gain_meters)
|
||||
//.add(pres)
|
||||
//.add(pre_meters)
|
||||
//.add(levels)
|
||||
//.add(pans)
|
||||
//.add(pan_meters)
|
||||
//.add(posts)
|
||||
//.add(routes)
|
||||
|
||||
//Rows::new()
|
||||
//.add(Columns::new()
|
||||
//.add(Rows::new()
|
||||
//.add("[Arrows]".bold())
|
||||
//.add("Navigate"))
|
||||
//.add(Rows::new()
|
||||
//.add("[+/-]".bold())
|
||||
//.add("Adjust"))
|
||||
//.add(Rows::new()
|
||||
//.add("[Ins/Del]".bold())
|
||||
//.add("Add/remove track")))
|
||||
//.add(tracks_table)
|
||||
//.render(engine)
|
||||
//}
|
||||
//}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
mod chain; pub use self::chain::{Chain, Plugin, Sampler, Sample, Voice};
|
||||
mod launcher; pub use self::launcher::{Launcher, Scene};
|
||||
mod looper; pub use self::looper::Looper;
|
||||
mod mixer; pub use self::mixer::Mixer;
|
||||
mod sequencer; pub use self::sequencer::{Sequencer, Phrase};
|
||||
mod track; pub use self::track::Track;
|
||||
mod transport; pub use self::transport::Transport;
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
use crate::core::*;
|
||||
use crate::layout::*;
|
||||
|
||||
mod keys; use self::keys::*;
|
||||
mod handle; pub use self::handle::*;
|
||||
mod phrase; pub use self::phrase::*;
|
||||
|
||||
pub mod horizontal;
|
||||
pub mod vertical;
|
||||
|
||||
pub struct Sequencer {
|
||||
pub name: String,
|
||||
/// JACK transport handle.
|
||||
pub transport: ::jack::Transport,
|
||||
/// JACK MIDI input port that will be created.
|
||||
pub midi_in: Port<MidiIn>,
|
||||
/// JACK MIDI output port that will be created.
|
||||
pub midi_out: Port<MidiOut>,
|
||||
/// Holds info about tempo
|
||||
pub timebase: Arc<Timebase>,
|
||||
/// Phrase selector
|
||||
pub sequence: Option<usize>,
|
||||
/// Map: tick -> MIDI events at tick
|
||||
pub phrases: Vec<Phrase>,
|
||||
/// Red keys on piano roll.
|
||||
pub notes_on: Vec<bool>,
|
||||
/// Play sequence to output.
|
||||
pub playing: TransportState,
|
||||
/// Play input through output.
|
||||
pub monitoring: bool,
|
||||
/// Write input to sequence.
|
||||
pub recording: bool,
|
||||
/// Don't delete when recording.
|
||||
pub overdub: bool,
|
||||
/// Display mode
|
||||
pub view: SequencerView,
|
||||
/// Range of notes to display
|
||||
pub note_start: usize,
|
||||
/// Position of cursor within note range
|
||||
pub note_cursor: usize,
|
||||
/// PPM per display unit
|
||||
pub time_zoom: usize,
|
||||
/// Range of time steps to display
|
||||
pub time_start: usize,
|
||||
/// Position of cursor within time range
|
||||
pub time_cursor: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SequencerView { Tiny, Compact, Horizontal, Vertical, }
|
||||
|
||||
impl Sequencer {
|
||||
pub fn new (
|
||||
name: &str,
|
||||
timebase: &Arc<Timebase>,
|
||||
phrases: Option<Vec<Phrase>>,
|
||||
) -> Usually<DynamicDevice<Self>> {
|
||||
let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||
let transport = client.transport();
|
||||
DynamicDevice::new(render, handle, Self::process, Self {
|
||||
name: name.into(),
|
||||
timebase: timebase.clone(),
|
||||
phrases: phrases.unwrap_or_else(||vec![Phrase::default()]),
|
||||
sequence: Some(0),
|
||||
|
||||
transport,
|
||||
midi_in: client.register_port("in", MidiIn::default())?,
|
||||
monitoring: true,
|
||||
recording: true,
|
||||
midi_out: client.register_port("out", MidiOut::default())?,
|
||||
playing: TransportState::Starting,
|
||||
overdub: true,
|
||||
|
||||
view: SequencerView::Horizontal,
|
||||
notes_on: vec![false;128],
|
||||
note_start: 12,
|
||||
note_cursor: 0,
|
||||
time_zoom: 24,
|
||||
time_start: 0,
|
||||
time_cursor: 0,
|
||||
}).activate(client)
|
||||
}
|
||||
|
||||
pub fn phrase <'a> (&'a self) -> Option<&'a Phrase> {
|
||||
self.phrases.get(self.sequence?)
|
||||
}
|
||||
|
||||
pub fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
if self.sequence.is_none() { return Control::Continue }
|
||||
let phrase = self.phrases.get_mut(self.sequence.unwrap());
|
||||
if phrase.is_none() { return Control::Continue }
|
||||
let phrase = phrase.unwrap();
|
||||
let frame = scope.last_frame_time() as usize;
|
||||
let frames = scope.n_frames() as usize;
|
||||
let mut output: Vec<Option<Vec<Vec<u8>>>> = vec![None;frames];
|
||||
let transport = self.transport.query().unwrap();
|
||||
if transport.state != self.playing {
|
||||
all_notes_off(&mut output);
|
||||
}
|
||||
self.playing = transport.state;
|
||||
// Play from phrase into output buffer
|
||||
if self.playing == TransportState::Rolling {
|
||||
phrase.process_out(
|
||||
&mut output,
|
||||
&mut self.notes_on,
|
||||
&self.timebase,
|
||||
frame,
|
||||
frames
|
||||
);
|
||||
}
|
||||
// Play from input to monitor, and record into phrase.
|
||||
phrase.process_in(
|
||||
self.midi_in.iter(scope),
|
||||
&mut self.notes_on,
|
||||
if self.monitoring { Some(&mut output) } else { None },
|
||||
self.recording && self.playing == TransportState::Rolling,
|
||||
&self.timebase,
|
||||
frame,
|
||||
);
|
||||
write_output(&mut self.midi_out.writer(scope), &mut output, frames);
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl PortList for Sequencer {
|
||||
fn midi_ins (&self) -> Usually<Vec<String>> { Ok(vec![self.midi_in.name()?]) }
|
||||
fn midi_outs (&self) -> Usually<Vec<String>> { Ok(vec![self.midi_out.name()?]) }
|
||||
}
|
||||
fn render (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let Rect { x, y, width, height } = area;
|
||||
let header = draw_header(s, buf, area)?;
|
||||
let piano = match s.view {
|
||||
SequencerView::Tiny => Rect { x, y, width, height: 0 },
|
||||
SequencerView::Compact => Rect { x, y, width, height: 0 },
|
||||
SequencerView::Vertical => self::vertical::draw(s, buf, Rect {
|
||||
x, y: y + header.height, width, height,
|
||||
})?,
|
||||
SequencerView::Horizontal => self::horizontal::draw(
|
||||
buf,
|
||||
Rect { x, y: y + header.height, width, height, },
|
||||
s.phrase(),
|
||||
s.timebase.ppq() as usize,
|
||||
s.time_cursor,
|
||||
s.time_start,
|
||||
s.time_zoom,
|
||||
s.note_cursor,
|
||||
s.note_start,
|
||||
None
|
||||
)?,
|
||||
};
|
||||
Ok(draw_box(buf, Rect {
|
||||
x, y,
|
||||
width: header.width.max(piano.width),
|
||||
height: header.height + piano.height
|
||||
}))
|
||||
}
|
||||
pub fn draw_header (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let Rect { x, y, width, .. } = area;
|
||||
let style = Style::default().gray();
|
||||
crate::device::Transport {
|
||||
timebase: &s.timebase,
|
||||
playing: s.playing,
|
||||
record: s.recording,
|
||||
overdub: s.overdub,
|
||||
monitor: s.monitoring,
|
||||
frame: 0
|
||||
}.render(buf, area)?;
|
||||
let separator = format!("├{}┤", "-".repeat((width - 2).into()));
|
||||
separator.blit(buf, x, y + 2, Some(style.dim()));
|
||||
let _ = draw_clips(s, buf, area)?;
|
||||
Ok(Rect { x, y, width, height: 3 })
|
||||
}
|
||||
pub fn draw_clips (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let Rect { x, y, .. } = area;
|
||||
let style = Style::default().gray();
|
||||
for (i, sequence) in s.phrases.iter().enumerate() {
|
||||
let label = format!("▶ {}", &sequence.name);
|
||||
label.blit(buf, x + 2, y + 3 + (i as u16)*2, Some(if Some(i) == s.sequence {
|
||||
match s.playing {
|
||||
TransportState::Rolling => style.white().bold(),
|
||||
_ => style.not_dim().bold()
|
||||
}
|
||||
} else {
|
||||
style.dim()
|
||||
}));
|
||||
}
|
||||
Ok(Rect { x, y, width: 14, height: 14 })
|
||||
}
|
||||
/// Add "all notes off" to the start of a buffer.
|
||||
pub fn all_notes_off (output: &mut MIDIChunk) {
|
||||
output[0] = Some(vec![]);
|
||||
if let Some(Some(frame)) = output.get_mut(0) {
|
||||
let mut buf = vec![];
|
||||
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
||||
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
||||
evt.write(&mut buf).unwrap();
|
||||
frame.push(buf);
|
||||
}
|
||||
}
|
||||
/// Write to JACK port from output buffer (containing notes from sequence and/or monitor)
|
||||
fn write_output (writer: &mut ::jack::MidiWriter, output: &mut MIDIChunk, frames: usize) {
|
||||
for time in 0..frames {
|
||||
if let Some(Some(frame)) = output.get_mut(time ) {
|
||||
for event in frame.iter() {
|
||||
writer.write(&::jack::RawMidi { time: time as u32, bytes: &event })
|
||||
.expect(&format!("{event:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
543
src/main.rs
543
src/main.rs
|
|
@ -6,256 +6,321 @@ extern crate clap;
|
|||
extern crate jack;
|
||||
extern crate crossterm;
|
||||
|
||||
use clap::{Parser};
|
||||
use std::error::Error;
|
||||
|
||||
pub mod core;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod device;
|
||||
pub mod layout;
|
||||
pub mod control;
|
||||
pub mod core;
|
||||
pub mod model;
|
||||
pub mod view;
|
||||
|
||||
use crate::core::*;
|
||||
use crate::device::*;
|
||||
use crate::model::*;
|
||||
use crate::view::*;
|
||||
|
||||
mod new {
|
||||
use crate::core::*;
|
||||
use crate::layout::Stack;
|
||||
type Phrase = (String, usize, BTreeMap<usize, Vec<MidiMessage>>);
|
||||
type Scene = (String, Option<usize>, Vec<Option<usize>>);
|
||||
type Track = (String, usize);
|
||||
#[derive(Default)]
|
||||
pub struct App {
|
||||
client: Option<DynamicAsyncClient>,
|
||||
phrases: BTreeMap<usize, Phrase>,
|
||||
scenes: BTreeMap<usize, Scene>,
|
||||
tracks: BTreeMap<usize, Track>,
|
||||
frame: usize,
|
||||
scene: Vec<usize>,
|
||||
timebase: Arc<Timebase>,
|
||||
}
|
||||
struct SceneGrid {}
|
||||
impl Render for SceneGrid {}
|
||||
struct Chains {}
|
||||
impl Render for Chains {}
|
||||
struct Sequencer {}
|
||||
impl Render for Sequencer {}
|
||||
impl Render for App {
|
||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
crate::device::Transport {
|
||||
timebase: &self.timebase,
|
||||
playing: TransportState::Stopped,
|
||||
record: false,
|
||||
overdub: false,
|
||||
monitor: false,
|
||||
frame: 0,
|
||||
}.render(buf, area)?;
|
||||
SceneGrid {
|
||||
}.render(buf, area)?;
|
||||
Chains {}.render(buf, area)?;
|
||||
Sequencer {}.render(buf, area)?;
|
||||
Ok(area)
|
||||
pub fn main () -> Usually<()> {
|
||||
App::default().run(Some(|app: Arc<Mutex<App>>|{
|
||||
let mut state = app.lock().unwrap();
|
||||
let xdg = Arc::new(microxdg::XdgApp::new("tek")?);
|
||||
state.xdg = Some(xdg.clone());
|
||||
if crate::config::AppPaths::new(&xdg)?.should_create() {
|
||||
state.modal = Some(Box::new(crate::config::SetupModal(Some(xdg.clone()))));
|
||||
}
|
||||
}
|
||||
impl Handle for App {
|
||||
fn handle (&mut self, _e: &AppEvent) -> Usually<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
impl Process for App {
|
||||
fn process (&mut self, _c: &Client, _s: &ProcessScope) -> Control {
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
pub fn main () -> Usually<()> {
|
||||
App::default().run(Some(|app: Arc<Mutex<App>>|{
|
||||
app.lock().unwrap().client = Some(jack_run("tek", &app)?);
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn main () -> Usually<()> {
|
||||
let _cli = cli::Cli::parse();
|
||||
let xdg = microxdg::XdgApp::new("tek")?;
|
||||
crate::config::create_dirs(&xdg)?;
|
||||
//run(Sampler::new("Sampler#000")?)
|
||||
let (client, _) = Client::new("init", ClientOptions::NO_START_SERVER)?;
|
||||
let timebase = Arc::new(Timebase::new(client.sample_rate() as f64, 125.0, 96.0));
|
||||
let ppq = timebase.ppq() as usize;
|
||||
macro_rules! play {
|
||||
($t1:expr => [ $($msg:expr),* $(,)? ]) => {
|
||||
( $t1 * ppq / 4, vec![ $($msg),* ] )
|
||||
}
|
||||
}
|
||||
macro_rules! phrase {
|
||||
($($t:expr => $msg:expr),* $(,)?) => {{
|
||||
let mut phrase = BTreeMap::new();
|
||||
$(phrase.insert($t, vec![]);)*
|
||||
$(phrase.get_mut(&$t).unwrap().push($msg);)*
|
||||
phrase
|
||||
}}
|
||||
}
|
||||
Launcher::new("Launcher#0", &timebase,
|
||||
Some(vec![
|
||||
|
||||
Track::new("Drums", &timebase, Some(vec![
|
||||
|
||||
state.scenes = vec![
|
||||
Scene::new("Intro", vec![None, None, None, None]),
|
||||
];
|
||||
state.phrases = vec![
|
||||
Phrase::new("4 kicks", state.timebase.ppq() as usize * 4, None),
|
||||
];
|
||||
state.tracks = vec![
|
||||
Track::new("Drums", &state.timebase, Some(vec![
|
||||
Sampler::new("Sampler", Some(BTreeMap::from([
|
||||
sample!(36, "Kick", "/home/user/Lab/Music/pak/kik.wav"),
|
||||
sample!(40, "Snare", "/home/user/Lab/Music/pak/sna.wav"),
|
||||
sample!(44, "Hihat", "/home/user/Lab/Music/pak/chh.wav"),
|
||||
])))?.boxed(),
|
||||
|
||||
//Plugin::lv2("Panagement", "file:///home/user/.lv2/Auburn Sounds Panagement 2.lv2")?.boxed(),
|
||||
|
||||
]), Some(vec![
|
||||
|
||||
Phrase::new("KSH", ppq * 4, Some(phrase! {
|
||||
00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
00 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
01 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
02 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
04 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() },
|
||||
04 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
06 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
08 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
10 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
10 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
11 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
12 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() },
|
||||
12 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
14 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() },
|
||||
14 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }
|
||||
})),
|
||||
|
||||
Phrase::new("4K", ppq * 4, Some(phrase! {
|
||||
00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
04 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
08 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
12 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
})),
|
||||
|
||||
Phrase::new("KS", ppq * 4, Some(phrase! {
|
||||
00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
04 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() },
|
||||
10 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
12 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() },
|
||||
})),
|
||||
|
||||
]))?,
|
||||
|
||||
Track::new("Odin2", &timebase, Some(vec![
|
||||
|
||||
Plugin::lv2("Odin2", "file:///home/user/.lv2/Odin2.lv2")?.boxed(),
|
||||
|
||||
]), Some(vec![
|
||||
Phrase::new("E G A Bb", ppq * 4, Some(BTreeMap::from([
|
||||
play!(2 => [
|
||||
MidiMessage::NoteOff { key: 42.into(), vel: 100.into() },
|
||||
MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(6 => [
|
||||
MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
MidiMessage::NoteOn { key: 39.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(10 => [
|
||||
MidiMessage::NoteOff { key: 39.into(), vel: 100.into() },
|
||||
MidiMessage::NoteOn { key: 41.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(14 => [
|
||||
MidiMessage::NoteOff { key: 41.into(), vel: 100.into() },
|
||||
MidiMessage::NoteOn { key: 42.into(), vel: 100.into() },
|
||||
]),
|
||||
]))),
|
||||
Phrase::new("E E G Bb", ppq * 4, Some(BTreeMap::from([
|
||||
play!(2 => [
|
||||
MidiMessage::NoteOff { key: 42.into(), vel: 100.into() },
|
||||
MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(6 => [
|
||||
MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(10 => [
|
||||
MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
MidiMessage::NoteOn { key: 39.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(14 => [
|
||||
MidiMessage::NoteOff { key: 39.into(), vel: 100.into() },
|
||||
MidiMessage::NoteOn { key: 42.into(), vel: 100.into() },
|
||||
]),
|
||||
]))),
|
||||
Phrase::new("E E E E", ppq * 4, Some(BTreeMap::from([
|
||||
play!(0 => [
|
||||
MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(2 => [
|
||||
MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(4 => [
|
||||
MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(6 => [
|
||||
MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(8 => [
|
||||
MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(10 => [
|
||||
MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(12 => [
|
||||
MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
]),
|
||||
play!(14 => [
|
||||
MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
]),
|
||||
])))
|
||||
]))?,
|
||||
|
||||
//Plugin::lv2("Helm", "file:///home/user/.lv2/Helm.lv2", &[1, 0, 0, 2])?.boxed(),
|
||||
//Plugin::lv2("Kick/ChowKick", "file:///home/user/.lv2/ChowKick.lv2", &[1, 1, 0, 2])?.boxed(),
|
||||
//Plugin::lv2("Bass/Helm", "file:///home/user/.lv2/Helm.lv2", &[1, 0, 0, 2])?.boxed(),
|
||||
//Plugin::lv2("Pads/Odin2", "file:///home/user/.lv2/Odin2.lv2", &[1, 0, 0, 2])?.boxed(),
|
||||
]),
|
||||
Some(vec![
|
||||
Scene::new(&"Scene 1", &[Some(0), None, None, None]),
|
||||
Scene::new(&"Scene 2", &[Some(0), Some(0), None, None]),
|
||||
Scene::new(&"Scene 3", &[Some(1), Some(1), None, None]),
|
||||
Scene::new(&"Scene 4", &[Some(2), Some(2), None, None]),
|
||||
//Scene::new(&"Scene#03", &[None, Some(0), None, None]),
|
||||
//Scene::new(&"Scene#04", &[None, None, None, None]),
|
||||
//Scene::new(&"Scene#05", &[None, None, None, None]),
|
||||
])
|
||||
|
||||
)?
|
||||
.run(Some(init))
|
||||
]), None)?,
|
||||
];
|
||||
state.jack = Some(jack_run("tek", &app)?);
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn init (state: Arc<Mutex<DynamicDevice<Launcher>>>) -> Usually<()> {
|
||||
let input = ".*nanoKEY.*";
|
||||
let output = ["Komplete.*:playback_FL", "Komplete.*:playback_FR"];
|
||||
let state = state.lock().unwrap();
|
||||
state.connect(input, &output)?;
|
||||
Ok(())
|
||||
#[derive(Default)]
|
||||
pub struct App {
|
||||
pub xdg: Option<Arc<XdgApp>>,
|
||||
pub jack: Option<DynamicAsyncClient>,
|
||||
pub phrases: Vec<Phrase>,
|
||||
pub scenes: Vec<Scene>,
|
||||
pub tracks: Vec<Track>,
|
||||
pub frame: usize,
|
||||
pub scene: Vec<usize>,
|
||||
pub timebase: Arc<Timebase>,
|
||||
pub modal: Option<Box<dyn Component>>,
|
||||
}
|
||||
|
||||
process!(App);
|
||||
|
||||
render!(App |self, buf, area| {
|
||||
let Rect { x, mut y, width, height } = area;
|
||||
|
||||
y = y + TransportView {
|
||||
timebase: &self.timebase,
|
||||
playing: TransportState::Stopped,
|
||||
record: false,
|
||||
overdub: false,
|
||||
monitor: false,
|
||||
frame: 0,
|
||||
}.render(buf, area)?.height;
|
||||
|
||||
y = y + SceneGridView {
|
||||
buf,
|
||||
area: Rect { x, y, width, height: height / 3 },
|
||||
name: "",
|
||||
focused: true,
|
||||
scenes: &self.scenes,
|
||||
tracks: &self.tracks,
|
||||
cursor: &(0, 0)
|
||||
}.draw()?.height;
|
||||
|
||||
y = y + ChainView {
|
||||
focused: true,
|
||||
chain: None
|
||||
}.render(buf, Rect { x, y, width, height: height / 3 })?.height;
|
||||
|
||||
y = y + SequencerView {
|
||||
focused: true,
|
||||
ppq: self.timebase.ppq() as usize,
|
||||
track: None,
|
||||
phrase: None,
|
||||
}.render(buf, Rect { x, y, width, height })?.height;
|
||||
|
||||
if let Some(ref modal) = self.modal {
|
||||
for cell in buf.content.iter_mut() {
|
||||
cell.fg = ratatui::style::Color::Gray;
|
||||
cell.modifier = ratatui::style::Modifier::DIM;
|
||||
}
|
||||
modal.render(buf, area)?;
|
||||
}
|
||||
|
||||
Ok(area)
|
||||
});
|
||||
|
||||
handle!(App |self, e| {
|
||||
if let Some(ref mut modal) = self.modal {
|
||||
if modal.handle(e)? {
|
||||
self.modal = None;
|
||||
return Ok(true)
|
||||
};
|
||||
}
|
||||
handle_keymap(self, e, keymap!(App {
|
||||
[F(1), NONE, "toggle_help", "toggle help", toggle_help],
|
||||
|
||||
[Up, NONE, "cursor_up", "move cursor up", cursor_up],
|
||||
[Down, NONE, "cursor_down", "move cursor down", cursor_down],
|
||||
[Left, NONE, "cursor_left", "move cursor left", cursor_left],
|
||||
[Right, NONE, "cursor_right", "move cursor right", cursor_right],
|
||||
[Char('.'), NONE, "increment", "increment value at cursor", increment],
|
||||
[Char(','), NONE, "decrement", "decrement value at cursor", decrement],
|
||||
[Delete, CONTROL, "delete", "delete track", delete],
|
||||
[Char('d'), CONTROL, "duplicate", "duplicate scene or track", duplicate],
|
||||
[Enter, NONE, "activate", "activate item at cursor", activate],
|
||||
|
||||
[Tab, NONE, "focus_next", "focus next area", focus_next],
|
||||
[Tab, SHIFT, "focus_prev", "focus previous area", focus_prev],
|
||||
[Char(' '), NONE, "play_toggle", "play or pause", play_toggle],
|
||||
[Char('r'), NONE, "record_toggle", "toggle recording", record_toggle],
|
||||
[Char('d'), NONE, "overdub_toggle", "toggle overdub", overdub_toggle],
|
||||
[Char('m'), NONE, "monitor_toggle", "toggle input monitoring", monitor_toggle],
|
||||
[Char('r'), CONTROL, "rename", "rename current element", rename],
|
||||
[Char('t'), CONTROL, "add_track", "add a new track", add_track],
|
||||
//[Char(' '), SHIFT, "play_start", "play from start", play_start],
|
||||
}))
|
||||
});
|
||||
|
||||
fn increment (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn decrement (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn delete (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn duplicate (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn activate (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn rename (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn add_track (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn delete_track (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn cursor_up (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn cursor_down (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn cursor_left (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn cursor_right (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn toggle_help (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn focus_next (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn focus_prev (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn clip_next (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn clip_prev (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn play_toggle (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn record_toggle (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn overdub_toggle (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
fn monitor_toggle (_: &mut App) -> Usually<bool> { Ok(true) }
|
||||
|
||||
//fn main () -> Usually<()> {
|
||||
//let _cli = cli::Cli::parse();
|
||||
//let xdg = microxdg::XdgApp::new("tek")?;
|
||||
//crate::config::create_dirs(&xdg)?;
|
||||
////run(Sampler::new("Sampler#000")?)
|
||||
//let (client, _) = Client::new("init", ClientOptions::NO_START_SERVER)?;
|
||||
//let timebase = Arc::new(Timebase::new(client.sample_rate() as f64, 125.0, 96.0));
|
||||
//let ppq = timebase.ppq() as usize;
|
||||
//macro_rules! play {
|
||||
//($t1:expr => [ $($msg:expr),* $(,)? ]) => {
|
||||
//( $t1 * ppq / 4, vec![ $($msg),* ] )
|
||||
//}
|
||||
//}
|
||||
//macro_rules! phrase {
|
||||
//($($t:expr => $msg:expr),* $(,)?) => {{
|
||||
//let mut phrase = BTreeMap::new();
|
||||
//$(phrase.insert($t, vec![]);)*
|
||||
//$(phrase.get_mut(&$t).unwrap().push($msg);)*
|
||||
//phrase
|
||||
//}}
|
||||
//}
|
||||
//Launcher::new("Launcher#0", &timebase,
|
||||
//Some(vec![
|
||||
|
||||
//Track::new("Drums", &timebase, Some(vec![
|
||||
|
||||
//Sampler::new("Sampler", Some(BTreeMap::from([
|
||||
//sample!(36, "Kick", "/home/user/Lab/Music/pak/kik.wav"),
|
||||
//sample!(40, "Snare", "/home/user/Lab/Music/pak/sna.wav"),
|
||||
//sample!(44, "Hihat", "/home/user/Lab/Music/pak/chh.wav"),
|
||||
//])))?.boxed(),
|
||||
|
||||
////Plugin::lv2("Panagement", "file:///home/user/.lv2/Auburn Sounds Panagement 2.lv2")?.boxed(),
|
||||
|
||||
//]), Some(vec![
|
||||
|
||||
//Phrase::new("KSH", ppq * 4, Some(phrase! {
|
||||
//00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//00 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
//01 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
//02 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
//04 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() },
|
||||
//04 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
//06 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
//08 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
//10 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//10 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
//11 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
//12 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() },
|
||||
//12 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() },
|
||||
//14 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() },
|
||||
//14 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }
|
||||
//})),
|
||||
|
||||
//Phrase::new("4K", ppq * 4, Some(phrase! {
|
||||
//00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//04 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//08 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//12 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//})),
|
||||
|
||||
//Phrase::new("KS", ppq * 4, Some(phrase! {
|
||||
//00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//04 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() },
|
||||
//10 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//12 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() },
|
||||
//})),
|
||||
|
||||
//]))?,
|
||||
|
||||
//Track::new("Odin2", &timebase, Some(vec![
|
||||
|
||||
//Plugin::lv2("Odin2", "file:///home/user/.lv2/Odin2.lv2")?.boxed(),
|
||||
|
||||
//]), Some(vec![
|
||||
//Phrase::new("E G A Bb", ppq * 4, Some(BTreeMap::from([
|
||||
//play!(2 => [
|
||||
//MidiMessage::NoteOff { key: 42.into(), vel: 100.into() },
|
||||
//MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(6 => [
|
||||
//MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
//MidiMessage::NoteOn { key: 39.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(10 => [
|
||||
//MidiMessage::NoteOff { key: 39.into(), vel: 100.into() },
|
||||
//MidiMessage::NoteOn { key: 41.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(14 => [
|
||||
//MidiMessage::NoteOff { key: 41.into(), vel: 100.into() },
|
||||
//MidiMessage::NoteOn { key: 42.into(), vel: 100.into() },
|
||||
//]),
|
||||
//]))),
|
||||
//Phrase::new("E E G Bb", ppq * 4, Some(BTreeMap::from([
|
||||
//play!(2 => [
|
||||
//MidiMessage::NoteOff { key: 42.into(), vel: 100.into() },
|
||||
//MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(6 => [
|
||||
//MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
//MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(10 => [
|
||||
//MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
//MidiMessage::NoteOn { key: 39.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(14 => [
|
||||
//MidiMessage::NoteOff { key: 39.into(), vel: 100.into() },
|
||||
//MidiMessage::NoteOn { key: 42.into(), vel: 100.into() },
|
||||
//]),
|
||||
//]))),
|
||||
//Phrase::new("E E E E", ppq * 4, Some(BTreeMap::from([
|
||||
//play!(0 => [
|
||||
//MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(2 => [
|
||||
//MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(4 => [
|
||||
//MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(6 => [
|
||||
//MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(8 => [
|
||||
//MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(10 => [
|
||||
//MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(12 => [
|
||||
//MidiMessage::NoteOff { key: 36.into(), vel: 100.into() },
|
||||
//]),
|
||||
//play!(14 => [
|
||||
//MidiMessage::NoteOn { key: 36.into(), vel: 100.into() },
|
||||
//]),
|
||||
//])))
|
||||
//]))?,
|
||||
|
||||
////Plugin::lv2("Helm", "file:///home/user/.lv2/Helm.lv2", &[1, 0, 0, 2])?.boxed(),
|
||||
////Plugin::lv2("Kick/ChowKick", "file:///home/user/.lv2/ChowKick.lv2", &[1, 1, 0, 2])?.boxed(),
|
||||
////Plugin::lv2("Bass/Helm", "file:///home/user/.lv2/Helm.lv2", &[1, 0, 0, 2])?.boxed(),
|
||||
////Plugin::lv2("Pads/Odin2", "file:///home/user/.lv2/Odin2.lv2", &[1, 0, 0, 2])?.boxed(),
|
||||
//]),
|
||||
//Some(vec![
|
||||
//Scene::new(&"Scene 1", &[Some(0), None, None, None]),
|
||||
//Scene::new(&"Scene 2", &[Some(0), Some(0), None, None]),
|
||||
//Scene::new(&"Scene 3", &[Some(1), Some(1), None, None]),
|
||||
//Scene::new(&"Scene 4", &[Some(2), Some(2), None, None]),
|
||||
////Scene::new(&"Scene#03", &[None, Some(0), None, None]),
|
||||
////Scene::new(&"Scene#04", &[None, None, None, None]),
|
||||
////Scene::new(&"Scene#05", &[None, None, None, None]),
|
||||
//])
|
||||
|
||||
//)?
|
||||
//.run(Some(init))
|
||||
//}
|
||||
|
||||
//fn init (state: Arc<Mutex<DynamicDevice<Launcher>>>) -> Usually<()> {
|
||||
//let input = ".*nanoKEY.*";
|
||||
//let output = ["Komplete.*:playback_FL", "Komplete.*:playback_FR"];
|
||||
//let state = state.lock().unwrap();
|
||||
//state.connect(input, &output)?;
|
||||
//Ok(())
|
||||
//}
|
||||
|
|
|
|||
20
src/model.rs
Normal file
20
src/model.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
pub mod chain;
|
||||
pub mod launcher;
|
||||
pub mod looper;
|
||||
pub mod mixer;
|
||||
pub mod phrase;
|
||||
pub mod plugin;
|
||||
pub mod sampler;
|
||||
pub mod scene;
|
||||
pub mod sequencer;
|
||||
pub mod track;
|
||||
|
||||
pub use self::phrase::Phrase;
|
||||
pub use self::scene::Scene;
|
||||
pub use self::track::Track;
|
||||
pub use self::sequencer::{Sequencer, SequencerMode};
|
||||
pub use self::launcher::{Launcher, LauncherMode};
|
||||
pub use self::chain::Chain;
|
||||
pub use self::sampler::{Sampler, Sample};
|
||||
pub use self::mixer::Mixer;
|
||||
pub use self::plugin::{Plugin, PluginKind, lv2::LV2Plugin};
|
||||
46
src/model/chain.rs
Normal file
46
src/model/chain.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use crate::{core::*, view::*};
|
||||
|
||||
pub struct Chain {
|
||||
pub name: String,
|
||||
pub focused: bool,
|
||||
pub focus: usize,
|
||||
pub items: Vec<Box<dyn Device>>,
|
||||
pub view: ChainViewMode,
|
||||
pub adding: bool,
|
||||
}
|
||||
render!(Chain = crate::view::chain::render);
|
||||
handle!(Chain = crate::control::chain::handle);
|
||||
process!(Chain);
|
||||
impl Chain {
|
||||
pub fn new (name: &str, items: Option<Vec<Box<dyn Device>>>) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
name: name.into(),
|
||||
focused: false,
|
||||
focus: 0,
|
||||
items: items.unwrap_or_else(||vec![]),
|
||||
view: ChainViewMode::Column,
|
||||
adding: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PortList for Chain {
|
||||
fn midi_ins (&self) -> Usually<Vec<String>> {
|
||||
if let Some(device) = self.items.get(0) {
|
||||
device.midi_ins()
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
fn audio_outs (&self) -> Usually<Vec<String>> {
|
||||
if let Some(device) = self.items.get(self.items.len().saturating_sub(1)) {
|
||||
device.audio_outs()
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process (_: &mut Chain, _: &Client, _: &ProcessScope) -> Control {
|
||||
Control::Continue
|
||||
}
|
||||
184
src/model/launcher.rs
Normal file
184
src/model/launcher.rs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
use crate::{core::*, model::*};
|
||||
pub enum LauncherMode {
|
||||
Tracks,
|
||||
Sequencer,
|
||||
Chains
|
||||
}
|
||||
impl LauncherMode {
|
||||
pub fn is_chains (&self) -> bool {
|
||||
match self { Self::Chains => true, _ => false }
|
||||
}
|
||||
pub fn is_tracks (&self) -> bool {
|
||||
match self { Self::Tracks => true, _ => false }
|
||||
}
|
||||
pub fn is_sequencer (&self) -> bool {
|
||||
match self { Self::Sequencer => true, _ => false }
|
||||
}
|
||||
}
|
||||
pub struct Launcher {
|
||||
pub name: String,
|
||||
pub timebase: Arc<Timebase>,
|
||||
pub transport: ::jack::Transport,
|
||||
pub playing: TransportState,
|
||||
pub monitoring: bool,
|
||||
pub recording: bool,
|
||||
pub overdub: bool,
|
||||
pub current_frame: usize,
|
||||
pub cursor: (usize, usize),
|
||||
pub tracks: Vec<Track>,
|
||||
pub scenes: Vec<Scene>,
|
||||
pub show_help: bool,
|
||||
pub view: LauncherMode,
|
||||
}
|
||||
render!(Launcher = crate::view::launcher::render);
|
||||
handle!(Launcher = crate::control::launcher::handle);
|
||||
process!(Launcher |self, _client, _scope| {
|
||||
let transport = self.transport.query().unwrap();
|
||||
self.playing = transport.state;
|
||||
self.current_frame = transport.pos.frame() as usize;
|
||||
Control::Continue
|
||||
});
|
||||
impl PortList for Launcher {}
|
||||
impl Launcher {
|
||||
pub fn new (
|
||||
name: &str,
|
||||
timebase: &Arc<Timebase>,
|
||||
tracks: Option<Vec<Track>>,
|
||||
scenes: Option<Vec<Scene>>
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||
let transport = client.transport();
|
||||
let ppq = timebase.ppq() as usize;
|
||||
Ok(Self {
|
||||
name: name.into(),
|
||||
view: LauncherMode::Chains,
|
||||
playing: transport.query_state()?,
|
||||
transport,
|
||||
timebase: timebase.clone(),
|
||||
monitoring: true,
|
||||
recording: false,
|
||||
overdub: true,
|
||||
cursor: (2, 2),
|
||||
current_frame: 0,
|
||||
scenes: scenes.unwrap_or_else(||vec![Scene::new(&"Scene 1", &[None])]),
|
||||
tracks: if let Some(tracks) = tracks { tracks } else { vec![
|
||||
Track::new("Track 1", &timebase, None, Some(vec![
|
||||
Phrase::new("MIDI Clip 1", ppq * 4, Some(BTreeMap::from([
|
||||
( ppq * 0, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ),
|
||||
( ppq * 1, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ),
|
||||
( ppq * 2, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ),
|
||||
( ppq * 3, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ),
|
||||
])))
|
||||
]))?,
|
||||
] },
|
||||
show_help: true,
|
||||
})
|
||||
}
|
||||
pub fn connect (&self, midi_in: &str, audio_outs: &[&str]) -> Usually<()> {
|
||||
let (client, _status) = Client::new(
|
||||
&format!("{}-init", &self.name), ClientOptions::NO_START_SERVER
|
||||
)?;
|
||||
let midi_ins = client.ports(Some(midi_in), None, PortFlags::IS_OUTPUT);
|
||||
let audio_outs: Vec<Vec<String>> = audio_outs.iter()
|
||||
.map(|pattern|client.ports(Some(pattern), None, PortFlags::IS_INPUT))
|
||||
.collect();
|
||||
for (i, sequencer) in self.tracks.iter().enumerate() {
|
||||
for sequencer_midi_in in sequencer.midi_ins()?.iter() {
|
||||
for midi_in in midi_ins.iter() {
|
||||
client.connect_ports_by_name(&midi_in, &sequencer_midi_in)?;
|
||||
}
|
||||
}
|
||||
let chain: &Chain = &self.tracks[i].chain;
|
||||
for port in sequencer.midi_outs()?.iter() {
|
||||
for midi_in in chain.midi_ins()?.iter() {
|
||||
client.connect_ports_by_name(&port, &midi_in)?;
|
||||
}
|
||||
}
|
||||
for (j, port) in chain.audio_outs()?.iter().enumerate() {
|
||||
for audio_out in audio_outs[j % audio_outs.len()].iter() {
|
||||
client.connect_ports_by_name(&port, &audio_out)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn cols (&self) -> usize {
|
||||
(self.tracks.len() + 2) as usize
|
||||
}
|
||||
pub fn col (&self) -> usize {
|
||||
self.cursor.0 as usize
|
||||
}
|
||||
pub fn dec_col (&mut self) {
|
||||
self.cursor.0 = if self.cursor.0 > 0 {
|
||||
self.cursor.0 - 1
|
||||
} else {
|
||||
(self.cols() - 1) as usize
|
||||
}
|
||||
}
|
||||
pub fn inc_col (&mut self) {
|
||||
self.cursor.0 = if self.cursor.0 >= self.cols() - 1 {
|
||||
0
|
||||
} else {
|
||||
self.cursor.0 + 1
|
||||
}
|
||||
}
|
||||
pub fn rows (&self) -> usize {
|
||||
(self.scenes.len() + 2) as usize
|
||||
}
|
||||
pub fn row (&self) -> usize {
|
||||
self.cursor.1 as usize
|
||||
}
|
||||
pub fn dec_row (&mut self) {
|
||||
self.cursor.1 = if self.cursor.1 > 0 {
|
||||
self.cursor.1 - 1
|
||||
} else {
|
||||
self.rows() - 1
|
||||
}
|
||||
}
|
||||
pub fn inc_row (&mut self) {
|
||||
self.cursor.1 = if self.cursor.1 >= self.rows() - 1 {
|
||||
0
|
||||
} else {
|
||||
self.cursor.1 + 1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track (&self) -> Option<(usize, &Track)> {
|
||||
match self.col() { 0 => None, _ => {
|
||||
let id = self.col() as usize - 1;
|
||||
self.tracks.get(id).map(|t|(id, t))
|
||||
} }
|
||||
}
|
||||
pub fn track_mut (&mut self) -> Option<(usize, &mut Track)> {
|
||||
match self.col() { 0 => None, _ => {
|
||||
let id = self.col() as usize - 1;
|
||||
self.tracks.get_mut(id).map(|t|(id, t))
|
||||
} }
|
||||
}
|
||||
pub fn scene (&self) -> Option<(usize, &Scene)> {
|
||||
match self.row() { 0 => None, _ => {
|
||||
let id = self.row() as usize - 1;
|
||||
self.scenes.get(id).map(|t|(id, t))
|
||||
} }
|
||||
}
|
||||
pub fn scene_mut (&mut self) -> Option<(usize, &mut Scene)> {
|
||||
match self.row() { 0 => None, _ => {
|
||||
let id = self.row() as usize - 1;
|
||||
self.scenes.get_mut(id).map(|t|(id, t))
|
||||
} }
|
||||
}
|
||||
pub fn sequencer (&self) -> Option<&Sequencer> {
|
||||
Some(&self.track()?.1.sequencer)
|
||||
}
|
||||
pub fn sequencer_mut (&mut self) -> Option<&mut Sequencer> {
|
||||
Some(&mut self.track_mut()?.1.sequencer)
|
||||
}
|
||||
pub fn chain (&self) -> Option<&Chain> {
|
||||
Some(&self.track()?.1.chain)
|
||||
}
|
||||
pub fn phrase_id (&self) -> Option<usize> {
|
||||
let (track_id, _) = self.track()?;
|
||||
let (_, scene) = self.scene()?;
|
||||
*scene.clips.get(track_id)?
|
||||
}
|
||||
}
|
||||
132
src/model/mixer.rs
Normal file
132
src/model/mixer.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
use crate::core::*;
|
||||
|
||||
pub struct Mixer {
|
||||
pub name: String,
|
||||
pub tracks: Vec<MixerTrack>,
|
||||
pub selected_track: usize,
|
||||
pub selected_column: usize,
|
||||
}
|
||||
|
||||
impl Mixer {
|
||||
pub fn new (name: &str) -> Result<DynamicDevice<Self>, Box<dyn Error>> {
|
||||
let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||
Ok(DynamicDevice::new(
|
||||
crate::view::mixer::render,
|
||||
crate::control::mixer::handle,
|
||||
process,
|
||||
Self {
|
||||
name: name.into(),
|
||||
selected_column: 0,
|
||||
selected_track: 1,
|
||||
tracks: vec![
|
||||
MixerTrack::new(&client, 1, "Mono 1")?,
|
||||
MixerTrack::new(&client, 1, "Mono 2")?,
|
||||
MixerTrack::new(&client, 2, "Stereo 1")?,
|
||||
MixerTrack::new(&client, 2, "Stereo 2")?,
|
||||
MixerTrack::new(&client, 2, "Stereo 3")?,
|
||||
MixerTrack::new(&client, 2, "Bus 1")?,
|
||||
MixerTrack::new(&client, 2, "Bus 2")?,
|
||||
MixerTrack::new(&client, 2, "Mix")?,
|
||||
],
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process (
|
||||
_: &mut Mixer,
|
||||
_: &Client,
|
||||
_: &ProcessScope
|
||||
) -> Control {
|
||||
Control::Continue
|
||||
}
|
||||
|
||||
pub struct MixerTrack {
|
||||
pub name: String,
|
||||
pub channels: u8,
|
||||
pub input_ports: Vec<Port<AudioIn>>,
|
||||
pub pre_gain_meter: f64,
|
||||
pub gain: f64,
|
||||
pub insert_ports: Vec<Port<AudioOut>>,
|
||||
pub return_ports: Vec<Port<AudioIn>>,
|
||||
pub post_gain_meter: f64,
|
||||
pub post_insert_meter: f64,
|
||||
pub level: f64,
|
||||
pub pan: f64,
|
||||
pub output_ports: Vec<Port<AudioOut>>,
|
||||
pub post_fader_meter: f64,
|
||||
pub route: String,
|
||||
}
|
||||
|
||||
impl MixerTrack {
|
||||
pub fn new (jack: &Client, channels: u8, name: &str) -> Result<Self, Box<dyn Error>> {
|
||||
let mut input_ports = vec![];
|
||||
let mut insert_ports = vec![];
|
||||
let mut return_ports = vec![];
|
||||
let mut output_ports = vec![];
|
||||
for channel in 1..=channels {
|
||||
input_ports.push(jack.register_port(&format!("{name} [input {channel}]"), AudioIn::default())?);
|
||||
output_ports.push(jack.register_port(&format!("{name} [out {channel}]"), AudioOut::default())?);
|
||||
let insert_port = jack.register_port(&format!("{name} [pre {channel}]"), AudioOut::default())?;
|
||||
let return_port = jack.register_port(&format!("{name} [insert {channel}]"), AudioIn::default())?;
|
||||
jack.connect_ports(&insert_port, &return_port)?;
|
||||
insert_ports.push(insert_port);
|
||||
return_ports.push(return_port);
|
||||
}
|
||||
Ok(Self {
|
||||
name: name.into(),
|
||||
channels,
|
||||
input_ports,
|
||||
pre_gain_meter: 0.0,
|
||||
gain: 0.0,
|
||||
post_gain_meter: 0.0,
|
||||
insert_ports,
|
||||
return_ports,
|
||||
post_insert_meter: 0.0,
|
||||
level: 0.0,
|
||||
pan: 0.0,
|
||||
post_fader_meter: 0.0,
|
||||
route: "---".into(),
|
||||
output_ports,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//impl<W: Write> Input<TUI<W>, bool> for Mixer {
|
||||
//fn handle (&mut self, engine: &mut TUI<W>) -> Result<Option<bool>> {
|
||||
//Ok(None)
|
||||
//}
|
||||
//
|
||||
|
||||
//impl<W: Write> Output<TUI<W>, [u16;2]> for Mixer {
|
||||
//fn render (&self, envine: &mut TUI<W>) -> Result<Option<[u16;2]>> {
|
||||
|
||||
//let tracks_table = Columns::new()
|
||||
//.add(titles)
|
||||
//.add(input_meters)
|
||||
//.add(gains)
|
||||
//.add(gain_meters)
|
||||
//.add(pres)
|
||||
//.add(pre_meters)
|
||||
//.add(levels)
|
||||
//.add(pans)
|
||||
//.add(pan_meters)
|
||||
//.add(posts)
|
||||
//.add(routes)
|
||||
|
||||
//Rows::new()
|
||||
//.add(Columns::new()
|
||||
//.add(Rows::new()
|
||||
//.add("[Arrows]".bold())
|
||||
//.add("Navigate"))
|
||||
//.add(Rows::new()
|
||||
//.add("[+/-]".bold())
|
||||
//.add("Adjust"))
|
||||
//.add(Rows::new()
|
||||
//.add("[Ins/Del]".bold())
|
||||
//.add("Add/remove track")))
|
||||
//.add(tracks_table)
|
||||
//.render(engine)
|
||||
//}
|
||||
//}
|
||||
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
use crate::core::*;
|
||||
|
||||
pub type PhraseData = BTreeMap<usize, Vec<MidiMessage>>;
|
||||
|
||||
pub type MIDIMessage = Vec<u8>;
|
||||
|
||||
pub type MIDIChunk = [Option<Vec<MIDIMessage>>];
|
||||
|
||||
pub struct Phrase {
|
||||
pub name: String,
|
||||
pub length: usize,
|
||||
pub notes: PhraseData,
|
||||
}
|
||||
|
||||
impl Default for Phrase {
|
||||
fn default () -> Self {
|
||||
Self::new("", 0, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Phrase {
|
||||
pub fn new (name: &str, length: usize, notes: Option<PhraseData>) -> Self {
|
||||
Self { name: name.to_string(), length, notes: notes.unwrap_or(BTreeMap::new()) }
|
||||
|
|
@ -127,9 +127,3 @@ impl Phrase {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
impl Default for Phrase {
|
||||
fn default () -> Self {
|
||||
Self::new("", 0, None)
|
||||
}
|
||||
}
|
||||
176
src/model/plugin.rs
Normal file
176
src/model/plugin.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
use crate::core::*;
|
||||
|
||||
pub mod lv2;
|
||||
pub mod vst2;
|
||||
pub mod vst3;
|
||||
|
||||
use self::lv2::*;
|
||||
|
||||
pub struct Plugin {
|
||||
pub name: String,
|
||||
pub path: Option<String>,
|
||||
pub plugin: Option<PluginKind>,
|
||||
pub selected: usize,
|
||||
pub mapping: bool,
|
||||
pub midi_ins: Vec<Port<MidiIn>>,
|
||||
pub midi_outs: Vec<Port<MidiOut>>,
|
||||
pub audio_ins: Vec<Port<AudioIn>>,
|
||||
pub audio_outs: Vec<Port<AudioOut>>,
|
||||
}
|
||||
|
||||
pub enum PluginKind {
|
||||
LV2(LV2Plugin),
|
||||
VST2 {
|
||||
instance: ::vst::host::PluginInstance
|
||||
},
|
||||
VST3,
|
||||
}
|
||||
|
||||
const HELM: &'static str = "file:///nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lv2/helm.lv2";
|
||||
|
||||
impl Plugin {
|
||||
/// Load a LV2 plugin.
|
||||
pub fn lv2 (name: &str, path: &str) -> Usually<DynamicDevice<Self>> {
|
||||
let host = Self::new(name)?;
|
||||
let plugin = LV2Plugin::new(path)?;
|
||||
let mut state = host.state();
|
||||
let client = host.client.as_ref().unwrap().as_client();
|
||||
let (midi_ins, midi_outs, audio_ins, audio_outs) = (
|
||||
plugin.plugin.port_counts().atom_sequence_inputs,
|
||||
plugin.plugin.port_counts().atom_sequence_outputs,
|
||||
plugin.plugin.port_counts().audio_inputs,
|
||||
plugin.plugin.port_counts().audio_outputs,
|
||||
);
|
||||
state.midi_ins = {
|
||||
let mut ports = vec![];
|
||||
for i in 0..midi_ins {
|
||||
ports.push(client.register_port(&format!("midi-in-{i}"), MidiIn::default())?)
|
||||
}
|
||||
ports
|
||||
};
|
||||
state.midi_outs = {
|
||||
let mut ports = vec![];
|
||||
for i in 0..midi_outs {
|
||||
ports.push(client.register_port(&format!("midi-out-{i}"), MidiOut::default())?)
|
||||
}
|
||||
ports
|
||||
};
|
||||
state.audio_ins = {
|
||||
let mut ports = vec![];
|
||||
for i in 0..audio_ins {
|
||||
ports.push(client.register_port(&format!("audio-in-{i}"), AudioIn::default())?)
|
||||
}
|
||||
ports
|
||||
};
|
||||
state.audio_outs = {
|
||||
let mut ports = vec![];
|
||||
for i in 0..audio_outs {
|
||||
ports.push(client.register_port(&format!("audio-out-{i}"), AudioOut::default())?)
|
||||
}
|
||||
ports
|
||||
};
|
||||
state.plugin = Some(PluginKind::LV2(plugin));
|
||||
state.path = Some(String::from(path));
|
||||
std::mem::drop(state);
|
||||
Ok(host)
|
||||
}
|
||||
pub fn new (name: &str) -> Usually<DynamicDevice<Self>> {
|
||||
DynamicDevice::new(
|
||||
crate::view::plugin::render,
|
||||
crate::control::plugin::handle,
|
||||
Self::process,
|
||||
Self {
|
||||
name: name.into(),
|
||||
path: None,
|
||||
plugin: None,
|
||||
selected: 0,
|
||||
mapping: false,
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
audio_ins: vec![],
|
||||
audio_outs: vec![],
|
||||
}
|
||||
).activate(Client::new(name, ClientOptions::NO_START_SERVER)?.0)
|
||||
}
|
||||
pub fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
match self.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin { features, ref mut instance, .. })) => {
|
||||
let urid = features.midi_urid();
|
||||
let mut inputs = vec![];
|
||||
for port in self.midi_ins.iter() {
|
||||
let mut atom = ::livi::event::LV2AtomSequence::new(
|
||||
&features,
|
||||
scope.n_frames() as usize
|
||||
);
|
||||
for event in port.iter(scope) {
|
||||
match event.bytes.len() {
|
||||
3 => atom.push_midi_event::<3>(
|
||||
event.time as i64,
|
||||
urid,
|
||||
&event.bytes[0..3]
|
||||
).unwrap(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
inputs.push(atom);
|
||||
}
|
||||
let mut outputs = vec![];
|
||||
for _ in self.midi_outs.iter() {
|
||||
outputs.push(::livi::event::LV2AtomSequence::new(
|
||||
&features,
|
||||
scope.n_frames() as usize
|
||||
));
|
||||
}
|
||||
let ports = ::livi::EmptyPortConnections::new()
|
||||
.with_atom_sequence_inputs(
|
||||
inputs.iter()
|
||||
)
|
||||
.with_atom_sequence_outputs(
|
||||
outputs.iter_mut()
|
||||
)
|
||||
.with_audio_inputs(
|
||||
self.audio_ins.iter().map(|o|o.as_slice(scope))
|
||||
)
|
||||
.with_audio_outputs(
|
||||
self.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))
|
||||
);
|
||||
unsafe {
|
||||
instance.run(scope.n_frames() as usize, ports).unwrap()
|
||||
};
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl PortList for Plugin {
|
||||
fn audio_ins (&self) -> Usually<Vec<String>> {
|
||||
let mut ports = vec![];
|
||||
for port in self.audio_ins.iter() {
|
||||
ports.push(port.name()?);
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
fn audio_outs (&self) -> Usually<Vec<String>> {
|
||||
let mut ports = vec![];
|
||||
for port in self.audio_outs.iter() {
|
||||
ports.push(port.name()?);
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
fn midi_ins (&self) -> Usually<Vec<String>> {
|
||||
let mut ports = vec![];
|
||||
for port in self.midi_ins.iter() {
|
||||
ports.push(port.name()?);
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
fn midi_outs (&self) -> Usually<Vec<String>> {
|
||||
let mut ports = vec![];
|
||||
for port in self.midi_outs.iter() {
|
||||
ports.push(port.name()?);
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
}
|
||||
|
|
@ -39,3 +39,4 @@ impl LV2Plugin {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ use super::*;
|
|||
|
||||
impl ::vst::host::Host for Plugin {}
|
||||
|
||||
fn set_vst_plugin (host: &Arc<Mutex<Plugin>>, path: &str) -> Usually<PluginKind> {
|
||||
fn set_vst_plugin (host: &Arc<Mutex<Plugin>>, _path: &str) -> Usually<PluginKind> {
|
||||
let mut loader = ::vst::host::PluginLoader::load(
|
||||
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
|
||||
host.clone()
|
||||
|
|
@ -12,3 +12,4 @@ fn set_vst_plugin (host: &Arc<Mutex<Plugin>>, path: &str) -> Usually<PluginKind>
|
|||
instance: loader.instance()?
|
||||
})
|
||||
}
|
||||
|
||||
175
src/model/sampler.rs
Normal file
175
src/model/sampler.rs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
use crate::core::*;
|
||||
|
||||
pub struct Voice {
|
||||
pub sample: Arc<Sample>,
|
||||
pub after: usize,
|
||||
pub position: usize,
|
||||
}
|
||||
|
||||
pub struct Sample {
|
||||
pub name: String,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub channels: Vec<Vec<f32>>,
|
||||
}
|
||||
|
||||
pub struct Sampler {
|
||||
pub name: String,
|
||||
pub cursor: (usize, usize),
|
||||
pub samples: BTreeMap<u7, Arc<Sample>>,
|
||||
pub voices: Vec<Voice>,
|
||||
pub midi_in: Port<MidiIn>,
|
||||
pub audio_ins: Vec<Port<AudioIn>>,
|
||||
pub audio_outs: Vec<Port<AudioOut>>,
|
||||
}
|
||||
|
||||
impl Voice {
|
||||
pub fn chunk (&mut self, mut frames: usize) -> Option<Vec<Vec<f32>>> {
|
||||
// Create output buffer for each channel
|
||||
let mut chunk = vec![vec![];self.sample.channels.len()];
|
||||
// If it's not time to play yet, count down
|
||||
if self.after >= frames {
|
||||
self.after = self.after - frames;
|
||||
return Some(chunk)
|
||||
}
|
||||
// If the voice will start playing within the current buffer,
|
||||
// subtract the remaining number of wait frames.
|
||||
if self.after > 0 && self.after < frames {
|
||||
chunk = vec![vec![0.0;self.after];self.sample.channels.len()];
|
||||
frames = frames - self.after;
|
||||
self.after = 0;
|
||||
}
|
||||
if self.position < self.sample.end {
|
||||
let start = self.position.min(self.sample.end);
|
||||
let end = (self.position + frames).min(self.sample.end);
|
||||
for (i, channel) in self.sample.channels.iter().enumerate() {
|
||||
chunk[i].extend_from_slice(&channel[start..end]);
|
||||
};
|
||||
self.position = self.position + frames;
|
||||
Some(chunk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sample {
|
||||
pub fn new (name: &str, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Arc<Self> {
|
||||
Arc::new(Self { name: name.to_string(), start, end, channels })
|
||||
}
|
||||
pub fn play (self: &Arc<Self>, after: usize) -> Voice {
|
||||
Voice { sample: self.clone(), after, position: self.start }
|
||||
}
|
||||
}
|
||||
|
||||
impl Sampler {
|
||||
pub fn new (
|
||||
name: &str,
|
||||
samples: Option<BTreeMap<u7, Arc<Sample>>>,
|
||||
) -> Result<DynamicDevice<Self>, Box<dyn Error>> {
|
||||
let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||
DynamicDevice::new(
|
||||
crate::view::sampler::render,
|
||||
crate::control::sampler::handle,
|
||||
Self::process,
|
||||
Self {
|
||||
name: name.into(),
|
||||
cursor: (0, 0),
|
||||
samples: samples.unwrap_or(BTreeMap::new()),
|
||||
voices: vec![],
|
||||
midi_in: client.register_port("midi", ::jack::MidiIn::default())?,
|
||||
audio_ins: vec![
|
||||
client.register_port("recL", ::jack::AudioIn::default())?,
|
||||
client.register_port("recR", ::jack::AudioIn::default())?,
|
||||
],
|
||||
audio_outs: vec![
|
||||
client.register_port("outL", ::jack::AudioOut::default())?,
|
||||
client.register_port("outR", ::jack::AudioOut::default())?,
|
||||
],
|
||||
}).activate(client)
|
||||
}
|
||||
|
||||
pub fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
// Output buffer: this will be copied to the audio outs.
|
||||
let channel_count = self.audio_outs.len();
|
||||
let mut mixed = vec![vec![0.0;scope.n_frames() as usize];channel_count];
|
||||
// Process MIDI input to add new voices.
|
||||
for RawMidi { time, bytes } in self.midi_in.iter(scope) {
|
||||
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
|
||||
if let MidiMessage::NoteOn { ref key, .. } = message {
|
||||
if let Some(sample) = self.samples.get(key) {
|
||||
self.voices.push(sample.play(time as usize));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Emit next chunk of each currently playing voice,
|
||||
// dropping voices that have reached their ends.
|
||||
let mut voices = vec![];
|
||||
std::mem::swap(&mut voices, &mut self.voices);
|
||||
loop {
|
||||
if voices.len() < 1 {
|
||||
break
|
||||
}
|
||||
let mut voice = voices.swap_remove(0);
|
||||
if let Some(chunk) = voice.chunk(scope.n_frames() as usize) {
|
||||
for (i, channel) in chunk.iter().enumerate() {
|
||||
let buffer = &mut mixed[i % channel_count];
|
||||
for (i, sample) in channel.iter().enumerate() {
|
||||
buffer[i] += sample;
|
||||
}
|
||||
}
|
||||
self.voices.push(voice);
|
||||
}
|
||||
}
|
||||
// Write output buffer to output ports.
|
||||
for (i, port) in self.audio_outs.iter_mut().enumerate() {
|
||||
let buffer = &mixed[i];
|
||||
for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() {
|
||||
*value = *buffer.get(i).unwrap_or(&0.0);
|
||||
}
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
|
||||
fn load_sample (&mut self, _path: &str) {}
|
||||
}
|
||||
|
||||
impl PortList for Sampler {
|
||||
fn midi_ins (&self) -> Usually<Vec<String>> {
|
||||
Ok(vec![self.midi_in.name()?])
|
||||
}
|
||||
fn audio_ins (&self) -> Usually<Vec<String>> {
|
||||
let mut ports = vec![];
|
||||
for port in self.audio_ins.iter() {
|
||||
ports.push(port.name()?);
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
fn audio_outs (&self) -> Usually<Vec<String>> {
|
||||
let mut ports = vec![];
|
||||
for port in self.audio_outs.iter() {
|
||||
ports.push(port.name()?);
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
};
|
||||
}
|
||||
141
src/model/sequencer.rs
Normal file
141
src/model/sequencer.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
use crate::core::*;
|
||||
use crate::model::*;
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SequencerMode { Tiny, Compact, Horizontal, Vertical, }
|
||||
pub struct Sequencer {
|
||||
pub name: String,
|
||||
/// JACK transport handle.
|
||||
pub transport: ::jack::Transport,
|
||||
/// JACK MIDI input port that will be created.
|
||||
pub midi_in: Port<MidiIn>,
|
||||
/// JACK MIDI output port that will be created.
|
||||
pub midi_out: Port<MidiOut>,
|
||||
/// Holds info about tempo
|
||||
pub timebase: Arc<Timebase>,
|
||||
/// Phrase selector
|
||||
pub sequence: Option<usize>,
|
||||
/// Map: tick -> MIDI events at tick
|
||||
pub phrases: Vec<Phrase>,
|
||||
/// Red keys on piano roll.
|
||||
pub notes_on: Vec<bool>,
|
||||
/// Play sequence to output.
|
||||
pub playing: TransportState,
|
||||
/// Play input through output.
|
||||
pub monitoring: bool,
|
||||
/// Write input to sequence.
|
||||
pub recording: bool,
|
||||
/// Don't delete when recording.
|
||||
pub overdub: bool,
|
||||
/// Display mode
|
||||
pub view: SequencerMode,
|
||||
/// Range of notes to display
|
||||
pub note_start: usize,
|
||||
/// Position of cursor within note range
|
||||
pub note_cursor: usize,
|
||||
/// PPM per display unit
|
||||
pub time_zoom: usize,
|
||||
/// Range of time steps to display
|
||||
pub time_start: usize,
|
||||
/// Position of cursor within time range
|
||||
pub time_cursor: usize,
|
||||
}
|
||||
render!(Sequencer = crate::view::sequencer::render);
|
||||
handle!(Sequencer = crate::control::sequencer::handle);
|
||||
process!(Sequencer |self, _client, scope| {
|
||||
if self.sequence.is_none() { return Control::Continue }
|
||||
let phrase = self.phrases.get_mut(self.sequence.unwrap());
|
||||
if phrase.is_none() { return Control::Continue }
|
||||
let phrase = phrase.unwrap();
|
||||
let frame = scope.last_frame_time() as usize;
|
||||
let frames = scope.n_frames() as usize;
|
||||
let mut output: Vec<Option<Vec<Vec<u8>>>> = vec![None;frames];
|
||||
let transport = self.transport.query().unwrap();
|
||||
if transport.state != self.playing {
|
||||
all_notes_off(&mut output);
|
||||
}
|
||||
self.playing = transport.state;
|
||||
// Play from phrase into output buffer
|
||||
if self.playing == TransportState::Rolling {
|
||||
phrase.process_out(
|
||||
&mut output,
|
||||
&mut self.notes_on,
|
||||
&self.timebase,
|
||||
frame,
|
||||
frames
|
||||
);
|
||||
}
|
||||
// Play from input to monitor, and record into phrase.
|
||||
phrase.process_in(
|
||||
self.midi_in.iter(scope),
|
||||
&mut self.notes_on,
|
||||
if self.monitoring { Some(&mut output) } else { None },
|
||||
self.recording && self.playing == TransportState::Rolling,
|
||||
&self.timebase,
|
||||
frame,
|
||||
);
|
||||
write_output(&mut self.midi_out.writer(scope), &mut output, frames);
|
||||
Control::Continue
|
||||
});
|
||||
impl Sequencer {
|
||||
pub fn new (
|
||||
name: &str,
|
||||
timebase: &Arc<Timebase>,
|
||||
phrases: Option<Vec<Phrase>>,
|
||||
) -> Usually<Self> {
|
||||
let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||
let transport = client.transport();
|
||||
Ok(Self {
|
||||
name: name.into(),
|
||||
timebase: timebase.clone(),
|
||||
phrases: phrases.unwrap_or_else(||vec![Phrase::default()]),
|
||||
sequence: Some(0),
|
||||
|
||||
transport,
|
||||
midi_in: client.register_port("in", MidiIn::default())?,
|
||||
monitoring: true,
|
||||
recording: true,
|
||||
midi_out: client.register_port("out", MidiOut::default())?,
|
||||
playing: TransportState::Starting,
|
||||
overdub: true,
|
||||
|
||||
view: SequencerMode::Horizontal,
|
||||
notes_on: vec![false;128],
|
||||
note_start: 12,
|
||||
note_cursor: 0,
|
||||
time_zoom: 24,
|
||||
time_start: 0,
|
||||
time_cursor: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn phrase <'a> (&'a self) -> Option<&'a Phrase> {
|
||||
self.phrases.get(self.sequence?)
|
||||
}
|
||||
}
|
||||
impl PortList for Sequencer {
|
||||
fn midi_ins (&self) -> Usually<Vec<String>> { Ok(vec![self.midi_in.name()?]) }
|
||||
fn midi_outs (&self) -> Usually<Vec<String>> { Ok(vec![self.midi_out.name()?]) }
|
||||
}
|
||||
/// Add "all notes off" to the start of a buffer.
|
||||
pub fn all_notes_off (output: &mut MIDIChunk) {
|
||||
output[0] = Some(vec![]);
|
||||
if let Some(Some(frame)) = output.get_mut(0) {
|
||||
let mut buf = vec![];
|
||||
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
||||
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
||||
evt.write(&mut buf).unwrap();
|
||||
frame.push(buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Write to JACK port from output buffer (containing notes from sequence and/or monitor)
|
||||
fn write_output (writer: &mut ::jack::MidiWriter, output: &mut MIDIChunk, frames: usize) {
|
||||
for time in 0..frames {
|
||||
if let Some(Some(frame)) = output.get_mut(time ) {
|
||||
for event in frame.iter() {
|
||||
writer.write(&::jack::RawMidi { time: time as u32, bytes: &event })
|
||||
.expect(&format!("{event:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
use crate::core::*;
|
||||
use crate::device::*;
|
||||
use crate::layout::*;
|
||||
use crate::model::*;
|
||||
|
||||
pub struct Track {
|
||||
pub name: String,
|
||||
pub sequencer: DynamicDevice<Sequencer>,
|
||||
pub chain: DynamicDevice<Chain>,
|
||||
pub sequencer: Sequencer,
|
||||
pub chain: Chain,
|
||||
}
|
||||
|
||||
impl Track {
|
||||
pub fn new (
|
||||
name: &str,
|
||||
|
|
@ -15,10 +16,11 @@ impl Track {
|
|||
) -> Usually<Self> {
|
||||
let sequencer = Sequencer::new(&name, tempo, phrases)?;
|
||||
let chain = Chain::new(&name, devices)?;
|
||||
let (client, _status) = Client::new("init", ClientOptions::NO_START_SERVER)?;
|
||||
{
|
||||
if let (Some(output), Some(input)) = (
|
||||
sequencer.midi_outs()?.get(0).clone(),
|
||||
if let Some(item) = chain.state().items.get(0) {
|
||||
if let Some(item) = chain.items.get(0) {
|
||||
if let Some(port) = item.midi_ins()?.get(0) {
|
||||
Some(port.clone())
|
||||
} else {
|
||||
|
|
@ -28,14 +30,13 @@ impl Track {
|
|||
None
|
||||
}
|
||||
) {
|
||||
if let Some(client) = &sequencer.client {
|
||||
client.as_client().connect_ports_by_name(&output, &input)?;
|
||||
}
|
||||
client.connect_ports_by_name(&output, &input)?;
|
||||
}
|
||||
}
|
||||
Ok(Self { name: name.to_string(), sequencer, chain })
|
||||
}
|
||||
}
|
||||
|
||||
impl PortList for Track {
|
||||
fn midi_ins (&self) -> Usually<Vec<String>> {
|
||||
self.sequencer.midi_ins()
|
||||
|
|
@ -44,3 +45,4 @@ impl PortList for Track {
|
|||
self.chain.audio_outs()
|
||||
}
|
||||
}
|
||||
|
||||
15
src/view.rs
Normal file
15
src/view.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
pub mod chain;
|
||||
pub mod grid;
|
||||
pub mod layout;
|
||||
pub mod launcher;
|
||||
pub mod mixer;
|
||||
pub mod sampler;
|
||||
pub mod sequencer;
|
||||
pub mod transport;
|
||||
pub mod plugin;
|
||||
|
||||
pub use self::layout::*;
|
||||
pub use self::transport::TransportView;
|
||||
pub use self::grid::SceneGridView;
|
||||
pub use self::chain::{ChainView, ChainViewMode};
|
||||
pub use self::sequencer::SequencerView;
|
||||
|
|
@ -1,57 +1,34 @@
|
|||
mod plugin; pub use self::plugin::*;
|
||||
mod sampler; pub use self::sampler::*;
|
||||
|
||||
use crate::core::*;
|
||||
use crate::layout::*;
|
||||
use crate::view::*;
|
||||
use crate::model::*;
|
||||
|
||||
pub struct Chain {
|
||||
pub name: String,
|
||||
pub focused: bool,
|
||||
pub focus: usize,
|
||||
pub items: Vec<Box<dyn Device>>,
|
||||
pub view: ChainView,
|
||||
pub adding: bool,
|
||||
}
|
||||
|
||||
pub enum ChainView {
|
||||
pub enum ChainViewMode {
|
||||
Hidden,
|
||||
Compact,
|
||||
Row,
|
||||
Column,
|
||||
}
|
||||
|
||||
impl Chain {
|
||||
pub fn new (name: &str, items: Option<Vec<Box<dyn Device>>>) -> Result<DynamicDevice<Self>, Box<dyn Error>> {
|
||||
Ok(DynamicDevice::new(render, handle, process, Self {
|
||||
name: name.into(),
|
||||
focused: false,
|
||||
focus: 0,
|
||||
items: items.unwrap_or_else(||vec![]),
|
||||
view: ChainView::Column,
|
||||
adding: false
|
||||
}))
|
||||
}
|
||||
pub struct ChainView<'a> {
|
||||
pub focused: bool,
|
||||
pub chain: Option<&'a Chain>,
|
||||
}
|
||||
|
||||
impl PortList for Chain {
|
||||
fn midi_ins (&self) -> Usually<Vec<String>> {
|
||||
if let Some(device) = self.items.get(0) {
|
||||
device.midi_ins()
|
||||
impl<'a> Render for ChainView<'a> {
|
||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let style = Some(Style::default().green().dim());
|
||||
if self.focused {
|
||||
let Rect { x, y, width, height} = area;
|
||||
lozenge_left(buf, x, y, height, style);
|
||||
lozenge_right(buf, x + width - 1, y, height, style);
|
||||
};
|
||||
let (area, _plugins) = if let Some(ref chain) = self.chain {
|
||||
draw_as_row(&*chain, buf, area, style)?
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
(area, vec![])
|
||||
};
|
||||
Ok(area)
|
||||
}
|
||||
fn audio_outs (&self) -> Usually<Vec<String>> {
|
||||
if let Some(device) = self.items.get(self.items.len().saturating_sub(1)) {
|
||||
device.audio_outs()
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process (_: &mut Chain, _: &Client, _: &ProcessScope) -> Control {
|
||||
Control::Continue
|
||||
}
|
||||
|
||||
pub fn render (state: &Chain, buf: &mut Buffer, area: Rect)
|
||||
|
|
@ -64,18 +41,18 @@ pub fn render (state: &Chain, buf: &mut Buffer, area: Rect)
|
|||
Style::default().green().dim()
|
||||
});
|
||||
let result = match state.view {
|
||||
ChainView::Hidden => Rect { x, y, width: 0, height: 0 },
|
||||
ChainView::Compact => {
|
||||
ChainViewMode::Hidden => Rect { x, y, width: 0, height: 0 },
|
||||
ChainViewMode::Compact => {
|
||||
let area = Rect { x, y, width: (state.name.len() + 4) as u16, height: 3 };
|
||||
buf.set_string(area.x + 2, area.y + 1, &state.name, Style::default());
|
||||
draw_box_styled(buf, area, selected)
|
||||
},
|
||||
ChainView::Row => {
|
||||
ChainViewMode::Row => {
|
||||
draw_box_styled(buf, area, selected);
|
||||
let (area, _) = Row::draw(buf, area, &state.items, 0)?;
|
||||
area
|
||||
},
|
||||
ChainView::Column => {
|
||||
ChainViewMode::Column => {
|
||||
draw_as_column(state, buf, area, selected)?
|
||||
},
|
||||
};
|
||||
|
|
@ -89,8 +66,8 @@ pub fn draw_as_row (
|
|||
let mut h = 0u16;
|
||||
let mut frames = vec![];
|
||||
for (i, device) in state.items.iter().enumerate() {
|
||||
let mut x2 = 0u16;
|
||||
let mut y2 = 1u16;
|
||||
let x2 = 0u16;
|
||||
let _y2 = 1u16;
|
||||
//for port in device.midi_ins()?.iter() {
|
||||
//port.blit(buf, x, y + y2, Some(Style::default()));
|
||||
//x2 = x2.max(port.len() as u16);
|
||||
|
|
@ -129,7 +106,7 @@ pub fn draw_as_row (
|
|||
h = h.max(frame.height);
|
||||
x = x + frame.width;
|
||||
}
|
||||
Ok((area, frames))
|
||||
Ok((Rect { x: area.x, y: area.y, width, height: h }, frames))
|
||||
}
|
||||
|
||||
pub fn draw_as_column (
|
||||
|
|
@ -215,73 +192,3 @@ pub fn draw_as_column (
|
|||
draw_box_styled(buf, frames[state.focus], selected);
|
||||
Ok(area)
|
||||
}
|
||||
|
||||
impl Focus for Chain {
|
||||
fn unfocus (&mut self) {
|
||||
self.focused = false
|
||||
}
|
||||
fn focused (&self) -> Option<&Box<dyn Device>> {
|
||||
match self.focused {
|
||||
true => self.items.get(self.focus),
|
||||
false => None
|
||||
}
|
||||
}
|
||||
fn focused_mut (&mut self) -> Option<&mut Box<dyn Device>> {
|
||||
match self.focused {
|
||||
true => self.items.get_mut(self.focus),
|
||||
false => None
|
||||
}
|
||||
}
|
||||
fn handle_focus (&mut self, event: &FocusEvent) -> Usually<bool> {
|
||||
Ok(match event {
|
||||
FocusEvent::Backward => {
|
||||
if self.focus == 0 {
|
||||
self.focus = self.items.len();
|
||||
}
|
||||
self.focus = self.focus - 1;
|
||||
true
|
||||
},
|
||||
FocusEvent::Forward => {
|
||||
self.focus = self.focus + 1;
|
||||
if self.focus >= self.items.len() {
|
||||
self.focus = 0;
|
||||
}
|
||||
true
|
||||
},
|
||||
FocusEvent::Inward => {
|
||||
self.focused = true;
|
||||
self.items[self.focus].handle(&AppEvent::Focus)?;
|
||||
true
|
||||
},
|
||||
FocusEvent::Outward => {
|
||||
if self.focused {
|
||||
self.focused = false;
|
||||
self.items[self.focus].handle(&AppEvent::Blur)?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle (state: &mut Chain, event: &AppEvent) -> Usually<bool> {
|
||||
Ok(handle_focus(state, event, keymap!(Chain {
|
||||
[Up, NONE, "focus_up", "focus row above",
|
||||
|s: &mut Chain|s.handle_focus(&FocusEvent::Backward)],
|
||||
[Down, NONE, "focus_down", "focus row below",
|
||||
|s: &mut Chain|s.handle_focus(&FocusEvent::Forward)],
|
||||
[Enter, NONE, "focus_down", "focus row below",
|
||||
|s: &mut Chain|s.handle_focus(&FocusEvent::Inward)],
|
||||
[Esc, NONE, "focus_down", "focus row below",
|
||||
|s: &mut Chain|s.handle_focus(&FocusEvent::Outward)],
|
||||
}))? || handle_keymap(state, event, keymap!(Chain {
|
||||
[Char('a'), NONE, "add_device", "add a device", add_device]
|
||||
}))?)
|
||||
}
|
||||
|
||||
fn add_device (state: &mut Chain) -> Usually<bool> {
|
||||
state.adding = true;
|
||||
Ok(true)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::core::*;
|
||||
use super::*;
|
||||
pub struct SceneGrid<'a> {
|
||||
use crate::model::*;
|
||||
use crate::view::*;
|
||||
pub struct SceneGridView<'a> {
|
||||
pub buf: &'a mut Buffer,
|
||||
pub area: Rect,
|
||||
pub name: &'a str,
|
||||
|
|
@ -9,7 +10,7 @@ pub struct SceneGrid<'a> {
|
|||
pub tracks: &'a[Track],
|
||||
pub cursor: &'a(usize, usize),
|
||||
}
|
||||
impl<'a> SceneGrid<'a> {
|
||||
impl<'a> SceneGridView<'a> {
|
||||
pub fn new (
|
||||
buf: &'a mut Buffer,
|
||||
area: Rect,
|
||||
|
|
@ -123,9 +124,7 @@ impl<'a> SceneGrid<'a> {
|
|||
let clip = scene.clips.get(track);
|
||||
let index = index as u16;
|
||||
let label = if let Some(Some(clip)) = clip {
|
||||
let track = self.tracks[track].sequencer.state();
|
||||
let phrase = track.phrases.get(*clip);
|
||||
if let Some(phrase) = phrase {
|
||||
if let Some(phrase) = self.tracks[track].sequencer.phrases.get(*clip) {
|
||||
format!("⯈{}", phrase.name)
|
||||
} else {
|
||||
format!("????")
|
||||
50
src/view/launcher.rs
Normal file
50
src/view/launcher.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use crate::{core::*, model::*, view::*};
|
||||
|
||||
pub fn render (state: &Launcher, buf: &mut Buffer, mut area: Rect) -> Usually<Rect> {
|
||||
//area.width = 80; // DOS mode
|
||||
//area.height = 25;
|
||||
let Rect { x, mut y, width, height } = area;
|
||||
|
||||
y = y + TransportView {
|
||||
timebase: &state.timebase,
|
||||
playing: state.playing,
|
||||
record: state.sequencer().map(|s|s.recording).unwrap_or(false),
|
||||
overdub: state.sequencer().map(|s|s.overdub).unwrap_or(false),
|
||||
monitor: state.sequencer().map(|s|s.monitoring).unwrap_or(false),
|
||||
frame: state.current_frame
|
||||
}.render(buf, area)?.height;
|
||||
|
||||
y = y + SceneGridView {
|
||||
buf,
|
||||
area: Rect { x, y, width, height },
|
||||
name: &state.name,
|
||||
focused: state.view.is_tracks(),
|
||||
scenes: &state.scenes,
|
||||
tracks: &state.tracks,
|
||||
cursor: &state.cursor
|
||||
}.draw()?.height;
|
||||
|
||||
if let Some(chain) = state.chain() {
|
||||
y = y + ChainView {
|
||||
focused: state.view.is_chains(),
|
||||
chain: Some(&*chain),
|
||||
}.render(buf, Rect { x, y, width, height: height/3 })?.height
|
||||
}
|
||||
|
||||
let track = state.track().map(|t|t.1);
|
||||
y = y + SequencerView {
|
||||
focused: state.view.is_sequencer(),
|
||||
ppq: state.timebase.ppq() as usize,
|
||||
track: track,
|
||||
phrase: track.unwrap().sequencer.phrases.get(state.phrase_id().unwrap())
|
||||
}.render(buf, Rect { x, y, width, height: height - y })?.height;
|
||||
|
||||
area.height = y;
|
||||
if state.show_help {
|
||||
let style = Some(Style::default().bold().white().not_dim().on_black().italic());
|
||||
let hide = "[Tab] Mode [Arrows] Move [.,] Value [F1] Toggle help ";
|
||||
hide.blit(buf, x + (width - hide.len() as u16) / 2, height - 1, style);
|
||||
}
|
||||
|
||||
Ok(area)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::core::*;
|
||||
|
||||
pub struct Stack<'a>(pub &'a[Box<dyn Render>]);
|
||||
pub struct Stack<'a>(pub &'a[Box<dyn Render + Sync>]);
|
||||
|
||||
impl<'a> Render for Stack<'a> {
|
||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
0
src/view/layout/focus.rs
Normal file
0
src/view/layout/focus.rs
Normal file
68
src/view/mixer.rs
Normal file
68
src/view/mixer.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
use crate::core::*;
|
||||
use crate::view::*;
|
||||
use crate::model::*;
|
||||
|
||||
pub fn render (state: &Mixer, buf: &mut Buffer, mut area: Rect)
|
||||
-> Usually<Rect>
|
||||
{
|
||||
if area.height < 2 {
|
||||
return Ok(area)
|
||||
}
|
||||
area.x = area.width.saturating_sub(80) / 2;
|
||||
area.width = area.width.min(80);
|
||||
area.height = state.tracks.len() as u16 + 2;
|
||||
draw_box(buf, area);
|
||||
let x = area.x + 1;
|
||||
let y = area.y + 1;
|
||||
let _h = area.height - 2;
|
||||
for (i, track) in state.tracks.iter().enumerate() {
|
||||
//buf.set_string(
|
||||
//x, y + index as u16,
|
||||
//&track.name, Style::default().bold().not_dim()
|
||||
//);
|
||||
for (j, (column, field)) in [
|
||||
(0, format!(" {:10} ", track.name)),
|
||||
(12, format!(" {:.1}dB ", track.gain)),
|
||||
(22, format!(" [ ] ")),
|
||||
(30, format!(" C ")),
|
||||
(35, format!(" {:.1}dB ", track.level)),
|
||||
(45, format!(" [ ] ")),
|
||||
(51, format!(" {:7} ", track.route)),
|
||||
].into_iter().enumerate() {
|
||||
buf.set_string(
|
||||
x + column as u16,
|
||||
y + i as u16,
|
||||
field,
|
||||
if state.selected_track == i && state.selected_column == j {
|
||||
Style::default().white().bold().not_dim()
|
||||
} else {
|
||||
Style::default().not_dim()
|
||||
}
|
||||
);
|
||||
//stdout.queue(move_to(column, row))?;
|
||||
//if state.selected_track == i && state.selected_column == j {
|
||||
//stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?;
|
||||
//} else {
|
||||
//stdout.queue(PrintStyledContent(field.to_string().bold()))?;
|
||||
//}
|
||||
//fn render_meters (
|
||||
//state: &mut Mixer,
|
||||
//stdout: &mut Stdout,
|
||||
//offset: Rect
|
||||
//) -> Result<(), Box<dyn Error>> {
|
||||
//let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row);
|
||||
//for (i, track) in state.tracks.iter().enumerate() {
|
||||
//let row = (i + 1) as u16;
|
||||
//stdout
|
||||
//.queue(move_to(10, row))?.queue(PrintStyledContent("▁".green()))?
|
||||
//.queue(move_to(20, row))?.queue(PrintStyledContent("▁".green()))?
|
||||
//.queue(move_to(28, row))?.queue(PrintStyledContent("▁".green()))?
|
||||
//.queue(move_to(43, row))?.queue(PrintStyledContent("▁".green()))?;
|
||||
//}
|
||||
//Ok(())
|
||||
//}
|
||||
}
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
|
||||
48
src/view/plugin.rs
Normal file
48
src/view/plugin.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use crate::{core::*, model::*};
|
||||
|
||||
pub fn render (state: &Plugin, buf: &mut Buffer, area: Rect)
|
||||
-> Usually<Rect>
|
||||
{
|
||||
let Rect { x, y, height, .. } = area;
|
||||
let mut width = 20u16;
|
||||
match &state.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => {
|
||||
let start = state.selected.saturating_sub((height as usize / 2).saturating_sub(1));
|
||||
let end = start + height as usize - 2;
|
||||
//draw_box(buf, Rect { x, y, width, height });
|
||||
for i in start..end {
|
||||
if let Some(port) = port_list.get(i) {
|
||||
let value = if let Some(value) = instance.control_input(port.index) {
|
||||
value
|
||||
} else {
|
||||
port.default_value
|
||||
};
|
||||
//let label = &format!("C·· M·· {:25} = {value:.03}", port.name);
|
||||
let label = &format!("{:25} = {value:.03}", port.name);
|
||||
width = width.max(label.len() as u16 + 4);
|
||||
label.blit(buf, x + 2, y + 1 + i as u16 - start as u16, if i == state.selected {
|
||||
Some(Style::default().green())
|
||||
} else {
|
||||
None
|
||||
});
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
draw_header(state, buf, area.x, area.y, width)?;
|
||||
Ok(Rect { width, ..area })
|
||||
}
|
||||
|
||||
fn draw_header (state: &Plugin, buf: &mut Buffer, x: u16, y: u16, w: u16) -> Usually<Rect> {
|
||||
let style = Style::default().gray();
|
||||
let label1 = format!(" {}", state.name);
|
||||
label1.blit(buf, x + 1, y, Some(style.white().bold()));
|
||||
if let Some(ref path) = state.path {
|
||||
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
|
||||
label2.blit(buf, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
|
||||
}
|
||||
Ok(Rect { x, y, width: w, height: 1 })
|
||||
}
|
||||
78
src/view/sampler.rs
Normal file
78
src/view/sampler.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use crate::core::*;
|
||||
use crate::model::*;
|
||||
|
||||
pub fn render (state: &Sampler, buf: &mut Buffer, Rect { x, y, height, .. }: Rect)
|
||||
-> Usually<Rect>
|
||||
{
|
||||
let style = Style::default().gray();
|
||||
let title = format!(" {} ({})", state.name, state.voices.len());
|
||||
title.blit(buf, x+1, y, Some(style.white().bold().not_dim()));
|
||||
let mut width = title.len() + 2;
|
||||
for (i, (note, sample)) in state.samples.iter().enumerate() {
|
||||
let style = if i == state.cursor.0 {
|
||||
Style::default().green()
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
let i = i as u16;
|
||||
let y1 = y+1+i;
|
||||
if y1 >= y + height {
|
||||
break
|
||||
}
|
||||
if i as usize == state.cursor.0 {
|
||||
"⯈".blit(buf, x+1, y1, Some(style.bold()));
|
||||
}
|
||||
let label1 = format!("{note:3} {:8}", sample.name);
|
||||
let label2 = format!("{:>6} {:>6}", sample.start, sample.end);
|
||||
label1.blit(buf, x+2, y1, Some(style.bold()));
|
||||
label2.blit(buf, x+3+label1.len()as u16, y1, Some(style));
|
||||
width = width.max(label1.len() + label2.len() + 4);
|
||||
}
|
||||
let height = ((1 + state.samples.len()) as u16).min(height);
|
||||
Ok(Rect { x, y, width: width as u16, height })
|
||||
}
|
||||
|
||||
//fn render_table (
|
||||
//state: &mut Sampler,
|
||||
//stdout: &mut Stdout,
|
||||
//offset: (u16, u16),
|
||||
//) -> Result<(), Box<dyn Error>> {
|
||||
//let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row);
|
||||
//stdout.queue(move_to(0, 3))?.queue(
|
||||
//Print(" Name Rate Trigger Route")
|
||||
//)?;
|
||||
//for (i, sample) in state.samples.lock().unwrap().iter().enumerate() {
|
||||
//let row = 4 + i as u16;
|
||||
//for (j, (column, field)) in [
|
||||
//(0, format!(" {:7} ", sample.name)),
|
||||
//(9, format!(" {:.1}Hz ", sample.rate)),
|
||||
//(18, format!(" MIDI C{} {} ", sample.trigger.0, sample.trigger.1)),
|
||||
//(33, format!(" {:.1}dB -> Output ", sample.gain)),
|
||||
//(50, format!(" {} ", sample.playing.unwrap_or(0))),
|
||||
//].into_iter().enumerate() {
|
||||
//stdout.queue(move_to(column, row))?;
|
||||
//if state.selected_sample == i && state.selected_column == j {
|
||||
//stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?;
|
||||
//} else {
|
||||
//stdout.queue(PrintStyledContent(field.to_string().bold()))?;
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//Ok(())
|
||||
//}
|
||||
|
||||
//fn render_meters (
|
||||
//state: &mut Sampler,
|
||||
//stdout: &mut Stdout,
|
||||
//offset: (u16, u16),
|
||||
//) -> Result<(), Box<dyn Error>> {
|
||||
//let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row);
|
||||
//for (i, sample) in state.samples.lock().iter().enumerate() {
|
||||
//let row = 4 + i as u16;
|
||||
//stdout.queue(move_to(32, row))?.queue(
|
||||
//PrintStyledContent("▁".green())
|
||||
//)?;
|
||||
//}
|
||||
//Ok(())
|
||||
//}
|
||||
|
||||
104
src/view/sequencer.rs
Normal file
104
src/view/sequencer.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use crate::{core::*,model::*,view::*};
|
||||
|
||||
pub mod horizontal;
|
||||
pub mod vertical;
|
||||
|
||||
pub struct SequencerView<'a> {
|
||||
pub focused: bool,
|
||||
pub phrase: Option<&'a Phrase>,
|
||||
pub track: Option<&'a Track>,
|
||||
pub ppq: usize,
|
||||
}
|
||||
|
||||
impl<'a> Render for SequencerView<'a> {
|
||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let Rect { x, y, width, height } = area;
|
||||
let style = Some(Style::default().green().dim());
|
||||
if self.focused {
|
||||
lozenge_left(buf, x, y, height, style);
|
||||
lozenge_right(buf, x + width - 1, y, height, style);
|
||||
}
|
||||
if let Some(ref track) = self.track {
|
||||
self::horizontal::draw(
|
||||
buf,
|
||||
area,
|
||||
self.phrase,
|
||||
self.ppq,
|
||||
track.sequencer.time_cursor,
|
||||
track.sequencer.time_start,
|
||||
track.sequencer.time_zoom,
|
||||
track.sequencer.note_cursor,
|
||||
track.sequencer.note_start,
|
||||
Some(if self.focused {
|
||||
Style::default().green().not_dim()
|
||||
} else {
|
||||
Style::default().green().dim()
|
||||
})
|
||||
)?;
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let Rect { x, y, width, height } = area;
|
||||
let header = draw_header(s, buf, area)?;
|
||||
let piano = match s.view {
|
||||
SequencerMode::Tiny => Rect { x, y, width, height: 0 },
|
||||
SequencerMode::Compact => Rect { x, y, width, height: 0 },
|
||||
SequencerMode::Vertical => self::vertical::draw(s, buf, Rect {
|
||||
x, y: y + header.height, width, height,
|
||||
})?,
|
||||
SequencerMode::Horizontal => self::horizontal::draw(
|
||||
buf,
|
||||
Rect { x, y: y + header.height, width, height, },
|
||||
s.phrase(),
|
||||
s.timebase.ppq() as usize,
|
||||
s.time_cursor,
|
||||
s.time_start,
|
||||
s.time_zoom,
|
||||
s.note_cursor,
|
||||
s.note_start,
|
||||
None
|
||||
)?,
|
||||
};
|
||||
Ok(draw_box(buf, Rect {
|
||||
x, y,
|
||||
width: header.width.max(piano.width),
|
||||
height: header.height + piano.height
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn draw_header (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let Rect { x, y, width, .. } = area;
|
||||
let style = Style::default().gray();
|
||||
crate::view::TransportView {
|
||||
timebase: &s.timebase,
|
||||
playing: s.playing,
|
||||
record: s.recording,
|
||||
overdub: s.overdub,
|
||||
monitor: s.monitoring,
|
||||
frame: 0
|
||||
}.render(buf, area)?;
|
||||
let separator = format!("├{}┤", "-".repeat((width - 2).into()));
|
||||
separator.blit(buf, x, y + 2, Some(style.dim()));
|
||||
let _ = draw_clips(s, buf, area)?;
|
||||
Ok(Rect { x, y, width, height: 3 })
|
||||
}
|
||||
|
||||
pub fn draw_clips (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let Rect { x, y, .. } = area;
|
||||
let style = Style::default().gray();
|
||||
for (i, sequence) in s.phrases.iter().enumerate() {
|
||||
let label = format!("▶ {}", &sequence.name);
|
||||
label.blit(buf, x + 2, y + 3 + (i as u16)*2, Some(if Some(i) == s.sequence {
|
||||
match s.playing {
|
||||
TransportState::Rolling => style.white().bold(),
|
||||
_ => style.not_dim().bold()
|
||||
}
|
||||
} else {
|
||||
style.dim()
|
||||
}));
|
||||
}
|
||||
Ok(Rect { x, y, width: 14, height: 14 })
|
||||
}
|
||||
|
|
@ -45,12 +45,7 @@ pub fn draw (
|
|||
Ok(area)
|
||||
}
|
||||
|
||||
pub fn timer (
|
||||
buf: &mut Buffer,
|
||||
area: Rect,
|
||||
time0: usize,
|
||||
now: usize
|
||||
) {
|
||||
pub fn timer (buf: &mut Buffer, area: Rect, time0: usize, now: usize) {
|
||||
let x = area.x + 5;
|
||||
for step in time0..(time0+area.width as usize).saturating_sub(5) {
|
||||
buf.set_string(x + step as u16, area.y, &"-", if step == now {
|
||||
|
|
@ -61,12 +56,9 @@ pub fn timer (
|
|||
}
|
||||
}
|
||||
|
||||
pub fn keys (
|
||||
buf: &mut Buffer,
|
||||
area: Rect,
|
||||
note0: usize,
|
||||
notes: &[bool],
|
||||
) -> Usually<Rect> {
|
||||
pub fn keys (buf: &mut Buffer, area: Rect, note0: usize, _notes: &[bool])
|
||||
-> Usually<Rect>
|
||||
{
|
||||
let bw = Style::default().dim();
|
||||
let Rect { x, y, width, height } = area;
|
||||
let h = height.saturating_sub(2);
|
||||
|
|
@ -98,13 +90,13 @@ pub fn lanes (
|
|||
phrase: &Phrase,
|
||||
ppq: usize,
|
||||
time_z: usize,
|
||||
time0: usize,
|
||||
_time0: usize,
|
||||
note0: usize,
|
||||
) {
|
||||
let Rect { x, y, width, height } = area;
|
||||
let time0 = time0 / time_z;
|
||||
let time1 = time0 + width as usize;
|
||||
let note1 = note0 + height as usize;
|
||||
//let time0 = time0 / time_z;
|
||||
//let time1 = time0 + width as usize;
|
||||
//let note1 = note0 + height as usize;
|
||||
let bg = Style::default();
|
||||
let (bw, wh) = (bg.dim(), bg.white());
|
||||
let offset = 5;
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::core::*;
|
||||
use super::*;
|
||||
|
||||
pub fn draw (
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
use crate::core::*;
|
||||
use crate::layout::*;
|
||||
|
||||
pub struct Transport<'a> {
|
||||
pub struct TransportView<'a> {
|
||||
pub timebase: &'a Arc<Timebase>,
|
||||
pub playing: TransportState,
|
||||
pub record: bool,
|
||||
|
|
@ -10,7 +9,7 @@ pub struct Transport<'a> {
|
|||
pub frame: usize,
|
||||
}
|
||||
|
||||
impl<'a> Render for Transport<'a> {
|
||||
impl<'a> Render for TransportView<'a> {
|
||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let Rect { x, y, width, .. } = area;
|
||||
draw_play_stop(buf, x + 1, y, &self.playing);
|
||||
|
|
@ -40,6 +39,7 @@ pub fn draw_timer (buf: &mut Buffer, x: u16, y: u16, timebase: &Arc<Timebase>, f
|
|||
);
|
||||
timer.blit(buf, x - timer.len() as u16, y, Some(Style::default().not_dim()));
|
||||
}
|
||||
|
||||
pub fn draw_play_stop (buf: &mut Buffer, x: u16, y: u16, state: &TransportState) {
|
||||
let style = Style::default().gray();
|
||||
match state {
|
||||
|
|
@ -52,6 +52,7 @@ pub fn draw_play_stop (buf: &mut Buffer, x: u16, y: u16, state: &TransportState)
|
|||
TransportState::Rolling => style.not_dim().white().bold()
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn draw_rec (buf: &mut Buffer, x: u16, y: u16, on: bool) {
|
||||
"⏺ REC".blit(buf, x, y, Some(if on {
|
||||
Style::default().bold().red()
|
||||
|
|
@ -59,6 +60,7 @@ pub fn draw_rec (buf: &mut Buffer, x: u16, y: u16, on: bool) {
|
|||
Style::default().bold().dim()
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn draw_dub (buf: &mut Buffer, x: u16, y: u16, on: bool) {
|
||||
"⏺ DUB".blit(buf, x, y, Some(if on {
|
||||
Style::default().bold().yellow()
|
||||
|
|
@ -66,6 +68,7 @@ pub fn draw_dub (buf: &mut Buffer, x: u16, y: u16, on: bool) {
|
|||
Style::default().bold().dim()
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn draw_mon (buf: &mut Buffer, x: u16, y: u16, on: bool) {
|
||||
"⏺ MON".blit(buf, x, y, Some(if on {
|
||||
Style::default().bold().green()
|
||||
|
|
@ -73,6 +76,7 @@ pub fn draw_mon (buf: &mut Buffer, x: u16, y: u16, on: bool) {
|
|||
Style::default().bold().dim()
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn draw_bpm (buf: &mut Buffer, x: u16, y: u16, bpm: usize) {
|
||||
let style = Style::default().not_dim();
|
||||
"BPM"
|
||||
|
|
@ -88,3 +92,4 @@ pub fn draw_bpm (buf: &mut Buffer, x: u16, y: u16, bpm: usize) {
|
|||
"1/16"
|
||||
.blit(buf, x + 29, y, Some(style.bold()));
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue