mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-10-08 18:56:42 +02:00
261 lines
8.9 KiB
Rust
261 lines
8.9 KiB
Rust
use crate::*;
|
|
|
|
pub type Clips = Vec<Arc<RwLock<MidiClip>>>;
|
|
|
|
pub trait HasClips {
|
|
/// Active clip slot (immutable). Use this to access or modify current clip.
|
|
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>>;
|
|
/// Active clip slot (mutable). Use this to place or remove a clip at current position.
|
|
fn clip_mut (&self) -> &mut Option<Arc<RwLock<MidiClip>>>;
|
|
/// Clip list (immutable). Use this to access or modify more than one clip.
|
|
fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips>;
|
|
/// Clip list (mutable). Use this to define or delete new clips.
|
|
fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips>;
|
|
}
|
|
|
|
/// A MIDI sequence.
|
|
///
|
|
/// ```
|
|
/// let clip = tek::MidiClip::default();
|
|
/// ```
|
|
#[derive(Debug, Clone, Default)] pub struct MidiClip {
|
|
pub uuid: uuid::Uuid,
|
|
/// Name of clip
|
|
pub name: Arc<str>,
|
|
/// Temporal resolution in pulses per quarter note
|
|
pub ppq: usize,
|
|
/// Length of clip in pulses
|
|
pub length: usize,
|
|
/// Notes in clip
|
|
pub notes: MidiData,
|
|
/// Whether to loop the clip or play it once
|
|
pub looped: bool,
|
|
/// Start of loop
|
|
pub loop_start: usize,
|
|
/// Length of loop
|
|
pub loop_length: usize,
|
|
/// All notes are displayed with minimum length
|
|
pub percussive: bool,
|
|
/// Identifying color of clip
|
|
pub color: ItemTheme,
|
|
}
|
|
|
|
impl MidiClip {
|
|
pub fn new (
|
|
name: impl AsRef<str>,
|
|
looped: bool,
|
|
length: usize,
|
|
notes: Option<MidiData>,
|
|
color: Option<ItemTheme>,
|
|
) -> Self {
|
|
Self {
|
|
uuid: uuid::Uuid::new_v4(),
|
|
name: name.as_ref().into(),
|
|
ppq: PPQ,
|
|
length,
|
|
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
|
looped,
|
|
loop_start: 0,
|
|
loop_length: length,
|
|
percussive: true,
|
|
color: color.unwrap_or_else(ItemTheme::random)
|
|
}
|
|
}
|
|
pub fn count_midi_messages (&self) -> usize {
|
|
let mut count = 0;
|
|
for tick in self.notes.iter() {
|
|
count += tick.len();
|
|
}
|
|
count
|
|
}
|
|
pub fn set_length (&mut self, length: usize) {
|
|
self.length = length;
|
|
self.notes = vec![Vec::with_capacity(16);length];
|
|
}
|
|
pub fn duplicate (&self) -> Self {
|
|
let mut clone = self.clone();
|
|
clone.uuid = uuid::Uuid::new_v4();
|
|
clone
|
|
}
|
|
/// Change the color of a clip, returning the previous color.
|
|
/// If value not provided, randomize.
|
|
pub fn set_color (&mut self, color: Option<ItemTheme>) -> ItemTheme {
|
|
let mut color = color.unwrap_or_else(||ItemTheme::random());
|
|
std::mem::swap(&mut color, &mut self.color);
|
|
color
|
|
}
|
|
/// Set loop mode for a clip, returning the previous loop state.
|
|
/// If value not provided, toggle.
|
|
pub fn toggle_loop (&mut self, looping: Option<bool>) -> bool {
|
|
match looping {
|
|
Some(true) => {
|
|
self.looped = true;
|
|
false
|
|
},
|
|
Some(false) => {
|
|
self.looped = false;
|
|
true
|
|
},
|
|
None => {
|
|
self.looped = !self.looped;
|
|
!self.looped
|
|
}
|
|
}
|
|
}
|
|
/// Add a [MidiMessage] at a given `pulse` position of this clip.
|
|
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
|
if pulse >= self.length { panic!("extend clip first") }
|
|
self.notes[pulse].push(message);
|
|
}
|
|
/// Check if a range `start..end` contains MIDI Note On `k`
|
|
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
|
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
|
for event in events.iter() {
|
|
if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } }
|
|
}
|
|
}
|
|
false
|
|
}
|
|
/// Create a panic clip.
|
|
pub fn stop_all () -> Self {
|
|
let color = Some(ItemColor::from_tui(Color::Rgb(32, 32, 32)).into());
|
|
Self::new("Stop", false, 1, Some(vec![vec![MIDI_PANIC]]), color)
|
|
}
|
|
}
|
|
|
|
const MIDI_PANIC: MidiMessage = MidiMessage::Controller {
|
|
controller: u7::new(123), value: u7::new(0),
|
|
};
|
|
|
|
pub type MidiData = Vec<Vec<MidiMessage>>;
|
|
impl PartialEq for MidiClip { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } }
|
|
impl Eq for MidiClip {}
|
|
|
|
impl HasMidiClip for Project {
|
|
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> { None }
|
|
}
|
|
impl HasMidiClip for App {
|
|
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> { self.project.clip() }
|
|
}
|
|
|
|
impl<T: HasMidiClip
|
|
+ Namespace<Option<bool>>
|
|
+ Namespace<Option<ItemTheme>>
|
|
> MidiClipController for T {}
|
|
|
|
#[tek_proc::commands(MidiClipCommand)]
|
|
pub trait MidiClipController: HasMidiClip
|
|
+ Namespace<Option<bool>>
|
|
+ Namespace<Option<ItemTheme>>
|
|
{
|
|
|
|
/// Create a clip at the current position if it does not already exist.
|
|
#[command(Upsert = "upsert")]
|
|
fn clip_upsert (&mut self) -> Perhaps<MidiClipCommand> {
|
|
let clip = self.clip();
|
|
//let old = self.scenes[scene].clips[track].clone();
|
|
//self.scenes[scene].clips[track] = clip;
|
|
//old
|
|
todo!()
|
|
}
|
|
|
|
#[command(SetColor = "color")]
|
|
fn clip_set_color (&mut self, color: Option<ItemTheme>) -> Perhaps<MidiClipCommand> {
|
|
Ok(self.clip().map(|clip|MidiClipCommand::SetColor {
|
|
color: Some(clip.write().set_color(color))
|
|
}))
|
|
}
|
|
|
|
/// Toggle looping for the active clip
|
|
#[command(SetLoop = "loop")]
|
|
fn clip_toggle_loop (&mut self, looping: Option<bool>) -> Perhaps<MidiClipCommand> {
|
|
Ok(self.clip().map(|clip|MidiClipCommand::SetLoop {
|
|
looping: Some(clip.write().toggle_loop(looping))
|
|
}))
|
|
}
|
|
|
|
}
|
|
|
|
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 trait HasClipsSize {
|
|
/// Size of clips area.
|
|
fn clips_size (&self) -> &Sizer;
|
|
/// Draw content annotated with value of [Self::clips_size].
|
|
fn with_clips_size (&self, show: bool, content: impl Draw<Tui>) -> impl Draw<Tui> {
|
|
let size = self.clips_size();
|
|
above(
|
|
when(show, fg(Green, east!(size.w() as usize, "x", size.h() as usize)).align_se()),
|
|
size.of(content.align_c().full_wh())
|
|
)
|
|
}
|
|
}
|
|
|
|
impl<T: TracksView + ScenesView + Send + Sync> ClipsView for T {}
|
|
|
|
pub trait ClipsView: TracksView + ScenesView {
|
|
|
|
/// Draw clips per scene
|
|
fn view_scenes_clips (&self) -> impl Draw<Tui> {
|
|
let select = self.selection();
|
|
let editor = self.editor();
|
|
let editing = self.is_editing();
|
|
self.with_clips_size(true, iter_east(move||self.tracks_with_sizes()
|
|
.map(move|(track_index, track, _, _)|iter_south(move||self.scenes_with_sizes()
|
|
.map(move|(scene_index, scene, _, _)|{
|
|
let (name, theme): (Arc<str>, ItemTheme) = view_scene_name_theme(scene, track_index);
|
|
let f = theme.lightest.term;
|
|
let (b, o) = view_scene_bg(theme, select, track_index, scene_index);
|
|
let is_selected = view_scene_sel(select, track_index, scene_index, editing);
|
|
below(
|
|
Outer(true, Style::default().fg(o)).full_wh(),
|
|
below(
|
|
below(
|
|
fg_bg(o, b, "".full_wh()),
|
|
fg_bg(f, b, bold(true, name)).align_nw().full_wh(),
|
|
),
|
|
when(is_selected, editor).full_wh()
|
|
).full_wh()
|
|
).exact_wh(
|
|
view_scene_w(track, select, track_index, editor.as_ref()),
|
|
view_scene_y(select, scene_index, editor.as_ref()),
|
|
)
|
|
})
|
|
)
|
|
.exact_w(track.width as u16))
|
|
)).align_c()
|
|
}
|
|
|
|
}
|
|
|
|
pub fn get_opt_arc_rwlock_midiclip <'a> (state: &App, src: impl Language)
|
|
-> Perhaps<Option<Arc<RwLock<MidiClip>>>>
|
|
{
|
|
eval_word(src, |word|Ok(match word {
|
|
|
|
":selected" if let Selection::TrackClip {
|
|
track, scene
|
|
} = state.selection() => {
|
|
state.scenes()[*scene].clips[*track].clone()
|
|
},
|
|
|
|
//":selected-or-create" if let Selection::TrackClip {
|
|
//track, scene
|
|
//} = state.selection() => Some(if let Some(Some(clip)) = state.scenes()[*scene].clips.get(*track) {
|
|
//clip.clone()
|
|
//} else {
|
|
//let clip: Arc<RwLock<MidiClip>> = Arc::new(RwLock::new(Default::default()));
|
|
//state.scenes()[*scene].clips[*track] = Some(clip.clone());
|
|
//clip
|
|
//}),
|
|
|
|
_ => return err!("not a clip: {word}")
|
|
|
|
}))
|
|
}
|
|
|
|
pub fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language)
|
|
-> Perhaps<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>>
|
|
{
|
|
todo!()
|
|
}
|