use crate::*; /// Root level object for standalone `tek_arranger` pub struct Arranger { /// JACK client handle (needs to not be dropped for standalone mode to work). pub jack: Arc>, /// Which view is focused pub focus_cursor: (usize, usize), /// Controls the JACK transport. pub transport: Option>>>, /// Global timebase pub clock: Arc, /// Contains all the sequencers. pub arrangement: Arrangement, /// Pool of all phrases in the arrangement pub phrases: Arc>>, /// Phrase editor view pub editor: PhraseEditor, /// Status bar pub status: ArrangerStatusBar, /// Height of arrangement pub arrangement_split: u16, /// Width of phrase pool pub phrases_split: u16, /// Width and height of app at last render pub size: Measure, /// Menu bar pub menu: MenuBar, } /// Sections in the arranger app that may be focused #[derive(Copy, Clone, PartialEq, Eq)] pub enum ArrangerFocus { /// The transport (toolbar) is focused Transport, /// The arrangement (grid) is focused Arrangement, /// The phrase list (pool) is focused PhrasePool, /// The phrase editor (sequencer) is focused PhraseEditor, } /// Status bar for arranger ap pub enum ArrangerStatusBar { Transport, ArrangementMix, ArrangementTrack, ArrangementScene, ArrangementClip, PhrasePool, PhraseView, PhraseEdit, } /// Represents the tracks and scenes of the composition. pub struct Arrangement { /// Global JACK client pub jack: Arc>, /// Global timebase pub clock: Arc, /// Name of arranger pub name: Arc>, /// Collection of phrases. pub phrases: Arc>>, /// Collection of tracks. pub tracks: Vec, /// Collection of scenes. pub scenes: Vec, /// Currently selected element. pub selected: ArrangementFocus, /// Display mode of arranger pub mode: ArrangementViewMode, /// Whether the arranger is currently focused pub focused: bool, /// Background color of arrangement pub color: ItemColor, /// Width and height of arrangement area at last render pub size: Measure, /// Whether this is currently in edit mode pub entered: bool, } /// Represents a track in the arrangement pub struct ArrangementTrack { /// Name of track pub name: Arc>, /// Preferred width of track column pub width: usize, /// Identifying color of track pub color: ItemColor, /// MIDI player/recorder pub player: PhrasePlayer, } #[derive(Default, Debug)] pub struct Scene { /// Name of scene pub name: Arc>, /// Clips in scene, one per track pub clips: Vec>>>, /// Identifying color of scene pub color: ItemColor, } #[derive(PartialEq, Clone, Copy)] /// Represents the current user selection in the arranger pub enum ArrangementFocus { /// The whole mix is selected Mix, /// A track is selected. Track(usize), /// A scene is selected. Scene(usize), /// A clip (track × scene) is selected. Clip(usize, usize), } /// Display mode of arranger #[derive(PartialEq)] pub enum ArrangementViewMode { /// Tracks are rows Horizontal, /// Tracks are columns Vertical(usize), } /// Arrangement, rendered vertically (session/grid mode). pub struct VerticalArranger<'a, E: Engine>(pub &'a Arrangement, pub usize); /// Arrangement, rendered horizontally (arrangement/track mode). pub struct HorizontalArranger<'a, E: Engine>(pub &'a Arrangement); /// General methods for arranger impl Arranger { pub fn new ( jack: &Arc>, transport: Option>>>, arrangement: Arrangement, phrases: Arc>>, ) -> Self { let mut app = Self { jack: jack.clone(), focus_cursor: (0, 1), phrases_split: 20, arrangement_split: 15, editor: PhraseEditor::new(), status: ArrangerStatusBar::ArrangementClip, transport: transport.clone(), arrangement, phrases, size: Measure::new(), clock: if let Some(ref transport) = transport { transport.read().unwrap().clock.clone() } else { Arc::new(TransportTime::default()) }, menu: { use ArrangerCommand::*; MenuBar::new() .add({ use ArrangementCommand::*; Menu::new("File") .cmd("n", "New project", Arrangement(New)) .cmd("l", "Load project", Arrangement(Load)) .cmd("s", "Save project", Arrangement(Save)) }) .add({ use TransportCommand::*; Menu::new("Transport") .cmd("p", "Play", Transport(Play)) .cmd("s", "Play from start", Transport(PlayFromStart)) .cmd("a", "Pause", Transport(Pause)) }) .add({ use ArrangementCommand::*; Menu::new("Track") .cmd("a", "Append new", Arrangement(AddTrack)) .cmd("i", "Insert new", Arrangement(AddTrack)) .cmd("n", "Rename", Arrangement(AddTrack)) .cmd("d", "Delete", Arrangement(AddTrack)) .cmd(">", "Move up", Arrangement(AddTrack)) .cmd("<", "Move down", Arrangement(AddTrack)) }) .add({ use ArrangementCommand::*; Menu::new("Scene") .cmd("a", "Append new", Arrangement(AddScene)) .cmd("i", "Insert new", Arrangement(AddTrack)) .cmd("n", "Rename", Arrangement(AddTrack)) .cmd("d", "Delete", Arrangement(AddTrack)) .cmd(">", "Move up", Arrangement(AddTrack)) .cmd("<", "Move down", Arrangement(AddTrack)) }) .add({ use PhrasePoolCommand::*; use PhraseRenameCommand as Rename; use PhraseLengthCommand as Length; Menu::new("Phrase") .cmd("a", "Append new", Phrases(Append)) .cmd("i", "Insert new", Phrases(Insert)) .cmd("n", "Rename", Phrases(Rename(Rename::Begin))) .cmd("t", "Set length", Phrases(Length(Length::Begin))) .cmd("d", "Delete", Phrases(Delete)) .cmd("l", "Load from MIDI...", Phrases(Import)) .cmd("s", "Save to MIDI...", Phrases(Export)) .cmd(">", "Move up", Phrases(MoveUp)) .cmd("<", "Move down", Phrases(MoveDown)) }) } }; app.update_focus(); app } /// Toggle global play/pause pub fn toggle_play (&mut self) -> Perhaps { match self.transport { Some(ref mut transport) => { transport.write().unwrap().toggle_play()?; }, None => { return Ok(None) } } Ok(Some(true)) } pub fn next_color (&self) -> ItemColor { if let ArrangementFocus::Clip(track, scene) = self.arrangement.selected { let track_color = self.arrangement.tracks[track].color; let scene_color = self.arrangement.scenes[scene].color; track_color.mix(scene_color, 0.5).mix(ItemColor::random(), 0.25) } else { panic!("could not compute next color") } } /// Focus the editor with the current phrase pub fn show_phrase (&mut self) { self.editor.show(self.arrangement.phrase().as_ref()); } /// Focus the editor with the current phrase pub fn edit_phrase (&mut self) { if self.arrangement.selected.is_clip() && self.arrangement.phrase().is_none() { self.phrases.write().unwrap().append_new(None, Some(self.next_color().into())); self.arrangement.phrase_put(); } self.show_phrase(); self.focus(ArrangerFocus::PhraseEditor); self.editor.entered = true; } /// Rename the selected track, scene, or clip pub fn rename_selected (&mut self) { let Arrangement { selected, ref scenes, .. } = self.arrangement; match selected { ArrangementFocus::Mix => {}, ArrangementFocus::Track(_) => { todo!("rename track"); }, ArrangementFocus::Scene(_) => { todo!("rename scene"); }, ArrangementFocus::Clip(t, s) => if let Some(ref phrase) = scenes[s].clips[t] { let index = self.phrases.read().unwrap().index_of(&*phrase.read().unwrap()); if let Some(index) = index { self.focus(ArrangerFocus::PhrasePool); self.phrases.write().unwrap().phrase = index; self.phrases.write().unwrap().begin_rename(); } }, } } /// Update status bar pub fn update_status (&mut self) { self.status = match self.focused() { ArrangerFocus::Transport => ArrangerStatusBar::Transport, ArrangerFocus::Arrangement => match self.arrangement.selected { ArrangementFocus::Mix => ArrangerStatusBar::ArrangementMix, ArrangementFocus::Track(_) => ArrangerStatusBar::ArrangementTrack, ArrangementFocus::Scene(_) => ArrangerStatusBar::ArrangementScene, ArrangementFocus::Clip(_, _) => ArrangerStatusBar::ArrangementClip, }, ArrangerFocus::PhrasePool => ArrangerStatusBar::PhrasePool, ArrangerFocus::PhraseEditor => match self.editor.entered { true => ArrangerStatusBar::PhraseEdit, false => ArrangerStatusBar::PhraseView, }, } } } /// Focus layout of arranger app impl FocusGrid for Arranger { fn cursor (&self) -> (usize, usize) { self.focus_cursor } fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor } fn layout (&self) -> &[&[ArrangerFocus]] { &[ &[ArrangerFocus::Transport], &[ArrangerFocus::Arrangement, ArrangerFocus::Arrangement], &[ArrangerFocus::PhrasePool, ArrangerFocus::PhraseEditor], ] } fn update_focus (&mut self) { let focused = *self.focused(); if let Some(transport) = self.transport.as_ref() { transport.write().unwrap().focused = focused == ArrangerFocus::Transport } self.arrangement.focused = focused == ArrangerFocus::Arrangement; self.phrases.write().unwrap().focused = focused == ArrangerFocus::PhrasePool; self.editor.focused = focused == ArrangerFocus::PhraseEditor; self.update_status(); } } /// General methods for arrangement impl Arrangement { pub fn new ( jack: &Arc>, clock: &Arc, name: &str, phrases: &Arc>> ) -> Self { Self { jack: jack.clone(), clock: clock.clone(), name: Arc::new(RwLock::new(name.into())), mode: ArrangementViewMode::Vertical(2), selected: ArrangementFocus::Clip(0, 0), phrases: phrases.clone(), scenes: vec![], tracks: vec![], focused: false, color: Color::Rgb(28, 35, 25).into(), size: Measure::new(), entered: false, } } fn is_stopped (&self) -> bool { *self.clock.playing.read().unwrap() == Some(TransportState::Stopped) } pub fn activate (&mut self) { match self.selected { ArrangementFocus::Scene(s) => { for (t, track) in self.tracks.iter_mut().enumerate() { let player = &mut track.player; let clip = self.scenes[s].clips[t].as_ref(); if player.phrase.is_some() || clip.is_some() { player.enqueue_next(clip); } } // TODO make transport available here, so that // activating a scene when stopped starts playback //if self.is_stopped() { //self.transport.toggle_play() //} }, ArrangementFocus::Clip(t, s) => { self.tracks[t].player.enqueue_next(self.scenes[s].clips[t].as_ref()); }, _ => {} } } pub fn delete (&mut self) { match self.selected { ArrangementFocus::Track(_) => self.track_del(), ArrangementFocus::Scene(_) => self.scene_del(), ArrangementFocus::Clip(_, _) => self.phrase_del(), _ => {} } } pub fn increment (&mut self) { match self.selected { ArrangementFocus::Track(_) => self.track_width_inc(), ArrangementFocus::Scene(_) => self.scene_next(), ArrangementFocus::Clip(_, _) => self.phrase_next(), ArrangementFocus::Mix => self.zoom_in(), } } pub fn decrement (&mut self) { match self.selected { ArrangementFocus::Track(_) => self.track_width_dec(), ArrangementFocus::Scene(_) => self.scene_prev(), ArrangementFocus::Clip(_, _) => self.phrase_prev(), ArrangementFocus::Mix => self.zoom_out(), } } pub fn zoom_in (&mut self) { if let ArrangementViewMode::Vertical(factor) = self.mode { self.mode = ArrangementViewMode::Vertical(factor + 1) } } pub fn zoom_out (&mut self) { if let ArrangementViewMode::Vertical(factor) = self.mode { self.mode = ArrangementViewMode::Vertical(factor.saturating_sub(1)) } } pub fn is_first_row (&self) -> bool { let selected = self.selected; selected.is_mix() || selected.is_track() } pub fn is_last_row (&self) -> bool { let selected = self.selected; (self.scenes.len() == 0 && (selected.is_mix() || selected.is_track())) || match selected { ArrangementFocus::Scene(s) => s == self.scenes.len() - 1, ArrangementFocus::Clip(_, s) => s == self.scenes.len() - 1, _ => false } } pub fn toggle_loop (&mut self) { if let Some(phrase) = self.phrase() { phrase.write().unwrap().toggle_loop() } } pub fn go_up (&mut self) { match self.mode { ArrangementViewMode::Horizontal => self.track_prev(), _ => self.scene_prev(), }; } pub fn go_down (&mut self) { match self.mode { ArrangementViewMode::Horizontal => self.track_next(), _ => self.scene_next(), }; } pub fn go_left (&mut self) { match self.mode { ArrangementViewMode::Horizontal => self.scene_prev(), _ => self.track_prev(), }; } pub fn go_right (&mut self) { match self.mode { ArrangementViewMode::Horizontal => self.scene_next(), _ => self.track_next(), }; } pub fn move_back (&mut self) { match self.selected { ArrangementFocus::Scene(s) => { if s > 0 { self.scenes.swap(s, s - 1); self.selected = ArrangementFocus::Scene(s - 1); } }, ArrangementFocus::Track(t) => { if t > 0 { self.tracks.swap(t, t - 1); self.selected = ArrangementFocus::Track(t - 1); // FIXME: also swap clip order in scenes } }, _ => todo!("arrangement: move forward") } } pub fn move_forward (&mut self) { match self.selected { ArrangementFocus::Scene(s) => { if s < self.scenes.len().saturating_sub(1) { self.scenes.swap(s, s + 1); self.selected = ArrangementFocus::Scene(s + 1); } }, ArrangementFocus::Track(t) => { if t < self.tracks.len().saturating_sub(1) { self.tracks.swap(t, t + 1); self.selected = ArrangementFocus::Track(t + 1); // FIXME: also swap clip order in scenes } }, _ => todo!("arrangement: move forward") } } pub fn randomize_color (&mut self) { match self.selected { ArrangementFocus::Mix => { self.color = ItemColor::random_dark() }, ArrangementFocus::Track(t) => { self.tracks[t].color = ItemColor::random() }, ArrangementFocus::Scene(s) => { self.scenes[s].color = ItemColor::random() }, ArrangementFocus::Clip(t, s) => if let Some(phrase) = &self.scenes[s].clips[t] { phrase.write().unwrap().color = ItemColorTriplet::random(); } } } } /// Methods for tracks in arrangement impl Arrangement { pub fn track (&self) -> Option<&ArrangementTrack> { self.selected.track().map(|t|self.tracks.get(t)).flatten() } pub fn track_mut (&mut self) -> Option<&mut ArrangementTrack> { self.selected.track().map(|t|self.tracks.get_mut(t)).flatten() } pub fn track_width_inc (&mut self) { self.track_mut().map(|t|t.width_inc()); } pub fn track_width_dec (&mut self) { self.track_mut().map(|t|t.width_dec()); } pub fn track_next (&mut self) { self.selected.track_next(self.tracks.len() - 1) } pub fn track_prev (&mut self) { self.selected.track_prev() } pub fn track_add ( &mut self, name: Option<&str>, color: Option ) -> Usually<&mut ArrangementTrack> { self.tracks.push(name.map_or_else( || ArrangementTrack::new( &self.jack, &self.clock, &self.track_default_name(), color ), |name| ArrangementTrack::new( &self.jack, &self.clock, name, color ), )?); let index = self.tracks.len() - 1; Ok(&mut self.tracks[index]) } pub fn track_del (&mut self) { if let Some(index) = self.selected.track() { self.tracks.remove(index); for scene in self.scenes.iter_mut() { scene.clips.remove(index); } } } pub fn track_default_name (&self) -> String { format!("Track {}", self.tracks.len() + 1) } pub fn track_widths (&self) -> Vec<(usize, usize)> { let mut widths = vec![]; let mut total = 0; for track in self.tracks.iter() { let width = track.width; widths.push((width, total)); total += width; } widths.push((0, total)); widths } } /// Methods for scenes in arrangement impl Arrangement { pub fn scene (&self) -> Option<&Scene> { self.selected.scene().map(|s|self.scenes.get(s)).flatten() } pub fn scene_mut (&mut self) -> Option<&mut Scene> { self.selected.scene().map(|s|self.scenes.get_mut(s)).flatten() } pub fn scene_next (&mut self) { self.selected.scene_next(self.scenes.len() - 1) } pub fn scene_prev (&mut self) { self.selected.scene_prev() } pub fn scene_add ( &mut self, name: Option<&str>, color: Option ) -> Usually<&mut Scene> { let clips = vec![None;self.tracks.len()]; let name = name.map(|x|x.to_string()).unwrap_or_else(||self.scene_default_name()); self.scenes.push(Scene::new(name, clips, color)); let index = self.scenes.len() - 1; Ok(&mut self.scenes[index]) } pub fn scene_del (&mut self) { if let Some(index) = self.selected.scene() { self.scenes.remove(index); } } pub fn scene_default_name (&self) -> String { format!("Scene {}", self.scenes.len() + 1) } } /// Methods for phrases in arrangement impl Arrangement { pub fn sequencer (&self) -> Option<&ArrangementTrack> { self.selected.track().map(|track|self.tracks.get(track)).flatten() } pub fn sequencer_mut (&mut self) -> Option<&mut ArrangementTrack> { self.selected.track().map(|track|self.tracks.get_mut(track)).flatten() } pub fn phrase (&self) -> Option>> { self.scene()?.clips.get(self.selected.track()?)?.clone() } pub fn phrase_del (&mut self) { let track_index = self.selected.track(); let scene_index = self.selected.scene(); track_index .and_then(|index|self.tracks.get_mut(index).map(|track|(index, track))) .map(|(track_index, _)|scene_index .and_then(|index|self.scenes.get_mut(index)) .map(|scene|scene.clips[track_index] = None)); } pub fn phrase_put (&mut self) { if let ArrangementFocus::Clip(track, scene) = self.selected { self.scenes[scene].clips[track] = Some(self.phrases.read().unwrap().phrase().clone()); } } pub fn phrase_get (&mut self) { if let ArrangementFocus::Clip(track, scene) = self.selected { if let Some(phrase) = &self.scenes[scene].clips[track] { let mut phrases = self.phrases.write().unwrap(); if let Some(index) = phrases.index_of(&*phrase.read().unwrap()) { phrases.phrase = index; } } } } pub fn phrase_next (&mut self) { if let ArrangementFocus::Clip(track, scene) = self.selected { if let Some(ref mut phrase) = self.scenes[scene].clips[track] { let phrases = self.phrases.read().unwrap(); let index = phrases.index_of(&*phrase.read().unwrap()); if let Some(index) = index { if index < phrases.phrases.len().saturating_sub(1) { *phrase = phrases.phrases[index + 1].clone(); } } } } } pub fn phrase_prev (&mut self) { if let ArrangementFocus::Clip(track, scene) = self.selected { if let Some(ref mut phrase) = self.scenes[scene].clips[track] { let phrases = self.phrases.read().unwrap(); let index = phrases.index_of(&*phrase.read().unwrap()); if let Some(index) = index { if index > 0 { *phrase = phrases.phrases[index - 1].clone(); } } } } } } impl ArrangementTrack { pub fn new ( jack: &Arc>, clock: &Arc, name: &str, color: Option ) -> Usually { Ok(Self { name: Arc::new(RwLock::new(name.into())), width: name.len() + 2, color: color.unwrap_or_else(ItemColor::random), player: PhrasePlayer::new(&jack, clock, name)?, }) } pub fn longest_name (tracks: &[Self]) -> usize { tracks.iter().map(|s|s.name.read().unwrap().len()).fold(0, usize::max) } pub const MIN_WIDTH: usize = 3; pub fn width_inc (&mut self) { self.width += 1; } pub fn width_dec (&mut self) { if self.width > Self::MIN_WIDTH { self.width -= 1; } } } /// Focus identification methods impl ArrangementFocus { pub fn description ( &self, tracks: &Vec, scenes: &Vec, ) -> String { format!("Selected: {}", match self { Self::Mix => format!("Everything"), Self::Track(t) => match tracks.get(*t) { Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), None => format!("T??"), }, Self::Scene(s) => match scenes.get(*s) { Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), None => format!("S??"), }, Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { (Some(_), Some(scene)) => match scene.clip(*t) { Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), None => format!("T{t} S{s}: Empty") }, _ => format!("T{t} S{s}: Empty"), } }) } pub fn is_mix (&self) -> bool { match self { Self::Mix => true, _ => false } } pub fn is_track (&self) -> bool { match self { Self::Track(_) => true, _ => false } } pub fn is_scene (&self) -> bool { match self { Self::Scene(_) => true, _ => false } } pub fn is_clip (&self) -> bool { match self { Self::Clip(_, _) => true, _ => false } } pub fn track (&self) -> Option { match self { Self::Clip(t, _) => Some(*t), Self::Track(t) => Some(*t), _ => None } } pub fn track_next (&mut self, last_track: usize) { *self = match self { Self::Mix => Self::Track(0), Self::Track(t) => Self::Track(last_track.min(*t + 1)), Self::Scene(s) => Self::Clip(0, *s), Self::Clip(t, s) => Self::Clip(last_track.min(*t + 1), *s), } } pub fn track_prev (&mut self) { *self = match self { Self::Mix => Self::Mix, Self::Scene(s) => Self::Scene(*s), Self::Track(t) => if *t == 0 { Self::Mix } else { Self::Track(*t - 1) }, Self::Clip(t, s) => if *t == 0 { Self::Scene(*s) } else { Self::Clip(t.saturating_sub(1), *s) } } } pub fn scene (&self) -> Option { match self { Self::Clip(_, s) => Some(*s), Self::Scene(s) => Some(*s), _ => None } } pub fn scene_next (&mut self, last_scene: usize) { *self = match self { Self::Mix => Self::Scene(0), Self::Track(t) => Self::Clip(*t, 0), Self::Scene(s) => Self::Scene(last_scene.min(*s + 1)), Self::Clip(t, s) => Self::Clip(*t, last_scene.min(*s + 1)), } } pub fn scene_prev (&mut self) { *self = match self { Self::Mix => Self::Mix, Self::Track(t) => Self::Track(*t), Self::Scene(s) => if *s == 0 { Self::Mix } else { Self::Scene(*s - 1) }, Self::Clip(t, s) => if *s == 0 { Self::Track(*t) } else { Self::Clip(*t, s.saturating_sub(1)) } } } } /// Arranger display mode can be cycled impl ArrangementViewMode { /// Cycle arranger display mode pub fn to_next (&mut self) { *self = match self { Self::Horizontal => Self::Vertical(1), Self::Vertical(1) => Self::Vertical(2), Self::Vertical(2) => Self::Vertical(2), Self::Vertical(0) => Self::Horizontal, Self::Vertical(_) => Self::Vertical(0), } } } impl Scene { pub fn new ( name: impl AsRef, clips: impl AsRef<[Option>>]>, color: Option, ) -> Self { Self { name: Arc::new(RwLock::new(name.as_ref().into())), clips: clips.as_ref().iter().map(|x|x.clone()).collect(), color: color.unwrap_or_else(ItemColor::random), } } /// Returns the pulse length of the longest phrase 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 phrases in the scene are currently playing pub fn is_playing (&self, tracks: &[ArrangementTrack]) -> bool { self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() .all(|(track_index, clip)|match clip { Some(clip) => tracks .get(track_index) .map(|track|if let Some((_, Some(phrase))) = &track.player.phrase { *phrase.read().unwrap() == *clip.read().unwrap() } else { false }) .unwrap_or(false), None => true }) } pub fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> { let mut total = 0; if factor == 0 { scenes.iter().map(|scene|{ let pulses = scene.pulses().max(PPQ); total = total + pulses; (pulses, total - pulses) }).collect() } else { (0..=scenes.len()).map(|i|{ (factor*PPQ, factor*PPQ*i) }).collect() } } pub fn longest_name (scenes: &[Self]) -> usize { scenes.iter().map(|s|s.name.read().unwrap().len()).fold(0, usize::max) } pub fn clip (&self, index: usize) -> Option<&Arc>> { match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } } }