Compare commits

...

2 commits

Author SHA1 Message Date
b0ef0cfd21 poke with a stick
Some checks are pending
/ build (push) Waiting to run
2025-05-12 21:37:45 +03:00
57102d7e6b stub save/load/options 2025-05-12 20:58:38 +03:00
11 changed files with 251 additions and 203 deletions

View file

@ -6,7 +6,7 @@
(@shift-O output add) (@shift-O output add)
(@shift-S scene add) (@shift-S scene add)
(@shift-T track add) (@shift-T track add)
(@shift-Z device picker) (@shift-D device picker)
(@up select :select-scene-prev) (@up select :select-scene-prev)
(@down select :select-scene-next) (@down select :select-scene-next)

View file

@ -1,4 +1,4 @@
(@esc device select-cancel) (@esc device cancel)
(@up device pick :device-kind-prev) (@up device pick :device-kind-prev)
(@down device pick :device-kind-next) (@down device pick :device-kind-next)
(@enter device add :device-kind) (@enter device add :device-kind)

View file

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

View file

@ -1 +1,3 @@
(@r sampler record/toggle :sample) (@r sampler record/toggle :sample)
(@tab focus-next)
(@shift-tab focus-prev)

View file

@ -57,6 +57,21 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm
fn focus_pool_length (&self) -> bool { fn focus_pool_length (&self) -> bool {
matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..))) matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..)))
} }
fn dialog_help (&self) -> Dialog {
Dialog::Help
}
fn dialog_menu (&self) -> Dialog {
Dialog::Menu
}
fn dialog_save (&self) -> Dialog {
Dialog::Save
}
fn dialog_load (&self) -> Dialog {
Dialog::Load
}
fn dialog_options (&self) -> Dialog {
Dialog::Options
}
fn editor_pitch (&self) -> Option<u7> { fn editor_pitch (&self) -> Option<u7> {
Some((self.editor().map(|e|e.get_note_pos()).unwrap() as u8).into()) Some((self.editor().map(|e|e.get_note_pos()).unwrap() as u8).into())
} }
@ -203,15 +218,11 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm
} }
#[tengri_proc::command(App)] impl AppCommand { #[tengri_proc::command(App)] impl AppCommand {
fn toggle_help (app: &mut App) -> Perhaps<Self> { fn toggle_dialog (app: &mut App, dialog: Dialog) -> Perhaps<Self> {
app.toggle_dialog(Some(Dialog::Help)); app.toggle_dialog(Some(dialog));
Ok(None) Ok(None)
} }
fn toggle_menu (app: &mut App) -> Perhaps<Self> { fn toggle_editor (app: &mut App, value: bool) -> Perhaps<Self> {
app.toggle_dialog(Some(Dialog::Menu));
Ok(None)
}
fn toggle_edit (app: &mut App, value: bool) -> Perhaps<Self> {
app.toggle_editor(Some(value)); app.toggle_editor(Some(value));
Ok(None) Ok(None)
} }
@ -346,6 +357,10 @@ impl<'state> Context<'state, SamplerCommand> for App {
} }
#[tengri_proc::command(App)] impl DeviceCommand { #[tengri_proc::command(App)] impl DeviceCommand {
fn cancel (app: &mut App) -> Perhaps<Self> {
app.dialog = None;
Ok(None)
}
fn picker (app: &mut App) -> Perhaps<Self> { fn picker (app: &mut App) -> Perhaps<Self> {
app.device_picker_show(); app.device_picker_show();
Ok(None) Ok(None)

View file

@ -19,7 +19,10 @@ pub enum Dialog {
Help, Help,
Menu, Menu,
Device(usize), Device(usize),
Message(Message) Message(Message),
Save,
Load,
Options,
} }
/// Various possible messages /// Various possible messages

View file

@ -2,6 +2,8 @@ use crate::*;
pub(crate) use std::fmt::Write; pub(crate) use std::fmt::Write;
pub(crate) use ::tengri::tui::ratatui::prelude::Position; pub(crate) use ::tengri::tui::ratatui::prelude::Position;
mod view_output; pub use self::view_output::*;
#[tengri_proc::view(TuiOut)] #[tengri_proc::view(TuiOut)]
impl App { impl App {
pub fn view_nil (&self) -> impl Content<TuiOut> + use<'_> { pub fn view_nil (&self) -> impl Content<TuiOut> + use<'_> {
@ -10,18 +12,14 @@ impl App {
pub fn view_status (&self) -> impl Content<TuiOut> + use<'_> { pub fn view_status (&self) -> impl Content<TuiOut> + use<'_> {
self.update_clock(); self.update_clock();
let cache = self.view_cache.read().unwrap(); let cache = self.view_cache.read().unwrap();
view_status( view_status(self.selected.describe(&self.tracks, &self.scenes),
self.selected.describe(&self.tracks, &self.scenes), cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone())
cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone(),
)
} }
pub fn view_transport (&self) -> impl Content<TuiOut> + use<'_> { pub fn view_transport (&self) -> impl Content<TuiOut> + use<'_> {
self.update_clock(); self.update_clock();
let cache = self.view_cache.read().unwrap(); let cache = self.view_cache.read().unwrap();
view_transport( view_transport(self.clock.is_rolling(),
self.clock.is_rolling(), cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone())
cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone(),
)
} }
pub fn view_arranger (&self) -> impl Content<TuiOut> + use<'_> { pub fn view_arranger (&self) -> impl Content<TuiOut> + use<'_> {
ArrangerView::new(self) ArrangerView::new(self)
@ -59,6 +57,9 @@ impl App {
.enclose(self.dialog.as_ref().map(|dialog|match dialog { .enclose(self.dialog.as_ref().map(|dialog|match dialog {
Dialog::Menu => self.view_dialog_menu().boxed(), Dialog::Menu => self.view_dialog_menu().boxed(),
Dialog::Help => self.view_dialog_help().boxed(), Dialog::Help => self.view_dialog_help().boxed(),
Dialog::Save => self.view_dialog_save().boxed(),
Dialog::Load => self.view_dialog_load().boxed(),
Dialog::Options => self.view_dialog_options().boxed(),
Dialog::Device(index) => self.view_dialog_device(*index).boxed(), Dialog::Device(index) => self.view_dialog_device(*index).boxed(),
Dialog::Message(message) => self.view_dialog_message(message).boxed(), Dialog::Message(message) => self.view_dialog_message(message).boxed(),
})) }))
@ -82,23 +83,24 @@ impl App {
} else { } else {
None None
}); });
let binding = |mut binding: TokenIter, _|Bsp::e( //let binding = ;[> Bsp::e(
Fixed::x(15, Align::w(Tui::bold(true, Tui::fg(Rgb(255,192,0), if let Some(Token { //Fixed::x(15, Align::w(Tui::bold(true, Tui::fg(Rgb(255,192,0), if let Some(Token {
value: Value::Sym(key), .. //value: Value::Sym(key), ..
}) = binding.next() { //}) = binding.next() {
Some(key.to_string()) //Some(key.to_string())
} else { //} else {
None //None
})))), //})))),
Bsp::e(" ", Tui::fg(Rgb(255,255,255), if let Some(Token { //Bsp::e(" ", Tui::fg(Rgb(255,255,255), if let Some(Token {
value: Value::Key(command), .. //value: Value::Key(command), ..
}) = binding.next() { //}) = binding.next() {
Some(command.to_string()) //Some(command.to_string())
} else { //} else {
None //None
})), //})),
); //);*/
Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, bindings, binding))) Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, bindings, |b,i|format!("{i}:{b:?}"))))
//|mut binding: TokenIter, _|Map::east(5, move||binding.clone(), |_,_|"kyp"))))
} }
fn view_dialog_device (&self, index: usize) -> impl Content<TuiOut> + use<'_> { fn view_dialog_device (&self, index: usize) -> impl Content<TuiOut> + use<'_> {
@ -115,6 +117,18 @@ impl App {
Bsp::s(message, Bsp::s("", "[ OK ]")) Bsp::s(message, Bsp::s("", "[ OK ]"))
} }
fn view_dialog_save <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
"WIP: SAVE"
}
fn view_dialog_load <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
"WIP: LOAD"
}
fn view_dialog_options <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
"WIP: OPTIONS"
}
/// Spacing between tracks. /// Spacing between tracks.
pub(crate) const TRACK_SPACING: usize = 0; pub(crate) const TRACK_SPACING: usize = 0;
@ -134,16 +148,6 @@ impl App {
}) })
} }
pub(crate) fn outputs_with_sizes (&self) -> impl PortsSizes<'_> {
let mut y = 0;
self.midi_outs.iter().enumerate().map(move|(i, output)|{
let height = 1 + output.conn().len();
let data = (i, output.name(), output.conn(), y, y + height);
y += height;
data
})
}
pub(crate) fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { pub(crate) fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
use Selection::*; use Selection::*;
let mut x = 0; let mut x = 0;
@ -277,20 +281,12 @@ impl<'a> ArrangerView<'a> {
)) ))
} }
/// Render output matrix.
pub(crate) fn outputs (&'a self) -> impl Content<TuiOut> + 'a {
Tui::bg(Reset, Align::n(Bsp::s(
Bsp::s(self.output_ports(), self.output_conns()),
Bsp::s(self.output_nexts(), self.output_froms()),
)))
}
/// Render device switches. /// Render device switches.
pub(crate) fn devices (&'a self) -> impl Content<TuiOut> + 'a { pub(crate) fn devices (&'a self) -> impl Content<TuiOut> + 'a {
let Self { width_side, width_mid, track_count, track_selected, is_editing, .. } = self; let Self { width_side, width_mid, track_count, track_selected, is_editing, .. } = self;
Tryptich::top(1) Tryptich::top(1)
.left(*width_side, button_3("z", "devices", format!("{}", 0), *is_editing)) .left(*width_side, button_3("d", "devices", format!("{}", 0), *is_editing))
.right(*width_side, button_2("Z", "add device", *is_editing)) .right(*width_side, button_2("D", "add device", *is_editing))
.middle(*width_mid, per_track_top(*width_mid, ||self.tracks_with_sizes_scrolled(), .middle(*width_mid, per_track_top(*width_mid, ||self.tracks_with_sizes_scrolled(),
move|index, track|{ move|index, track|{
let bg = if *track_selected == Some(index) { let bg = if *track_selected == Some(index) {
@ -373,72 +369,6 @@ impl<'a> ArrangerView<'a> {
|_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ "))))) |_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ ")))))
} }
fn output_nexts (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(2)
.left(self.width_side, Align::ne("From clip:"))
.middle(self.width_mid, per_track_top(
self.width_mid,
||self.tracks_with_sizes_scrolled(),
|_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default())))))
}
fn output_froms (&'a self) -> impl Content<TuiOut> + 'a {
let label = Align::ne("Next clip:");
Tryptich::top(2).left(self.width_side, label).middle(self.width_mid, per_track_top(
self.width_mid, ||self.tracks_with_sizes_scrolled(), |t, track|{
let queued = track.sequencer.next_clip.is_some();
let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ "));
let queued_clip = Thunk::new(||{
Tui::bg(Reset, if let Some((_, clip)) = track.sequencer.next_clip.as_ref() {
if let Some(clip) = clip {
clip.read().unwrap().name.clone()
} else {
"Stop".into()
}
} else {
"".into()
})
});
Either(queued, queued_clip, queued_blank)
}))
}
fn output_ports (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(1)
.left(self.width_side,
button_3("o", "midi outs", format!("{}", self.outputs_count), self.is_editing))
.right(self.width_side,
button_2("O", "add midi out", self.is_editing))
.middle(self.width_mid,
per_track_top(self.width_mid, ||self.tracks_with_sizes_scrolled(), move|i, t|{
let mute = false;
let solo = false;
let mute = if mute { White } else { t.color.darkest.rgb };
let solo = if solo { White } else { t.color.darkest.rgb };
let bg_1 = if self.track_selected == Some(i) {
t.color.light.rgb
} else {
t.color.base.rgb
};
let bg_2 = if i > 0 { t.color.base.rgb } else { Reset };
let mute = Tui::fg_bg(mute, bg_1, "Play ");
let solo = Tui::fg_bg(solo, bg_1, "Solo ");
wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo))))
}))
}
fn output_conns (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(self.outputs_height)
.left(self.width_side,
io_ports(Tui::g(224), Tui::g(32), ||self.app.outputs_with_sizes()))
.middle(self.width_mid,
per_track_top(self.width_mid, ||self.tracks_with_sizes_scrolled(), |_, t|io_conns(
t.color.dark.rgb,
t.color.darker.rgb,
||self.app.outputs_with_sizes()
)))
}
/// Render scenes with clips /// Render scenes with clips
pub(crate) fn scenes (&'a self) -> impl Content<TuiOut> + 'a { pub(crate) fn scenes (&'a self) -> impl Content<TuiOut> + 'a {
@ -597,83 +527,6 @@ def_sizes_iter!(OutputsSizes => JackMidiOut);
def_sizes_iter!(PortsSizes => Arc<str>, [PortConnect]); def_sizes_iter!(PortsSizes => Arc<str>, [PortConnect]);
fn view_transport (
play: bool,
bpm: Arc<RwLock<String>>,
beat: Arc<RwLock<String>>,
time: Arc<RwLock<String>>,
) -> impl Content<TuiOut> {
let theme = ItemTheme::G[96];
Tui::bg(Black, row!(Bsp::a(
Fill::xy(Align::w(button_play_pause(play))),
Fill::xy(Align::e(row!(
FieldH(theme, "BPM", bpm),
FieldH(theme, "Beat", beat),
FieldH(theme, "Time", time),
)))
)))
}
fn view_status (
sel: Arc<str>,
sr: Arc<RwLock<String>>,
buf: Arc<RwLock<String>>,
lat: Arc<RwLock<String>>,
) -> impl Content<TuiOut> {
let theme = ItemTheme::G[96];
Tui::bg(Black, row!(Bsp::a(
Fill::xy(Align::w(FieldH(theme, "Selected", sel))),
Fill::xy(Align::e(row!(
FieldH(theme, "SR", sr),
FieldH(theme, "Buf", buf),
FieldH(theme, "Lat", lat),
)))
)))
}
pub(crate) fn button_play_pause (playing: bool) -> impl Content<TuiOut> {
let compact = true;//self.is_editing();
Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
Either::new(compact,
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(" ▗▄▖ ", " ▝▀▘ ",))))
)
)
)
}
pub (crate) 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 }
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(crate) 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)
}
pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content<TuiOut>) -> impl Content<TuiOut> { pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
let left = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV(""))); let left = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("")));
let right = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV(""))); let right = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("")));

View file

@ -0,0 +1,92 @@
use crate::*;
/// Outputs.
impl App {
pub(crate) fn outputs_with_sizes (&self) -> impl PortsSizes<'_> {
let mut y = 0;
self.midi_outs.iter().enumerate().map(move|(i, output)|{
let height = 1 + output.conn().len();
let data = (i, output.name(), output.conn(), y, y + height);
y += height;
data
})
}
}
impl<'a> ArrangerView<'a> {
fn output_nexts (&self) -> impl Content<TuiOut> + '_ {
Tryptich::top(2)
.left(self.width_side, Align::ne("From clip:"))
.middle(self.width_mid, per_track_top(
self.width_mid,
||self.tracks_with_sizes_scrolled(),
|_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default())))))
}
/// Render output matrix.
pub(crate) fn outputs (&'a self) -> impl Content<TuiOut> + 'a {
Tui::bg(Reset, Align::n(Bsp::s(
Bsp::s(self.output_ports(), self.output_conns()),
Bsp::s(self.output_nexts(), self.output_froms()),
)))
}
fn output_froms (&'a self) -> impl Content<TuiOut> + 'a {
let label = Align::ne("Next clip:");
Tryptich::top(2).left(self.width_side, label).middle(self.width_mid, per_track_top(
self.width_mid, ||self.tracks_with_sizes_scrolled(), |t, track|{
let queued = track.sequencer.next_clip.is_some();
let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ "));
let queued_clip = Thunk::new(||{
Tui::bg(Reset, if let Some((_, clip)) = track.sequencer.next_clip.as_ref() {
if let Some(clip) = clip {
clip.read().unwrap().name.clone()
} else {
"Stop".into()
}
} else {
"".into()
})
});
Either(queued, queued_clip, queued_blank)
}))
}
fn output_ports (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(1)
.left(self.width_side, self.output_count())
.right(self.width_side, self.output_add())
.middle(self.width_mid, self.output_map())
}
fn output_count (&'a self) -> impl Content<TuiOut> + 'a {
button_3("o", "midi outs", format!("{}", self.outputs_count), self.is_editing)
}
fn output_add (&'a self) -> impl Content<TuiOut> + 'a {
button_2("O", "add midi out", self.is_editing)
}
fn output_map (&'a self) -> impl Content<TuiOut> + 'a {
per_track_top(self.width_mid, ||self.tracks_with_sizes_scrolled(), move|i, t|{
let mute = false;
let solo = false;
let mute = if mute { White } else { t.color.darkest.rgb };
let solo = if solo { White } else { t.color.darkest.rgb };
let bg_1 = if self.track_selected == Some(i) {
t.color.light.rgb
} else {
t.color.base.rgb
};
let bg_2 = if i > 0 { t.color.base.rgb } else { Reset };
let mute = Tui::fg_bg(mute, bg_1, "Play ");
let solo = Tui::fg_bg(solo, bg_1, "Solo ");
wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo))))
})
}
fn output_conns (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(self.outputs_height)
.left(self.width_side, io_ports(Tui::g(224), Tui::g(32), ||self.app.outputs_with_sizes()))
.middle(self.width_mid, per_track_top(
self.width_mid,
||self.tracks_with_sizes_scrolled(),
|_, t|io_conns(
t.color.dark.rgb,
t.color.darker.rgb,
||self.app.outputs_with_sizes()
)))
}
}

View file

@ -1,5 +1,6 @@
mod clock_api; pub use self::clock_api::*; mod clock_api; pub use self::clock_api::*;
mod clock_model; pub use self::clock_model::*; mod clock_model; pub use self::clock_model::*;
mod clock_view; pub use self::clock_view::*;
pub trait HasClock: Send + Sync { pub trait HasClock: Send + Sync {
fn clock (&self) -> &Clock; fn clock (&self) -> &Clock;

View file

@ -0,0 +1,51 @@
use crate::*;
pub fn view_transport (
play: bool,
bpm: Arc<RwLock<String>>,
beat: Arc<RwLock<String>>,
time: Arc<RwLock<String>>,
) -> impl Content<TuiOut> {
let theme = ItemTheme::G[96];
Tui::bg(Black, row!(Bsp::a(
Fill::xy(Align::w(button_play_pause(play))),
Fill::xy(Align::e(row!(
FieldH(theme, "BPM", bpm),
FieldH(theme, "Beat", beat),
FieldH(theme, "Time", time),
)))
)))
}
pub fn view_status (
sel: Arc<str>,
sr: Arc<RwLock<String>>,
buf: Arc<RwLock<String>>,
lat: Arc<RwLock<String>>,
) -> impl Content<TuiOut> {
let theme = ItemTheme::G[96];
Tui::bg(Black, row!(Bsp::a(
Fill::xy(Align::w(FieldH(theme, "Selected", sel))),
Fill::xy(Align::e(row!(
FieldH(theme, "SR", sr),
FieldH(theme, "Buf", buf),
FieldH(theme, "Lat", lat),
)))
)))
}
pub(crate) fn button_play_pause (playing: bool) -> impl Content<TuiOut> {
let compact = true;//self.is_editing();
Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
Either::new(compact,
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(" ▗▄▖ ", " ▝▀▘ ",))))
)
)
)
}

View file

@ -52,3 +52,30 @@ pub fn to_rms (samples: &[f32]) -> f32 {
.unwrap_or(0.0); .unwrap_or(0.0);
(sum / samples.len() as f32).sqrt() (sum / samples.len() as f32).sqrt()
} }
pub (crate) 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 }
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(crate) 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)
}