got gear (m8)

This commit is contained in:
i do not exist 2026-10-04 23:32:19 +03:00
parent a355e482c7
commit b050e0e014
14 changed files with 799 additions and 809 deletions

View file

@ -8,6 +8,7 @@ impl_as_mut!(Config: |self: ConfigWatcher| &mut self.config);
impl_as_ref!(ConfigWatcher: |self: ConfigWatcher| &self); impl_as_ref!(ConfigWatcher: |self: ConfigWatcher| &self);
mod bind; pub use self::bind::*; mod bind; pub use self::bind::*;
mod lang; pub use self::lang::*;
mod mode; pub use self::mode::*; mod mode; pub use self::mode::*;
mod view; pub use self::view::*; mod view; pub use self::view::*;

154
src/conf/lang.rs Normal file
View 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!()
}

View file

@ -115,7 +115,7 @@ impl Mode {
} }
impl ControlAxis { 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()))) eval_word(src, |word|Ok(ControlAxis(word.into())))
} }
} }

View file

@ -1,6 +1,5 @@
use crate::*; use crate::*;
def_sizes_iter!(TracksSizes => Track);
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut()); impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref()); impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref());
impl_has!(Sequencer: |self: Track| self.sequencer); impl_has!(Sequencer: |self: Track| self.sequencer);

View file

@ -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);

View file

@ -1,5 +1,18 @@
#![allow(clippy::unit_arg)] #![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_ref!(Config: |self: App|self.config.config.as_ref());
impl_as_mut!(Option<MidiEditor>: |self: App|self.project.as_mut()); impl_as_mut!(Option<MidiEditor>: |self: App|self.project.as_mut());
impl_as_mut!(Option<Project>: |self: App|todo!()); impl_as_mut!(Option<Project>: |self: App|todo!());
@ -46,14 +59,15 @@ macro_rules! def_sizes_iter {
} }
} }
mod conf; pub use self::conf::*; def_sizes_iter!(ScenesSizes => Scene);
mod deps; pub use self::deps::*; def_sizes_iter!(TracksSizes => Track);
mod pool; pub use self::pool::*; def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
mod project; pub use self::project::*;
mod sequence; pub use self::sequence::*;
pub use self::device::*; pub use self::conf::*; mod conf;
mod device { 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 mixer; pub use self::mixer::*;
mod sample; pub use self::sample::*; mod sample; pub use self::sample::*;
@ -223,13 +237,69 @@ fn main_plain (config: Arc<ConfigWatcher>) -> Usually<()> {
Ok(()) 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. /// Total application state.
/// ///
/// ``` /// ```
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView, ScenesControl}; /// # use tek::*;
/// let mut app = tek::App::default(); /// let mut app = App::default();
/// let _ = app.scenes_add_one(None, None).unwrap(); /// let _ = app.scenes_add_one(None, None).unwrap();
/// let _ = app.update_clock(); /// let _ = app.update_clock();
/// app.project.editor = Some(Default::default()); /// app.project.editor = Some(Default::default());
@ -249,34 +319,34 @@ fn main_plain (config: Arc<ConfigWatcher>) -> Usually<()> {
/// let _ = app.project.h_scenes(); /// let _ = app.project.h_scenes();
/// ``` /// ```
#[derive(Default, Debug)] #[derive(Default, Debug)]
#[namespace(Arc<[Connect]> get_arc_array_connect)] #[namespace(Arc<[Connect]> tek_arc_array_connect)]
#[namespace(Arc<str> get_arc_str)] #[namespace(Arc<str> tek_arc_str)]
#[namespace(Color get_color)] #[namespace(Color tek_color)]
#[namespace(ControlAxis ControlAxis::get_axis)] #[namespace(Option<Arc<[Connect]>> tek_opt_arc_array_connect)]
#[namespace(Dialog Dialog::get_dialog)] #[namespace(Option<Arc<str>> tek_opt_arc_str)]
#[namespace(ItemColor)] #[namespace(Option<ItemTheme> tek_opt_itemtheme)]
#[namespace(ItemTheme)] #[namespace(Option<bool> tek_opt_bool)]
#[namespace(MidiClip)] #[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(ClipSlot ClipSlot::eval)]
#[namespace(ClipPool ClipPool::eval)] #[namespace(ClipPool ClipPool::eval)]
#[namespace(SampleSlot SampleSlot::eval)] #[namespace(SampleSlot SampleSlot::eval)]
#[namespace(SamplePool SamplePool::eval)] #[namespace(SamplePool SamplePool::eval)]
#[namespace(Option<Arc<[Connect]>> get_opt_arc_array_connect)] #[namespace(ItemColor)]
#[namespace(Option<Arc<str>> get_opt_arc_str)] #[namespace(ItemTheme)]
#[namespace(Option<ItemTheme> get_opt_itemtheme)] #[namespace(MidiClip)]
#[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(isize)] #[namespace(isize)]
#[namespace(u16 get_u16)]
#[namespace(u32)] #[namespace(u32)]
#[namespace(u8)] #[namespace(u8)]
#[namespace(usize get_usize)]
pub struct App { pub struct App {
/// Exit flag /// Exit flag
pub exit: Exit, 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 { fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control {
let t0 = state.perf.get_t0(); let t0 = state.perf.get_t0();
state.clock().update_from_scope(scope).unwrap(); 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>> { /// Arranger.
Ok(src.src()?.map(|x|x.into())) ///
/// ```
/// 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> { impl Project {
Ok(Some(match src.word()? { pub fn new (jack: &Jack<'static>) -> Self {
Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()), Self {
Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9), jack: jack.clone(),
_ => return try_to_u16(src) ..Default::default()
}))
}
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}"),
} }
} else if let Ok(Some(sym)) = src.word() { }
Ok(match sym {
":color/bg" => Some(Color::Rgb(28, 32, 36)), pub fn new_jack (name: impl AsRef<str>) -> Usually<Self> {
":color/fg" => Some(Color::Rgb(98, 92, 96)), let name = name.as_ref();
_ => return err!("not a color: {sym}") 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)] impl HasJack<'static> for App {
fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps<Option<u7>> { fn jack (&self) -> &Jack<'static> {
eval_word(src, |word|Ok(match word { &self.jack
}
":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 Project {
fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps<Option<usize>> { fn jack (&self) -> &Jack<'static> {
eval_word(src, |word|Ok(match word { &self.jack
}
":selected/scene" => state.selection().scene(),
":selected/track" => state.selection().track(),
_ => return err!("unknown opt<usize>: {word}")
}))
} }
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> { impl ScenesView for Project {
Ok(None) 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>> { pub fn swap_value <T: Clone + PartialEq, U> (
Ok(None) 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>> { pub fn toggle_bool <U> (
todo!() 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]>>> { /// Modes for clip pool
todo!() #[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]>> { /// Focused field of `ClipLength`
todo!() #[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>>> { impl ClipLengthFocus {
todo!() 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>> { /// Displays and edits clip length.
todo!() #[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> { impl ClipLength {
todo!() 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>>> { impl HasScenes for Project {
todo!() 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 { impl HasModeStack for App {
fn mode_stack (&self) -> &RwLock<Vec<Arc<str>>> { fn mode_stack (&self) -> &RwLock<Vec<Arc<str>>> {
&self.modes &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 { impl ScenesView for App {
fn w_mid (&self) -> u16 { fn w_mid (&self) -> u16 {
(self.size.w() as u16).saturating_sub(self.w_side()) (self.size.w() as u16).saturating_sub(self.w_side())
@ -590,17 +970,6 @@ impl HasScenes for App {
self.project.scenes_mut() 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. /// Various possible dialog modes.
/// ///
@ -619,7 +988,7 @@ impl HasTrackScroll for App {
impl Dialog { 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 { eval_word(&src, |word|Ok(match word {
@ -1184,6 +1553,39 @@ pub trait HasTrackScroll {
fn tracks_last (&self) -> usize; fn tracks_last (&self) -> usize;
fn tracks_last_mut (&mut self) -> &mut 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> { pub trait HasSampler: AsRef<Sampler> + AsMut<Sampler> {
fn sampler (&self) -> &Sampler { fn sampler (&self) -> &Sampler {
@ -1203,6 +1605,23 @@ pub trait HasClip {
self.clip_slot().map(|slot|slot.write()) 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 { pub trait HasWidth {
const MIN_WIDTH: usize; 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 { tui_keys!(self: App, input {
#[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
@ -2022,33 +2482,3 @@ pub enum MidiOutputCmd {
Close, Close,
Connect(Arc<str>), 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
}