Compare commits

...

9 commits

27 changed files with 507 additions and 212 deletions

View file

@ -2,14 +2,6 @@
(info "A session grid.")
(view
(bsp/a :view-dialog
(bsp/s (fixed/y 1 :view-transport)
(bsp/n (fixed/y 1 :view-status)
(fill/xy (bsp/a
(fill/xy (align/e :view-pool))
:view-arranger))))))
(keys
(layer-if :focus-message "./keys_message.edn")
(layer-if :focus-device-add "./keys_device_add.edn")
@ -24,3 +16,17 @@
(layer "./keys_clock.edn")
(layer "./keys_arranger.edn")
(layer "./keys_global.edn"))
(view (bsp/a :view-dialog
(bsp/s
(fixed/y 8 (bsp/e
(fixed/x 20 (fill/y (align/n (bsp/s :view-status-v
(bsp/s :view-audio-ports-status :view-editor-status)))))
(fill/xy (align/n (bsp/s :view-arranger-track-names
(bsp/s :view-arranger-track-outputs
(bsp/s :view-arranger-track-devices :view-arranger-track-inputs)))))))
(bsp/w
(fixed/x 20 :view-pool)
(bsp/e
(fixed/x 20 (fill/xy (align/n :view-arranger-scene-names)))
(fill/xy (align/n :view-arranger-scene-clips)))))))

View file

@ -2,18 +2,6 @@
(info "A sequencer with built-in sampler.")
(view
(bsp/a :view-dialog
(bsp/s (fixed/y 1 :view-transport)
(bsp/n (fixed/y 1 :view-status)
(bsp/w :view-meters-output
(bsp/e :view-meters-input
(bsp/n :view-sample-info
(bsp/n (fixed/y 5 :view-sample-viewer)
(bsp/w :view-pool
(bsp/e :view-samples-keys
(fill/y :view-editor)))))))))))
(keys
(layer-if :focus-browser "./keys_browser.edn")
(layer-if :focus-pool-rename "./keys_rename.edn")
@ -22,3 +10,11 @@
(layer "./keys_editor.edn")
(layer "./keys_sampler.edn")
(layer "./keys_global.edn"))
(view (bsp/a :view-dialog (bsp/w :view-meters-output (bsp/e :view-meters-input
(bsp/n (fixed/y 6 (bsp/e :view-sample-status :view-sample-viewer))
(bsp/e
(fill/y (align/n (bsp/s :view-status-v (bsp/s
(bsp/s :view-midi-ports-status :view-audio-ports-status)
(bsp/n :view-editor-status :view-pool)))))
(bsp/e :view-samples-keys :view-editor)))))))

View file

@ -2,12 +2,12 @@
(info "A sampling soundboard.")
(keys
(layer "./keys_sampler.edn")
(layer "./keys_global.edn"))
(view
(bsp/a :view-dialog
(bsp/s (fixed/y 1 :view-transport)
(bsp/n (fixed/y 1 :view-status)
(fill/xy :view-samples-grid)))))
(keys
(layer "./keys_sampler.edn")
(layer "./keys_global.edn"))

View file

@ -2,14 +2,6 @@
(info "A MIDI sequencer.")
(view
(bsp/a :view-dialog
(bsp/s (fixed/y 1 :view-transport)
(bsp/n (fixed/y 1 :view-status)
(fill/xy (bsp/a
(fill/xy (align/e :view-pool))
:view-editor)))))
(keys
(layer-if :focus-browser "./keys_browser.edn")
(layer-if :mode-pool-rename "./keys_rename.edn")
@ -17,3 +9,11 @@
(layer "./keys_editor.edn")
(layer "./keys_clock.edn")
(layer "./keys_global.edn"))
(view
(bsp/a :view-dialog
(bsp/s (fixed/y 1 :view-transport)
(bsp/n (fixed/y 1 :view-status)
(fill/xy (bsp/a
(fill/xy (align/e :view-pool))
:view-editor)))))

View file

@ -2,8 +2,8 @@
(info "A JACK transport controller.")
(view :view-transport)
(keys
(layer "./keys_clock.edn")
(layer "./keys_global.edn"))
(view :view-transport)

View file

@ -1,9 +1,9 @@
(@esc dialog :dialog-none)
(@f1 dialog :dialog-help)
(@f6 dialog :dialog-save)
(@f8 dialog :dialog-options)
(@f9 dialog :dialog-load)
(@f10 dialog :dialog-quit)
(@esc dialog :dialog-none)
(@f1 dialog :dialog-help)
(@f6 dialog :dialog-save)
(@f8 dialog :dialog-options)
(@f9 dialog :dialog-load)
(@f10 dialog :dialog-quit)
(@u undo 1)
(@r redo 1)

View file

@ -2,7 +2,11 @@
(@down sampler select :sample-below)
(@left sampler select :sample-to-left)
(@right sampler select :sample-to-right)
(@r sampler record-toggle :sample-selected)
(@shift-R sampler record-cancel)
(@p sampler play-sample :sample-selected)
(@P sampler stop-sample :sample-selected)
(@shift-f6 dialog :dialog-import-sample)
(@shift-f9 dialog :dialog-export-sample)

View file

@ -67,7 +67,7 @@ impl App {
}
pub fn browser (&self) -> Option<&Browser> {
self.dialog.as_ref().and_then(|dialog|match dialog {
Dialog::Save(b) | Dialog::Load(b) => Some(b),
Dialog::Browser(_, b) => Some(b),
_ => None
})
}
@ -156,11 +156,20 @@ pub enum Dialog {
Menu(usize),
Device(usize),
Message(Message),
Save(Browser),
Load(Browser),
Browser(BrowserTarget, Browser),
Options,
}
#[derive(Clone, Debug)]
pub enum BrowserTarget {
SaveProject,
LoadProject,
ImportSample(Arc<RwLock<Option<Sample>>>),
ExportSample(Arc<RwLock<Option<Sample>>>),
ImportClip(Arc<RwLock<Option<MidiClip>>>),
ExportClip(Arc<RwLock<Option<MidiClip>>>),
}
/// Various possible messages
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum Message {
@ -235,10 +244,22 @@ impl App {
Some(Dialog::Menu(0))
}
fn dialog_save (&self) -> Option<Dialog> {
Some(Dialog::Save(Default::default()))
Some(Dialog::Browser(BrowserTarget::SaveProject, Browser::new(None).unwrap()))
}
fn dialog_load (&self) -> Option<Dialog> {
Some(Dialog::Load(Default::default()))
Some(Dialog::Browser(BrowserTarget::LoadProject, Browser::new(None).unwrap()))
}
fn dialog_import_clip (&self) -> Option<Dialog> {
Some(Dialog::Browser(BrowserTarget::ImportClip(Default::default()), Browser::new(None).unwrap()))
}
fn dialog_export_clip (&self) -> Option<Dialog> {
Some(Dialog::Browser(BrowserTarget::ExportClip(Default::default()), Browser::new(None).unwrap()))
}
fn dialog_import_sample (&self) -> Option<Dialog> {
Some(Dialog::Browser(BrowserTarget::ImportSample(Default::default()), Browser::new(None).unwrap()))
}
fn dialog_export_sample (&self) -> Option<Dialog> {
Some(Dialog::Browser(BrowserTarget::ExportSample(Default::default()), Browser::new(None).unwrap()))
}
fn dialog_options (&self) -> Option<Dialog> {
Some(Dialog::Options)

View file

@ -14,6 +14,36 @@ impl App {
pub fn view_nil (&self) -> impl Content<TuiOut> + use<'_> {
"nil"
}
pub fn view_status_v (&self) -> impl Content<TuiOut> + use<'_> {
self.update_clock();
let cache = self.view_cache.read().unwrap();
let theme = self.color;
let playing = self.clock().is_rolling();
Tui::bg(theme.darkest.rgb, Fixed::xy(20, 6, col!(
Fill::x(Align::w(Bsp::e(
Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
Either::new(false, // TODO
Thunk::new(move||Fixed::x(9, Either::new(playing,
Tui::fg(Rgb(0, 255, 0), " PLAYING "),
Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
),
Thunk::new(move||Fixed::x(5, Either::new(playing,
Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))
)
)
)),
Bsp::s(
FieldH(theme, "Beat", cache.beat.view.clone()),
FieldH(theme, "Time", cache.time.view.clone()),
),
))),
Fill::x(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))),
Fill::x(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))),
Fill::x(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))),
Fill::x(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))),
)))
}
pub fn view_status (&self) -> impl Content<TuiOut> + use<'_> {
self.update_clock();
let cache = self.view_cache.read().unwrap();
@ -27,16 +57,172 @@ impl App {
cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone())
}
pub fn view_editor (&self) -> impl Content<TuiOut> + use<'_> {
self.editor().map(|e|Bsp::n(Bsp::e(e.clip_status(), e.edit_status()), e))
self.editor()
}
pub fn view_editor_status (&self) -> impl Content<TuiOut> + use<'_> {
self.editor().map(|e|Bsp::s(e.clip_status(), e.edit_status()))
}
pub fn view_midi_ports_status (&self) -> impl Content<TuiOut> + use<'_> {
self.project.get_track().map(|track|Bsp::s(
Fixed::xy(20, 1 + track.sequencer.midi_ins.len() as u16, FieldV(self.color,
format!("MIDI ins: "),
Map::south(1, ||track.sequencer.midi_ins.iter(),
|port, index|Fill::x(Align::w(format!(" {index} {}", port.name())))))),
Fixed::xy(20, 1 + track.sequencer.midi_outs.len() as u16, FieldV(
self.color,
format!("MIDI outs: "),
Map::south(1, ||track.sequencer.midi_outs.iter(),
|port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))
))
}
pub fn view_audio_ports_status (&self) -> impl Content<TuiOut> + use<'_> {
self.project.get_track().map(|track|Bsp::s(
track.devices.get(0).map(|device|
Fixed::xy(20, 1 + device.audio_ins().len() as u16, FieldV(self.color,
format!("Audio ins: "),
Map::south(1, ||device.audio_ins().iter(),
|port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))),
track.devices.last().map(|device|
Fixed::xy(20, 1 + device.audio_outs().len() as u16, FieldV(self.color,
format!("Audio outs:"),
Map::south(1, ||device.audio_outs().iter(),
|port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))),
))
}
pub fn view_arranger (&self) -> impl Content<TuiOut> + use<'_> {
ArrangerView::new(&self.project, self.editor.as_ref())
}
pub fn view_arranger_scene_names <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
let h = self.project.scenes.len() as u16 * 2;
let bg = self.color.darker.rgb;
Fixed::y(h, Tui::bg(bg, Align::w(Fill::x(Map::new(
||self.project.scenes.iter().skip(self.project.scene_scroll),
move|scene: &Scene, index|
Push::y(index as u16 * 2u16, Fixed::xy(20, 2,
Tui::bg(scene.color.dark.rgb, Align::nw(Bsp::e(
format!(" {index:2} "),
Tui::fg(Rgb(255, 255, 255),
Tui::bold(true, format!("{}", scene.name)))))))))))))
}
pub fn view_arranger_scene_clips <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
let h = self.project.scenes.len() as u16 * 2;
let bg = self.color.darker.rgb;
Fixed::y(h, Tui::bg(bg, Align::w(Fill::x(Map::new(
||self.project.scenes.iter().skip(self.project.scene_scroll),
move|scene: &'a Scene, index1|
Push::y(index1 as u16 * 2u16, Fixed::xy(20, 2,
Map::new(
move||scene.clips.iter().skip(self.project.track_scroll),
move|clip: &'a Option<Arc<RwLock<MidiClip>>>, index2|{
let (theme, text) = if let Some(clip) = clip {
let clip = clip.read().unwrap();
(clip.color, clip.name.clone())
} else {
(scene.color, Default::default())
};
Push::x(index2 as u16 * 14, Tui::bg(theme.dark.rgb, Bsp::e(
format!(" {index1:2} {index2:2} "),
Tui::fg(Rgb(255, 255, 255),
Tui::bold(true, format!("{}", text))))))
}))))))))
}
pub fn view_arranger_track_names (&self) -> impl Content<TuiOut> + use<'_> {
let mut max_outputs = 0u16;
for track in self.project.tracks.iter() {
max_outputs = max_outputs.max(track.sequencer.midi_outs.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(self.color.darkest.rgb,
col!(Tui::bold(true, "[t]rack"), "[T] Add"))),
Align::w(Fixed::y(max_outputs + 1, Tui::bg(self.color.darker.rgb, Align::w(Fill::x(Map::new(
||self.project.tracks_with_sizes(&self.project.selection, None)
.skip(self.project.track_scroll),
move|(index, track, x1, x2): (usize, &Track, usize, usize), _|
Push::x(index as u16 * 14, Fixed::xy(track.width as u16, max_outputs + 1,
Tui::bg(track.color.dark.rgb, Align::nw(Bsp::s(
Tui::fg(Rgb(255, 255, 255), Tui::bold(true, format!("{}", track.name))),
format!("{index} {x1} {x2}")))))))))))))
}
pub fn view_arranger_track_outputs <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
let mut max_outputs = 0u16;
for track in self.project.tracks.iter() {
max_outputs = max_outputs.max(track.sequencer.midi_outs.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(self.color.darkest.rgb,
col!(Tui::bold(true, "[o]utput"), "[O] Add"))),
Align::w(Fixed::y(max_outputs + 1, Tui::bg(self.color.darker.rgb, Align::w(Fill::x(Map::new(
||self.project.tracks_with_sizes(&self.project.selection, None)
.skip(self.project.track_scroll),
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|
Push::x(x2 as u16, Tui::bg(track.color.dark.rgb, Fixed::xy(
track.width as u16,
max_outputs + 1,
Align::nw(Bsp::s(
format!("[mut] [sol]"),
Map::south(1, ||track.sequencer.midi_outs.iter(),
|port, index|Tui::fg(Rgb(255, 255, 255),
format!("{index}: {}", port.name())))))))))))))))
}
pub fn view_arranger_track_inputs <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
let mut max_inputs = 0u16;
for track in self.project.tracks.iter() {
max_inputs = max_inputs.max(track.sequencer.midi_ins.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(self.color.darkest.rgb,
col!(Tui::bold(true, "[i]nputs"), "[I] Add"))),
Fill::x(Align::w(Fixed::y(max_inputs + 1, Tui::bg(self.color.darker.rgb, Align::w(Fill::x(Map::new(
||self.project.tracks_with_sizes(&self.project.selection, None)
.skip(self.project.track_scroll),
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|
Push::x(x2 as u16, Fixed::xy(track.width as u16, max_inputs + 1,
Tui::bg(track.color.dark.rgb, Align::nw(Bsp::s(
format!("[rec] [mon]"),
Map::south(1, ||track.sequencer.midi_ins.iter(),
|port, index|Tui::fg(Rgb(255, 255, 255),
format!("{index}: {}", port.name()))))))))))))))))
}
pub fn view_arranger_track_devices <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
let mut max_devices = 2u16;
for track in self.project.tracks.iter() {
max_devices = max_devices.max(track.devices.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(self.color.darkest.rgb,
col!(Tui::bold(true, "[d]evice"), "[D] Add"))),
Fixed::y(max_devices, Tui::bg(self.color.darker.rgb, Align::w(Fill::x(Map::new(
||self.project.tracks_with_sizes(&self.project.selection, None)
.skip(self.project.track_scroll),
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|
Push::x(x2 as u16, Fixed::xy(track.width as u16, max_devices + 1,
Tui::bg(track.color.dark.rgb, Align::nw(Map::south(1, move||0..max_devices,
|_, index|format!("{index}: {}", "--------"))))))))))))
}
pub fn view_arranger_track_scenes <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
let mut max_devices = 0u16;
for track in self.project.tracks.iter() {
max_devices = max_devices.max(track.devices.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(self.color.darkest.rgb,
col!(Tui::bold(true, "Devices"), "[d] Select", "[D] Add"))),
Fixed::y(max_devices + 1, Tui::bg(self.color.darker.rgb, Align::w(Fill::x(Map::new(
||self.project.tracks_with_sizes(&self.project.selection, None)
.skip(self.project.track_scroll),
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|
Push::x(x2 as u16, Fixed::xy(track.width as u16, max_devices + 1,
Align::nw(Map::south(1, ||track.devices.iter(),
|device, index|format!("{index}: {}", device.name())))))))))))
}
pub fn view_pool (&self) -> impl Content<TuiOut> + use<'_> {
PoolView(&self.project.pool)
Fixed::x(20, Bsp::s(
Fill::x(Align::w(FieldH(self.color, "Clip pool:", ""))),
Fill::y(Align::n(Tui::bg(Rgb(0, 0, 0), PoolView(&self.project.pool)))),
))
}
pub fn view_samples_keys (&self) -> impl Content<TuiOut> + use<'_> {
self.project.sampler().map(|s|s.view_list(false, self.editor().unwrap()))
self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap()))
}
pub fn view_samples_grid (&self) -> impl Content<TuiOut> + use<'_> {
self.project.sampler().map(|s|s.view_grid())
@ -47,6 +233,9 @@ impl App {
pub fn view_sample_info (&self) -> impl Content<TuiOut> + use<'_> {
self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos()))
}
pub fn view_sample_status (&self) -> impl Content<TuiOut> + use<'_> {
self.project.sampler().map(|s|s.view_sample_status(self.editor().unwrap().get_note_pos()))
}
pub fn view_meters_input (&self) -> impl Content<TuiOut> + use<'_> {
self.project.sampler().map(|s|s.view_meters_input())
}
@ -62,10 +251,8 @@ impl App {
self.view_dialog_menu().boxed(),
Dialog::Help(offset) =>
self.view_dialog_help(*offset).boxed(),
Dialog::Save(browser) =>
self.view_dialog_save().boxed(),
Dialog::Load(browser) =>
self.view_dialog_load().boxed(),
Dialog::Browser(target, browser) =>
self.view_dialog_browser(target, browser).boxed(),
Dialog::Options =>
self.view_dialog_options().boxed(),
Dialog::Device(index) =>
@ -108,21 +295,45 @@ impl App {
pub fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content<TuiOut> + use<'a> {
Bsp::s(message, Bsp::s("", "[ OK ]"))
}
pub fn view_dialog_save <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
pub fn view_dialog_browser <'a> (&'a self, target: &BrowserTarget, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
Bsp::s(
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
Tui::bold(true, " Save project: "),
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
Padding::xy(3, 1, Fill::x(Align::w(FieldV(
self.color,
match target {
BrowserTarget::SaveProject => "Save project:",
BrowserTarget::LoadProject => "Load project:",
BrowserTarget::ImportSample(_) => "Import sample:",
BrowserTarget::ExportSample(_) => "Export sample:",
BrowserTarget::ImportClip(_) => "Import clip:",
BrowserTarget::ExportClip(_) => "Export clip:",
},
Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))),
Outer(true, Style::default().fg(Tui::g(96)))
.enclose(Fill::xy("todo file browser")))
.enclose(Fill::xy(browser)))
}
pub fn view_dialog_load <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
pub fn view_dialog_load <'a> (&'a self, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
Bsp::s(
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
Tui::bold(true, " Load project: "),
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
Outer(true, Style::default().fg(Tui::g(96)))
.enclose(Fill::xy("todo file browser")))
.enclose(Fill::xy(browser)))
}
pub fn view_dialog_export <'a> (&'a self, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
Bsp::s(
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
Tui::bold(true, " Export: "),
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
Outer(true, Style::default().fg(Tui::g(96)))
.enclose(Fill::xy(browser)))
}
pub fn view_dialog_import <'a> (&'a self, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
Bsp::s(
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
Tui::bold(true, " Import: "),
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
Outer(true, Style::default().fg(Tui::g(96)))
.enclose(Fill::xy(browser)))
}
pub fn view_dialog_options <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
"TODO"

View file

@ -60,7 +60,7 @@ pub enum LaunchMode {
/// Number of tracks
#[arg(short = 'x', long, default_value_t = 4)] tracks: usize,
/// Width of tracks
#[arg(short = 'w', long, default_value_t = 12)] track_width: usize,
#[arg(short = 'w', long, default_value_t = 14)] track_width: usize,
},
/// TODO: A MIDI-controlled audio mixer
Mixer,

View file

@ -3,7 +3,7 @@ use crate::*;
/// Define a type alias for iterators of sized items (columns).
macro_rules! def_sizes_iter {
($Type:ident => $($Item:ty),+) => {
pub(crate) trait $Type<'a> =
pub trait $Type<'a> =
Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a;
}
}

View file

@ -64,27 +64,12 @@ impl Arrangement {
}
/// Width available to display tracks.
pub fn w_tracks_area (&self, is_editing: bool) -> u16 {
self.w().saturating_sub(2 * self.w_sidebar(is_editing))
self.w().saturating_sub(self.w_sidebar(is_editing))
}
/// Height of display
pub fn h (&self) -> u16 {
self.size.h() as u16
}
/// Height available to display track headers.
pub fn h_tracks_area (&self) -> u16 {
5 // FIXME
//self.h().saturating_sub(self.h_inputs() + self.h_outputs())
}
/// Height available to display tracks.
pub fn h_scenes_area (&self) -> u16 {
//15
self.h().saturating_sub(
self.h_inputs() +
self.h_outputs() +
self.h_devices() +
13 // FIXME
)
}
/// Height taken by all scenes.
pub fn h_scenes (&self, is_editing: bool) -> u16 {
self.scenes_with_sizes(
@ -111,27 +96,27 @@ impl Arrangement {
//1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
}
/// Get the active track
fn get_track (&self) -> Option<&Track> {
pub fn get_track (&self) -> Option<&Track> {
let index = self.selection().track()?;
Has::<Vec<Track>>::get(self).get(index)
}
/// Get a mutable reference to the active track
fn get_track_mut (&mut self) -> Option<&mut Track> {
pub fn get_track_mut (&mut self) -> Option<&mut Track> {
let index = self.selection().track()?;
Has::<Vec<Track>>::get_mut(self).get_mut(index)
}
/// Get the active scene
fn get_scene (&self) -> Option<&Scene> {
pub fn get_scene (&self) -> Option<&Scene> {
let index = self.selection().scene()?;
Has::<Vec<Scene>>::get(self).get(index)
}
/// Get a mutable reference to the active scene
fn get_scene_mut (&mut self) -> Option<&mut Scene> {
pub fn get_scene_mut (&mut self) -> Option<&mut Scene> {
let index = self.selection().scene()?;
Has::<Vec<Scene>>::get_mut(self).get_mut(index)
}
/// Get the active clip
fn get_clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
pub fn get_clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
self.get_scene()?.clips.get(self.selection().track()?)?.clone()
}
/// Put a clip in a slot

View file

@ -3,7 +3,7 @@ use crate::*;
impl<'a> ArrangerView<'a> {
pub(crate) fn input_routes (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(self.inputs_height)
Tryptich::top(self.arrangement.h_inputs())
.left(self.width_side,
io_ports(Tui::g(224), Tui::g(32), ||self.arrangement.midi_ins_with_sizes()))
.middle(self.width_mid,
@ -127,7 +127,7 @@ impl<'a> ArrangerView<'a> {
}
pub(crate) fn output_conns (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(self.outputs_height)
Tryptich::top(self.arrangement.h_outputs())
.left(self.width_side, io_ports(
Tui::g(224), Tui::g(32), ||self.arrangement.midi_outs_with_sizes()))
.middle(self.width_mid, per_track_top(

View file

@ -2,26 +2,19 @@ use crate::*;
pub struct ArrangerView<'a> {
pub arrangement: &'a Arrangement,
pub is_editing: bool,
pub width: u16,
pub width_mid: u16,
pub width_side: u16,
pub inputs_height: u16,
pub outputs_height: u16,
pub scene_last: usize,
pub scene_scroll: Fill<Fixed<u16, ScrollbarV>>,
pub scene_selected: Option<usize>,
/// Height available to display scene/track content.
pub scenes_height: u16,
pub track_scroll: Fill<Fixed<u16, ScrollbarH>>,
pub track_selected: Option<usize>,
/// Height available to display track headers.
pub tracks_height: u16,
pub show_debug_info: bool,
}
impl<'a> ArrangerView<'a> {
@ -30,20 +23,15 @@ impl<'a> ArrangerView<'a> {
editor: Option<&'a MidiEditor>
) -> Self {
let is_editing = editor.is_some();
let h_tracks_area = arrangement.h_tracks_area();
let h_scenes_area = arrangement.h_scenes_area();
let h_tracks_area = 5;
let h_scenes_area = (arrangement.height() as u16).saturating_sub(20);
let h_scenes = arrangement.h_scenes(is_editing);
Self {
arrangement,
is_editing,
width: arrangement.w(),
width_mid: arrangement.w_tracks_area(is_editing),
width_side: arrangement.w_sidebar(is_editing),
inputs_height: arrangement.h_inputs(),
outputs_height: arrangement.h_outputs(),
width: arrangement.w_tracks_area(is_editing),
width_mid: arrangement.w_tracks_area(is_editing).saturating_sub(20),
width_side: 20,
scenes_height: h_scenes_area,
scene_selected: arrangement.selection().scene(),
scene_last: arrangement.scenes.len().saturating_sub(1),
@ -52,7 +40,6 @@ impl<'a> ArrangerView<'a> {
length: h_scenes_area as usize,
total: h_scenes as usize,
})),
tracks_height: h_tracks_area,
track_selected: arrangement.selection().track(),
track_scroll: Fill::x(Fixed::y(1, ScrollbarH {
@ -60,8 +47,6 @@ impl<'a> ArrangerView<'a> {
length: h_tracks_area as usize,
total: h_scenes as usize,
})),
show_debug_info: false
}
}
}
@ -75,7 +60,7 @@ impl<'a> Content<TuiOut> for ArrangerView<'a> {
let bg = |x|Tui::bg(Reset, x);
//let track_scroll = |x|Bsp::s(&self.track_scroll, x);
//let scene_scroll = |x|Bsp::e(&self.scene_scroll, x);
outs(tracks(devices(ins(bg(self.scenes(&None))))))
self.arrangement.size.of(outs(tracks(devices(ins(bg(self.scenes(&None)))))))
}
}
@ -148,7 +133,6 @@ impl<'a> ArrangerView<'a> {
scenes_height, scene_last, scene_selected,
track_selected, is_editing, ..
} = self;
let selection = Has::<Selection>::get(self.arrangement);
let selected_track = selection.track();
let selected_scene = selection.scene();

View file

@ -1,19 +1,33 @@
use crate::*;
content!(TuiOut: |self: Browser| /*Stack::down(|add|{
let mut i = 0;
for (_, name) in self.dirs.iter() {
if i >= self.scroll {
add(&Tui::bold(i == self.index, name.as_str()))?;
content!(TuiOut: |self: Browser|Map::south(1, ||EntriesIterator {
offset: 0,
index: 0,
length: self.dirs.len() + self.files.len(),
browser: self,
}, |entry, index|Fill::x(Align::w(entry))));
struct EntriesIterator<'a> {
browser: &'a Browser,
offset: usize,
length: usize,
index: usize,
}
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
}
i += 1;
}
for (_, name) in self.files.iter() {
if i >= self.scroll {
add(&Tui::bold(i == self.index, name.as_str()))?;
}
i += 1;
}
add(&format!("{}/{i}", self.index))?;
Ok(())
})*/"todo");
}

View file

@ -37,6 +37,30 @@ impl Device {
_ => todo!(),
}
}
pub fn midi_ins (&self) -> &[JackMidiIn] {
match self {
//Self::Sampler(Sampler { midi_in, .. }) => &[midi_in],
_ => todo!()
}
}
pub fn midi_outs (&self) -> &[JackMidiOut] {
match self {
Self::Sampler(_) => &[],
_ => todo!()
}
}
pub fn audio_ins (&self) -> &[JackAudioIn] {
match self {
Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(),
_ => todo!()
}
}
pub fn audio_outs (&self) -> &[JackAudioOut] {
match self {
Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(),
_ => todo!()
}
}
}
pub struct DeviceAudio<'a>(pub &'a mut Device);

View file

@ -95,34 +95,36 @@ use crate::*;
#[tengri_proc::command(MidiEditor)] impl MidiEditCommand {
fn append_note (editor: &mut MidiEditor, advance: bool) -> Perhaps<Self> {
editor.put_note(advance);
editor.redraw();
Ok(None)
}
fn delete_note (_editor: &mut MidiEditor) -> Perhaps<Self> {
fn delete_note (editor: &mut MidiEditor) -> Perhaps<Self> {
editor.redraw();
todo!()
}
fn set_note_pos (editor: &mut MidiEditor, pos: usize) -> Perhaps<Self> {
editor.set_note_pos(pos.min(127));
editor.redraw();
Ok(None)
}
fn set_note_len (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
//let note_len = editor.get_note_len();
//let time_zoom = editor.get_time_zoom();
editor.set_note_len(value);
//if note_len / time_zoom != x / time_zoom {
editor.redraw();
//}
editor.redraw();
Ok(None)
}
fn set_note_scroll (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
editor.set_note_lo(value.min(127));
editor.redraw();
Ok(None)
}
fn set_time_pos (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
editor.set_time_pos(value);
editor.redraw();
Ok(None)
}
fn set_time_scroll (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
editor.set_time_start(value);
editor.redraw();
Ok(None)
}
fn set_time_zoom (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
@ -132,10 +134,12 @@ use crate::*;
}
fn set_time_lock (editor: &mut MidiEditor, value: bool) -> Perhaps<Self> {
editor.set_time_lock(value);
editor.redraw();
Ok(None)
}
fn show (editor: &mut MidiEditor, clip: Option<Arc<RwLock<MidiClip>>>) -> Perhaps<Self> {
editor.set_clip(clip.as_ref());
editor.redraw();
Ok(None)
}
// TODO: 1-9 seek markers that by default start every 8th of the clip

View file

@ -12,10 +12,11 @@ impl MidiEditor {
let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
(clip.color, clip.name.clone(), clip.length, clip.looped)
} else { (ItemTheme::G[64], String::new().into(), 0, false) };
Bsp::e(
FieldH(color, "Edit", format!("{name} ({length})")),
FieldH(color, "Loop", looped.to_string())
)
Fixed::x(20, col!(
Fill::x(Align::w(FieldV(color, "Clip ", format!("{name}")))),
Fill::x(Align::w(FieldH(color, "Length", format!("{length}")))),
Fill::x(Align::w(FieldH(color, "Loop ", looped.to_string()))),
))
}
pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
@ -29,10 +30,11 @@ impl MidiEditor {
let note_name = format!("{:4}", Note::pitch_to_name(note_pos));
let note_pos = format!("{:>3}", note_pos);
let note_len = format!("{:>4}", self.get_note_len());
Bsp::e(
FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")),
FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")),
)
Fixed::x(20, col!(
Fill::x(Align::w(FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos}")))),
Fill::x(Align::w(FieldH(color, "Lock", format!("{time_lock}")))),
Fill::x(Align::w(FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")))),
))
}
}

View file

@ -64,11 +64,22 @@ impl PianoHorizontal {
/// Draw the piano roll background.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize) {
fn draw_bg (
buf: &mut BigBuffer,
clip: &MidiClip,
zoom: usize,
note_len: usize,
note_point: usize,
time_point: usize,
) {
for (y, note) in (0..=127).rev().enumerate() {
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
let cell = buf.get_mut(x, y).unwrap();
cell.set_bg(clip.color.darkest.rgb);
if note == (127-note_point) || time == time_point {
cell.set_bg(Rgb(0,0,0));
} else {
cell.set_bg(clip.color.darkest.rgb);
}
if time % 384 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('│');
@ -267,10 +278,10 @@ impl MidiViewer for PianoHorizontal {
let clip = clip.read().unwrap();
let buf_size = self.buffer_size(&clip);
let mut buffer = BigBuffer::from(buf_size);
let note_len = self.get_note_len();
let time_zoom = self.get_time_zoom();
self.time_len().set(clip.length);
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len);
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,
self.get_note_len(), self.get_note_pos(), self.get_time_pos());
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
buffer
} else {

View file

@ -1,6 +1,7 @@
#![feature(let_chains)]
#![feature(trait_alias)]
#![feature(if_let_guard)]
#![feature(impl_trait_in_assoc_type)]
pub(crate) use std::cmp::Ord;
pub(crate) use std::fmt::{Debug, Formatter};

View file

@ -53,7 +53,7 @@ pub fn to_rms (samples: &[f32]) -> f32 {
(sum / samples.len() as f32).sqrt()
}
pub (crate) fn view_meter <'a> (label: &'a str, value: f32) -> impl Content<TuiOut> + 'a {
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Content<TuiOut> + 'a {
col!(
FieldH(ItemTheme::G[128], label, format!("{:>+9.3}", value)),
Fixed::xy(if value >= 0.0 { 13 }
@ -74,7 +74,7 @@ pub (crate) fn view_meter <'a> (label: &'a str, value: f32) -> impl Content<TuiO
else { Green }, ())))
}
pub(crate) fn view_meters (values: &[f32;2]) -> impl Content<TuiOut> + use<'_> {
pub fn view_meters (values: &[f32;2]) -> impl Content<TuiOut> + use<'_> {
let left = format!("L/{:>+9.3}", values[0]);
let right = format!("R/{:>+9.3}", values[1]);
Bsp::s(left, right)

View file

@ -8,7 +8,7 @@ content!(TuiOut: |self: PoolView<'a>| {
//let on_bg = |x|x;//Bsp::b(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;
Fixed::x(20, Fill::y(Align::c(Map::new(
Fixed::x(20, Fill::y(Align::n(Map::new(
||pool.clips().clone().into_iter(),
move|clip: Arc<RwLock<MidiClip>>, i: usize|{
let item_height = 1;

View file

@ -7,20 +7,18 @@ impl Sampler {
/// Create [Voice]s from [Sample]s in response to MIDI input.
pub fn process_midi_in (&mut self, scope: &ProcessScope) {
let Sampler { midi_in, mapped, voices, .. } = self;
if let Some(ref midi_in) = midi_in {
for RawMidi { time, bytes } in midi_in.port().iter(scope) {
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
match message {
MidiMessage::NoteOn { ref key, ref vel } => {
if let Some(ref sample) = mapped[key.as_int() as usize] {
voices.write().unwrap().push(Sample::play(sample, time as usize, vel));
}
},
MidiMessage::Controller { controller: _, value: _ } => {
// TODO
for RawMidi { time, bytes } in midi_in.port().iter(scope) {
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
match message {
MidiMessage::NoteOn { ref key, ref vel } => {
if let Some(ref sample) = mapped[key.as_int() as usize] {
voices.write().unwrap().push(Sample::play(sample, time as usize, vel));
}
_ => {}
},
MidiMessage::Controller { controller: _, value: _ } => {
// TODO
}
_ => {}
}
}
}

View file

@ -22,7 +22,7 @@ pub struct Sampler {
/// Sample currently being edited.
pub editing: Option<Arc<RwLock<Sample>>>,
/// MIDI input port. Triggers sample playback.
pub midi_in: Option<JackMidiIn>,
pub midi_in: JackMidiIn,
/// Collection of currently playing instances of samples.
pub voices: Arc<RwLock<Vec<Voice>>>,
/// Audio output ports. Voices get played here.
@ -47,15 +47,28 @@ pub struct Sampler {
pub cursor: (AtomicUsize, AtomicUsize),
}
impl Default for Sampler {
fn default () -> Self {
Self {
midi_in: None,
audio_ins: vec![],
impl Sampler {
pub fn new (
jack: &Jack,
name: impl AsRef<str>,
midi_from: &[PortConnect],
audio_from: &[&[PortConnect];2],
audio_to: &[&[PortConnect];2],
) -> Usually<Self> {
let name = name.as_ref();
Ok(Self {
name: name.into(),
midi_in: JackMidiIn::new(jack, format!("M/{name}"), midi_from)?,
audio_ins: vec![
JackAudioIn::new(jack, &format!("L/{name}"), audio_from[0])?,
JackAudioIn::new(jack, &format!("R/{name}"), audio_from[1])?,
],
audio_outs: vec![
JackAudioOut::new(jack, &format!("{name}/L"), audio_to[0])?,
JackAudioOut::new(jack, &format!("{name}/R"), audio_to[1])?,
],
input_meters: vec![0.0;2],
output_meters: vec![0.0;2],
audio_outs: vec![],
name: "tek_sampler".into(),
mapped: [const { None };128],
unmapped: vec![],
voices: Arc::new(RwLock::new(vec![])),
@ -71,31 +84,6 @@ impl Default for Sampler {
color: Default::default(),
mixing_mode: Default::default(),
metering_mode: Default::default(),
}
}
}
impl Sampler {
pub fn new (
jack: &Jack,
name: impl AsRef<str>,
midi_from: &[PortConnect],
audio_from: &[&[PortConnect];2],
audio_to: &[&[PortConnect];2],
) -> Usually<Self> {
let name = name.as_ref();
Ok(Self {
name: name.into(),
midi_in: Some(JackMidiIn::new(jack, format!("M/{name}"), midi_from)?),
audio_ins: vec![
JackAudioIn::new(jack, &format!("L/{name}"), audio_from[0])?,
JackAudioIn::new(jack, &format!("R/{name}"), audio_from[1])?,
],
audio_outs: vec![
JackAudioOut::new(jack, &format!("{name}/L"), audio_to[0])?,
JackAudioOut::new(jack, &format!("{name}/R"), audio_to[1])?,
],
..Default::default()
})
}
/// Value of cursor
@ -146,11 +134,20 @@ pub struct Sample {
pub channels: Vec<Vec<f32>>,
pub rate: Option<usize>,
pub gain: f32,
pub color: ItemTheme,
}
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 }
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 {

View file

@ -59,17 +59,25 @@ impl Sampler {
let note_lo = editor.get_note_lo();
let note_pt = editor.get_note_pos();
let note_hi = editor.get_note_hi();
Fixed::x(12, Map::south(
Fixed::x(if compact { 4 } else { 12 }, Map::south(
1,
move||(note_lo..=note_hi).rev(),
move|note, _index| {
//let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a))));
let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset };
let mut fg = Tui::g(160);
let mapped: &Option<Arc<RwLock<Sample>>> = &self.mapped[note];
if mapped.is_some() {
fg = Tui::g(224);
bg = Color::Rgb(0, if note == note_pt { 96 } else { 64 }, 0);
if let Some(mapped) = &self.mapped[note] {
let sample = mapped.read().unwrap();
fg = if note == note_pt {
sample.color.lightest.rgb
} else {
Tui::g(224)
};
bg = if note == note_pt {
sample.color.light.rgb
} else {
sample.color.base.rgb
};
}
if let Some((index, _)) = self.recording {
if note == index {
@ -110,6 +118,16 @@ impl Sampler {
})))
}
pub fn view_sample_status (&self, note_pt: usize) -> impl Content<TuiOut> + use<'_> {
Fixed::x(20, draw_info_v(if let Some((_, sample)) = &self.recording {
Some(sample)
} else if let Some(sample) = &self.mapped[note_pt] {
Some(sample)
} else {
None
}))
}
pub fn view_status (&self, index: usize) -> impl Content<TuiOut> {
draw_status(self.mapped[index].as_ref())
}
@ -189,11 +207,11 @@ fn draw_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> +
.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()
);
//ctx.print(
//(width - text.len() as u16) as f64 / 2.0,
//height as f64 / 2.0,
//text.red()
//);
})
.render(area, &mut to.buffer);
}
@ -203,18 +221,37 @@ fn draw_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> +
fn draw_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> {
When(sample.is_some(), Thunk::new(move||{
let sample = sample.unwrap().read().unwrap();
let theme = ItemTheme::G[96];
let theme = sample.color;
row!(
FieldH(theme, "Name", format!("{:<10}", sample.name.clone())),
FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())),
FieldH(theme, "Start", format!("{:<8}", sample.start)),
FieldH(theme, "End", format!("{:<8}", sample.end)),
FieldH(theme, "Transpose", " 0 "),
FieldH(theme, "Gain", format!("{}", sample.gain)),
FieldH(theme, "Name", format!("{:<10}", sample.name.clone())),
FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())),
FieldH(theme, "Start", format!("{:<8}", sample.start)),
FieldH(theme, "End", format!("{:<8}", sample.end)),
FieldH(theme, "Trans", "0"),
FieldH(theme, "Gain", format!("{}", sample.gain)),
)
}))
}
fn draw_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> {
Either(sample.is_some(), Thunk::new(move||{
let sample = sample.unwrap().read().unwrap();
let theme = sample.color;
Fixed::x(20, col!(
Fill::x(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))),
Fill::x(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))),
Fill::x(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))),
Fill::x(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))),
Fill::x(Align::w(FieldH(theme, "Trans ", "0"))),
Fill::x(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))),
))
}), Thunk::new(move||Tui::fg(Red, col!(
Tui::bold(true, "× No sample."),
"[r] record",
"[Shift-F9] import",
))))
}
fn draw_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> {
Tui::bold(true, Tui::fg(Tui::g(224), sample
.map(|sample|{

View file

@ -36,16 +36,16 @@ impl<E: Engine> Handle<E> for JackDevice<E> {
}
impl<E: Engine> Ports for JackDevice<E> {
fn audio_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
fn audio_ins (&self) -> Usually<Vec<&Port<Unowned>>> {
Ok(self.ports.audio_ins.values().collect())
}
fn audio_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
fn audio_outs (&self) -> Usually<Vec<&Port<Unowned>>> {
Ok(self.ports.audio_outs.values().collect())
}
fn midi_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
fn midi_ins (&self) -> Usually<Vec<&Port<Unowned>>> {
Ok(self.ports.midi_ins.values().collect())
}
fn midi_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
fn midi_outs (&self) -> Usually<Vec<&Port<Unowned>>> {
Ok(self.ports.midi_outs.values().collect())
}
}

2
deps/tengri vendored

@ -1 +1 @@
Subproject commit 8bfd1a23a1f880a1d2fb104a158fc51f244acd6e
Subproject commit c954965ae125136286291e5f0d4532edf98f46ad