mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
wip: refactor arranger to device
This commit is contained in:
parent
fa73821a0b
commit
89288f2920
40 changed files with 2015 additions and 1919 deletions
207
crates/device/src/arranger/arranger_api.rs
Normal file
207
crates/device/src/arranger/arranger_api.rs
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
use crate::*;
|
||||
|
||||
#[tengri_proc::command(Arrangement)]
|
||||
impl ArrangementCommand {
|
||||
fn track (arranger: &mut Arrangement, track: TrackCommand) -> Perhaps<Self> {
|
||||
todo!("delegate")
|
||||
}
|
||||
fn track_add (arranger: &mut Arrangement) -> Perhaps<Self> {
|
||||
let index = arranger.track_add(None, None, &[], &[])?.0;
|
||||
arranger.selected = match arranger.selected {
|
||||
Some(Selection::Track(_)) =>
|
||||
Selection::Track(index),
|
||||
Some(Selection::TrackClip { track, scene }) =>
|
||||
Selection::TrackClip { track: index, scene },
|
||||
_ => arranger.selected
|
||||
};
|
||||
Ok(Some(Self::TrackDelete { index }))
|
||||
}
|
||||
fn track_swap (arranger: &mut Arrangement, index: usize, other: usize) -> Perhaps<Self> {
|
||||
todo!();
|
||||
Ok(Some(Self::TrackSwap { index, other }))
|
||||
}
|
||||
fn track_delete (arranger: &mut Arrangement, index: usize) -> Perhaps<Self> {
|
||||
let exists = arranger.tracks().get(index).is_some();
|
||||
if exists {
|
||||
let track = arranger.tracks_mut().remove(index);
|
||||
let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track;
|
||||
for port in midi_ins.into_iter() {
|
||||
port.close()?;
|
||||
}
|
||||
for port in midi_outs.into_iter() {
|
||||
port.close()?;
|
||||
}
|
||||
for scene in arranger.scenes_mut().iter_mut() {
|
||||
scene.clips.remove(index);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
//TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) })
|
||||
}
|
||||
fn midi_in (arranger: &mut Arrangement, input: InputCommand) -> Perhaps<Self> {
|
||||
todo!("delegate")
|
||||
}
|
||||
fn midi_in_add (arranger: &mut Arrangement) -> Perhaps<Self> {
|
||||
arranger.midi_in_add()?;
|
||||
Ok(None)
|
||||
}
|
||||
fn midi_out (arranger: &mut Arrangement, input: OutputCommand) -> Perhaps<Self> {
|
||||
todo!("delegate")
|
||||
}
|
||||
fn midi_out_add (arranger: &mut Arrangement) -> Perhaps<Self> {
|
||||
arranger.midi_out_add()?;
|
||||
Ok(None)
|
||||
}
|
||||
fn device (arranger: &mut Arrangement, input: DeviceCommand) -> Perhaps<Self> {
|
||||
todo!("delegate")
|
||||
}
|
||||
fn device_add (arranger: &mut Arrangement, i: usize) -> Perhaps<Self> {
|
||||
arranger.device_add(i);
|
||||
Ok(None)
|
||||
}
|
||||
fn scene (arranger: &mut Arrangement, scene: SceneCommand) -> Perhaps<Self> {
|
||||
todo!("delegate")
|
||||
}
|
||||
fn scene_add (arranger: &mut Arrangement) -> Perhaps<Self> {
|
||||
let index = arranger.scene_add(None, None)?.0;
|
||||
arranger.selected = match arranger.selected {
|
||||
Selection::Scene(_) =>
|
||||
Selection::Scene(index),
|
||||
Selection::TrackClip { track, scene } =>
|
||||
Selection::TrackClip { track, scene: index },
|
||||
_ => arranger.selected
|
||||
};
|
||||
Ok(None) // TODO
|
||||
}
|
||||
fn scene_swap (arranger: &mut Arrangement, index: usize, other: usize) -> Perhaps<Self> {
|
||||
todo!();
|
||||
Ok(Some(Self::SceneSwap { index, other }))
|
||||
}
|
||||
fn scene_delete (arranger: &mut Arrangement, index: usize) -> Perhaps<Self> {
|
||||
let scenes = arranger.scenes_mut();
|
||||
Ok(if scenes.get(index).is_some() {
|
||||
Some(scenes.remove(index))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
fn scene_launch (arranger: &mut Arrangement, index: usize) -> Perhaps<Self> {
|
||||
for track in 0..arranger.tracks.len() {
|
||||
let clip = arranger.scenes[index].clips[track].as_ref();
|
||||
arranger.tracks[track].sequencer.enqueue_next(clip);
|
||||
}
|
||||
}
|
||||
fn clip (arranger: &mut Arrangement, scene: ClipCommand) -> Perhaps<Self> {
|
||||
todo!("delegate")
|
||||
}
|
||||
fn clip_get (arranger: &mut Arrangement, a: usize, b: usize) -> Perhaps<Self> {
|
||||
//(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}"))
|
||||
//("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap())))
|
||||
todo!()
|
||||
}
|
||||
fn clip_put (arranger: &mut Arrangement, a: usize, b: usize) -> Perhaps<Self> {
|
||||
//(Put [t: usize, s: usize, c: MaybeClip]
|
||||
//Some(Self::Put(t, s, arranger.clip_put(t, s, c))))
|
||||
//("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap())))
|
||||
todo!()
|
||||
}
|
||||
fn clip_del (arranger: &mut Arrangement, a: usize, b: usize) -> Perhaps<Self> {
|
||||
//("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None))))
|
||||
todo!()
|
||||
}
|
||||
fn clip_enqueue (arranger: &mut Arrangement, a: usize, b: usize) -> Perhaps<Self> {
|
||||
//(Enqueue [t: usize, s: usize]
|
||||
//cmd!(arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref())))
|
||||
//("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap())))
|
||||
todo!()
|
||||
}
|
||||
fn clip_edit (arranger: &mut Arrangement, a: usize, b: usize) -> Perhaps<Self> {
|
||||
//(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}"))
|
||||
//("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap())))
|
||||
todo!()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[tengri_proc::command(MidiClip)]
|
||||
impl ClipCommand {
|
||||
fn set_color (clip: &mut MidiClip, color: Option<ItemTheme>) -> Perhaps<Self> {
|
||||
//(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!()
|
||||
}
|
||||
fn set_loop (clip: &mut MidiClip, looping: Option<bool>) -> Perhaps<Self> {
|
||||
//(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!()
|
||||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::command(Device)]
|
||||
impl DeviceCommand {
|
||||
}
|
||||
|
||||
#[tengri_proc::command(JackMidiIn)]
|
||||
impl InputCommand {
|
||||
}
|
||||
|
||||
#[tengri_proc::command(JackMidiOut)]
|
||||
impl OutputCommand {
|
||||
}
|
||||
|
||||
#[tengri_proc::command(Scene)]
|
||||
impl SceneCommand {
|
||||
fn set_name (scene: &mut Scene, mut name: Arc<str>) -> Perhaps<Self> {
|
||||
std::mem::swap(&mut scene.name, &mut name);
|
||||
Ok(Some(Self::SetName { name }))
|
||||
}
|
||||
fn set_color (scene: &mut Scene, mut color: ItemTheme) -> Perhaps<Self> {
|
||||
std::mem::swap(&mut scene.color, &mut color);
|
||||
Ok(Some(Self::SetColor { color }))
|
||||
}
|
||||
fn set_size (scene: &mut Scene, size: usize) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
fn set_zoom (scene: &mut Scene, zoom: usize) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[tengri_proc::command(Track)]
|
||||
impl TrackCommand {
|
||||
fn set_name (track: &mut Track, mut name: Arc<str>) -> Perhaps<Self> {
|
||||
std::mem::swap(&mut name, &mut track.name);
|
||||
Ok(Some(Self::SetName { name }))
|
||||
}
|
||||
fn set_color (track: &mut Track, color: ItemTheme) -> Perhaps<Self> {
|
||||
std::mem::swap(color, track.color);
|
||||
Ok(Some(Self::SetColor { color }))
|
||||
}
|
||||
fn set_mute (track: &mut Track, value: Option<bool>) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
fn set_solo (track: &mut Track, value: Option<bool>) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
fn set_rec (track: &mut Track, value: Option<bool>) -> Perhaps<Self> {
|
||||
let current = track.sequencer.recording;
|
||||
let value = value.unwrap_or(!current);
|
||||
Ok((value != current).then_some(Self::SetRec { value: Some(current) }))
|
||||
}
|
||||
fn set_mon (track: &mut Track, value: Option<bool>) -> Perhaps<Self> {
|
||||
let current = track.sequencer.monitoring;
|
||||
let value = value.unwrap_or(!current);
|
||||
Ok((value != current).then_some(Self::SetRec { value: Some(current) }))
|
||||
}
|
||||
fn set_size (track: &mut Track, size: usize) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
fn set_zoom (track: &mut Track, zoom: usize) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
fn stop (track: &mut Track) -> Perhaps<Self> {
|
||||
track.sequencer.enqueue_next(None);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
7
crates/device/src/arranger/arranger_audio.rs
Normal file
7
crates/device/src/arranger/arranger_audio.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait RenderTracks {
|
||||
}
|
||||
|
||||
impl<T: HasTracks> Audio for T {
|
||||
}
|
||||
35
crates/device/src/arranger/arranger_clip.rs
Normal file
35
crates/device/src/arranger/arranger_clip.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use crate::*;
|
||||
|
||||
|
||||
impl Arrangement {
|
||||
/// Put a clip in a slot
|
||||
pub(crate) 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
|
||||
pub(crate) 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
|
||||
})
|
||||
}
|
||||
/// Get the active clip
|
||||
pub(crate) fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||
self.scene()?.clips.get(self.selected().track()?)?.clone()
|
||||
}
|
||||
/// Toggle looping for the active clip
|
||||
pub(crate) fn toggle_loop (&mut self) {
|
||||
if let Some(clip) = self.clip() {
|
||||
clip.write().unwrap().toggle_loop()
|
||||
}
|
||||
}
|
||||
}
|
||||
27
crates/device/src/arranger/arranger_device.rs
Normal file
27
crates/device/src/arranger/arranger_device.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use crate::*;
|
||||
|
||||
impl Arrangement {
|
||||
fn device_add_lv2 (&mut self) -> Usually<()> {
|
||||
todo!();
|
||||
Ok(())
|
||||
}
|
||||
fn device_add_sampler (&mut self) -> Usually<()> {
|
||||
let name = self.jack.with_client(|c|c.name().to_string());
|
||||
let midi = self.track().expect("no active track").sequencer.midi_outs[0].name();
|
||||
let sampler = if let Ok(sampler) = Sampler::new(
|
||||
&self.jack,
|
||||
&format!("{}/Sampler", &self.track().expect("no active track").name),
|
||||
&[PortConnect::exact(format!("{name}:{midi}"))],
|
||||
&[&[], &[]],
|
||||
&[&[], &[]]
|
||||
) {
|
||||
self.dialog = None;
|
||||
Device::Sampler(sampler)
|
||||
} else {
|
||||
self.dialog = Some(Dialog::Message(Message::FailedToAddDevice));
|
||||
return Err("failed to add device".into())
|
||||
};
|
||||
self.track_mut().expect("no active track").devices.push(sampler);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
62
crates/device/src/arranger/arranger_edit.rs
Normal file
62
crates/device/src/arranger/arranger_edit.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use crate::*;
|
||||
|
||||
#[cfg(all(feature = "pool", feature = "editor"))]
|
||||
pub trait AutoCreate:
|
||||
Has<Vec<Scene>> +
|
||||
Has<Vec<Track>> +
|
||||
Has<Option<MidiEditor>> +
|
||||
Has<Option<Selection>> +
|
||||
Has<Option<Pool>> +
|
||||
Send + Sync
|
||||
{
|
||||
// Create new clip in pool when entering empty cell
|
||||
fn clip_auto_create (&mut self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||
if let Some(pool) = Has::<Pool>::get(self)
|
||||
&& let Some(Selection::TrackClip { track, scene }) = self.get()
|
||||
&& let Some(scene) = Has::<Vec<Scene>>::get_mut(self).get_mut(scene)
|
||||
&& let Some(slot) = scene.clips.get_mut(track)
|
||||
&& slot.is_none()
|
||||
&& let Some(track) = Has::<Vec<Track>>::get_mut(self).get_mut(track)
|
||||
{
|
||||
let (index, mut clip) = pool.add_new_clip();
|
||||
// autocolor: new clip colors from scene and track color
|
||||
let color = track.color.base.mix(scene.color.base, 0.5);
|
||||
clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into();
|
||||
if let Some(ref mut editor) = Has::<Option<MidiEditor>>::get_mut(self) {
|
||||
editor.set_clip(Some(&clip));
|
||||
}
|
||||
*slot = Some(clip.clone());
|
||||
Some(clip)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "pool", feature = "editor"))]
|
||||
pub trait AutoRemove:
|
||||
Has<Vec<Scene>> +
|
||||
Has<Vec<Track>> +
|
||||
Has<Option<MidiEditor>> +
|
||||
Has<Option<Selection>> +
|
||||
Has<Option<Pool>> +
|
||||
Send + Sync
|
||||
{
|
||||
// Remove clip from arrangement when exiting empty clip editor
|
||||
fn clip_auto_remove (&mut self) {
|
||||
if let Some(ref mut pool) = Has::<Pool>::get(self)
|
||||
&& let Some(Selection::TrackClip { track, scene }) = self.get()
|
||||
&& let Some(scene) = Has::<Vec<Scene>>::get_mut(self).get_mut(scene)
|
||||
&& let Some(slot) = scene.clips.get_mut(track)
|
||||
&& let Some(clip) = slot.as_mut()
|
||||
{
|
||||
let mut swapped = None;
|
||||
if clip.read().unwrap().count_midi_messages() == 0 {
|
||||
std::mem::swap(&mut swapped, slot);
|
||||
}
|
||||
if let Some(clip) = swapped {
|
||||
pool.delete_clip(&clip.read().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
crates/device/src/arranger/arranger_model.rs
Normal file
74
crates/device/src/arranger/arranger_model.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Arrangement {
|
||||
/// Project name.
|
||||
pub name: Arc<str>,
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// List of global midi inputs
|
||||
pub midi_ins: Vec<JackMidiIn>,
|
||||
/// List of global midi outputs
|
||||
pub midi_outs: Vec<JackMidiOut>,
|
||||
/// List of global audio inputs
|
||||
pub audio_ins: Vec<JackAudioIn>,
|
||||
/// List of global audio outputs
|
||||
pub audio_outs: Vec<JackAudioOut>,
|
||||
/// Buffer for writing a midi event
|
||||
pub note_buf: Vec<u8>,
|
||||
/// Buffer for writing a chunk of midi events
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
/// Last track number (to avoid duplicate port names)
|
||||
pub track_last: usize,
|
||||
/// List of tracks
|
||||
pub tracks: Vec<Track>,
|
||||
/// Scroll offset of tracks
|
||||
pub track_scroll: usize,
|
||||
/// List of scenes
|
||||
pub scenes: Vec<Scene>,
|
||||
/// Scroll offset of scenes
|
||||
pub scene_scroll: usize,
|
||||
/// Selected UI element
|
||||
pub selected: Option<Selection>,
|
||||
/// Contains 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<TuiOut>,
|
||||
}
|
||||
|
||||
has!(Option<Selection>: |self: Arrangement|self.selected);
|
||||
has!(Vec<JackMidiIn>: |self: Arrangement|self.midi_ins);
|
||||
has!(Vec<JackMidiOut>: |self: Arrangement|self.midi_outs);
|
||||
has!(Vec<Scene>: |self: Arrangement|self.scenes);
|
||||
has!(Vec<Track>: |self: Arrangement|self.tracks);
|
||||
|
||||
impl Has<Option<Track>> for Arrangement {
|
||||
fn get (&self) -> &Option<Track> {
|
||||
Has::<Selection>::get(self)
|
||||
.and_then(|selection|selection.track())
|
||||
.and_then(|index|Has::<Vec<Track>>::get(self).get(index))
|
||||
.flatten()
|
||||
}
|
||||
fn get_mut (&mut self) -> &mut Option<Track> {
|
||||
Has::<Selection>::get(self)
|
||||
.and_then(|selection|selection.track())
|
||||
.and_then(|index|Has::<Vec<Track>>::get_mut(self).get_mut(index))
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl Has<Option<Scene>> for Arrangement {
|
||||
fn get (&self) -> &Option<Scene> {
|
||||
Has::<Selection>::get(self)
|
||||
.and_then(|selection|selection.track())
|
||||
.and_then(|index|Has::<Vec<Scene>>::get(self).get(index))
|
||||
.flatten()
|
||||
}
|
||||
fn get_mut (&mut self) -> &mut Option<Scene> {
|
||||
Has::<Selection>::get(self)
|
||||
.and_then(|selection|selection.track())
|
||||
.and_then(|index|Has::<Vec<Scene>>::get_mut(self).get_mut(index))
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
134
crates/device/src/arranger/arranger_port.rs
Normal file
134
crates/device/src/arranger/arranger_port.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
use crate::*;
|
||||
|
||||
impl<'a> ArrangerView<'a> {
|
||||
|
||||
pub(crate) fn input_routes (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
Tryptich::top(self.inputs_height)
|
||||
.left(self.width_side,
|
||||
io_ports(Tui::g(224), Tui::g(32), ||self.arrangement.midi_ins_with_sizes()))
|
||||
.middle(self.width_mid,
|
||||
per_track_top(self.width_mid, ||self.tracks_with_sizes_scrolled(),
|
||||
move|_, &Track { color, .. }|io_conns(
|
||||
color.dark.rgb,
|
||||
color.darker.rgb,
|
||||
||self.arrangement.midi_ins_with_sizes()
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn input_ports (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
Tryptich::top(1)
|
||||
.left(self.width_side,
|
||||
button_3("i", "midi ins", format!("{}", self.inputs_count), self.is_editing))
|
||||
.right(self.width_side,
|
||||
button_2("I", "add midi in", self.is_editing))
|
||||
.middle(self.width_mid,
|
||||
per_track_top(
|
||||
self.width_mid,
|
||||
||self.tracks_with_sizes_scrolled(),
|
||||
move|t, track|{
|
||||
let rec = track.sequencer.recording;
|
||||
let mon = track.sequencer.monitoring;
|
||||
let rec = if rec { White } else { track.color.darkest.rgb };
|
||||
let mon = if mon { White } else { track.color.darkest.rgb };
|
||||
let bg = if self.track_selected == Some(t) {
|
||||
track.color.light.rgb
|
||||
} else {
|
||||
track.color.base.rgb
|
||||
};
|
||||
//let bg2 = if t > 0 { track.color.base.rgb } else { Reset };
|
||||
wrap(bg, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(
|
||||
Tui::fg_bg(rec, bg, "Rec "),
|
||||
Tui::fg_bg(mon, bg, "Mon "),
|
||||
))))
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn input_intos (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
Tryptich::top(2)
|
||||
.left(self.width_side,
|
||||
Bsp::s(Align::e("Input:"), Align::e("Into clip:")))
|
||||
.middle(self.width_mid,
|
||||
per_track_top(
|
||||
self.width_mid,
|
||||
||self.tracks_with_sizes_scrolled(),
|
||||
|_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ ")))))
|
||||
}
|
||||
|
||||
pub(crate) fn output_nexts (&self) -> impl Content<TuiOut> + '_ {
|
||||
Tryptich::top(2)
|
||||
.left(self.width_side, Align::ne("From clip:"))
|
||||
.middle(self.width_mid, per_track_top(
|
||||
self.width_mid,
|
||||
||self.tracks_with_sizes_scrolled(),
|
||||
|_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default())))))
|
||||
}
|
||||
|
||||
pub(crate) fn output_froms (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
let label = Align::ne("Next clip:");
|
||||
Tryptich::top(2).left(self.width_side, label).middle(self.width_mid, per_track_top(
|
||||
self.width_mid, ||self.tracks_with_sizes_scrolled(), |t, track|{
|
||||
let queued = track.sequencer.next_clip.is_some();
|
||||
let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ "));
|
||||
let queued_clip = Thunk::new(||{
|
||||
Tui::bg(Reset, if let Some((_, clip)) = track.sequencer.next_clip.as_ref() {
|
||||
if let Some(clip) = clip {
|
||||
clip.read().unwrap().name.clone()
|
||||
} else {
|
||||
"Stop".into()
|
||||
}
|
||||
} else {
|
||||
"".into()
|
||||
})
|
||||
});
|
||||
Either(queued, queued_clip, queued_blank)
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn output_ports (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
Tryptich::top(1)
|
||||
.left(self.width_side, self.output_count())
|
||||
.right(self.width_side, self.output_add())
|
||||
.middle(self.width_mid, self.output_map())
|
||||
}
|
||||
|
||||
pub(crate) fn output_count (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
button_3("o", "midi outs", format!("{}", self.outputs_count), self.is_editing)
|
||||
}
|
||||
|
||||
pub(crate) fn output_add (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
button_2("O", "add midi out", self.is_editing)
|
||||
}
|
||||
|
||||
pub(crate) fn output_map (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
per_track_top(self.width_mid, ||self.tracks_with_sizes_scrolled(), move|i, t|{
|
||||
let mute = false;
|
||||
let solo = false;
|
||||
let mute = if mute { White } else { t.color.darkest.rgb };
|
||||
let solo = if solo { White } else { t.color.darkest.rgb };
|
||||
let bg_1 = if self.track_selected == Some(i) {
|
||||
t.color.light.rgb
|
||||
} else {
|
||||
t.color.base.rgb
|
||||
};
|
||||
let bg_2 = if i > 0 { t.color.base.rgb } else { Reset };
|
||||
let mute = Tui::fg_bg(mute, bg_1, "Play ");
|
||||
let solo = Tui::fg_bg(solo, bg_1, "Solo ");
|
||||
wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo))))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn output_conns (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
Tryptich::top(self.outputs_height)
|
||||
.left(self.width_side, io_ports(
|
||||
Tui::g(224), Tui::g(32), ||self.arrangement.midi_outs_with_sizes()))
|
||||
.middle(self.width_mid, per_track_top(
|
||||
self.width_mid,
|
||||
||self.tracks_with_sizes_scrolled(),
|
||||
|_, t|io_conns(
|
||||
t.color.dark.rgb,
|
||||
t.color.darker.rgb,
|
||||
||self.arrangement.midi_outs_with_sizes()
|
||||
)))
|
||||
}
|
||||
|
||||
}
|
||||
261
crates/device/src/arranger/arranger_scene.rs
Normal file
261
crates/device/src/arranger/arranger_scene.rs
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
use crate::*;
|
||||
|
||||
#[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 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 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Has<Vec<Scene>> + Send + Sync> HasScenes for T {}
|
||||
|
||||
pub trait HasScenes: Has<Vec<Scene>> + Send + Sync {
|
||||
fn scenes (&self) -> &Vec<Scene> {
|
||||
Has::<Vec<Scene>>::get(self)
|
||||
}
|
||||
fn scenes_mut (&self) -> &mut Vec<Scene> {
|
||||
Has::<Vec<Scene>>::get_mut(self)
|
||||
}
|
||||
fn scenes_with_sizes (
|
||||
&self,
|
||||
editing: bool,
|
||||
height: usize,
|
||||
larger: usize,
|
||||
selected_track: Option<usize>,
|
||||
selected_scene: Option<usize>,
|
||||
) -> impl ScenesSizes<'_> {
|
||||
let mut y = 0;
|
||||
self.scenes().iter().enumerate().map(move|(s, scene)|{
|
||||
let active = editing && selected_track.is_some() && selected_scene == Some(s);
|
||||
let height = if active { larger } else { height };
|
||||
let data = (s, scene, y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
/// Generate the default name for a new scene
|
||||
fn scene_default_name (&self) -> Arc<str> {
|
||||
format!("Sc{:3>}", self.scenes().len() + 1).into()
|
||||
}
|
||||
fn scene_longest_name (&self) -> usize {
|
||||
self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Has<Option<Scene>> + Send + Sync> HasScene for T {}
|
||||
|
||||
pub trait HasScene: Has<Option<Scene>> + Send + Sync {
|
||||
fn scene (&self) -> Option<&Scene> {
|
||||
Has::<Option<Scene>>::get(self)
|
||||
}
|
||||
fn scene_mut (&mut self) -> Option<&mut Scene> {
|
||||
Has::<Option<Scene>>::get_mut(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ArrangerView<'a> {
|
||||
/// Default scene height.
|
||||
pub(crate) const H_SCENE: usize = 2;
|
||||
/// Default editor height.
|
||||
pub(crate) const H_EDITOR: usize = 15;
|
||||
|
||||
/// Render scenes with clips
|
||||
pub(crate) fn scenes (&'a self, editor: Option<MidiEditor>) -> impl Content<TuiOut> + 'a {
|
||||
|
||||
/// A scene with size and color.
|
||||
type SceneWithColor<'a> = (usize, &'a Scene, usize, usize, Option<ItemTheme>);
|
||||
|
||||
let Self {
|
||||
arrangement,
|
||||
width, width_side, width_mid,
|
||||
scenes_height, scene_last, scene_selected,
|
||||
track_selected, is_editing, ..
|
||||
} = self;
|
||||
|
||||
let selection = Has::<Option<Selection>>::get(*arrangement);
|
||||
let (selected_track, selected_scene) = match selection {
|
||||
Some(Selection::Track(t)) => (Some(*t), None),
|
||||
Some(Selection::Scene(s)) => (None, Some(*s)),
|
||||
Some(Selection::TrackClip { track, scene }) => (Some(*track), Some(*scene)),
|
||||
_ => (None, None)
|
||||
};
|
||||
|
||||
let scenes_with_scene_colors = ||arrangement.scenes_with_sizes(
|
||||
*is_editing,
|
||||
Self::H_SCENE,
|
||||
Self::H_EDITOR,
|
||||
selected_track,
|
||||
selected_scene,
|
||||
).map_while(|(s, scene, y1, y2)|if y2 as u16 > *scenes_height {
|
||||
None
|
||||
} else {
|
||||
Some((s, scene, y1, y2, if s == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(arrangement.scenes()[s-1].color)
|
||||
}))
|
||||
});
|
||||
|
||||
let scene_header = |(s, scene, y1, y2, previous): SceneWithColor, _|{
|
||||
let height = (1 + y2 - y1) as u16;
|
||||
let name = Some(scene.name.clone());
|
||||
let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⯈ ", name))));
|
||||
let same_track = true;
|
||||
let selected = same_track && *scene_selected == Some(s);
|
||||
let neighbor = same_track && s > 0 && *scene_selected == Some(s - 1);
|
||||
let is_last = *scene_last == s;
|
||||
let theme = scene.color;
|
||||
let fg = theme.lightest.rgb;
|
||||
let bg = if selected { theme.light } else { theme.base }.rgb;
|
||||
let hi = if let Some(previous) = previous {
|
||||
if neighbor {
|
||||
previous.light.rgb
|
||||
} else {
|
||||
previous.base.rgb
|
||||
}
|
||||
} else {
|
||||
Reset
|
||||
};
|
||||
let lo = if is_last {
|
||||
Reset
|
||||
} else if selected {
|
||||
theme.light.rgb
|
||||
} else {
|
||||
theme.base.rgb
|
||||
};
|
||||
Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat {
|
||||
width: 0, height: 0, content, colors: [fg, bg, hi, lo]
|
||||
})))
|
||||
};
|
||||
|
||||
let scenes_with_track_colors = |track: usize| arrangement.scenes_with_sizes(
|
||||
self.is_editing,
|
||||
Self::H_SCENE,
|
||||
Self::H_EDITOR,
|
||||
selected_track,
|
||||
selected_scene,
|
||||
).map_while(|(s, scene, y1, y2)|if y2 as u16 > self.scenes_height {
|
||||
None
|
||||
} else {
|
||||
Some((s, scene, y1, y2, if s == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.arrangement.scenes[s-1].clips[track].as_ref()
|
||||
.map(|c|c.read().unwrap().color)
|
||||
.unwrap_or(ItemTheme::G[32]))
|
||||
}))
|
||||
});
|
||||
|
||||
let scene_track_clips = |track_index, track|Map::new(
|
||||
||scenes_with_track_colors(track_index),
|
||||
|(s, scene, y1, y2, previous): SceneWithColor<'a>, _|{
|
||||
let (name, theme) = if let Some(clip) = &scene.clips[track_index] {
|
||||
let clip = clip.read().unwrap();
|
||||
(Some(clip.name.clone()), clip.color)
|
||||
} else {
|
||||
(None, ItemTheme::G[32])
|
||||
};
|
||||
let height = (1 + y2 - y1) as u16;
|
||||
let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⏹ ", name))));
|
||||
let same_track = *track_selected == Some(track_index);
|
||||
let selected = same_track && *scene_selected == Some(s);
|
||||
let neighbor = same_track && s > 0 && *scene_selected == Some(s - 1);
|
||||
let is_last = *scene_last == s;
|
||||
let fg = theme.lightest.rgb;
|
||||
let bg = if selected { theme.light } else { theme.base }.rgb;
|
||||
let hi = if let Some(previous) = previous {
|
||||
if neighbor {
|
||||
previous.light.rgb
|
||||
} else {
|
||||
previous.base.rgb
|
||||
}
|
||||
} else {
|
||||
Reset
|
||||
};
|
||||
let lo = if is_last {
|
||||
Reset
|
||||
} else if selected {
|
||||
theme.light.rgb
|
||||
} else {
|
||||
theme.base.rgb
|
||||
};
|
||||
map_south(y1 as u16, height, Bsp::b(Fixed::y(height, Phat {
|
||||
width: 0, height: 0, content, colors: [fg, bg, hi, lo]
|
||||
}), When(
|
||||
*is_editing && same_track && *scene_selected == Some(s),
|
||||
editor
|
||||
)))
|
||||
});
|
||||
|
||||
Tryptich::center(*scenes_height)
|
||||
.left(*width_side, Map::new(
|
||||
||scenes_with_scene_colors(),
|
||||
scene_header))
|
||||
.middle(*width_mid, per_track(
|
||||
*width_mid,
|
||||
||self.tracks_with_sizes_scrolled(),
|
||||
scene_track_clips))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Arrangement {
|
||||
/// Add multiple scenes
|
||||
pub 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
|
||||
pub 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]))
|
||||
}
|
||||
}
|
||||
215
crates/device/src/arranger/arranger_select.rs
Normal file
215
crates/device/src/arranger/arranger_select.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
use crate::*;
|
||||
|
||||
/// Represents the current user selection in the arranger
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
||||
pub enum Selection {
|
||||
/// The whole mix is selected
|
||||
#[default] Mix,
|
||||
/// A MIDI input is selected.
|
||||
Input(usize),
|
||||
/// A MIDI output is selected.
|
||||
Output(usize),
|
||||
/// A scene is selected.
|
||||
Scene(usize),
|
||||
/// A track is selected.
|
||||
Track(usize),
|
||||
/// A clip (track × scene) is selected.
|
||||
TrackClip { track: usize, scene: usize },
|
||||
/// A track's MIDI input connection is selected.
|
||||
TrackInput { track: usize, port: usize },
|
||||
/// A track's MIDI output connection is selected.
|
||||
TrackOutput { track: usize, port: usize },
|
||||
/// A track device slot is selected.
|
||||
TrackDevice { track: usize, device: usize },
|
||||
}
|
||||
|
||||
/// Focus identification methods
|
||||
impl Selection {
|
||||
pub fn is_mix (&self) -> bool {
|
||||
matches!(self, Self::Mix)
|
||||
}
|
||||
pub fn is_track (&self) -> bool {
|
||||
matches!(self, Self::Track(_))
|
||||
}
|
||||
pub fn is_scene (&self) -> bool {
|
||||
matches!(self, Self::Scene(_))
|
||||
}
|
||||
pub fn is_clip (&self) -> bool {
|
||||
matches!(self, Self::TrackClip {..})
|
||||
}
|
||||
pub fn track (&self) -> Option<usize> {
|
||||
use Selection::*;
|
||||
match self {
|
||||
Track(track)
|
||||
| TrackClip { track, .. }
|
||||
| TrackInput { track, .. }
|
||||
| TrackOutput { track, .. }
|
||||
| TrackDevice { track, .. } => Some(*track),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub fn track_header (&self, track_count: usize) -> Self {
|
||||
use Selection::*;
|
||||
match self {
|
||||
Mix => Track(0),
|
||||
Scene(_) => Mix,
|
||||
Track(t) => Track((t + 1) % track_count),
|
||||
TrackClip { track, .. } => Track(*track),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
pub fn track_next (&self, len: usize) -> Self {
|
||||
use Selection::*;
|
||||
match self {
|
||||
Mix => Track(0),
|
||||
Scene(s) => TrackClip { track: 0, scene: *s },
|
||||
Track(t) => if t + 1 < len {
|
||||
Track(t + 1)
|
||||
} else {
|
||||
Mix
|
||||
},
|
||||
TrackClip {track, scene} => if track + 1 < len {
|
||||
TrackClip { track: track + 1, scene: *scene }
|
||||
} else {
|
||||
Scene(*scene)
|
||||
},
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
pub fn track_prev (&self) -> Self {
|
||||
use Selection::*;
|
||||
match self {
|
||||
Mix => Mix,
|
||||
Scene(s) => Scene(*s),
|
||||
Track(0) => Mix,
|
||||
Track(t) => Track(t - 1),
|
||||
TrackClip { track: 0, scene } => Scene(*scene),
|
||||
TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene },
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
pub fn scene (&self) -> Option<usize> {
|
||||
use Selection::*;
|
||||
match self {
|
||||
Scene(scene) | TrackClip { scene, .. } => Some(*scene),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub fn scene_next (&self, len: usize) -> Self {
|
||||
use Selection::*;
|
||||
match self {
|
||||
Mix => Scene(0),
|
||||
Track(t) => TrackClip { track: *t, scene: 0 },
|
||||
Scene(s) => if s + 1 < len {
|
||||
Scene(s + 1)
|
||||
} else {
|
||||
Mix
|
||||
},
|
||||
TrackClip { track, scene } => if scene + 1 < len {
|
||||
TrackClip { track: *track, scene: scene + 1 }
|
||||
} else {
|
||||
Track(*track)
|
||||
},
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
pub fn scene_prev (&self) -> Self {
|
||||
use Selection::*;
|
||||
match self {
|
||||
Mix | Scene(0) => Mix,
|
||||
Scene(s) => Scene(s - 1),
|
||||
Track(t) => Track(*t),
|
||||
TrackClip { track, scene: 0 } => Track(*track),
|
||||
TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 },
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
pub fn describe (&self, tracks: &[Track], scenes: &[Scene]) -> Arc<str> {
|
||||
use Selection::*;
|
||||
format!("{}", match self {
|
||||
Mix => "Everything".to_string(),
|
||||
Scene(s) => scenes.get(*s)
|
||||
.map(|scene|format!("S{s}: {}", &scene.name))
|
||||
.unwrap_or_else(||"S??".into()),
|
||||
Track(t) => tracks.get(*t)
|
||||
.map(|track|format!("T{t}: {}", &track.name))
|
||||
.unwrap_or_else(||"T??".into()),
|
||||
TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) {
|
||||
(Some(_), Some(s)) => match s.clip(*track) {
|
||||
Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name),
|
||||
None => format!("T{track} S{scene}: Empty")
|
||||
},
|
||||
_ => format!("T{track} S{scene}: Empty"),
|
||||
},
|
||||
_ => todo!()
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Arrangement {
|
||||
/// Set the selection
|
||||
pub(crate) fn select (&mut self, s: Selection) {
|
||||
self.selected = s;
|
||||
// autoedit: load focused clip in editor.
|
||||
if let Some(ref mut editor) = self.editor {
|
||||
editor.set_clip(match self.selected {
|
||||
Selection::TrackClip { track, scene }
|
||||
if let Some(Some(Some(clip))) = self
|
||||
.scenes.get(scene)
|
||||
.map(|s|s.clips.get(track)) => Some(clip),
|
||||
_ => None
|
||||
});
|
||||
}
|
||||
}
|
||||
/// Launch a clip or scene
|
||||
pub(crate) fn launch (&mut self) {
|
||||
use Selection::*;
|
||||
match self.selected {
|
||||
Track(t) => {
|
||||
self.tracks[t].sequencer.enqueue_next(None)
|
||||
},
|
||||
TrackClip { track, scene } => {
|
||||
self.tracks[track].sequencer.enqueue_next(self.scenes[scene].clips[track].as_ref())
|
||||
},
|
||||
Scene(s) => {
|
||||
for t in 0..self.tracks.len() {
|
||||
self.tracks[t].sequencer.enqueue_next(self.scenes[s].clips[t].as_ref())
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
/// Set the color of the selected entity
|
||||
pub fn set_color (&mut self, palette: Option<ItemTheme>) -> Option<ItemTheme> {
|
||||
use Selection::*;
|
||||
let palette = palette.unwrap_or_else(||ItemTheme::random());
|
||||
Some(match self.selected {
|
||||
Mix => {
|
||||
let old = self.color;
|
||||
self.color = palette;
|
||||
old
|
||||
},
|
||||
Scene(s) => {
|
||||
let old = self.scenes[s].color;
|
||||
self.scenes[s].color = palette;
|
||||
old
|
||||
}
|
||||
Track(t) => {
|
||||
let old = self.tracks[t].color;
|
||||
self.tracks[t].color = palette;
|
||||
old
|
||||
}
|
||||
TrackClip { track, scene } => {
|
||||
if let Some(ref clip) = self.scenes[scene].clips[track] {
|
||||
let mut clip = clip.write().unwrap();
|
||||
let old = clip.color;
|
||||
clip.color = palette;
|
||||
old
|
||||
} else {
|
||||
return None
|
||||
}
|
||||
},
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
}
|
||||
290
crates/device/src/arranger/arranger_track.rs
Normal file
290
crates/device/src/arranger/arranger_track.rs
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
use crate::*;
|
||||
|
||||
#[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 Track {
|
||||
/// Create a new track with only the default [Sequencer].
|
||||
pub fn new (
|
||||
name: &impl AsRef<str>,
|
||||
color: Option<ItemTheme>,
|
||||
jack: &Jack,
|
||||
clock: Option<&Clock>,
|
||||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||
midi_from: &[PortConnect],
|
||||
midi_to: &[PortConnect],
|
||||
) -> 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()
|
||||
})
|
||||
}
|
||||
/// Create a new track connecting the [Sequencer] to a [Sampler].
|
||||
pub fn new_with_sampler (
|
||||
name: &impl AsRef<str>,
|
||||
color: Option<ItemTheme>,
|
||||
jack: &Jack,
|
||||
clock: Option<&Clock>,
|
||||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||
midi_from: &[PortConnect],
|
||||
midi_to: &[PortConnect],
|
||||
audio_from: &[&[PortConnect];2],
|
||||
audio_to: &[&[PortConnect];2],
|
||||
) -> Usually<Self> {
|
||||
let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?;
|
||||
track.devices.push(Device::Sampler(Sampler::new(
|
||||
jack,
|
||||
&format!("{}/sampler", name.as_ref()),
|
||||
&[PortConnect::exact(format!("{}:{}",
|
||||
jack.with_client(|c|c.name().to_string()),
|
||||
track.sequencer.midi_outs[0].name()
|
||||
))],
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Has<Vec<Track>> + Send + Sync> HasTracks for T {}
|
||||
|
||||
pub trait HasTracks: 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 tracks_render (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
for track in self.tracks.iter_mut() {
|
||||
if Control::Quit == PlayerAudio(
|
||||
track.sequencer_mut(), &mut self.note_buf, &mut self.midi_buf
|
||||
).process(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<Arc<RwLock<MidiClip>>>>) {
|
||||
if let Some(clips) = clips {
|
||||
for (clip, track) in clips.iter().zip(self.tracks_mut()) {
|
||||
track.sequencer.enqueue_next(clip);
|
||||
}
|
||||
} else {
|
||||
for track in self.tracks_mut().iter_mut() {
|
||||
track.sequencer.enqueue_next(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Iterate over tracks with their corresponding sizes.
|
||||
fn tracks_with_sizes (
|
||||
&self,
|
||||
selection: &Option<Selection>,
|
||||
editor_width: Option<usize>
|
||||
) -> impl TracksSizes<'_> {
|
||||
let mut x = 0;
|
||||
let active_track = if let Some(width) = editor_width && let Some(selection) = selection {
|
||||
selection.track()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.tracks().iter().enumerate().map(move |(index, track)|{
|
||||
let width = active_track
|
||||
.and_then(|_|editor_width)
|
||||
.unwrap_or(track.width.max(8));
|
||||
let data = (index, track, x, x + width);
|
||||
x += width + Self::TRACK_SPACING;
|
||||
data
|
||||
})
|
||||
}
|
||||
/// Spacing between tracks.
|
||||
const TRACK_SPACING: usize = 0;
|
||||
}
|
||||
|
||||
impl<T: Has<Option<Track>> + Send + Sync> HasTrack for T {}
|
||||
|
||||
pub trait HasTrack: Has<Option<Track>> + Send + Sync {
|
||||
fn scene (&self) -> Option<&Track> {
|
||||
Has::<Option<Track>>::get(self)
|
||||
}
|
||||
fn scene_mut (&mut self) -> Option<&mut Track> {
|
||||
Has::<Option<Track>>::get_mut(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ArrangerView<'a> {
|
||||
pub(crate) fn tracks_with_sizes_scrolled (&'a self)
|
||||
-> impl TracksSizes<'a>
|
||||
{
|
||||
self.arrangement
|
||||
.tracks_with_sizes(&self.arrangement.selected, self.is_editing.then_some(20/*FIXME*/))
|
||||
.map_while(move|(t, track, x1, x2)|{
|
||||
(self.width_mid > x2 as u16).then_some((t, track, x1, x2))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn per_track_top <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
||||
width: u16,
|
||||
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>> (
|
||||
width: u16,
|
||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
per_track_top(
|
||||
width,
|
||||
tracks,
|
||||
move|index, track|Fill::y(Align::y(callback(index, track)))
|
||||
)
|
||||
}
|
||||
|
||||
impl Arrangement {
|
||||
/// Add multiple tracks
|
||||
pub fn tracks_add (
|
||||
&mut self,
|
||||
count: usize,
|
||||
width: Option<usize>,
|
||||
mins: &[PortConnect],
|
||||
mouts: &[PortConnect],
|
||||
) -> Usually<()> {
|
||||
let jack = self.jack().clone();
|
||||
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 mut track = self.track_add(None, Some(color), mins, mouts)?.1;
|
||||
if let Some(width) = width {
|
||||
track.width = width;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a track
|
||||
pub fn track_add (
|
||||
&mut self,
|
||||
name: Option<&str>,
|
||||
color: Option<ItemTheme>,
|
||||
mins: &[PortConnect],
|
||||
mouts: &[PortConnect],
|
||||
) -> Usually<(usize, &mut Track)> {
|
||||
self.track_last += 1;
|
||||
let name: Arc<str> = name.map_or_else(
|
||||
||format!("Track{:02}", self.track_last).into(),
|
||||
|x|x.to_string().into()
|
||||
);
|
||||
let mut 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 = "sampler")]
|
||||
impl Arrangement {
|
||||
/// Get the first sampler of the active track
|
||||
pub fn sampler (&self) -> Option<&Sampler> {
|
||||
self.track().map(|t|t.sampler(0)).flatten()
|
||||
}
|
||||
/// Get the first sampler of the active track
|
||||
pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
|
||||
self.track_mut().map(|t|t.sampler_mut(0)).flatten()
|
||||
}
|
||||
}
|
||||
217
crates/device/src/arranger/arranger_view.rs
Normal file
217
crates/device/src/arranger/arranger_view.rs
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
use crate::*;
|
||||
|
||||
impl Arrangement {
|
||||
/// Width of display
|
||||
pub(crate) fn w (&self) -> u16 {
|
||||
self.size.w() as u16
|
||||
}
|
||||
/// Width allocated for sidebar.
|
||||
pub(crate) fn w_sidebar (&self) -> u16 {
|
||||
self.w() / if self.is_editing() { 16 } else { 8 } as u16
|
||||
}
|
||||
/// Width taken by all tracks.
|
||||
pub(crate) fn w_tracks (&self) -> u16 {
|
||||
self.tracks_with_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0)
|
||||
}
|
||||
/// Width available to display tracks.
|
||||
pub(crate) fn w_tracks_area (&self) -> u16 {
|
||||
self.w().saturating_sub(2 * self.w_sidebar())
|
||||
}
|
||||
/// Height of display
|
||||
pub(crate) fn h (&self) -> u16 {
|
||||
self.size.h() as u16
|
||||
}
|
||||
/// Height available to display track headers.
|
||||
pub(crate) fn h_tracks_area (&self) -> u16 {
|
||||
5 // FIXME
|
||||
//self.h().saturating_sub(self.h_inputs() + self.h_outputs())
|
||||
}
|
||||
/// Height available to display tracks.
|
||||
pub(crate) fn h_scenes_area (&self) -> u16 {
|
||||
//15
|
||||
self.h().saturating_sub(
|
||||
self.h_inputs() +
|
||||
self.h_outputs() +
|
||||
self.h_devices() +
|
||||
13 // FIXME
|
||||
)
|
||||
}
|
||||
/// Height taken by all scenes.
|
||||
pub(crate) fn h_scenes (&self) -> u16 {
|
||||
let (selected_track, selected_scene) = match Has::<Option<Selection>>::get(self) {
|
||||
Some(Selection::Track(t)) => (Some(*t), None),
|
||||
Some(Selection::Scene(s)) => (None, Some(*s)),
|
||||
Some(Selection::TrackClip { track, scene }) => (Some(*track), Some(*scene)),
|
||||
_ => (None, None)
|
||||
};
|
||||
self.scenes_with_sizes(
|
||||
self.is_editing,
|
||||
ArrangerView::H_SCENE,
|
||||
ArrangerView::H_EDITOR,
|
||||
selected_track,
|
||||
selected_scene
|
||||
)
|
||||
.last()
|
||||
.map(|(_, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
/// Height taken by all inputs.
|
||||
pub(crate) fn h_inputs (&self) -> u16 {
|
||||
self.inputs_with_sizes()
|
||||
.last()
|
||||
.map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
/// Height taken by all outputs.
|
||||
pub(crate) fn h_outputs (&self) -> u16 {
|
||||
self.outputs_with_sizes()
|
||||
.last()
|
||||
.map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
/// Height taken by visible device slots.
|
||||
pub(crate) fn h_devices (&self) -> u16 {
|
||||
2
|
||||
//1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ArrangerView<'a> {
|
||||
pub arrangement: &'a Arrangement,
|
||||
|
||||
pub is_editing: bool,
|
||||
|
||||
pub width: u16,
|
||||
pub width_mid: u16,
|
||||
pub width_side: u16,
|
||||
|
||||
pub inputs_count: usize,
|
||||
pub inputs_height: u16,
|
||||
|
||||
pub outputs_count: usize,
|
||||
pub outputs_height: u16,
|
||||
|
||||
pub scene_last: usize,
|
||||
pub scene_count: usize,
|
||||
pub scene_scroll: Fill<Fixed<u16, ScrollbarV>>,
|
||||
pub scene_selected: Option<usize>,
|
||||
pub scenes_height: u16,
|
||||
|
||||
pub track_scroll: Fill<Fixed<u16, ScrollbarH>>,
|
||||
pub track_count: usize,
|
||||
pub track_selected: Option<usize>,
|
||||
pub tracks_height: u16,
|
||||
|
||||
pub show_debug_info: bool,
|
||||
}
|
||||
|
||||
impl<'a> ArrangerView<'a> {
|
||||
pub fn new (
|
||||
arrangement: &'a Arrangement,
|
||||
editor: Option<&'a MidiEditor>
|
||||
) -> Self {
|
||||
let selected = arrangement.selected;
|
||||
let h_tracks_area = arrangement.h_tracks_area();
|
||||
let h_scenes_area = arrangement.h_scenes_area();
|
||||
let h_scenes = arrangement.h_scenes();
|
||||
Self {
|
||||
arrangement,
|
||||
is_editing: editor.is_some(),
|
||||
|
||||
width: arrangement.w(),
|
||||
width_mid: arrangement.w_tracks_area(),
|
||||
width_side: arrangement.w_sidebar(),
|
||||
|
||||
inputs_height: arrangement.h_inputs(),
|
||||
inputs_count: arrangement.midi_ins.len(),
|
||||
|
||||
outputs_height: arrangement.h_outputs(),
|
||||
outputs_count: arrangement.midi_outs.len(),
|
||||
|
||||
scenes_height: h_scenes_area,
|
||||
scene_selected: selected.map(|s|s.scene()).flatten(),
|
||||
scene_count: arrangement.scenes.len(),
|
||||
scene_last: arrangement.scenes.len().saturating_sub(1),
|
||||
scene_scroll: Fill::y(Fixed::x(1, ScrollbarV {
|
||||
offset: arrangement.scene_scroll,
|
||||
length: h_scenes_area as usize,
|
||||
total: h_scenes as usize,
|
||||
})),
|
||||
|
||||
tracks_height: h_tracks_area,
|
||||
track_count: arrangement.tracks.len(),
|
||||
track_selected: selected.map(|s|s.track()).flatten(),
|
||||
track_scroll: Fill::x(Fixed::y(1, ScrollbarH {
|
||||
offset: arrangement.track_scroll,
|
||||
length: h_tracks_area as usize,
|
||||
total: h_scenes as usize,
|
||||
})),
|
||||
|
||||
show_debug_info: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Content<TuiOut> for ArrangerView<'a> {
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
let ins = |x|Bsp::n(self.inputs(), x);
|
||||
let tracks = |x|Bsp::s(self.tracks(), x);
|
||||
let devices = |x|Bsp::s(self.devices(), x);
|
||||
let outs = |x|Bsp::s(self.outputs(), x);
|
||||
let bg = |x|Tui::bg(Reset, x);
|
||||
//let track_scroll = |x|Bsp::s(&self.track_scroll, x);
|
||||
//let scene_scroll = |x|Bsp::e(&self.scene_scroll, x);
|
||||
outs(tracks(devices(ins(bg(self.scenes(None))))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ArrangerView<'a> {
|
||||
/// Render input matrix.
|
||||
pub(crate) fn inputs (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
Tui::bg(Reset, Bsp::s(
|
||||
self.input_intos(),
|
||||
Bsp::s(self.input_routes(), self.input_ports()),
|
||||
))
|
||||
}
|
||||
|
||||
/// Render output matrix.
|
||||
pub(crate) fn outputs (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
Tui::bg(Reset, Align::n(Bsp::s(
|
||||
Bsp::s(self.output_ports(), self.output_conns()),
|
||||
Bsp::s(self.output_nexts(), self.output_froms()),
|
||||
)))
|
||||
}
|
||||
|
||||
/// Render track headers
|
||||
pub(crate) fn tracks (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
let Self { width_side, width_mid, track_count, track_selected, is_editing, .. } = self;
|
||||
Tryptich::center(3)
|
||||
.left(*width_side, button_3("t", "track", format!("{}", *track_count), *is_editing))
|
||||
.right(*width_side, button_2("T", "add track", *is_editing))
|
||||
.middle(*width_mid, per_track(*width_mid, ||self.tracks_with_sizes_scrolled(),
|
||||
|index, track|wrap(
|
||||
if *track_selected == Some(index) {
|
||||
track.color.light
|
||||
} else {
|
||||
track.color.base
|
||||
}.rgb,
|
||||
track.color.lightest.rgb,
|
||||
Tui::bold(true, Fill::xy(Align::nw(&track.name)))
|
||||
)))
|
||||
}
|
||||
|
||||
/// Render device switches.
|
||||
pub(crate) fn devices (&'a self) -> impl Content<TuiOut> + 'a {
|
||||
let Self { width_side, width_mid, track_count, track_selected, is_editing, .. } = self;
|
||||
Tryptich::top(1)
|
||||
.left(*width_side, button_3("d", "devices", format!("{}", 0), *is_editing))
|
||||
.right(*width_side, button_2("D", "add device", *is_editing))
|
||||
.middle(*width_mid, per_track_top(*width_mid, ||self.tracks_with_sizes_scrolled(),
|
||||
move|index, track|{
|
||||
let bg = if *track_selected == Some(index) {
|
||||
track.color.light
|
||||
} else {
|
||||
track.color.base
|
||||
};
|
||||
let fg = Tui::g(224);
|
||||
track.devices.get(0).map(|device|wrap(bg.rgb, fg, device.name()))
|
||||
}))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue