mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
347 lines
15 KiB
Rust
347 lines
15 KiB
Rust
use crate::*;
|
|
|
|
def_sizes_iter!(TracksSizes => Track);
|
|
|
|
impl<T: Has<Vec<Track>> + Send + Sync> HasTracks for T {}
|
|
|
|
pub trait HasTracks: Has<Vec<Track>> + Send + Sync {
|
|
fn tracks (&self) -> &Vec<Track> { Has::<Vec<Track>>::get(self) }
|
|
fn tracks_mut (&mut self) -> &mut Vec<Track> { Has::<Vec<Track>>::get_mut(self) }
|
|
/// 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 HasTrackScroll: HasTracks { fn track_scroll (&self) -> usize; }
|
|
impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } }
|
|
impl<T: MaybeHas<Track>> HasTrack for T {
|
|
fn track (&self) -> Option<&Track> { self.get() }
|
|
fn track_mut (&mut self) -> Option<&mut Track> { self.get_mut() }
|
|
}
|
|
impl Track {
|
|
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!() }
|
|
}
|
|
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 }),
|
|
});
|
|
|
|
#[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>,
|
|
}
|
|
|
|
has!(Clock: |self: Track|self.sequencer.clock);
|
|
has!(Sequencer: |self: Track|self.sequencer);
|
|
|
|
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()
|
|
})
|
|
}
|
|
}
|
|
#[cfg(feature = "sampler")]
|
|
impl Track {
|
|
/// Create a new track connecting the [Sequencer] to a [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)
|
|
}
|
|
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
|
|
}
|
|
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 trait HasTrack {
|
|
fn track (&self) -> Option<&Track>;
|
|
fn track_mut (&mut self) -> Option<&mut Track>;
|
|
#[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> + '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 Content<TuiOut> + '_ {
|
|
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 Content<TuiOut> {
|
|
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 Content<TuiOut> {
|
|
self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
|
}
|
|
}
|
|
|
|
impl<T: TracksView + ScenesView + Send + Sync> ClipsView for T {}
|
|
|
|
pub trait TracksView:
|
|
ScenesView +
|
|
HasMidiIns +
|
|
HasMidiOuts +
|
|
HasSize<TuiOut> +
|
|
HasTrackScroll +
|
|
HasSelection +
|
|
HasEditor +
|
|
HasClipsSize
|
|
{
|
|
fn tracks_width_available (&self) -> u16 {
|
|
(self.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.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() {
|
|
let data = (index, track, x, x + width);
|
|
x += width + Self::TRACK_SPACING;
|
|
Some(data)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
fn view_track_names (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
|
let track_count = self.tracks().len();
|
|
let scene_count = self.scenes().len();
|
|
let selected = self.selection();
|
|
let button = Bsp::s(
|
|
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 = Bsp::s(
|
|
button_2("T", "+", false),
|
|
button_2("S", "+", false));
|
|
view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb,
|
|
Fixed::Y(2, Thunk::new(|to: &mut TuiOut|{
|
|
for (index, track, x1, _x2) in self.tracks_with_sizes() {
|
|
to.place(&Push::X(x1 as u16, Fixed::X(track_width(index, track),
|
|
Tui::bg(if selected.track() == Some(index) {
|
|
track.color.light.rgb
|
|
} else {
|
|
track.color.base.rgb
|
|
}, Bsp::s(Fill::X(Align::nw(Bsp::e(
|
|
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 Content<TuiOut> {
|
|
view_track_row_section(theme,
|
|
Bsp::s(Fill::X(Align::w(button_2("o", "utput", false))),
|
|
Thunk::new(|to: &mut TuiOut|for port in self.midi_outs().iter() {
|
|
to.place(&Fill::X(Align::w(port.port_name())));
|
|
})),
|
|
button_2("O", "+", false),
|
|
Tui::bg(theme.darker.rgb, Align::w(Thunk::new(|to: &mut TuiOut|{
|
|
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
|
to.place(&Fixed::X(track_width(index, track),
|
|
Align::nw(Fill::Y(Map::south(1, ||track.sequencer.midi_outs.iter(),
|
|
|port, index|Tui::fg(Rgb(255, 255, 255),
|
|
Fixed::Y(1, Tui::bg(track.color.dark.rgb, Fill::X(Align::w(
|
|
format!("·o{index:02} {}", port.port_name())))))))))));}}))))
|
|
}
|
|
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
|
|
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 TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
|
to.place(&Fixed::XY(track_width(index, track), h + 1,
|
|
Align::nw(Bsp::s(
|
|
Tui::bg(track.color.base.rgb,
|
|
Fill::X(Align::w(row!(
|
|
Either::new(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "),
|
|
Either::new(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "),
|
|
Either::new(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "),
|
|
)))),
|
|
Map::south(1, ||track.sequencer.midi_ins.iter(),
|
|
|port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb,
|
|
Fill::X(Align::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, Align::w(content)))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
|
|
track.width as u16
|
|
}
|
|
|
|
fn view_track_header (theme: ItemTheme, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
|
Fixed::X(12, Tui::bg(theme.darker.rgb, Fill::X(Align::e(content))))
|
|
}
|
|
|
|
fn view_ports_status <T: JackPort> (theme: ItemTheme, title: &str, ports: &[T]) {
|
|
let ins = ports.len() as u16;
|
|
let frame = Outer(true, Style::default().fg(Tui::g(96)));
|
|
let iter = move||ports.iter();
|
|
let names = Map::south(1, iter, |port, index|Fill::Y(Align::w(format!(" {index} {}", port.port_name()))));
|
|
let field = FieldV(theme, title, names);
|
|
Fixed::XY(20, 1 + ins, frame.enclose(Fixed::XY(20, 1 + ins, field.content())))
|
|
}
|
|
|
|
impl Track {
|
|
pub fn per <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
|
) -> impl Content<TuiOut> + 'a {
|
|
Map::new(tracks,
|
|
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
|
let width = (x2 - x1) as u16;
|
|
map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg(
|
|
track.color.lightest.rgb,
|
|
track.color.base.rgb,
|
|
callback(index, track))))})
|
|
}
|
|
}
|
|
|
|
pub(crate) fn per_track_top <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
|
) -> impl Content<TuiOut> + 'a {
|
|
Align::x(Tui::bg(Reset, Map::new(tracks,
|
|
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
|
let width = (x2 - x1) as u16;
|
|
map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg(
|
|
track.color.lightest.rgb,
|
|
track.color.base.rgb,
|
|
callback(index, track))))})))
|
|
}
|
|
|
|
pub(crate) fn per_track <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
|
) -> impl Content<TuiOut> + 'a {
|
|
per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track))))
|
|
}
|
|
|
|
pub(crate) fn io_ports <'a, T: PortsSizes<'a>> (
|
|
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
|
|
) -> impl Content<TuiOut> + 'a {
|
|
Map::new(iter, move|(
|
|
_index, name, connections, y, y2
|
|
): (usize, &'a Arc<str>, &'a [Connect], usize, usize), _|
|
|
map_south(y as u16, (y2-y) as u16, Bsp::s(
|
|
Fill::Y(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(&" ", name))))),
|
|
Map::new(||connections.iter(), move|connect: &'a Connect, index|map_south(index as u16, 1,
|
|
Fill::Y(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg,
|
|
&connect.info)))))))))
|
|
}
|
|
|
|
//pub(crate) fn io_conns <'a, T: PortsSizes<'a>> (
|
|
//fg: Color, bg: Color, iter: &mut impl Iterator<Item = (usize, &'a Arc<str>, &'a [Connect], usize, usize)>
|
|
//) -> impl Content<TuiOut> + 'a {
|
|
//Fill::XY(Thunk::new(move|to: &mut TuiOut|for (_, _, connections, y, y2) in &mut *iter {
|
|
//to.place(&map_south(y as u16, (y2-y) as u16, Bsp::s(
|
|
//Fill::Y(Tui::bold(true, wrap(bg, fg, Fill::Y(Align::w(&"▞▞▞▞ ▞▞▞▞"))))),
|
|
//Thunk::new(|to: &mut TuiOut|for (index, _connection) in connections.iter().enumerate() {
|
|
//to.place(&map_south(index as u16, 1, Fill::Y(Align::w(Tui::bold(false,
|
|
//wrap(bg, fg, Fill::Y(&"")))))))
|
|
//})
|
|
//)))
|
|
//}))
|
|
//}
|
|
//track_scroll: Fill::Y(Fixed::Y(1, ScrollbarH {
|
|
//offset: arrangement.track_scroll,
|
|
//length: h_tracks_area as usize,
|
|
//total: h_scenes as usize,
|
|
//})),
|
|
//take!(TrackCommand |state: Arrangement, iter|state.selected_track().as_ref()
|
|
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|