mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-21 08:19:03 +01:00
Some checks are pending
/ build (push) Waiting to run
might join app + device + engine again?
794 lines
31 KiB
Rust
794 lines
31 KiB
Rust
use crate::*;
|
|
|
|
pub trait HasSceneScroll: HasScenes {
|
|
fn scene_scroll (&self) -> usize;
|
|
}
|
|
|
|
pub trait HasTrackScroll: HasTracks {
|
|
fn track_scroll (&self) -> usize;
|
|
}
|
|
|
|
pub trait HasMidiClip {
|
|
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>>;
|
|
}
|
|
|
|
pub trait HasClipsSize {
|
|
fn clips_size (&self) -> &Measure<TuiOut>;
|
|
}
|
|
|
|
pub trait HasClock: Send + Sync {
|
|
fn clock (&self) -> &Clock;
|
|
fn clock_mut (&mut self) -> &mut Clock;
|
|
}
|
|
|
|
pub trait HasDevices {
|
|
fn devices (&self) -> &Vec<Device>;
|
|
fn devices_mut (&mut self) -> &mut Vec<Device>;
|
|
}
|
|
|
|
pub trait HasSelection: Has<Selection> {
|
|
fn selection (&self) -> &Selection { self.get() }
|
|
fn selection_mut (&mut self) -> &mut Selection { self.get_mut() }
|
|
}
|
|
|
|
pub trait HasWidth {
|
|
const MIN_WIDTH: usize;
|
|
/// Increment track width.
|
|
fn width_inc (&mut self);
|
|
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
|
|
fn width_dec (&mut self);
|
|
}
|
|
|
|
pub trait HasMidiBuffers {
|
|
fn note_buf_mut (&mut self) -> &mut Vec<u8>;
|
|
fn midi_buf_mut (&mut self) -> &mut Vec<Vec<Vec<u8>>>;
|
|
}
|
|
|
|
pub trait HasSequencer {
|
|
fn sequencer (&self) -> &Sequencer;
|
|
fn sequencer_mut (&mut self) -> &mut Sequencer;
|
|
}
|
|
|
|
pub trait HasScene: Has<Option<Scene>> + Send + Sync {
|
|
fn scene (&self) -> Option<&Scene> {
|
|
Has::<Option<Scene>>::get(self).as_ref()
|
|
}
|
|
fn scene_mut (&mut self) -> &mut Option<Scene> {
|
|
Has::<Option<Scene>>::get_mut(self)
|
|
}
|
|
}
|
|
|
|
pub trait HasScenes: Has<Vec<Scene>> + Send + Sync {
|
|
fn scenes (&self) -> &Vec<Scene> {
|
|
Has::<Vec<Scene>>::get(self)
|
|
}
|
|
fn scenes_mut (&mut self) -> &mut Vec<Scene> {
|
|
Has::<Vec<Scene>>::get_mut(self)
|
|
}
|
|
/// 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)
|
|
}
|
|
}
|
|
|
|
/// ```
|
|
/// struct TestEditorHost(Option<MidiEditor>);
|
|
/// has!(Option<MidiEditor>: |self: TestEditorHost|self.0);
|
|
/// let mut host = TestEditorHost(Some(MidiEditor::default()));
|
|
/// let _ = host.editor();
|
|
/// let _ = host.editor_mut();
|
|
/// let _ = host.is_editing();
|
|
/// let _ = host.editor_w();
|
|
/// let _ = host.editor_h();
|
|
/// ```
|
|
pub trait HasEditor: Has<Option<MidiEditor>> {
|
|
fn editor (&self) -> Option<&MidiEditor> {
|
|
self.get().as_ref()
|
|
}
|
|
fn editor_mut (&mut self) -> Option<&mut MidiEditor> {
|
|
self.get_mut().as_mut()
|
|
}
|
|
fn is_editing (&self) -> bool {
|
|
self.editor().is_some()
|
|
}
|
|
fn editor_w (&self) -> usize {
|
|
self.editor().map(|e|e.size.w()).unwrap_or(0) as usize
|
|
}
|
|
fn editor_h (&self) -> usize {
|
|
self.editor().map(|e|e.size.h()).unwrap_or(0) as usize
|
|
}
|
|
}
|
|
|
|
pub trait HasClips {
|
|
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
|
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
|
|
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
|
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
|
|
self.clips_mut().push(clip.clone());
|
|
(self.clips().len() - 1, clip)
|
|
}
|
|
}
|
|
|
|
/// Trait for thing that may receive MIDI.
|
|
pub trait HasMidiIns {
|
|
fn midi_ins (&self) -> &Vec<MidiInput>;
|
|
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput>;
|
|
/// Collect MIDI input from app ports (TODO preallocate large buffers)
|
|
fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> {
|
|
self.midi_ins().iter()
|
|
.map(|port|port.port().iter(scope)
|
|
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
|
|
.collect::<Vec<_>>())
|
|
.collect::<Vec<_>>()
|
|
}
|
|
fn midi_ins_with_sizes <'a> (&'a self) ->
|
|
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
|
{
|
|
let mut y = 0;
|
|
self.midi_ins().iter().enumerate().map(move|(i, input)|{
|
|
let height = 1 + input.connections().len();
|
|
let data = (i, input.port_name(), input.connections(), y, y + height);
|
|
y += height;
|
|
data
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Trait for thing that may output MIDI.
|
|
pub trait HasMidiOuts {
|
|
fn midi_outs (&self) -> &Vec<MidiOutput>;
|
|
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput>;
|
|
fn midi_outs_with_sizes <'a> (&'a self) ->
|
|
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
|
{
|
|
let mut y = 0;
|
|
self.midi_outs().iter().enumerate().map(move|(i, output)|{
|
|
let height = 1 + output.connections().len();
|
|
let data = (i, output.port_name(), output.connections(), y, y + height);
|
|
y += height;
|
|
data
|
|
})
|
|
}
|
|
fn midi_outs_emit (&mut self, scope: &ProcessScope) {
|
|
for port in self.midi_outs_mut().iter_mut() {
|
|
port.buffer_emit(scope)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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()))
|
|
}
|
|
}
|
|
|
|
pub trait HasPlayClip: HasClock {
|
|
|
|
fn reset (&self) -> bool;
|
|
|
|
fn reset_mut (&mut self) -> &mut bool;
|
|
|
|
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
|
|
|
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
|
|
|
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
|
|
|
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
|
|
|
fn pulses_since_start (&self) -> Option<f64> {
|
|
if let Some((started, Some(_))) = self.play_clip().as_ref() {
|
|
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
|
return Some(elapsed)
|
|
}
|
|
None
|
|
}
|
|
|
|
fn pulses_since_start_looped (&self) -> Option<(f64, f64)> {
|
|
if let Some((started, Some(clip))) = self.play_clip().as_ref() {
|
|
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
|
let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip
|
|
let times = (elapsed as usize / length) as f64;
|
|
let elapsed = (elapsed as usize % length) as f64;
|
|
return Some((times, elapsed))
|
|
}
|
|
None
|
|
}
|
|
|
|
fn enqueue_next (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
|
*self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned()));
|
|
*self.reset_mut() = true;
|
|
}
|
|
|
|
fn play_status (&self) -> impl Content<TuiOut> {
|
|
let (name, color): (Arc<str>, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() {
|
|
let MidiClip { ref name, color, .. } = *clip.read().unwrap();
|
|
(name.clone(), color)
|
|
} else {
|
|
("".into(), Tui::g(64).into())
|
|
};
|
|
let time: String = self.pulses_since_start_looped()
|
|
.map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time)))
|
|
.unwrap_or_else(||String::from(" ")).into();
|
|
FieldV(color, "Now:", format!("{} {}", time, name))
|
|
}
|
|
|
|
fn next_status (&self) -> impl Content<TuiOut> {
|
|
let mut time: Arc<str> = String::from("--.-.--").into();
|
|
let mut name: Arc<str> = String::from("").into();
|
|
let mut color = ItemTheme::G[64];
|
|
let clock = self.clock();
|
|
if let Some((t, Some(clip))) = self.next_clip() {
|
|
let clip = clip.read().unwrap();
|
|
name = clip.name.clone();
|
|
color = clip.color.clone();
|
|
time = {
|
|
let target = t.pulse.get();
|
|
let current = clock.playhead.pulse.get();
|
|
if target > current {
|
|
let remaining = target - current;
|
|
format!("-{:>}", clock.timebase.format_beats_1(remaining))
|
|
} else {
|
|
String::new()
|
|
}
|
|
}.into()
|
|
} else if let Some((t, Some(clip))) = self.play_clip() {
|
|
let clip = clip.read().unwrap();
|
|
if clip.looped {
|
|
name = clip.name.clone();
|
|
color = clip.color.clone();
|
|
let target = t.pulse.get() + clip.length as f64;
|
|
let current = clock.playhead.pulse.get();
|
|
if target > current {
|
|
time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into()
|
|
}
|
|
} else {
|
|
name = "Stop".to_string().into();
|
|
}
|
|
};
|
|
FieldV(color, "Next:", format!("{} {}", time, name))
|
|
}
|
|
}
|
|
|
|
pub trait MidiMonitor: HasMidiIns + HasMidiBuffers {
|
|
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
|
fn monitoring (&self) -> bool;
|
|
fn monitoring_mut (&mut self) -> &mut bool;
|
|
fn toggle_monitor (&mut self) {
|
|
*self.monitoring_mut() = !self.monitoring();
|
|
}
|
|
fn monitor (&mut self, scope: &ProcessScope) {
|
|
}
|
|
}
|
|
|
|
pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip {
|
|
fn recording (&self) -> bool;
|
|
fn recording_mut (&mut self) -> &mut bool;
|
|
fn toggle_record (&mut self) {
|
|
*self.recording_mut() = !self.recording();
|
|
}
|
|
|
|
fn overdub (&self) -> bool;
|
|
fn overdub_mut (&mut self) -> &mut bool;
|
|
fn toggle_overdub (&mut self) {
|
|
*self.overdub_mut() = !self.overdub();
|
|
}
|
|
|
|
fn record_clip (
|
|
&mut self,
|
|
scope: &ProcessScope,
|
|
started: Moment,
|
|
clip: &Option<Arc<RwLock<MidiClip>>>,
|
|
) {
|
|
if let Some(clip) = clip {
|
|
let sample0 = scope.last_frame_time() as usize;
|
|
let start = started.sample.get() as usize;
|
|
let _recording = self.recording();
|
|
let timebase = self.clock().timebase().clone();
|
|
let quant = self.clock().quant.get();
|
|
let mut clip = clip.write().unwrap();
|
|
let length = clip.length;
|
|
for input in self.midi_ins_mut().iter() {
|
|
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
|
|
if let LiveEvent::Midi { message, .. } = event {
|
|
clip.record_event({
|
|
let sample = (sample0 + sample - start) as f64;
|
|
let pulse = timebase.samples_to_pulse(sample);
|
|
let quantized = (pulse / quant).round() * quant;
|
|
quantized as usize % length
|
|
}, message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn record_next (&mut self) {
|
|
// TODO switch to next clip and record into it
|
|
}
|
|
}
|
|
|
|
pub trait MidiViewer: Measured<TuiOut> + MidiRange + MidiPoint + Debug + Send + Sync {
|
|
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize);
|
|
fn redraw (&self);
|
|
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>>;
|
|
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
|
|
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
|
*self.clip_mut() = clip.cloned();
|
|
self.redraw();
|
|
}
|
|
/// Make sure cursor is within note range
|
|
fn autoscroll (&self) {
|
|
let note_pos = self.get_note_pos().min(127);
|
|
let note_lo = self.get_note_lo();
|
|
let note_hi = self.get_note_hi();
|
|
if note_pos < note_lo {
|
|
self.note_lo().set(note_pos);
|
|
} else if note_pos > note_hi {
|
|
self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi));
|
|
}
|
|
}
|
|
/// Make sure time range is within display
|
|
fn autozoom (&self) {
|
|
if self.time_lock().get() {
|
|
let time_len = self.get_time_len();
|
|
let time_axis = self.get_time_axis();
|
|
let time_zoom = self.get_time_zoom();
|
|
loop {
|
|
let time_zoom = self.time_zoom().get();
|
|
let time_area = time_axis * time_zoom;
|
|
if time_area > time_len {
|
|
let next_time_zoom = note_duration_prev(time_zoom);
|
|
if next_time_zoom <= 1 {
|
|
break
|
|
}
|
|
let next_time_area = time_axis * next_time_zoom;
|
|
if next_time_area >= time_len {
|
|
self.time_zoom().set(next_time_zoom);
|
|
} else {
|
|
break
|
|
}
|
|
} else if time_area < time_len {
|
|
let prev_time_zoom = note_duration_next(time_zoom);
|
|
if prev_time_zoom > 384 {
|
|
break
|
|
}
|
|
let prev_time_area = time_axis * prev_time_zoom;
|
|
if prev_time_area <= time_len {
|
|
self.time_zoom().set(prev_time_zoom);
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if time_zoom != self.time_zoom().get() {
|
|
self.redraw()
|
|
}
|
|
}
|
|
//while time_len.div_ceil(time_zoom) > time_axis {
|
|
//println!("\r{time_len} {time_zoom} {time_axis}");
|
|
//time_zoom = Note::next(time_zoom);
|
|
//}
|
|
//self.time_zoom().set(time_zoom);
|
|
}
|
|
}
|
|
|
|
pub trait AddScene: HasScenes + HasTracks {
|
|
/// Add multiple scenes
|
|
fn scenes_add (&mut self, n: usize) -> Usually<()> {
|
|
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)>
|
|
{
|
|
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 ClipsView: TracksView + ScenesView {
|
|
|
|
fn view_scenes_clips <'a> (&'a self)
|
|
-> impl Content<TuiOut> + 'a
|
|
{
|
|
self.clips_size().of(Fill::XY(Bsp::a(
|
|
Fill::XY(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))),
|
|
Thunk::new(|to: &mut TuiOut|for (
|
|
track_index, track, _, _
|
|
) in self.tracks_with_sizes() {
|
|
to.place(&Fixed::X(track.width as u16,
|
|
Fill::Y(self.view_track_clips(track_index, track))))
|
|
}))))
|
|
}
|
|
|
|
fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Content<TuiOut> + 'a {
|
|
Thunk::new(move|to: &mut TuiOut|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(&Fixed::XY(w, y, Bsp::b(
|
|
Fill::XY(Outer(true, Style::default().fg(outline))),
|
|
Fill::XY(Bsp::b(
|
|
Bsp::b(
|
|
Tui::fg_bg(outline, bg, Fill::XY("")),
|
|
Fill::XY(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))),
|
|
),
|
|
Fill::XY(When::new(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<TuiOut> {
|
|
|
|
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 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 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 Content<TuiOut> {
|
|
Fixed::X(20, Thunk::new(|to: &mut TuiOut|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 Content<TuiOut> + '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 = Fill::X(Align::w(Bsp::e(format!("·s{index:02} "),
|
|
Tui::fg(Tui::g(255), Tui::bold(true, &scene.name)))));
|
|
let b = When::new(self.selection().scene() == Some(index) && self.is_editing(),
|
|
Fill::XY(Align::nw(Bsp::s(
|
|
self.editor().as_ref().map(|e|e.clip_status()),
|
|
self.editor().as_ref().map(|e|e.edit_status())))));
|
|
Fixed::XY(20, h, Tui::bg(bg, Align::nw(Bsp::s(a, b))))
|
|
}
|
|
|
|
}
|
|
|
|
/// May create new MIDI input ports.
|
|
pub trait AddMidiIn {
|
|
fn midi_in_add (&mut self) -> Usually<()>;
|
|
}
|
|
|
|
/// May create new MIDI output ports.
|
|
pub trait AddMidiOut {
|
|
fn midi_out_add (&mut self) -> Usually<()>;
|
|
}
|
|
|
|
pub trait RegisterPorts: HasJack<'static> {
|
|
/// Register a MIDI input port.
|
|
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput>;
|
|
/// Register a MIDI output port.
|
|
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput>;
|
|
/// Register an audio input port.
|
|
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput>;
|
|
/// Register an audio output port.
|
|
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput>;
|
|
}
|
|
|
|
pub trait JackPort: HasJack<'static> {
|
|
type Port: PortSpec + Default;
|
|
type Pair: PortSpec + Default;
|
|
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
|
-> Usually<Self> where Self: Sized;
|
|
fn register (jack: &Jack<'static>, name: &impl AsRef<str>) -> Usually<Port<Self::Port>> {
|
|
jack.with_client(|c|c.register_port::<Self::Port>(name.as_ref(), Default::default()))
|
|
.map_err(|e|e.into())
|
|
}
|
|
fn port_name (&self) -> &Arc<str>;
|
|
fn connections (&self) -> &[Connect];
|
|
fn port (&self) -> &Port<Self::Port>;
|
|
fn port_mut (&mut self) -> &mut Port<Self::Port>;
|
|
fn into_port (self) -> Port<Self::Port> where Self: Sized;
|
|
fn close (self) -> Usually<()> where Self: Sized {
|
|
let jack = self.jack().clone();
|
|
Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?)
|
|
}
|
|
fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec<String> {
|
|
self.with_client(|c|c.ports(re_name, re_type, flags))
|
|
}
|
|
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
|
self.with_client(|c|c.port_by_id(id))
|
|
}
|
|
fn port_by_name (&self, name: impl AsRef<str>) -> Option<Port<Unowned>> {
|
|
self.with_client(|c|c.port_by_name(name.as_ref()))
|
|
}
|
|
fn connect_to_matching <'k> (&'k self) -> Usually<()> {
|
|
for connect in self.connections().iter() {
|
|
//panic!("{connect:?}");
|
|
let status = match &connect.name {
|
|
Exact(name) => self.connect_exact(name),
|
|
RegExp(re) => self.connect_regexp(re, connect.scope),
|
|
}?;
|
|
*connect.status.write().unwrap() = status;
|
|
}
|
|
Ok(())
|
|
}
|
|
fn connect_exact <'k> (&'k self, name: &str) ->
|
|
Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>
|
|
{
|
|
self.with_client(move|c|{
|
|
let mut status = vec![];
|
|
for port in c.ports(None, None, PortFlags::empty()).iter() {
|
|
if port.as_str() == &*name {
|
|
if let Some(port) = c.port_by_name(port.as_str()) {
|
|
let port_status = self.connect_to_unowned(&port)?;
|
|
let name = port.name()?.into();
|
|
status.push((port, name, port_status));
|
|
if port_status == Connected {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(status)
|
|
})
|
|
}
|
|
fn connect_regexp <'k> (
|
|
&'k self, re: &str, scope: ConnectScope
|
|
) -> Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>> {
|
|
self.with_client(move|c|{
|
|
let mut status = vec![];
|
|
let ports = c.ports(Some(&re), None, PortFlags::empty());
|
|
for port in ports.iter() {
|
|
if let Some(port) = c.port_by_name(port.as_str()) {
|
|
let port_status = self.connect_to_unowned(&port)?;
|
|
let name = port.name()?.into();
|
|
status.push((port, name, port_status));
|
|
if port_status == Connected && scope == One {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
Ok(status)
|
|
})
|
|
}
|
|
/** Connect to a matching port by name. */
|
|
fn connect_to_name (&self, name: impl AsRef<str>) -> Usually<ConnectStatus> {
|
|
self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) {
|
|
self.connect_to_unowned(port)
|
|
} else {
|
|
Ok(Missing)
|
|
})
|
|
}
|
|
/** Connect to a matching port by reference. */
|
|
fn connect_to_unowned (&self, port: &Port<Unowned>) -> Usually<ConnectStatus> {
|
|
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
|
Connected
|
|
} else if let Ok(_) = c.connect_ports(port, self.port()) {
|
|
Connected
|
|
} else {
|
|
Mismatch
|
|
}))
|
|
}
|
|
/** Connect to an owned matching port by reference. */
|
|
fn connect_to_owned (&self, port: &Port<Self::Pair>) -> Usually<ConnectStatus> {
|
|
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
|
Connected
|
|
} else if let Ok(_) = c.connect_ports(port, self.port()) {
|
|
Connected
|
|
} else {
|
|
Mismatch
|
|
}))
|
|
}
|
|
}
|