Merge pull request #380 from grovebit/controls-look
Draw faders and meters more like Pro Tools
This commit is contained in:
commit
a96cf50a3a
|
|
@ -3,8 +3,8 @@
|
||||||
use crate::SoundApp;
|
use crate::SoundApp;
|
||||||
use crate::theme::{Tokens, bold, regular, rgb};
|
use crate::theme::{Tokens, bold, regular, rgb};
|
||||||
use crate::widgets::{
|
use crate::widgets::{
|
||||||
PannerEdit, PannerSpeaker, db_text, fader, groove, inset, meter, multi_meter, pan_knob, pan_text, raised_key, rec_toggle, selector_box,
|
PannerEdit, PannerSpeaker, db_text, fader, fader_slot, groove, inset, meter, meter_scale, multi_meter, pan_knob, pan_text, raised_key,
|
||||||
selector_box_with_tooltip, shade, surround_panner, text_toggle, vgradient,
|
rec_toggle, selector_box, selector_box_with_tooltip, shade, surround_panner, text_toggle, vgradient,
|
||||||
};
|
};
|
||||||
use egui::{Align2, Color32, CornerRadius, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
use egui::{Align2, Color32, CornerRadius, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
@ -410,7 +410,11 @@ fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapsh
|
||||||
let _ = app.engine.execute_merged("mix.volume", &json!({"track": id.0, "db": db}), &format!("fader:{}", id.0));
|
let _ = app.engine.execute_merged("mix.volume", &json!({"track": id.0, "db": db}), &format!("fader:{}", id.0));
|
||||||
}
|
}
|
||||||
let md = app.meters.get(&id).copied().unwrap_or_default();
|
let md = app.meters.get(&id).copied().unwrap_or_default();
|
||||||
let mr = Rect::from_min_max(pos2(fr.max.x + 4.0, fr.min.y + 6.0), pos2(x0 + inner_w - 2.0, fr.max.y - 6.0));
|
let slot = fader_slot(fr);
|
||||||
|
let mr = Rect::from_min_max(pos2(fr.max.x + 4.0, slot.min.y - 5.0), pos2(x0 + inner_w - 2.0, slot.max.y + if wide_meter { 17.0 } else { 1.0 }));
|
||||||
|
if !narrow && !wide_meter {
|
||||||
|
meter_scale(ui.painter(), mr, mr.min.x - 1.0);
|
||||||
|
}
|
||||||
let strip_fmt = if track.kind == TrackKind::Master { app.engine.session().main_format() } else { track.format };
|
let strip_fmt = if track.kind == TrackKind::Master { app.engine.session().main_format() } else { track.format };
|
||||||
let all_chans = strip_fmt.channels().min(soundcraft_mix::MAX_CHANNELS);
|
let all_chans = strip_fmt.channels().min(soundcraft_mix::MAX_CHANNELS);
|
||||||
let chans = if track.kind == TrackKind::Master { 2 } else { track.channels().min(2) };
|
let chans = if track.kind == TrackKind::Master { 2 } else { track.channels().min(2) };
|
||||||
|
|
|
||||||
|
|
@ -217,37 +217,63 @@ pub fn pan_text(v: f32) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vertical LED peak meter with green/yellow/red zones; values are linear peaks.
|
/// The dB marks of a meter's scale, also ruled across its bar.
|
||||||
|
const METER_MARKS: [f32; 8] = [0.0, -5.0, -10.0, -15.0, -20.0, -30.0, -40.0, -60.0];
|
||||||
|
|
||||||
|
/// The bar of a [`meter`] drawn in `r`, below its clip light.
|
||||||
|
fn meter_bar(r: Rect) -> Rect {
|
||||||
|
Rect::from_min_max(pos2(r.min.x + 1.0, r.min.y + 5.0), pos2(r.max.x - 1.0, r.max.y - 1.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The marks that fit on `bar`, at least 9 pt apart from the top down, with their heights.
|
||||||
|
fn meter_marks(bar: Rect) -> impl Iterator<Item = (f32, f32)> {
|
||||||
|
let mut last = f32::NEG_INFINITY;
|
||||||
|
METER_MARKS.into_iter().filter_map(move |db| {
|
||||||
|
let y = bar.max.y - bar.height() * meter_pos(db);
|
||||||
|
(y - last >= 9.0).then(|| {
|
||||||
|
last = y;
|
||||||
|
(db, y)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vertical peak meter as Pro Tools draws it: a clip light above a bar that lights green, yellow
|
||||||
|
/// and red over dark tints of those zones, ruled at the scale's marks. Values are linear peaks.
|
||||||
pub fn meter(ui: &Ui, r: Rect, level: f32, hold: f32, clip: bool) {
|
pub fn meter(ui: &Ui, r: Rect, level: f32, hold: f32, clip: bool) {
|
||||||
let t = Tokens::current();
|
let t = Tokens::current();
|
||||||
let p = ui.painter();
|
let p = ui.painter();
|
||||||
p.rect_filled(r, 0.0, t.meter_bg);
|
p.rect_filled(Rect::from_min_max(r.min, pos2(r.max.x, r.min.y + 3.0)), 1.0, if clip { t.meter_red } else { t.meter_clip_off });
|
||||||
let h = r.height() - 4.0;
|
p.rect_filled(Rect::from_min_max(pos2(r.min.x, r.min.y + 4.0), r.max), 0.0, t.meter_bg);
|
||||||
let inner = Rect::from_min_max(pos2(r.min.x + 1.0, r.min.y + 3.0), pos2(r.max.x - 1.0, r.max.y - 1.0));
|
let bar = meter_bar(r);
|
||||||
let lv = level_pos(level);
|
let y = |pos: f32| bar.max.y - bar.height() * pos;
|
||||||
let (amber, red) = (meter_pos(-12.0), meter_pos(-3.0));
|
let (amber, red) = (meter_pos(-12.0), meter_pos(-3.0));
|
||||||
let mut leds = Mesh::default();
|
let zones = [(0.0, amber, t.meter_green), (amber, red, t.meter_yellow), (red, 1.0, t.meter_red)];
|
||||||
for i in 0..(h / 3.0).clamp(0.0, 1000.0) as usize {
|
let lit = level_pos(level);
|
||||||
let y = inner.max.y - i as f32 * 3.0;
|
let unlit = if t.meter_bg.intensity() > 0.5 { 0.08 } else { 0.18 };
|
||||||
let f = (i as f32 * 3.0 + 1.0) / h;
|
let mut mesh = Mesh::default();
|
||||||
let zone = if f > red {
|
for (lo, hi, color) in zones {
|
||||||
t.meter_red
|
let top = lit.clamp(lo, hi);
|
||||||
} else if f > amber {
|
quad(&mut mesh, Rect::from_x_y_ranges(bar.x_range(), y(top)..=y(lo)), [color; 4]);
|
||||||
t.meter_yellow
|
quad(&mut mesh, Rect::from_x_y_ranges(bar.x_range(), y(hi)..=y(top)), [color.gamma_multiply(unlit); 4]);
|
||||||
} else {
|
|
||||||
t.meter_green
|
|
||||||
};
|
|
||||||
let col = if f <= lv { zone } else { zone.gamma_multiply(0.1) };
|
|
||||||
quad(&mut leds, Rect::from_x_y_ranges(inner.x_range(), y - 2.0..=y), [col; 4]);
|
|
||||||
}
|
}
|
||||||
p.add(leds);
|
p.add(mesh);
|
||||||
let hv = level_pos(hold);
|
for (_, at) in meter_marks(bar).skip(1) {
|
||||||
if hv > 0.01 {
|
p.hline(bar.x_range(), p.round_to_pixel_center(at), Stroke::new(1.0, Color32::from_black_alpha(90)));
|
||||||
let y = inner.max.y - h * hv;
|
}
|
||||||
p.line_segment([pos2(inner.min.x, y), pos2(inner.max.x, y)], Stroke::new(1.0, if hv > red { t.meter_red } else { t.meter_yellow }));
|
let held = level_pos(hold);
|
||||||
|
if held > 0.01 {
|
||||||
|
let color = zones.iter().find(|z| held <= z.1).map_or(t.meter_red, |z| z.2);
|
||||||
|
p.rect_filled(Rect::from_x_y_ranges(bar.x_range(), y(held) - 1.0..=y(held) + 1.0), 0.0, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The dB scale of a [`meter`] drawn in `r`, its numbers ending at `x`.
|
||||||
|
pub fn meter_scale(p: &Painter, r: Rect, x: f32) {
|
||||||
|
let t = Tokens::current();
|
||||||
|
for (db, y) in meter_marks(meter_bar(r)) {
|
||||||
|
p.text(pos2(x - 3.0, y), Align2::RIGHT_CENTER, format!("{}", db.abs()), regular(8.0), t.text_dim);
|
||||||
|
p.hline(x - 2.0..=x, y, Stroke::new(1.0, t.text_dim.gamma_multiply(0.7)));
|
||||||
}
|
}
|
||||||
let clip_r = Rect::from_min_max(r.min, pos2(r.max.x, r.min.y + 2.5));
|
|
||||||
p.rect_filled(clip_r, 0.0, if clip { t.meter_red } else { t.meter_clip_off });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A linear peak → 0..1 meter position.
|
/// A linear peak → 0..1 meter position.
|
||||||
|
|
@ -284,36 +310,48 @@ pub fn clip_gain_to_pos(db: f32) -> f32 {
|
||||||
if db >= -36.0 { 0.25 + (db + 36.0) / 72.0 * 0.75 } else { (1.0 - ((-36.0 - db) / 108.0).powf(1.0 / 1.5)) * 0.25 }
|
if db >= -36.0 { 0.25 + (db + 36.0) / 72.0 * 0.75 } else { (1.0 - ((-36.0 - db) / 108.0).powf(1.0 / 1.5)) * 0.25 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FADER_CAP_H: f32 = 40.0;
|
||||||
|
|
||||||
|
/// The travel of a [`fader`] drawn in `r`: its cap's centre runs from the bottom (-inf) to the
|
||||||
|
/// top (+12 dB).
|
||||||
|
pub fn fader_slot(r: Rect) -> Rect {
|
||||||
|
Rect::from_center_size(r.center(), vec2(4.0, r.height() - FADER_CAP_H))
|
||||||
|
}
|
||||||
|
|
||||||
/// A fader: returns the new dB value when dragged. `db` in -144..12.
|
/// A fader: returns the new dB value when dragged. `db` in -144..12.
|
||||||
pub fn fader(ui: &mut Ui, r: Rect, db: f32, id: egui::Id) -> Option<f32> {
|
pub fn fader(ui: &mut Ui, r: Rect, db: f32, id: egui::Id) -> Option<f32> {
|
||||||
let t = Tokens::current();
|
let t = Tokens::current();
|
||||||
let resp = ui.interact(r, id, Sense::click_and_drag());
|
let resp = ui.interact(r, id, Sense::click_and_drag());
|
||||||
let p = ui.painter();
|
let p = ui.painter();
|
||||||
let cap_size = vec2((r.width() * 0.44).clamp(16.0, 24.0), 34.0);
|
let slot = fader_slot(r);
|
||||||
let slot = Rect::from_center_size(r.center(), vec2(5.0, r.height() - cap_size.y));
|
let cap_w = (r.width() * 0.4).clamp(16.0, 18.0);
|
||||||
p.rect_filled(slot, 2.0, t.fader_track);
|
p.rect_filled(slot, 2.0, t.fader_track);
|
||||||
p.vline(slot.min.x + 0.5, slot.min.y + 1.0..=slot.max.y - 1.0, Stroke::new(1.0, Color32::from_black_alpha(120)));
|
p.vline(slot.min.x + 0.5, slot.min.y + 1.0..=slot.max.y - 1.0, Stroke::new(1.0, Color32::from_black_alpha(120)));
|
||||||
p.vline(slot.max.x + 0.5, slot.min.y + 2.0..=slot.max.y - 2.0, Stroke::new(1.0, Color32::from_white_alpha(18)));
|
p.vline(slot.max.x + 0.5, slot.min.y + 2.0..=slot.max.y - 2.0, Stroke::new(1.0, Color32::from_white_alpha(18)));
|
||||||
|
let numbers_x = slot.center().x - cap_w * 0.5 - 1.0;
|
||||||
|
let mut last = f32::NEG_INFINITY;
|
||||||
for mark in [12.0f32, 6.0, 0.0, -5.0, -10.0, -15.0, -20.0, -30.0, -40.0, -60.0] {
|
for mark in [12.0f32, 6.0, 0.0, -5.0, -10.0, -15.0, -20.0, -30.0, -40.0, -60.0] {
|
||||||
let y = slot.max.y - slot.height() * soundcraft_model::fader_db_to_pos(mark);
|
let y = slot.max.y - slot.height() * soundcraft_model::fader_db_to_pos(mark);
|
||||||
let x = p.text(pos2(r.min.x, y), Align2::LEFT_CENTER, format!("{}", mark.abs()), regular(8.0), t.text_dim).max.x + 2.0;
|
p.hline(slot.min.x - 6.0..=slot.min.x - 2.0, y, Stroke::new(1.0, t.text_dim.gamma_multiply(0.7)));
|
||||||
if x < slot.min.x - 3.0 {
|
if numbers_x - r.min.x >= 10.0 && y - last >= 9.0 {
|
||||||
p.hline(x..=slot.min.x - 3.0, y, Stroke::new(1.0, t.text_dim.gamma_multiply(0.7)));
|
p.text(pos2(numbers_x, y), Align2::RIGHT_CENTER, format!("{}", mark.abs()), regular(8.0), t.text_dim);
|
||||||
|
last = y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let pos = soundcraft_model::fader_db_to_pos(db);
|
let pos = soundcraft_model::fader_db_to_pos(db);
|
||||||
let y = slot.max.y - slot.height() * pos;
|
let y = slot.max.y - slot.height() * pos;
|
||||||
let cap = Rect::from_center_size(pos2(slot.center().x, y), cap_size);
|
let cap = Rect::from_center_size(pos2(slot.center().x, y), vec2(cap_w, FADER_CAP_H));
|
||||||
p.rect_filled(cap.translate(vec2(0.0, 2.0)).expand(1.0), 4.0, Color32::from_black_alpha(80));
|
p.rect_filled(cap.translate(vec2(0.0, 2.0)).expand(1.0), 4.0, Color32::from_black_alpha(80));
|
||||||
let g = Color32::from_gray;
|
let g = Color32::from_gray;
|
||||||
vgradient(p, cap.shrink(1.0), g(226), g(146));
|
p.rect_filled(cap, 3.0, g(34));
|
||||||
p.rect_stroke(cap, 3.0, Stroke::new(1.0, g(34)), StrokeKind::Inside);
|
let face = cap.shrink(1.0);
|
||||||
for dy in [-10.0, -7.0, 7.0, 10.0] {
|
rounded_vgradient(p, face, 2.0, g(236), g(104));
|
||||||
let x = cap.min.x + 3.0..=cap.max.x - 3.0;
|
vgradient(p, Rect::from_x_y_ranges(face.x_range(), y + 1.0..=face.max.y - 2.0), g(214), g(150));
|
||||||
p.hline(x.clone(), cap.center().y + dy, Stroke::new(1.0, Color32::from_black_alpha(50)));
|
p.hline(face.min.x + 2.0..=face.max.x - 2.0, face.min.y + 1.5, Stroke::new(1.0, Color32::from_white_alpha(110)));
|
||||||
p.hline(x, cap.center().y + dy + 1.0, Stroke::new(1.0, Color32::from_white_alpha(90)));
|
p.hline(face.x_range(), y, Stroke::new(2.0, g(30)));
|
||||||
|
for (x, dx) in [(cap.min.x, 2.5), (cap.max.x, -2.5)] {
|
||||||
|
p.add(egui::Shape::convex_polygon(vec![pos2(x, y - 3.0), pos2(x + dx, y), pos2(x, y + 3.0)], g(34), Stroke::NONE));
|
||||||
}
|
}
|
||||||
p.hline(cap.min.x + 1.0..=cap.max.x - 1.0, cap.center().y, Stroke::new(2.0, g(24)));
|
|
||||||
if resp.double_clicked() || (resp.clicked() && ui.input(|i| i.modifiers.alt)) {
|
if resp.double_clicked() || (resp.clicked() && ui.input(|i| i.modifiers.alt)) {
|
||||||
return Some(0.0);
|
return Some(0.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,3 +85,30 @@ fn stereo_strips_line_up_with_mono_strips() {
|
||||||
let (top, bottom) = solo_keys.iter().fold((f32::MAX, f32::MIN), |(lo, hi), &y| (lo.min(y), hi.max(y)));
|
let (top, bottom) = solo_keys.iter().fold((f32::MAX, f32::MIN), |(lo, hi), &y| (lo.min(y), hi.max(y)));
|
||||||
assert!(bottom - top < 0.5, "solo keys span {top}..{bottom}");
|
assert!(bottom - top < 0.5, "solo keys span {top}..{bottom}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_meter_starts_level_with_the_top_of_the_faders_travel() {
|
||||||
|
let _palette = PALETTE.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let harness = mix(ThemeMode::Dark);
|
||||||
|
let numbers: Vec<_> = texts(harness.output(), |t| t.galley.job.sections[0].format.font_id.size == 8.0)
|
||||||
|
.map(|t| (t.galley.job.text.as_str(), t.galley.rect.translate(t.pos.to_vec2())))
|
||||||
|
.collect();
|
||||||
|
let fader_tops: Vec<_> = numbers.iter().filter(|(text, _)| *text == "12").map(|(_, r)| *r).collect();
|
||||||
|
assert!(fader_tops.len() >= 6, "found {} fader scales", fader_tops.len());
|
||||||
|
for top in fader_tops {
|
||||||
|
let meter_top = numbers.iter().find(|(text, r)| *text == "0" && r.min.x > top.max.x + 16.0 && r.min.x < top.max.x + 60.0);
|
||||||
|
assert!(meter_top.is_some_and(|(_, r)| (r.center().y - top.center().y).abs() < 0.5), "meter scale beside {top:?}: {meter_top:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn narrow_strips_show_ticks_without_numbers() {
|
||||||
|
let _palette = PALETTE.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let mut harness = mix(ThemeMode::Dark);
|
||||||
|
harness.state_mut().ui.narrow_mix = true;
|
||||||
|
for _ in 0..3 {
|
||||||
|
harness.step();
|
||||||
|
}
|
||||||
|
let numbers = texts(harness.output(), |t| t.galley.job.sections[0].format.font_id.size == 8.0 && SCALE.contains(&t.galley.job.text.as_str()));
|
||||||
|
assert_eq!(numbers.count(), 0);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue