`Controls/SegmentMeter` draws a 24-segment ladder in ONE OnRender pass — no per-segment elements updating on the UI thread, which is the difference between a meter and a UI thread tax. Level and peak are display fractions (what was MEASURED), never gain settings; vertical for the bay ladders, horizontal for the master strip. Bands come from `Helpers/MeterScale` (nominal/caution/peak, DESIGN.md thresholds) and live as dependencies of the control, so the painting logic is pure and testable: the band a segment paints is the level it measured. Brushes resolve from the theme tokens via SetResourceReference, so the ladder cannot drift from DESIGN.md. Peak-hold is the reason the header exists: a bar that follows the moment hides clipping, and clipping is the one audio fault that cannot be fixed after the stream. Guard: MeterScaleTests — band edges (inclusive at −12 and −3, as the standard states the ranges), the −60…0 display scale, and peak-hold rising instantly then decaying at 0.35/s.
106 lines
5.6 KiB
C#
106 lines
5.6 KiB
C#
using System;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Media;
|
|
using ytLive.Helpers;
|
|
|
|
namespace ytLive.Controls;
|
|
|
|
/// <summary>A segmented level ladder for the audio dock.
|
|
///
|
|
/// <para>Drawn in ONE <see cref="OnRender"/> pass. Twenty-four <c>Rectangle</c>s per meter would
|
|
/// mean hundreds of elements updating on the UI thread; a single drawing call does not. The
|
|
/// consumer updates <see cref="Level"/> at ~20 Hz — music-level smoothness is not needed and the
|
|
/// paint cost should never compete with the compositor.</para>
|
|
///
|
|
/// <para>Level is a 0…1 display fraction (see <see cref="MeterScale"/>), never a gain setting: the
|
|
/// ladder shows what was measured.</para></summary>
|
|
public sealed class SegmentMeter : FrameworkElement
|
|
{
|
|
public static readonly DependencyProperty LevelProperty = DependencyProperty.Register(
|
|
nameof(Level), typeof(double), typeof(SegmentMeter),
|
|
new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsRender));
|
|
|
|
/// <summary>Peak-hold mark, 0…1. Falls back slowly so a clip stays visible long enough to see.</summary>
|
|
public static readonly DependencyProperty PeakProperty = DependencyProperty.Register(
|
|
nameof(Peak), typeof(double), typeof(SegmentMeter),
|
|
new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsRender));
|
|
|
|
public static readonly DependencyProperty SegmentsProperty = DependencyProperty.Register(
|
|
nameof(Segments), typeof(int), typeof(SegmentMeter),
|
|
new FrameworkPropertyMetadata(24, FrameworkPropertyMetadataOptions.AffectsRender));
|
|
|
|
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
|
|
nameof(Orientation), typeof(Orientation), typeof(SegmentMeter),
|
|
new FrameworkPropertyMetadata(Orientation.Vertical, FrameworkPropertyMetadataOptions.AffectsRender));
|
|
|
|
public double Level { get => (double)GetValue(LevelProperty); set => SetValue(LevelProperty, value); }
|
|
public double Peak { get => (double)GetValue(PeakProperty); set => SetValue(PeakProperty, value); }
|
|
public int Segments { get => (int)GetValue(SegmentsProperty); set => SetValue(SegmentsProperty, value); }
|
|
public Orientation Orientation { get => (Orientation)GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); }
|
|
|
|
public SegmentMeter()
|
|
{
|
|
// Token-driven, not literal: the ladder cannot drift from DESIGN.md.
|
|
SetResourceReference(NominalBrushProperty, "Brush.Meter.Nominal");
|
|
SetResourceReference(CautionBrushProperty, "Brush.Meter.Caution");
|
|
SetResourceReference(PeakBrushProperty, "Brush.Meter.Peak");
|
|
SetResourceReference(DimBrushProperty, "Brush.SurfaceContainerHighest");
|
|
}
|
|
|
|
private static readonly DependencyProperty NominalBrushProperty = DependencyProperty.Register(
|
|
"NominalBrush", typeof(Brush), typeof(SegmentMeter), new FrameworkPropertyMetadata(Brushes.Cyan, FrameworkPropertyMetadataOptions.AffectsRender));
|
|
private static readonly DependencyProperty CautionBrushProperty = DependencyProperty.Register(
|
|
"CautionBrush", typeof(Brush), typeof(SegmentMeter), new FrameworkPropertyMetadata(Brushes.Orange, FrameworkPropertyMetadataOptions.AffectsRender));
|
|
private static readonly DependencyProperty PeakBrushProperty = DependencyProperty.Register(
|
|
"PeakBrush", typeof(Brush), typeof(SegmentMeter), new FrameworkPropertyMetadata(Brushes.Red, FrameworkPropertyMetadataOptions.AffectsRender));
|
|
private static readonly DependencyProperty DimBrushProperty = DependencyProperty.Register(
|
|
"DimBrush", typeof(Brush), typeof(SegmentMeter), new FrameworkPropertyMetadata(Brushes.DimGray, FrameworkPropertyMetadataOptions.AffectsRender));
|
|
|
|
private Brush NominalBrush => (Brush)GetValue(NominalBrushProperty);
|
|
private Brush CautionBrush => (Brush)GetValue(CautionBrushProperty);
|
|
private Brush PeakBrush => (Brush)GetValue(PeakBrushProperty);
|
|
private Brush DimBrush => (Brush)GetValue(DimBrushProperty);
|
|
|
|
private const double Gap = 2;
|
|
|
|
protected override void OnRender(DrawingContext dc)
|
|
{
|
|
var count = Math.Max(1, Segments);
|
|
var vertical = Orientation == Orientation.Vertical;
|
|
var extent = vertical ? ActualHeight : ActualWidth;
|
|
var thickness = vertical ? ActualWidth : ActualHeight;
|
|
if (extent <= 0 || thickness <= 0) return;
|
|
|
|
var segmentSize = (extent - Gap * (count - 1)) / count;
|
|
if (segmentSize <= 0) return;
|
|
|
|
var lit = Math.Clamp(Level, 0, 1) * count;
|
|
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
// Segment 0 sits at the bottom (vertical) or the left (horizontal).
|
|
var fraction = (i + 1) / (double)count;
|
|
var brush = fraction >= MeterScale.ToFraction(MeterScale.PeakDb) ? PeakBrush
|
|
: fraction >= MeterScale.ToFraction(MeterScale.CautionDb) ? CautionBrush
|
|
: NominalBrush;
|
|
|
|
if (i >= lit) brush = DimBrush;
|
|
|
|
var offset = i * (segmentSize + Gap);
|
|
var rect = vertical
|
|
? new Rect(0, extent - offset - segmentSize, thickness, segmentSize)
|
|
: new Rect(offset, 0, segmentSize, thickness);
|
|
dc.DrawRectangle(brush, null, rect);
|
|
}
|
|
|
|
// Peak-hold: a 2px marker at the held level, drawn above the ladder.
|
|
var peak = Math.Clamp(Peak, 0, 1);
|
|
if (peak <= 0) return;
|
|
var line = vertical
|
|
? new Rect(0, extent - peak * extent - 1, thickness, 2)
|
|
: new Rect(peak * extent - 1, 0, 2, thickness);
|
|
dc.DrawRectangle(PeakBrush, null, line);
|
|
}
|
|
}
|