skin_engine: hold option for the peak meter bars

The %pL and %pR bars followed every drop of the level, and had nothing
like the peak marker of the built-in peak meter. With the new "hold"
bar option a bar falls back at the peak meter's release rate, and the
highest level is held for the peak hold time, both from the same
settings as the built-in meter (peak_meter_get_times()). Without the
option the bars are as before.

The held peak is a block ending at the held level, 1 pixel thick or as
many as the optional number after "hold" says (eg. "hold, 4"). On a bar
with a fill image the block is drawn from the same part of the image,
so a bar drawn as LED segments holds a whole segment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Change-Id: I5c6262ba02b7d2df6c5f80f15d4a1d4354ef8e26
This commit is contained in:
Marcin Bukat 2026-10-07 11:51:13 +02:00 • committed by Solomon Peachy
parent 70c5a4e9e9
commit 7d090f24b9
6 changed files with 139 additions and 2 deletions

View file

@ -170,6 +170,34 @@ void ab_draw_markers(struct screen * screen, int capacity,
#endif
/* A peak meter bar with the hold option: the level falls back by the peak
* release rate - pixels per tick, here of a bar of the given size - and
* the highest recent level is held for the peak hold time. */
static unsigned long peak_bar_hold(struct progressbar *pb, unsigned long end,
int size)
{
int release, hold_ticks;
long delta = current_tick - pb->last_tick;
int level = end;
peak_meter_get_times(&release, &hold_ticks);
pb->last_tick = current_tick;
if (size <= 0)
size = 1;
if (delta >= 0 && delta < HZ)
level = MAX(level, pb->level - delta * release * MAX_PEAK / size);
pb->level = level;
if (TIME_AFTER(current_tick, pb->hold_tick))
pb->hold_pos = 0;
if (level > pb->hold_pos)
{
pb->hold_pos = level;
pb->hold_tick = current_tick + hold_ticks;
}
return level;
}
void draw_progressbar(struct gui_wps *gwps, struct skin_viewport* skin_viewport,
int line, struct progressbar *pb)
{
@ -230,6 +258,8 @@ void draw_progressbar(struct gui_wps *gwps, struct skin_viewport* skin_viewport,
val = pb->type == SKIN_TOKEN_PEAKMETER_LEFTBAR ? left : right;
length = MAX_PEAK;
end = peak_meter_scale_value(val, length);
if (pb->hold)
end = peak_bar_hold(pb, end, pb->horizontal ? width : height);
}
else if (pb->type == SKIN_TOKEN_PLAYLIST_PERCENTBAR)
{
@ -345,6 +375,59 @@ void draw_progressbar(struct gui_wps *gwps, struct skin_viewport* skin_viewport,
length, 0, end, flags);
}
/* the held peak of a peak meter bar: a block hold_size pixels thick
* ending at the held level, cut from the fill image if the bar has one,
* so that a bar drawn as LED segments holds a segment */
if (pb->hold && pb->hold_pos > 0 &&
(pb->type == SKIN_TOKEN_PEAKMETER_LEFTBAR ||
pb->type == SKIN_TOKEN_PEAKMETER_RIGHTBAR))
{
struct gui_img *img = pb->nobar ? NULL :
SKINOFFSETTOPTR(get_skin_buffer(gwps->data), pb->image);
int hx = x, hy = y, hw = width, hh = height;
int len, pos, off, size = pb->hold_size;
if (!img && !(flags&BORDER_NOFILL))
{
/* inside the frame, like the bar */
hx++; hy++; hw -= 2; hh -= 2;
}
len = (flags&HORIZONTAL) ? hw : hh;
pos = len * pb->hold_pos / MAX_PEAK;
off = pos - size;
if (off < 0)
{
size += off;
off = 0;
}
if (flags&INVERTFILL)
off = len - pos;
if (flags&HORIZONTAL)
{
hx += off;
hw = size;
}
else
{
hy += off;
hh = size;
}
if (img)
{
/* the image maps onto the bar 1:1, as in the bar itself */
int bx = hx - x, by = hy - y;
img->bm.data = core_get_data(img->buflib_handle);
hw = MIN(hw, img->bm.width - bx);
hh = MIN(hh, img->bm.height - by);
if (hw > 0 && hh > 0)
display->bmp_part(&img->bm, bx, by, hx, hy, hw, hh);
}
else if (hw > 0 && hh > 0)
display->fillrect(hx, hy, hw, hh);
}
if (SKINOFFSETTOPTR(get_skin_buffer(gwps->data), pb->slider))
{
int xoff = 0, yoff = 0;

View file

@ -998,6 +998,10 @@ static int parse_progressbar_tag(struct skin_element* element,
pb->nofill = false;
pb->noborder = false;
pb->nobar = false;
pb->hold = false;
pb->hold_size = 1;
pb->level = pb->hold_pos = 0;
pb->last_tick = pb->hold_tick = 0;
pb->image = PTRTOSKINOFFSET(skin_buffer, NULL);
pb->slider = PTRTOSKINOFFSET(skin_buffer, NULL);
pb->backdrop = PTRTOSKINOFFSET(skin_buffer, NULL);
@ -1083,14 +1087,16 @@ static int parse_progressbar_tag(struct skin_element* element,
{
eINVERT = 0, eNOFILL, eNOBORDER, eNOBAR, eSLIDER, eIMAGE,
eBACKDROP, eVERTICAL, eHORIZONTAL, eNOTOUCH, eSETTING, eSETTING_OFFSET,
e_PB_TAG_COUNT
eHOLD, e_PB_TAG_COUNT
};
static const char *pb_options[e_PB_TAG_COUNT + 1] = {[eINVERT] = "invert",
[eNOFILL] = "nofill", [eNOBORDER] = "noborder", [eNOBAR] = "nobar",
[eSLIDER] = "slider", [eIMAGE] = "image", [eBACKDROP] = "backdrop",
[eVERTICAL] = "vertical", [eHORIZONTAL] = "horizontal",
[eNOTOUCH] = "notouch", [eSETTING] = "setting", [eSETTING_OFFSET] = "soffset", [e_PB_TAG_COUNT] = NULL};
[eNOTOUCH] = "notouch", [eSETTING] = "setting",
[eSETTING_OFFSET] = "soffset", [eHOLD] = "hold",
[e_PB_TAG_COUNT] = NULL};
int pb_op;
while (curr_param < element->params_count)
@ -1108,6 +1114,32 @@ static int parse_progressbar_tag(struct skin_element* element,
pb->noborder = true;
else if (pb_op == eNOBAR)
pb->nobar = true;
else if (pb_op == eHOLD)
{
pb->hold = true;
/* optional: the thickness of the held peak */
if (curr_param+1 < element->params_count)
{
struct skin_tag_parameter *next = param + 1;
int size = -1;
if (next->type == INTEGER)
size = next->data.number;
else if (next->type == STRING)
{
text = SKINOFFSETTOPTR(skin_buffer, next->data.text);
if (isdigit(*text))
size = atoi(text);
}
if (size >= 0)
{
if (size == 0)
return WPS_ERROR_INVALID_PARAM;
pb->hold_size = size;
curr_param++;
param++;
}
}
}
else if (pb_op == eSLIDER)
{
if (curr_param+1 < element->params_count)

View file

@ -136,6 +136,12 @@ struct progressbar {
bool nofill;
bool noborder;
bool nobar;
/* peak meter bars: fall back at the peak release rate and mark the
* held peak, as the recording screen's peak meter does */
bool hold;
int hold_size; /* thickness of the held peak, in pixels */
int level, hold_pos; /* in units of MAX_PEAK */
long last_tick, hold_tick;
OFFSETTYPE(struct gui_img *) slider;
OFFSETTYPE(struct gui_img *) backdrop;

View file

@ -492,6 +492,14 @@ void peak_meter_init_times(int release, int hold_ms, int clip_hold_sec)
pm_clip_hold = HZ * clip_hold_sec;
}
/* The release, in pixels per tick, and the peak hold time in ticks, as set
* by peak_meter_init_times(): for peak meters drawn elsewhere */
void peak_meter_get_times(int *release, int *hold_ticks)
{
*release = pm_peak_release;
*hold_ticks = pm_peak_hold;
}
#ifdef HAVE_RECORDING
/**
* Enable/disable clip counting

View file

@ -44,6 +44,7 @@ extern void peak_meter_set_clip_hold(int time);
extern void peak_meter_peek(void);
extern void peak_meter_init_range( bool dbfs, int range_min, int range_max);
extern void peak_meter_init_times(int release, int hold_ms, int clip_hold_sec);
extern void peak_meter_get_times(int *release, int *hold_ticks);
#ifdef HAVE_AGC
extern void peak_meter_get_peakhold(int *peak_left, int *peak_right);
#endif

View file

@ -810,6 +810,13 @@ display cycling round the defined sublines. See
specify an image.
\item[nobar] -- don't draw the bar or its frame (for use with the
``slider'' option).
\item[hold] -- for the peak meter bars (\config{\%pL}, \config{\%pR}):
the bar falls back at the peak meter's release rate instead of
following each drop, and the highest level is held for the peak hold
time. The next param may be a number, the thickness of the held peak in
pixels (default 1). With a fill image the held peak is drawn from that
part of the image, so for a bar drawn as LED segments set it to the size
of one segment.
\opt{touchscreen}{
\item[notouch] -- don't create the touchregion for progress/volume bars.
\item[touchlabel] -- the next option sets the label of the bar touch area, for linking with the \%Tl tag.