mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-04-03 12:50:44 +02:00
416 lines
18 KiB
Rust
416 lines
18 KiB
Rust
|
||
/// Represents the current user selection in the arranger
|
||
#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection {
|
||
#[default]
|
||
/// Nothing is selected
|
||
Nothing,
|
||
/// The whole mix is selected
|
||
Mix,
|
||
/// A MIDI input is selected.
|
||
Input(usize),
|
||
/// A MIDI output is selected.
|
||
Output(usize),
|
||
/// A scene is selected.
|
||
#[cfg(feature = "scene")] Scene(usize),
|
||
/// A track is selected.
|
||
#[cfg(feature = "track")] Track(usize),
|
||
/// A clip (track × scene) is selected.
|
||
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
|
||
/// A track's MIDI input connection is selected.
|
||
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
|
||
/// A track's MIDI output connection is selected.
|
||
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
|
||
/// A track device slot is selected.
|
||
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
|
||
}
|
||
|
||
/// 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>>>>,
|
||
}
|
||
|
||
/// 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: Measure<Tui>,
|
||
/// Display size of clips area
|
||
pub size_inner: Measure<Tui>,
|
||
/// 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,
|
||
}
|
||
|
||
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(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))),
|
||
Thunk::new(|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))))
|
||
}))))
|
||
}
|
||
|
||
fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw<Tui> + 'a {
|
||
Thunk::new(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.rgb;
|
||
let mut outline = theme.base.rgb;
|
||
let bg = if self.selection().track() == Some(track_index)
|
||
&& self.selection().scene() == Some(scene_index)
|
||
{
|
||
outline = theme.lighter.rgb;
|
||
theme.light.rgb
|
||
} else if self.selection().track() == Some(track_index)
|
||
|| self.selection().scene() == Some(scene_index)
|
||
{
|
||
outline = theme.darkest.rgb;
|
||
theme.base.rgb
|
||
} else {
|
||
theme.dark.rgb
|
||
};
|
||
let w = if self.selection().track() == Some(track_index)
|
||
&& let Some(editor) = self.editor ()
|
||
{
|
||
(editor.measure_width() 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.measure_height() as usize).max(12)
|
||
} else {
|
||
Self::H_SCENE as usize
|
||
} as u16;
|
||
|
||
to.place(&wh_exact(w, y, below(
|
||
wh_full(Outer(true, Style::default().fg(outline))),
|
||
wh_full(below(
|
||
below(
|
||
Tui::fg_bg(outline, bg, wh_full("")),
|
||
wh_full(origin_nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))),
|
||
),
|
||
wh_full(when(self.selection().track() == Some(track_index)
|
||
&& self.selection().scene() == Some(scene_index)
|
||
&& self.is_editing(), self.editor())))))));
|
||
})
|
||
}
|
||
|
||
}
|
||
|
||
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured<Tui> {
|
||
|
||
fn tracks_width_available (&self) -> u16 {
|
||
(self.measure_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.measure_width());
|
||
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, Tui::bg(theme.darker.rgb,
|
||
h_exact(2, Thunk::new(|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),
|
||
Tui::bg(if selected.track() == Some(index) {
|
||
track.color.light.rgb
|
||
} else {
|
||
track.color.base.rgb
|
||
}, south(w_full(origin_nw(east(
|
||
format!("·t{index:02} "),
|
||
Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name))
|
||
))), ""))) ));}}))))
|
||
}
|
||
|
||
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::new(|to: &mut Tui|for port in self.midi_outs().iter() {
|
||
to.place(&w_full(origin_w(port.port_name())));
|
||
})),
|
||
button_2("O", "+", false),
|
||
Tui::bg(theme.darker.rgb, origin_w(Thunk::new(|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(1, ||track.sequencer.midi_outs.iter(),
|
||
|port, index|Tui::fg(Rgb(255, 255, 255),
|
||
h_exact(1, Tui::bg(track.color.dark.rgb, w_full(origin_w(
|
||
format!("·o{index:02} {}", port.port_name())))))))))));}}))))
|
||
}
|
||
|
||
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::new(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(
|
||
Tui::bg(track.color.base.rgb,
|
||
w_full(origin_w(east!(
|
||
either(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "),
|
||
either(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "),
|
||
either(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "),
|
||
)))),
|
||
iter_south(1, ||track.sequencer.midi_ins.iter(),
|
||
|port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb,
|
||
w_full(origin_w(format!("·i{index:02} {}", port.port_name())))))))));
|
||
});
|
||
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
|
||
Tui::bg(theme.darker.rgb, 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::new(|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.rgb
|
||
} else {
|
||
scene.color.base.rgb
|
||
};
|
||
let a = w_full(origin_w(east(format!("·s{index:02} "),
|
||
Tui::fg(Tui::g(255), Tui::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, Tui::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 HasSelection: AsRef<Selection> + AsMut<Selection> {
|
||
fn selection (&self) -> &Selection { self.as_ref() }
|
||
fn selection_mut (&mut self) -> &mut Selection { self.as_mut() }
|
||
/// Get the active track
|
||
#[cfg(feature = "track")]
|
||
fn selected_track (&self) -> Option<&Track> where Self: HasTracks {
|
||
let index = self.selection().track()?;
|
||
self.tracks().get(index)
|
||
}
|
||
/// Get a mutable reference to the active track
|
||
#[cfg(feature = "track")]
|
||
fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks {
|
||
let index = self.selection().track()?;
|
||
self.tracks_mut().get_mut(index)
|
||
}
|
||
/// Get the active scene
|
||
#[cfg(feature = "scene")]
|
||
fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes {
|
||
let index = self.selection().scene()?;
|
||
self.scenes().get(index)
|
||
}
|
||
/// Get a mutable reference to the active scene
|
||
#[cfg(feature = "scene")]
|
||
fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes {
|
||
let index = self.selection().scene()?;
|
||
self.scenes_mut().get_mut(index)
|
||
}
|
||
/// Get the active clip
|
||
#[cfg(feature = "clip")]
|
||
fn selected_clip (&self) -> Option<Arc<RwLock<MidiClip>>> where Self: HasScenes + HasTracks {
|
||
self.selected_scene()?.clips.get(self.selection().track()?)?.clone()
|
||
}
|
||
}
|
||
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()))
|
||
}
|
||
}
|