Files
AudioMixer/ui_mixer.c
T
2026-09-07 16:53:18 +03:00

568 lines
19 KiB
C

#include "ui_mixer.h"
#include "lcd.h"
#include "lvgl.h"
#include "src/draw/sw/lv_draw_sw_utils.h"
#include "pico/stdlib.h"
#include <stdio.h>
#include <string.h>
#define UI_HEADER_H 56
#define UI_LIST_TOP 58
#define UI_ROW_H 45
#define UI_MAX_ROWS 4
#define UI_BUF_LINES 30
#define UI_SB_W 8
#define UI_SB_PAD 4
#define UI_CONTENT_R (UI_SB_W + UI_SB_PAD + 2) /* right inset for list content */
#define COL_BG lv_color_make(10, 14, 20)
#define COL_PANEL lv_color_make(22, 30, 42)
#define COL_SEL lv_color_make(28, 48, 72)
#define COL_ACCENT lv_color_make(0, 200, 210)
#define COL_ACCENT2 lv_color_make(255, 170, 40)
#define COL_MUTED lv_color_make(120, 130, 145)
#define COL_BAR_BG lv_color_make(40, 52, 68)
#define COL_BAR_HI lv_color_make(80, 230, 170)
#define COL_VU lv_color_make(255, 200, 60)
#define COL_VU_HOT lv_color_make(255, 90, 70)
#define COL_TEXT lv_color_make(235, 240, 245)
typedef struct {
lv_obj_t *row;
lv_obj_t *name;
lv_obj_t *pct;
lv_obj_t *vu;
lv_obj_t *bar;
} ui_row_t;
static lv_display_t *s_disp;
static uint8_t s_draw_buf[LCD_WIDTH * UI_BUF_LINES * 2];
static lv_obj_t *s_scr;
static lv_obj_t *s_wait;
static lv_obj_t *s_main;
static lv_obj_t *s_master_pct;
static lv_obj_t *s_master_vu;
static lv_obj_t *s_master_bar;
static lv_obj_t *s_empty;
static lv_obj_t *s_caret;
static lv_obj_t *s_sb_track;
static lv_obj_t *s_sb_thumb;
static ui_row_t s_rows[UI_MAX_ROWS];
static bool s_waiting = true;
static uint8_t s_master = 0xFF;
static uint8_t s_master_peak = 0xFF;
static uint8_t s_n = 0xFF;
static uint8_t s_sel = 0xFF;
static uint8_t s_page = 0xFF;
static uint8_t s_count = 0xFF;
static char s_names[AUSIO_MAX_VISIBLE][AUSIO_NAME_CHARS + 1];
static uint8_t s_vols[AUSIO_MAX_VISIBLE];
static uint8_t s_peaks[AUSIO_MAX_VISIBLE];
static absolute_time_t s_last_tick;
static int16_t s_caret_y;
static void flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map)
{
int32_t w = area->x2 - area->x1 + 1;
int32_t h = area->y2 - area->y1 + 1;
uint32_t stride = lv_draw_buf_width_to_stride((uint32_t)w, lv_display_get_color_format(disp));
/* SPI wants big-endian RGB565 bytes (same as lcd_draw_char). */
if (stride == (uint32_t)w * 2u) {
lv_draw_sw_rgb565_swap(px_map, (uint32_t)(w * h));
} else {
for (int32_t row = 0; row < h; row++) {
lv_draw_sw_rgb565_swap(px_map + (size_t)row * stride, (uint32_t)w);
}
}
lcd_blit((int16_t)area->x1, (int16_t)area->y1, (int16_t)w, (int16_t)h, px_map,
(int32_t)stride);
lv_display_flush_ready(disp);
}
static void style_bar(lv_obj_t *bar, lv_color_t fill)
{
lv_obj_set_style_bg_color(bar, COL_BAR_BG, LV_PART_MAIN);
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_radius(bar, 4, LV_PART_MAIN);
lv_obj_set_style_pad_all(bar, 0, LV_PART_MAIN);
lv_obj_set_style_bg_color(bar, fill, LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_INDICATOR);
lv_obj_set_style_radius(bar, 4, LV_PART_INDICATOR);
lv_bar_set_range(bar, 0, 100);
}
static void style_vu(lv_obj_t *bar)
{
style_bar(bar, COL_VU);
lv_obj_set_style_radius(bar, 2, LV_PART_MAIN);
lv_obj_set_style_radius(bar, 2, LV_PART_INDICATOR);
}
static void set_vu_value(lv_obj_t *bar, uint8_t peak)
{
lv_color_t c = (peak >= 90) ? COL_VU_HOT : COL_VU;
lv_obj_set_style_bg_color(bar, c, LV_PART_INDICATOR);
lv_bar_set_value(bar, peak, LV_ANIM_OFF);
}
static void anim_caret_y(void *obj, int32_t v)
{
lv_obj_set_y((lv_obj_t *)obj, (int32_t)v);
s_caret_y = (int16_t)v;
}
static void move_caret_to(uint8_t row_idx, bool animate)
{
int16_t target = (int16_t)(UI_LIST_TOP + (int16_t)row_idx * UI_ROW_H + 8);
if (!animate) {
lv_anim_delete(s_caret, anim_caret_y);
lv_obj_set_y(s_caret, target);
s_caret_y = target;
return;
}
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, s_caret);
lv_anim_set_values(&a, s_caret_y, target);
lv_anim_set_duration(&a, 180);
lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
lv_anim_set_exec_cb(&a, anim_caret_y);
lv_anim_start(&a);
}
static void update_scrollbar(uint8_t total, uint8_t page_start, uint8_t visible)
{
if (total <= visible || visible == 0) {
lv_obj_add_flag(s_sb_track, LV_OBJ_FLAG_HIDDEN);
return;
}
lv_obj_clear_flag(s_sb_track, LV_OBJ_FLAG_HIDDEN);
const int32_t track_h = lv_obj_get_height(s_sb_track);
/* Thumb shows the visible window; at least ~1/4 of track so it's obvious. */
int32_t thumb_h = (track_h * (int32_t)visible) / (int32_t)total;
int32_t min_h = track_h / 4;
if (min_h < 18) {
min_h = 18;
}
if (thumb_h < min_h) {
thumb_h = min_h;
}
if (thumb_h > track_h) {
thumb_h = track_h;
}
uint8_t max_start = (uint8_t)(total - visible);
int32_t travel = track_h - thumb_h;
int32_t thumb_y = 0;
if (max_start > 0 && travel > 0) {
if (page_start > max_start) {
page_start = max_start;
}
thumb_y = (travel * (int32_t)page_start) / (int32_t)max_start;
}
lv_obj_set_height(s_sb_thumb, thumb_h);
lv_obj_set_y(s_sb_thumb, thumb_y);
}
static void build_wait_screen(void)
{
s_wait = lv_obj_create(s_scr);
lv_obj_set_size(s_wait, LCD_WIDTH, LCD_HEIGHT);
lv_obj_set_style_bg_color(s_wait, COL_BG, 0);
lv_obj_set_style_bg_opa(s_wait, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(s_wait, 0, 0);
lv_obj_set_style_pad_all(s_wait, 0, 0);
lv_obj_set_style_radius(s_wait, 0, 0);
lv_obj_clear_flag(s_wait, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *top = lv_obj_create(s_wait);
lv_obj_set_size(top, LCD_WIDTH, 3);
lv_obj_set_pos(top, 0, 0);
lv_obj_set_style_bg_color(top, COL_ACCENT, 0);
lv_obj_set_style_bg_opa(top, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(top, 0, 0);
lv_obj_set_style_radius(top, 0, 0);
lv_obj_clear_flag(top, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *spin = lv_spinner_create(s_wait);
lv_obj_set_size(spin, 42, 42);
lv_obj_align(spin, LV_ALIGN_CENTER, 0, -28);
lv_obj_set_style_arc_color(spin, COL_PANEL, LV_PART_MAIN);
lv_obj_set_style_arc_width(spin, 4, LV_PART_MAIN);
lv_obj_set_style_arc_color(spin, COL_ACCENT, LV_PART_INDICATOR);
lv_obj_set_style_arc_width(spin, 4, LV_PART_INDICATOR);
lv_obj_t *l1 = lv_label_create(s_wait);
lv_label_set_text(l1, "Waiting for host");
lv_obj_set_style_text_color(l1, COL_MUTED, 0);
lv_obj_set_style_text_font(l1, &lv_font_montserrat_16, 0);
lv_obj_align(l1, LV_ALIGN_CENTER, 0, 28);
lv_obj_t *l2 = lv_label_create(s_wait);
lv_label_set_text(l2, "Ausiomixer");
lv_obj_set_style_text_color(l2, COL_ACCENT, 0);
lv_obj_set_style_text_font(l2, &lv_font_montserrat_20, 0);
lv_obj_align(l2, LV_ALIGN_CENTER, 0, 54);
}
static void build_main_screen(void)
{
s_main = lv_obj_create(s_scr);
lv_obj_set_size(s_main, LCD_WIDTH, LCD_HEIGHT);
lv_obj_set_style_bg_color(s_main, COL_BG, 0);
lv_obj_set_style_bg_opa(s_main, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(s_main, 0, 0);
lv_obj_set_style_pad_all(s_main, 0, 0);
lv_obj_set_style_radius(s_main, 0, 0);
lv_obj_clear_flag(s_main, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(s_main, LV_OBJ_FLAG_HIDDEN);
lv_obj_t *header = lv_obj_create(s_main);
lv_obj_set_size(header, LCD_WIDTH, UI_HEADER_H);
lv_obj_set_pos(header, 0, 0);
lv_obj_set_style_bg_color(header, COL_PANEL, 0);
lv_obj_set_style_bg_opa(header, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(header, 0, 0);
lv_obj_set_style_pad_all(header, 0, 0);
lv_obj_set_style_radius(header, 0, 0);
lv_obj_clear_flag(header, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *accent = lv_obj_create(header);
lv_obj_set_size(accent, LCD_WIDTH, 2);
lv_obj_set_pos(accent, 0, 0);
lv_obj_set_style_bg_color(accent, COL_ACCENT, 0);
lv_obj_set_style_bg_opa(accent, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(accent, 0, 0);
lv_obj_set_style_radius(accent, 0, 0);
lv_obj_t *ml = lv_label_create(header);
lv_label_set_text(ml, "MASTER");
lv_obj_set_style_text_color(ml, COL_ACCENT, 0);
lv_obj_set_style_text_font(ml, &lv_font_montserrat_14, 0);
lv_obj_set_pos(ml, 10, 8);
s_master_pct = lv_label_create(header);
lv_label_set_text(s_master_pct, "0%");
lv_obj_set_style_text_color(s_master_pct, COL_TEXT, 0);
lv_obj_set_style_text_font(s_master_pct, &lv_font_montserrat_28, 0);
lv_obj_align(s_master_pct, LV_ALIGN_TOP_RIGHT, -10, 2);
s_master_vu = lv_bar_create(header);
lv_obj_set_size(s_master_vu, LCD_WIDTH - 20, 5);
lv_obj_set_pos(s_master_vu, 10, 30);
style_vu(s_master_vu);
s_master_bar = lv_bar_create(header);
lv_obj_set_size(s_master_bar, LCD_WIDTH - 20, 12);
lv_obj_set_pos(s_master_bar, 10, 38);
style_bar(s_master_bar, COL_BAR_HI);
for (uint8_t i = 0; i < UI_MAX_ROWS; i++) {
int16_t y = (int16_t)(UI_LIST_TOP + (int16_t)i * UI_ROW_H);
ui_row_t *r = &s_rows[i];
r->row = lv_obj_create(s_main);
lv_obj_set_size(r->row, LCD_WIDTH, UI_ROW_H - 1);
lv_obj_set_pos(r->row, 0, y);
lv_obj_set_style_bg_color(r->row, COL_BG, 0);
lv_obj_set_style_bg_opa(r->row, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(r->row, 0, 0);
lv_obj_set_style_pad_all(r->row, 0, 0);
lv_obj_set_style_radius(r->row, 0, 0);
lv_obj_clear_flag(r->row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(r->row, LV_OBJ_FLAG_HIDDEN);
r->name = lv_label_create(r->row);
lv_label_set_long_mode(r->name, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_width(r->name, LCD_WIDTH - 14 - 64 - UI_CONTENT_R);
lv_obj_set_style_text_color(r->name, COL_ACCENT2, 0);
lv_obj_set_style_text_font(r->name, &lv_font_montserrat_16, 0);
lv_obj_set_pos(r->name, 14, 2);
lv_label_set_text(r->name, "");
r->pct = lv_label_create(r->row);
lv_label_set_text(r->pct, "0%");
lv_obj_set_style_text_color(r->pct, COL_MUTED, 0);
lv_obj_set_style_text_font(r->pct, &lv_font_montserrat_14, 0);
lv_obj_align(r->pct, LV_ALIGN_TOP_RIGHT, -(10 + UI_CONTENT_R), 4);
r->vu = lv_bar_create(r->row);
lv_obj_set_size(r->vu, LCD_WIDTH - 28 - UI_CONTENT_R, 5);
lv_obj_set_pos(r->vu, 14, 22);
style_vu(r->vu);
r->bar = lv_bar_create(r->row);
lv_obj_set_size(r->bar, LCD_WIDTH - 28 - UI_CONTENT_R, 9);
lv_obj_set_pos(r->bar, 14, 30);
style_bar(r->bar, COL_BAR_HI);
}
s_empty = lv_label_create(s_main);
lv_label_set_text(s_empty, "No audio apps");
lv_obj_set_style_text_color(s_empty, COL_MUTED, 0);
lv_obj_set_style_text_font(s_empty, &lv_font_montserrat_16, 0);
lv_obj_align(s_empty, LV_ALIGN_CENTER, 0, 20);
lv_obj_add_flag(s_empty, LV_OBJ_FLAG_HIDDEN);
s_caret = lv_obj_create(s_main);
lv_obj_set_size(s_caret, 4, UI_ROW_H - 16);
lv_obj_set_pos(s_caret, 0, UI_LIST_TOP + 8);
lv_obj_set_style_bg_color(s_caret, COL_ACCENT, 0);
lv_obj_set_style_bg_opa(s_caret, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(s_caret, 0, 0);
lv_obj_set_style_radius(s_caret, 2, 0);
lv_obj_clear_flag(s_caret, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(s_caret, LV_OBJ_FLAG_HIDDEN);
s_caret_y = UI_LIST_TOP + 8;
/* List scrollbar — visible only when session_count > visible rows. */
{
const int16_t list_h = (int16_t)(UI_MAX_ROWS * UI_ROW_H);
s_sb_track = lv_obj_create(s_main);
lv_obj_set_size(s_sb_track, UI_SB_W, list_h - 12);
lv_obj_set_pos(s_sb_track, LCD_WIDTH - UI_SB_W - UI_SB_PAD, UI_LIST_TOP + 6);
lv_obj_set_style_bg_color(s_sb_track, COL_BAR_BG, 0);
lv_obj_set_style_bg_opa(s_sb_track, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(s_sb_track, 0, 0);
lv_obj_set_style_radius(s_sb_track, 4, 0);
lv_obj_set_style_pad_all(s_sb_track, 0, 0);
lv_obj_clear_flag(s_sb_track, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(s_sb_track, LV_OBJ_FLAG_HIDDEN);
s_sb_thumb = lv_obj_create(s_sb_track);
lv_obj_set_size(s_sb_thumb, UI_SB_W, 40);
lv_obj_set_pos(s_sb_thumb, 0, 0);
lv_obj_set_style_bg_color(s_sb_thumb, COL_ACCENT, 0);
lv_obj_set_style_bg_opa(s_sb_thumb, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(s_sb_thumb, 0, 0);
lv_obj_set_style_radius(s_sb_thumb, 4, 0);
lv_obj_set_style_pad_all(s_sb_thumb, 0, 0);
lv_obj_clear_flag(s_sb_thumb, LV_OBJ_FLAG_SCROLLABLE);
}
}
static void show_waiting(bool on)
{
if (on) {
lv_obj_clear_flag(s_wait, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(s_main, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(s_wait, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(s_main, LV_OBJ_FLAG_HIDDEN);
}
s_waiting = on;
}
static void set_row_selected(ui_row_t *r, bool sel)
{
lv_obj_set_style_bg_color(r->row, sel ? COL_SEL : COL_BG, 0);
lv_obj_set_style_text_color(r->name, sel ? COL_TEXT : COL_ACCENT2, 0);
lv_obj_set_style_text_color(r->pct, sel ? COL_ACCENT : COL_MUTED, 0);
lv_obj_set_style_bg_color(r->bar, sel ? COL_ACCENT : COL_BAR_HI, LV_PART_INDICATOR);
}
void ui_mixer_init(void)
{
lv_init();
s_disp = lv_display_create(LCD_WIDTH, LCD_HEIGHT);
lv_display_set_flush_cb(s_disp, flush_cb);
lv_display_set_buffers(s_disp, s_draw_buf, NULL, sizeof(s_draw_buf),
LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_display_set_color_format(s_disp, LV_COLOR_FORMAT_RGB565);
s_scr = lv_screen_active();
lv_obj_set_style_bg_color(s_scr, COL_BG, 0);
lv_obj_set_style_bg_opa(s_scr, LV_OPA_COVER, 0);
build_wait_screen();
build_main_screen();
show_waiting(true);
s_last_tick = get_absolute_time();
s_master = 0xFF;
s_master_peak = 0xFF;
s_n = 0xFF;
s_sel = 0xFF;
s_page = 0xFF;
s_count = 0xFF;
memset(s_names, 0, sizeof(s_names));
memset(s_vols, 0, sizeof(s_vols));
memset(s_peaks, 0, sizeof(s_peaks));
/* First paint */
lv_timer_handler();
}
void ui_mixer_task(void)
{
absolute_time_t now = get_absolute_time();
uint32_t elapsed = (uint32_t)absolute_time_diff_us(s_last_tick, now) / 1000u;
if (elapsed > 0) {
if (elapsed > 50) {
elapsed = 50;
}
lv_tick_inc(elapsed);
s_last_tick = now;
}
lv_timer_handler();
}
bool ui_mixer_needs_tick(void)
{
return true;
}
void ui_mixer_draw(const mixer_state_t *st)
{
if (!st) {
return;
}
if (!st->host_alive) {
if (!s_waiting) {
show_waiting(true);
s_master = 0xFF;
s_master_peak = 0xFF;
s_n = 0xFF;
s_sel = 0xFF;
s_count = 0xFF;
}
return;
}
if (s_waiting) {
show_waiting(false);
s_master = 0xFF;
s_master_peak = 0xFF;
s_n = 0xFF;
s_sel = 0xFF;
s_page = 0xFF;
s_count = 0xFF;
}
uint8_t n = st->n_visible;
if (n > UI_MAX_ROWS) {
n = UI_MAX_ROWS;
}
if (st->master_vol != s_master) {
char buf[8];
snprintf(buf, sizeof(buf), "%u%%", st->master_vol);
lv_label_set_text(s_master_pct, buf);
lv_bar_set_value(s_master_bar, st->master_vol, LV_ANIM_ON);
s_master = st->master_vol;
}
if (st->master_peak != s_master_peak) {
set_vu_value(s_master_vu, st->master_peak);
s_master_peak = st->master_peak;
}
bool list_changed = (n != s_n) || (st->selected != s_sel) ||
(st->page_start != s_page) || (st->session_count != s_count);
if (!list_changed) {
for (uint8_t i = 0; i < n; i++) {
if (s_vols[i] != st->visible[i].vol ||
strncmp(s_names[i], st->visible[i].name, AUSIO_NAME_CHARS) != 0) {
list_changed = true;
break;
}
}
}
bool peaks_changed = false;
if (!list_changed && s_n != 0xFF) {
for (uint8_t i = 0; i < n; i++) {
if (s_peaks[i] != st->visible[i].peak) {
peaks_changed = true;
break;
}
}
if (!peaks_changed) {
return;
}
for (uint8_t i = 0; i < n; i++) {
if (s_peaks[i] != st->visible[i].peak) {
set_vu_value(s_rows[i].vu, st->visible[i].peak);
s_peaks[i] = st->visible[i].peak;
}
}
return;
}
if (n == 0) {
for (uint8_t i = 0; i < UI_MAX_ROWS; i++) {
lv_obj_add_flag(s_rows[i].row, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_add_flag(s_caret, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(s_sb_track, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(s_empty, LV_OBJ_FLAG_HIDDEN);
set_vu_value(s_master_vu, 0);
s_master_peak = 0;
} else {
lv_obj_add_flag(s_empty, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(s_caret, LV_OBJ_FLAG_HIDDEN);
int rel = (int)st->selected - (int)st->page_start;
if (rel < 0) {
rel = 0;
}
if (rel >= (int)n) {
rel = (int)n - 1;
}
bool sel_moved = (st->selected != s_sel) || (st->page_start != s_page) || (s_n == 0xFF);
bool first = (s_n == 0xFF);
for (uint8_t i = 0; i < UI_MAX_ROWS; i++) {
ui_row_t *r = &s_rows[i];
if (i >= n) {
lv_obj_add_flag(r->row, LV_OBJ_FLAG_HIDDEN);
continue;
}
lv_obj_clear_flag(r->row, LV_OBJ_FLAG_HIDDEN);
char name[AUSIO_NAME_CHARS + 1];
memcpy(name, st->visible[i].name, AUSIO_NAME_CHARS);
name[AUSIO_NAME_CHARS] = '\0';
if (strncmp(s_names[i], name, AUSIO_NAME_CHARS + 1) != 0 || s_n == 0xFF) {
lv_label_set_text(r->name, name);
memcpy(s_names[i], name, sizeof(name));
}
if (s_vols[i] != st->visible[i].vol || s_n == 0xFF) {
char pct[8];
snprintf(pct, sizeof(pct), "%u%%", st->visible[i].vol);
lv_label_set_text(r->pct, pct);
lv_bar_set_value(r->bar, st->visible[i].vol, first ? LV_ANIM_OFF : LV_ANIM_ON);
s_vols[i] = st->visible[i].vol;
}
set_vu_value(r->vu, st->visible[i].peak);
s_peaks[i] = st->visible[i].peak;
bool sel = ((int)i == rel);
set_row_selected(r, sel);
}
move_caret_to((uint8_t)rel, !first && sel_moved);
update_scrollbar(st->session_count, st->page_start, n);
}
s_n = n;
s_sel = st->selected;
s_page = st->page_start;
s_count = st->session_count;
}