mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-04-04 05:10:44 +02:00
wip: nmoralize
This commit is contained in:
parent
513b8354a3
commit
915e13aec8
14 changed files with 2 additions and 2 deletions
1188
src/.scratch.rs
Normal file
1188
src/.scratch.rs
Normal file
File diff suppressed because it is too large
Load diff
416
src/arrange.rs
Normal file
416
src/arrange.rs
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
|
||||
/// 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 scene is selected.
|
||||
#[cfg(feature = "scene")] Scene(usize),
|
||||
/// A track is selected.
|
||||
#[cfg(feature = "track")] Track(usize),
|
||||
/// A clip (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 },
|
||||
}
|
||||
|
||||
/// 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: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
||||
}
|
||||
|
||||
/// Arranger.
|
||||
///
|
||||
/// ```
|
||||
/// let arranger = tek::Arrangement::default();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Arrangement {
|
||||
/// Project name.
|
||||
pub name: Arc<str>,
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// JACK client handle.
|
||||
pub jack: Jack<'static>,
|
||||
/// 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: Measure<Tui>,
|
||||
/// Display size of clips area
|
||||
pub size_inner: Measure<Tui>,
|
||||
/// 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>,
|
||||
/// Selected UI element
|
||||
#[cfg(feature = "select")] pub selection: Selection,
|
||||
/// 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 scene_scroll: usize,
|
||||
}
|
||||
|
||||
pub trait ClipsView: TracksView + ScenesView {
|
||||
|
||||
fn view_scenes_clips <'a> (&'a self)
|
||||
-> impl Draw<Tui> + 'a
|
||||
{
|
||||
self.clips_size().of(wh_full(above(
|
||||
wh_full(origin_se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))),
|
||||
Thunk::new(|to: &mut Tui|for (
|
||||
track_index, track, _, _
|
||||
) in self.tracks_with_sizes() {
|
||||
to.place(&w_exact(track.width as u16,
|
||||
h_full(self.view_track_clips(track_index, track))))
|
||||
}))))
|
||||
}
|
||||
|
||||
fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw<Tui> + 'a {
|
||||
Thunk::new(move|to: &mut Tui|for (
|
||||
scene_index, scene, ..
|
||||
) in self.scenes_with_sizes() {
|
||||
let (name, theme): (Arc<str>, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
||||
let clip = clip.read().unwrap();
|
||||
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
||||
} else {
|
||||
(" ⏹ -- ".into(), ItemTheme::G[32])
|
||||
};
|
||||
let fg = theme.lightest.rgb;
|
||||
let mut outline = theme.base.rgb;
|
||||
let bg = if self.selection().track() == Some(track_index)
|
||||
&& self.selection().scene() == Some(scene_index)
|
||||
{
|
||||
outline = theme.lighter.rgb;
|
||||
theme.light.rgb
|
||||
} else if self.selection().track() == Some(track_index)
|
||||
|| self.selection().scene() == Some(scene_index)
|
||||
{
|
||||
outline = theme.darkest.rgb;
|
||||
theme.base.rgb
|
||||
} else {
|
||||
theme.dark.rgb
|
||||
};
|
||||
let w = if self.selection().track() == Some(track_index)
|
||||
&& let Some(editor) = self.editor ()
|
||||
{
|
||||
(editor.measure_width() as usize).max(24).max(track.width)
|
||||
} else {
|
||||
track.width
|
||||
} as u16;
|
||||
let y = if self.selection().scene() == Some(scene_index)
|
||||
&& let Some(editor) = self.editor ()
|
||||
{
|
||||
(editor.measure_height() as usize).max(12)
|
||||
} else {
|
||||
Self::H_SCENE as usize
|
||||
} as u16;
|
||||
|
||||
to.place(&wh_exact(w, y, below(
|
||||
wh_full(Outer(true, Style::default().fg(outline))),
|
||||
wh_full(below(
|
||||
below(
|
||||
Tui::fg_bg(outline, bg, wh_full("")),
|
||||
wh_full(origin_nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))),
|
||||
),
|
||||
wh_full(when(self.selection().track() == Some(track_index)
|
||||
&& self.selection().scene() == Some(scene_index)
|
||||
&& self.is_editing(), self.editor())))))));
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured<Tui> {
|
||||
|
||||
fn tracks_width_available (&self) -> u16 {
|
||||
(self.measure_width() as u16).saturating_sub(40)
|
||||
}
|
||||
|
||||
/// Iterate over tracks with their corresponding sizes.
|
||||
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
||||
let _editor_width = self.editor().map(|e|e.measure_width());
|
||||
let _active_track = self.selection().track();
|
||||
let mut x = 0;
|
||||
self.tracks().iter().enumerate().map_while(move |(index, track)|{
|
||||
let width = track.width.max(8);
|
||||
if x + width < self.clips_size().w() as usize {
|
||||
let data = (index, track, x, x + width);
|
||||
x += width + Self::TRACK_SPACING;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn view_track_names (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
let track_count = self.tracks().len();
|
||||
let scene_count = self.scenes().len();
|
||||
let selected = self.selection();
|
||||
let button = south(
|
||||
button_3("t", "rack ", format!("{}{track_count}", selected.track()
|
||||
.map(|track|format!("{track}/")).unwrap_or_default()), false),
|
||||
button_3("s", "cene ", format!("{}{scene_count}", selected.scene()
|
||||
.map(|scene|format!("{scene}/")).unwrap_or_default()), false));
|
||||
let button_2 = south(
|
||||
button_2("T", "+", false),
|
||||
button_2("S", "+", false));
|
||||
view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb,
|
||||
h_exact(2, Thunk::new(|to: &mut Tui|{
|
||||
for (index, track, x1, _x2) in self.tracks_with_sizes() {
|
||||
to.place(&x_push(x1 as u16, w_exact(track_width(index, track),
|
||||
Tui::bg(if selected.track() == Some(index) {
|
||||
track.color.light.rgb
|
||||
} else {
|
||||
track.color.base.rgb
|
||||
}, south(w_full(origin_nw(east(
|
||||
format!("·t{index:02} "),
|
||||
Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name))
|
||||
))), ""))) ));}}))))
|
||||
}
|
||||
|
||||
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw<Tui> {
|
||||
view_track_row_section(theme,
|
||||
south(w_full(origin_w(button_2("o", "utput", false))),
|
||||
Thunk::new(|to: &mut Tui|for port in self.midi_outs().iter() {
|
||||
to.place(&w_full(origin_w(port.port_name())));
|
||||
})),
|
||||
button_2("O", "+", false),
|
||||
Tui::bg(theme.darker.rgb, origin_w(Thunk::new(|to: &mut Tui|{
|
||||
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||
to.place(&w_exact(track_width(index, track),
|
||||
origin_nw(h_full(iter_south(1, ||track.sequencer.midi_outs.iter(),
|
||||
|port, index|Tui::fg(Rgb(255, 255, 255),
|
||||
h_exact(1, Tui::bg(track.color.dark.rgb, w_full(origin_w(
|
||||
format!("·o{index:02} {}", port.port_name())))))))))));}}))))
|
||||
}
|
||||
|
||||
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
let mut h = 0u16;
|
||||
for track in self.tracks().iter() {
|
||||
h = h.max(track.sequencer.midi_ins.len() as u16);
|
||||
}
|
||||
let content = Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||
to.place(&wh_exact(track_width(index, track), h + 1,
|
||||
origin_nw(south(
|
||||
Tui::bg(track.color.base.rgb,
|
||||
w_full(origin_w(east!(
|
||||
either(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "),
|
||||
either(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "),
|
||||
either(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "),
|
||||
)))),
|
||||
iter_south(1, ||track.sequencer.midi_ins.iter(),
|
||||
|port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb,
|
||||
w_full(origin_w(format!("·i{index:02} {}", port.port_name())))))))));
|
||||
});
|
||||
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
|
||||
Tui::bg(theme.darker.rgb, origin_w(content)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
|
||||
/// Default scene height.
|
||||
const H_SCENE: usize = 2;
|
||||
/// Default editor height.
|
||||
const H_EDITOR: usize = 15;
|
||||
fn h_scenes (&self) -> u16;
|
||||
fn w_side (&self) -> u16;
|
||||
fn w_mid (&self) -> u16;
|
||||
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
|
||||
let mut y = 0;
|
||||
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
|
||||
let height = if self.selection().scene() == Some(s) && self.editor().is_some() {
|
||||
8
|
||||
} else {
|
||||
Self::H_SCENE
|
||||
};
|
||||
if y + height <= self.clips_size().h() as usize {
|
||||
let data = (s, scene, y, y + height);
|
||||
y += height;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn view_scenes_names (&self) -> impl Draw<Tui> {
|
||||
w_exact(20, Thunk::new(|to: &mut Tui|for (index, scene, ..) in self.scenes_with_sizes() {
|
||||
to.place(&self.view_scene_name(index, scene));
|
||||
}))
|
||||
}
|
||||
|
||||
fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw<Tui> + 'a {
|
||||
let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() {
|
||||
7
|
||||
} else {
|
||||
Self::H_SCENE as u16
|
||||
};
|
||||
let bg = if self.selection().scene() == Some(index) {
|
||||
scene.color.light.rgb
|
||||
} else {
|
||||
scene.color.base.rgb
|
||||
};
|
||||
let a = w_full(origin_w(east(format!("·s{index:02} "),
|
||||
Tui::fg(Tui::g(255), Tui::bold(true, &scene.name)))));
|
||||
let b = when(self.selection().scene() == Some(index) && self.is_editing(),
|
||||
wh_full(origin_nw(south(
|
||||
self.editor().as_ref().map(|e|e.clip_status()),
|
||||
self.editor().as_ref().map(|e|e.edit_status())))));
|
||||
wh_exact(20, h, Tui::bg(bg, origin_nw(south(a, b))))
|
||||
}
|
||||
|
||||
}
|
||||
pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; }
|
||||
pub trait HasTrackScroll: HasTracks { fn track_scroll (&self) -> usize; }
|
||||
pub trait HasScene: AsRefOpt<Scene> + AsMutOpt<Scene> {
|
||||
fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() }
|
||||
fn scene (&self) -> Option<&Scene> { self.as_ref_opt() }
|
||||
}
|
||||
pub trait HasSelection: AsRef<Selection> + AsMut<Selection> {
|
||||
fn selection (&self) -> &Selection { self.as_ref() }
|
||||
fn selection_mut (&mut self) -> &mut Selection { self.as_mut() }
|
||||
/// 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<Arc<RwLock<MidiClip>>> where Self: HasScenes + HasTracks {
|
||||
self.selected_scene()?.clips.get(self.selection().track()?)?.clone()
|
||||
}
|
||||
}
|
||||
pub trait HasScenes: AsRef<Vec<Scene>> + AsMut<Vec<Scene>> {
|
||||
fn scenes (&self) -> &Vec<Scene> { self.as_ref() }
|
||||
fn scenes_mut (&mut self) -> &mut Vec<Scene> { self.as_mut() }
|
||||
/// Generate the default name for a new scene
|
||||
fn scene_default_name (&self) -> Arc<str> { format!("s{:3>}", self.scenes().len() + 1).into() }
|
||||
fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) }
|
||||
/// Add multiple scenes
|
||||
fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks {
|
||||
let scene_color_1 = ItemColor::random();
|
||||
let scene_color_2 = ItemColor::random();
|
||||
for i in 0..n {
|
||||
let _ = self.scene_add(None, Some(
|
||||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Add a scene
|
||||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemTheme>)
|
||||
-> Usually<(usize, &mut Scene)> where Self: HasTracks
|
||||
{
|
||||
let scene = Scene {
|
||||
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
||||
clips: vec![None;self.tracks().len()],
|
||||
color: color.unwrap_or_else(ItemTheme::random),
|
||||
};
|
||||
self.scenes_mut().push(scene);
|
||||
let index = self.scenes().len() - 1;
|
||||
Ok((index, &mut self.scenes_mut()[index]))
|
||||
}
|
||||
}
|
||||
pub trait HasTracks: AsRef<Vec<Track>> + AsMut<Vec<Track>> {
|
||||
fn tracks (&self) -> &Vec<Track> { self.as_ref() }
|
||||
fn tracks_mut (&mut self) -> &mut Vec<Track> { self.as_mut() }
|
||||
/// Run audio callbacks for every track and every device
|
||||
fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
for track in self.tracks_mut().iter_mut() {
|
||||
if Control::Quit == Audio::process(&mut track.sequencer, client, scope) {
|
||||
return Control::Quit
|
||||
}
|
||||
for device in track.devices.iter_mut() {
|
||||
if Control::Quit == DeviceAudio(device).process(client, scope) {
|
||||
return Control::Quit
|
||||
}
|
||||
}
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) }
|
||||
/// Stop all playing clips
|
||||
fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } }
|
||||
/// Stop all playing clips
|
||||
fn tracks_launch (&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>) {
|
||||
if let Some(clips) = clips {
|
||||
for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); }
|
||||
} else {
|
||||
for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); }
|
||||
}
|
||||
}
|
||||
/// Spacing between tracks.
|
||||
const TRACK_SPACING: usize = 0;
|
||||
}
|
||||
pub trait HasTrack: AsRefOpt<Track> + AsMutOpt<Track> {
|
||||
fn track (&self) -> Option<&Track> { self.as_ref_opt() }
|
||||
fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() }
|
||||
#[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> + 'a {
|
||||
self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
|
||||
}
|
||||
#[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> + '_ {
|
||||
self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
|
||||
}
|
||||
#[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
|
||||
}
|
||||
#[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
||||
}
|
||||
}
|
||||
97
src/browse.rs
Normal file
97
src/browse.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/// Browses for files to load/save.
|
||||
///
|
||||
/// ```
|
||||
/// let browse = tek::Browse::default();
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default, PartialEq)] pub struct Browse {
|
||||
pub cwd: PathBuf,
|
||||
pub dirs: Vec<(OsString, String)>,
|
||||
pub files: Vec<(OsString, String)>,
|
||||
pub filter: String,
|
||||
pub index: usize,
|
||||
pub scroll: usize,
|
||||
pub size: Measure<Tui>,
|
||||
}
|
||||
|
||||
pub(crate) struct EntriesIterator<'a> {
|
||||
pub browser: &'a Browse,
|
||||
pub offset: usize,
|
||||
pub length: usize,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)] pub enum BrowseTarget {
|
||||
SaveProject,
|
||||
LoadProject,
|
||||
ImportSample(Arc<RwLock<Option<Sample>>>),
|
||||
ExportSample(Arc<RwLock<Option<Sample>>>),
|
||||
ImportClip(Arc<RwLock<Option<MidiClip>>>),
|
||||
ExportClip(Arc<RwLock<Option<MidiClip>>>),
|
||||
}
|
||||
|
||||
/// A clip pool.
|
||||
///
|
||||
/// ```
|
||||
/// let pool = tek::Pool::default();
|
||||
/// ```
|
||||
#[derive(Debug)] pub struct Pool {
|
||||
pub visible: bool,
|
||||
/// Selected clip
|
||||
pub clip: AtomicUsize,
|
||||
/// Mode switch
|
||||
pub mode: Option<PoolMode>,
|
||||
/// Embedded file browse
|
||||
#[cfg(feature = "browse")] pub browse: Option<Browse>,
|
||||
/// Collection of MIDI clips.
|
||||
#[cfg(feature = "clip")] pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
||||
/// Collection of sound samples.
|
||||
#[cfg(feature = "sampler")] pub samples: Arc<RwLock<Vec<Arc<RwLock<Sample>>>>>,
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
/// Some sort of wrapper again?
|
||||
pub struct PoolView<'a>(pub &'a Pool);
|
||||
|
||||
// Commands supported by [Browse]
|
||||
//#[derive(Debug, Clone, PartialEq)]
|
||||
//pub enum BrowseCommand {
|
||||
//Begin,
|
||||
//Cancel,
|
||||
//Confirm,
|
||||
//Select(usize),
|
||||
//Chdir(PathBuf),
|
||||
//Filter(Arc<str>),
|
||||
//}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
306
src/connect.rs
Normal file
306
src/connect.rs
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
use crate::*;
|
||||
|
||||
/// Audio input port.
|
||||
#[derive(Debug)] pub struct AudioInput {
|
||||
/// Handle to JACK client, for receiving reconnect events.
|
||||
pub jack: Jack<'static>,
|
||||
/// Port name
|
||||
pub name: Arc<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<AudioIn>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
}
|
||||
|
||||
/// Audio output port.
|
||||
#[derive(Debug)] pub struct AudioOutput {
|
||||
/// Handle to JACK client, for receiving reconnect events.
|
||||
pub jack: Jack<'static>,
|
||||
/// Port name
|
||||
pub name: Arc<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<AudioOut>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
}
|
||||
|
||||
/// MIDI input port.
|
||||
#[derive(Debug)] pub struct MidiInput {
|
||||
/// Handle to JACK client, for receiving reconnect events.
|
||||
pub jack: Jack<'static>,
|
||||
/// Port name
|
||||
pub name: Arc<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<MidiIn>,
|
||||
/// List of currently held notes.
|
||||
pub held: Arc<RwLock<[bool;128]>>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
}
|
||||
|
||||
/// MIDI output port.
|
||||
#[derive(Debug)] pub struct MidiOutput {
|
||||
/// Handle to JACK client, for receiving reconnect events.
|
||||
pub jack: Jack<'static>,
|
||||
/// Port name
|
||||
pub name: Arc<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<MidiOut>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
/// List of currently held notes.
|
||||
pub held: Arc<RwLock<[bool;128]>>,
|
||||
/// Buffer
|
||||
pub note_buffer: Vec<u8>,
|
||||
/// Buffer
|
||||
pub output_buffer: Vec<Vec<Vec<u8>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)] pub enum ConnectName {
|
||||
/** Exact match */
|
||||
Exact(Arc<str>),
|
||||
/** Match regular expression */
|
||||
RegExp(Arc<str>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope {
|
||||
One,
|
||||
All
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus {
|
||||
Missing,
|
||||
Disconnected,
|
||||
Connected,
|
||||
Mismatch,
|
||||
}
|
||||
|
||||
/// Port connection manager.
|
||||
///
|
||||
/// ```
|
||||
/// let connect = tek::Connect::default();
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default)] pub struct Connect {
|
||||
pub name: Option<ConnectName>,
|
||||
pub scope: Option<ConnectScope>,
|
||||
pub status: Arc<RwLock<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>>,
|
||||
pub info: Arc<str>,
|
||||
}
|
||||
|
||||
impl Connect {
|
||||
pub fn collect (exact: &[impl AsRef<str>], re: &[impl AsRef<str>], re_all: &[impl AsRef<str>])
|
||||
-> Vec<Self>
|
||||
{
|
||||
let mut connections = vec![];
|
||||
for port in exact.iter() { connections.push(Self::exact(port)) }
|
||||
for port in re.iter() { connections.push(Self::regexp(port)) }
|
||||
for port in re_all.iter() { connections.push(Self::regexp_all(port)) }
|
||||
connections
|
||||
}
|
||||
/// Connect to this exact port
|
||||
pub fn exact (name: impl AsRef<str>) -> Self {
|
||||
let info = format!("=:{}", name.as_ref()).into();
|
||||
let name = Some(Exact(name.as_ref().into()));
|
||||
Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info }
|
||||
}
|
||||
pub fn regexp (name: impl AsRef<str>) -> Self {
|
||||
let info = format!("~:{}", name.as_ref()).into();
|
||||
let name = Some(RegExp(name.as_ref().into()));
|
||||
Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info }
|
||||
}
|
||||
pub fn regexp_all (name: impl AsRef<str>) -> Self {
|
||||
let info = format!("+:{}", name.as_ref()).into();
|
||||
let name = Some(RegExp(name.as_ref().into()));
|
||||
Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info }
|
||||
}
|
||||
pub fn info (&self) -> Arc<str> {
|
||||
format!(" ({}) {} {}", {
|
||||
let status = self.status.read().unwrap();
|
||||
let mut ok = 0;
|
||||
for (_, _, state) in status.iter() {
|
||||
if *state == Connected {
|
||||
ok += 1
|
||||
}
|
||||
}
|
||||
format!("{ok}/{}", status.len())
|
||||
}, match self.scope {
|
||||
None => "x",
|
||||
Some(One) => " ",
|
||||
Some(All) => "*",
|
||||
}, match &self.name {
|
||||
None => format!("x"),
|
||||
Some(Exact(name)) => format!("= {name}"),
|
||||
Some(RegExp(name)) => format!("~ {name}"),
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl<J: HasJack<'static>> RegisterPorts for J {
|
||||
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput> {
|
||||
MidiInput::new(self.jack(), name, connect)
|
||||
}
|
||||
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput> {
|
||||
MidiOutput::new(self.jack(), name, connect)
|
||||
}
|
||||
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput> {
|
||||
AudioInput::new(self.jack(), name, connect)
|
||||
}
|
||||
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput> {
|
||||
AudioOutput::new(self.jack(), name, connect)
|
||||
}
|
||||
}
|
||||
|
||||
/// May create new MIDI input ports.
|
||||
pub trait AddMidiIn {
|
||||
fn midi_in_add (&mut self) -> Usually<()>;
|
||||
}
|
||||
|
||||
/// May create new MIDI output ports.
|
||||
pub trait AddMidiOut {
|
||||
fn midi_out_add (&mut self) -> Usually<()>;
|
||||
}
|
||||
|
||||
pub trait RegisterPorts: HasJack<'static> {
|
||||
/// Register a MIDI input port.
|
||||
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput>;
|
||||
/// Register a MIDI output port.
|
||||
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput>;
|
||||
/// Register an audio input port.
|
||||
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput>;
|
||||
/// Register an audio output port.
|
||||
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput>;
|
||||
}
|
||||
|
||||
pub trait JackPort: HasJack<'static> {
|
||||
|
||||
type Port: PortSpec + Default;
|
||||
|
||||
type Pair: PortSpec + Default;
|
||||
|
||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||
-> Usually<Self> where Self: Sized;
|
||||
|
||||
fn register (jack: &Jack<'static>, name: &impl AsRef<str>) -> Usually<Port<Self::Port>> {
|
||||
jack.with_client(|c|c.register_port::<Self::Port>(name.as_ref(), Default::default()))
|
||||
.map_err(|e|e.into())
|
||||
}
|
||||
|
||||
fn port_name (&self) -> &Arc<str>;
|
||||
|
||||
fn connections (&self) -> &[Connect];
|
||||
|
||||
fn port (&self) -> &Port<Self::Port>;
|
||||
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port>;
|
||||
|
||||
fn into_port (self) -> Port<Self::Port> where Self: Sized;
|
||||
|
||||
fn close (self) -> Usually<()> where Self: Sized {
|
||||
let jack = self.jack().clone();
|
||||
Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?)
|
||||
}
|
||||
|
||||
fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec<String> {
|
||||
self.with_client(|c|c.ports(re_name, re_type, flags))
|
||||
}
|
||||
|
||||
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
||||
self.with_client(|c|c.port_by_id(id))
|
||||
}
|
||||
|
||||
fn port_by_name (&self, name: impl AsRef<str>) -> Option<Port<Unowned>> {
|
||||
self.with_client(|c|c.port_by_name(name.as_ref()))
|
||||
}
|
||||
|
||||
fn connect_to_matching <'k> (&'k self) -> Usually<()> {
|
||||
for connect in self.connections().iter() {
|
||||
match &connect.name {
|
||||
Some(Exact(name)) => {
|
||||
*connect.status.write().unwrap() = self.connect_exact(name)?;
|
||||
},
|
||||
Some(RegExp(re)) => {
|
||||
*connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?;
|
||||
},
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn connect_exact <'k> (&'k self, name: &str) ->
|
||||
Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>
|
||||
{
|
||||
self.with_client(move|c|{
|
||||
let mut status = vec![];
|
||||
for port in c.ports(None, None, PortFlags::empty()).iter() {
|
||||
if port.as_str() == &*name {
|
||||
if let Some(port) = c.port_by_name(port.as_str()) {
|
||||
let port_status = self.connect_to_unowned(&port)?;
|
||||
let name = port.name()?.into();
|
||||
status.push((port, name, port_status));
|
||||
if port_status == Connected {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(status)
|
||||
})
|
||||
}
|
||||
|
||||
fn connect_regexp <'k> (
|
||||
&'k self, re: &str, scope: Option<ConnectScope>
|
||||
) -> Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>> {
|
||||
self.with_client(move|c|{
|
||||
let mut status = vec![];
|
||||
let ports = c.ports(Some(&re), None, PortFlags::empty());
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = c.port_by_name(port.as_str()) {
|
||||
let port_status = self.connect_to_unowned(&port)?;
|
||||
let name = port.name()?.into();
|
||||
status.push((port, name, port_status));
|
||||
if port_status == Connected && scope == Some(One) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(status)
|
||||
})
|
||||
}
|
||||
|
||||
/** Connect to a matching port by name. */
|
||||
fn connect_to_name (&self, name: impl AsRef<str>) -> Usually<ConnectStatus> {
|
||||
self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) {
|
||||
self.connect_to_unowned(port)
|
||||
} else {
|
||||
Ok(Missing)
|
||||
})
|
||||
}
|
||||
|
||||
/** Connect to a matching port by reference. */
|
||||
fn connect_to_unowned (&self, port: &Port<Unowned>) -> Usually<ConnectStatus> {
|
||||
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
||||
Connected
|
||||
} else if let Ok(_) = c.connect_ports(port, self.port()) {
|
||||
Connected
|
||||
} else {
|
||||
Mismatch
|
||||
}))
|
||||
}
|
||||
|
||||
/** Connect to an owned matching port by reference. */
|
||||
fn connect_to_owned (&self, port: &Port<Self::Pair>) -> Usually<ConnectStatus> {
|
||||
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
||||
Connected
|
||||
} else if let Ok(_) = c.connect_ports(port, self.port()) {
|
||||
Connected
|
||||
} else {
|
||||
Mismatch
|
||||
}))
|
||||
}
|
||||
|
||||
}
|
||||
0
src/device.rs
Normal file
0
src/device.rs
Normal file
14
src/mix.rs
Normal file
14
src/mix.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#[derive(Debug, Default)] pub enum MeteringMode {
|
||||
#[default] Rms,
|
||||
Log10,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)] pub struct Log10Meter(pub f32);
|
||||
|
||||
#[derive(Debug, Default, Clone)] pub struct RmsMeter(pub f32);
|
||||
|
||||
#[derive(Debug, Default)] pub enum MixingMode {
|
||||
#[default] Summing,
|
||||
Average,
|
||||
}
|
||||
|
||||
28
src/plugin.rs
Normal file
28
src/plugin.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
/// A LV2 plugin.
|
||||
#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 {
|
||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||
pub jack: Jack<'static>,
|
||||
pub name: Arc<str>,
|
||||
pub path: Option<Arc<str>>,
|
||||
pub selected: usize,
|
||||
pub mapping: bool,
|
||||
pub midi_ins: Vec<Port<MidiIn>>,
|
||||
pub midi_outs: Vec<Port<MidiOut>>,
|
||||
pub audio_ins: Vec<Port<AudioIn>>,
|
||||
pub audio_outs: Vec<Port<AudioOut>>,
|
||||
|
||||
pub lv2_world: livi::World,
|
||||
pub lv2_instance: livi::Instance,
|
||||
pub lv2_plugin: livi::Plugin,
|
||||
pub lv2_features: Arc<livi::Features>,
|
||||
pub lv2_port_list: Vec<livi::Port>,
|
||||
pub lv2_input_buffer: Vec<livi::event::LV2AtomSequence>,
|
||||
pub lv2_ui_thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
/// A LV2 plugin's X11 UI.
|
||||
#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI {
|
||||
pub window: Option<Window>
|
||||
}
|
||||
|
||||
104
src/sample.rs
Normal file
104
src/sample.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/// Plays [Voice]s from [Sample]s.
|
||||
///
|
||||
/// ```
|
||||
/// let sampler = tek::Sampler::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Sampler {
|
||||
/// Name of sampler.
|
||||
pub name: Arc<str>,
|
||||
/// Device color.
|
||||
pub color: ItemTheme,
|
||||
/// Sample currently being recorded.
|
||||
pub recording: Option<(usize, Option<Arc<RwLock<Sample>>>)>,
|
||||
/// Recording buffer.
|
||||
pub buffer: Vec<Vec<f32>>,
|
||||
/// Samples mapped to MIDI notes.
|
||||
pub samples: SampleKit<128>,
|
||||
/// Collection of currently playing instances of samples.
|
||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||
/// Samples that are not mapped to MIDI notes.
|
||||
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
||||
/// Sample currently being edited.
|
||||
pub editing: Option<Arc<RwLock<Sample>>>,
|
||||
/// How to mix the voices.
|
||||
pub mixing_mode: MixingMode,
|
||||
/// How to meter the inputs and outputs.
|
||||
pub metering_mode: MeteringMode,
|
||||
/// Fixed gain applied to all output.
|
||||
pub output_gain: f32,
|
||||
/// Currently active modal, if any.
|
||||
pub mode: Option<SamplerMode>,
|
||||
/// Size of rendered sampler.
|
||||
pub size: Measure<Tui>,
|
||||
/// Lowest note displayed.
|
||||
pub note_lo: AtomicUsize,
|
||||
/// Currently selected note.
|
||||
pub note_pt: AtomicUsize,
|
||||
/// Selected note as row/col.
|
||||
pub cursor: (AtomicUsize, AtomicUsize),
|
||||
/// Audio input meters.
|
||||
#[cfg(feature = "meter")] pub input_meters: Vec<f32>,
|
||||
/// Audio input ports. Samples are recorded from here.
|
||||
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
||||
/// MIDI input port. Sampler are triggered from here.
|
||||
#[cfg(feature = "port")] pub midi_in: Option<MidiInput>,
|
||||
/// Audio output ports. Voices are played into here.
|
||||
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
||||
/// Audio output meters.
|
||||
#[cfg(feature = "meter")] pub output_meters: Vec<f32>,
|
||||
}
|
||||
|
||||
/// Collection of samples, one per slot, fixed number of slots.
|
||||
///
|
||||
/// History: Separated to cleanly implement [Default].
|
||||
///
|
||||
/// ```
|
||||
/// let samples = tek::SampleKit([None, None, None, None]);
|
||||
/// ```
|
||||
#[derive(Debug)] pub struct SampleKit<const N: usize>(
|
||||
pub [Option<Arc<RwLock<Sample>>>;N]
|
||||
);
|
||||
|
||||
/// A sound cut.
|
||||
///
|
||||
/// ```
|
||||
/// let sample = tek::Sample::default();
|
||||
/// let sample = tek::Sample::new("test", 0, 0, vec![]);
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Sample {
|
||||
pub name: Arc<str>,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub channels: Vec<Vec<f32>>,
|
||||
pub rate: Option<usize>,
|
||||
pub gain: f32,
|
||||
pub color: ItemTheme,
|
||||
}
|
||||
|
||||
/// A currently playing instance of a sample.
|
||||
#[derive(Default, Debug, Clone)] pub struct Voice {
|
||||
pub sample: Arc<RwLock<Sample>>,
|
||||
pub after: usize,
|
||||
pub position: usize,
|
||||
pub velocity: f32,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)] pub struct SampleAdd {
|
||||
pub exited: bool,
|
||||
pub dir: PathBuf,
|
||||
pub subdirs: Vec<OsString>,
|
||||
pub files: Vec<OsString>,
|
||||
pub cursor: usize,
|
||||
pub offset: usize,
|
||||
pub sample: Arc<RwLock<Sample>>,
|
||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||
pub _search: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)] pub enum SamplerMode {
|
||||
// Load sample from path
|
||||
Import(usize, Browse),
|
||||
}
|
||||
|
||||
pub type MidiSample =
|
||||
(Option<u7>, Arc<RwLock<crate::Sample>>);
|
||||
596
src/sequence.rs
Normal file
596
src/sequence.rs
Normal file
|
|
@ -0,0 +1,596 @@
|
|||
|
||||
/// Contains state for viewing and editing a clip.
|
||||
///
|
||||
/// ```
|
||||
/// use std::sync::{Arc, RwLock};
|
||||
/// let clip = tek::MidiClip::stop_all();
|
||||
/// let mut editor = tek::MidiEditor {
|
||||
/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))),
|
||||
/// size: Default::default(),
|
||||
/// //keys: Default::default(),
|
||||
/// };
|
||||
/// let _ = editor.put_note(true);
|
||||
/// let _ = editor.put_note(false);
|
||||
/// let _ = editor.clip_status();
|
||||
/// let _ = editor.edit_status();
|
||||
/// ```
|
||||
pub struct MidiEditor {
|
||||
/// Size of editor on screen
|
||||
pub size: Measure<Tui>,
|
||||
/// View mode and state of editor
|
||||
pub mode: PianoHorizontal,
|
||||
}
|
||||
|
||||
/// A clip, rendered as a horizontal piano roll.
|
||||
///
|
||||
/// ```
|
||||
/// let piano = tek::PianoHorizontal::default();
|
||||
/// ```
|
||||
#[derive(Clone, Default)] pub struct PianoHorizontal {
|
||||
pub clip: Option<Arc<RwLock<MidiClip>>>,
|
||||
/// Buffer where the whole clip is rerendered on change
|
||||
pub buffer: Arc<RwLock<BigBuffer>>,
|
||||
/// Size of actual notes area
|
||||
pub size: Measure<Tui>,
|
||||
/// The display window
|
||||
pub range: MidiSelection,
|
||||
/// The note cursor
|
||||
pub point: MidiCursor,
|
||||
/// The highlight color palette
|
||||
pub color: ItemTheme,
|
||||
/// Width of the keyboard
|
||||
pub keys_width: u16,
|
||||
}
|
||||
|
||||
/// 12 piano keys, some highlighted.
|
||||
///
|
||||
/// ```
|
||||
/// let keys = tek::OctaveVertical::default();
|
||||
/// ```
|
||||
#[derive(Copy, Clone)] pub struct OctaveVertical {
|
||||
pub on: [bool; 12],
|
||||
pub colors: [Color; 3]
|
||||
}
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Contains state for playing a clip
|
||||
///
|
||||
/// ```
|
||||
/// let clip = tek::MidiClip::default();
|
||||
/// println!("Empty clip: {clip:?}");
|
||||
///
|
||||
/// let clip = tek::MidiClip::stop_all();
|
||||
/// println!("Panic clip: {clip:?}");
|
||||
///
|
||||
/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None);
|
||||
/// clip.set_length(96);
|
||||
/// clip.toggle_loop();
|
||||
/// clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
|
||||
/// assert!(clip.contains_note_on(36.into(), 6, 18));
|
||||
/// assert_eq!(&clip.notes, &clip.duplicate().notes);
|
||||
///
|
||||
/// let clip = std::sync::Arc::new(clip);
|
||||
/// assert_eq!(clip.clone(), clip);
|
||||
///
|
||||
/// let sequencer = tek::Sequencer::default();
|
||||
/// println!("{sequencer:?}");
|
||||
/// ```
|
||||
pub struct Sequencer {
|
||||
/// State of clock and playhead
|
||||
#[cfg(feature = "clock")] pub clock: Clock,
|
||||
/// Start time and clip being played
|
||||
#[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||
/// Start time and next clip
|
||||
#[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||
/// Record from MIDI ports to current sequence.
|
||||
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
||||
/// Play from current sequence to MIDI ports
|
||||
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
||||
/// Play input through output.
|
||||
pub monitoring: bool,
|
||||
/// Write input to sequence.
|
||||
pub recording: bool,
|
||||
/// Overdub input to sequence.
|
||||
pub overdub: bool,
|
||||
/// Send all notes off
|
||||
pub reset: bool, // TODO?: after Some(nframes)
|
||||
/// Notes currently held at input
|
||||
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
/// Notes currently held at output
|
||||
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
||||
/// MIDI output buffer
|
||||
pub note_buf: Vec<u8>,
|
||||
/// MIDI output buffer
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
}
|
||||
|
||||
/// A track consists of a sequencer and zero or more devices chained after it.
|
||||
///
|
||||
/// ```
|
||||
/// let track: tek::Track = Default::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Track {
|
||||
/// Name of track
|
||||
pub name: Arc<str>,
|
||||
/// Identifying color of track
|
||||
pub color: ItemTheme,
|
||||
/// Preferred width of track column
|
||||
pub width: usize,
|
||||
/// MIDI sequencer state
|
||||
pub sequencer: Sequencer,
|
||||
/// Device chain
|
||||
pub devices: Vec<Device>,
|
||||
}
|
||||
|
||||
|
||||
pub trait HasPlayClip: HasClock {
|
||||
|
||||
fn reset (&self) -> bool;
|
||||
|
||||
fn reset_mut (&mut self) -> &mut bool;
|
||||
|
||||
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
|
||||
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
|
||||
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
|
||||
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
|
||||
fn pulses_since_start (&self) -> Option<f64> {
|
||||
if let Some((started, Some(_))) = self.play_clip().as_ref() {
|
||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||
return Some(elapsed)
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn pulses_since_start_looped (&self) -> Option<(f64, f64)> {
|
||||
if let Some((started, Some(clip))) = self.play_clip().as_ref() {
|
||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||
let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip
|
||||
let times = (elapsed as usize / length) as f64;
|
||||
let elapsed = (elapsed as usize % length) as f64;
|
||||
return Some((times, elapsed))
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn enqueue_next (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
||||
*self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned()));
|
||||
*self.reset_mut() = true;
|
||||
}
|
||||
|
||||
fn play_status (&self) -> impl Draw<Tui> {
|
||||
let (name, color): (Arc<str>, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() {
|
||||
let MidiClip { ref name, color, .. } = *clip.read().unwrap();
|
||||
(name.clone(), color)
|
||||
} else {
|
||||
("".into(), Tui::g(64).into())
|
||||
};
|
||||
let time: String = self.pulses_since_start_looped()
|
||||
.map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time)))
|
||||
.unwrap_or_else(||String::from(" ")).into();
|
||||
field_v(color, "Now:", format!("{} {}", time, name))
|
||||
}
|
||||
|
||||
fn next_status (&self) -> impl Draw<Tui> {
|
||||
let mut time: Arc<str> = String::from("--.-.--").into();
|
||||
let mut name: Arc<str> = String::from("").into();
|
||||
let mut color = ItemTheme::G[64];
|
||||
let clock = self.clock();
|
||||
if let Some((t, Some(clip))) = self.next_clip() {
|
||||
let clip = clip.read().unwrap();
|
||||
name = clip.name.clone();
|
||||
color = clip.color.clone();
|
||||
time = {
|
||||
let target = t.pulse.get();
|
||||
let current = clock.playhead.pulse.get();
|
||||
if target > current {
|
||||
let remaining = target - current;
|
||||
format!("-{:>}", clock.timebase.format_beats_1(remaining))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}.into()
|
||||
} else if let Some((t, Some(clip))) = self.play_clip() {
|
||||
let clip = clip.read().unwrap();
|
||||
if clip.looped {
|
||||
name = clip.name.clone();
|
||||
color = clip.color.clone();
|
||||
let target = t.pulse.get() + clip.length as f64;
|
||||
let current = clock.playhead.pulse.get();
|
||||
if target > current {
|
||||
time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into()
|
||||
}
|
||||
} else {
|
||||
name = "Stop".to_string().into();
|
||||
}
|
||||
};
|
||||
field_v(color, "Next:", format!("{} {}", time, name))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MidiMonitor: HasMidiIns + HasMidiBuffers {
|
||||
/// Input note flags.
|
||||
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||
/// Current monitoring status.
|
||||
fn monitoring (&self) -> bool;
|
||||
/// Mutable monitoring status.
|
||||
fn monitoring_mut (&mut self) -> &mut bool;
|
||||
/// Enable or disable monitoring.
|
||||
fn toggle_monitor (&mut self) { *self.monitoring_mut() = !self.monitoring(); }
|
||||
/// Perform monitoring.
|
||||
fn monitor (&mut self, _scope: &ProcessScope) { /* do nothing by default */ }
|
||||
}
|
||||
|
||||
pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip {
|
||||
fn recording (&self) -> bool;
|
||||
fn recording_mut (&mut self) -> &mut bool;
|
||||
fn toggle_record (&mut self) {
|
||||
*self.recording_mut() = !self.recording();
|
||||
}
|
||||
|
||||
fn overdub (&self) -> bool;
|
||||
fn overdub_mut (&mut self) -> &mut bool;
|
||||
fn toggle_overdub (&mut self) {
|
||||
*self.overdub_mut() = !self.overdub();
|
||||
}
|
||||
|
||||
fn record_clip (
|
||||
&mut self,
|
||||
scope: &ProcessScope,
|
||||
started: Moment,
|
||||
clip: &Option<Arc<RwLock<MidiClip>>>,
|
||||
) {
|
||||
if let Some(clip) = clip {
|
||||
let sample0 = scope.last_frame_time() as usize;
|
||||
let start = started.sample.get() as usize;
|
||||
let _recording = self.recording();
|
||||
let timebase = self.clock().timebase().clone();
|
||||
let quant = self.clock().quant.get();
|
||||
let mut clip = clip.write().unwrap();
|
||||
let length = clip.length;
|
||||
for input in self.midi_ins_mut().iter() {
|
||||
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
|
||||
if let LiveEvent::Midi { message, .. } = event {
|
||||
clip.record_event({
|
||||
let sample = (sample0 + sample - start) as f64;
|
||||
let pulse = timebase.samples_to_pulse(sample);
|
||||
let quantized = (pulse / quant).round() * quant;
|
||||
quantized as usize % length
|
||||
}, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_next (&mut self) {
|
||||
// TODO switch to next clip and record into it
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync {
|
||||
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize);
|
||||
fn redraw (&self);
|
||||
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>>;
|
||||
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
|
||||
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
||||
*self.clip_mut() = clip.cloned();
|
||||
self.redraw();
|
||||
}
|
||||
/// Make sure cursor is within note range
|
||||
fn autoscroll (&self) {
|
||||
let note_pos = self.get_note_pos().min(127);
|
||||
let note_lo = self.get_note_lo();
|
||||
let note_hi = self.get_note_hi();
|
||||
if note_pos < note_lo {
|
||||
self.note_lo().set(note_pos);
|
||||
} else if note_pos > note_hi {
|
||||
self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi));
|
||||
}
|
||||
}
|
||||
/// Make sure time range is within display
|
||||
fn autozoom (&self) {
|
||||
if self.time_lock().get() {
|
||||
let time_len = self.get_time_len();
|
||||
let time_axis = self.get_time_axis();
|
||||
let time_zoom = self.get_time_zoom();
|
||||
loop {
|
||||
let time_zoom = self.time_zoom().get();
|
||||
let time_area = time_axis * time_zoom;
|
||||
if time_area > time_len {
|
||||
let next_time_zoom = note_duration_prev(time_zoom);
|
||||
if next_time_zoom <= 1 {
|
||||
break
|
||||
}
|
||||
let next_time_area = time_axis * next_time_zoom;
|
||||
if next_time_area >= time_len {
|
||||
self.time_zoom().set(next_time_zoom);
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else if time_area < time_len {
|
||||
let prev_time_zoom = note_duration_next(time_zoom);
|
||||
if prev_time_zoom > 384 {
|
||||
break
|
||||
}
|
||||
let prev_time_area = time_axis * prev_time_zoom;
|
||||
if prev_time_area <= time_len {
|
||||
self.time_zoom().set(prev_time_zoom);
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if time_zoom != self.time_zoom().get() {
|
||||
self.redraw()
|
||||
}
|
||||
}
|
||||
//while time_len.div_ceil(time_zoom) > time_axis {
|
||||
//println!("\r{time_len} {time_zoom} {time_axis}");
|
||||
//time_zoom = Note::next(time_zoom);
|
||||
//}
|
||||
//self.time_zoom().set(time_zoom);
|
||||
}
|
||||
}
|
||||
pub type MidiData =
|
||||
Vec<Vec<MidiMessage>>;
|
||||
pub type ClipPool =
|
||||
Vec<Arc<RwLock<MidiClip>>>;
|
||||
pub type CollectedMidiInput<'a> =
|
||||
Vec<Vec<(u32, Result<LiveEvent<'a>, MidiError>)>>;
|
||||
|
||||
pub trait HasClips {
|
||||
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
||||
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
|
||||
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
||||
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
|
||||
self.clips_mut().push(clip.clone());
|
||||
(self.clips().len() - 1, clip)
|
||||
}
|
||||
}
|
||||
/// ```
|
||||
/// use tek::{*, tengri::*};
|
||||
///
|
||||
/// struct Test(Option<MidiEditor>);
|
||||
/// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref());
|
||||
/// impl_as_mut_opt!(MidiEditor: |self: Test|self.0.as_mut());
|
||||
///
|
||||
/// let mut host = Test(Some(MidiEditor::default()));
|
||||
/// let _ = host.editor();
|
||||
/// let _ = host.editor_mut();
|
||||
/// let _ = host.is_editing();
|
||||
/// let _ = host.editor_w();
|
||||
/// let _ = host.editor_h();
|
||||
/// ```
|
||||
pub trait HasEditor: AsRefOpt<MidiEditor> + AsMutOpt<MidiEditor> {
|
||||
fn editor (&self) -> Option<&MidiEditor> { self.as_ref_opt() }
|
||||
fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.as_mut_opt() }
|
||||
fn is_editing (&self) -> bool { self.editor().is_some() }
|
||||
fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize }
|
||||
fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize }
|
||||
}
|
||||
/// Trait for thing that may receive MIDI.
|
||||
pub trait HasMidiIns {
|
||||
fn midi_ins (&self) -> &Vec<MidiInput>;
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput>;
|
||||
/// Collect MIDI input from app ports (TODO preallocate large buffers)
|
||||
fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> {
|
||||
self.midi_ins().iter()
|
||||
.map(|port|port.port().iter(scope)
|
||||
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
|
||||
.collect::<Vec<_>>())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
fn midi_ins_with_sizes <'a> (&'a self) ->
|
||||
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
||||
{
|
||||
let mut y = 0;
|
||||
self.midi_ins().iter().enumerate().map(move|(i, input)|{
|
||||
let height = 1 + input.connections().len();
|
||||
let data = (i, input.port_name(), input.connections(), y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
}
|
||||
/// Trait for thing that may output MIDI.
|
||||
pub trait HasMidiOuts {
|
||||
fn midi_outs (&self) -> &Vec<MidiOutput>;
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput>;
|
||||
fn midi_outs_with_sizes <'a> (&'a self) ->
|
||||
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
||||
{
|
||||
let mut y = 0;
|
||||
self.midi_outs().iter().enumerate().map(move|(i, output)|{
|
||||
let height = 1 + output.connections().len();
|
||||
let data = (i, output.port_name(), output.connections(), y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
fn midi_outs_emit (&mut self, scope: &ProcessScope) {
|
||||
for port in self.midi_outs_mut().iter_mut() {
|
||||
port.buffer_emit(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub trait HasMidiClip {
|
||||
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>>;
|
||||
}
|
||||
pub trait HasSequencer: AsRef<Sequencer> + AsMut<Sequencer> {
|
||||
fn sequencer_mut (&mut self) -> &mut Sequencer { self.as_mut() }
|
||||
fn sequencer (&self) -> &Sequencer { self.as_ref() }
|
||||
}
|
||||
pub trait HasMidiBuffers {
|
||||
fn note_buf_mut (&mut self) -> &mut Vec<u8>;
|
||||
fn midi_buf_mut (&mut self) -> &mut Vec<Vec<Vec<u8>>>;
|
||||
}
|
||||
|
||||
|
||||
pub trait NotePoint {
|
||||
fn note_len (&self) -> &AtomicUsize;
|
||||
/// Get the current length of the note cursor.
|
||||
fn get_note_len (&self) -> usize {
|
||||
self.note_len().load(Relaxed)
|
||||
}
|
||||
/// Set the length of the note cursor, returning the previous value.
|
||||
fn set_note_len (&self, x: usize) -> usize {
|
||||
self.note_len().swap(x, Relaxed)
|
||||
}
|
||||
|
||||
fn note_pos (&self) -> &AtomicUsize;
|
||||
/// Get the current pitch of the note cursor.
|
||||
fn get_note_pos (&self) -> usize {
|
||||
self.note_pos().load(Relaxed).min(127)
|
||||
}
|
||||
/// Set the current pitch fo the note cursor, returning the previous value.
|
||||
fn set_note_pos (&self, x: usize) -> usize {
|
||||
self.note_pos().swap(x.min(127), Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TimePoint {
|
||||
fn time_pos (&self) -> &AtomicUsize;
|
||||
/// Get the current time position of the note cursor.
|
||||
fn get_time_pos (&self) -> usize {
|
||||
self.time_pos().load(Relaxed)
|
||||
}
|
||||
/// Set the current time position of the note cursor, returning the previous value.
|
||||
fn set_time_pos (&self, x: usize) -> usize {
|
||||
self.time_pos().swap(x, Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MidiPoint: NotePoint + TimePoint {
|
||||
/// Get the current end of the note cursor.
|
||||
fn get_note_end (&self) -> usize {
|
||||
self.get_time_pos() + self.get_note_len()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TimeRange {
|
||||
fn time_len (&self) -> &AtomicUsize;
|
||||
fn get_time_len (&self) -> usize {
|
||||
self.time_len().load(Ordering::Relaxed)
|
||||
}
|
||||
fn time_zoom (&self) -> &AtomicUsize;
|
||||
fn get_time_zoom (&self) -> usize {
|
||||
self.time_zoom().load(Ordering::Relaxed)
|
||||
}
|
||||
fn set_time_zoom (&self, value: usize) -> usize {
|
||||
self.time_zoom().swap(value, Ordering::Relaxed)
|
||||
}
|
||||
fn time_lock (&self) -> &AtomicBool;
|
||||
fn get_time_lock (&self) -> bool {
|
||||
self.time_lock().load(Ordering::Relaxed)
|
||||
}
|
||||
fn set_time_lock (&self, value: bool) -> bool {
|
||||
self.time_lock().swap(value, Ordering::Relaxed)
|
||||
}
|
||||
fn time_start (&self) -> &AtomicUsize;
|
||||
fn get_time_start (&self) -> usize {
|
||||
self.time_start().load(Ordering::Relaxed)
|
||||
}
|
||||
fn set_time_start (&self, value: usize) -> usize {
|
||||
self.time_start().swap(value, Ordering::Relaxed)
|
||||
}
|
||||
fn time_axis (&self) -> &AtomicUsize;
|
||||
fn get_time_axis (&self) -> usize {
|
||||
self.time_axis().load(Ordering::Relaxed)
|
||||
}
|
||||
fn get_time_end (&self) -> usize {
|
||||
self.time_start().get() + self.time_axis().get() * self.time_zoom().get()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NoteRange {
|
||||
fn note_lo (&self) -> &AtomicUsize;
|
||||
fn get_note_lo (&self) -> usize {
|
||||
self.note_lo().load(Ordering::Relaxed)
|
||||
}
|
||||
fn set_note_lo (&self, x: usize) -> usize {
|
||||
self.note_lo().swap(x, Ordering::Relaxed)
|
||||
}
|
||||
fn note_axis (&self) -> &AtomicUsize;
|
||||
fn get_note_axis (&self) -> usize {
|
||||
self.note_axis().load(Ordering::Relaxed)
|
||||
}
|
||||
fn get_note_hi (&self) -> usize {
|
||||
(self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MidiRange: TimeRange + NoteRange {}
|
||||
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::MidiCursor::default();
|
||||
/// ```
|
||||
#[derive(Debug, Clone)] pub struct MidiCursor {
|
||||
/// Time coordinate of cursor
|
||||
pub time_pos: Arc<AtomicUsize>,
|
||||
/// Note coordinate of cursor
|
||||
pub note_pos: Arc<AtomicUsize>,
|
||||
/// Length of note that will be inserted, in pulses
|
||||
pub note_len: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
///
|
||||
/// ```
|
||||
/// use tek::{TimeRange, NoteRange};
|
||||
/// let model = tek::MidiSelection::from((1, false));
|
||||
///
|
||||
/// let _ = model.get_time_len();
|
||||
/// let _ = model.get_time_zoom();
|
||||
/// let _ = model.get_time_lock();
|
||||
/// let _ = model.get_time_start();
|
||||
/// let _ = model.get_time_axis();
|
||||
/// let _ = model.get_time_end();
|
||||
///
|
||||
/// let _ = model.get_note_lo();
|
||||
/// let _ = model.get_note_axis();
|
||||
/// let _ = model.get_note_hi();
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)] pub struct MidiSelection {
|
||||
pub time_len: Arc<AtomicUsize>,
|
||||
/// Length of visible time axis
|
||||
pub time_axis: Arc<AtomicUsize>,
|
||||
/// Earliest time displayed
|
||||
pub time_start: Arc<AtomicUsize>,
|
||||
/// Time step
|
||||
pub time_zoom: Arc<AtomicUsize>,
|
||||
/// Auto rezoom to fit in time axis
|
||||
pub time_lock: Arc<AtomicBool>,
|
||||
/// Length of visible note axis
|
||||
pub note_axis: Arc<AtomicUsize>,
|
||||
// Lowest note displayed
|
||||
pub note_lo: Arc<AtomicUsize>,
|
||||
}
|
||||
122
src/tek.edn
Normal file
122
src/tek.edn
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
(keys :back (@escape back))
|
||||
(keys :confirm (@enter confirm))
|
||||
(keys :axis/x (@left x/dec) (@right x/inc))
|
||||
(keys :axis/x2 (@shift/left x2/dec) (@shift/right x2/inc))
|
||||
(keys :axis/y (@up y/dec) (@down y/inc))
|
||||
(keys :axis/y2 (@shift/up y2/dec) (@shift/down y2/inc))
|
||||
(keys :axis/z (@minus z/dec) (@equal z/inc))
|
||||
(keys :axis/z2 (@underscore z2/dec) (@plus z2/inc))
|
||||
(keys :axis/i (@comma i/dec) (@period z/inc))
|
||||
(keys :axis/i2 (@lt i2/dec) (@gt z2/inc))
|
||||
(keys :axis/w (@openbracket w/dec) (@closebracket w/inc))
|
||||
(keys :axis/w2 (@openbrace w2/dec) (@closebrace w2/inc))
|
||||
|
||||
(mode :menu
|
||||
(name Menu)
|
||||
(info Mode selector.)
|
||||
(keys :axis/y :confirm)
|
||||
(view (bg (g 0) (bsp/s
|
||||
:ports/out
|
||||
(bsp/n
|
||||
:ports/in
|
||||
(bg (g 30) (bsp/s
|
||||
(fixed/y 7 :logo)
|
||||
(fill :dialog/menu))))))))
|
||||
|
||||
(view :ports/out (fill/x (fixed/y 3 (bsp/a
|
||||
(fill/x (align/w (text L-AUDIO-OUT)))
|
||||
(bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R))))))))
|
||||
(view :ports/in (fill/x (fixed/y 3 (bsp/a
|
||||
(fill/x (align/w (text L-AUDIO-IN)))
|
||||
(bsp/a (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R))))))))
|
||||
(mode :transport (name Transport) (info JACK transport controller.)
|
||||
(keys :clock :global)
|
||||
:transport)
|
||||
(mode :sequencer (name Sequencer) (info MIDI sequencer.)
|
||||
(mode browse (keys :browse))
|
||||
(mode rename (keys :pool/rename))
|
||||
(mode length (keys :pool/length))
|
||||
(keys :editor :clock :global)
|
||||
(bsp/s (fixed/y 1 :transport)
|
||||
(bsp/n (fixed/y 1 :status)
|
||||
(fill (bsp/a (fill/xy (align/e :pool)) :editor)))))
|
||||
(keys :editor (see :axis/i :axis/i2 :axis/y :page :editor/view :editor/add :editor/del))
|
||||
(keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2) (@z toggle :lock))
|
||||
(keys :editor/add (@a editor/append :true) (@enter editor/append :false))
|
||||
(keys :editor/del (@del editor/delete/note) (@shift/del editor/delete/note))
|
||||
(mode :sampler (name Sampler) (info Sample player.)
|
||||
(keys :sampler/directions :sampler/record :sampler/play)
|
||||
(bsp/s (fixed/y 1 :transport) (bsp/n (fixed/y 1 :status) (fill :samples/grid))))
|
||||
(keys :sampler/record
|
||||
(@r sampler/record/toggle :sample/selected)
|
||||
(@shift/R sampler/record/back))
|
||||
(keys :sampler/play
|
||||
(@p sampler/play/sample :sample/selected)
|
||||
(@P sampler/stop/sample :sample/selected))
|
||||
(keys :sampler/import-export
|
||||
(@shift/f6 dialog :dialog/export/sample)
|
||||
(@shift/f9 dialog :dialog/import/sample))
|
||||
(keys :sampler/directions
|
||||
(@up sampler/select :sample/above)
|
||||
(@down sampler/select :sample/below)
|
||||
(@left sampler/select :sample/to/left)
|
||||
(@right sampler/select :sample/to/right))
|
||||
(mode :groovebox (name Groovebox) (info Sequencer with sampler.)
|
||||
(mode browse (keys :browse))
|
||||
(mode rename (keys :pool-rename))
|
||||
(mode length (keys :pool-length))
|
||||
(keys :clock :editor :sampler :global)
|
||||
(bsp/w :meters/output (bsp/e :meters/input (bsp/w :groove/meta :groove/editor))))
|
||||
(view :groove/meta (fill/y (align/n (stack/s :midi-ins/status :midi-outs/status :audio-ins/status :audio-outs/status :pool))))
|
||||
(view :groove/editor (bsp/n :groove/sample :groove/sequence))
|
||||
(view :groove/sample (fixed/y :h-sample-detail (bsp/e (fill/y (fixed/x 20 (align/nw :sample-status))) :sample-viewer)))
|
||||
(view :groove/sequence (bsp/e (fill/y (align/n (bsp/s :status/v :editor-status))) (bsp/e :samples/keys :editor)))
|
||||
(mode :arranger (name Arranger) (info Launch grid.)
|
||||
(mode :editor (keys :editor))
|
||||
(mode :dialog (keys :dialog))
|
||||
(mode :message (keys :message))
|
||||
(mode :add-device (keys :add-device))
|
||||
(mode :browse (keys :browse))
|
||||
(mode :rename (keys :input))
|
||||
(mode :length (keys :rename))
|
||||
(mode :clip (keys :clip))
|
||||
(mode :track (keys :track))
|
||||
(mode :scene (keys :scene))
|
||||
(mode :mix (keys :mix))
|
||||
(keys (see :clock :color :launch :scenes :tracks :global)
|
||||
(@tab project/edit)
|
||||
(@shift/I project/input/add) (@shift/O project/output/add)
|
||||
(@shift/D dialog/show :dialog/device))
|
||||
(bsp/n :status (bsp/w :meters/output (bsp/e :meters/input (bsp/n :tracks/inputs
|
||||
(bsp/s :tracks/outputs (bsp/s :tracks/names (bsp/s :tracks/devices
|
||||
(fill (either :mode/editor (bsp/e :scenes/names :editor) :scenes))))))))))
|
||||
(keys :tracks (@t select :select/track) (@shift/T project/track/add)
|
||||
(@left select :select/track/dec) (@right select :select/track/inc))
|
||||
(keys :track (see :color :launch :axis/z :axis/z2 :delete)
|
||||
(@r toggle :rec) (@m toggle :mon) (@p toggle :play) (@P toggle :solo))
|
||||
(keys :scenes (@s select :select/scene) (@shift/S project/scene/add)
|
||||
(@up select :select/scene/dec) (@down select :select/scene/inc))
|
||||
(keys :scene (see :color :launch :axis/z :axis/z2 :delete))
|
||||
|
||||
(keys :focus)
|
||||
(keys :help (@f1 dialog :help))
|
||||
(keys :page (@pgup page/up) (@pgdn page/down))
|
||||
(keys :delete (@delete delete) (@backspace delete/back))
|
||||
(keys :input (see :axis/x :delete) (:char input))
|
||||
(keys :list (see :axis/y :confirm))
|
||||
(keys :length (see :axis/x :axis/y :confirm))
|
||||
(keys :browse (see :list :input :focus))
|
||||
(keys :history (@u undo 1) (@r redo 1))
|
||||
(keys :saveload (@f6 dialog :save) (@f9 dialog :load))
|
||||
(keys :color (@c color))
|
||||
(keys :launch (@q launch))
|
||||
(keys :clock (@space clock/toggle 0) (@shift/space clock/toggle 0))
|
||||
(keys :global (see :history :saveload) (@f8 dialog :options) (@f10 dialog :quit))
|
||||
(keys :clip (see :color :launch :axis/z :axis/z2 :delete) (@l toggle :loop))
|
||||
(keys :sequencer (see :color :launch) (@shift/I input/add) (@shift/O output/add))
|
||||
(keys :pool (see :axis-y :axis-w :axis/z2 :color :delete)
|
||||
(@n rename/begin) (@t length/begin) (@m import/begin) (@x export/begin)
|
||||
(@shift/A clip/add :after :new/clip) (@shift/D clip/add :after :cloned/clip))
|
||||
(view :browse (bsp/s
|
||||
(padding 3 1 :browse-title)
|
||||
(enclose (fg (g 96)) browser)))
|
||||
1808
src/tek.rs
Normal file
1808
src/tek.rs
Normal file
File diff suppressed because it is too large
Load diff
3586
src/tek_impls.rs
Normal file
3586
src/tek_impls.rs
Normal file
File diff suppressed because it is too large
Load diff
168
src/tick.rs
Normal file
168
src/tick.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
pub trait HasClock: AsRef<Clock> + AsMut<Clock> {
|
||||
fn clock (&self) -> &Clock { self.as_ref() }
|
||||
fn clock_mut (&mut self) -> &mut Clock { self.as_mut() }
|
||||
}
|
||||
|
||||
|
||||
/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::Timebase::default();
|
||||
/// ```
|
||||
#[derive(Debug, Clone)] pub struct Timebase {
|
||||
/// Audio samples per second
|
||||
pub sr: SampleRate,
|
||||
/// MIDI beats per minute
|
||||
pub bpm: Bpm,
|
||||
/// MIDI ticks per beat
|
||||
pub ppq: Ppq,
|
||||
}
|
||||
|
||||
/// Iterator that emits subsequent ticks within a range.
|
||||
///
|
||||
/// ```
|
||||
/// let iter = tek::Ticker::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Ticker {
|
||||
pub spp: f64,
|
||||
pub sample: usize,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::Moment::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone)] pub struct Moment {
|
||||
pub timebase: Arc<Timebase>,
|
||||
/// Current time in microseconds
|
||||
pub usec: Microsecond,
|
||||
/// Current time in audio samples
|
||||
pub sample: SampleCount,
|
||||
/// Current time in MIDI pulses
|
||||
pub pulse: Pulse,
|
||||
}
|
||||
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::Moment2::default();
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)] pub enum Moment2 {
|
||||
#[default] None,
|
||||
Zero,
|
||||
Usec(Microsecond),
|
||||
Sample(SampleCount),
|
||||
Pulse(Pulse),
|
||||
}
|
||||
|
||||
/// MIDI resolution in PPQ (pulses per quarter note)
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64);
|
||||
|
||||
/// Timestamp in MIDI pulses
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64);
|
||||
|
||||
/// Tempo in beats per minute
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64);
|
||||
|
||||
/// Quantization setting for launching clips
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64);
|
||||
|
||||
/// Quantization setting for notes
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64);
|
||||
|
||||
/// Timestamp in audio samples
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64);
|
||||
|
||||
/// Audio sample rate in Hz (samples per second)
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64);
|
||||
|
||||
/// Timestamp in microseconds
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64);
|
||||
|
||||
/// The source of time.
|
||||
///
|
||||
/// ```
|
||||
/// let clock = tek::Clock::default();
|
||||
/// ```
|
||||
#[derive(Clone, Default)] pub struct Clock {
|
||||
/// JACK transport handle.
|
||||
pub transport: Arc<Option<Transport>>,
|
||||
/// Global temporal resolution (shared by [Moment] fields)
|
||||
pub timebase: Arc<Timebase>,
|
||||
/// Current global sample and usec (monotonic from JACK clock)
|
||||
pub global: Arc<Moment>,
|
||||
/// Global sample and usec at which playback started
|
||||
pub started: Arc<RwLock<Option<Moment>>>,
|
||||
/// Playback offset (when playing not from start)
|
||||
pub offset: Arc<Moment>,
|
||||
/// Current playhead position
|
||||
pub playhead: Arc<Moment>,
|
||||
/// Note quantization factor
|
||||
pub quant: Arc<Quantize>,
|
||||
/// Launch quantization factor
|
||||
pub sync: Arc<LaunchSync>,
|
||||
/// Size of buffer in samples
|
||||
pub chunk: Arc<AtomicUsize>,
|
||||
// Cache of formatted strings
|
||||
pub view_cache: Arc<RwLock<ClockView>>,
|
||||
/// For syncing the clock to an external source
|
||||
#[cfg(feature = "port")] pub midi_in: Arc<RwLock<Option<MidiInput>>>,
|
||||
/// For syncing other devices to this clock
|
||||
#[cfg(feature = "port")] pub midi_out: Arc<RwLock<Option<MidiOutput>>>,
|
||||
/// For emitting a metronome
|
||||
#[cfg(feature = "port")] pub click_out: Arc<RwLock<Option<AudioOutput>>>,
|
||||
}
|
||||
|
||||
/// A unit of time, represented as an atomic 64-bit float.
|
||||
///
|
||||
/// According to https://stackoverflow.com/a/873367, as per IEEE754,
|
||||
/// every integer between 1 and 2^53 can be represented exactly.
|
||||
/// This should mean that, even at 192kHz sampling rate, over 1 year of audio
|
||||
/// can be clocked in microseconds with f64 without losing precision.
|
||||
pub trait TimeUnit: InteriorMutable<f64> {}
|
||||
|
||||
/// Contains memoized renders of clock values.
|
||||
///
|
||||
/// Performance optimization.
|
||||
#[derive(Debug)] pub struct ClockView {
|
||||
pub sr: Memo<Option<(bool, f64)>, String>,
|
||||
pub buf: Memo<Option<f64>, String>,
|
||||
pub lat: Memo<Option<f64>, String>,
|
||||
pub bpm: Memo<Option<f64>, String>,
|
||||
pub beat: Memo<Option<f64>, String>,
|
||||
pub time: Memo<Option<f64>, String>,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue