mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-07-17 07:46:57 +02:00
852 lines
37 KiB
Rust
852 lines
37 KiB
Rust
use ::std::sync::{Arc, RwLock};
|
|
use ::tengri::{space::east, color::ItemTheme};
|
|
use ::tengri::{draw::*, term::*};
|
|
use crate::{*, device::*, editor::*, sequence::*, clock::*, select::*, sample::*};
|
|
|
|
/// Arranger.
|
|
///
|
|
/// ```
|
|
/// let arranger = tek::Arrangement::default();
|
|
/// ```
|
|
#[derive(Default, Debug)] pub struct Arrangement {
|
|
/// Project name.
|
|
pub name: Arc<str>,
|
|
/// Base color.
|
|
pub color: ItemTheme,
|
|
/// JACK client handle.
|
|
pub jack: Jack<'static>,
|
|
/// FIXME a render of the project arrangement, redrawn on update.
|
|
/// TODO rename to "render_cache" or smth
|
|
pub arranger: Arc<RwLock<Buffer>>,
|
|
/// Display size
|
|
pub size: Size,
|
|
/// Display size of clips area
|
|
pub size_inner: Size,
|
|
/// Source of time
|
|
#[cfg(feature = "clock")] pub clock: Clock,
|
|
/// Allows one MIDI clip to be edited
|
|
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
|
|
/// List of global midi inputs
|
|
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
|
/// List of global midi outputs
|
|
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
|
/// List of global audio inputs
|
|
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
|
/// List of global audio outputs
|
|
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
|
/// Selected UI element
|
|
#[cfg(feature = "select")] pub selection: Selection,
|
|
/// Last track number (to avoid duplicate port names)
|
|
#[cfg(feature = "track")] pub track_last: usize,
|
|
/// List of tracks
|
|
#[cfg(feature = "track")] pub tracks: Vec<Track>,
|
|
/// Scroll offset of tracks
|
|
#[cfg(feature = "track")] pub track_scroll: usize,
|
|
/// List of scenes
|
|
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
|
|
/// Scroll offset of scenes
|
|
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
|
}
|
|
|
|
/// A track consists of a sequencer and zero or more devices chained after it.
|
|
///
|
|
/// ```
|
|
/// let track: tek::Track = Default::default();
|
|
/// ```
|
|
#[derive(Debug, Default)] pub struct Track {
|
|
/// Name of track
|
|
pub name: Arc<str>,
|
|
/// Identifying color of track
|
|
pub color: ItemTheme,
|
|
/// Preferred width of track column
|
|
pub width: usize,
|
|
/// MIDI sequencer state
|
|
pub sequencer: Sequencer,
|
|
/// Device chain
|
|
pub devices: Vec<Device>,
|
|
}
|
|
|
|
/// A scene consists of a set of clips to play together.
|
|
///
|
|
/// ```
|
|
/// let scene: tek::Scene = Default::default();
|
|
/// let _ = scene.pulses();
|
|
/// let _ = scene.is_playing(&[]);
|
|
/// ```
|
|
#[derive(Debug, Default)] pub struct Scene {
|
|
/// Name of scene
|
|
pub name: Arc<str>,
|
|
/// Identifying color of scene
|
|
pub color: ItemTheme,
|
|
/// Clips in scene, one per track
|
|
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
|
}
|
|
|
|
impl_has!(Jack<'static>: |self: Arrangement| self.jack);
|
|
impl HasJack<'static> for Arrangement {
|
|
fn jack (&self) -> &Jack<'static> { &self.jack }
|
|
}
|
|
|
|
impl_has!(Size: |self: Arrangement| self.size);
|
|
impl_has!(Vec<Track>: |self: Arrangement| self.tracks);
|
|
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
|
|
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins);
|
|
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs);
|
|
impl_has!(Clock: |self: Arrangement| self.clock);
|
|
impl_has!(Selection: |self: Arrangement| self.selection);
|
|
impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref());
|
|
impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut());
|
|
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
|
|
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
|
|
|
|
impl <T: AsRef<Vec<Scene>>+AsMut<Vec<Scene>>> HasScenes for T {}
|
|
impl <T: AsRef<Vec<Track>>+AsMut<Vec<Track>>> HasTracks for T {}
|
|
impl <T: AsRefOpt<Scene>+AsMutOpt<Scene>+Send+Sync> HasScene for T {}
|
|
impl <T: AsRefOpt<Track>+AsMutOpt<Track>+Send+Sync> HasTrack for T {}
|
|
impl <T: ScenesView+HasMidiIns+HasMidiOuts+HasTrackScroll> TracksView for T {}
|
|
impl <T: TracksView+ScenesView+Send+Sync> ClipsView for T {}
|
|
|
|
pub trait ClipsView: TracksView + ScenesView {
|
|
|
|
fn view_scenes_clips <'a> (&'a self)
|
|
-> impl Draw<Tui> + 'a
|
|
{
|
|
self.clips_size().of(wh_full(above(
|
|
wh_full(origin_se(fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))),
|
|
thunk(|to: &mut Tui|{
|
|
for (track_index, track, _, _) in self.tracks_with_sizes() {
|
|
to.place(&w_exact(track.width as u16,
|
|
h_full(self.view_track_clips(track_index, track))))
|
|
}
|
|
Ok(XYWH(0, 0, 0, 0))
|
|
}))))
|
|
}
|
|
|
|
fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw<Tui> + 'a {
|
|
thunk(move|to: &mut Tui|{
|
|
for (scene_index, scene, ..) in self.scenes_with_sizes() {
|
|
let (name, theme): (Arc<str>, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
|
let clip = clip.read().unwrap();
|
|
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
|
} else {
|
|
(" ⏹ -- ".into(), ItemTheme::G[32])
|
|
};
|
|
let fg = theme.lightest.term;
|
|
let mut outline = theme.base.term;
|
|
let bg = if self.selection().track() == Some(track_index)
|
|
&& self.selection().scene() == Some(scene_index)
|
|
{
|
|
outline = theme.lighter.term;
|
|
theme.light.term
|
|
} else if self.selection().track() == Some(track_index)
|
|
|| self.selection().scene() == Some(scene_index)
|
|
{
|
|
outline = theme.darkest.term;
|
|
theme.base.term
|
|
} else {
|
|
theme.dark.term
|
|
};
|
|
let w = if self.selection().track() == Some(track_index)
|
|
&& let Some(editor) = self.editor ()
|
|
{
|
|
(editor.size.w() as usize).max(24).max(track.width)
|
|
} else {
|
|
track.width
|
|
} as u16;
|
|
let y = if self.selection().scene() == Some(scene_index)
|
|
&& let Some(editor) = self.editor ()
|
|
{
|
|
(editor.size.h() as usize).max(12)
|
|
} else {
|
|
Self::H_SCENE as usize
|
|
} as u16;
|
|
|
|
let is_selected =
|
|
self.selection().track() == Some(track_index) &&
|
|
self.selection().scene() == Some(scene_index) &&
|
|
self.is_editing();
|
|
|
|
to.place(&wh_exact(Some(w), Some(y), below(
|
|
wh_full(Outer(true, Style::default().fg(outline))),
|
|
wh_full(below(
|
|
below(
|
|
fg_bg(outline, bg, wh_full("")),
|
|
wh_full(origin_nw(fg_bg(fg, bg, bold(true, &name)))),
|
|
),
|
|
wh_full(when(is_selected, self.editor().map(|e|e.view()))))))));
|
|
}
|
|
Ok(XYWH(0, 0, 0, 0))
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll {
|
|
|
|
fn tracks_width_available (&self) -> u16 {
|
|
(self.size.width() as u16).saturating_sub(40)
|
|
}
|
|
|
|
/// Iterate over tracks with their corresponding sizes.
|
|
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
|
let _editor_width = self.editor().map(|e|e.size.w());
|
|
let _active_track = self.selection().track();
|
|
let mut x = 0;
|
|
self.tracks().iter().enumerate().map_while(move |(index, track)|{
|
|
let width = track.width.max(8);
|
|
if x + width < self.clips_size().w() as usize {
|
|
let data = (index, track, x, x + width);
|
|
x += width + Self::TRACK_SPACING;
|
|
Some(data)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
fn view_track_names (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
|
let track_count = self.tracks().len();
|
|
let scene_count = self.scenes().len();
|
|
let selected = self.selection();
|
|
let button = south(
|
|
button_3("t", "rack ", format!("{}{track_count}", selected.track()
|
|
.map(|track|format!("{track}/")).unwrap_or_default()), false),
|
|
button_3("s", "cene ", format!("{}{scene_count}", selected.scene()
|
|
.map(|scene|format!("{scene}/")).unwrap_or_default()), false));
|
|
let button_2 = south(
|
|
button_2("T", "+", false),
|
|
button_2("S", "+", false));
|
|
view_track_row_section(theme, button, button_2, bg(theme.darker.term,
|
|
h_exact(2, thunk(|to: &mut Tui|{
|
|
for (index, track, x1, _x2) in self.tracks_with_sizes() {
|
|
to.place(&x_push(x1 as u16, w_exact(track_width(index, track),
|
|
bg(if selected.track() == Some(index) {
|
|
track.color.light.term
|
|
} else {
|
|
track.color.base.term
|
|
}, south(w_full(origin_nw(east(
|
|
format!("·t{index:02} "),
|
|
fg(Rgb(255, 255, 255), bold(true, &track.name))
|
|
))), ""))) ));}
|
|
Ok(XYWH(0, 0, 0, 0))
|
|
}))))
|
|
}
|
|
|
|
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw<Tui> {
|
|
view_track_row_section(theme,
|
|
south(w_full(origin_w(button_2("o", "utput", false))),
|
|
thunk(|to: &mut Tui|{
|
|
for port in self.midi_outs().iter() {
|
|
to.place(&w_full(origin_w(port.port_name())));
|
|
}
|
|
Ok(XYWH(0, 0, 0, 0))
|
|
})),
|
|
button_2("O", "+", false),
|
|
bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{
|
|
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
|
to.place(&w_exact(track_width(index, track),
|
|
origin_nw(h_full(iter_south(||track.sequencer.midi_outs.iter(),
|
|
|port, index|fg(Rgb(255, 255, 255),
|
|
h_exact(1, bg(track.color.dark.term, w_full(origin_w(
|
|
format!("·o{index:02} {}", port.port_name())))))))))));
|
|
}
|
|
Ok(XYWH(0, 0, 0, 0))
|
|
}))))
|
|
}
|
|
|
|
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> {
|
|
let mut h = 0u16;
|
|
for track in self.tracks().iter() {
|
|
h = h.max(track.sequencer.midi_ins.len() as u16);
|
|
}
|
|
let content = thunk(move|to: &mut Tui|{
|
|
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
|
to.place(&wh_exact(track_width(index, track), h + 1,
|
|
origin_nw(south(
|
|
bg(track.color.base.term,
|
|
w_full(origin_w(east!(
|
|
either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "),
|
|
either(track.sequencer.recording, fg(Red, "●rec "), "·rec "),
|
|
either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "),
|
|
)))),
|
|
iter_south(1, ||track.sequencer.midi_ins.iter(),
|
|
|port, index|fg_bg(Rgb(255, 255, 255), track.color.dark.term,
|
|
w_full(origin_w(format!("·i{index:02} {}", port.port_name())))))))));
|
|
}
|
|
Ok(XYWH(0, 0, 0, 0))
|
|
});
|
|
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
|
|
bg(theme.darker.term, origin_w(content)))
|
|
}
|
|
|
|
}
|
|
|
|
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
|
|
/// Default scene height.
|
|
const H_SCENE: usize = 2;
|
|
/// Default editor height.
|
|
const H_EDITOR: usize = 15;
|
|
fn h_scenes (&self) -> u16;
|
|
fn w_side (&self) -> u16;
|
|
fn w_mid (&self) -> u16;
|
|
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
|
|
let mut y = 0;
|
|
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
|
|
let height = if self.selection().scene() == Some(s) && self.editor().is_some() {
|
|
8
|
|
} else {
|
|
Self::H_SCENE
|
|
};
|
|
if y + height <= self.clips_size().h() as usize {
|
|
let data = (s, scene, y, y + height);
|
|
y += height;
|
|
Some(data)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
fn view_scenes_names (&self) -> impl Draw<Tui> {
|
|
w_exact(20, thunk(|to: &mut Tui|for (index, scene, ..) in self.scenes_with_sizes() {
|
|
to.place(&self.view_scene_name(index, scene));
|
|
}))
|
|
}
|
|
|
|
fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw<Tui> + 'a {
|
|
let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() {
|
|
7
|
|
} else {
|
|
Self::H_SCENE as u16
|
|
};
|
|
let bg = if self.selection().scene() == Some(index) {
|
|
scene.color.light.term
|
|
} else {
|
|
scene.color.base.term
|
|
};
|
|
let a = w_full(origin_w(east(format!("·s{index:02} "),
|
|
fg(g(255), bold(true, &scene.name)))));
|
|
let b = when(self.selection().scene() == Some(index) && self.is_editing(),
|
|
wh_full(origin_nw(south(
|
|
self.editor().as_ref().map(|e|e.clip_status()),
|
|
self.editor().as_ref().map(|e|e.edit_status())))));
|
|
wh_exact(20, h, bg(bg, origin_nw(south(a, b))))
|
|
}
|
|
|
|
}
|
|
pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; }
|
|
pub trait HasTrackScroll: HasTracks { fn track_scroll (&self) -> usize; }
|
|
pub trait HasScene: AsRefOpt<Scene> + AsMutOpt<Scene> {
|
|
fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() }
|
|
fn scene (&self) -> Option<&Scene> { self.as_ref_opt() }
|
|
}
|
|
pub trait HasScenes: AsRef<Vec<Scene>> + AsMut<Vec<Scene>> {
|
|
fn scenes (&self) -> &Vec<Scene> { self.as_ref() }
|
|
fn scenes_mut (&mut self) -> &mut Vec<Scene> { self.as_mut() }
|
|
/// Generate the default name for a new scene
|
|
fn scene_default_name (&self) -> Arc<str> { format!("s{:3>}", self.scenes().len() + 1).into() }
|
|
fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) }
|
|
/// Add multiple scenes
|
|
fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks {
|
|
let scene_color_1 = ItemColor::random();
|
|
let scene_color_2 = ItemColor::random();
|
|
for i in 0..n {
|
|
let _ = self.scene_add(None, Some(
|
|
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
|
))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
/// Add a scene
|
|
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemTheme>)
|
|
-> Usually<(usize, &mut Scene)> where Self: HasTracks
|
|
{
|
|
let scene = Scene {
|
|
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
|
clips: vec![None;self.tracks().len()],
|
|
color: color.unwrap_or_else(ItemTheme::random),
|
|
};
|
|
self.scenes_mut().push(scene);
|
|
let index = self.scenes().len() - 1;
|
|
Ok((index, &mut self.scenes_mut()[index]))
|
|
}
|
|
}
|
|
pub trait HasTracks: AsRef<Vec<Track>> + AsMut<Vec<Track>> {
|
|
fn tracks (&self) -> &Vec<Track> { self.as_ref() }
|
|
fn tracks_mut (&mut self) -> &mut Vec<Track> { self.as_mut() }
|
|
/// Run audio callbacks for every track and every device
|
|
fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
|
for track in self.tracks_mut().iter_mut() {
|
|
if Control::Quit == Audio::process(&mut track.sequencer, client, scope) {
|
|
return Control::Quit
|
|
}
|
|
for device in track.devices.iter_mut() {
|
|
if Control::Quit == DeviceAudio(device).process(client, scope) {
|
|
return Control::Quit
|
|
}
|
|
}
|
|
}
|
|
Control::Continue
|
|
}
|
|
fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) }
|
|
/// Stop all playing clips
|
|
fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } }
|
|
/// Stop all playing clips
|
|
fn tracks_launch (&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>) {
|
|
if let Some(clips) = clips {
|
|
for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); }
|
|
} else {
|
|
for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); }
|
|
}
|
|
}
|
|
/// Spacing between tracks.
|
|
const TRACK_SPACING: usize = 0;
|
|
}
|
|
pub trait HasTrack: AsRefOpt<Track> + AsMutOpt<Track> {
|
|
fn track (&self) -> Option<&Track> { self.as_ref_opt() }
|
|
fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() }
|
|
#[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> + 'a {
|
|
self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
|
|
}
|
|
#[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> + '_ {
|
|
self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
|
|
}
|
|
#[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
|
self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
|
|
}
|
|
#[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
|
self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
|
}
|
|
}
|
|
impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref());
|
|
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
|
|
#[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt());
|
|
#[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt());
|
|
impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } }
|
|
impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } }
|
|
|
|
impl HasWidth for Track {
|
|
const MIN_WIDTH: usize = 9;
|
|
fn width_inc (&mut self) { self.width += 1; }
|
|
fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } }
|
|
}
|
|
|
|
impl Track {
|
|
/// Create a new track with only the default [Sequencer].
|
|
pub fn new (
|
|
name: &impl AsRef<str>,
|
|
color: Option<ItemTheme>,
|
|
jack: &Jack<'static>,
|
|
clock: Option<&Clock>,
|
|
clip: Option<&Arc<RwLock<MidiClip>>>,
|
|
midi_from: &[Connect],
|
|
midi_to: &[Connect],
|
|
) -> Usually<Self> {
|
|
Ok(Self {
|
|
name: name.as_ref().into(),
|
|
color: color.unwrap_or_default(),
|
|
sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?,
|
|
..Default::default()
|
|
})
|
|
}
|
|
pub fn audio_ins (&self) -> &[AudioInput] {
|
|
self.devices.first().map(|x|x.audio_ins()).unwrap_or_default()
|
|
}
|
|
pub fn audio_outs (&self) -> &[AudioOutput] {
|
|
self.devices.last().map(|x|x.audio_outs()).unwrap_or_default()
|
|
}
|
|
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
|
|
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
|
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
|
|
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
|
|
pub fn per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
|
) -> impl Draw<Tui> + 'a {
|
|
iter(tracks,
|
|
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
|
let width = (x2 - x1) as u16;
|
|
iter_east(x1 as u16, width, w_exact(width, fg_bg(
|
|
track.color.lightest.term,
|
|
track.color.base.term,
|
|
callback(index, track))))})
|
|
}
|
|
/// Create a new track connecting the [Sequencer] to a [Sampler].
|
|
#[cfg(feature = "sampler")] pub fn new_with_sampler (
|
|
name: &impl AsRef<str>,
|
|
color: Option<ItemTheme>,
|
|
jack: &Jack<'static>,
|
|
clock: Option<&Clock>,
|
|
clip: Option<&Arc<RwLock<MidiClip>>>,
|
|
midi_from: &[Connect],
|
|
midi_to: &[Connect],
|
|
audio_from: &[&[Connect];2],
|
|
audio_to: &[&[Connect];2],
|
|
) -> Usually<Self> {
|
|
let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?;
|
|
let client_name = jack.with_client(|c|c.name().to_string());
|
|
let port_name = track.sequencer.midi_outs[0].port_name();
|
|
let connect = [Connect::exact(format!("{client_name}:{}", port_name))];
|
|
track.devices.push(Device::Sampler(Sampler::new(
|
|
jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to
|
|
)?));
|
|
Ok(track)
|
|
}
|
|
#[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> {
|
|
for device in self.devices.iter() {
|
|
match device {
|
|
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
|
|
_ => {}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
#[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> {
|
|
for device in self.devices.iter_mut() {
|
|
match device {
|
|
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
|
|
_ => {}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn per_track <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
|
) -> impl Draw<Tui> + 'a {
|
|
per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track))))
|
|
}
|
|
|
|
pub fn per_track_top <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
|
) -> impl Draw<Tui> + 'a {
|
|
origin_x(bg(Reset, iter(tracks,
|
|
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
|
let width = (x2 - x1) as u16;
|
|
iter_east(x1 as u16, width, w_exact(width, fg_bg(
|
|
track.color.lightest.term,
|
|
track.color.base.term,
|
|
callback(index, track))))})))
|
|
}
|
|
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
|
|
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
|
|
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene());
|
|
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut());
|
|
impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } }
|
|
impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } }
|
|
impl ScenesView for App {
|
|
fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(self.w_side()) }
|
|
fn w_side (&self) -> u16 { 20 }
|
|
fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) }
|
|
}
|
|
impl Scene {
|
|
/// Returns the pulse length of the longest clip in the scene
|
|
pub fn pulses (&self) -> usize {
|
|
self.clips.iter().fold(0, |a, p|{
|
|
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
|
})
|
|
}
|
|
/// Returns true if all clips in the scene are
|
|
/// currently playing on the given collection of tracks.
|
|
pub fn is_playing (&self, tracks: &[Track]) -> bool {
|
|
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
|
|
.all(|(track_index, clip)|match clip {
|
|
Some(c) => tracks
|
|
.get(track_index)
|
|
.map(|track|{
|
|
if let Some((_, Some(clip))) = track.sequencer().play_clip() {
|
|
*clip.read().unwrap() == *c.read().unwrap()
|
|
} else {
|
|
false
|
|
}
|
|
})
|
|
.unwrap_or(false),
|
|
None => true
|
|
})
|
|
}
|
|
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
|
|
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
|
}
|
|
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
|
|
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
|
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
|
|
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
|
|
}
|
|
|
|
impl Arrangement {
|
|
/// Create a new arrangement.
|
|
pub fn new (
|
|
jack: &Jack<'static>,
|
|
name: Option<Arc<str>>,
|
|
clock: Clock,
|
|
tracks: Vec<Track>,
|
|
scenes: Vec<Scene>,
|
|
midi_ins: Vec<MidiInput>,
|
|
midi_outs: Vec<MidiOutput>,
|
|
) -> Self {
|
|
Self {
|
|
clock, tracks, scenes, midi_ins, midi_outs,
|
|
jack: jack.clone(),
|
|
name: name.unwrap_or_default(),
|
|
color: ItemTheme::random(),
|
|
selection: Selection::TrackClip { track: 0, scene: 0 },
|
|
..Default::default()
|
|
}
|
|
}
|
|
/// Width of display
|
|
pub fn w (&self) -> u16 {
|
|
self.size.w() as u16
|
|
}
|
|
/// Width allocated for sidebar.
|
|
pub fn w_sidebar (&self, is_editing: bool) -> u16 {
|
|
self.w() / if is_editing { 16 } else { 8 } as u16
|
|
}
|
|
/// Width available to display tracks.
|
|
pub fn w_tracks_area (&self, is_editing: bool) -> u16 {
|
|
self.w().saturating_sub(self.w_sidebar(is_editing))
|
|
}
|
|
/// Height of display
|
|
pub fn h (&self) -> u16 {
|
|
self.size.h() as u16
|
|
}
|
|
/// Height taken by visible device slots.
|
|
pub fn h_devices (&self) -> u16 {
|
|
2
|
|
//1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
|
}
|
|
/// Add multiple tracks
|
|
#[cfg(feature = "track")] pub fn tracks_add (
|
|
&mut self,
|
|
count: usize, width: Option<usize>,
|
|
mins: &[Connect], mouts: &[Connect],
|
|
) -> Usually<()> {
|
|
let track_color_1 = ItemColor::random();
|
|
let track_color_2 = ItemColor::random();
|
|
for i in 0..count {
|
|
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
|
|
let track = self.track_add(None, Some(color), mins, mouts)?.1;
|
|
if let Some(width) = width {
|
|
track.width = width;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
/// Add a track
|
|
#[cfg(feature = "track")] pub fn track_add (
|
|
&mut self,
|
|
name: Option<&str>, color: Option<ItemTheme>,
|
|
mins: &[Connect], mouts: &[Connect],
|
|
) -> Usually<(usize, &mut Track)> {
|
|
let name: Arc<str> = name.map_or_else(
|
|
||format!("trk{:02}", self.track_last).into(),
|
|
|x|x.to_string().into()
|
|
);
|
|
self.track_last += 1;
|
|
let track = Track {
|
|
width: (name.len() + 2).max(12),
|
|
color: color.unwrap_or_else(ItemTheme::random),
|
|
sequencer: Sequencer::new(
|
|
&format!("{name}"),
|
|
self.jack(),
|
|
Some(self.clock()),
|
|
None,
|
|
mins,
|
|
mouts
|
|
)?,
|
|
name,
|
|
..Default::default()
|
|
};
|
|
self.tracks_mut().push(track);
|
|
let len = self.tracks().len();
|
|
let index = len - 1;
|
|
for scene in self.scenes_mut().iter_mut() {
|
|
while scene.clips.len() < len {
|
|
scene.clips.push(None);
|
|
}
|
|
}
|
|
Ok((index, &mut self.tracks_mut()[index]))
|
|
}
|
|
#[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw<Tui> + '_ {
|
|
south(
|
|
h_exact(1, self.view_inputs_header()),
|
|
thunk(|to: &mut Tui|{
|
|
for (index, port) in self.midi_ins().iter().enumerate() {
|
|
to.place(&x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port))))
|
|
}
|
|
})
|
|
)
|
|
}
|
|
#[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw<Tui> + '_ {
|
|
east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))),
|
|
west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() {
|
|
#[cfg(feature = "track")]
|
|
to.place(&x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!(
|
|
either(track.sequencer.monitoring, fg(Green, "mon "), "mon "),
|
|
either(track.sequencer.recording, fg(Red, "rec "), "rec "),
|
|
either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "),
|
|
))))))
|
|
})))
|
|
}
|
|
#[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw<Tui> {
|
|
east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))),
|
|
west(w_exact(4, ()), thunk(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() {
|
|
#[cfg(feature = "track")]
|
|
to.place(&bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!(
|
|
either(track.sequencer.monitoring, fg(Green, " ● "), " · "),
|
|
either(track.sequencer.recording, fg(Red, " ● "), " · "),
|
|
either(track.sequencer.overdub, fg(Yellow, " ● "), " · "),
|
|
)))))
|
|
})))
|
|
}
|
|
#[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
|
let mut h = 1;
|
|
for output in self.midi_outs().iter() {
|
|
h += 1 + output.connections.len();
|
|
}
|
|
let h = h as u16;
|
|
let list = south(
|
|
h_exact(1, w_full(origin_w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))),
|
|
h_exact(h - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{
|
|
for (_index, port) in self.midi_outs().iter().enumerate() {
|
|
to.place(&h_exact(1,w_full(east(
|
|
origin_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))),
|
|
w_full(origin_e(format!("{}/{} ",
|
|
port.port().get_connections().len(),
|
|
port.connections.len())))))));
|
|
for (index, conn) in port.connections.iter().enumerate() {
|
|
to.place(&h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info())))));
|
|
}
|
|
}
|
|
})))));
|
|
h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false),
|
|
bg(theme.darker.term, origin_w(w_full(
|
|
thunk(|to: &mut Tui|{
|
|
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
|
to.place(&w_exact(track_width(index, track),
|
|
thunk(|to: &mut Tui|{
|
|
to.place(&h_exact(1, origin_w(east(
|
|
either(true, fg(Green, "play "), "play "),
|
|
either(false, fg(Yellow, "solo "), "solo "),
|
|
))));
|
|
for (_index, port) in self.midi_outs().iter().enumerate() {
|
|
to.place(&h_exact(1, origin_w(east(
|
|
either(true, fg(Green, " ● "), " · "),
|
|
either(false, fg(Yellow, " ● "), " · "),
|
|
))));
|
|
for (_index, _conn) in port.connections.iter().enumerate() {
|
|
to.place(&h_exact(1, w_full("")));
|
|
}
|
|
}})))}}))))))
|
|
}
|
|
#[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
|
let mut h = 2u16;
|
|
for track in self.tracks().iter() {
|
|
h = h.max(track.devices.len() as u16 * 2);
|
|
}
|
|
view_track_row_section(theme,
|
|
button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false),
|
|
button_2("D", "+", false),
|
|
thunk(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
|
to.place(&wh_exact(track_width(index, track), h + 1,
|
|
bg(track.color.dark.term, origin_nw(iter_south(2, move||0..h,
|
|
|_, _index|wh_exact(track.width as u16, 2,
|
|
fg_bg(
|
|
ItemTheme::G[32].lightest.term,
|
|
ItemTheme::G[32].dark.term,
|
|
origin_nw(format!(" · {}", "--")))))))));
|
|
}))
|
|
}
|
|
/// Put a clip in a slot
|
|
#[cfg(feature = "clip")] pub fn clip_put (
|
|
&mut self, track: usize, scene: usize, clip: Option<Arc<RwLock<MidiClip>>>
|
|
) -> Option<Arc<RwLock<MidiClip>>> {
|
|
let old = self.scenes[scene].clips[track].clone();
|
|
self.scenes[scene].clips[track] = clip;
|
|
old
|
|
}
|
|
/// Change the color of a clip, returning the previous one
|
|
#[cfg(feature = "clip")] pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme)
|
|
-> Option<ItemTheme>
|
|
{
|
|
self.scenes[scene].clips[track].as_ref().map(|clip|{
|
|
let mut clip = clip.write().unwrap();
|
|
let old = clip.color.clone();
|
|
clip.color = color.clone();
|
|
panic!("{color:?} {old:?}");
|
|
//old
|
|
})
|
|
}
|
|
/// Toggle looping for the active clip
|
|
#[cfg(feature = "clip")] pub fn toggle_loop (&mut self) {
|
|
if let Some(clip) = self.selected_clip() {
|
|
clip.write().unwrap().toggle_loop()
|
|
}
|
|
}
|
|
/// Get the first sampler of the active track
|
|
#[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> {
|
|
self.selected_track()?.sampler(0)
|
|
}
|
|
/// Get the first sampler of the active track
|
|
#[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
|
|
self.selected_track_mut()?.sampler_mut(0)
|
|
}
|
|
}
|
|
impl ScenesView for Arrangement {
|
|
fn h_scenes (&self) -> u16 {
|
|
(self.size.h() as u16).saturating_sub(20)
|
|
}
|
|
fn w_side (&self) -> u16 {
|
|
(self.size.w() as u16 * 2 / 10).max(20)
|
|
}
|
|
fn w_mid (&self) -> u16 {
|
|
(self.size.width() as u16).saturating_sub(2 * self.w_side()).max(40)
|
|
}
|
|
}
|
|
impl HasClipsSize for Arrangement {
|
|
fn clips_size (&self) -> &Size { &self.size_inner }
|
|
}
|
|
|
|
pub type SceneWith<'a, T> =
|
|
(usize, &'a Scene, usize, usize, T);
|
|
|
|
def_command!(SceneCommand: |scene: Scene| {
|
|
SetSize { size: usize } => { todo!() },
|
|
SetZoom { size: usize } => { todo!() },
|
|
SetName { name: Arc<str> } =>
|
|
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
|
|
SetColor { color: ItemTheme } =>
|
|
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
|
|
});
|
|
|
|
def_command!(TrackCommand: |track: Track| {
|
|
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
|
|
SetMute { mute: Option<bool> } => todo!(),
|
|
SetSolo { solo: Option<bool> } => todo!(),
|
|
SetSize { size: usize } => todo!(),
|
|
SetZoom { zoom: usize } => todo!(),
|
|
SetName { name: Arc<str> } =>
|
|
swap_value(&mut track.name, name, |name|Self::SetName { name }),
|
|
SetColor { color: ItemTheme } =>
|
|
swap_value(&mut track.color, color, |color|Self::SetColor { color }),
|
|
SetRec { rec: Option<bool> } =>
|
|
toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
|
|
SetMon { mon: Option<bool> } =>
|
|
toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
|
|
});
|
|
|
|
def_command!(ClipCommand: |clip: MidiClip| {
|
|
SetColor { color: Option<ItemTheme> } => {
|
|
//(SetColor [t: usize, s: usize, c: ItemTheme]
|
|
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
|
|
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
|
|
todo!()
|
|
},
|
|
SetLoop { looping: Option<bool> } => {
|
|
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
|
|
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
|
|
todo!()
|
|
}
|
|
});
|