mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 20:26:42 +01:00
wip: refactor into crates
This commit is contained in:
parent
96e17e7f7c
commit
5ae99b4ada
87 changed files with 2281 additions and 2217 deletions
144
crates/tek_sequencer/src/arranger.rs
Normal file
144
crates/tek_sequencer/src/arranger.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
//! Clip launcher and arrangement editor.
|
||||
|
||||
use crate::*;
|
||||
|
||||
use self::arr_focus::ArrangerFocus;
|
||||
pub use self::arr_scene::Scene;
|
||||
|
||||
submod! { arr_draw_h arr_draw_v arr_focus arr_phrase arr_scene arr_track }
|
||||
|
||||
/// Key bindings for arranger section.
|
||||
pub const KEYMAP_ARRANGER: &'static [KeyBinding<Arranger>] = keymap!(Arranger {
|
||||
[Char('`'), NONE, "arranger_mode_switch", "switch the display mode", |app: &mut Arranger| {
|
||||
app.mode.to_next();
|
||||
Ok(true)
|
||||
}],
|
||||
[Up, NONE, "arranger_cursor_up", "move cursor up", |app: &mut Arranger| {
|
||||
match app.mode {
|
||||
ArrangerViewMode::Horizontal => app.track_prev(),
|
||||
_ => app.scene_prev(),
|
||||
};
|
||||
app.show_phrase()?;
|
||||
Ok(true)
|
||||
}],
|
||||
[Down, NONE, "arranger_cursor_down", "move cursor down", |app: &mut Arranger| {
|
||||
match app.mode {
|
||||
ArrangerViewMode::Horizontal => app.track_next(),
|
||||
_ => app.scene_next(),
|
||||
};
|
||||
app.show_phrase()?;
|
||||
Ok(true)
|
||||
}],
|
||||
[Left, NONE, "arranger_cursor_left", "move cursor left", |app: &mut Arranger| {
|
||||
match app.mode {
|
||||
ArrangerViewMode::Horizontal => app.scene_prev(),
|
||||
_ => app.track_prev(),
|
||||
};
|
||||
app.show_phrase()?;
|
||||
Ok(true)
|
||||
}],
|
||||
[Right, NONE, "arranger_cursor_right", "move cursor right", |app: &mut Arranger| {
|
||||
match app.mode {
|
||||
ArrangerViewMode::Horizontal => app.scene_next(),
|
||||
_ => app.track_next(),
|
||||
};
|
||||
app.show_phrase()?;
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('.'), NONE, "arranger_increment", "set next clip at cursor", |app: &mut Arranger| {
|
||||
app.phrase_next();
|
||||
app.sequencer.phrase = app.phrase().map(Clone::clone);
|
||||
Ok(true)
|
||||
}],
|
||||
[Char(','), NONE, "arranger_decrement", "set previous clip at cursor", |app: &mut Arranger| {
|
||||
app.phrase_prev();
|
||||
app.sequencer.phrase = app.phrase().map(Clone::clone);
|
||||
Ok(true)
|
||||
}],
|
||||
[Enter, NONE, "arranger_activate", "activate item at cursor", |app: &mut Arranger| {
|
||||
app.activate();
|
||||
Ok(true)
|
||||
}],
|
||||
});
|
||||
|
||||
/// Represents the tracks and scenes of the composition.
|
||||
pub struct Arranger {
|
||||
/// Display mode of arranger
|
||||
pub mode: ArrangerViewMode,
|
||||
/// Currently selected element.
|
||||
pub selected: ArrangerFocus,
|
||||
/// Collection of tracks.
|
||||
pub tracks: Vec<Track>,
|
||||
/// Collection of scenes.
|
||||
pub scenes: Vec<Scene>,
|
||||
|
||||
pub focused: bool,
|
||||
pub entered: bool,
|
||||
pub fixed_height: bool,
|
||||
|
||||
pub sequencer: Sequencer,
|
||||
}
|
||||
|
||||
/// Display mode of arranger
|
||||
pub enum ArrangerViewMode {
|
||||
Vertical,
|
||||
VerticalCompact,
|
||||
Horizontal,
|
||||
}
|
||||
|
||||
impl ArrangerViewMode {
|
||||
fn to_next (&mut self) {
|
||||
*self = match self {
|
||||
Self::Vertical => Self::VerticalCompact,
|
||||
Self::VerticalCompact => Self::Horizontal,
|
||||
Self::Horizontal => Self::Vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arranger {
|
||||
|
||||
pub fn new () -> Self {
|
||||
Self {
|
||||
mode: ArrangerViewMode::Vertical,
|
||||
selected: ArrangerFocus::Clip(0, 0),
|
||||
scenes: vec![],
|
||||
tracks: vec![],
|
||||
entered: true,
|
||||
focused: true,
|
||||
fixed_height: false,
|
||||
sequencer: Sequencer::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate (&mut self) {
|
||||
match self.selected {
|
||||
ArrangerFocus::Scene(s) => {
|
||||
for (track_index, track) in self.tracks.iter_mut().enumerate() {
|
||||
track.sequence = self.scenes[s].clips[track_index];
|
||||
track.reset = true;
|
||||
}
|
||||
},
|
||||
ArrangerFocus::Clip(t, s) => {
|
||||
self.tracks[t].sequence = self.scenes[s].clips[t];
|
||||
self.tracks[t].reset = true;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_phrase (&mut self) -> Usually<()> {
|
||||
let phrase = self.phrase();
|
||||
self.sequencer.show(phrase)
|
||||
}
|
||||
}
|
||||
|
||||
render!(Arranger |self, buf, area| match self.mode {
|
||||
ArrangerViewMode::Horizontal =>
|
||||
self::arr_draw_h::draw(self, buf, area),
|
||||
ArrangerViewMode::Vertical =>
|
||||
self::arr_draw_v::draw_expanded(self, buf, area),
|
||||
ArrangerViewMode::VerticalCompact =>
|
||||
self::arr_draw_v::draw_compact(self, buf, area),
|
||||
});
|
||||
|
||||
98
crates/tek_sequencer/src/arranger_focus.rs
Normal file
98
crates/tek_sequencer/src/arranger_focus.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
/// Represents the current user selection in the arranger
|
||||
pub enum ArrangerFocus {
|
||||
/** The whole mix is selected */
|
||||
Mix,
|
||||
/// A track is selected.
|
||||
Track(usize),
|
||||
/// A scene is selected.
|
||||
Scene(usize),
|
||||
/// A clip (track × scene) is selected.
|
||||
Clip(usize, usize),
|
||||
}
|
||||
|
||||
/// Focus identification methods
|
||||
impl ArrangerFocus {
|
||||
pub fn is_track (&self) -> bool {
|
||||
match self { Self::Track(_) => true, _ => false }
|
||||
}
|
||||
pub fn is_scene (&self) -> bool {
|
||||
match self { Self::Scene(_) => true, _ => false }
|
||||
}
|
||||
pub fn is_clip (&self) -> bool {
|
||||
match self { Self::Clip(_, _) => true, _ => false }
|
||||
}
|
||||
}
|
||||
|
||||
/// Track focus methods
|
||||
impl ArrangerFocus {
|
||||
pub fn track (&self) -> Option<usize> {
|
||||
match self {
|
||||
Self::Clip(t, _) => Some(*t),
|
||||
Self::Track(t) => Some(*t),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub fn track_next (&mut self, last_track: usize) {
|
||||
*self = match self {
|
||||
Self::Mix => Self::Track(0),
|
||||
Self::Track(t) => Self::Track(last_track.min(*t + 1)),
|
||||
Self::Scene(s) => Self::Clip(0, *s),
|
||||
Self::Clip(t, s) => Self::Clip(last_track.min(*t + 1), *s),
|
||||
}
|
||||
}
|
||||
pub fn track_prev (&mut self) {
|
||||
*self = match self {
|
||||
Self::Mix => Self::Mix,
|
||||
Self::Scene(s) => Self::Scene(*s),
|
||||
Self::Track(t) => if *t == 0 {
|
||||
Self::Mix
|
||||
} else {
|
||||
Self::Track(*t - 1)
|
||||
},
|
||||
Self::Clip(t, s) => if *t == 0 {
|
||||
Self::Scene(*s)
|
||||
} else {
|
||||
Self::Clip(t.saturating_sub(1), *s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scene focus methods
|
||||
impl ArrangerFocus {
|
||||
pub fn scene (&self) -> Option<usize> {
|
||||
match self {
|
||||
Self::Clip(_, s) => Some(*s),
|
||||
Self::Scene(s) => Some(*s),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub fn scene_next (&mut self, last_scene: usize) {
|
||||
*self = match self {
|
||||
Self::Mix => Self::Scene(0),
|
||||
Self::Track(t) => Self::Clip(*t, 0),
|
||||
Self::Scene(s) => Self::Scene(last_scene.min(*s + 1)),
|
||||
Self::Clip(t, s) => Self::Clip(*t, last_scene.min(*s + 1)),
|
||||
}
|
||||
}
|
||||
pub fn scene_prev (&mut self) {
|
||||
*self = match self {
|
||||
Self::Mix => Self::Mix,
|
||||
Self::Track(t) => Self::Track(*t),
|
||||
Self::Scene(s) => if *s == 0 {
|
||||
Self::Mix
|
||||
} else {
|
||||
Self::Scene(*s - 1)
|
||||
},
|
||||
Self::Clip(t, s) => if *s == 0 {
|
||||
Self::Track(*t)
|
||||
} else {
|
||||
Self::Clip(*t, s.saturating_sub(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
57
crates/tek_sequencer/src/arranger_phrase.rs
Normal file
57
crates/tek_sequencer/src/arranger_phrase.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
use crate::*;
|
||||
|
||||
use super::Arranger;
|
||||
|
||||
/// Phrase management methods
|
||||
impl Arranger {
|
||||
pub fn phrase (&self) -> Option<&Arc<RwLock<Phrase>>> {
|
||||
let track_id = self.selected.track()?;
|
||||
self.tracks.get(track_id)?.phrases.get((*self.scene()?.clips.get(track_id)?)?)
|
||||
}
|
||||
pub fn phrase_next (&mut self) {
|
||||
let track_index = self.selected.track();
|
||||
let scene_index = self.selected.scene();
|
||||
track_index
|
||||
.and_then(|index|self.tracks.get_mut(index).map(|track|(index, track)))
|
||||
.and_then(|(track_index, track)|{
|
||||
let phrases = track.phrases.len();
|
||||
scene_index
|
||||
.and_then(|index|self.scenes.get_mut(index))
|
||||
.and_then(|scene|{
|
||||
if let Some(phrase_index) = scene.clips[track_index] {
|
||||
if phrase_index >= phrases - 1 {
|
||||
scene.clips[track_index] = None;
|
||||
} else {
|
||||
scene.clips[track_index] = Some(phrase_index + 1);
|
||||
}
|
||||
} else if phrases > 0 {
|
||||
scene.clips[track_index] = Some(0);
|
||||
}
|
||||
Some(())
|
||||
})
|
||||
});
|
||||
}
|
||||
pub fn phrase_prev (&mut self) {
|
||||
let track_index = self.selected.track();
|
||||
let scene_index = self.selected.scene();
|
||||
track_index
|
||||
.and_then(|index|self.tracks.get_mut(index).map(|track|(index, track)))
|
||||
.and_then(|(track_index, track)|{
|
||||
let phrases = track.phrases.len();
|
||||
scene_index
|
||||
.and_then(|index|self.scenes.get_mut(index))
|
||||
.and_then(|scene|{
|
||||
if let Some(phrase_index) = scene.clips[track_index] {
|
||||
scene.clips[track_index] = if phrase_index == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(phrase_index - 1)
|
||||
};
|
||||
} else if phrases > 0 {
|
||||
scene.clips[track_index] = Some(phrases - 1);
|
||||
}
|
||||
Some(())
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
80
crates/tek_sequencer/src/arranger_scene.rs
Normal file
80
crates/tek_sequencer/src/arranger_scene.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use crate::*;
|
||||
|
||||
use super::Arranger;
|
||||
|
||||
/// A collection of phrases to play on each track.
|
||||
pub struct Scene {
|
||||
pub name: String,
|
||||
pub clips: Vec<Option<usize>>,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
pub fn new (name: impl AsRef<str>, clips: impl AsRef<[Option<usize>]>) -> Self {
|
||||
let name = name.as_ref().into();
|
||||
let clips = clips.as_ref().iter().map(|x|x.clone()).collect();
|
||||
Self { name, clips, }
|
||||
}
|
||||
/// Returns the pulse length of the longest phrase in the scene
|
||||
pub fn pulses (&self, tracks: &[Track]) -> usize {
|
||||
self.clips.iter().enumerate()
|
||||
.filter_map(|(i, c)|c.map(|c|tracks[i].phrases.get(c)))
|
||||
.filter_map(|p|p)
|
||||
.fold(0, |a, p|a.max(p.read().unwrap().length))
|
||||
}
|
||||
/// Returns true if all phrases in the scene are currently playing
|
||||
pub fn is_playing (&self, tracks: &[Track]) -> bool {
|
||||
self.clips.iter().enumerate()
|
||||
.all(|(track_index, phrase_index)|match phrase_index {
|
||||
Some(i) => tracks[track_index].sequence == Some(*i),
|
||||
None => true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scene_name_max_len (scenes: &[Scene]) -> usize {
|
||||
scenes.iter()
|
||||
.map(|s|s.name.len())
|
||||
.fold(0, usize::max)
|
||||
}
|
||||
|
||||
pub fn scene_ppqs (tracks: &[Track], scenes: &[Scene]) -> Vec<(usize, usize)> {
|
||||
let mut total = 0;
|
||||
let mut scenes: Vec<(usize, usize)> = scenes.iter().map(|scene|{
|
||||
let pulses = scene.pulses(tracks);
|
||||
total = total + pulses;
|
||||
(pulses, total - pulses)
|
||||
}).collect();
|
||||
scenes.push((0, total));
|
||||
scenes
|
||||
}
|
||||
|
||||
/// Scene management methods
|
||||
impl Arranger {
|
||||
pub fn scene (&self) -> Option<&Scene> {
|
||||
self.selected.scene().map(|s|self.scenes.get(s)).flatten()
|
||||
}
|
||||
pub fn scene_mut (&mut self) -> Option<&mut Scene> {
|
||||
self.selected.scene().map(|s|self.scenes.get_mut(s)).flatten()
|
||||
}
|
||||
pub fn scene_next (&mut self) {
|
||||
self.selected.scene_next(self.scenes.len() - 1)
|
||||
}
|
||||
pub fn scene_prev (&mut self) {
|
||||
self.selected.scene_prev()
|
||||
}
|
||||
pub fn scene_add (&mut self, name: Option<&str>) -> Usually<&mut Scene> {
|
||||
let clips = vec![None;self.tracks.len()];
|
||||
self.scenes.push(match name {
|
||||
Some(name) => Scene::new(name, clips),
|
||||
None => Scene::new(&self.scene_default_name(), clips),
|
||||
});
|
||||
let index = self.scenes.len() - 1;
|
||||
Ok(&mut self.scenes[index])
|
||||
}
|
||||
pub fn scene_del (&mut self) {
|
||||
unimplemented!("Arranger::scene_del");
|
||||
}
|
||||
pub fn scene_default_name (&self) -> String {
|
||||
format!("Scene {}", self.scenes.len() + 1)
|
||||
}
|
||||
}
|
||||
54
crates/tek_sequencer/src/arranger_track.rs
Normal file
54
crates/tek_sequencer/src/arranger_track.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use crate::*;
|
||||
|
||||
use super::Arranger;
|
||||
|
||||
/// Track management methods
|
||||
impl Arranger {
|
||||
pub fn track (&self) -> Option<&Track> {
|
||||
self.selected.track().map(|t|self.tracks.get(t)).flatten()
|
||||
}
|
||||
pub fn track_mut (&mut self) -> Option<&mut Track> {
|
||||
self.selected.track().map(|t|self.tracks.get_mut(t)).flatten()
|
||||
}
|
||||
pub fn track_next (&mut self) {
|
||||
self.selected.track_next(self.tracks.len() - 1)
|
||||
}
|
||||
pub fn track_prev (&mut self) {
|
||||
self.selected.track_prev()
|
||||
}
|
||||
pub fn track_add (&mut self, name: Option<&str>) -> Usually<&mut Track> {
|
||||
self.tracks.push(name.map_or_else(
|
||||
|| Track::new(&self.track_default_name()),
|
||||
|name| Track::new(name),
|
||||
)?);
|
||||
let index = self.tracks.len() - 1;
|
||||
Ok(&mut self.tracks[index])
|
||||
}
|
||||
pub fn track_del (&mut self) {
|
||||
unimplemented!("Arranger::track_del");
|
||||
}
|
||||
pub fn track_default_name (&self) -> String {
|
||||
format!("Track {}", self.tracks.len() + 1)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_name_max_len (tracks: &[Track]) -> usize {
|
||||
tracks.iter()
|
||||
.map(|s|s.name.len())
|
||||
.fold(0, usize::max)
|
||||
}
|
||||
|
||||
pub fn track_clip_name_lengths (tracks: &[Track]) -> Vec<(usize, usize)> {
|
||||
let mut total = 0;
|
||||
let mut lengths: Vec<(usize, usize)> = tracks.iter().map(|track|{
|
||||
let len = 2 + track.phrases
|
||||
.iter()
|
||||
.fold(track.name.len(), |len, phrase|{
|
||||
len.max(phrase.read().unwrap().name.len())
|
||||
});
|
||||
total = total + len;
|
||||
(len, total - len)
|
||||
}).collect();
|
||||
lengths.push((0, total));
|
||||
lengths
|
||||
}
|
||||
200
crates/tek_sequencer/src/arranger_view_h.rs
Normal file
200
crates/tek_sequencer/src/arranger_view_h.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
use crate::*;
|
||||
use super::{Arranger, arr_track::*};
|
||||
|
||||
pub fn draw (state: &Arranger, buf: &mut Buffer, mut area: Rect) -> Usually<Rect> {
|
||||
area.height = area.height.min((2 + state.tracks.len() * 2) as u16);
|
||||
Layered([
|
||||
&to_fill_bg(Nord::bg_lo(state.focused, state.entered)),
|
||||
&Split::right([
|
||||
&track_name_column(state),
|
||||
&track_mon_column(state),
|
||||
&track_rec_column(state),
|
||||
&track_ovr_column(state),
|
||||
&track_del_column(state),
|
||||
&track_gain_column(state),
|
||||
&track_scenes_column(state),
|
||||
]),
|
||||
]).render(buf, area)
|
||||
}
|
||||
|
||||
fn track_name_column <'a> (state: &'a Arranger) -> impl Render + 'a {
|
||||
let dim = Some(Style::default().dim());
|
||||
let yellow = Some(Style::default().yellow().bold().not_dim());
|
||||
let white = Some(Style::default().white().bold().not_dim());
|
||||
move |buf: &mut Buffer, mut area: Rect| {
|
||||
area.width = 3 + 5.max(track_name_max_len(state.tracks.as_slice())) as u16;
|
||||
let offset = 0; // track scroll offset
|
||||
for y in 0..area.height {
|
||||
if y == 0 {
|
||||
"Mixer".blit(buf, area.x + 1, area.y + y, dim)?;
|
||||
} else if y % 2 == 0 {
|
||||
let index = (y as usize - 2) / 2 + offset;
|
||||
if let Some(track) = state.tracks.get(index) {
|
||||
let selected = state.selected.track() == Some(index);
|
||||
let style = if selected { yellow } else { white };
|
||||
format!(" {index:>02} ").blit(buf, area.x, area.y + y, style)?;
|
||||
track.name.blit(buf, area.x + 4, area.y + y, style)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
}
|
||||
|
||||
fn track_mon_column <'a> (state: &'a Arranger) -> impl Render + 'a {
|
||||
let on = Some(Style::default().not_dim().green().bold());
|
||||
let off = Some(Style::default().dim());
|
||||
move |buf: &mut Buffer, mut area: Rect| {
|
||||
area.x = area.x + 1;
|
||||
for y in 0..area.height {
|
||||
if y == 0 {
|
||||
//" MON ".blit(buf, area.x, area.y + y, style2)?;
|
||||
} else if y % 2 == 0 {
|
||||
let index = (y as usize - 2) / 2;
|
||||
if let Some(track) = state.tracks.get(index) {
|
||||
let style = if track.monitoring { on } else { off };
|
||||
" MON ".blit(buf, area.x, area.y + y, style)?;
|
||||
} else {
|
||||
area.height = y;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
area.width = 4;
|
||||
Ok(area)
|
||||
}
|
||||
}
|
||||
|
||||
fn track_rec_column <'a> (state: &'a Arranger) -> impl Render + 'a {
|
||||
let on = Some(Style::default().not_dim().red().bold());
|
||||
let off = Some(Style::default().dim());
|
||||
move |buf: &mut Buffer, mut area: Rect| {
|
||||
area.x = area.x + 1;
|
||||
for y in 0..area.height {
|
||||
if y == 0 {
|
||||
//" REC ".blit(buf, area.x, area.y + y, style2)?;
|
||||
} else if y % 2 == 0 {
|
||||
let index = (y as usize - 2) / 2;
|
||||
if let Some(track) = state.tracks.get(index) {
|
||||
let style = if track.recording { on } else { off };
|
||||
" REC ".blit(buf, area.x, area.y + y, style)?;
|
||||
} else {
|
||||
area.height = y;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
area.width = 4;
|
||||
Ok(area)
|
||||
}
|
||||
}
|
||||
|
||||
fn track_ovr_column <'a> (state: &'a Arranger) -> impl Render + 'a {
|
||||
let on = Some(Style::default().not_dim().yellow().bold());
|
||||
let off = Some(Style::default().dim());
|
||||
move |buf: &mut Buffer, mut area: Rect| {
|
||||
area.x = area.x + 1;
|
||||
for y in 0..area.height {
|
||||
if y == 0 {
|
||||
//" OVR ".blit(buf, area.x, area.y + y, style2)?;
|
||||
} else if y % 2 == 0 {
|
||||
let index = (y as usize - 2) / 2;
|
||||
if let Some(track) = state.tracks.get(index) {
|
||||
" OVR ".blit(buf, area.x, area.y + y, if track.overdub {
|
||||
on
|
||||
} else {
|
||||
off
|
||||
})?;
|
||||
} else {
|
||||
area.height = y;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
area.width = 4;
|
||||
Ok(area)
|
||||
}
|
||||
}
|
||||
|
||||
fn track_del_column <'a> (state: &'a Arranger) -> impl Render + 'a {
|
||||
let off = Some(Style::default().dim());
|
||||
move |buf: &mut Buffer, mut area: Rect| {
|
||||
area.x = area.x + 1;
|
||||
for y in 0..area.height {
|
||||
if y == 0 {
|
||||
//" DEL ".blit(buf, area.x, area.y + y, style2)?;
|
||||
} else if y % 2 == 0 {
|
||||
let index = (y as usize - 2) / 2;
|
||||
if let Some(_) = state.tracks.get(index) {
|
||||
" DEL ".blit(buf, area.x, area.y + y, off)?;
|
||||
} else {
|
||||
area.height = y;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
area.width = 4;
|
||||
Ok(area)
|
||||
}
|
||||
}
|
||||
|
||||
fn track_gain_column <'a> (state: &'a Arranger) -> impl Render + 'a {
|
||||
let off = Some(Style::default().dim());
|
||||
move |buf: &mut Buffer, mut area: Rect| {
|
||||
area.x = area.x + 1;
|
||||
for y in 0..area.height {
|
||||
if y == 0 {
|
||||
//" GAIN ".blit(buf, area.x, area.y + y, style2)?;
|
||||
} else if y % 2 == 0 {
|
||||
let index = (y as usize - 2) / 2;
|
||||
if let Some(_) = state.tracks.get(index) {
|
||||
" +0.0 ".blit(buf, area.x, area.y + y, off)?;
|
||||
} else {
|
||||
area.height = y;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
area.width = 7;
|
||||
Ok(area)
|
||||
}
|
||||
}
|
||||
|
||||
fn track_scenes_column <'a> (state: &'a Arranger) -> impl Render + 'a {
|
||||
|buf: &mut Buffer, area: Rect| {
|
||||
let mut x2 = 0;
|
||||
let Rect { x, y, height, .. } = area;
|
||||
for (scene_index, scene) in state.scenes.iter().enumerate() {
|
||||
let active_scene = state.selected.scene() == Some(scene_index);
|
||||
let sep = Some(if active_scene {
|
||||
Style::default().yellow().not_dim()
|
||||
} else {
|
||||
Style::default().dim()
|
||||
});
|
||||
for y in y+1..y+height {
|
||||
"│".blit(buf, x + x2, y, sep)?;
|
||||
}
|
||||
let mut x3 = scene.name.len() as u16;
|
||||
scene.name.blit(buf, x + x2, y, sep)?;
|
||||
for (i, clip) in scene.clips.iter().enumerate() {
|
||||
let active_track = state.selected.track() == Some(i);
|
||||
if let Some(clip) = clip {
|
||||
let y2 = y + 2 + i as u16 * 2;
|
||||
let label = match state.tracks[i].phrases.get(*clip) {
|
||||
Some(phrase) => &format!("{}", phrase.read().unwrap().name),
|
||||
None => "...."
|
||||
};
|
||||
label.blit(buf, x + x2, y2, Some(if active_track && active_scene {
|
||||
Style::default().not_dim().yellow().bold()
|
||||
} else {
|
||||
Style::default().not_dim()
|
||||
}))?;
|
||||
x3 = x3.max(label.len() as u16)
|
||||
}
|
||||
}
|
||||
x2 = x2 + x3 + 1;
|
||||
}
|
||||
Ok(Rect { x, y, height, width: x2 })
|
||||
}
|
||||
}
|
||||
|
||||
225
crates/tek_sequencer/src/arranger_view_v.rs
Normal file
225
crates/tek_sequencer/src/arranger_view_v.rs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
use crate::*;
|
||||
use super::{
|
||||
Arranger,
|
||||
arr_focus::ArrangerFocus,
|
||||
arr_track::track_clip_name_lengths,
|
||||
arr_scene::{Scene, scene_ppqs, scene_name_max_len}
|
||||
};
|
||||
|
||||
pub fn draw_expanded (state: &Arranger, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let track_cols = track_clip_name_lengths(state.tracks.as_slice());
|
||||
let scene_rows = scene_ppqs(state.tracks.as_slice(), state.scenes.as_slice());
|
||||
draw(state, buf, area, track_cols.as_slice(), scene_rows.as_slice())
|
||||
}
|
||||
|
||||
pub fn draw_compact (state: &Arranger, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let track_cols = track_clip_name_lengths(state.tracks.as_slice());
|
||||
let scene_rows = (0..=state.scenes.len()+3).map(|i|(96, 96*i)).collect::<Vec<_>>();
|
||||
draw(state, buf, area, track_cols.as_slice(), scene_rows.as_slice())
|
||||
}
|
||||
|
||||
pub fn draw (
|
||||
state: &Arranger,
|
||||
buf: &mut Buffer,
|
||||
mut area: Rect,
|
||||
cols: &[(usize, usize)],
|
||||
rows: &[(usize, usize)],
|
||||
) -> Usually<Rect> {
|
||||
area.height = 2 + (rows[rows.len() - 1].1 / 96) as u16;
|
||||
let offset = 3 + scene_name_max_len(state.scenes.as_ref()) as u16;
|
||||
Layered([
|
||||
&to_fill_bg(Nord::bg_lo(state.focused, state.entered)),
|
||||
&column_separators(offset, cols),
|
||||
&cursor_focus(state, offset, cols, rows),
|
||||
&Split::down([
|
||||
&tracks_header(state, cols, offset),
|
||||
&scene_rows(state, cols, rows, offset),
|
||||
]),
|
||||
&row_separators(rows),
|
||||
]).render(buf, area)
|
||||
}
|
||||
|
||||
fn column_separators <'a> (offset: u16, cols: &'a [(usize, usize)]) -> impl Render + 'a {
|
||||
move |buf: &mut Buffer, area: Rect|{
|
||||
let style = Some(Style::default().fg(Nord::SEPARATOR));
|
||||
for (_, x) in cols.iter() {
|
||||
let x = offset + area.x + *x as u16 - 1;
|
||||
for y in area.y..area.height+area.y {
|
||||
"▎".blit(buf, x, y, style)?;
|
||||
}
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
}
|
||||
|
||||
fn row_separators <'a> (rows: &'a [(usize, usize)]) -> impl Render + 'a {
|
||||
move |buf: &mut Buffer, area: Rect| {
|
||||
for (_, y) in rows.iter() {
|
||||
let y = area.y + (*y / 96) as u16 + 1;
|
||||
if y >= buf.area.height {
|
||||
break
|
||||
}
|
||||
for x in area.x..area.width+area.y-2 {
|
||||
let cell = buf.get_mut(x, y);
|
||||
cell.modifier = Modifier::UNDERLINED;
|
||||
cell.underline_color = Nord::SEPARATOR;
|
||||
}
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_focus <'a> (
|
||||
state: &'a Arranger, offset: u16, cols: &'a [(usize, usize)], rows: &'a [(usize, usize)],
|
||||
) -> impl Render + 'a {
|
||||
move |buf: &mut Buffer, area: Rect| {
|
||||
match state.selected {
|
||||
ArrangerFocus::Mix => if state.focused
|
||||
&& state.entered
|
||||
&& state.selected == ArrangerFocus::Mix
|
||||
{
|
||||
fill_bg(buf, area, Nord::bg_hi(state.focused, state.entered));
|
||||
Corners(Style::default().green().not_dim()).draw(buf, area)
|
||||
} else {
|
||||
Ok(area)
|
||||
},
|
||||
ArrangerFocus::Track(t) => {
|
||||
let area = Rect {
|
||||
x: offset + area.x + cols[t].1 as u16 - 1,
|
||||
y: area.y,
|
||||
width: cols[t].0 as u16,
|
||||
height: area.height
|
||||
};
|
||||
fill_bg(buf, area, Nord::bg_hi(state.focused, state.entered));
|
||||
Corners(Style::default().green().not_dim()).draw(buf, area)
|
||||
},
|
||||
ArrangerFocus::Scene(s) => {
|
||||
let area = Rect {
|
||||
x: area.x,
|
||||
y: 2 + area.y + (rows[s].1 / 96) as u16,
|
||||
width: area.width,
|
||||
height: (rows[s].0 / 96) as u16
|
||||
};
|
||||
fill_bg(buf, area, Nord::bg_hi(state.focused, state.entered));
|
||||
Corners(Style::default().green().not_dim()).draw(buf, area)
|
||||
},
|
||||
ArrangerFocus::Clip(t, s) => {
|
||||
let track_area = Rect {
|
||||
x: offset + area.x + cols[t].1 as u16 - 1,
|
||||
y: area.y,
|
||||
width: cols[t].0 as u16,
|
||||
height: area.height
|
||||
};
|
||||
let scene_area = Rect {
|
||||
x: area.x,
|
||||
y: 2 + area.y + (rows[s].1 / 96) as u16,
|
||||
width: area.width,
|
||||
height: (rows[s].0 / 96) as u16
|
||||
};
|
||||
let area = Rect {
|
||||
x: offset + area.x + cols[t].1 as u16 - 1,
|
||||
y: 2 + area.y + (rows[s].1 / 96) as u16,
|
||||
width: cols[t].0 as u16,
|
||||
height: (rows[s].0 / 96) as u16
|
||||
};
|
||||
let lo = Nord::bg_hi(state.focused, state.entered);
|
||||
let hi = Nord::bg_hier(state.focused, state.entered);
|
||||
fill_bg(buf, track_area, lo);
|
||||
fill_bg(buf, scene_area, lo);
|
||||
fill_bg(buf, area, hi);
|
||||
Corners(Style::default().green().not_dim()).draw(buf, area)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tracks_header <'a> (
|
||||
state: &'a Arranger,
|
||||
track_cols: &'a [(usize, usize)],
|
||||
offset: u16,
|
||||
) -> impl Render + 'a {
|
||||
move |buf: &mut Buffer, area: Rect| {
|
||||
let Rect { y, width, .. } = area;
|
||||
for (track, (_, x)) in state.tracks.iter().zip(track_cols) {
|
||||
let x = *x as u16;
|
||||
if x > width {
|
||||
break
|
||||
}
|
||||
track.name.blit(buf, offset + x, y, Some(Style::default()))?;
|
||||
}
|
||||
Ok(Rect { x: area.x, y, width, height: 2 })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scene_rows <'a> (
|
||||
state: &'a Arranger,
|
||||
track_cols: &'a [(usize, usize)],
|
||||
scene_rows: &'a [(usize, usize)],
|
||||
offset: u16,
|
||||
) -> impl Render + 'a {
|
||||
move |buf: &mut Buffer, area: Rect| {
|
||||
let black = Some(Style::default().fg(Nord::SEPARATOR));
|
||||
let Rect { mut y, height, .. } = area;
|
||||
for (_, x) in track_cols.iter() {
|
||||
let x = *x as u16;
|
||||
if x > 0 {
|
||||
for y in area.y-2..y-2 {
|
||||
"▎".blit(buf, x - 1, y, black)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (scene, (pulses, _)) in state.scenes.iter().zip(scene_rows) {
|
||||
if y > height {
|
||||
break
|
||||
}
|
||||
let h = 1.max((pulses / 96) as u16);
|
||||
scene_row(state, buf, Rect {
|
||||
x: area.x,
|
||||
y,
|
||||
width: area.width,
|
||||
height: h,//.min(area.height - y)
|
||||
}, scene, track_cols, offset)?;
|
||||
y = y + h
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
}
|
||||
|
||||
fn scene_row (
|
||||
state: &Arranger,
|
||||
buf: &mut Buffer,
|
||||
area: Rect,
|
||||
scene: &Scene,
|
||||
track_cols: &[(usize, usize)],
|
||||
offset: u16
|
||||
) -> Usually<u16> {
|
||||
let Rect { y, width, .. } = area;
|
||||
let tracks = state.tracks.as_ref();
|
||||
let playing = scene.is_playing(tracks);
|
||||
(if playing { "▶" } else { " " }).blit(buf, area.x, y, None)?;
|
||||
scene.name.blit(buf, area.x + 1, y, None)?;
|
||||
let style = Some(Style::default().white());
|
||||
for (track, (w, x)) in track_cols.iter().enumerate() {
|
||||
let x = *x as u16 + offset;
|
||||
if x > width {
|
||||
break
|
||||
}
|
||||
if let (Some(track), Some(Some(clip))) = (
|
||||
tracks.get(track), scene.clips.get(track)
|
||||
) {
|
||||
if let Some(phrase) = track.phrases.get(*clip) {
|
||||
let phrase = phrase.read().unwrap();
|
||||
phrase.name.blit(buf, x, y, style)?;
|
||||
if track.sequence == Some(*clip) {
|
||||
fill_bg(buf, Rect {
|
||||
x: x - 1,
|
||||
y,
|
||||
width: *w as u16,
|
||||
height: area.height,
|
||||
}, Nord::PLAYING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((scene.pulses(tracks) / 96) as u16)
|
||||
}
|
||||
3
crates/tek_sequencer/src/bin/mod.rs
Normal file
3
crates/tek_sequencer/src/bin/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main () {
|
||||
panic!()
|
||||
}
|
||||
53
crates/tek_sequencer/src/lib.rs
Normal file
53
crates/tek_sequencer/src/lib.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
//! Phrase editor.
|
||||
|
||||
pub(crate) use tek_core::*;
|
||||
pub(crate) use tek_core::ratatui::prelude::*;
|
||||
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
|
||||
pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
|
||||
pub(crate) use tek_jack::{*, jack::*};
|
||||
pub(crate) use tek_timer::*;
|
||||
pub(crate) use std::sync::{Arc, RwLock};
|
||||
|
||||
submod! { midi phrase arranger sequencer sequencer_track }
|
||||
|
||||
/// Key bindings for phrase editor.
|
||||
pub const KEYMAP_SEQUENCER: &'static [KeyBinding<Sequencer>] = keymap!(Sequencer {
|
||||
[Up, NONE, "seq_cursor_up", "move cursor up", |sequencer: &mut Sequencer| {
|
||||
match sequencer.entered {
|
||||
true => { sequencer.note_axis.point_dec(); },
|
||||
false => { sequencer.note_axis.start_dec(); },
|
||||
}
|
||||
Ok(true)
|
||||
}],
|
||||
[Down, NONE, "seq_cursor_down", "move cursor down", |sequencer: &mut Sequencer| {
|
||||
match sequencer.entered {
|
||||
true => { sequencer.note_axis.point_inc(); },
|
||||
false => { sequencer.note_axis.start_inc(); },
|
||||
}
|
||||
Ok(true)
|
||||
}],
|
||||
[Left, NONE, "seq_cursor_left", "move cursor up", |sequencer: &mut Sequencer| {
|
||||
match sequencer.entered {
|
||||
true => { sequencer.time_axis.point_dec(); },
|
||||
false => { sequencer.time_axis.start_dec(); },
|
||||
}
|
||||
Ok(true)
|
||||
}],
|
||||
[Right, NONE, "seq_cursor_right", "move cursor up", |sequencer: &mut Sequencer| {
|
||||
match sequencer.entered {
|
||||
true => { sequencer.time_axis.point_inc(); },
|
||||
false => { sequencer.time_axis.start_inc(); },
|
||||
}
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('`'), NONE, "seq_mode_switch", "switch the display mode", |sequencer: &mut Sequencer| {
|
||||
sequencer.mode = !sequencer.mode;
|
||||
Ok(true)
|
||||
}],
|
||||
/*
|
||||
[Char('a'), NONE, "note_add", "Add note", note_add],
|
||||
[Char('z'), NONE, "note_del", "Delete note", note_del],
|
||||
[CapsLock, NONE, "advance", "Toggle auto advance", nop],
|
||||
[Char('w'), NONE, "rest", "Advance by note duration", nop],
|
||||
*/
|
||||
});
|
||||
36
crates/tek_sequencer/src/midi.rs
Normal file
36
crates/tek_sequencer/src/midi.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use crate::*;
|
||||
use tek_jack::jack::*;
|
||||
|
||||
/// MIDI message serialized to bytes
|
||||
pub type MIDIMessage = Vec<u8>;
|
||||
|
||||
/// Collection of serialized MIDI messages
|
||||
pub type MIDIChunk = [Vec<MIDIMessage>];
|
||||
|
||||
/// Add "all notes off" to the start of a buffer.
|
||||
pub fn all_notes_off (output: &mut MIDIChunk) {
|
||||
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();
|
||||
output[0].push(buf);
|
||||
}
|
||||
|
||||
/// Return boxed iterator of MIDI events
|
||||
pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
|
||||
Box::new(input.map(|RawMidi { time, bytes }|(
|
||||
time as usize,
|
||||
LiveEvent::parse(bytes).unwrap(),
|
||||
bytes
|
||||
)))
|
||||
}
|
||||
|
||||
/// Write to JACK port from output buffer (containing notes from sequence and/or monitor)
|
||||
pub fn write_midi_output (writer: &mut MidiWriter, output: &MIDIChunk, frames: usize) {
|
||||
for time in 0..frames {
|
||||
for event in output[time].iter() {
|
||||
writer.write(&RawMidi { time: time as u32, bytes: &event })
|
||||
.expect(&format!("{event:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
93
crates/tek_sequencer/src/phrase.rs
Normal file
93
crates/tek_sequencer/src/phrase.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use crate::*;
|
||||
|
||||
/// Define a MIDI phrase.
|
||||
#[macro_export] macro_rules! phrase {
|
||||
($($t:expr => $msg:expr),* $(,)?) => {{
|
||||
#[allow(unused_mut)]
|
||||
let mut phrase = BTreeMap::new();
|
||||
$(phrase.insert($t, vec![]);)*
|
||||
$(phrase.get_mut(&$t).unwrap().push($msg);)*
|
||||
phrase
|
||||
}}
|
||||
}
|
||||
|
||||
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A MIDI sequence.
|
||||
pub struct Phrase {
|
||||
pub name: String,
|
||||
pub length: usize,
|
||||
pub notes: PhraseData,
|
||||
pub looped: Option<(usize, usize)>,
|
||||
/// All notes are displayed with minimum length
|
||||
pub percussive: bool
|
||||
}
|
||||
|
||||
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(vec![Vec::with_capacity(16);length]),
|
||||
looped: Some((0, length)),
|
||||
percussive: true,
|
||||
}
|
||||
}
|
||||
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
||||
if pulse >= self.length {
|
||||
panic!("extend phrase first")
|
||||
}
|
||||
self.notes[pulse].push(message);
|
||||
}
|
||||
/// Check if a range `start..end` contains MIDI Note On `k`
|
||||
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
||||
//panic!("{:?} {start} {end}", &self);
|
||||
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
||||
for event in events.iter() {
|
||||
match event {
|
||||
MidiMessage::NoteOn {key,..} => {
|
||||
if *key == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
/// Write a chunk of MIDI events to an output port.
|
||||
pub fn process_out (
|
||||
&self,
|
||||
output: &mut MIDIChunk,
|
||||
notes_on: &mut [bool;128],
|
||||
timebase: &Arc<Timebase>,
|
||||
(frame0, frames, _): (usize, usize, f64),
|
||||
) {
|
||||
let mut buf = Vec::with_capacity(8);
|
||||
for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames(
|
||||
frame0, frame0 + frames
|
||||
) {
|
||||
let tick = tick % self.length;
|
||||
for message in self.notes[tick].iter() {
|
||||
buf.clear();
|
||||
let channel = 0.into();
|
||||
let message = *message;
|
||||
LiveEvent::Midi { channel, message }.write(&mut buf).unwrap();
|
||||
output[time as usize].push(buf.clone());
|
||||
match message {
|
||||
MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true,
|
||||
MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
332
crates/tek_sequencer/src/sequencer.rs
Normal file
332
crates/tek_sequencer/src/sequencer.rs
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
use crate::*;
|
||||
|
||||
/// Phrase editor.
|
||||
pub struct Sequencer {
|
||||
pub mode: bool,
|
||||
pub focused: bool,
|
||||
pub entered: bool,
|
||||
|
||||
pub phrase: Option<Arc<RwLock<Phrase>>>,
|
||||
pub buffer: BigBuffer,
|
||||
pub keys: Buffer,
|
||||
/// Highlight input keys
|
||||
pub keys_in: [bool; 128],
|
||||
/// Highlight output keys
|
||||
pub keys_out: [bool; 128],
|
||||
|
||||
pub now: usize,
|
||||
pub ppq: usize,
|
||||
pub note_axis: FixedAxis<usize>,
|
||||
pub time_axis: ScaledAxis<usize>,
|
||||
|
||||
}
|
||||
|
||||
render!(Sequencer |self, buf, area| {
|
||||
fill_bg(buf, area, Nord::bg_lo(self.focused, self.entered));
|
||||
self.horizontal_draw(buf, area)?;
|
||||
if self.focused && self.entered {
|
||||
Corners(Style::default().green().not_dim()).draw(buf, area)?;
|
||||
}
|
||||
Ok(area)
|
||||
});
|
||||
|
||||
impl Sequencer {
|
||||
pub fn new () -> Self {
|
||||
Self {
|
||||
buffer: Default::default(),
|
||||
keys: keys_vert(),
|
||||
entered: false,
|
||||
focused: false,
|
||||
mode: false,
|
||||
keys_in: [false;128],
|
||||
keys_out: [false;128],
|
||||
phrase: None,
|
||||
now: 0,
|
||||
ppq: 96,
|
||||
note_axis: FixedAxis {
|
||||
start: 12,
|
||||
point: Some(36)
|
||||
},
|
||||
time_axis: ScaledAxis {
|
||||
start: 0,
|
||||
scale: 24,
|
||||
point: Some(0)
|
||||
},
|
||||
}
|
||||
}
|
||||
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
|
||||
/// FIXME: Support phrases longer that 65536 ticks
|
||||
pub fn show (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) -> Usually<()> {
|
||||
self.phrase = phrase.map(Clone::clone);
|
||||
if let Some(ref phrase) = self.phrase {
|
||||
let width = usize::MAX.min(phrase.read().unwrap().length);
|
||||
let mut buffer = BigBuffer::new(width, 64);
|
||||
let phrase = phrase.read().unwrap();
|
||||
fill_seq_bg(&mut buffer, phrase.length, self.ppq)?;
|
||||
fill_seq_fg(&mut buffer, &phrase)?;
|
||||
self.buffer = buffer;
|
||||
} else {
|
||||
self.buffer = Default::default();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn style_focus (&self) -> Option<Style> {
|
||||
Some(if self.focused {
|
||||
Style::default().green().not_dim()
|
||||
} else {
|
||||
Style::default().green().dim()
|
||||
})
|
||||
}
|
||||
|
||||
fn style_timer_step (now: usize, step: usize, next_step: usize) -> Style {
|
||||
if step <= now && now < next_step {
|
||||
Style::default().yellow().bold().not_dim()
|
||||
} else {
|
||||
Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn index_to_color (&self, index: u16, default: Color) -> Color {
|
||||
let index = index as usize;
|
||||
if self.keys_in[index] && self.keys_out[index] {
|
||||
Color::Yellow
|
||||
} else if self.keys_in[index] {
|
||||
Color::Red
|
||||
} else if self.keys_out[index] {
|
||||
Color::Green
|
||||
} else {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
const H_KEYS_OFFSET: usize = 5;
|
||||
|
||||
fn horizontal_draw (&self, buf: &mut Buffer, area: Rect) -> Usually<()> {
|
||||
self.horizontal_keys(buf, area)?;
|
||||
if let Some(ref phrase) = self.phrase {
|
||||
self.horizontal_timer(buf, area, phrase)?;
|
||||
}
|
||||
self.horizontal_notes(buf, area)?;
|
||||
self.horizontal_cursor(buf, area)?;
|
||||
self.horizontal_quant(buf, area)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn horizontal_notes (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
if area.height < 2 {
|
||||
return Ok(area)
|
||||
}
|
||||
let area = Rect {
|
||||
x: area.x + Self::H_KEYS_OFFSET as u16,
|
||||
y: area.y + 1,
|
||||
width: area.width - Self::H_KEYS_OFFSET as u16,
|
||||
height: area.height - 2
|
||||
};
|
||||
buffer_update(buf, area, &move |cell, x, y|{
|
||||
let src_x = ((x as usize + self.time_axis.start) * self.time_axis.scale) as usize;
|
||||
let src_y = (y as usize + self.note_axis.start) as usize;
|
||||
if src_x < self.buffer.width && src_y < self.buffer.height - 1 {
|
||||
let src = self.buffer.get(src_x, self.buffer.height - src_y);
|
||||
src.map(|src|{
|
||||
cell.set_symbol(src.symbol());
|
||||
cell.set_fg(src.fg);
|
||||
});
|
||||
}
|
||||
});
|
||||
Ok(area)
|
||||
}
|
||||
|
||||
fn horizontal_keys (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
if area.height < 2 {
|
||||
return Ok(area)
|
||||
}
|
||||
let area = Rect {
|
||||
x: area.x,
|
||||
y: area.y + 1,
|
||||
width: 5,
|
||||
height: area.height - 2
|
||||
};
|
||||
buffer_update(buf, area, &|cell, x, y|{
|
||||
let y = y + self.note_axis.start as u16;
|
||||
if x < self.keys.area.width && y < self.keys.area.height {
|
||||
*cell = self.keys.get(x, y).clone()
|
||||
}
|
||||
});
|
||||
Ok(area)
|
||||
}
|
||||
|
||||
fn horizontal_quant (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
let quant = ppq_to_name(self.time_axis.scale);
|
||||
let quant_x = area.x + area.width - 1 - quant.len() as u16;
|
||||
let quant_y = area.y + area.height - 2;
|
||||
quant.blit(buf, quant_x, quant_y, self.style_focus())
|
||||
}
|
||||
|
||||
fn horizontal_cursor (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||
if let (Some(time), Some(note)) = (self.time_axis.point, self.note_axis.point) {
|
||||
let x = area.x + Self::H_KEYS_OFFSET as u16 + time as u16;
|
||||
let y = area.y + 1 + note as u16 / 2;
|
||||
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
||||
c.blit(buf, x, y, self.style_focus())
|
||||
} else {
|
||||
Ok(Rect::default())
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_timer (
|
||||
&self, buf: &mut Buffer, area: Rect, phrase: &RwLock<Phrase>
|
||||
) -> Usually<Rect> {
|
||||
let phrase = phrase.read().unwrap();
|
||||
let (time0, time_z, now) = (self.time_axis.start, self.time_axis.scale, self.now % phrase.length);
|
||||
let Rect { x, width, .. } = area;
|
||||
let x2 = x as usize + Self::H_KEYS_OFFSET;
|
||||
let x3 = x as usize + width as usize;
|
||||
for x in x2..x3 {
|
||||
let step = (time0 + x2) * time_z;
|
||||
let next_step = (time0 + x2 + 1) * time_z;
|
||||
let style = Self::style_timer_step(now, step as usize, next_step as usize);
|
||||
"-".blit(buf, x as u16, area.y, Some(style))?;
|
||||
}
|
||||
return Ok(Rect { x: area.x, y: area.y, width: area.width, height: 1 })
|
||||
}
|
||||
}
|
||||
|
||||
fn keys_vert () -> Buffer {
|
||||
let area = Rect { x: 0, y: 0, width: 5, height: 64 };
|
||||
let mut buffer = Buffer::empty(area);
|
||||
buffer_update(&mut buffer, area, &|cell, x, y| {
|
||||
let y = 63 - y;
|
||||
match x {
|
||||
0 => {
|
||||
cell.set_char('▀');
|
||||
let (fg, bg) = key_colors(6 - y % 6);
|
||||
cell.set_fg(fg);
|
||||
cell.set_bg(bg);
|
||||
},
|
||||
1 => {
|
||||
cell.set_char('▀');
|
||||
cell.set_fg(Color::White);
|
||||
cell.set_bg(Color::White);
|
||||
},
|
||||
2 => if y % 6 == 0 {
|
||||
cell.set_char('C');
|
||||
},
|
||||
3 => if y % 6 == 0 {
|
||||
cell.set_symbol(nth_octave(y / 6));
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
buffer
|
||||
}
|
||||
|
||||
fn nth_octave (index: u16) -> &'static str {
|
||||
match index {
|
||||
0 => "-1",
|
||||
1 => "0",
|
||||
2 => "1",
|
||||
3 => "2",
|
||||
4 => "3",
|
||||
5 => "4",
|
||||
6 => "5",
|
||||
7 => "6",
|
||||
8 => "7",
|
||||
9 => "8",
|
||||
10 => "9",
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
fn key_colors (index: u16) -> (Color, Color) {
|
||||
match index % 6 {
|
||||
0 => (Color::White, Color::Black),
|
||||
1 => (Color::White, Color::Black),
|
||||
2 => (Color::White, Color::White),
|
||||
3 => (Color::Black, Color::White),
|
||||
4 => (Color::Black, Color::White),
|
||||
5 => (Color::Black, Color::White),
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) -> Usually<()> {
|
||||
for x in 0..buf.width {
|
||||
if x as usize >= length {
|
||||
break
|
||||
}
|
||||
let style = Style::default();
|
||||
buf.get_mut(x, 0).map(|cell|{
|
||||
cell.set_char('-');
|
||||
cell.set_style(style);
|
||||
});
|
||||
for y in 0 .. buf.height {
|
||||
buf.get_mut(x, y).map(|cell|{
|
||||
cell.set_char(char_seq_bg(ppq, x as u16));
|
||||
cell.set_fg(Color::Gray);
|
||||
cell.modifier = Modifier::DIM;
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn char_seq_bg (ppq: usize, x: u16) -> char {
|
||||
if ppq == 0 {
|
||||
'·'
|
||||
} else if x % (4 * ppq as u16) == 0 {
|
||||
'│'
|
||||
} else if x % ppq as u16 == 0 {
|
||||
'╎'
|
||||
} else {
|
||||
'·'
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) -> Usually<()> {
|
||||
let mut notes_on = [false;128];
|
||||
for x in 0..buf.width {
|
||||
if x as usize >= phrase.length {
|
||||
break
|
||||
}
|
||||
if let Some(notes) = phrase.notes.get(x as usize) {
|
||||
if phrase.percussive {
|
||||
for note in notes {
|
||||
match note {
|
||||
MidiMessage::NoteOn { key, .. } =>
|
||||
notes_on[key.as_int() as usize] = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for note in notes {
|
||||
match note {
|
||||
MidiMessage::NoteOn { key, .. } =>
|
||||
notes_on[key.as_int() as usize] = true,
|
||||
MidiMessage::NoteOff { key, .. } =>
|
||||
notes_on[key.as_int() as usize] = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
for y in 0..buf.height/2 {
|
||||
if y >= 64 {
|
||||
break
|
||||
}
|
||||
if let Some(block) = half_block(
|
||||
notes_on[y as usize * 2],
|
||||
notes_on[y as usize * 2 + 1],
|
||||
) {
|
||||
buf.get_mut(x, y).map(|cell|{
|
||||
cell.set_char(block);
|
||||
cell.set_fg(Color::White);
|
||||
});
|
||||
}
|
||||
}
|
||||
if phrase.percussive {
|
||||
notes_on.fill(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
227
crates/tek_sequencer/src/sequencer_track.rs
Normal file
227
crates/tek_sequencer/src/sequencer_track.rs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
use crate::*;
|
||||
use tek_core::Direction;
|
||||
|
||||
/// A sequencer track.
|
||||
#[derive(Debug)]
|
||||
pub struct SequencerTrack {
|
||||
pub name: String,
|
||||
/// Play input through output.
|
||||
pub monitoring: bool,
|
||||
/// Write input to sequence.
|
||||
pub recording: bool,
|
||||
/// Overdub input to sequence.
|
||||
pub overdub: bool,
|
||||
/// Map: tick -> MIDI events at tick
|
||||
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||
/// Phrase selector
|
||||
pub sequence: Option<usize>,
|
||||
/// Output from current sequence.
|
||||
pub midi_out: Option<Port<MidiOut>>,
|
||||
/// MIDI output buffer
|
||||
midi_out_buf: Vec<Vec<Vec<u8>>>,
|
||||
/// Send all notes off
|
||||
pub reset: bool, // TODO?: after Some(nframes)
|
||||
/// Highlight keys on piano roll.
|
||||
pub notes_in: [bool;128],
|
||||
/// Highlight keys on piano roll.
|
||||
pub notes_out: [bool;128],
|
||||
}
|
||||
|
||||
impl SequencerTrack {
|
||||
pub fn new (name: &str) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
monitoring: false,
|
||||
recording: false,
|
||||
overdub: true,
|
||||
phrases: vec![],
|
||||
sequence: None,
|
||||
midi_out: None,
|
||||
midi_out_buf: vec![Vec::with_capacity(16);16384],
|
||||
reset: true,
|
||||
notes_in: [false;128],
|
||||
notes_out: [false;128],
|
||||
})
|
||||
}
|
||||
pub fn toggle_monitor (&mut self) {
|
||||
self.monitoring = !self.monitoring;
|
||||
}
|
||||
pub fn toggle_record (&mut self) {
|
||||
self.recording = !self.recording;
|
||||
}
|
||||
pub fn toggle_overdub (&mut self) {
|
||||
self.overdub = !self.overdub;
|
||||
}
|
||||
pub fn process (
|
||||
&mut self,
|
||||
input: Option<MidiIter>,
|
||||
timebase: &Arc<Timebase>,
|
||||
playing: Option<TransportState>,
|
||||
started: Option<(usize, usize)>,
|
||||
quant: usize,
|
||||
reset: bool,
|
||||
scope: &ProcessScope,
|
||||
(frame0, frames): (usize, usize),
|
||||
(_usec0, _usecs): (usize, usize),
|
||||
period: f64,
|
||||
) {
|
||||
if self.midi_out.is_some() {
|
||||
// Clear the section of the output buffer that we will be using
|
||||
for frame in &mut self.midi_out_buf[0..frames] {
|
||||
frame.clear();
|
||||
}
|
||||
// Emit "all notes off" at start of buffer if requested
|
||||
if self.reset {
|
||||
all_notes_off(&mut self.midi_out_buf);
|
||||
self.reset = false;
|
||||
} else if reset {
|
||||
all_notes_off(&mut self.midi_out_buf);
|
||||
}
|
||||
}
|
||||
if let (
|
||||
Some(TransportState::Rolling), Some((start_frame, _)), Some(phrase)
|
||||
) = (
|
||||
playing, started, self.sequence.and_then(|id|self.phrases.get_mut(id))
|
||||
) {
|
||||
phrase.read().map(|phrase|{
|
||||
if self.midi_out.is_some() {
|
||||
phrase.process_out(
|
||||
&mut self.midi_out_buf,
|
||||
&mut self.notes_out,
|
||||
timebase,
|
||||
(frame0.saturating_sub(start_frame), frames, period)
|
||||
);
|
||||
}
|
||||
}).unwrap();
|
||||
let mut phrase = phrase.write().unwrap();
|
||||
let length = phrase.length;
|
||||
// Monitor and record input
|
||||
if input.is_some() && (self.recording || self.monitoring) {
|
||||
// For highlighting keys and note repeat
|
||||
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
|
||||
match event {
|
||||
LiveEvent::Midi { message, .. } => {
|
||||
if self.monitoring {
|
||||
self.midi_out_buf[frame].push(bytes.to_vec())
|
||||
}
|
||||
if self.recording {
|
||||
phrase.record_event({
|
||||
let pulse = timebase.frame_to_pulse(
|
||||
(frame0 + frame - start_frame) as f64
|
||||
);
|
||||
let quantized = (
|
||||
pulse / quant as f64
|
||||
).round() as usize * quant;
|
||||
let looped = quantized % length;
|
||||
looped
|
||||
}, message);
|
||||
}
|
||||
match message {
|
||||
MidiMessage::NoteOn { key, .. } => {
|
||||
self.notes_in[key.as_int() as usize] = true;
|
||||
}
|
||||
MidiMessage::NoteOff { key, .. } => {
|
||||
self.notes_in[key.as_int() as usize] = false;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if input.is_some() && self.midi_out.is_some() && self.monitoring {
|
||||
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
|
||||
self.process_monitor_event(frame, &event, bytes)
|
||||
}
|
||||
}
|
||||
if let Some(out) = &mut self.midi_out {
|
||||
write_midi_output(&mut out.writer(scope), &self.midi_out_buf, frames);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn process_monitor_event (&mut self, frame: usize, event: &LiveEvent, bytes: &[u8]) {
|
||||
match event {
|
||||
LiveEvent::Midi { message, .. } => {
|
||||
self.write_to_output_buffer(frame, bytes);
|
||||
self.process_monitor_message(&message);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline] fn write_to_output_buffer (&mut self, frame: usize, bytes: &[u8]) {
|
||||
self.midi_out_buf[frame].push(bytes.to_vec());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn process_monitor_message (&mut self, message: &MidiMessage) {
|
||||
match message {
|
||||
MidiMessage::NoteOn { key, .. } => {
|
||||
self.notes_in[key.as_int() as usize] = true;
|
||||
}
|
||||
MidiMessage::NoteOff { key, .. } => {
|
||||
self.notes_in[key.as_int() as usize] = false;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct ChainView<'a> {
|
||||
pub track: Option<&'a Track>,
|
||||
pub direction: Direction,
|
||||
pub focused: bool,
|
||||
pub entered: bool,
|
||||
}
|
||||
|
||||
impl<'a> ChainView<'a> {
|
||||
pub fn horizontal (app: &'a App) -> Self {
|
||||
Self::new(app, Direction::Right)
|
||||
}
|
||||
pub fn vertical (app: &'a App) -> Self {
|
||||
Self::new(app, Direction::Down)
|
||||
}
|
||||
pub fn new (app: &'a App, direction: Direction) -> Self {
|
||||
Self {
|
||||
direction,
|
||||
entered: app.entered,
|
||||
focused: app.section == AppFocus::Chain,
|
||||
track: app.arranger.track()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Render for ChainView<'a> {
|
||||
fn render (&self, buf: &mut Buffer, mut area: Rect) -> Usually<Rect> {
|
||||
if let Some(track) = self.track {
|
||||
match self.direction {
|
||||
Direction::Down => area.width = area.width.min(40),
|
||||
Direction::Right => area.width = area.width.min(10),
|
||||
}
|
||||
fill_bg(buf, area, Nord::bg_lo(self.focused, self.entered));
|
||||
let (area, areas) = self.direction
|
||||
.split_focus(0, track.devices.as_slice(), if self.focused {
|
||||
Style::default().green().dim()
|
||||
} else {
|
||||
Style::default().dim()
|
||||
})
|
||||
.render_areas(buf, area)?;
|
||||
if self.focused && self.entered {
|
||||
Corners(Style::default().green().not_dim()).draw(buf, areas[0])?;
|
||||
}
|
||||
Ok(area)
|
||||
} else {
|
||||
let Rect { x, y, width, height } = area;
|
||||
let label = "No track selected";
|
||||
let x = x + (width - label.len() as u16) / 2;
|
||||
let y = y + height / 2;
|
||||
label.blit(buf, x, y, Some(Style::default().dim().bold()))?;
|
||||
Ok(area)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue