mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-10-08 10:46:43 +02:00
got gear (m8)
This commit is contained in:
parent
a355e482c7
commit
b050e0e014
14 changed files with 799 additions and 809 deletions
|
|
@ -8,6 +8,7 @@ impl_as_mut!(Config: |self: ConfigWatcher| &mut self.config);
|
|||
impl_as_ref!(ConfigWatcher: |self: ConfigWatcher| &self);
|
||||
|
||||
mod bind; pub use self::bind::*;
|
||||
mod lang; pub use self::lang::*;
|
||||
mod mode; pub use self::mode::*;
|
||||
mod view; pub use self::view::*;
|
||||
|
||||
|
|
|
|||
154
src/conf/lang.rs
Normal file
154
src/conf/lang.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
use crate::*;
|
||||
|
||||
pub fn tek_arc_str <'a> (_: &App, src: impl Language) -> Perhaps<Arc<str>> {
|
||||
Ok(src.src()?.map(|x|x.into()))
|
||||
}
|
||||
|
||||
pub fn tek_u16 <'a> (state: &App, src: impl Language) -> Perhaps<u16> {
|
||||
Ok(Some(match src.word()? {
|
||||
Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()),
|
||||
Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9),
|
||||
_ => return try_to_u16(src)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn tek_usize <'a> (state: &App, src: impl Language) -> Perhaps<usize> {
|
||||
Ok(Some(match src.word()? {
|
||||
Some(":scene-count") => state.scenes().len(),
|
||||
Some(":track-count") => state.tracks().len(),
|
||||
Some(":device-kind") => state.dialog.device_kind().unwrap_or(0),
|
||||
Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0),
|
||||
Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0),
|
||||
_ => return try_to_usize(src)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn tek_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps<f64> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn tek_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
"Y" => true,
|
||||
"N" => false,
|
||||
":mode/editor" => state.project.editor.is_some(),
|
||||
":focused/dialog" => !matches!(state.dialog, Dialog::None),
|
||||
":focused/message" => matches!(state.dialog, Dialog::Message(..)),
|
||||
":focused/add_device" => matches!(state.dialog, Dialog::Device(..)),
|
||||
":focused/browser" => state.dialog.browser().is_some(),
|
||||
//":focused/pool/import" => matches!(state.project.pool.mode, Some(PoolMode::Import(..))),
|
||||
//":focused/pool/export" => matches!(state.project.pool.mode, Some(PoolMode::Export(..))),
|
||||
//":focused/pool/rename" => matches!(state.project.pool.mode, Some(PoolMode::Rename(..))),
|
||||
//":focused/pool/length" => matches!(state.project.pool.mode, Some(PoolMode::Length(..))),
|
||||
":focused/clip" => matches!(state.selection(), Selection::TrackClip{..}),
|
||||
":focused/track" => matches!(state.selection(), Selection::Track(..)),
|
||||
":focused/scene" => matches!(state.selection(), Selection::Scene(..)),
|
||||
":focused/mix" => matches!(state.selection(), Selection::Mix),
|
||||
_ => return err!("not bool: {word}")
|
||||
})).transpose()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn tek_selection <'a> (state: &App, src: impl Language) -> Perhaps<Selection> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":select/scene" => state.selection().select_scene(state.tracks().len()),
|
||||
":select/scene/next" => state.selection().select_scene_next(state.scenes().len()),
|
||||
":select/scene/prev" => state.selection().select_scene_prev(),
|
||||
":select/track" => state.selection().select_track(state.tracks().len()),
|
||||
":select/track/next" => state.selection().select_track_next(state.tracks().len()),
|
||||
":select/track/prev" => state.selection().select_track_prev(),
|
||||
_ => return err!("not selection: {word}")
|
||||
})).transpose()
|
||||
}
|
||||
|
||||
pub fn tek_color <'a> (_: &App, src: impl Language) -> Perhaps<Color> {
|
||||
if let Some(expr) = src.expr()? {
|
||||
match (expr.head()?, expr.tail()?) {
|
||||
(Some("g"), Some(tail)) => {
|
||||
let n = try_to_u8(tail.head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not gray"))?;
|
||||
Ok(Some(Color::Rgb(n, n, n)))
|
||||
},
|
||||
(Some("rgb"), Some(tail)) => {
|
||||
let r = try_to_u8(tail.head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not red"))?;
|
||||
let g = try_to_u8(tail.tail().head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not green"))?;
|
||||
let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not blue"))?;
|
||||
Ok(Some(Color::Rgb(r, g, b)))
|
||||
},
|
||||
(Some(_), _) => return err!("not a color expression: {expr}"),
|
||||
(None, _) => return err!("not a color expression: {expr}"),
|
||||
}
|
||||
} else if let Ok(Some(sym)) = src.word() {
|
||||
Ok(match sym {
|
||||
":color/bg" => Some(Color::Rgb(28, 32, 36)),
|
||||
":color/fg" => Some(Color::Rgb(98, 92, 96)),
|
||||
_ => return err!("not a color: {sym}")
|
||||
})
|
||||
} else {
|
||||
return err!("not a color: {:?}", src.src()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn tek_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps<Option<u7>> {
|
||||
eval_word(src, |word|Ok(match word {
|
||||
|
||||
":editor/pitch" => Some((
|
||||
state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
|
||||
).into()),
|
||||
|
||||
_ => return err!("unknown midi note: {word}")
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn tek_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps<Option<usize>> {
|
||||
eval_word(src, |word|Ok(match word {
|
||||
|
||||
":selected/scene" => state.selection().scene(),
|
||||
":selected/track" => state.selection().track(),
|
||||
|
||||
_ => return err!("unknown opt<usize>: {word}")
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
pub fn tek_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn tek_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u32>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn tek_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn tek_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<[Connect]>>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn tek_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Arc<[Connect]>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn tek_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<str>>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn tek_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps<Option<bool>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn tek_path_buf <'a> (_: &App, _: impl Language) -> Perhaps<PathBuf> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn tek_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Vec<Arc<str>>> {
|
||||
todo!()
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ impl Mode {
|
|||
}
|
||||
|
||||
impl ControlAxis {
|
||||
pub fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
|
||||
pub fn eval <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
|
||||
eval_word(src, |word|Ok(ControlAxis(word.into())))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use crate::*;
|
||||
|
||||
def_sizes_iter!(TracksSizes => Track);
|
||||
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
|
||||
impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref());
|
||||
impl_has!(Sequencer: |self: Track| self.sequencer);
|
||||
594
src/project.rs
594
src/project.rs
|
|
@ -1,594 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||||
def_sizes_iter!(ScenesSizes => Scene);
|
||||
impl_as_mut!(Option<MidiEditor>: |self: Project| &mut self.editor);
|
||||
impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut());
|
||||
impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut());
|
||||
impl_as_ref!(Option<MidiEditor>: |self: Project| &self.editor);
|
||||
impl_as_ref_opt!(Scene: |self: Project| self.selected_scene());
|
||||
impl_as_ref_opt!(Track: |self: Project| self.selected_track());
|
||||
impl_has!(Clock: |self: Project| self.clock);
|
||||
impl_has!(Jack<'static>: |self: Project| self.jack);
|
||||
impl_has!(Selection: |self: Project| self.selection);
|
||||
impl_has!(Sizer: |self: Project| self.size);
|
||||
impl_has!(Vec<MidiInput>: |self: Project| self.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: Project| self.midi_outs);
|
||||
|
||||
#[cfg(feature = "cli")] pub use self::cli::*;
|
||||
#[cfg(feature = "cli")] mod cli {
|
||||
use super::*;
|
||||
|
||||
/// Command to launch into new project from CLI.
|
||||
#[derive(Debug, Clone, Parser, Default)]
|
||||
pub struct ProjectInit {
|
||||
/// Name of JACK client
|
||||
#[arg(short='n', long)] name: Option<String>,
|
||||
/// Whether to attempt to become transport master
|
||||
#[arg(short='Y', long, default_value_t = false)] sync_lead: bool,
|
||||
/// Whether to sync to external transport master
|
||||
#[arg(short='y', long, default_value_t = true)] sync_follow: bool,
|
||||
/// Initial tempo in beats per minute
|
||||
#[arg(short='b', long, default_value = None)] bpm: Option<f64>,
|
||||
/// Whether to include a transport toolbar (default: true)
|
||||
#[arg(short='c', long, default_value_t = true)] show_clock: bool,
|
||||
/// MIDI outs to connect to (multiple instances accepted)
|
||||
#[arg(short='I', long)] midi_from: Vec<String>,
|
||||
/// MIDI outs to connect to (multiple instances accepted)
|
||||
#[arg(short='i', long)] midi_from_re: Vec<String>,
|
||||
/// MIDI ins to connect to (multiple instances accepted)
|
||||
#[arg(short='O', long)] midi_to: Vec<String>,
|
||||
/// MIDI ins to connect to (multiple instances accepted)
|
||||
#[arg(short='o', long)] midi_to_re: Vec<String>,
|
||||
/// Audio outs to connect to left input
|
||||
#[arg(short='l', long)] left_from: Vec<String>,
|
||||
/// Audio outs to connect to right input
|
||||
#[arg(short='r', long)] right_from: Vec<String>,
|
||||
/// Audio ins to connect from left output
|
||||
#[arg(short='L', long)] left_to: Vec<String>,
|
||||
/// Audio ins to connect from right output
|
||||
#[arg(short='R', long)] right_to: Vec<String>,
|
||||
/// Tracks to creat
|
||||
#[arg(short='t', long)] tracks: Option<usize>,
|
||||
/// Scenes to create
|
||||
#[arg(short='s', long)] scenes: Option<usize>,
|
||||
}
|
||||
|
||||
impl ProjectInit {
|
||||
pub fn init (&self) -> Usually<Project> {
|
||||
let name = self.name.as_ref().map_or("tek", |x|x.as_str());
|
||||
let jack = Jack::new(&name)?;
|
||||
let midi_ins = connect_midi_ins(&jack, &"M".to_string(), &self.midi_from, Some(&self.midi_from_re))?;
|
||||
let midi_out = connect_midi_outs(&jack, &"M".to_string(), &self.midi_to, Some(&self.midi_to_re))?;
|
||||
let inputs = [].into_iter()
|
||||
.chain(connect_audio_ins(&jack, &"L".to_string(), &self.left_from, None)?.into_iter())
|
||||
.chain(connect_audio_ins(&jack, &"R".to_string(), &self.right_from, None)?.into_iter());
|
||||
let outputs = [].into_iter()
|
||||
.chain(connect_audio_outs(&jack, &"L".to_string(), &self.left_to, None)?.into_iter())
|
||||
.chain(connect_audio_outs(&jack, &"R".to_string(), &self.right_to, None)?.into_iter());
|
||||
let mut proj = Project::create(&jack, name.into(), Clock::new(&jack, self.bpm)?,
|
||||
[].into_iter(), [].into_iter(), midi_ins.into_iter(), midi_out.into_iter(),
|
||||
inputs, outputs);
|
||||
proj.tracks_add_many(self.tracks.unwrap_or(0), None, [].into(), [].into())?;
|
||||
proj.scenes_add_many(self.scenes.unwrap_or(0))?;
|
||||
Ok(proj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Arranger.
|
||||
///
|
||||
/// ```
|
||||
/// let arranger = tek::Project::default();
|
||||
/// ```
|
||||
#[namespace(ClipPool ClipPool::eval)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Project {
|
||||
/// JACK client handle.
|
||||
pub jack: Jack<'static>,
|
||||
/// Project name.
|
||||
pub name: Arc<str>,
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// FIXME a render of the project arrangement, redrawn on update.
|
||||
/// TODO rename to "render_cache" or smth
|
||||
pub arranger: Arc<RwLock<Buffer>>,
|
||||
/// Display size
|
||||
pub size: Sizer,
|
||||
/// Display size of clips area
|
||||
pub size_inner: Sizer,
|
||||
/// Selected UI element
|
||||
pub selection: Selection,
|
||||
/// Contains all recently created clips.
|
||||
#[cfg(feature = "sequencer")] pub clips: Pool<MidiClip>,
|
||||
/// Contains all recently created clips.
|
||||
#[cfg(feature = "sampler")] pub samples: Pool<Sample>,
|
||||
/// Source of time
|
||||
#[cfg(feature = "clock")] pub clock: Clock,
|
||||
/// Allows one MIDI clip to be edited
|
||||
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
|
||||
/// List of global midi inputs
|
||||
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
||||
/// List of global midi outputs
|
||||
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
||||
/// List of global audio inputs
|
||||
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
||||
/// List of global audio outputs
|
||||
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
||||
/// Last track number (to avoid duplicate port names)
|
||||
#[cfg(feature = "track")] pub track_last: usize,
|
||||
/// List of tracks
|
||||
#[cfg(feature = "track")] pub tracks: Vec<Track>,
|
||||
/// Scroll offset of tracks
|
||||
#[cfg(feature = "track")] pub track_scroll: usize,
|
||||
/// List of scenes
|
||||
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
|
||||
/// Scroll offset of scenes
|
||||
#[cfg(feature = "scene")] pub scenes_scroll: usize,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn new (jack: &Jack<'static>) -> Self {
|
||||
Self {
|
||||
jack: jack.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_jack (name: impl AsRef<str>) -> Usually<Self> {
|
||||
let name = name.as_ref();
|
||||
Ok(Self {
|
||||
name: name.into(),
|
||||
jack: Jack::new(name)?,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new arrangement.
|
||||
pub fn create (
|
||||
jack: &Jack<'static>,
|
||||
name: Arc<str>,
|
||||
clock: Clock,
|
||||
tracks: impl Iterator<Item = Track>,
|
||||
scenes: impl Iterator<Item = Scene>,
|
||||
midi_ins: impl Iterator<Item = MidiInput>,
|
||||
midi_outs: impl Iterator<Item = MidiOutput>,
|
||||
audio_ins: impl Iterator<Item = AudioInput>,
|
||||
audio_outs: impl Iterator<Item = AudioOutput>,
|
||||
) -> Self {
|
||||
Self {
|
||||
jack: jack.clone(),
|
||||
color: ItemTheme::random(),
|
||||
selection: Selection::TrackClip { track: 0, scene: 0 },
|
||||
tracks: tracks.collect(),
|
||||
scenes: scenes.collect(),
|
||||
midi_ins: midi_ins.collect(),
|
||||
midi_outs: midi_outs.collect(),
|
||||
audio_ins: audio_ins.collect(),
|
||||
audio_outs: audio_outs.collect(),
|
||||
size_inner: Sizer(Arc::new(40.into()), Arc::new(25.into())),
|
||||
clock,
|
||||
name,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Width of display
|
||||
pub fn w (&self) -> u16 {
|
||||
self.size.w() as u16
|
||||
}
|
||||
|
||||
/// Width allocated for sidebar.
|
||||
pub fn w_sidebar (&self, is_editing: bool) -> u16 {
|
||||
self.w() / if is_editing { 16 } else { 8 } as u16
|
||||
}
|
||||
|
||||
/// Width available to display tracks.
|
||||
pub fn w_tracks_area (&self, is_editing: bool) -> u16 {
|
||||
self.w().saturating_sub(self.w_sidebar(is_editing))
|
||||
}
|
||||
|
||||
/// Height of display
|
||||
pub fn h (&self) -> u16 {
|
||||
self.size.h() as u16
|
||||
}
|
||||
|
||||
/// Height taken by visible device slots.
|
||||
pub fn h_devices (&self) -> u16 {
|
||||
2
|
||||
//1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasJack<'static> for Project {
|
||||
fn jack (&self) -> &Jack<'static> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
impl HasClip for Project {
|
||||
fn clip_slot (&self) -> Option<Slot<MidiClip>> {
|
||||
match self.selection {
|
||||
Selection::Clip(index) =>
|
||||
Some(self.clips.slot(index)),
|
||||
Selection::TrackClip { track, scene } if let Some(scene) = self.scenes.get(scene) =>
|
||||
Some(scene.clips.slot(track)),
|
||||
_ =>
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasClipsSize for Project {
|
||||
fn clips_size (&self) -> &Sizer {
|
||||
&self.size_inner
|
||||
}
|
||||
}
|
||||
|
||||
impl ScenesView for Project {
|
||||
fn h_scenes (&self) -> u16 {
|
||||
(self.size.h() as u16).saturating_sub(20)
|
||||
}
|
||||
fn w_side (&self) -> u16 {
|
||||
(self.size.w() as u16 * 2 / 10).max(20)
|
||||
}
|
||||
fn w_mid (&self) -> u16 {
|
||||
(self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap_value <T: Clone + PartialEq, U> (
|
||||
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||||
) -> Perhaps<U> {
|
||||
if *target == *value {
|
||||
Ok(None)
|
||||
} else {
|
||||
let mut value = value.clone();
|
||||
std::mem::swap(target, &mut value);
|
||||
Ok(Some(returned(value)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_bool <U> (
|
||||
target: &mut bool, value: &Option<bool>, returned: impl Fn(Option<bool>)->U
|
||||
) -> Perhaps<U> {
|
||||
let mut value = value.unwrap_or(!*target);
|
||||
if value == *target {
|
||||
Ok(None)
|
||||
} else {
|
||||
std::mem::swap(target, &mut value);
|
||||
Ok(Some(returned(Some(value))))
|
||||
}
|
||||
}
|
||||
|
||||
/// Modes for clip pool
|
||||
#[derive(Debug, Clone)] pub enum PoolMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, Arc<str>),
|
||||
/// Editing the length of a pattern
|
||||
Length(usize, usize, ClipLengthFocus),
|
||||
/// Load clip from disk
|
||||
Import(usize, Browse),
|
||||
/// Save clip to disk
|
||||
Export(usize, Browse),
|
||||
}
|
||||
|
||||
/// Focused field of `ClipLength`
|
||||
#[derive(Copy, Clone, Debug)] pub enum ClipLengthFocus {
|
||||
/// Editing the number of bars
|
||||
Bar,
|
||||
/// Editing the number of beats
|
||||
Beat,
|
||||
/// Editing the number of ticks
|
||||
Tick,
|
||||
}
|
||||
|
||||
impl ClipLengthFocus {
|
||||
pub fn next (&mut self) {
|
||||
use ClipLengthFocus::*;
|
||||
*self = match self { Bar => Beat, Beat => Tick, Tick => Bar, }
|
||||
}
|
||||
pub fn prev (&mut self) {
|
||||
use ClipLengthFocus::*;
|
||||
*self = match self { Bar => Tick, Beat => Bar, Tick => Beat, }
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays and edits clip length.
|
||||
#[derive(Clone, Debug, Default)] pub struct ClipLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of clip in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<ClipLengthFocus>,
|
||||
}
|
||||
|
||||
impl ClipLength {
|
||||
pub fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
|
||||
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||
}
|
||||
pub fn bars (&self) -> usize {
|
||||
self.pulses / (self.bpb * self.ppq)
|
||||
}
|
||||
pub fn beats (&self) -> usize {
|
||||
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||
}
|
||||
pub fn ticks (&self) -> usize {
|
||||
self.pulses % self.ppq
|
||||
}
|
||||
}
|
||||
|
||||
impl HasTrackScroll for &mut Project {
|
||||
fn track_scroll (&self) -> usize {
|
||||
self.track_scroll
|
||||
}
|
||||
fn tracks_last (&self) -> usize {
|
||||
self.track_last
|
||||
}
|
||||
fn tracks_last_mut (&mut self) -> &mut usize {
|
||||
&mut self.track_last
|
||||
}
|
||||
}
|
||||
|
||||
impl HasTrackScroll for Project {
|
||||
fn track_scroll (&self) -> usize {
|
||||
self.track_scroll
|
||||
}
|
||||
fn tracks_last (&self) -> usize {
|
||||
self.track_last
|
||||
}
|
||||
fn tracks_last_mut (&mut self) -> &mut usize {
|
||||
&mut self.track_last
|
||||
}
|
||||
}
|
||||
|
||||
impl HasScenes for Project {
|
||||
fn scenes_scroll (&self) -> usize {
|
||||
self.scenes_scroll
|
||||
}
|
||||
fn scenes (&self) -> &Vec<Scene> {
|
||||
&self.scenes
|
||||
}
|
||||
fn scenes_mut (&mut self) -> &mut Vec<Scene> {
|
||||
&mut self.scenes
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Junction<T: JackPort>(T);
|
||||
|
||||
impl<T: JackPort> Draw<Tui> for Junction<T> {
|
||||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
T::KIND.draw(to)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the current user selection in the arranger
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
||||
pub enum Selection {
|
||||
#[default]
|
||||
/// Nothing is selected
|
||||
Nothing,
|
||||
/// The whole mix is selected
|
||||
Mix,
|
||||
/// A MIDI input is selected.
|
||||
Input(usize),
|
||||
/// A MIDI output is selected.
|
||||
Output(usize),
|
||||
/// A clip is selected.
|
||||
Clip(usize),
|
||||
/// A sample is selected.
|
||||
Sample(usize),
|
||||
/// A scene is selected.
|
||||
#[cfg(feature = "scene")] Scene(usize),
|
||||
/// A track is selected.
|
||||
#[cfg(feature = "track")] Track(usize),
|
||||
/// A clip slot (track × scene) is selected.
|
||||
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
|
||||
/// A track's MIDI input connection is selected.
|
||||
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
|
||||
/// A track's MIDI output connection is selected.
|
||||
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
|
||||
/// A track device slot is selected.
|
||||
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
|
||||
pub fn describe (
|
||||
&self,
|
||||
#[cfg(feature = "track")] tracks: &[Track],
|
||||
#[cfg(feature = "scene")] scenes: &[Scene],
|
||||
) -> Arc<str> {
|
||||
use Selection::*;
|
||||
format!("{}", match self {
|
||||
Mix => "Everything".to_string(),
|
||||
#[cfg(feature = "scene")] Scene(s) =>
|
||||
scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()),
|
||||
#[cfg(feature = "track")] 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.clips.get(*track) {
|
||||
None => format!("T{track} S{scene}: Empty"),
|
||||
Some(clip) => format!("T{track} S{scene} C{}",
|
||||
clip.as_ref().map(|clip|clip.read().name.clone()).unwrap_or_default()),
|
||||
},
|
||||
_ => format!("T{track} S{scene}: Empty"),
|
||||
},
|
||||
_ => todo!()
|
||||
}).into()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(feature = "scene")]
|
||||
impl Selection {
|
||||
|
||||
pub fn scene (&self) -> Option<usize> {
|
||||
use Selection::*;
|
||||
match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None }
|
||||
}
|
||||
|
||||
pub fn select_scene (&self, scene_count: usize) -> Self {
|
||||
use Selection::*;
|
||||
match self {
|
||||
Mix | Track(_) => Scene(0),
|
||||
Scene(s) => Scene((s + 1) % scene_count),
|
||||
TrackClip { scene, .. } => Track(*scene),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_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 select_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!()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")]
|
||||
impl Selection {
|
||||
|
||||
pub fn track (&self) -> Option<usize> {
|
||||
use Selection::*;
|
||||
if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self {
|
||||
Some(*track)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_track (&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 select_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 select_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!()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<T: HasSelection> SelectionModel for T {}
|
||||
|
||||
pub trait SelectionModel: HasSelection {
|
||||
/// Get the active track
|
||||
#[cfg(feature = "track")] fn selected_track (&self) -> Option<&Track> where Self: HasTracks {
|
||||
let index = self.selection().track()?;
|
||||
self.tracks().get(index)
|
||||
}
|
||||
/// Get a mutable reference to the active track
|
||||
#[cfg(feature = "track")] fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks {
|
||||
let index = self.selection().track()?;
|
||||
self.tracks_mut().get_mut(index)
|
||||
}
|
||||
/// Get the active scene
|
||||
#[cfg(feature = "scene")] fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes {
|
||||
let index = self.selection().scene()?;
|
||||
self.scenes().get(index)
|
||||
}
|
||||
/// Get a mutable reference to the active scene
|
||||
#[cfg(feature = "scene")] fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes {
|
||||
let index = self.selection().scene()?;
|
||||
self.scenes_mut().get_mut(index)
|
||||
}
|
||||
/// Get the active clip
|
||||
#[cfg(feature = "clip")] fn selected_clip (&self) -> Option<Slot<MidiClip>> where Self: HasScenes + HasTracks {
|
||||
self.selected_scene()?.clips.read().get(self.selection().track()?).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A scene consists of a set of clips to play together.
|
||||
///
|
||||
/// ```
|
||||
/// let scene: tek::Scene = Default::default();
|
||||
/// let _ = scene.pulses();
|
||||
/// let _ = scene.is_playing(&[]);
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Scene {
|
||||
/// Name of scene
|
||||
pub name: Arc<str>,
|
||||
/// Identifying color of scene
|
||||
pub color: ItemTheme,
|
||||
/// Clips in scene, one per track
|
||||
pub clips: Pool<MidiClip>,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
pub const DEFAULT_HEIGHT: usize = 2;
|
||||
/// Get pulse length of the longest clip in the scene
|
||||
pub fn pulses (&self) -> usize {
|
||||
self.clips.read().iter().fold(0, |a, p|{
|
||||
a.max(p.read().as_ref().map(|q|q.read().length).unwrap_or(0))
|
||||
})
|
||||
}
|
||||
/// True if all clips in scene are currently playing on given tracks.
|
||||
pub fn is_playing (&self, tracks: &[Track]) -> bool {
|
||||
// Not "playing" if there are no clips
|
||||
if !self.clips.read().iter().any(|clip|clip.read().is_some()) {
|
||||
return false
|
||||
}
|
||||
// Playing if at least 1 clip is playing
|
||||
self.clips.read().iter().enumerate().all(|(track_index, clip)|match &*clip.read() {
|
||||
Some(c) => tracks
|
||||
.get(track_index)
|
||||
.map(|track|{
|
||||
if let Some((_, Some(clip))) = track.sequencer().play_clip() {
|
||||
*clip.read() == *c.read()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false),
|
||||
None => true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
|
||||
856
src/tek.rs
856
src/tek.rs
|
|
@ -1,5 +1,18 @@
|
|||
#![allow(clippy::unit_arg)]
|
||||
|
||||
impl_as_mut!(Option<MidiEditor>: |self: Project| &mut self.editor);
|
||||
impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut());
|
||||
impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut());
|
||||
impl_as_ref!(Option<MidiEditor>: |self: Project| &self.editor);
|
||||
impl_as_ref_opt!(Scene: |self: Project| self.selected_scene());
|
||||
impl_as_ref_opt!(Track: |self: Project| self.selected_track());
|
||||
impl_has!(Clock: |self: Project| self.clock);
|
||||
impl_has!(Jack<'static>: |self: Project| self.jack);
|
||||
impl_has!(Selection: |self: Project| self.selection);
|
||||
impl_has!(Sizer: |self: Project| self.size);
|
||||
impl_has!(Vec<MidiInput>: |self: Project| self.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: Project| self.midi_outs);
|
||||
|
||||
impl_as_ref!(Config: |self: App|self.config.config.as_ref());
|
||||
impl_as_mut!(Option<MidiEditor>: |self: App|self.project.as_mut());
|
||||
impl_as_mut!(Option<Project>: |self: App|todo!());
|
||||
|
|
@ -46,14 +59,15 @@ macro_rules! def_sizes_iter {
|
|||
}
|
||||
}
|
||||
|
||||
mod conf; pub use self::conf::*;
|
||||
mod deps; pub use self::deps::*;
|
||||
mod pool; pub use self::pool::*;
|
||||
mod project; pub use self::project::*;
|
||||
mod sequence; pub use self::sequence::*;
|
||||
def_sizes_iter!(ScenesSizes => Scene);
|
||||
def_sizes_iter!(TracksSizes => Track);
|
||||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||||
|
||||
pub use self::device::*;
|
||||
mod device {
|
||||
pub use self::conf::*; mod conf;
|
||||
pub use self::deps::*; mod deps;
|
||||
pub use self::gear::*; mod gear {
|
||||
mod pool; pub use self::pool::*;
|
||||
mod midi; pub use self::midi::*;
|
||||
mod mixer; pub use self::mixer::*;
|
||||
mod sample; pub use self::sample::*;
|
||||
|
||||
|
|
@ -223,13 +237,69 @@ fn main_plain (config: Arc<ConfigWatcher>) -> Usually<()> {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to launch into new project from CLI.
|
||||
#[derive(Debug, Clone, Parser, Default)]
|
||||
pub struct ProjectInit {
|
||||
/// Name of JACK client
|
||||
#[arg(short='n', long)] name: Option<String>,
|
||||
/// Whether to attempt to become transport master
|
||||
#[arg(short='Y', long, default_value_t = false)] sync_lead: bool,
|
||||
/// Whether to sync to external transport master
|
||||
#[arg(short='y', long, default_value_t = true)] sync_follow: bool,
|
||||
/// Initial tempo in beats per minute
|
||||
#[arg(short='b', long, default_value = None)] bpm: Option<f64>,
|
||||
/// Whether to include a transport toolbar (default: true)
|
||||
#[arg(short='c', long, default_value_t = true)] show_clock: bool,
|
||||
/// MIDI outs to connect to (multiple instances accepted)
|
||||
#[arg(short='I', long)] midi_from: Vec<String>,
|
||||
/// MIDI outs to connect to (multiple instances accepted)
|
||||
#[arg(short='i', long)] midi_from_re: Vec<String>,
|
||||
/// MIDI ins to connect to (multiple instances accepted)
|
||||
#[arg(short='O', long)] midi_to: Vec<String>,
|
||||
/// MIDI ins to connect to (multiple instances accepted)
|
||||
#[arg(short='o', long)] midi_to_re: Vec<String>,
|
||||
/// Audio outs to connect to left input
|
||||
#[arg(short='l', long)] left_from: Vec<String>,
|
||||
/// Audio outs to connect to right input
|
||||
#[arg(short='r', long)] right_from: Vec<String>,
|
||||
/// Audio ins to connect from left output
|
||||
#[arg(short='L', long)] left_to: Vec<String>,
|
||||
/// Audio ins to connect from right output
|
||||
#[arg(short='R', long)] right_to: Vec<String>,
|
||||
/// Tracks to creat
|
||||
#[arg(short='t', long)] tracks: Option<usize>,
|
||||
/// Scenes to create
|
||||
#[arg(short='s', long)] scenes: Option<usize>,
|
||||
}
|
||||
|
||||
impl ProjectInit {
|
||||
pub fn init (&self) -> Usually<Project> {
|
||||
let name = self.name.as_ref().map_or("tek", |x|x.as_str());
|
||||
let jack = Jack::new(&name)?;
|
||||
let midi_ins = connect_midi_ins(&jack, &"M".to_string(), &self.midi_from, Some(&self.midi_from_re))?;
|
||||
let midi_out = connect_midi_outs(&jack, &"M".to_string(), &self.midi_to, Some(&self.midi_to_re))?;
|
||||
let inputs = [].into_iter()
|
||||
.chain(connect_audio_ins(&jack, &"L".to_string(), &self.left_from, None)?.into_iter())
|
||||
.chain(connect_audio_ins(&jack, &"R".to_string(), &self.right_from, None)?.into_iter());
|
||||
let outputs = [].into_iter()
|
||||
.chain(connect_audio_outs(&jack, &"L".to_string(), &self.left_to, None)?.into_iter())
|
||||
.chain(connect_audio_outs(&jack, &"R".to_string(), &self.right_to, None)?.into_iter());
|
||||
let mut proj = Project::create(&jack, name.into(), Clock::new(&jack, self.bpm)?,
|
||||
[].into_iter(), [].into_iter(), midi_ins.into_iter(), midi_out.into_iter(),
|
||||
inputs, outputs);
|
||||
proj.tracks_add_many(self.tracks.unwrap_or(0), None, [].into(), [].into())?;
|
||||
proj.scenes_add_many(self.scenes.unwrap_or(0))?;
|
||||
Ok(proj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Total application state.
|
||||
///
|
||||
/// ```
|
||||
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView, ScenesControl};
|
||||
/// let mut app = tek::App::default();
|
||||
/// # use tek::*;
|
||||
/// let mut app = App::default();
|
||||
/// let _ = app.scenes_add_one(None, None).unwrap();
|
||||
/// let _ = app.update_clock();
|
||||
/// app.project.editor = Some(Default::default());
|
||||
|
|
@ -249,34 +319,34 @@ fn main_plain (config: Arc<ConfigWatcher>) -> Usually<()> {
|
|||
/// let _ = app.project.h_scenes();
|
||||
/// ```
|
||||
#[derive(Default, Debug)]
|
||||
#[namespace(Arc<[Connect]> get_arc_array_connect)]
|
||||
#[namespace(Arc<str> get_arc_str)]
|
||||
#[namespace(Color get_color)]
|
||||
#[namespace(ControlAxis ControlAxis::get_axis)]
|
||||
#[namespace(Dialog Dialog::get_dialog)]
|
||||
#[namespace(ItemColor)]
|
||||
#[namespace(ItemTheme)]
|
||||
#[namespace(MidiClip)]
|
||||
#[namespace(Arc<[Connect]> tek_arc_array_connect)]
|
||||
#[namespace(Arc<str> tek_arc_str)]
|
||||
#[namespace(Color tek_color)]
|
||||
#[namespace(Option<Arc<[Connect]>> tek_opt_arc_array_connect)]
|
||||
#[namespace(Option<Arc<str>> tek_opt_arc_str)]
|
||||
#[namespace(Option<ItemTheme> tek_opt_itemtheme)]
|
||||
#[namespace(Option<bool> tek_opt_bool)]
|
||||
#[namespace(Option<u16> tek_opt_u16)]
|
||||
#[namespace(Option<u32> tek_opt_u32)]
|
||||
#[namespace(Option<usize> tek_opt_usize)]
|
||||
#[namespace(PathBuf tek_path_buf)]
|
||||
#[namespace(Vec<Arc<str>> tek_vec_arc_str)]
|
||||
#[namespace(bool tek_bool)]
|
||||
#[namespace(f64 tek_f64)]
|
||||
#[namespace(u16 tek_u16)]
|
||||
#[namespace(usize tek_usize)]
|
||||
#[namespace(ControlAxis ControlAxis::eval)]
|
||||
#[namespace(Dialog Dialog::eval)]
|
||||
#[namespace(ClipSlot ClipSlot::eval)]
|
||||
#[namespace(ClipPool ClipPool::eval)]
|
||||
#[namespace(SampleSlot SampleSlot::eval)]
|
||||
#[namespace(SamplePool SamplePool::eval)]
|
||||
#[namespace(Option<Arc<[Connect]>> get_opt_arc_array_connect)]
|
||||
#[namespace(Option<Arc<str>> get_opt_arc_str)]
|
||||
#[namespace(Option<ItemTheme> get_opt_itemtheme)]
|
||||
#[namespace(Option<bool> get_opt_bool)]
|
||||
#[namespace(Option<u16> get_opt_u16)]
|
||||
#[namespace(Option<u32> get_opt_u32)]
|
||||
#[namespace(Option<usize> get_opt_usize)]
|
||||
#[namespace(PathBuf get_path_buf)]
|
||||
#[namespace(Vec<Arc<str>> get_vec_arc_str)]
|
||||
#[namespace(bool get_bool)]
|
||||
#[namespace(f64 get_f64)]
|
||||
#[namespace(ItemColor)]
|
||||
#[namespace(ItemTheme)]
|
||||
#[namespace(MidiClip)]
|
||||
#[namespace(isize)]
|
||||
#[namespace(u16 get_u16)]
|
||||
#[namespace(u32)]
|
||||
#[namespace(u8)]
|
||||
#[namespace(usize get_usize)]
|
||||
pub struct App {
|
||||
/// Exit flag
|
||||
pub exit: Exit,
|
||||
|
|
@ -346,13 +416,6 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
impl HasJack<'static> for App {
|
||||
fn jack (&self) -> &Jack<'static> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control {
|
||||
let t0 = state.perf.get_t0();
|
||||
state.clock().update_from_scope(scope).unwrap();
|
||||
|
|
@ -400,174 +463,491 @@ fn tek_jack_event (state: &mut App, event: JackEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps<Arc<str>> {
|
||||
Ok(src.src()?.map(|x|x.into()))
|
||||
/// Arranger.
|
||||
///
|
||||
/// ```
|
||||
/// let arranger = tek::Project::default();
|
||||
/// ```
|
||||
#[namespace(ClipPool ClipPool::eval)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Project {
|
||||
/// JACK client handle.
|
||||
pub jack: Jack<'static>,
|
||||
/// Project name.
|
||||
pub name: Arc<str>,
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// FIXME a render of the project arrangement, redrawn on update.
|
||||
/// TODO rename to "render_cache" or smth
|
||||
pub arranger: Arc<RwLock<Buffer>>,
|
||||
/// Display size
|
||||
pub size: Sizer,
|
||||
/// Display size of clips area
|
||||
pub size_inner: Sizer,
|
||||
/// Selected UI element
|
||||
pub selection: Selection,
|
||||
/// Contains all recently created clips.
|
||||
#[cfg(feature = "sequencer")] pub clips: Pool<MidiClip>,
|
||||
/// Contains all recently created clips.
|
||||
#[cfg(feature = "sampler")] pub samples: Pool<Sample>,
|
||||
/// Source of time
|
||||
#[cfg(feature = "clock")] pub clock: Clock,
|
||||
/// Allows one MIDI clip to be edited
|
||||
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
|
||||
/// List of global midi inputs
|
||||
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
||||
/// List of global midi outputs
|
||||
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
||||
/// List of global audio inputs
|
||||
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
||||
/// List of global audio outputs
|
||||
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
||||
/// Last track number (to avoid duplicate port names)
|
||||
#[cfg(feature = "track")] pub track_last: usize,
|
||||
/// List of tracks
|
||||
#[cfg(feature = "track")] pub tracks: Vec<Track>,
|
||||
/// Scroll offset of tracks
|
||||
#[cfg(feature = "track")] pub track_scroll: usize,
|
||||
/// List of scenes
|
||||
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
|
||||
/// Scroll offset of scenes
|
||||
#[cfg(feature = "scene")] pub scenes_scroll: usize,
|
||||
}
|
||||
|
||||
fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps<u16> {
|
||||
Ok(Some(match src.word()? {
|
||||
Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()),
|
||||
Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9),
|
||||
_ => return try_to_u16(src)
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps<usize> {
|
||||
Ok(Some(match src.word()? {
|
||||
Some(":scene-count") => state.scenes().len(),
|
||||
Some(":track-count") => state.tracks().len(),
|
||||
Some(":device-kind") => state.dialog.device_kind().unwrap_or(0),
|
||||
Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0),
|
||||
Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0),
|
||||
_ => return try_to_usize(src)
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps<f64> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
"Y" => true,
|
||||
"N" => false,
|
||||
":mode/editor" => state.project.editor.is_some(),
|
||||
":focused/dialog" => !matches!(state.dialog, Dialog::None),
|
||||
":focused/message" => matches!(state.dialog, Dialog::Message(..)),
|
||||
":focused/add_device" => matches!(state.dialog, Dialog::Device(..)),
|
||||
":focused/browser" => state.dialog.browser().is_some(),
|
||||
//":focused/pool/import" => matches!(state.project.pool.mode, Some(PoolMode::Import(..))),
|
||||
//":focused/pool/export" => matches!(state.project.pool.mode, Some(PoolMode::Export(..))),
|
||||
//":focused/pool/rename" => matches!(state.project.pool.mode, Some(PoolMode::Rename(..))),
|
||||
//":focused/pool/length" => matches!(state.project.pool.mode, Some(PoolMode::Length(..))),
|
||||
":focused/clip" => matches!(state.selection(), Selection::TrackClip{..}),
|
||||
":focused/track" => matches!(state.selection(), Selection::Track(..)),
|
||||
":focused/scene" => matches!(state.selection(), Selection::Scene(..)),
|
||||
":focused/mix" => matches!(state.selection(), Selection::Mix),
|
||||
_ => return err!("not bool: {word}")
|
||||
})).transpose()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps<Selection> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":select/scene" => state.selection().select_scene(state.tracks().len()),
|
||||
":select/scene/next" => state.selection().select_scene_next(state.scenes().len()),
|
||||
":select/scene/prev" => state.selection().select_scene_prev(),
|
||||
":select/track" => state.selection().select_track(state.tracks().len()),
|
||||
":select/track/next" => state.selection().select_track_next(state.tracks().len()),
|
||||
":select/track/prev" => state.selection().select_track_prev(),
|
||||
_ => return err!("not selection: {word}")
|
||||
})).transpose()
|
||||
}
|
||||
|
||||
fn get_color <'a> (_: &App, src: impl Language) -> Perhaps<Color> {
|
||||
if let Some(expr) = src.expr()? {
|
||||
match (expr.head()?, expr.tail()?) {
|
||||
(Some("g"), Some(tail)) => {
|
||||
let n = try_to_u8(tail.head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not gray"))?;
|
||||
Ok(Some(Color::Rgb(n, n, n)))
|
||||
},
|
||||
(Some("rgb"), Some(tail)) => {
|
||||
let r = try_to_u8(tail.head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not red"))?;
|
||||
let g = try_to_u8(tail.tail().head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not green"))?;
|
||||
let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not blue"))?;
|
||||
Ok(Some(Color::Rgb(r, g, b)))
|
||||
},
|
||||
(Some(_), _) => return err!("not a color expression: {expr}"),
|
||||
(None, _) => return err!("not a color expression: {expr}"),
|
||||
impl Project {
|
||||
pub fn new (jack: &Jack<'static>) -> Self {
|
||||
Self {
|
||||
jack: jack.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
} else if let Ok(Some(sym)) = src.word() {
|
||||
Ok(match sym {
|
||||
":color/bg" => Some(Color::Rgb(28, 32, 36)),
|
||||
":color/fg" => Some(Color::Rgb(98, 92, 96)),
|
||||
_ => return err!("not a color: {sym}")
|
||||
}
|
||||
|
||||
pub fn new_jack (name: impl AsRef<str>) -> Usually<Self> {
|
||||
let name = name.as_ref();
|
||||
Ok(Self {
|
||||
name: name.into(),
|
||||
jack: Jack::new(name)?,
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
return err!("not a color: {:?}", src.src()?)
|
||||
}
|
||||
|
||||
/// Create a new arrangement.
|
||||
pub fn create (
|
||||
jack: &Jack<'static>,
|
||||
name: Arc<str>,
|
||||
clock: Clock,
|
||||
tracks: impl Iterator<Item = Track>,
|
||||
scenes: impl Iterator<Item = Scene>,
|
||||
midi_ins: impl Iterator<Item = MidiInput>,
|
||||
midi_outs: impl Iterator<Item = MidiOutput>,
|
||||
audio_ins: impl Iterator<Item = AudioInput>,
|
||||
audio_outs: impl Iterator<Item = AudioOutput>,
|
||||
) -> Self {
|
||||
Self {
|
||||
jack: jack.clone(),
|
||||
color: ItemTheme::random(),
|
||||
selection: Selection::TrackClip { track: 0, scene: 0 },
|
||||
tracks: tracks.collect(),
|
||||
scenes: scenes.collect(),
|
||||
midi_ins: midi_ins.collect(),
|
||||
midi_outs: midi_outs.collect(),
|
||||
audio_ins: audio_ins.collect(),
|
||||
audio_outs: audio_outs.collect(),
|
||||
size_inner: Sizer(Arc::new(40.into()), Arc::new(25.into())),
|
||||
clock,
|
||||
name,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Width of display
|
||||
pub fn w (&self) -> u16 {
|
||||
self.size.w() as u16
|
||||
}
|
||||
|
||||
/// Width allocated for sidebar.
|
||||
pub fn w_sidebar (&self, is_editing: bool) -> u16 {
|
||||
self.w() / if is_editing { 16 } else { 8 } as u16
|
||||
}
|
||||
|
||||
/// Width available to display tracks.
|
||||
fn w_tracks_area (&self, is_editing: bool) -> u16 {
|
||||
self.w().saturating_sub(self.w_sidebar(is_editing))
|
||||
}
|
||||
|
||||
/// Height of display
|
||||
pub fn h (&self) -> u16 {
|
||||
self.size.h() as u16
|
||||
}
|
||||
|
||||
/// Height taken by visible device slots.
|
||||
fn h_devices (&self) -> u16 {
|
||||
2
|
||||
//1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps<Option<u7>> {
|
||||
eval_word(src, |word|Ok(match word {
|
||||
|
||||
":editor/pitch" => Some((
|
||||
state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
|
||||
).into()),
|
||||
|
||||
_ => return err!("unknown midi note: {word}")
|
||||
|
||||
}))
|
||||
impl HasJack<'static> for App {
|
||||
fn jack (&self) -> &Jack<'static> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps<Option<usize>> {
|
||||
eval_word(src, |word|Ok(match word {
|
||||
|
||||
":selected/scene" => state.selection().scene(),
|
||||
":selected/track" => state.selection().track(),
|
||||
|
||||
_ => return err!("unknown opt<usize>: {word}")
|
||||
|
||||
}))
|
||||
impl HasJack<'static> for Project {
|
||||
fn jack (&self) -> &Jack<'static> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
|
||||
Ok(None)
|
||||
impl ScenesView for Project {
|
||||
fn h_scenes (&self) -> u16 {
|
||||
(self.size.h() as u16).saturating_sub(20)
|
||||
}
|
||||
fn w_side (&self) -> u16 {
|
||||
(self.size.w() as u16 * 2 / 10).max(20)
|
||||
}
|
||||
fn w_mid (&self) -> u16 {
|
||||
(self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u32>> {
|
||||
Ok(None)
|
||||
pub fn swap_value <T: Clone + PartialEq, U> (
|
||||
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||||
) -> Perhaps<U> {
|
||||
if *target == *value {
|
||||
Ok(None)
|
||||
} else {
|
||||
let mut value = value.clone();
|
||||
std::mem::swap(target, &mut value);
|
||||
Ok(Some(returned(value)))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
|
||||
todo!()
|
||||
pub fn toggle_bool <U> (
|
||||
target: &mut bool, value: &Option<bool>, returned: impl Fn(Option<bool>)->U
|
||||
) -> Perhaps<U> {
|
||||
let mut value = value.unwrap_or(!*target);
|
||||
if value == *target {
|
||||
Ok(None)
|
||||
} else {
|
||||
std::mem::swap(target, &mut value);
|
||||
Ok(Some(returned(Some(value))))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<[Connect]>>> {
|
||||
todo!()
|
||||
/// Modes for clip pool
|
||||
#[derive(Debug, Clone)] pub enum PoolMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, Arc<str>),
|
||||
/// Editing the length of a pattern
|
||||
Length(usize, usize, ClipLengthFocus),
|
||||
/// Load clip from disk
|
||||
Import(usize, Browse),
|
||||
/// Save clip to disk
|
||||
Export(usize, Browse),
|
||||
}
|
||||
|
||||
fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Arc<[Connect]>> {
|
||||
todo!()
|
||||
/// Focused field of `ClipLength`
|
||||
#[derive(Copy, Clone, Debug)] pub enum ClipLengthFocus {
|
||||
/// Editing the number of bars
|
||||
Bar,
|
||||
/// Editing the number of beats
|
||||
Beat,
|
||||
/// Editing the number of ticks
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<str>>> {
|
||||
todo!()
|
||||
impl ClipLengthFocus {
|
||||
pub fn next (&mut self) {
|
||||
use ClipLengthFocus::*;
|
||||
*self = match self { Bar => Beat, Beat => Tick, Tick => Bar, }
|
||||
}
|
||||
pub fn prev (&mut self) {
|
||||
use ClipLengthFocus::*;
|
||||
*self = match self { Bar => Tick, Beat => Bar, Tick => Beat, }
|
||||
}
|
||||
}
|
||||
|
||||
fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps<Option<bool>> {
|
||||
todo!()
|
||||
/// Displays and edits clip length.
|
||||
#[derive(Clone, Debug, Default)] pub struct ClipLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of clip in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<ClipLengthFocus>,
|
||||
}
|
||||
|
||||
fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps<PathBuf> {
|
||||
todo!()
|
||||
impl ClipLength {
|
||||
pub fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
|
||||
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||
}
|
||||
pub fn bars (&self) -> usize {
|
||||
self.pulses / (self.bpb * self.ppq)
|
||||
}
|
||||
pub fn beats (&self) -> usize {
|
||||
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||
}
|
||||
pub fn ticks (&self) -> usize {
|
||||
self.pulses % self.ppq
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Vec<Arc<str>>> {
|
||||
todo!()
|
||||
impl HasScenes for Project {
|
||||
fn scenes_scroll (&self) -> usize {
|
||||
self.scenes_scroll
|
||||
}
|
||||
fn scenes (&self) -> &Vec<Scene> {
|
||||
&self.scenes
|
||||
}
|
||||
fn scenes_mut (&mut self) -> &mut Vec<Scene> {
|
||||
&mut self.scenes
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Junction<T: JackPort>(T);
|
||||
|
||||
impl<T: JackPort> Draw<Tui> for Junction<T> {
|
||||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
T::KIND.draw(to)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the current user selection in the arranger
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
||||
pub enum Selection {
|
||||
#[default]
|
||||
/// Nothing is selected
|
||||
Nothing,
|
||||
/// The whole mix is selected
|
||||
Mix,
|
||||
/// A MIDI input is selected.
|
||||
Input(usize),
|
||||
/// A MIDI output is selected.
|
||||
Output(usize),
|
||||
/// A clip is selected.
|
||||
Clip(usize),
|
||||
/// A sample is selected.
|
||||
Sample(usize),
|
||||
/// A scene is selected.
|
||||
#[cfg(feature = "scene")] Scene(usize),
|
||||
/// A track is selected.
|
||||
#[cfg(feature = "track")] Track(usize),
|
||||
/// A clip slot (track × scene) is selected.
|
||||
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
|
||||
/// A track's MIDI input connection is selected.
|
||||
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
|
||||
/// A track's MIDI output connection is selected.
|
||||
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
|
||||
/// A track device slot is selected.
|
||||
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
|
||||
pub fn describe (
|
||||
&self,
|
||||
#[cfg(feature = "track")] tracks: &[Track],
|
||||
#[cfg(feature = "scene")] scenes: &[Scene],
|
||||
) -> Arc<str> {
|
||||
use Selection::*;
|
||||
format!("{}", match self {
|
||||
Mix => "Everything".to_string(),
|
||||
#[cfg(feature = "scene")] Scene(s) =>
|
||||
scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()),
|
||||
#[cfg(feature = "track")] 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.clips.get(*track) {
|
||||
None => format!("T{track} S{scene}: Empty"),
|
||||
Some(clip) => format!("T{track} S{scene} C{}",
|
||||
clip.as_ref().map(|clip|clip.read().name.clone()).unwrap_or_default()),
|
||||
},
|
||||
_ => format!("T{track} S{scene}: Empty"),
|
||||
},
|
||||
_ => todo!()
|
||||
}).into()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(feature = "scene")]
|
||||
impl Selection {
|
||||
|
||||
pub fn scene (&self) -> Option<usize> {
|
||||
use Selection::*;
|
||||
match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None }
|
||||
}
|
||||
|
||||
pub fn select_scene (&self, scene_count: usize) -> Self {
|
||||
use Selection::*;
|
||||
match self {
|
||||
Mix | Track(_) => Scene(0),
|
||||
Scene(s) => Scene((s + 1) % scene_count),
|
||||
TrackClip { scene, .. } => Track(*scene),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_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 select_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!()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")]
|
||||
impl Selection {
|
||||
|
||||
pub fn track (&self) -> Option<usize> {
|
||||
use Selection::*;
|
||||
if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self {
|
||||
Some(*track)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_track (&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 select_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 select_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!()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<T: HasSelection> SelectionModel for T {}
|
||||
|
||||
pub trait SelectionModel: HasSelection {
|
||||
/// Get the active track
|
||||
#[cfg(feature = "track")] fn selected_track (&self) -> Option<&Track> where Self: HasTracks {
|
||||
let index = self.selection().track()?;
|
||||
self.tracks().get(index)
|
||||
}
|
||||
/// Get a mutable reference to the active track
|
||||
#[cfg(feature = "track")] fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks {
|
||||
let index = self.selection().track()?;
|
||||
self.tracks_mut().get_mut(index)
|
||||
}
|
||||
/// Get the active scene
|
||||
#[cfg(feature = "scene")] fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes {
|
||||
let index = self.selection().scene()?;
|
||||
self.scenes().get(index)
|
||||
}
|
||||
/// Get a mutable reference to the active scene
|
||||
#[cfg(feature = "scene")] fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes {
|
||||
let index = self.selection().scene()?;
|
||||
self.scenes_mut().get_mut(index)
|
||||
}
|
||||
/// Get the active clip
|
||||
#[cfg(feature = "clip")] fn selected_clip (&self) -> Option<Slot<MidiClip>> where Self: HasScenes + HasTracks {
|
||||
self.selected_scene()?.clips.read().get(self.selection().track()?).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A scene consists of a set of clips to play together.
|
||||
///
|
||||
/// ```
|
||||
/// let scene: tek::Scene = Default::default();
|
||||
/// let _ = scene.pulses();
|
||||
/// let _ = scene.is_playing(&[]);
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Scene {
|
||||
/// Name of scene
|
||||
pub name: Arc<str>,
|
||||
/// Identifying color of scene
|
||||
pub color: ItemTheme,
|
||||
/// Clips in scene, one per track
|
||||
pub clips: Pool<MidiClip>,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
pub const DEFAULT_HEIGHT: usize = 2;
|
||||
/// Get pulse length of the longest clip in the scene
|
||||
pub fn pulses (&self) -> usize {
|
||||
self.clips.read().iter().fold(0, |a, p|{
|
||||
a.max(p.read().as_ref().map(|q|q.read().length).unwrap_or(0))
|
||||
})
|
||||
}
|
||||
/// True if all clips in scene are currently playing on given tracks.
|
||||
pub fn is_playing (&self, tracks: &[Track]) -> bool {
|
||||
// Not "playing" if there are no clips
|
||||
if !self.clips.read().iter().any(|clip|clip.read().is_some()) {
|
||||
return false
|
||||
}
|
||||
// Playing if at least 1 clip is playing
|
||||
self.clips.read().iter().enumerate().all(|(track_index, clip)|match &*clip.read() {
|
||||
Some(c) => tracks
|
||||
.get(track_index)
|
||||
.map(|track|{
|
||||
if let Some((_, Some(clip))) = track.sequencer().play_clip() {
|
||||
*clip.read() == *c.read()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false),
|
||||
None => true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
|
||||
|
||||
impl HasModeStack for App {
|
||||
fn mode_stack (&self) -> &RwLock<Vec<Arc<str>>> {
|
||||
&self.modes
|
||||
}
|
||||
}
|
||||
impl HasClip for App {
|
||||
fn clip_slot (&self) -> Option<Slot<MidiClip>> {
|
||||
self.project.clip_slot()
|
||||
}
|
||||
}
|
||||
impl HasClipsSize for App {
|
||||
fn clips_size (&self) -> &Sizer {
|
||||
&self.project.size_inner
|
||||
}
|
||||
}
|
||||
impl ScenesView for App {
|
||||
fn w_mid (&self) -> u16 {
|
||||
(self.size.w() as u16).saturating_sub(self.w_side())
|
||||
|
|
@ -590,17 +970,6 @@ impl HasScenes for App {
|
|||
self.project.scenes_mut()
|
||||
}
|
||||
}
|
||||
impl HasTrackScroll for App {
|
||||
fn track_scroll (&self) -> usize {
|
||||
self.project.track_scroll()
|
||||
}
|
||||
fn tracks_last (&self) -> usize {
|
||||
self.project.track_last
|
||||
}
|
||||
fn tracks_last_mut (&mut self) -> &mut usize {
|
||||
&mut self.project.track_last
|
||||
}
|
||||
}
|
||||
|
||||
/// Various possible dialog modes.
|
||||
///
|
||||
|
|
@ -619,7 +988,7 @@ impl HasTrackScroll for App {
|
|||
|
||||
impl Dialog {
|
||||
|
||||
fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
|
||||
fn eval <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
|
||||
|
||||
eval_word(&src, |word|Ok(match word {
|
||||
|
||||
|
|
@ -1184,6 +1553,39 @@ pub trait HasTrackScroll {
|
|||
fn tracks_last (&self) -> usize;
|
||||
fn tracks_last_mut (&mut self) -> &mut usize;
|
||||
}
|
||||
impl HasTrackScroll for &mut Project {
|
||||
fn track_scroll (&self) -> usize {
|
||||
self.track_scroll
|
||||
}
|
||||
fn tracks_last (&self) -> usize {
|
||||
self.track_last
|
||||
}
|
||||
fn tracks_last_mut (&mut self) -> &mut usize {
|
||||
&mut self.track_last
|
||||
}
|
||||
}
|
||||
impl HasTrackScroll for Project {
|
||||
fn track_scroll (&self) -> usize {
|
||||
self.track_scroll
|
||||
}
|
||||
fn tracks_last (&self) -> usize {
|
||||
self.track_last
|
||||
}
|
||||
fn tracks_last_mut (&mut self) -> &mut usize {
|
||||
&mut self.track_last
|
||||
}
|
||||
}
|
||||
impl HasTrackScroll for App {
|
||||
fn track_scroll (&self) -> usize {
|
||||
self.project.track_scroll()
|
||||
}
|
||||
fn tracks_last (&self) -> usize {
|
||||
self.project.track_last
|
||||
}
|
||||
fn tracks_last_mut (&mut self) -> &mut usize {
|
||||
&mut self.project.track_last
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasSampler: AsRef<Sampler> + AsMut<Sampler> {
|
||||
fn sampler (&self) -> &Sampler {
|
||||
|
|
@ -1203,6 +1605,23 @@ pub trait HasClip {
|
|||
self.clip_slot().map(|slot|slot.write())
|
||||
}
|
||||
}
|
||||
impl HasClip for App {
|
||||
fn clip_slot (&self) -> Option<Slot<MidiClip>> {
|
||||
self.project.clip_slot()
|
||||
}
|
||||
}
|
||||
impl HasClip for Project {
|
||||
fn clip_slot (&self) -> Option<Slot<MidiClip>> {
|
||||
match self.selection {
|
||||
Selection::Clip(index) =>
|
||||
Some(self.clips.slot(index)),
|
||||
Selection::TrackClip { track, scene } if let Some(scene) = self.scenes.get(scene) =>
|
||||
Some(scene.clips.slot(track)),
|
||||
_ =>
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasWidth {
|
||||
const MIN_WIDTH: usize;
|
||||
|
|
@ -1224,6 +1643,47 @@ pub trait HasClipsSize {
|
|||
)
|
||||
}
|
||||
}
|
||||
impl HasClipsSize for App {
|
||||
fn clips_size (&self) -> &Sizer {
|
||||
&self.project.size_inner
|
||||
}
|
||||
}
|
||||
impl HasClipsSize for Project {
|
||||
fn clips_size (&self) -> &Sizer {
|
||||
&self.size_inner
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn print_status (project: &Project) {
|
||||
println!("Name: {:?}", &project.name);
|
||||
println!("JACK: {:?}", &project.jack);
|
||||
println!("Buffer: {:?}", &project.clock.chunk);
|
||||
println!("Sample rate: {:?}", &project.clock.timebase.sr);
|
||||
println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq);
|
||||
println!("Tempo: {:?}", &project.clock.timebase.bpm);
|
||||
println!("Quantize: {:?}", &project.clock.quant);
|
||||
println!("Launch: {:?}", &project.clock.sync);
|
||||
println!("Playhead: {:?}us", &project.clock.playhead.usec);
|
||||
println!("Playhead: {:?}s", &project.clock.playhead.sample);
|
||||
println!("Playhead: {:?}p", &project.clock.playhead.pulse);
|
||||
println!("Started: {:?}", &project.clock.started);
|
||||
println!("Tracks:");
|
||||
for (i, t) in project.tracks.iter().enumerate() {
|
||||
println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width,
|
||||
&t.sequencer.play_clip, &t.sequencer.next_clip);
|
||||
}
|
||||
println!("Scenes:");
|
||||
for (i, t) in project.scenes.iter().enumerate() {
|
||||
println!(" Scene {i}: {} {:?}", &t.name, &t.clips);
|
||||
}
|
||||
println!("MIDI Ins: {:?}", &project.midi_ins);
|
||||
println!("MIDI Outs: {:?}", &project.midi_outs);
|
||||
println!("Audio Ins: {:?}", &project.audio_ins);
|
||||
println!("Audio Outs: {:?}", &project.audio_outs);
|
||||
// TODO git integration
|
||||
// TODO dawvert integration
|
||||
}
|
||||
|
||||
tui_keys!(self: App, input {
|
||||
#[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
|
||||
|
|
@ -2022,33 +2482,3 @@ pub enum MidiOutputCmd {
|
|||
Close,
|
||||
Connect(Arc<str>),
|
||||
}
|
||||
|
||||
pub fn print_status (project: &Project) {
|
||||
println!("Name: {:?}", &project.name);
|
||||
println!("JACK: {:?}", &project.jack);
|
||||
println!("Buffer: {:?}", &project.clock.chunk);
|
||||
println!("Sample rate: {:?}", &project.clock.timebase.sr);
|
||||
println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq);
|
||||
println!("Tempo: {:?}", &project.clock.timebase.bpm);
|
||||
println!("Quantize: {:?}", &project.clock.quant);
|
||||
println!("Launch: {:?}", &project.clock.sync);
|
||||
println!("Playhead: {:?}us", &project.clock.playhead.usec);
|
||||
println!("Playhead: {:?}s", &project.clock.playhead.sample);
|
||||
println!("Playhead: {:?}p", &project.clock.playhead.pulse);
|
||||
println!("Started: {:?}", &project.clock.started);
|
||||
println!("Tracks:");
|
||||
for (i, t) in project.tracks.iter().enumerate() {
|
||||
println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width,
|
||||
&t.sequencer.play_clip, &t.sequencer.next_clip);
|
||||
}
|
||||
println!("Scenes:");
|
||||
for (i, t) in project.scenes.iter().enumerate() {
|
||||
println!(" Scene {i}: {} {:?}", &t.name, &t.clips);
|
||||
}
|
||||
println!("MIDI Ins: {:?}", &project.midi_ins);
|
||||
println!("MIDI Outs: {:?}", &project.midi_outs);
|
||||
println!("Audio Ins: {:?}", &project.audio_ins);
|
||||
println!("Audio Outs: {:?}", &project.audio_outs);
|
||||
// TODO git integration
|
||||
// TODO dawvert integration
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue