Compare commits

..

No commits in common. "9ef63324af597e72de1fd59675e72af14b423e0d" and "0577309b19c4cc77525f44877eac4b0d9b219884" have entirely different histories.

26 changed files with 7150 additions and 7072 deletions

View file

@ -4,11 +4,11 @@ edition = "2024"
version = "0.3.0" version = "0.3.0"
[lib] [lib]
path = "src/tek.rs" path = "app/tek.rs"
[[bin]] [[bin]]
name = "tek" name = "tek"
path = "src/tek.rs" path = "app/tek.rs"
[target.'cfg(target_os = "linux")'] [target.'cfg(target_os = "linux")']
rustflags = ["-C", "link-arg=-fuse-ld=mold"] rustflags = ["-C", "link-arg=-fuse-ld=mold"]

1214
app/tek.rs Normal file

File diff suppressed because it is too large Load diff

3947
app/tek_impls.rs Normal file

File diff suppressed because it is too large Load diff

1042
app/tek_struct.rs Normal file

File diff suppressed because it is too large Load diff

919
app/tek_trait.rs Normal file
View file

@ -0,0 +1,919 @@
use crate::*;
use std::sync::atomic::Ordering;
pub trait Gettable<T> {
/// Returns current value
fn get (&self) -> T;
}
pub trait Mutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&mut self, value: T) -> T;
}
pub trait InteriorMutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&self, value: T) -> T;
}
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 {}
/// 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> {}
pub trait HasClipsSize { fn clips_size (&self) -> &Measure<TuiOut>; }
pub trait HasMidiClip {
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>>;
}
pub trait HasClock: AsRef<Clock> + AsMut<Clock> {
fn clock (&self) -> &Clock { self.as_ref() }
fn clock_mut (&mut self) -> &mut Clock { self.as_mut() }
}
pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
fn devices (&self) -> &Vec<Device> { self.as_ref() }
fn devices_mut (&mut self) -> &mut Vec<Device> { self.as_mut() }
}
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 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 HasWidth {
const MIN_WIDTH: usize;
/// Increment track width.
fn width_inc (&mut self);
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
fn width_dec (&mut self);
}
pub trait HasMidiBuffers {
fn note_buf_mut (&mut self) -> &mut Vec<u8>;
fn midi_buf_mut (&mut self) -> &mut Vec<Vec<Vec<u8>>>;
}
/// ```
/// 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 }
}
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)
}
}
/// 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 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 Content<TuiOut> + '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 Content<TuiOut> + '_ {
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 Content<TuiOut> {
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 Content<TuiOut> {
self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
}
}
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 Content<TuiOut> {
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();
FieldV(color, "Now:", format!("{} {}", time, name))
}
fn next_status (&self) -> impl Content<TuiOut> {
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();
}
};
FieldV(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: Measured<TuiOut> + 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 trait ClipsView: TracksView + ScenesView {
fn view_scenes_clips <'a> (&'a self)
-> impl Content<TuiOut> + 'a
{
self.clips_size().of(Fill::XY(Bsp::a(
Fill::XY(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))),
Thunk::new(|to: &mut TuiOut|for (
track_index, track, _, _
) in self.tracks_with_sizes() {
to.place(&Fixed::X(track.width as u16,
Fill::Y(self.view_track_clips(track_index, track))))
}))))
}
fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Content<TuiOut> + 'a {
Thunk::new(move|to: &mut TuiOut|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(&Fixed::XY(w, y, Bsp::b(
Fill::XY(Outer(true, Style::default().fg(outline))),
Fill::XY(Bsp::b(
Bsp::b(
Tui::fg_bg(outline, bg, Fill::XY("")),
Fill::XY(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))),
),
Fill::XY(When::new(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<TuiOut> {
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 Content<TuiOut> {
let track_count = self.tracks().len();
let scene_count = self.scenes().len();
let selected = self.selection();
let button = Bsp::s(
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 = Bsp::s(
button_2("T", "+", false),
button_2("S", "+", false));
view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb,
Fixed::Y(2, Thunk::new(|to: &mut TuiOut|{
for (index, track, x1, _x2) in self.tracks_with_sizes() {
to.place(&Push::X(x1 as u16, Fixed::X(track_width(index, track),
Tui::bg(if selected.track() == Some(index) {
track.color.light.rgb
} else {
track.color.base.rgb
}, Bsp::s(Fill::X(Align::nw(Bsp::e(
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 Content<TuiOut> {
view_track_row_section(theme,
Bsp::s(Fill::X(Align::w(button_2("o", "utput", false))),
Thunk::new(|to: &mut TuiOut|for port in self.midi_outs().iter() {
to.place(&Fill::X(Align::w(port.port_name())));
})),
button_2("O", "+", false),
Tui::bg(theme.darker.rgb, Align::w(Thunk::new(|to: &mut TuiOut|{
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
to.place(&Fixed::X(track_width(index, track),
Align::nw(Fill::Y(Map::south(1, ||track.sequencer.midi_outs.iter(),
|port, index|Tui::fg(Rgb(255, 255, 255),
Fixed::Y(1, Tui::bg(track.color.dark.rgb, Fill::X(Align::w(
format!("·o{index:02} {}", port.port_name())))))))))));}}))))
}
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
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 TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() {
to.place(&Fixed::XY(track_width(index, track), h + 1,
Align::nw(Bsp::s(
Tui::bg(track.color.base.rgb,
Fill::X(Align::w(row!(
Either::new(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "),
Either::new(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "),
Either::new(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "),
)))),
Map::south(1, ||track.sequencer.midi_ins.iter(),
|port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb,
Fill::X(Align::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, Align::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 Content<TuiOut> {
Fixed::X(20, Thunk::new(|to: &mut TuiOut|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 Content<TuiOut> + '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 = Fill::X(Align::w(Bsp::e(format!("·s{index:02} "),
Tui::fg(Tui::g(255), Tui::bold(true, &scene.name)))));
let b = When::new(self.selection().scene() == Some(index) && self.is_editing(),
Fill::XY(Align::nw(Bsp::s(
self.editor().as_ref().map(|e|e.clip_status()),
self.editor().as_ref().map(|e|e.edit_status())))));
Fixed::XY(20, h, Tui::bg(bg, Align::nw(Bsp::s(a, b))))
}
}
/// 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
}))
}
}

25
app/tek_type.rs Normal file
View file

@ -0,0 +1,25 @@
use crate::*;
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 type SceneWith<'a, T> =
(usize, &'a Scene, usize, usize, T);
pub type MidiSample =
(Option<u7>, Arc<RwLock<crate::Sample>>);
/// Collection of interaction modes.
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
/// Collection of input bindings.
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
/// Collection of view definitions.
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;

View file

@ -1,833 +0,0 @@
use ::std::sync::{Arc, RwLock};
use ::tengri::{space::east, color::ItemTheme};
use ::tengri::{draw::*, term::*};
use crate::{*, device::*, sequence::*, clock::*, select::*, sample::*};
impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } }
/// 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,
}
/// 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>,
}
/// 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>>>>,
}
impl_has!(Jack<'static>: |self: Arrangement| self.jack);
impl_has!(Measure<Tui>: |self: Arrangement| self.size);
impl_has!(Vec<Track>: |self: Arrangement| self.tracks);
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins);
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs);
impl_has!(Clock: |self: Arrangement| self.clock);
impl_has!(Selection: |self: Arrangement| self.selection);
impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref());
impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut());
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
impl <T: AsRef<Vec<Scene>>+AsMut<Vec<Scene>>> HasScenes for T {}
impl <T: AsRef<Vec<Track>>+AsMut<Vec<Track>>> HasTracks for T {}
impl <T: AsRefOpt<Scene>+AsMutOpt<Scene>+Send+Sync> HasScene for T {}
impl <T: AsRefOpt<Track>+AsMutOpt<Track>+Send+Sync> HasTrack for T {}
impl <T: ScenesView+HasMidiIns+HasMidiOuts+HasTrackScroll+Measured<Tui>> TracksView for T {}
impl <T: TracksView+ScenesView+Send+Sync> ClipsView for T {}
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 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()))
}
}
impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref());
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
#[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt());
#[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt());
impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } }
impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } }
impl HasWidth for Track {
const MIN_WIDTH: usize = 9;
fn width_inc (&mut self) { self.width += 1; }
fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } }
}
impl Track {
/// Create a new track with only the default [Sequencer].
pub fn new (
name: &impl AsRef<str>,
color: Option<ItemTheme>,
jack: &Jack<'static>,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[Connect],
midi_to: &[Connect],
) -> Usually<Self> {
Ok(Self {
name: name.as_ref().into(),
color: color.unwrap_or_default(),
sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?,
..Default::default()
})
}
pub fn audio_ins (&self) -> &[AudioInput] {
self.devices.first().map(|x|x.audio_ins()).unwrap_or_default()
}
pub fn audio_outs (&self) -> &[AudioOutput] {
self.devices.last().map(|x|x.audio_outs()).unwrap_or_default()
}
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
fn _todo_usize_stub_ (&self) -> usize { todo!() }
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
pub fn per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
iter(tracks,
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
let width = (x2 - x1) as u16;
iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg(
track.color.lightest.rgb,
track.color.base.rgb,
callback(index, track))))})
}
/// Create a new track connecting the [Sequencer] to a [Sampler].
#[cfg(feature = "sampler")] pub fn new_with_sampler (
name: &impl AsRef<str>,
color: Option<ItemTheme>,
jack: &Jack<'static>,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[Connect],
midi_to: &[Connect],
audio_from: &[&[Connect];2],
audio_to: &[&[Connect];2],
) -> Usually<Self> {
let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?;
let client_name = jack.with_client(|c|c.name().to_string());
let port_name = track.sequencer.midi_outs[0].port_name();
let connect = [Connect::exact(format!("{client_name}:{}", port_name))];
track.devices.push(Device::Sampler(Sampler::new(
jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to
)?));
Ok(track)
}
#[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> {
for device in self.devices.iter() {
match device {
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
_ => {}
}
}
None
}
#[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> {
for device in self.devices.iter_mut() {
match device {
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
_ => {}
}
}
None
}
}
pub fn per_track <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track))))
}
pub fn per_track_top <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
origin_x(Tui::bg(Reset, iter(tracks,
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
let width = (x2 - x1) as u16;
iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg(
track.color.lightest.rgb,
track.color.base.rgb,
callback(index, track))))})))
}
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene());
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut());
impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } }
impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } }
impl ScenesView for App {
fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) }
fn w_side (&self) -> u16 { 20 }
fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) }
}
impl Scene {
/// Returns the pulse length of the longest clip in the scene
pub fn pulses (&self) -> usize {
self.clips.iter().fold(0, |a, p|{
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
})
}
/// Returns true if all clips in the scene are
/// currently playing on the given collection of tracks.
pub fn is_playing (&self, tracks: &[Track]) -> bool {
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
.all(|(track_index, clip)|match clip {
Some(c) => tracks
.get(track_index)
.map(|track|{
if let Some((_, Some(clip))) = track.sequencer().play_clip() {
*clip.read().unwrap() == *c.read().unwrap()
} else {
false
}
})
.unwrap_or(false),
None => true
})
}
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
}
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
fn _todo_usize_stub_ (&self) -> usize { todo!() }
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
}
impl Arrangement {
/// Create a new arrangement.
pub fn new (
jack: &Jack<'static>,
name: Option<Arc<str>>,
clock: Clock,
tracks: Vec<Track>,
scenes: Vec<Scene>,
midi_ins: Vec<MidiInput>,
midi_outs: Vec<MidiOutput>,
) -> Self {
Self {
clock, tracks, scenes, midi_ins, midi_outs,
jack: jack.clone(),
name: name.unwrap_or_default(),
color: ItemTheme::random(),
selection: Selection::TrackClip { track: 0, scene: 0 },
..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)
}
/// Add multiple tracks
#[cfg(feature = "track")] pub fn tracks_add (
&mut self,
count: usize, width: Option<usize>,
mins: &[Connect], mouts: &[Connect],
) -> Usually<()> {
let track_color_1 = ItemColor::random();
let track_color_2 = ItemColor::random();
for i in 0..count {
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
let track = self.track_add(None, Some(color), mins, mouts)?.1;
if let Some(width) = width {
track.width = width;
}
}
Ok(())
}
/// Add a track
#[cfg(feature = "track")] pub fn track_add (
&mut self,
name: Option<&str>, color: Option<ItemTheme>,
mins: &[Connect], mouts: &[Connect],
) -> Usually<(usize, &mut Track)> {
let name: Arc<str> = name.map_or_else(
||format!("trk{:02}", self.track_last).into(),
|x|x.to_string().into()
);
self.track_last += 1;
let track = Track {
width: (name.len() + 2).max(12),
color: color.unwrap_or_else(ItemTheme::random),
sequencer: Sequencer::new(
&format!("{name}"),
self.jack(),
Some(self.clock()),
None,
mins,
mouts
)?,
name,
..Default::default()
};
self.tracks_mut().push(track);
let len = self.tracks().len();
let index = len - 1;
for scene in self.scenes_mut().iter_mut() {
while scene.clips.len() < len {
scene.clips.push(None);
}
}
Ok((index, &mut self.tracks_mut()[index]))
}
#[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw<Tui> + '_ {
south(
h_exact(1, self.view_inputs_header()),
Thunk::new(|to: &mut Tui|{
for (index, port) in self.midi_ins().iter().enumerate() {
to.place(&x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port))))
}
})
)
}
#[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw<Tui> + '_ {
east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))),
west(w_exact(4, button_2("I", "+", false)), Thunk::new(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() {
#[cfg(feature = "track")]
to.place(&x_push(x1 as u16, Tui::bg(track.color.dark.rgb, origin_w(w_exact(track.width as u16, 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 "),
))))))
})))
}
#[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw<Tui> {
east(w_exact(20, origin_w(east("", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))),
west(w_exact(4, ()), Thunk::new(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() {
#[cfg(feature = "track")]
to.place(&Tui::bg(track.color.darker.rgb, origin_w(w_exact(track.width as u16, east!(
either(track.sequencer.monitoring, Tui::fg(Green, ""), " · "),
either(track.sequencer.recording, Tui::fg(Red, ""), " · "),
either(track.sequencer.overdub, Tui::fg(Yellow, ""), " · "),
)))))
})))
}
#[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> {
let mut h = 1;
for output in self.midi_outs().iter() {
h += 1 + output.connections.len();
}
let h = h as u16;
let list = south(
h_exact(1, w_full(origin_w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))),
h_exact(h - 1, wh_full(origin_nw(Thunk::new(|to: &mut Tui|{
for (_index, port) in self.midi_outs().iter().enumerate() {
to.place(&h_exact(1,w_full(east(
origin_w(east("", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))),
w_full(origin_e(format!("{}/{} ",
port.port().get_connections().len(),
port.connections.len())))))));
for (index, conn) in port.connections.iter().enumerate() {
to.place(&h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info())))));
}
}
})))));
h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false),
Tui::bg(theme.darker.rgb, origin_w(w_full(
Thunk::new(|to: &mut Tui|{
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
to.place(&w_exact(track_width(index, track),
Thunk::new(|to: &mut Tui|{
to.place(&h_exact(1, origin_w(east(
either(true, Tui::fg(Green, "play "), "play "),
either(false, Tui::fg(Yellow, "solo "), "solo "),
))));
for (_index, port) in self.midi_outs().iter().enumerate() {
to.place(&h_exact(1, origin_w(east(
either(true, Tui::fg(Green, ""), " · "),
either(false, Tui::fg(Yellow, ""), " · "),
))));
for (_index, _conn) in port.connections.iter().enumerate() {
to.place(&h_exact(1, w_full("")));
}
}})))}}))))))
}
#[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> {
let mut h = 2u16;
for track in self.tracks().iter() {
h = h.max(track.devices.len() as u16 * 2);
}
view_track_row_section(theme,
button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false),
button_2("D", "+", false),
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,
Tui::bg(track.color.dark.rgb, origin_nw(iter_south(2, move||0..h,
|_, _index|wh_exact(track.width as u16, 2,
Tui::fg_bg(
ItemTheme::G[32].lightest.rgb,
ItemTheme::G[32].dark.rgb,
origin_nw(format!(" · {}", "--")))))))));
}))
}
/// Put a clip in a slot
#[cfg(feature = "clip")] pub fn clip_put (
&mut self, track: usize, scene: usize, clip: Option<Arc<RwLock<MidiClip>>>
) -> Option<Arc<RwLock<MidiClip>>> {
let old = self.scenes[scene].clips[track].clone();
self.scenes[scene].clips[track] = clip;
old
}
/// Change the color of a clip, returning the previous one
#[cfg(feature = "clip")] pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme)
-> Option<ItemTheme>
{
self.scenes[scene].clips[track].as_ref().map(|clip|{
let mut clip = clip.write().unwrap();
let old = clip.color.clone();
clip.color = color.clone();
panic!("{color:?} {old:?}");
//old
})
}
/// Toggle looping for the active clip
#[cfg(feature = "clip")] pub fn toggle_loop (&mut self) {
if let Some(clip) = self.selected_clip() {
clip.write().unwrap().toggle_loop()
}
}
/// Get the first sampler of the active track
#[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> {
self.selected_track()?.sampler(0)
}
/// Get the first sampler of the active track
#[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
self.selected_track_mut()?.sampler_mut(0)
}
}
impl ScenesView for Arrangement {
fn h_scenes (&self) -> u16 {
(self.measure_height() as u16).saturating_sub(20)
}
fn w_side (&self) -> u16 {
(self.measure_width() as u16 * 2 / 10).max(20)
}
fn w_mid (&self) -> u16 {
(self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40)
}
}
impl HasClipsSize for Arrangement {
fn clips_size (&self) -> &Measure<Tui> { &self.size_inner }
}
pub type SceneWith<'a, T> =
(usize, &'a Scene, usize, usize, T);
def_command!(SceneCommand: |scene: Scene| {
SetSize { size: usize } => { todo!() },
SetZoom { size: usize } => { todo!() },
SetName { name: Arc<str> } =>
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
SetColor { color: ItemTheme } =>
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
});
def_command!(TrackCommand: |track: Track| {
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
SetMute { mute: Option<bool> } => todo!(),
SetSolo { solo: Option<bool> } => todo!(),
SetSize { size: usize } => todo!(),
SetZoom { zoom: usize } => todo!(),
SetName { name: Arc<str> } =>
swap_value(&mut track.name, name, |name|Self::SetName { name }),
SetColor { color: ItemTheme } =>
swap_value(&mut track.color, color, |color|Self::SetColor { color }),
SetRec { rec: Option<bool> } =>
toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
SetMon { mon: Option<bool> } =>
toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
});
def_command!(ClipCommand: |clip: MidiClip| {
SetColor { color: Option<ItemTheme> } => {
//(SetColor [t: usize, s: usize, c: ItemTheme]
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
todo!()
},
SetLoop { looping: Option<bool> } => {
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
todo!()
}
});

View file

@ -1,130 +0,0 @@
use crate::*;
/// A control axis.
///
/// ```
/// let axis = tek::ControlAxis::X;
/// ```
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
X, Y, Z, I
}
/// Collection of input bindings.
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
pub(crate) fn load_bind (binds: &Binds, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
binds.write().unwrap().insert(name.as_ref().into(), Bind::load(body)?);
Ok(())
}
/// An map of input events (e.g. [TuiEvent]) to [Binding]s.
///
/// ```
/// let lang = "(@x (nop)) (@y (nop) (nop))";
/// let bind = tek::Bind::<tek::tengri::TuiEvent, std::sync::Arc<str>>::load(&lang).unwrap();
/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1));
/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2));
/// ```
#[derive(Debug)] pub struct Bind<E, C>(
/// Map of each event (e.g. key combination) to
/// all command expressions bound to it by
/// all loaded input layers.
pub BTreeMap<E, Vec<Binding<C>>>
);
/// A sequence of zero or more commands (e.g. [AppCommand]),
/// optionally filtered by [Condition] to form layers.
///
/// ```
/// //FIXME: Why does it overflow?
/// //let binding: Binding<()> = tek::Binding { ..Default::default() };
/// ```
#[derive(Debug, Clone)] pub struct Binding<C> {
pub commands: Arc<[C]>,
pub condition: Option<Condition>,
pub description: Option<Arc<str>>,
pub source: Option<Arc<PathBuf>>,
}
/// Condition that must evaluate to true in order to enable an input layer.
///
/// ```
/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true})));
/// ```
#[derive(Clone)] pub struct Condition(
pub Arc<Box<dyn Fn()->bool + Send + Sync>>
);
impl Bind<TuiEvent, Arc<str>> {
pub fn load (lang: &impl Language) -> Usually<Self> {
let mut map = Bind::new();
lang.each(|item|if item.expr().head() == Ok(Some("see")) {
// TODO
Ok(())
} else if let Ok(Some(_word)) = item.expr().head().word() {
if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? {
map.add(key, Binding {
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
condition: None,
description: None,
source: None
});
Ok(())
} else if Some(":char") == item.expr()?.head()? {
// TODO
return Ok(())
} else {
return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into())
}
} else {
return Err(format!("Config::load_bind: unexpected: {item:?}").into())
})?;
Ok(map)
}
}
/// Default is always empty map regardless if `E` and `C` implement [Default].
impl<E, C> Default for Bind<E, C> {
fn default () -> Self { Self(Default::default()) }
}
impl<C: Default> Default for Binding<C> {
fn default () -> Self {
Self {
commands: Default::default(),
condition: Default::default(),
description: Default::default(),
source: Default::default(),
}
}
}
impl<E: Clone + Ord, C> Bind<E, C> {
/// Create a new event map
pub fn new () -> Self {
Default::default()
}
/// Add a binding to an owned event map.
pub fn def (mut self, event: E, binding: Binding<C>) -> Self {
self.add(event, binding);
self
}
/// Add a binding to an event map.
pub fn add (&mut self, event: E, binding: Binding<C>) -> &mut Self {
if !self.0.contains_key(&event) {
self.0.insert(event.clone(), Default::default());
}
self.0.get_mut(&event).unwrap().push(binding);
self
}
/// Return the binding(s) that correspond to an event.
pub fn query (&self, event: &E) -> Option<&[Binding<C>]> {
self.0.get(event).map(|x|x.as_slice())
}
/// Return the first binding that corresponds to an event, considering conditions.
pub fn dispatch (&self, event: &E) -> Option<&Binding<C>> {
self.query(event)
.map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next())
.flatten()
}
}
impl_debug!(Condition |self, w| { write!(w, "*") });

View file

@ -1,529 +0,0 @@
use crate::{*, clock::*, sequence::*, sample::*};
def_command!(FileBrowserCommand: |sampler: Sampler|{
//("begin" [] Some(Self::Begin))
//("cancel" [] Some(Self::Cancel))
//("confirm" [] Some(Self::Confirm))
//("select" [i: usize] Some(Self::Select(i.expect("no index"))))
//("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path"))))
//("filter" [f: Arc<str>] Some(Self::Filter(f.expect("no filter")))))
});
/// 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,
}
has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone()));
impl_has_clips!(|self: Pool|self.clips);
impl_from!(Pool: |clip:&Arc<RwLock<MidiClip>>|{
let model = Self::default();
model.clips.write().unwrap().push(clip.clone());
model.clip.store(1, Relaxed);
model
});
impl_default!(Pool: Self {
browse: None,
clip: 0.into(),
clips: Arc::from(RwLock::from(vec![])),
mode: None,
samples: Arc::from(RwLock::from(vec![])),
visible: true,
});
impl Pool {
pub fn clip_index (&self) -> usize {
self.clip.load(Relaxed)
}
pub fn set_clip_index (&self, value: usize) {
self.clip.store(value, Relaxed);
}
pub fn mode (&self) -> &Option<PoolMode> {
&self.mode
}
pub fn mode_mut (&mut self) -> &mut Option<PoolMode> {
&mut self.mode
}
pub fn begin_clip_length (&mut self) {
let length = self.clips()[self.clip_index()].read().unwrap().length;
*self.mode_mut() = Some(PoolMode::Length(
self.clip_index(),
length,
ClipLengthFocus::Bar
));
}
pub fn begin_clip_rename (&mut self) {
let name = self.clips()[self.clip_index()].read().unwrap().name.clone();
*self.mode_mut() = Some(PoolMode::Rename(
self.clip_index(),
name
));
}
pub fn begin_import (&mut self) -> Usually<()> {
*self.mode_mut() = Some(PoolMode::Import(
self.clip_index(),
Browse::new(None)?
));
Ok(())
}
pub fn begin_export (&mut self) -> Usually<()> {
*self.mode_mut() = Some(PoolMode::Export(
self.clip_index(),
Browse::new(None)?
));
Ok(())
}
pub fn new_clip (&self) -> MidiClip {
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random()))
}
pub fn cloned_clip (&self) -> MidiClip {
let index = self.clip_index();
let mut clip = self.clips()[index].read().unwrap().duplicate();
clip.color = ItemTheme::random_near(clip.color, 0.25);
clip
}
pub fn add_new_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
let clip = Arc::new(RwLock::new(self.new_clip()));
let index = {
let mut clips = self.clips.write().unwrap();
clips.push(clip.clone());
clips.len().saturating_sub(1)
};
self.clip.store(index, Relaxed);
(index, clip)
}
pub fn delete_clip (&mut self, clip: &MidiClip) -> bool {
let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip);
if let Some(index) = index {
self.clips.write().unwrap().remove(index);
return true
}
false
}
}
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, }
}
}
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
}
pub fn bars_string (&self) -> Arc<str> {
format!("{}", self.bars()).into()
}
pub fn beats_string (&self) -> Arc<str> {
format!("{}", self.beats()).into()
}
pub fn ticks_string (&self) -> Arc<str> {
format!("{:>02}", self.ticks()).into()
}
}
impl Pool {
fn _todo_usize_ (&self) -> usize { todo!() }
fn _todo_bool_ (&self) -> bool { todo!() }
fn _todo_clip_ (&self) -> MidiClip { todo!() }
fn _todo_path_ (&self) -> PathBuf { todo!() }
fn _todo_color_ (&self) -> ItemColor { todo!() }
fn _todo_str_ (&self) -> Arc<str> { todo!() }
fn _clip_new (&self) -> MidiClip { self.new_clip() }
fn _clip_cloned (&self) -> MidiClip { self.cloned_clip() }
fn _clip_index_current (&self) -> usize { 0 }
fn _clip_index_after (&self) -> usize { 0 }
fn _clip_index_previous (&self) -> usize { 0 }
fn _clip_index_next (&self) -> usize { 0 }
fn _color_random (&self) -> ItemColor { ItemColor::random() }
}
impl<'a> PoolView<'a> {
fn tui (&self) -> impl Draw<Tui> {
let Self(pool) = self;
//let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into());
//let on_bg = |x|x;//below(Repeat(" "), Tui::bg(color.darkest.rgb, x));
//let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
//let height = pool.clips.read().unwrap().len() as u16;
w_exact(20, h_full(origin_n(iter(
||pool.clips().clone().into_iter(),
move|clip: Arc<RwLock<MidiClip>>, i: usize|{
let item_height = 1;
let item_offset = i as u16 * item_height;
let selected = i == pool.clip_index();
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
let bg = if selected { color.light.rgb } else { color.base.rgb };
let fg = color.lightest.rgb;
let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
let length = if false { String::default() } else { format!("{length} ") };
h_exact(1, iter_south(item_offset, item_height, Tui::bg(bg, below!(
w_full(origin_w(Tui::fg(fg, Tui::bold(selected, name)))),
w_full(origin_e(Tui::fg(fg, Tui::bold(selected, length)))),
w_full(origin_w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), ""))))),
w_full(origin_e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), ""))))),
))))
}))))
}
}
impl ClipLength {
fn tui (&self) -> impl Draw<Tui> {
use ClipLengthFocus::*;
let bars = ||self.bars_string();
let beats = ||self.beats_string();
let ticks = ||self.ticks_string();
match self.focus {
None => east!(" ", bars(), ".", beats(), ".", ticks()),
Some(Bar) => east!("[", bars(), "]", beats(), ".", ticks()),
Some(Beat) => east!(" ", bars(), "[", beats(), "]", ticks()),
Some(Tick) => east!(" ", bars(), ".", beats(), "[", ticks()),
}
}
}
impl Browse {
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
let mut dirs = vec![];
let mut files = vec![];
for entry in std::fs::read_dir(&cwd)? {
let entry = entry?;
let name = entry.file_name();
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
let meta = entry.metadata()?;
if meta.is_dir() {
dirs.push((name, format!("📁 {decoded}")));
} else if meta.is_file() {
files.push((name, format!("📄 {decoded}")));
}
}
Ok(Self { cwd, dirs, files, ..Default::default() })
}
pub fn chdir (&self) -> Usually<Self> { Self::new(Some(self.path())) }
pub fn len (&self) -> usize { self.dirs.len() + self.files.len() }
pub fn is_dir (&self) -> bool { self.index < self.dirs.len() }
pub fn is_file (&self) -> bool { self.index >= self.dirs.len() }
pub fn path (&self) -> PathBuf {
self.cwd.join(if self.is_dir() {
&self.dirs[self.index].0
} else if self.is_file() {
&self.files[self.index - self.dirs.len()].0
} else {
unreachable!()
})
}
fn _todo_stub_path_buf (&self) -> PathBuf { todo!() }
fn _todo_stub_usize (&self) -> usize { todo!() }
fn _todo_stub_arc_str (&self) -> Arc<str> { todo!() }
}
impl Browse {
fn tui (&self) -> impl Draw<Tui> {
iter_south(1, ||EntriesIterator {
offset: 0,
index: 0,
length: self.dirs.len() + self.files.len(),
browser: self,
}, |entry, _index|w_full(origin_w(entry)))
}
}
impl<'a> Iterator for EntriesIterator<'a> {
type Item = Modify<&'a str>;
fn next (&mut self) -> Option<Self::Item> {
let dirs = self.browser.dirs.len();
let files = self.browser.files.len();
let index = self.index;
if self.index < dirs {
self.index += 1;
Some(Tui::bold(true, self.browser.dirs[index].1.as_str()))
} else if self.index < dirs + files {
self.index += 1;
Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str()))
} else {
None
}
}
}
impl PartialEq for BrowseTarget {
fn eq (&self, other: &Self) -> bool {
match self {
Self::ImportSample(_) => false,
Self::ExportSample(_) => false,
Self::ImportClip(_) => false,
Self::ExportClip(_) => false,
#[allow(unused)] t => matches!(other, t)
}
}
}
def_command!(BrowseCommand: |browse: Browse| {
SetVisible => Ok(None),
SetPath { address: PathBuf } => Ok(None),
SetSearch { filter: Arc<str> } => Ok(None),
SetCursor { cursor: usize } => Ok(None),
});
def_command!(PoolCommand: |pool: Pool| {
// Toggle visibility of pool
Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) },
// Select a clip from the clip pool
Select { index: usize } => { pool.set_clip_index(*index); Ok(None) },
// Update the contents of the clip pool
Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})),
// Rename a clip
Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?),
// Change the length of a clip
Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?),
// Import from file
Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
command.delegate(browse, |command|Self::Import{command})?
} else {
None
}),
// Export to file
Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
command.delegate(browse, |command|Self::Export{command})?
} else {
None
}),
});
def_command!(PoolClipCommand: |pool: Pool| {
Delete { index: usize } => {
let index = *index;
let clip = pool.clips_mut().remove(index).read().unwrap().clone();
Ok(Some(Self::Add { index, clip }))
},
Swap { index: usize, other: usize } => {
let index = *index;
let other = *other;
pool.clips_mut().swap(index, other);
Ok(Some(Self::Swap { index, other }))
},
Export { index: usize, path: PathBuf } => {
todo!("export clip to midi file");
},
Add { index: usize, clip: MidiClip } => {
let index = *index;
let mut index = index;
let clip = Arc::new(RwLock::new(clip.clone()));
let mut clips = pool.clips_mut();
if index >= clips.len() {
index = clips.len();
clips.push(clip)
} else {
clips.insert(index, clip);
}
Ok(Some(Self::Delete { index }))
},
Import { index: usize, path: PathBuf } => {
let index = *index;
let bytes = std::fs::read(&path)?;
let smf = Smf::parse(bytes.as_slice())?;
let mut t = 0u32;
let mut events = vec![];
for track in smf.tracks.iter() {
for event in track.iter() {
t += event.delta.as_int();
if let TrackEventKind::Midi { channel, message } = event.kind {
events.push((t, channel.as_int(), message));
}
}
}
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
for event in events.iter() {
clip.notes[event.0 as usize].push(event.2);
}
Ok(Self::Add { index, clip }.execute(pool)?)
},
SetName { index: usize, name: Arc<str> } => {
let index = *index;
let clip = &mut pool.clips_mut()[index];
let old_name = clip.read().unwrap().name.clone();
clip.write().unwrap().name = name.clone();
Ok(Some(Self::SetName { index, name: old_name }))
},
SetLength { index: usize, length: usize } => {
let index = *index;
let clip = &mut pool.clips_mut()[index];
let old_len = clip.read().unwrap().length;
clip.write().unwrap().length = *length;
Ok(Some(Self::SetLength { index, length: old_len }))
},
SetColor { index: usize, color: ItemColor } => {
let index = *index;
let mut color = ItemTheme::from(*color);
std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color);
Ok(Some(Self::SetColor { index, color: color.base }))
},
});
def_command!(RenameCommand: |pool: Pool| {
Begin => unreachable!(),
Cancel => {
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
pool.clips()[clip].write().unwrap().name = old_name.clone().into();
}
Ok(None)
},
Confirm => {
if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() {
let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name }))
}
Ok(None)
},
Set { value: Arc<str> } => {
if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() {
pool.clips()[clip].write().unwrap().name = value.clone();
}
Ok(None)
},
});
def_command!(CropCommand: |pool: Pool| {
Begin => unreachable!(),
Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) },
Set { length: usize } => {
if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus))
= pool.mode_mut().clone()
{
let old_length;
{
let clip = pool.clips()[clip].clone();//.write().unwrap();
old_length = Some(clip.read().unwrap().length);
clip.write().unwrap().length = *length;
}
*pool.mode_mut() = None;
return Ok(old_length.map(|length|Self::Set { length }))
}
Ok(None)
},
Next => {
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None)
},
Prev => {
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None)
},
Inc => {
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
match focus {
ClipLengthFocus::Bar => { *length += 4 * PPQ },
ClipLengthFocus::Beat => { *length += PPQ },
ClipLengthFocus::Tick => { *length += 1 },
}
}
Ok(None)
},
Dec => {
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
match focus {
ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) },
ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) },
ClipLengthFocus::Tick => { *length = length.saturating_sub(1) },
}
}
Ok(None)
}
});

View file

@ -1,253 +0,0 @@
use crate::{*, arrange::*, clock::*, config::*, device::*};
/// The command-line interface descriptor.
///
/// ```
/// let cli: tek::Cli = Default::default();
///
/// use clap::CommandFactory;
/// tek::Cli::command().debug_assert();
/// ```
#[derive(Parser)]
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
#[derive(Debug, Default)] pub struct Cli {
/// Pre-defined configuration modes.
///
/// TODO: Replace these with scripted configurations.
#[command(subcommand)] pub action: Action,
}
/// Application modes that can be passed to the mommand line interface.
///
/// ```
/// let action: tek::Action = Default::default();
/// ```
#[derive(Debug, Clone, Subcommand, Default)] pub enum Action {
/// Continue where you left off
#[default] Resume,
/// Run headlessly in current session.
Headless,
/// Show status of current session.
Status,
/// List known sessions.
List,
/// Continue work in a copy of the current session.
Fork,
/// Create a new empty session.
New {
/// 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 create
#[arg(short='t', long)] tracks: Option<usize>,
/// Scenes to create
#[arg(short='s', long)] scenes: Option<usize>,
},
/// Import media as new session.
Import,
/// Show configuration.
Config,
/// Show version.
Version,
}
/// Command-line configuration.
#[cfg(feature = "cli")] impl Cli {
pub fn run (&self) -> Usually<()> {
if let Action::Version = self.action {
return Ok(tek_show_version())
}
let mut config = Config::new(None);
config.init()?;
if let Action::Config = self.action {
tek_print_config(&config);
} else if let Action::List = self.action {
todo!("list sessions")
} else if let Action::Resume = self.action {
todo!("resume session")
} else if let Action::New {
name, bpm, tracks, scenes, sync_lead, sync_follow,
midi_from, midi_from_re, midi_to, midi_to_re,
left_from, right_from, left_to, right_to, ..
} = &self.action {
// Connect to JACK
let name = name.as_ref().map_or("tek", |x|x.as_str());
let jack = Jack::new(&name)?;
// TODO: Collect audio IO:
let empty = &[] as &[&str];
let left_froms = Connect::collect(&left_from, empty, empty);
let left_tos = Connect::collect(&left_to, empty, empty);
let right_froms = Connect::collect(&right_from, empty, empty);
let right_tos = Connect::collect(&right_to, empty, empty);
let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()];
// Create initial project:
let clock = Clock::new(&jack, *bpm)?;
let mut project = Arrangement::new(
&jack,
None,
clock,
vec![],
vec![],
Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate()
.map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()]))
.collect::<Result<Vec<_>, _>>()?,
Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate()
.map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()]))
.collect::<Result<Vec<_>, _>>()?
);
project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?;
project.scenes_add(scenes.unwrap_or(0))?;
if matches!(self.action, Action::Status) {
// Show status and exit
tek_print_status(&project);
return Ok(())
}
// Initialize the app state
let app = tek(&jack, project, config, ":menu");
if matches!(self.action, Action::Headless) {
// TODO: Headless mode (daemon + client over IPC, then over network...)
println!("todo headless");
return Ok(())
}
// Run the [Tui] and [Jack] threads with the [App] state.
Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{
// Between jack init and app's first cycle:
jack.sync_lead(*sync_lead, |mut state|{
let clock = app.clock();
clock.playhead.update_from_sample(state.position.frame() as f64);
state.position.bbt = Some(clock.bbt());
state.position
})?;
jack.sync_follow(*sync_follow)?;
// FIXME: They don't work properly.
Ok(app)
})?)?;
}
Ok(())
}
}
pub fn tek_show_version () {
println!("todo version");
}
pub fn tek_print_config (config: &Config) {
use ::ansi_term::Color::*;
println!("{:?}", config.dirs);
for (k, v) in config.views.read().unwrap().iter() {
println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")));
}
for (k, v) in config.binds.read().unwrap().iter() {
println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}")));
for (k, v) in v.0.iter() {
print!("{} ", &Yellow.paint(match &k.0 {
Event::Key(KeyEvent { modifiers, .. }) =>
format!("{:>16}", format!("{modifiers}")),
_ => unimplemented!()
}));
print!("{}", &Yellow.bold().paint(match &k.0 {
Event::Key(KeyEvent { code, .. }) =>
format!("{:<10}", format!("{code}")),
_ => unimplemented!()
}));
for v in v.iter() {
print!(" => {:?}", v.commands);
print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default());
println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default());
//println!(" {:?}", v.source);
}
}
}
for (k, v) in config.modes.read().unwrap().iter() {
println!();
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }
for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); }
print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}")));
print!("\n{}", Blue.paint("KEYS"));
for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); }
println!();
for (k, v) in v.modes.read().unwrap().iter() {
print!("{} {} {:?}",
Blue.paint("MODE"),
Green.bold().paint(format!("{k:<16}")),
v.name);
print!(" INFO={:?}",
v.info);
print!(" VIEW={:?}",
v.view);
println!(" KEYS={:?}",
v.keys);
}
print!("{}", Blue.paint("VIEW"));
for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); }
println!();
}
}
pub fn tek_print_status (project: &Arrangement) {
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
}

View file

@ -1,707 +0,0 @@
use crate::*;
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
use ::atomic_float::AtomicF64;
use ::tengri::{draw::*, term::*};
impl <T: AsRef<Clock>+AsMut<Clock>> HasClock for T {}
pub trait HasClock: AsRef<Clock> + AsMut<Clock> {
fn clock (&self) -> &Clock { self.as_ref() }
fn clock_mut (&mut self) -> &mut Clock { self.as_mut() }
}
/// 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>>>,
}
/// 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);
/// 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>,
}
/// FIXME: remove this and use PPQ from timebase everywhere:
pub const PPQ: usize = 96;
/// (pulses, name), assuming 96 PPQ
pub const NOTE_DURATIONS: [(usize, &str);26] = [
(1, "1/384"), (2, "1/192"),
(3, "1/128"), (4, "1/96"),
(6, "1/64"), (8, "1/48"),
(12, "1/32"), (16, "1/24"),
(24, "1/16"), (32, "1/12"),
(48, "1/8"), (64, "1/6"),
(96, "1/4"), (128, "1/3"),
(192, "1/2"), (256, "2/3"),
(384, "1/1"), (512, "4/3"),
(576, "3/2"), (768, "2/1"),
(1152, "3/1"), (1536, "4/1"),
(2304, "6/1"), (3072, "8/1"),
(3456, "9/1"), (6144, "16/1"),
];
pub const NOTE_NAMES: [&str; 128] = [
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9",
"C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10",
];
pub const DEFAULT_PPQ: f64 = 96.0;
def_command!(ClockCommand: |clock: Clock| {
SeekUsec { usec: f64 } => {
clock.playhead.update_from_usec(*usec); Ok(None) },
SeekSample { sample: f64 } => {
clock.playhead.update_from_sample(*sample); Ok(None) },
SeekPulse { pulse: f64 } => {
clock.playhead.update_from_pulse(*pulse); Ok(None) },
SetBpm { bpm: f64 } => Ok(Some(
Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })),
SetQuant { quant: f64 } => Ok(Some(
Self::SetQuant { quant: clock.quant.set(*quant) })),
SetSync { sync: f64 } => Ok(Some(
Self::SetSync { sync: clock.sync.set(*sync) })),
Play { position: Option<u32> } => {
clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ },
Pause { position: Option<u32> } => {
clock.pause_at(*position)?; Ok(None) },
TogglePlayback { position: u32 } => Ok(if clock.is_rolling() {
clock.pause_at(Some(*position))?; None
} else {
clock.play_from(Some(*position))?; None
}),
});
impl Moment {
pub fn zero (timebase: &Arc<Timebase>) -> Self {
Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() }
}
pub fn from_usec (timebase: &Arc<Timebase>, usec: f64) -> Self {
Self {
usec: usec.into(),
sample: timebase.sr.usecs_to_sample(usec).into(),
pulse: timebase.usecs_to_pulse(usec).into(),
timebase: timebase.clone(),
}
}
pub fn from_sample (timebase: &Arc<Timebase>, sample: f64) -> Self {
Self {
sample: sample.into(),
usec: timebase.sr.samples_to_usec(sample).into(),
pulse: timebase.samples_to_pulse(sample).into(),
timebase: timebase.clone(),
}
}
pub fn from_pulse (timebase: &Arc<Timebase>, pulse: f64) -> Self {
Self {
pulse: pulse.into(),
sample: timebase.pulses_to_sample(pulse).into(),
usec: timebase.pulses_to_usec(pulse).into(),
timebase: timebase.clone(),
}
}
#[inline] pub fn update_from_usec (&self, usec: f64) {
self.usec.set(usec);
self.pulse.set(self.timebase.usecs_to_pulse(usec));
self.sample.set(self.timebase.sr.usecs_to_sample(usec));
}
#[inline] pub fn update_from_sample (&self, sample: f64) {
self.usec.set(self.timebase.sr.samples_to_usec(sample));
self.pulse.set(self.timebase.samples_to_pulse(sample));
self.sample.set(sample);
}
#[inline] pub fn update_from_pulse (&self, pulse: f64) {
self.usec.set(self.timebase.pulses_to_usec(pulse));
self.pulse.set(pulse);
self.sample.set(self.timebase.pulses_to_sample(pulse));
}
#[inline] pub fn format_beat (&self) -> Arc<str> {
self.timebase.format_beats_1(self.pulse.get()).into()
}
}
impl LaunchSync {
pub fn next (&self) -> f64 {
note_duration_next(self.get() as usize) as f64
}
pub fn prev (&self) -> f64 {
note_duration_prev(self.get() as usize) as f64
}
}
impl Quantize {
pub fn next (&self) -> f64 {
note_duration_next(self.get() as usize) as f64
}
pub fn prev (&self) -> f64 {
note_duration_prev(self.get() as usize) as f64
}
}
impl Timebase {
/// Specify sample rate, BPM and PPQ
pub fn new (
s: impl Into<SampleRate>,
b: impl Into<Bpm>,
p: impl Into<Ppq>
) -> Self {
Self { sr: s.into(), bpm: b.into(), ppq: p.into() }
}
/// Iterate over ticks between start and end.
#[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> Ticker {
Ticker { spp: self.samples_per_pulse(), sample: start, start, end }
}
/// Return the duration fo a beat in microseconds
#[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() }
/// Return the number of beats in a second
#[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 }
/// Return the number of microseconds corresponding to a note of the given duration
#[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 {
4.0 * self.usec_per_beat() * num / den
}
/// Return duration of a pulse in microseconds (BPM-dependent)
#[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() }
/// Return duration of a pulse in microseconds (BPM-dependent)
#[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() }
/// Return number of pulses to which a number of microseconds corresponds (BPM-dependent)
#[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() }
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
#[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() }
/// Return number of pulses in a second (BPM-dependent)
#[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() }
/// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent)
#[inline] pub fn pulses_per_sample (&self) -> f64 {
self.usec_per_pulse() / self.sr.usec_per_sample()
}
/// Return number of samples in a pulse (SR- and BPM-dependent)
#[inline] pub fn samples_per_pulse (&self) -> f64 {
self.sr.get() / self.pulses_per_second()
}
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
#[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 {
self.pulses_per_sample() * p
}
/// Convert a number of samples to a pulse number (SR- and BPM-dependent)
#[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 {
s / self.pulses_per_sample()
}
/// Return the number of samples corresponding to a note of the given duration
#[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 {
self.usec_to_sample(self.note_to_usec(note))
}
/// Return the number of samples corresponding to the given number of microseconds
#[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 {
usec * self.sr.get() / 1000f64
}
/// Return the quantized position of a moment in time given a step
#[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) {
let step = self.note_to_usec(step);
(time / step, time % step)
}
/// Quantize a collection of events
#[inline] pub fn quantize_into <E: Iterator<Item=(f64, f64)> + Sized, T> (
&self, step: (f64, f64), events: E
) -> Vec<(f64, f64)> {
events.map(|(time, event)|(self.quantize(step, time).0, event)).collect()
}
/// Format a number of pulses into Beat.Bar.Pulse starting from 0
#[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc<str> {
let pulse = pulse as usize;
let ppq = self.ppq.get() as usize;
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into()
}
/// Format a number of pulses into Beat.Bar starting from 0
#[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc<str> {
let pulse = pulse as usize;
let ppq = self.ppq.get() as usize;
let beats = if ppq > 0 { pulse / ppq } else { 0 };
format!("{}.{}", beats / 4, beats % 4).into()
}
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
#[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc<str> {
let mut string = String::with_capacity(16);
self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat");
string.into()
}
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
#[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> {
let pulse = pulse as usize;
let ppq = self.ppq.get() as usize;
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1)
}
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
#[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc<str> {
let pulse = pulse as usize;
let ppq = self.ppq.get() as usize;
let beats = if ppq > 0 { pulse / ppq } else { 0 };
format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into()
}
}
impl SampleRate {
/// Return the duration of a sample in microseconds (floating)
#[inline] pub fn usec_per_sample (&self) -> f64 {
1_000_000f64 / self.get()
}
/// Return the duration of a sample in microseconds (floating)
#[inline] pub fn sample_per_usec (&self) -> f64 {
self.get() / 1_000_000f64
}
/// Convert a number of samples to microseconds (floating)
#[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 {
self.usec_per_sample() * samples
}
/// Convert a number of microseconds to samples (floating)
#[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 {
self.sample_per_usec() * usecs
}
}
impl Microsecond {
#[inline] pub fn format_msu (&self) -> Arc<str> {
let usecs = self.get() as usize;
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
let (minutes, seconds) = (seconds / 60, seconds % 60);
format!("{minutes}:{seconds:02}:{msecs:03}").into()
}
}
/// Define and implement a unit of time
#[macro_export] macro_rules! impl_time_unit {
($T:ident) => {
impl Gettable<f64> for $T {
fn get (&self) -> f64 { self.0.load(Relaxed) }
}
impl InteriorMutable<f64> for $T {
fn set (&self, value: f64) -> f64 {
let old = self.get();
self.0.store(value, Relaxed);
old
}
}
impl TimeUnit for $T {}
impl_op!($T, Add, add, |a, b|{a + b});
impl_op!($T, Sub, sub, |a, b|{a - b});
impl_op!($T, Mul, mul, |a, b|{a * b});
impl_op!($T, Div, div, |a, b|{a / b});
impl_op!($T, Rem, rem, |a, b|{a % b});
impl From<f64> for $T { fn from (value: f64) -> Self { Self(value.into()) } }
impl From<usize> for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } }
impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } }
impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } }
impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } }
impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } }
impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } }
}
}
impl std::fmt::Debug for Clock {
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("Clock")
.field("timebase", &self.timebase)
.field("chunk", &self.chunk)
.field("quant", &self.quant)
.field("sync", &self.sync)
.field("global", &self.global)
.field("playhead", &self.playhead)
.field("started", &self.started)
.finish()
}
}
impl Clock {
pub fn new (jack: &Jack<'static>, bpm: Option<f64>) -> Usually<Self> {
let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport()));
let timebase = Arc::new(Timebase::default());
let clock = Self {
quant: Arc::new(24.into()),
sync: Arc::new(384.into()),
transport: Arc::new(Some(transport)),
chunk: Arc::new((chunk as usize).into()),
global: Arc::new(Moment::zero(&timebase)),
playhead: Arc::new(Moment::zero(&timebase)),
offset: Arc::new(Moment::zero(&timebase)),
started: RwLock::new(None).into(),
timebase,
midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))),
midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))),
click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))),
..Default::default()
};
if let Some(bpm) = bpm {
clock.timebase.bpm.set(bpm);
}
Ok(clock)
}
pub fn timebase (&self) -> &Arc<Timebase> {
&self.timebase
}
/// Current sample rate
pub fn sr (&self) -> &SampleRate {
&self.timebase.sr
}
/// Current tempo
pub fn bpm (&self) -> &Bpm {
&self.timebase.bpm
}
/// Current MIDI resolution
pub fn ppq (&self) -> &Ppq {
&self.timebase.ppq
}
/// Next pulse that matches launch sync (for phrase switchover)
pub fn next_launch_pulse (&self) -> usize {
let sync = self.sync.get() as usize;
let pulse = self.playhead.pulse.get() as usize;
if pulse % sync == 0 {
pulse
} else {
(pulse / sync + 1) * sync
}
}
/// Start playing, optionally seeking to a given location beforehand
pub fn play_from (&self, start: Option<u32>) -> Usually<()> {
if let Some(transport) = self.transport.as_ref() {
if let Some(start) = start {
transport.locate(start)?;
}
transport.start()?;
}
Ok(())
}
/// Pause, optionally seeking to a given location afterwards
pub fn pause_at (&self, pause: Option<u32>) -> Usually<()> {
if let Some(transport) = self.transport.as_ref() {
transport.stop()?;
if let Some(pause) = pause {
transport.locate(pause)?;
}
}
Ok(())
}
/// Is currently paused?
pub fn is_stopped (&self) -> bool {
self.started.read().unwrap().is_none()
}
/// Is currently playing?
pub fn is_rolling (&self) -> bool {
self.started.read().unwrap().is_some()
}
/// Update chunk size
pub fn set_chunk (&self, n_frames: usize) {
self.chunk.store(n_frames, Relaxed);
}
pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> {
// Store buffer length
self.set_chunk(scope.n_frames() as usize);
// Store reported global frame and usec
let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?;
self.global.sample.set(current_frames as f64);
self.global.usec.set(current_usecs as f64);
let mut started = self.started.write().unwrap();
// If transport has just started or just stopped,
// update starting point:
if let Some(transport) = self.transport.as_ref() {
match (transport.query_state()?, started.as_ref()) {
(TransportState::Rolling, None) => {
let moment = Moment::zero(&self.timebase);
moment.sample.set(current_frames as f64);
moment.usec.set(current_usecs as f64);
*started = Some(moment);
},
(TransportState::Stopped, Some(_)) => {
*started = None;
},
_ => {}
};
}
self.playhead.update_from_sample(started.as_ref()
.map(|started|current_frames as f64 - started.sample.get())
.unwrap_or(0.));
Ok(())
}
pub fn bbt (&self) -> PositionBBT {
let pulse = self.playhead.pulse.get() as i32;
let ppq = self.timebase.ppq.get() as i32;
let bpm = self.timebase.bpm.get();
let bar = (pulse / ppq) / 4;
PositionBBT {
bar: 1 + bar,
beat: 1 + (pulse / ppq) % 4,
tick: (pulse % ppq),
bar_start_tick: (bar * 4 * ppq) as f64,
beat_type: 4.,
beats_per_bar: 4.,
beats_per_minute: bpm,
ticks_per_beat: ppq as f64
}
}
pub fn next_launch_instant (&self) -> Moment {
Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64)
}
/// Get index of first sample to populate.
///
/// Greater than 0 means that the first pulse of the clip
/// falls somewhere in the middle of the chunk.
pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{
(scope.last_frame_time() as usize).saturating_sub(
started.sample.get() as usize +
self.started.read().unwrap().as_ref().unwrap().sample.get() as usize
)
}
// Get iterator that emits sample paired with pulse.
//
// * Sample: index into output buffer at which to write MIDI event
// * Pulse: index into clip from which to take the MIDI event
//
// Emitted for each sample of the output buffer that corresponds to a MIDI pulse.
pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> Ticker {
self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize)
}
}
impl Clock {
fn _todo_provide_u32 (&self) -> u32 {
todo!()
}
fn _todo_provide_opt_u32 (&self) -> Option<u32> {
todo!()
}
fn _todo_provide_f64 (&self) -> f64 {
todo!()
}
}
impl<T: HasClock> Command<T> for ClockCommand {
fn execute (&self, state: &mut T) -> Perhaps<Self> {
self.execute(state.clock_mut()) // awesome
}
}
impl ClockView {
pub const BEAT_EMPTY: &'static str = "-.-.--";
pub const TIME_EMPTY: &'static str = "-.---s";
pub const BPM_EMPTY: &'static str = "---.---";
pub fn update_clock (cache: &Arc<RwLock<Self>>, clock: &Clock, compact: bool) {
let rate = clock.timebase.sr.get();
let chunk = clock.chunk.load(Relaxed) as f64;
let lat = chunk / rate * 1000.;
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
let mut cache = cache.write().unwrap();
cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}"));
cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms"));
cache.sr.update(Some((compact, rate)), |buf,_,_|{
buf.clear();
if compact {
write!(buf, "{:.1}kHz", rate / 1000.)
} else {
write!(buf, "{:.0}Hz", rate)
}
});
if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) {
let pulse = clock.timebase.usecs_to_pulse(now);
let time = now/1000000.;
let bpm = clock.timebase.bpm.get();
cache.beat.update(Some(pulse), |buf, _, _|{
buf.clear();
clock.timebase.format_beats_1_to(buf, pulse)
});
cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time));
cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm));
} else {
cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY));
cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY));
cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY));
}
}
}
impl_default!(ClockView: {
let mut beat = String::with_capacity(16);
let _ = write!(beat, "{}", Self::BEAT_EMPTY);
let mut time = String::with_capacity(16);
let _ = write!(time, "{}", Self::TIME_EMPTY);
let mut bpm = String::with_capacity(16);
let _ = write!(bpm, "{}", Self::BPM_EMPTY);
Self {
beat: Memo::new(None, beat),
time: Memo::new(None, time),
bpm: Memo::new(None, bpm),
sr: Memo::new(None, String::with_capacity(16)),
buf: Memo::new(None, String::with_capacity(16)),
lat: Memo::new(None, String::with_capacity(16)),
}
});
#[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock);
impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ));
impl_time_unit!(SampleCount);
impl_time_unit!(SampleRate);
impl_time_unit!(Microsecond);
impl_time_unit!(Quantize);
impl_time_unit!(Ppq);
impl_time_unit!(Pulse);
impl_time_unit!(Bpm);
impl_time_unit!(LaunchSync);

View file

@ -1,92 +0,0 @@
use crate::{*, bind::*, mode::*, view::*};
/// Configuration: mode, view, and bind definitions.
///
/// ```
/// let config = tek::Config::default();
/// ```
///
/// ```
/// // Some dizzle.
/// // What indentation to use here lol?
/// let source = stringify!((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)))))))));
/// // Add this definition to the config and try to load it.
/// // A "mode" is basically a state machine
/// // with associated input and output definitions.
/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap();
/// ```
#[derive(Default, Debug)] pub struct Config {
/// XDG base directories of running user.
pub dirs: BaseDirectories,
/// Active collection of interaction modes.
pub modes: Modes,
/// Active collection of event bindings.
pub binds: Binds,
/// Active collection of view definitions.
pub views: Views,
}
impl Config {
const CONFIG_DIR: &'static str = "tek";
const CONFIG_SUB: &'static str = "v0";
const CONFIG: &'static str = "tek.edn";
const DEFAULTS: &'static str = include_str!("./tek.edn");
/// Create a new app configuration from a set of XDG base directories,
pub fn new (dirs: Option<BaseDirectories>) -> Self {
let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB);
let dirs = dirs.unwrap_or_else(default);
Self { dirs, ..Default::default() }
}
/// Write initial contents of configuration.
pub fn init (&mut self) -> Usually<()> {
self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{
cfgs.add(&dsl)?;
Ok(())
})?;
Ok(())
}
/// Write initial contents of a configuration file.
pub fn init_one (
&mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()>
) -> Usually<()> {
if self.dirs.find_config_file(path).is_none() {
//println!("Creating {path:?}");
std::fs::write(self.dirs.place_config_file(path)?, defaults)?;
}
Ok(if let Some(path) = self.dirs.find_config_file(path) {
//println!("Loading {path:?}");
let src = std::fs::read_to_string(&path)?;
src.as_str().each(move|item|each(self, item))?;
} else {
return Err(format!("{path}: not found").into())
})
}
/// Add statements to configuration from [Dsl] source.
pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> {
dsl.each(|item|self.add_one(item))?;
Ok(self)
}
fn add_one (&self, item: impl Language) -> Usually<()> {
if let Some(expr) = item.expr()? {
let head = expr.head()?;
let tail = expr.tail()?;
let name = tail.head()?;
let body = tail.tail()?;
//println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default());
match head {
Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?,
Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?,
Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?,
_ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into())
}
Ok(())
} else {
return Err(format!("Config::load: expected expr, got: {item:?}").into())
}
}
}

View file

@ -1,670 +0,0 @@
use crate::*;
use ConnectName::*;
use ConnectScope::*;
use ConnectStatus::*;
def_command!(DeviceCommand: |device: Device| {});
def_command!(MidiInputCommand: |port: MidiInput| {
Close => todo!(),
Connect { midi_out: Arc<str> } => todo!(),
});
def_command!(MidiOutputCommand: |port: MidiOutput| {
Close => todo!(),
Connect { midi_in: Arc<str> } => todo!(),
});
def_command!(AudioInputCommand: |port: AudioInput| {
Close => todo!(),
Connect { audio_out: Arc<str> } => todo!(),
});
def_command!(AudioOutputCommand: |port: AudioOutput| {
Close => todo!(),
Connect { audio_in: Arc<str> } => todo!(),
});
impl Device {
pub fn name (&self) -> &str {
match self {
Self::Sampler(sampler) => sampler.name.as_ref(),
_ => todo!(),
}
}
pub fn midi_ins (&self) -> &[MidiInput] {
match self {
//Self::Sampler(Sampler { midi_in, .. }) => &[midi_in],
_ => todo!()
}
}
pub fn midi_outs (&self) -> &[MidiOutput] {
match self {
Self::Sampler(_) => &[],
_ => todo!()
}
}
pub fn audio_ins (&self) -> &[AudioInput] {
match self {
Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(),
_ => todo!()
}
}
pub fn audio_outs (&self) -> &[AudioOutput] {
match self {
Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(),
_ => todo!()
}
}
}
/// A device that can be plugged into the chain.
///
/// ```
/// let device = tek::Device::default();
/// ```
#[derive(Debug, Default)] pub enum Device {
#[default]
Bypass,
Mute,
#[cfg(feature = "sampler")]
Sampler(Sampler),
#[cfg(feature = "lv2")] // TODO
Lv2(Lv2),
#[cfg(feature = "vst2")] // TODO
Vst2,
#[cfg(feature = "vst3")] // TODO
Vst3,
#[cfg(feature = "clap")] // TODO
Clap,
#[cfg(feature = "sf2")] // TODO
Sf2,
}
/// Some sort of wrapper?
pub struct DeviceAudio<'a>(pub &'a mut Device);
/// 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
}))
}
}
impl JackPort for MidiInput {
type Port = MidiIn;
type Pair = MidiOut;
fn port_name (&self) -> &Arc<str> {
&self.name
}
fn port (&self) -> &Port<Self::Port> {
&self.port
}
fn port_mut (&mut self) -> &mut Port<Self::Port> {
&mut self.port
}
fn into_port (self) -> Port<Self::Port> {
self.port
}
fn connections (&self) -> &[Connect] {
self.connections.as_slice()
}
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
-> Usually<Self> where Self: Sized
{
let port = Self {
port: Self::register(jack, name)?,
jack: jack.clone(),
name: name.as_ref().into(),
connections: connect.to_vec(),
held: Arc::new(RwLock::new([false;128]))
};
port.connect_to_matching()?;
Ok(port)
}
}
impl JackPort for MidiOutput {
type Port = MidiOut;
type Pair = MidiIn;
fn port_name (&self) -> &Arc<str> {
&self.name
}
fn port (&self) -> &Port<Self::Port> {
&self.port
}
fn port_mut (&mut self) -> &mut Port<Self::Port> {
&mut self.port
}
fn into_port (self) -> Port<Self::Port> {
self.port
}
fn connections (&self) -> &[Connect] {
self.connections.as_slice()
}
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
-> Usually<Self> where Self: Sized
{
let port = Self::register(jack, name)?;
let jack = jack.clone();
let name = name.as_ref().into();
let connections = connect.to_vec();
let port = Self {
jack,
port,
name,
connections,
held: Arc::new([false;128].into()),
note_buffer: vec![0;8],
output_buffer: vec![vec![];65536],
};
port.connect_to_matching()?;
Ok(port)
}
}
impl MidiOutput {
/// Clear the section of the output buffer that we will be using,
/// emitting "all notes off" at start of buffer if requested.
pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) {
let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len());
for frame in &mut self.output_buffer[0..n_frames] {
frame.clear();
}
if reset {
all_notes_off(&mut self.output_buffer);
}
}
/// Write a note to the output buffer
pub fn buffer_write <'a> (
&'a mut self,
sample: usize,
event: LiveEvent,
) {
self.note_buffer.fill(0);
event.write(&mut self.note_buffer).expect("failed to serialize MIDI event");
self.output_buffer[sample].push(self.note_buffer.clone());
// Update the list of currently held notes.
if let LiveEvent::Midi { ref message, .. } = event {
update_keys(&mut*self.held.write().unwrap(), message);
}
}
/// Write a chunk of MIDI data from the output buffer to the output port.
pub fn buffer_emit (&mut self, scope: &ProcessScope) {
let samples = scope.n_frames() as usize;
let mut writer = self.port.writer(scope);
for (time, events) in self.output_buffer.iter().enumerate().take(samples) {
for bytes in events.iter() {
writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{
panic!("Failed to write MIDI data: {bytes:?}");
});
}
}
}
}
impl MidiInput {
pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> {
parse_midi_input(self.port().iter(scope))
}
}
impl<T: AsRef<Vec<MidiInput>> + AsMut<Vec<MidiInput>>> HasMidiIns for T {
fn midi_ins (&self) -> &Vec<MidiInput> { self.as_ref() }
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput> { self.as_mut() }
}
impl<T: AsRef<Vec<MidiOutput>> + AsMut<Vec<MidiOutput>>> HasMidiOuts for T {
fn midi_outs (&self) -> &Vec<MidiOutput> { self.as_ref() }
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput> { self.as_mut() }
}
impl<T: HasMidiIns + HasJack<'static>> AddMidiIn for T {
fn midi_in_add (&mut self) -> Usually<()> {
let index = self.midi_ins().len();
let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?;
self.midi_ins_mut().push(port);
Ok(())
}
}
/// Trail for thing that may gain new MIDI ports.
impl<T: HasMidiOuts + HasJack<'static>> AddMidiOut for T {
fn midi_out_add (&mut self) -> Usually<()> {
let index = self.midi_outs().len();
let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?;
self.midi_outs_mut().push(port);
Ok(())
}
}
impl JackPort for AudioInput {
type Port = AudioIn;
type Pair = AudioOut;
fn port_name (&self) -> &Arc<str> {
&self.name
}
fn port (&self) -> &Port<Self::Port> {
&self.port
}
fn port_mut (&mut self) -> &mut Port<Self::Port> {
&mut self.port
}
fn into_port (self) -> Port<Self::Port> {
self.port
}
fn connections (&self) -> &[Connect] {
self.connections.as_slice()
}
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
-> Usually<Self> where Self: Sized
{
let port = Self {
port: Self::register(jack, name)?,
jack: jack.clone(),
name: name.as_ref().into(),
connections: connect.to_vec()
};
port.connect_to_matching()?;
Ok(port)
}
}
impl JackPort for AudioOutput {
type Port = AudioOut;
type Pair = AudioIn;
fn port_name (&self) -> &Arc<str> {
&self.name
}
fn port (&self) -> &Port<Self::Port> {
&self.port
}
fn port_mut (&mut self) -> &mut Port<Self::Port> {
&mut self.port
}
fn into_port (self) -> Port<Self::Port> {
self.port
}
fn connections (&self) -> &[Connect] {
self.connections.as_slice()
}
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
-> Usually<Self> where Self: Sized
{
let port = Self {
port: Self::register(jack, name)?,
jack: jack.clone(),
name: name.as_ref().into(),
connections: connect.to_vec()
};
port.connect_to_matching()?;
Ok(port)
}
}
impl_audio!(|self: DeviceAudio<'a>, client, scope|{
use Device::*;
match self.0 {
Mute => { Control::Continue },
Bypass => { /*TODO*/ Control::Continue },
#[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope),
#[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope),
#[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO
#[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO
#[cfg(feature = "clap")] Clap => { todo!() }, // TODO
#[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO
}
});
pub fn device_kinds () -> &'static [&'static str] {
&[
#[cfg(feature = "sampler")] "Sampler",
#[cfg(feature = "lv2")] "Plugin (LV2)",
]
}
impl<T: AsRef<Vec<Device>> + AsMut<Vec<Device>>> HasDevices for T {
fn devices (&self) -> &Vec<Device> {
self.as_ref()
}
fn devices_mut (&mut self) -> &mut Vec<Device> {
self.as_mut()
}
}
/// 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)
}
}
}

View file

@ -1,106 +0,0 @@
use crate::{*, browse::*, device::*, menu::*};
/// Various possible dialog modes.
///
/// ```
/// let dialog: tek::Dialog = Default::default();
/// ```
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
#[default] None,
Help(usize),
Menu(usize, MenuItems),
Device(usize),
Message(Arc<str>),
Browse(BrowseTarget, Arc<Browse>),
Options,
}
namespace!(App: Dialog { symbol = |app| {
":dialog/none" => Dialog::None,
":dialog/options" => Dialog::Options,
":dialog/device" => Dialog::Device(0),
":dialog/device/prev" => Dialog::Device(0),
":dialog/device/next" => Dialog::Device(0),
":dialog/help" => Dialog::Help(0),
":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject,
Browse::new(None).unwrap().into()),
":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject,
Browse::new(None).unwrap().into()),
":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()),
Browse::new(None).unwrap().into()),
":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()),
Browse::new(None).unwrap().into()),
":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()),
Browse::new(None).unwrap().into()),
":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()),
Browse::new(None).unwrap().into()),
}; });
impl Dialog {
/// ```
/// let _ = tek::Dialog::welcome();
/// ```
pub fn welcome () -> Self {
Self::Menu(1, MenuItems([
MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))),
MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({
app.dialog = Dialog::None;
app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap();
})))),
MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))),
].into()))
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_selected();
/// ```
pub fn menu_selected (&self) -> Option<usize> {
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_next();
/// ```
pub fn menu_next (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()),
_ => Self::None
}
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_prev();
/// ```
pub fn menu_prev (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()),
_ => Self::None
}
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind();
/// ```
pub fn device_kind (&self) -> Option<usize> {
if let Self::Device(index) = self { Some(*index) } else { None }
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind_next();
/// ```
pub fn device_kind_next (&self) -> Option<usize> {
self.device_kind().map(|index|(index + 1) % device_kinds().len())
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind_prev();
/// ```
pub fn device_kind_prev (&self) -> Option<usize> {
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
}
/// FIXME: implement
pub fn message (&self) -> Option<&str> { todo!() }
/// FIXME: implement
pub fn browser (&self) -> Option<&Arc<Browse>> { todo!() }
/// FIXME: implement
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
}

View file

@ -1,27 +0,0 @@
use crate::*;
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
impl_default!(MenuItem: Self("".into(), Arc::new(Box::new(|_|Ok(())))));
impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } }
impl AsRef<Arc<[MenuItem]>> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } }
/// List of menu items.
///
/// ```
/// let items: tek::MenuItems = Default::default();
/// ```
#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems(
pub Arc<[MenuItem]>
);
/// An item of a menu.
///
/// ```
/// let item: tek::MenuItem = Default::default();
/// ```
#[derive(Clone)] pub struct MenuItem(
/// Label
pub Arc<str>,
/// Callback
pub Arc<Box<dyn Fn(&mut App)->Usually<()> + Send + Sync>>
);

View file

@ -1,121 +0,0 @@
use crate::*;
#[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,
}
#[cfg(test)] mod test_view_meter {
use super::*;
use proptest::prelude::*;
proptest! {
#[test] fn proptest_view_meter (
label in "\\PC*", value in f32::MIN..f32::MAX
) {
let _ = view_meter(&label, value);
}
#[test] fn proptest_view_meters (
value1 in f32::MIN..f32::MAX,
value2 in f32::MIN..f32::MAX
) {
let _ = view_meters(&[value1, value2]);
}
}
}
impl Draw<Tui> for RmsMeter {
fn draw(self, to: &mut Tui) -> Usually<XYWH<u16>> {
let XYWH(x, y, w, h) = to.area();
let signal = f32::max(0.0, f32::min(100.0, self.0.abs()));
let v = (signal * h as f32).ceil() as u16;
let y2 = y + h;
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default()));
for y in y..(y + v) {
for x in x..(x + w) {
to.blit(&"", x, y2.saturating_sub(y), Some(Style::default().green()));
}
}
}
}
impl Draw<Tui> for Log10Meter {
fn draw(self, to: &mut Tui) -> Usually<XYWH<u16>> {
let XYWH(x, y, w, h) = to.area();
let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs()));
let v = (signal * h as f32 / 100.0).ceil() as u16;
let y2 = y + h;
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None);
for y in y..(y + v) {
for x in x..(x + w) {
to.blit(&"", x, y2 - y, Some(Style::default().green()));
}
}
}
}
fn draw_meters (meters: &[f32]) -> impl Draw<Tui> + use<'_> {
Tui::bg(Black, w_exact(2, iter_east(1, ||meters.iter(), |value, _index|{
h_full(RmsMeter(*value))
})))
}
pub fn mix_summing <const N: usize> (
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
) -> bool {
let channels = buffer.len();
for index in 0..frames {
if let Some(frame) = next() {
for (channel, sample) in frame.iter().enumerate() {
let channel = channel % channels;
buffer[channel][index] += sample * gain;
}
} else {
return false
}
}
true
}
pub fn mix_average <const N: usize> (
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
) -> bool {
let channels = buffer.len();
for index in 0..frames {
if let Some(frame) = next() {
for (channel, sample) in frame.iter().enumerate() {
let channel = channel % channels;
let value = buffer[channel][index];
buffer[channel][index] = (value + sample * gain) / 2.0;
}
} else {
return false
}
}
true
}
pub fn to_log10 (samples: &[f32]) -> f32 {
let total: f32 = samples.iter().map(|x|x.abs()).sum();
let count = samples.len() as f32;
10. * (total / count).log10()
}
pub fn to_rms (samples: &[f32]) -> f32 {
let sum = samples.iter()
.map(|s|*s)
.reduce(|sum, sample|sum + sample.abs())
.unwrap_or(0.0);
(sum / samples.len() as f32).sqrt()
}

View file

@ -1,99 +0,0 @@
use crate::{*, config::*};
impl Config {
pub fn get_mode (&self, mode: impl AsRef<str>) -> Option<Arc<Mode<Arc<str>>>> {
self.modes.clone().read().unwrap().get(mode.as_ref()).cloned()
}
}
pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
let mut mode = Mode::default();
body.each(|item|mode.add(item))?;
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
Ok(())
}
/// Collection of interaction modes.
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
impl Mode<Arc<str>> {
/// Add a definition to the mode.
///
/// Supported definitions:
///
/// - (name ...) -> name
/// - (info ...) -> description
/// - (keys ...) -> key bindings
/// - (mode ...) -> submode
/// - ... -> view
///
/// ```
/// let mut mode: tek::Mode<std::sync::Arc<str>> = Default::default();
/// mode.add("(name hello)").unwrap();
/// ```
pub fn add (&mut self, dsl: impl Language) -> Usually<()> {
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
//println!("Mode::add: {head} {:?}", expr.tail());
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
match head {
"name" => self.add_name(tail)?,
"info" => self.add_info(tail)?,
"keys" => self.add_keys(tail)?,
"mode" => self.add_mode(tail)?,
_ => self.add_view(tail)?,
};
} else if let Ok(Some(word)) = dsl.word() {
self.add_view(word);
} else {
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
})
//DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into()))
//.word(|word|self.add_view(word))
//.expr(|expr|expr.head(|head|{
////println!("Mode::add: {head} {:?}", expr.tail());
//let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
//match head {
//"name" => self.add_name(tail),
//"info" => self.add_info(tail),
//"keys" => self.add_keys(tail)?,
//"mode" => self.add_mode(tail)?,
//_ => self.add_view(tail),
//};
//}))
}
fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> {
Ok(dsl.src()?.map(|src|self.name.push(src.into())))
}
fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> {
Ok(dsl.src()?.map(|src|self.info.push(src.into())))
}
fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> {
Ok(dsl.src()?.map(|src|self.view.push(src.into())))
}
fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> {
Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?))
}
fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> {
Ok(Some(if let Some(id) = dsl.head()? {
load_mode(&self.modes, &id, &dsl.tail())?;
} else {
return Err(format!("Mode::add: self: incomplete: {dsl:?}").into());
}))
}
}
/// Group of view and keys definitions.
///
/// ```
/// let mode = tek::Mode::<std::sync::Arc<str>>::default();
/// ```
#[derive(Default, Debug)] pub struct Mode<D: Language + Ord> {
pub path: PathBuf,
pub name: Vec<D>,
pub info: Vec<D>,
pub view: Vec<D>,
pub keys: Vec<D>,
pub modes: Modes,
}

View file

@ -1,206 +0,0 @@
use crate::*;
/// 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>
}
impl_audio!(Lv2: lv2_jack_process);
impl Lv2 {
const INPUT_BUFFER: usize = 1024;
pub fn new (
jack: &Jack<'static>,
name: &str,
uri: &str,
) -> Usually<Self> {
let lv2_world = livi::World::with_load_bundle(&uri);
let lv2_features = lv2_world.build_features(livi::FeaturesBuilder {
min_block_length: 1,
max_block_length: 65536,
});
let lv2_plugin = lv2_world.iter_plugins().nth(0)
.unwrap_or_else(||panic!("plugin not found: {uri}"));
Ok(Self {
jack: jack.clone(),
name: name.into(),
path: Some(String::from(uri).into()),
selected: 0,
mapping: false,
midi_ins: vec![],
midi_outs: vec![],
audio_ins: vec![],
audio_outs: vec![],
lv2_instance: unsafe {
lv2_plugin
.instantiate(lv2_features.clone(), 48000.0)
.expect(&format!("instantiate failed: {uri}"))
},
lv2_port_list: lv2_plugin.ports().collect::<Vec<_>>(),
lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER),
lv2_ui_thread: None,
lv2_world,
lv2_features,
lv2_plugin,
})
}
}
fn lv2_jack_process (
Lv2 {
midi_ins, midi_outs, audio_ins, audio_outs,
lv2_features, lv2_instance, lv2_input_buffer, ..
}: &mut Lv2,
_client: &Client,
scope: &ProcessScope
) -> Control {
let urid = lv2_features.midi_urid();
lv2_input_buffer.clear();
for port in midi_ins.iter() {
let mut atom = ::livi::event::LV2AtomSequence::new(
&lv2_features,
scope.n_frames() as usize
);
for event in port.iter(scope) {
match event.bytes.len() {
3 => atom.push_midi_event::<3>(
event.time as i64,
urid,
&event.bytes[0..3]
).unwrap(),
_ => {}
}
}
lv2_input_buffer.push(atom);
}
let mut outputs = vec![];
for _ in midi_outs.iter() {
outputs.push(::livi::event::LV2AtomSequence::new(
lv2_features,
scope.n_frames() as usize
));
}
let ports = ::livi::EmptyPortConnections::new()
.with_atom_sequence_inputs(lv2_input_buffer.iter())
.with_atom_sequence_outputs(outputs.iter_mut())
.with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope)))
.with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)));
unsafe {
lv2_instance.run(scope.n_frames() as usize, ports).unwrap()
};
Control::Continue
}
impl LV2PluginUI { pub fn new () -> Usually<Self> { Ok(Self { window: None }) } }
impl ApplicationHandler for LV2PluginUI {
fn resumed (&mut self, event_loop: &ActiveEventLoop) {
self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap());
}
fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => {
self.window.as_ref().unwrap().set_visible(false);
event_loop.exit();
},
WindowEvent::RedrawRequested => {
self.window.as_ref().unwrap().request_redraw();
}
_ => (),
}
}
}
impl Draw<Tui> for Lv2 {
fn draw(self, to: &mut Tui) {
let area = to.area();
let XYWH(x, y, _, height) = area;
let mut width = 20u16;
let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1));
let end = start + height as usize - 2;
//draw_box(buf, Rect { x, y, width, height });
for i in start..end {
if let Some(port) = self.lv2_port_list.get(i) {
let value = if let Some(value) = self.lv2_instance.control_input(port.index) {
value
} else {
port.default_value
};
//let label = &format!("C·· M·· {:25} = {value:.03}", port.name);
let label = &format!("{:25} = {value:.03}", port.name);
width = width.max(label.len() as u16 + 4);
let style = if i == self.selected {
Some(Style::default().green())
} else {
None
} ;
to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style);
} else {
break
}
}
draw_header(self, to, x, y, width);
}
}
fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) {
let style = Style::default().gray();
let label1 = format!(" {}", state.name);
to.blit(&label1, x + 1, y, Some(style.white().bold()));
if let Some(ref path) = state.path {
let label2 = format!("{}", &path[..((w as usize - 10).min(path.len()))]);
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
}
//Ok(Rect { x, y, width: w, height: 1 })
}
#[cfg(feature = "vst2")] impl<E: Engine> ::vst::host::Host for Plugin<E> {}
#[cfg(feature = "vst2")] fn set_vst_plugin <E: Engine> (
host: &Arc<Mutex<Plugin<E>>>, _path: &str
) -> Usually<PluginKind> {
let mut loader = ::vst::host::PluginLoader::load(
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
host.clone()
)?;
Ok(PluginKind::VST2 {
instance: loader.instance()?
})
}
#[cfg(feature = "lv2_gui")]
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
Ok(spawn(move||{
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
event_loop.set_control_flow(ControlFlow::Wait);
event_loop.run_app(&mut ui).unwrap()
}))
}
#[cfg(feature = "lv2_gui")]
fn lv2_ui_instantiate (kind: &str) {
//let host = Suil
}

View file

@ -1,703 +0,0 @@
use crate::{*, device::*, browse::*, mix::*};
def_command!(SamplerCommand: |sampler: Sampler| {
RecordToggle { slot: usize } => {
let slot = *slot;
let recording = sampler.recording.as_ref().map(|x|x.0);
let _ = Self::RecordFinish.execute(sampler)?;
// autoslice: continue recording at next slot
if recording != Some(slot) {
Self::RecordBegin { slot }.execute(sampler)
} else {
Ok(None)
}
},
RecordBegin { slot: usize } => {
let slot = *slot;
sampler.recording = Some((
slot,
Some(Arc::new(RwLock::new(Sample::new(
"Sample", 0, 0, vec![vec![];sampler.audio_ins.len()]
))))
));
Ok(None)
},
RecordFinish => {
let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{
std::mem::swap(sample, &mut sampler.samples.0[*index]);
sample
}); // TODO: undo
Ok(None)
},
RecordCancel => {
sampler.recording = None;
Ok(None)
},
PlaySample { slot: usize } => {
let slot = *slot;
if let Some(ref sample) = sampler.samples.0[slot] {
sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
}
Ok(None)
},
StopSample { slot: usize } => {
let _slot = *slot;
todo!();
//Ok(None)
},
});
/// 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>>);
impl<const N: usize> Default for SampleKit<N> {
fn default () -> Self { Self([const { None }; N]) }
}
impl Iterator for Voice {
type Item = [f32;2];
fn next (&mut self) -> Option<Self::Item> {
if self.after > 0 {
self.after -= 1;
return Some([0.0, 0.0])
}
let sample = self.sample.read().unwrap();
if self.position < sample.end {
let position = self.position;
self.position += 1;
return sample.channels[0].get(position).map(|_amplitude|[
sample.channels[0][position] * self.velocity * sample.gain,
sample.channels[0][position] * self.velocity * sample.gain,
])
}
None
}
}
impl NoteRange for Sampler {
fn note_lo (&self) -> &AtomicUsize {
&self.note_lo
}
fn note_axis (&self) -> &AtomicUsize {
&self.size.y
}
}
impl NotePoint for Sampler {
fn note_len (&self) -> &AtomicUsize {
unreachable!();
}
fn get_note_len (&self) -> usize {
0
}
fn set_note_len (&self, _x: usize) -> usize {
0 /*TODO?*/
}
fn note_pos (&self) -> &AtomicUsize {
&self.note_pt
}
fn get_note_pos (&self) -> usize {
self.note_pt.load(Relaxed)
}
fn set_note_pos (&self, x: usize) -> usize {
let old = self.note_pt.swap(x, Relaxed);
self.cursor.0.store(x % 8, Relaxed);
self.cursor.1.store(x / 8, Relaxed);
old
}
}
impl Sampler {
pub fn new (
jack: &Jack<'static>,
name: impl AsRef<str>,
#[cfg(feature = "port")] midi_from: &[Connect],
#[cfg(feature = "port")] audio_from: &[&[Connect];2],
#[cfg(feature = "port")] audio_to: &[&[Connect];2],
) -> Usually<Self> {
let name = name.as_ref();
Ok(Self {
name: name.into(),
input_meters: vec![0.0;2],
output_meters: vec![0.0;2],
output_gain: 1.,
buffer: vec![vec![0.0;16384];2],
#[cfg(feature = "port")] midi_in: Some(
MidiInput::new(jack, &format!("M/{name}"), midi_from)?
),
#[cfg(feature = "port")] audio_ins: vec![
AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?,
AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?,
],
#[cfg(feature = "port")] audio_outs: vec![
AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?,
AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?,
],
..Default::default()
})
}
/// Value of cursor
pub fn cursor (&self) -> (usize, usize) {
(self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed))
}
fn sample_selected (&self) -> usize {
(self.get_note_pos() as u8).into()
}
fn sample_selected_pitch (&self) -> u7 {
(self.get_note_pos() as u8).into()
}
pub fn process_audio_in (&mut self, scope: &ProcessScope) {
self.reset_input_meters();
if self.recording.is_some() {
self.record_into(scope);
} else {
self.update_input_meters(scope);
}
}
/// Make sure that input meter count corresponds to input channel count
fn reset_input_meters (&mut self) {
let channels = self.audio_ins.len();
if self.input_meters.len() != channels {
self.input_meters = vec![f32::MIN;channels];
}
}
/// Record from inputs to sample
fn record_into (&mut self, scope: &ProcessScope) {
if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 {
let mut sample = sample.write().unwrap();
if sample.channels.len() != self.audio_ins.len() {
panic!("channel count mismatch");
}
let samples_with_meters = self.audio_ins.iter()
.zip(self.input_meters.iter_mut())
.zip(sample.channels.iter_mut());
let mut length = 0;
for ((input, meter), channel) in samples_with_meters {
let slice = input.port().as_slice(scope);
length = length.max(slice.len());
*meter = to_rms(slice);
channel.extend_from_slice(slice);
}
sample.end += length;
} else {
panic!("tried to record into the void")
}
}
/// Update input meters
fn update_input_meters (&mut self, scope: &ProcessScope) {
for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) {
let slice = input.port().as_slice(scope);
*meter = to_rms(slice);
}
}
/// Make sure that output meter count corresponds to input channel count
fn reset_output_meters (&mut self) {
let channels = self.audio_outs.len();
if self.output_meters.len() != channels {
self.output_meters = vec![f32::MIN;channels];
}
}
/// Mix all currently playing samples into the output.
pub fn process_audio_out (&mut self, scope: &ProcessScope) {
self.clear_output_buffer();
self.populate_output_buffer(scope.n_frames() as usize);
self.write_output_buffer(scope);
}
/// Zero the output buffer.
fn clear_output_buffer (&mut self) {
for buffer in self.buffer.iter_mut() {
buffer.fill(0.0);
}
}
/// Write playing voices to output buffer
fn populate_output_buffer (&mut self, frames: usize) {
let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self;
let _channel_count = buffer.len();
match mixing_mode {
MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{
mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next())
}),
MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{
mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next())
}),
}
}
/// Write output buffer to output ports.
fn write_output_buffer (&mut self, scope: &ProcessScope) {
let Sampler { audio_outs, buffer, .. } = self;
for (i, port) in audio_outs.iter_mut().enumerate() {
let buffer = &buffer[i];
for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() {
*value = *buffer.get(i).unwrap_or(&0.0);
}
}
}
}
impl SampleAdd {
fn exited (&self) -> bool {
self.exited
}
fn exit (&mut self) {
self.exited = true
}
pub fn new (
sample: &Arc<RwLock<Sample>>,
voices: &Arc<RwLock<Vec<Voice>>>
) -> Usually<Self> {
let dir = std::env::current_dir()?;
let (subdirs, files) = scan(&dir)?;
Ok(Self {
exited: false,
dir,
subdirs,
files,
cursor: 0,
offset: 0,
sample: sample.clone(),
voices: voices.clone(),
_search: None
})
}
fn rescan (&mut self) -> Usually<()> {
scan(&self.dir).map(|(subdirs, files)|{
self.subdirs = subdirs;
self.files = files;
})
}
fn prev (&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
fn next (&mut self) {
self.cursor = self.cursor + 1;
}
fn try_preview (&mut self) -> Usually<()> {
if let Some(path) = self.cursor_file() {
if let Ok(sample) = Sample::from_file(&path) {
*self.sample.write().unwrap() = sample;
self.voices.write().unwrap().push(
Sample::play(&self.sample, 0, &u7::from(100u8))
);
}
//load_sample(&path)?;
//let src = std::fs::File::open(&path)?;
//let mss = MediaSourceStream::new(Box::new(src), Default::default());
//let mut hint = Hint::new();
//if let Some(ext) = path.extension() {
//hint.with_extension(&ext.to_string_lossy());
//}
//let meta_opts: MetadataOptions = Default::default();
//let fmt_opts: FormatOptions = Default::default();
//if let Ok(mut probed) = symphonia::default::get_probe()
//.format(&hint, mss, &fmt_opts, &meta_opts)
//{
//panic!("{:?}", probed.format.metadata());
//};
}
Ok(())
}
fn cursor_dir (&self) -> Option<PathBuf> {
if self.cursor < self.subdirs.len() {
Some(self.dir.join(&self.subdirs[self.cursor]))
} else {
None
}
}
fn cursor_file (&self) -> Option<PathBuf> {
if self.cursor < self.subdirs.len() {
return None
}
let index = self.cursor.saturating_sub(self.subdirs.len());
if index < self.files.len() {
Some(self.dir.join(&self.files[index]))
} else {
None
}
}
fn pick (&mut self) -> Usually<bool> {
if self.cursor == 0 {
if let Some(parent) = self.dir.parent() {
self.dir = parent.into();
self.rescan()?;
self.cursor = 0;
return Ok(false)
}
}
if let Some(dir) = self.cursor_dir() {
self.dir = dir;
self.rescan()?;
self.cursor = 0;
return Ok(false)
}
if let Some(path) = self.cursor_file() {
let (end, channels) = read_sample_data(&path.to_string_lossy())?;
let mut sample = self.sample.write().unwrap();
sample.name = path.file_name().unwrap().to_string_lossy().into();
sample.end = end;
sample.channels = channels;
return Ok(true)
}
return Ok(false)
}
}
impl<const N: usize> SampleKit<N> {
pub fn get (&self, index: usize) -> &Option<Arc<RwLock<Sample>>> {
if index < self.0.len() {
&self.0[index]
} else {
&None
}
}
}
impl Sample {
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
Self {
name: name.as_ref().into(),
start,
end,
channels,
rate: None,
gain: 1.0,
color: ItemTheme::random(),
}
}
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
Voice {
sample: sample.clone(),
after,
position: sample.read().unwrap().start,
velocity: velocity.as_int() as f32 / 127.0,
}
}
pub fn handle_cc (&mut self, controller: u7, value: u7) {
let percentage = value.as_int() as f64 / 127.;
match controller.as_int() {
20 => {
self.start = (percentage * self.end as f64) as usize;
},
21 => {
let length = self.channels[0].len();
self.end = length.min(
self.start + (percentage * (length as f64 - self.start as f64)) as usize
);
},
22 => { /*attack*/ },
23 => { /*decay*/ },
24 => {
self.gain = percentage as f32 * 2.0;
},
26 => { /* pan */ }
25 => { /* pitch */ }
_ => {}
}
}
/// Read WAV from file
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
let mut channels: Vec<wavers::Samples<f32>> = vec![];
for channel in wavers::Wav::from_path(src)?.channels() {
channels.push(channel);
}
let mut end = 0;
let mut data: Vec<Vec<f32>> = vec![];
for samples in channels.iter() {
let channel = Vec::from(samples.as_ref());
end = end.max(channel.len());
data.push(channel);
}
Ok((end, data))
}
pub fn from_file (path: &PathBuf) -> Usually<Self> {
let name = path.file_name().unwrap().to_string_lossy().into();
let mut sample = Self { name, ..Default::default() };
// Use file extension if present
let mut hint = Hint::new();
if let Some(ext) = path.extension() {
hint.with_extension(&ext.to_string_lossy());
}
let probed = symphonia::default::get_probe().format(
&hint,
MediaSourceStream::new(
Box::new(File::open(path)?),
Default::default(),
),
&Default::default(),
&Default::default()
)?;
let mut format = probed.format;
let params = &format.tracks().iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
.expect("no tracks found")
.codec_params;
let mut decoder = get_codecs().make(params, &Default::default())?;
loop {
match format.next_packet() {
Ok(packet) => sample.decode_packet(&mut decoder, packet)?,
Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(),
Err(err) => return Err(err.into()),
};
};
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
Ok(sample)
}
fn decode_packet (
&mut self, decoder: &mut Box<dyn Decoder>, packet: Packet
) -> Usually<()> {
// Decode a packet
let decoded = decoder
.decode(&packet)
.map_err(|e|Box::<dyn std::error::Error>::from(e))?;
// Determine sample rate
let spec = *decoded.spec();
if let Some(rate) = self.rate {
if rate != spec.rate as usize {
panic!("sample rate changed");
}
} else {
self.rate = Some(spec.rate as usize);
}
// Determine channel count
while self.channels.len() < spec.channels.count() {
self.channels.push(vec![]);
}
// Load sample
let mut samples = SampleBuffer::new(
decoded.frames() as u64,
spec
);
if samples.capacity() > 0 {
samples.copy_interleaved_ref(decoded);
for frame in samples.samples().chunks(spec.channels.count()) {
for (chan, frame) in frame.iter().enumerate() {
self.channels[chan].push(*frame)
}
}
}
Ok(())
}
}
impl Draw<Tui> for SampleAdd {
fn draw (self, _to: &mut Tui) -> Usually<XYWH<u16>> {
todo!()
}
}
fn draw_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String {
if let Some(sample) = sample {
let sample = sample.read().unwrap();
format!("{:8}", sample.name)
//format!("{:8} {:3} {:6}-{:6}/{:6}",
//sample.name,
//sample.gain,
//sample.start,
//sample.end,
//sample.channels[0].len()
//)
} else {
String::from("........")
}
}
fn draw_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
let min_db = -64.0;
Thunk::new(move|to: &mut Tui|{
let XYWH(x, y, width, height) = to.area();
let area = Rect { x, y, width, height };
if let Some(sample) = &sample {
let sample = sample.read().unwrap();
let start = sample.start as f64;
let end = sample.end as f64;
let length = end - start;
let step = length / width as f64;
let mut t = start;
let mut lines = vec![];
while t < end {
let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)];
let total: f32 = chunk.iter().map(|x|x.abs()).sum();
let count = chunk.len() as f32;
let meter = 10. * (total / count).log10();
let x = t as f64;
let y = meter as f64;
lines.push(Line::new(x, min_db, x, y, Color::Green));
t += step / 2.;
}
Canvas::default()
.x_bounds([sample.start as f64, sample.end as f64])
.y_bounds([min_db, 0.])
.paint(|ctx| {
for line in lines.iter() {
ctx.draw(line);
}
//FIXME: proportions
//let text = "press record to finish sampling";
//ctx.print(
//(width - text.len() as u16) as f64 / 2.0,
//height as f64 / 2.0,
//text.red()
//);
}).render(area, &mut to.buffer);
} else {
Canvas::default()
.x_bounds([0.0, width as f64])
.y_bounds([0.0, height as f64])
.paint(|_ctx| {
//let text = "press record to begin sampling";
//ctx.print(
//(width - text.len() as u16) as f64 / 2.0,
//height as f64 / 2.0,
//text.red()
//);
})
.render(area, &mut to.buffer);
}
})
}
impl_audio!(Sampler: sampler_jack_process);
pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control {
if let Some(midi_in) = &state.midi_in {
for midi in midi_in.port().iter(scope) {
sampler_midi_in(&state.samples, &state.voices, midi)
}
}
state.process_audio_out(scope);
state.process_audio_in(scope);
Control::Continue
}
/// Create [Voice]s from [Sample]s in response to MIDI input.
fn sampler_midi_in (
samples: &SampleKit<128>, voices: &Arc<RwLock<Vec<Voice>>>, RawMidi { time, bytes }: RawMidi
) {
if let Ok(LiveEvent::Midi { message, .. }) = LiveEvent::parse(bytes) {
match message {
MidiMessage::NoteOn { ref key, ref vel } => {
if let Some(sample) = samples.get(key.as_int() as usize) {
voices.write().unwrap().push(Sample::play(sample, time as usize, vel));
}
},
MidiMessage::Controller { controller: _, value: _ } => {
// TODO
}
_ => {}
}
}
}
fn draw_sample (
to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
) -> Usually<usize> {
let style = if focus { Style::default().green() } else { Style::default() };
if focus {
to.blit(&"🬴", x+1, y, Some(style.bold()));
}
let label1 = format!("{:3} {:12}",
note.map(|n|n.to_string()).unwrap_or(String::default()),
sample.name);
let label2 = format!("{:>6} {:>6} +0.0",
sample.start,
sample.end);
to.blit(&label1, x+2, y, Some(style.bold()));
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
Ok(label1.len() + label2.len() + 4)
}
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
todo!();
}

View file

@ -1,161 +0,0 @@
use crate::*;
/// 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 },
}
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.clip(*track) {
Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name),
None => format!("T{track} S{scene}: Empty")
},
_ => format!("T{track} S{scene}: Empty"),
},
_ => todo!()
}).into()
}
#[cfg(feature = "scene")] pub fn scene (&self) -> Option<usize> {
use Selection::*;
match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None }
}
#[cfg(feature = "scene")] 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!(),
}
}
#[cfg(feature = "scene")] 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!()
}
}
#[cfg(feature = "scene")] 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")] 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
}
}
#[cfg(feature = "track")] 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!(),
}
}
#[cfg(feature = "track")] 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!()
}
}
#[cfg(feature = "track")] 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: AsRef<Selection>+AsMut<Selection>> HasSelection for T {}
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()
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,974 +0,0 @@
#![allow(clippy::unit_arg)]
#![feature(
adt_const_params, associated_type_defaults, closure_lifetime_binder,
impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, type_changing_struct_update
)]
/// Implement an arithmetic operation for a unit of time
#[macro_export] macro_rules! impl_op {
($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => {
impl $Op<Self> for $T {
type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output {
let $a = self.get(); let $b = other.get(); Self($impl.into())
}
}
impl $Op<usize> for $T {
type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output {
let $a = self.get(); let $b = other as f64; Self($impl.into())
}
}
impl $Op<f64> for $T {
type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output {
let $a = self.get(); let $b = other; Self($impl.into())
}
}
}
}
#[macro_export] macro_rules! impl_has_clips {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
$cb.read().unwrap()
}
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
$cb.write().unwrap()
}
}
}
}
pub mod arrange;
pub mod bind;
pub mod browse;
pub mod cli;
pub mod clock;
pub mod config;
pub mod device;
pub mod dialog;
pub mod menu;
pub mod mix;
pub mod mode;
pub mod plugin;
pub mod sample;
pub mod sequence;
pub mod select;
pub mod view;
use clap::{self, Parser, Subcommand};
use builder_pattern::Builder;
use self::{
arrange::*, clock::*, dialog::*, browse::*, select::*, sequence::*, device::*,
config::*, mode::*, view::*, bind::*
};
extern crate xdg;
pub(crate) use ::xdg::BaseDirectories;
pub extern crate atomic_float;
pub(crate) use atomic_float::AtomicF64;
//pub extern crate jack;
//pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}};
pub extern crate midly;
pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*};
pub extern crate tengri;
pub(crate) use tengri::{
*,
lang::*,
play::*,
keys::*,
sing::*,
time::*,
draw::*,
term::*,
color::*,
space::*,
crossterm::event::{Event, KeyEvent},
ratatui::{
self,
prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
widgets::{Widget, canvas::{Canvas, Line}},
},
};
#[cfg(feature = "sampler")] pub(crate) use symphonia::{
default::get_codecs,
core::{//errors::Error as SymphoniaError,
audio::SampleBuffer, formats::Packet, io::MediaSourceStream, probe::Hint,
codecs::{Decoder, CODEC_TYPE_NULL},
},
};
#[cfg(feature = "lv2_gui")] use ::winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::{Window, WindowId},
platform::x11::EventLoopBuilderExtX11
};
#[allow(unused)] pub(crate) use ::{
std::{
cmp::Ord,
collections::BTreeMap,
error::Error,
ffi::OsString,
fmt::{Write, Debug, Formatter},
fs::File,
ops::{Add, Sub, Mul, Div, Rem},
path::{Path, PathBuf},
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}},
time::Duration,
thread::{spawn, JoinHandle},
},
};
/// Command-line entrypoint.
#[cfg(feature = "cli")] pub fn main () -> Usually<()> {
Config::watch(|config|{
Exit::enter(|exit|{
Jack::connect("tek", |jack|{
let state = Arc::new(RwLock::new(App {
color: ItemTheme::random(),
config: Config::init(),
dialog: Dialog::welcome(),
jack: jack.clone(),
mode: ":menu",
project: Arrangement::new(&jack, &Clock::new(&jack, 51)),
..Default::default()
}));
// TODO: Sync these timings with main clock, so that things
// "accidentally" fall on the beat in overload conditions.
let keyboard = run_tui_in(&exit, &state, Duration::from_millis(100))?;
let terminal = run_tui_out(&exit, &state, Duration::from_millis(10))?;
(keyboard, terminal)
})
})
})
}
/// Create a new application from a backend, project, config, and mode
///
/// ```
/// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
/// let proj = tek::Arrangement::default();
/// let mut conf = tek::Config::default();
/// conf.add("(mode hello)");
/// let tek = tek::tek(&jack, proj, conf, "hello");
/// ```
pub fn tek (
jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
) -> App {
let mode: &str = mode.as_ref();
App {
color: ItemTheme::random(),
dialog: Dialog::welcome(),
jack: jack.clone(),
mode: config.get_mode(mode).expect(&format!("failed to find mode '{mode}'")),
config,
project,
..Default::default()
}
}
fn tek_confirm (state: &mut App) -> Perhaps<AppCommand> {
Ok(match &state.dialog {
Dialog::Menu(index, items) => {
let callback = items.0[*index].1.clone();
callback(state)?;
None
},
_ => todo!(),
})
}
fn tek_inc (state: &mut App, axis: &ControlAxis) -> Perhaps<AppCommand> {
Ok(match (&state.dialog, axis) {
(Dialog::None, _) => todo!(),
(Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_next() }
.execute(state)?,
_ => todo!()
})
}
fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps<AppCommand> {
Ok(match (&state.dialog, axis) {
(Dialog::None, _) => None,
(Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_prev() }
.execute(state)?,
_ => todo!()
})
}
fn collect_commands (app: &App, input: &TuiIn) -> Usually<Vec<AppCommand>> {
let mut commands = vec![];
for id in app.mode.keys.iter() {
if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref())
&& let Some(bindings) = event_map.query(input.event()) {
for binding in bindings {
for command in binding.commands.iter() {
if let Some(command) = app.namespace(command)? as Option<AppCommand> {
commands.push(command)
}
}
}
}
}
Ok(commands)
}
fn execute_commands (
app: &mut App, commands: Vec<AppCommand>
) -> Usually<Vec<(AppCommand, Option<AppCommand>)>> {
let mut history = vec![];
for command in commands.into_iter() {
let result = command.execute(app);
match result { Err(err) => { history.push((command, None)); return Err(err) }
Ok(undo) => { history.push((command, undo)); } };
}
Ok(history)
}
pub fn tek_jack_process (app: &mut App, client: &Client, scope: &ProcessScope) -> Control {
let t0 = app.perf.get_t0();
app.clock().update_from_scope(scope).unwrap();
let midi_in = app.project.midi_input_collect(scope);
if let Some(editor) = &app.editor() {
let mut pitch: Option<u7> = None;
for port in midi_in.iter() {
for event in port.iter() {
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
= event
{
pitch = Some(key.clone());
}
}
}
if let Some(pitch) = pitch {
editor.set_note_pos(pitch.as_int() as usize);
}
}
let result = app.project.process_tracks(client, scope);
app.perf.update_from_jack_scope(t0, scope);
result
}
pub fn tek_jack_event (app: &mut App, event: JackEvent) {
use JackEvent::*;
match event {
SampleRate(sr) => { app.clock().timebase.sr.set(sr as f64); },
PortRegistration(_id, true) => {
//let port = app.jack().port_by_id(id);
//println!("\rport add: {id} {port:?}");
//println!("\rport add: {id}");
},
PortRegistration(_id, false) => {
/*println!("\rport del: {id}")*/
},
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
ClientRegistration(_id, true) => {},
ClientRegistration(_id, false) => {},
ThreadInit => {},
XRun => {},
GraphReorder => {},
_ => { panic!("{event:?}"); }
}
}
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))))
}
}
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
#[macro_export] macro_rules! has_clip {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
}
}
}
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
let entry = entry.expect("failed to read drectory entry");
let meta = entry.metadata().expect("failed to read entry metadata");
if meta.is_file() {
files.push(entry.file_name());
} else if meta.is_dir() {
subdirs.push(entry.file_name());
}
(subdirs, files)
});
subdirs.sort();
files.sort();
Ok((subdirs, files))
}
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
track.width as u16
}
def_command!(AppCommand: |app: App| {
Nop => Ok(None),
Confirm => tek_confirm(app),
Cancel => todo!(), // TODO delegate:
Inc { axis: ControlAxis } => tek_inc(app, axis),
Dec { axis: ControlAxis } => tek_dec(app, axis),
SetDialog { dialog: Dialog } => {
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog })
},
});
/// Define a type alias for iterators of sized items (columns).
macro_rules! def_sizes_iter {
($Type:ident => $($Item:ty),+) => {
pub trait $Type<'a> =
Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a;
}
}
def_sizes_iter!(InputsSizes => MidiInput);
def_sizes_iter!(OutputsSizes => MidiOutput);
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
def_sizes_iter!(ScenesSizes => Scene);
def_sizes_iter!(TracksSizes => Track);
/// ```
/// let _ = tek::view_logo();
/// ```
pub fn view_logo () -> impl Draw<Tui> {
wh_exact(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), south!{
h_exact(1, ""),
h_exact(1, ""),
h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"),
h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))),
h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"),
})))
}
/// ```
/// let x = std::sync::Arc::<std::sync::RwLock<String>>::default();
/// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone());
/// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone());
/// ```
pub fn view_transport (
play: bool,
bpm: Arc<RwLock<String>>,
beat: Arc<RwLock<String>>,
time: Arc<RwLock<String>>,
) -> impl Draw<Tui> {
let theme = ItemTheme::G[96];
Tui::bg(Black, east!(above(
wh_full(origin_w(button_play_pause(play))),
wh_full(origin_e(east!(
field_h(theme, "BPM", bpm),
field_h(theme, "Beat", beat),
field_h(theme, "Time", time),
)))
)))
}
/// ```
/// let x = std::sync::Arc::<std::sync::RwLock<String>>::default();
/// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone());
/// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone());
/// ```
pub fn view_status (
sel: Option<Arc<str>>,
sr: Arc<RwLock<String>>,
buf: Arc<RwLock<String>>,
lat: Arc<RwLock<String>>,
) -> impl Draw<Tui> {
let theme = ItemTheme::G[96];
Tui::bg(Black, east!(above(
wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))),
wh_full(origin_e(east!(
field_h(theme, "SR", sr),
field_h(theme, "Buf", buf),
field_h(theme, "Lat", lat),
)))
)))
}
/// ```
/// let _ = tek::button_play_pause(true);
/// ```
pub fn button_play_pause (playing: bool) -> impl Draw<Tui> {
let compact = true;//self.is_editing();
Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
either(compact,
Thunk::new(move|to: &mut Tui|to.place(&w_exact(9, either(playing,
Tui::fg(Rgb(0, 255, 0), " PLAYING "),
Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
)),
Thunk::new(move|to: &mut Tui|to.place(&w_exact(5, either(playing,
Tui::fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
Tui::fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",))))
))
)
)
}
#[cfg(feature = "track")] pub fn view_track_row_section (
_theme: ItemTheme,
button: impl Draw<Tui>,
button_add: impl Draw<Tui>,
content: impl Draw<Tui>,
) -> impl Draw<Tui> {
west(h_full(w_exact(4, origin_nw(button_add))),
east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content))))
}
/// ```
/// let bg = tengri::ratatui::style::Color::Red;
/// let fg = tengri::ratatui::style::Color::Green;
/// let _ = tek::view_wrap(bg, fg, "and then blue, too!");
/// ```
pub fn view_wrap (bg: Color, fg: Color, content: impl Draw<Tui>) -> impl Draw<Tui> {
let left = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("")));
let right = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("")));
east(left, west(right, Tui::fg_bg(fg, bg, content)))
}
/// ```
/// let _ = tek::view_meter("", 0.0);
/// let _ = tek::view_meters(&[0.0, 0.0]);
/// ```
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw<Tui> + 'a {
south!(
field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)),
wh_exact(if value >= 0.0 { 13 }
else if value >= -1.0 { 12 }
else if value >= -2.0 { 11 }
else if value >= -3.0 { 10 }
else if value >= -4.0 { 9 }
else if value >= -6.0 { 8 }
else if value >= -9.0 { 7 }
else if value >= -12.0 { 6 }
else if value >= -15.0 { 5 }
else if value >= -20.0 { 4 }
else if value >= -25.0 { 3 }
else if value >= -30.0 { 2 }
else if value >= -40.0 { 1 }
else { 0 }, 1, Tui::bg(if value >= 0.0 { Red }
else if value >= -3.0 { Yellow }
else { Green }, ())))
}
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
let left = format!("L/{:>+9.3}", values[0]);
let right = format!("R/{:>+9.3}", values[1]);
south(left, right)
}
pub fn draw_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
when(sample.is_some(), Thunk::new(move|to: &mut Tui|{
let sample = sample.unwrap().read().unwrap();
let theme = sample.color;
to.place(&east!(
field_h(theme, "Name", format!("{:<10}", sample.name.clone())),
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())),
field_h(theme, "Start", format!("{:<8}", sample.start)),
field_h(theme, "End", format!("{:<8}", sample.end)),
field_h(theme, "Trans", "0"),
field_h(theme, "Gain", format!("{}", sample.gain)),
))
}))
}
pub fn draw_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
either(sample.is_some(), Thunk::new(move|to: &mut Tui|{
let sample = sample.unwrap().read().unwrap();
let theme = sample.color;
to.place(&w_exact(20, south!(
w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))),
w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))),
w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))),
w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))),
w_full(origin_w(field_h(theme, "Trans ", "0"))),
w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))),
)))
}), Thunk::new(|to: &mut Tui|to.place(&Tui::fg(Red, south!(
Tui::bold(true, "× No sample."),
"[r] record",
"[Shift-F9] import",
)))))
}
pub fn draw_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> {
Tui::bold(true, Tui::fg(Tui::g(224), sample
.map(|sample|{
let sample = sample.read().unwrap();
format!("Sample {}-{}", sample.start, sample.end)
})
.unwrap_or_else(||"No sample".to_string())))
}
pub fn view_track_header (theme: ItemTheme, content: impl Draw<Tui>) -> impl Draw<Tui> {
w_exact(12, Tui::bg(theme.darker.rgb, w_full(origin_e(content))))
}
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
-> impl Draw<Tui> + use<'a, T>
{
let ins = ports.len() as u16;
let frame = Outer(true, Style::default().fg(Tui::g(96)));
let iter = move||ports.iter();
let names = iter_south(1, iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name()))));
let field = field_v(theme, title, names);
wh_exact(20, 1 + ins, frame.enclose(wh_exact(20, 1 + ins, field)))
}
pub fn io_ports <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
iter(items, move|(
_index, name, connections, y, y2
): (usize, &'a Arc<str>, &'a [Connect], usize, usize), _|
iter_south(y as u16, (y2-y) as u16, south(
h_full(Tui::bold(true, Tui::fg_bg(fg, bg, origin_w(east(&" 󰣲 ", name))))),
iter(||connections.iter(), move|connect: &'a Connect, index|iter_south(index as u16, 1,
h_full(origin_w(Tui::bold(false, Tui::fg_bg(fg, bg,
&connect.info)))))))))
}
/// CLI banner.
pub(crate) const HEADER: &'static str = r#"
~ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
term ~ v0.4.0, 2026 winter (or is it) ~
~ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
/// Total state
///
/// ```
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
/// let mut app = tek::App::default();
/// let _ = app.scene_add(None, None).unwrap();
/// let _ = app.update_clock();
/// app.project.editor = Some(Default::default());
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
/// //let _: Vec<_> = app.project.outputs_with_sizes().collect();
/// let _: Vec<_> = app.project.tracks_with_sizes().collect();
/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
/// let _ = app.project.w();
/// //let _ = app.project.w_sidebar();
/// //let _ = app.project.w_tracks_area();
/// let _ = app.project.h();
/// //let _ = app.project.h_tracks_area();
/// //let _ = app.project.h_inputs();
/// //let _ = app.project.h_outputs();
/// let _ = app.project.h_scenes();
/// ```
#[derive(Default, Debug)] pub struct App {
/// Base color.
pub color: ItemTheme,
/// Must not be dropped for the duration of the process
pub jack: Jack<'static>,
/// Display size
pub size: Measure<Tui>,
/// Performance counter
pub perf: PerfModel,
/// Available view modes and input bindings
pub config: Config,
/// Currently selected mode
pub mode: Arc<Mode<Arc<str>>>,
/// Undo history
pub history: Vec<(AppCommand, Option<AppCommand>)>,
/// Dialog overlay
pub dialog: Dialog,
/// Contains all recently created clips.
pub pool: Pool,
/// Contains the currently edited musical arrangement
pub project: Arrangement,
/// Error, if any
pub error: Arc<RwLock<Option<Arc<str>>>>
}
impl_has!(Clock: |self: App|self.project.clock);
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
impl_has!(Dialog: |self: App|self.dialog);
impl_has!(Jack<'static>: |self: App|self.jack);
impl_has!(Measure<Tui>: |self: App|self.size);
impl_has!(Pool: |self: App|self.pool);
impl_has!(Selection: |self: App|self.project.selection);
impl_as_ref!(Vec<Scene>: |self: App|self.project.as_ref());
impl_as_mut!(Vec<Scene>: |self: App|self.project.as_mut());
impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt());
impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
impl_has_clips!( |self: App|self.pool.clips);
impl_audio!(App: tek_jack_process, tek_jack_event);
impl_handle!(TuiIn: |self: App, input|{
let commands = collect_commands(self, input)?;
let history = execute_commands(self, commands)?;
self.history.extend(history.into_iter());
Ok(None)
});
namespace!(App: Arc<str> { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); });
namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); });
namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| {
":w/sidebar" => app.project.w_sidebar(app.editor().is_some()),
":h/sample-detail" => 6.max(app.measure_height() as u16 * 3 / 9), }; });
namespace!(App: isize { literal = |dsl|try_to_isize(dsl); });
namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| {
":scene-count" => app.scenes().len(),
":track-count" => app.tracks().len(),
":device-kind" => app.dialog.device_kind().unwrap_or(0),
":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0),
":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; });
namespace!(App: bool { symbol = |app| { // Provide boolean values.
":mode/editor" => app.project.editor.is_some(),
":focused/dialog" => !matches!(app.dialog, Dialog::None),
":focused/message" => matches!(app.dialog, Dialog::Message(..)),
":focused/add_device" => matches!(app.dialog, Dialog::Device(..)),
":focused/browser" => app.dialog.browser().is_some(),
":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))),
":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))),
":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))),
":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))),
":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}),
":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)),
":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)),
":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix),
}; });
namespace!(App: ItemTheme {}); // TODO: provide colors here
namespace!(App: Selection { symbol = |app| {
":select/scene" => app.selection().select_scene(app.tracks().len()),
":select/scene/next" => app.selection().select_scene_next(app.scenes().len()),
":select/scene/prev" => app.selection().select_scene_prev(),
":select/track" => app.selection().select_track(app.tracks().len()),
":select/track/next" => app.selection().select_track_next(app.tracks().len()),
":select/track/prev" => app.selection().select_track_prev(),
}; });
namespace!(App: Color {
symbol = |app| {
":color/bg" => Color::Rgb(28, 32, 36),
};
expression = |app| {
"g" (n: u8) => Color::Rgb(n, n, n),
"rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b),
};
});
namespace!(App: Option<u7> { symbol = |app| {
":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into())
}; });
namespace!(App: Option<usize> { symbol = |app| {
":selected/scene" => app.selection().scene(),
":selected/track" => app.selection().track(),
}; });
namespace!(App: Option<Arc<RwLock<MidiClip>>> {
symbol = |app| {
":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() {
app.scenes()[*scene].clips[*track].clone()
} else {
None
}
};
});
pub trait HasClipsSize { fn clips_size (&self) -> &Measure<Tui>; }
pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
fn devices (&self) -> &Vec<Device> { self.as_ref() }
fn devices_mut (&mut self) -> &mut Vec<Device> { self.as_mut() }
}
pub trait HasWidth {
const MIN_WIDTH: usize;
/// Increment track width.
fn width_inc (&mut self);
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
fn width_dec (&mut self);
}
impl<'a> Namespace<'a, AppCommand> for App {
symbols!('a |app| -> AppCommand {
"x/inc" => AppCommand::Inc { axis: ControlAxis::X },
"x/dec" => AppCommand::Dec { axis: ControlAxis::X },
"y/inc" => AppCommand::Inc { axis: ControlAxis::Y },
"y/dec" => AppCommand::Dec { axis: ControlAxis::Y },
"confirm" => AppCommand::Confirm,
"cancel" => AppCommand::Cancel,
});
}
impl Understand<Tui, ()> for App {
fn understand_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> {
app_understand_expr(self, to, lang)
}
fn understand_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> {
app_understand_word(self, to, lang)
}
}
fn app_understand_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> {
if evaluate_output_expression(state, to, lang)?
|| evaluate_output_expression_tui(state, to, lang)? {
Ok(())
} else {
Err(format!("App::understand_expr: unexpected: {lang:?}").into())
}
}
fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> {
let mut frags = dsl.src()?.unwrap().split("/");
match frags.next() {
Some(":logo") => to.place(&view_logo()),
Some(":status") => to.place(&h_exact(1, "TODO: Status Bar")),
Some(":meters") => match frags.next() {
Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters")))),
Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters")))),
_ => panic!()
},
Some(":tracks") => match frags.next() {
None => to.place(&"TODO tracks"),
Some("names") => to.place(&state.project.view_track_names(state.color.clone())),//Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))),
Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs")))),
Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices")))),
Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs")))),
_ => panic!()
},
Some(":scenes") => match frags.next() {
None => to.place(&"TODO scenes"),
Some(":scenes/names") => to.place(&"TODO Scene Names"),
_ => panic!()
},
Some(":editor") => to.place(&"TODO Editor"),
Some(":dialog") => match frags.next() {
Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &state.dialog {
let items = items.clone();
let selected = selected;
Some(wh_full(Thunk::new(move|to: &mut Tui|{
for (index, MenuItem(item, _)) in items.0.iter().enumerate() {
to.place(&y_push((2 * index) as u16,
Tui::fg_bg(
if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) },
if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) },
h_exact(2, origin_n(w_full(item)))
)));
}
})))
} else {
None
}),
_ => unimplemented!("App::understand_word: {dsl:?} ({frags:?})"),
},
Some(":templates") => to.place(&{
let modes = state.config.modes.clone();
let height = (modes.read().unwrap().len() * 2) as u16;
h_exact(height, w_min(30, Thunk::new(move |to: &mut Tui|{
for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() {
let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
let fg1 = Rgb(224, 192, 128);
let fg2 = Rgb(224, 128, 32);
let field_name = w_full(origin_w(Tui::fg(fg1, name)));
let field_id = w_full(origin_e(Tui::fg(fg2, id)));
let field_info = w_full(origin_w(info));
to.place(&y_push((2 * index) as u16,
h_exact(2, w_full(Tui::bg(bg, south(
above(field_name, field_id), field_info))))));
}
})))
}),
Some(":sessions") => to.place(&h_exact(6, w_min(30, Thunk::new(|to: &mut Tui|{
let fg = Rgb(224, 192, 128);
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
to.place(&y_push((2 * index) as u16,
&h_exact(2, w_full(Tui::bg(bg, origin_w(Tui::fg(fg, name)))))));
}
})))),
Some(":browse/title") => to.place(&w_full(origin_w(field_v(ItemColor::default(),
match state.dialog.browser_target().unwrap() {
BrowseTarget::SaveProject => "Save project:",
BrowseTarget::LoadProject => "Load project:",
BrowseTarget::ImportSample(_) => "Import sample:",
BrowseTarget::ExportSample(_) => "Export sample:",
BrowseTarget::ImportClip(_) => "Import clip:",
BrowseTarget::ExportClip(_) => "Export clip:",
}, w_shrink(3, h_exact(1, Tui::fg(Tui::g(96), x_repeat("🭻")))))))),
Some(":device") => {
let selected = state.dialog.device_kind().unwrap();
to.place(&south(Tui::bold(true, "Add device"), iter_south(1,
move||device_kinds().iter(),
move|_label: &&'static str, i|{
let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
let lb = if i == selected { "[ " } else { " " };
let rb = if i == selected { " ]" } else { " " };
w_full(Tui::bg(bg, east(lb, west(rb, "FIXME device name")))) })))
},
Some(":debug") => to.place(&h_exact(1, format!("[{:?}]", to.area()))),
Some(_) => {
let views = state.config.views.read().unwrap();
if let Some(dsl) = views.get(dsl.src()?.unwrap()) {
let dsl = dsl.clone();
std::mem::drop(views);
state.understand(to, &dsl)?
} else {
unimplemented!("{dsl:?}");
}
},
_ => unreachable!()
}
Ok(())
}
impl App {
/// Update memoized render of clock values.
/// ```
/// tek::App::default().update_clock();
/// ```
pub fn update_clock (&self) {
ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80)
}
/// Set modal dialog.
///
/// ```
/// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome());
/// ```
pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog {
std::mem::swap(&mut self.dialog, &mut dialog);
dialog
}
/// FIXME: generalize. Set picked device in device pick dialog.
///
/// ```
/// tek::App::default().device_pick(0);
/// ```
pub fn device_pick (&mut self, index: usize) {
self.dialog = Dialog::Device(index);
}
/// FIXME: generalize. Add device to current track.
pub fn add_device (&mut self, index: usize) -> Usually<()> {
match index {
0 => {
let name = self.jack.with_client(|c|c.name().to_string());
let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name();
let track = self.track().expect("no active track");
let port = format!("{}/Sampler", &track.name);
let connect = Connect::exact(format!("{name}:{midi}"));
let sampler = if let Ok(sampler) = Sampler::new(
&self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]]
) {
self.dialog = Dialog::None;
Device::Sampler(sampler)
} else {
self.dialog = Dialog::Message("Failed to add device.".into());
return Err("failed to add device".into())
};
let track = self.track_mut().expect("no active track");
track.devices.push(sampler);
Ok(())
},
1 => {
todo!();
//Ok(())
},
_ => unreachable!(),
}
}
/// Return reference to content browser if open.
///
/// ```
/// assert_eq!(tek::App::default().browser(), None);
/// ```
pub fn browser (&self) -> Option<&Browse> {
if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None }
}
/// Is a MIDI editor currently focused?
///
/// ```
/// tek::App::default().editor_focused();
/// ```
pub fn editor_focused (&self) -> bool {
false
}
/// Toggle MIDI editor.
///
/// ```
/// tek::App::default().toggle_editor(None);
/// ```
pub fn toggle_editor (&mut self, value: Option<bool>) {
//FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed);
let value = value.unwrap_or_else(||!self.editor().is_some());
if value {
// Create new clip in pool when entering empty cell
if let Selection::TrackClip { track, scene } = *self.selection()
&& let Some(scene) = self.project.scenes.get_mut(scene)
&& let Some(slot) = scene.clips.get_mut(track)
&& slot.is_none()
&& let Some(track) = self.project.tracks.get_mut(track)
{
let (_index, clip) = self.pool.add_new_clip();
// autocolor: new clip colors from scene and track color
let color = track.color.base.mix(scene.color.base, 0.5);
clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into();
if let Some(editor) = &mut self.project.editor {
editor.set_clip(Some(&clip));
}
*slot = Some(clip.clone());
//Some(clip)
} else {
//None
}
} else if let Selection::TrackClip { track, scene } = *self.selection()
&& let Some(scene) = self.project.scenes.get_mut(scene)
&& let Some(slot) = scene.clips.get_mut(track)
&& let Some(clip) = slot.as_mut()
{
// Remove clip from arrangement when exiting empty clip editor
let mut swapped = None;
if clip.read().unwrap().count_midi_messages() == 0 {
std::mem::swap(&mut swapped, slot);
}
if let Some(clip) = swapped {
self.pool.delete_clip(&clip.read().unwrap());
}
}
}
}
impl Draw<Tui> for App {
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
if let Some(e) = self.error.read().unwrap().as_ref() {
to.show(to.area(), e);
}
for (index, dsl) in self.mode.view.iter().enumerate() {
if let Err(e) = self.understand(to, dsl) {
*self.error.write().unwrap() = Some(format!("view #{index}: {e}").into());
break;
}
}
}
}
impl HasClipsSize for App { fn clips_size (&self) -> &Measure<Tui> { &self.project.size_inner } }
impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } }
impl_default!(AppCommand: Self::Nop);
primitive!(u8: try_to_u8);
primitive!(u16: try_to_u16);
primitive!(usize: try_to_usize);
primitive!(isize: try_to_isize);

View file

@ -1,10 +0,0 @@
use crate::*;
/// Collection of view definitions.
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
pub(crate) fn load_view (views: &Views, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into());
Ok(())
}

2
tengri

@ -1 +1 @@
Subproject commit cf57f44933c45507e8de072e32c284f20f12ac7a Subproject commit 9dbf4fcab5f31a68e3d24c8f8f7fc866159e89f1