wip: nromalize

This commit is contained in:
okay stopped screaming 2026-03-21 22:54:29 +02:00
parent 7ff1d989a9
commit 513b8354a3
14 changed files with 2324 additions and 2337 deletions

416
app/arrange.rs Normal file
View file

@ -0,0 +1,416 @@
/// 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()))
}
}