feat: add Music app features, fix Window Server bug

This commit is contained in:
2026-03-27 15:37:34 +01:00
parent 8f118c7a0f
commit 994c32c09d
5 changed files with 623 additions and 173 deletions
+10
View File
@@ -398,6 +398,16 @@ bool desktop_poll_external_windows(DesktopState* ds) {
for (int i = 0; i < ds->window_count; i++) {
if (ds->windows[i].external && ds->windows[i].ext_win_id == extId) {
found = true;
if (ds->windows[i].content_w != extWins[e].width ||
ds->windows[i].content_h != extWins[e].height) {
ds->windows[i].content_w = extWins[e].width;
ds->windows[i].content_h = extWins[e].height;
ds->windows[i].dirty = true;
if (ds->windows[i].state != WIN_MINIMIZED && ds->windows[i].state != WIN_CLOSED) {
changed = true;
}
}
montauk::strncpy(ds->windows[i].title, extWins[e].title, MAX_TITLE_LEN);
// Update dirty flag and cursor
if (extWins[e].dirty) {
ds->windows[i].dirty = true;
+327 -51
View File
@@ -45,6 +45,7 @@ static constexpr int TRANSPORT_H = 44;
static constexpr int MAX_FILES = 128;
static constexpr int MAX_PATH = 256;
static constexpr int MAX_FILTER = 64;
// Decode buffer: one MP3 frame = up to 1152 samples * 2 channels
static constexpr int PCM_BUF_SAMPLES = 1152 * 2;
@@ -62,6 +63,7 @@ static constexpr Color TRACK_BG = Color::from_rgb(0xDD, 0xDD, 0xDD);
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr Color LIST_HOVER = Color::from_rgb(0xE8, 0xF0, 0xFD);
static constexpr Color LIST_PLAYING = Color::from_rgb(0xD0, 0xE2, 0xFB);
static constexpr Color LIST_SELECTED = Color::from_rgb(0xF0, 0xF5, 0xFC);
static constexpr Color BTN_BG = Color::from_rgb(0xE8, 0xE8, 0xE8);
static constexpr Color BTN_HOVER = Color::from_rgb(0xD8, 0xD8, 0xD8);
static constexpr Color STOP_COLOR = Color::from_rgb(0xCC, 0x33, 0x33);
@@ -100,6 +102,11 @@ struct PlayerState {
// List UI
int scroll_y;
int hovered_item;
int selected_item;
int filtered_indices[MAX_FILES];
int filtered_count;
char filter_text[MAX_FILTER];
bool filter_active;
// Playback
PlayState play_state;
@@ -166,6 +173,8 @@ struct PlayerState {
static PlayerState g;
static int visible_items();
// ============================================================================
// Pixel helpers
// ============================================================================
@@ -366,6 +375,151 @@ static Rect visualizer_button_rect() {
return {shuffle.x - TOOL_ICON_GAP - TOOL_ICON_W, shuffle.y, TOOL_ICON_W, TOOL_ICON_H};
}
static char ascii_lower(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
}
static bool is_printable_ascii(char c) {
return c >= 32 && c < 127;
}
static bool str_contains_case_insensitive(const char* text, const char* needle) {
if (!needle || !needle[0]) return true;
if (!text || !text[0]) return false;
int text_len = montauk::slen(text);
int needle_len = montauk::slen(needle);
if (needle_len > text_len) return false;
for (int i = 0; i <= text_len - needle_len; i++) {
bool match = true;
for (int j = 0; j < needle_len; j++) {
if (ascii_lower(text[i + j]) != ascii_lower(needle[j])) {
match = false;
break;
}
}
if (match) return true;
}
return false;
}
static int filtered_raw_at(int visible_index) {
if (visible_index < 0 || visible_index >= g.filtered_count) return -1;
return g.filtered_indices[visible_index];
}
static bool filtered_contains(int raw_index) {
if (raw_index < 0) return false;
for (int i = 0; i < g.filtered_count; i++) {
if (g.filtered_indices[i] == raw_index) return true;
}
return false;
}
static int selected_visible_index() {
for (int i = 0; i < g.filtered_count; i++) {
if (g.filtered_indices[i] == g.selected_item) return i;
}
return -1;
}
static void clamp_list_scroll() {
int max_scroll = g.filtered_count - visible_items();
if (max_scroll < 0) max_scroll = 0;
if (g.scroll_y < 0) g.scroll_y = 0;
if (g.scroll_y > max_scroll) g.scroll_y = max_scroll;
}
static void ensure_selected_visible() {
int sel = selected_visible_index();
if (sel < 0) {
clamp_list_scroll();
return;
}
int visible = visible_items();
if (visible <= 0) {
g.scroll_y = 0;
return;
}
if (sel < g.scroll_y) g.scroll_y = sel;
if (sel >= g.scroll_y + visible) g.scroll_y = sel - visible + 1;
clamp_list_scroll();
}
static void rebuild_filtered_view() {
int prev_selected = g.selected_item;
g.filtered_count = 0;
for (int i = 0; i < g.file_count && g.filtered_count < MAX_FILES; i++) {
if (str_contains_case_insensitive(g.files[i].name, g.filter_text)) {
g.filtered_indices[g.filtered_count++] = i;
}
}
if (!filtered_contains(g.hovered_item)) g.hovered_item = -1;
if (g.filtered_count <= 0) {
g.selected_item = -1;
g.scroll_y = 0;
return;
}
if (filtered_contains(prev_selected)) {
g.selected_item = prev_selected;
} else if (filtered_contains(g.current_track)) {
g.selected_item = g.current_track;
} else {
g.selected_item = g.filtered_indices[0];
}
clamp_list_scroll();
ensure_selected_visible();
}
static void append_filter_char(char c) {
int len = montauk::slen(g.filter_text);
if (len >= MAX_FILTER - 1) return;
g.filter_text[len] = c;
g.filter_text[len + 1] = '\0';
rebuild_filtered_view();
}
static void pop_filter_char() {
int len = montauk::slen(g.filter_text);
if (len <= 0) return;
g.filter_text[len - 1] = '\0';
rebuild_filtered_view();
}
static void clear_filter() {
if (!g.filter_text[0]) return;
g.filter_text[0] = '\0';
rebuild_filtered_view();
}
static void move_selection(int delta) {
if (g.filtered_count <= 0) {
g.selected_item = -1;
g.scroll_y = 0;
return;
}
int sel = selected_visible_index();
if (sel < 0) {
sel = delta >= 0 ? 0 : g.filtered_count - 1;
} else {
sel += delta;
if (sel < 0) sel = 0;
if (sel >= g.filtered_count) sel = g.filtered_count - 1;
}
g.selected_item = g.filtered_indices[sel];
ensure_selected_visible();
}
// ============================================================================
// Directory scanning
// ============================================================================
@@ -389,6 +543,8 @@ static void scan_directory() {
f.is_mp3 = is_mp3;
g.file_count++;
}
rebuild_filtered_view();
}
// ============================================================================
@@ -680,6 +836,13 @@ static bool start_track(int index) {
g.mp3_seek_discard_samples = 0;
music_visualizer::reset(g.visualizer);
if (filtered_contains(index)) {
g.selected_item = index;
} else if (g.filtered_count > 0 && selected_visible_index() < 0) {
g.selected_item = g.filtered_indices[0];
}
ensure_selected_visible();
return true;
}
@@ -693,6 +856,16 @@ static void toggle_pause() {
}
}
static bool play_selected_or_default() {
if (g.selected_item >= 0 && filtered_contains(g.selected_item)) return start_track(g.selected_item);
if (g.filtered_count > 0) return start_track(g.filtered_indices[0]);
if (g.filter_text[0]) return false;
if (g.selected_item >= 0) return start_track(g.selected_item);
if (g.current_track >= 0) return start_track(g.current_track);
if (g.file_count > 0) return start_track(0);
return false;
}
static bool next_track(bool auto_advanced = false) {
if (g.file_count == 0) return false;
if (g.current_track < 0) return start_track(0);
@@ -880,9 +1053,45 @@ static void render(uint32_t* pixels) {
// Directory path in toolbar
{
int max_dir_w = visualizer_rect.x - 20;
if (g.filter_active || g.filter_text[0]) {
char filter_query[24];
char filter_label[48];
const char* query = g.filter_text;
int qlen = montauk::slen(query);
if (qlen > 15) {
montauk::memcpy(filter_query, "...", 3);
montauk::memcpy(filter_query + 3, query + qlen - 15, 15);
filter_query[18] = '\0';
query = filter_query;
}
if (g.filter_text[0]) {
snprintf(filter_label, sizeof(filter_label), "%s%s%s",
g.filter_active ? "Find: " : "Filter: ",
query,
g.filter_active ? "_" : "");
} else {
snprintf(filter_label, sizeof(filter_label), "%s",
g.filter_active ? "Find" : "Filter");
}
int pill_h = TOOL_ICON_H;
int pill_w = text_w(filter_label, FONT_SIZE_SM) + 14;
int pill_x = visualizer_rect.x - TOOL_ICON_GAP - pill_w;
int pill_y = (TOOLBAR_H - pill_h) / 2;
Color pill_bg = g.filter_active ? LIST_HOVER : BTN_BG;
Color pill_fg = g.filter_active ? ACCENT_DARK : DIM_TEXT;
if (pill_x > 80) {
px_fill_rounded(pixels, W, H, pill_x, pill_y, pill_w, pill_h, 6, pill_bg);
px_text(pixels, W, H, pill_x + 7, pill_y + (pill_h - fh_sm) / 2,
filter_label, pill_fg, FONT_SIZE_SM);
max_dir_w = pill_x - 12;
}
}
const char* display_dir = g.dir_path;
int dw = text_w(display_dir, FONT_SIZE_SM);
int max_dir_w = visualizer_rect.x - 20;
int dir_len = montauk::slen(g.dir_path);
if (dw > max_dir_w && dir_len > 20) {
display_dir = g.dir_path + montauk::slen(g.dir_path) - 20;
@@ -897,19 +1106,25 @@ static void render(uint32_t* pixels) {
px_hline(pixels, W, H, 0, list_end, W, BORDER_COLOR);
if (!g.show_visualizer) {
for (int i = 0; i < visible_count && (i + g.scroll_y) < g.file_count; i++) {
int idx = i + g.scroll_y;
for (int i = 0; i < visible_count && (i + g.scroll_y) < g.filtered_count; i++) {
int idx = filtered_raw_at(i + g.scroll_y);
if (idx < 0) break;
int iy = LIST_TOP + i * LIST_ITEM_H;
Color bg = BG_COLOR;
if (idx == g.current_track && g.play_state != PlayState::Stopped)
bg = LIST_PLAYING;
else if (idx == g.selected_item)
bg = LIST_SELECTED;
else if (idx == g.hovered_item)
bg = LIST_HOVER;
if (bg.r != BG_COLOR.r || bg.g != BG_COLOR.g || bg.b != BG_COLOR.b)
px_fill(pixels, W, H, 0, iy, W, LIST_ITEM_H, bg);
if (idx == g.selected_item && !(idx == g.current_track && g.play_state != PlayState::Stopped))
px_fill(pixels, W, H, 0, iy, 3, LIST_ITEM_H, ACCENT);
if (idx == g.current_track && g.play_state == PlayState::Playing) {
px_triangle_right(pixels, W, H, 8, iy + 8, 8, 12, ACCENT);
} else if (idx == g.current_track && g.play_state == PlayState::Paused) {
@@ -928,19 +1143,25 @@ static void render(uint32_t* pixels) {
tag, DIM_TEXT, FONT_SIZE_SM);
}
if (g.file_count > visible_count && visible_count > 0) {
if (g.filtered_count > visible_count && visible_count > 0) {
int list_h = list_end - LIST_TOP;
int sb_h = (visible_count * list_h) / g.file_count;
int sb_h = (visible_count * list_h) / g.filtered_count;
if (sb_h < 20) sb_h = 20;
int sb_y = LIST_TOP + (g.scroll_y * (list_h - sb_h)) / (g.file_count - visible_count);
int sb_y = LIST_TOP + (g.scroll_y * (list_h - sb_h)) / (g.filtered_count - visible_count);
px_fill_rounded(pixels, W, H, W - 6, sb_y, 4, sb_h, 2, TRACK_BG);
}
if (g.file_count == 0) {
const char* msg = "No audio files found";
if (g.filtered_count == 0) {
const char* msg = g.file_count == 0 ? "No audio files found" : "No matches for filter";
int mw = text_w(msg);
int cy = LIST_TOP + (list_end - LIST_TOP) / 2 - fh / 2;
px_text(pixels, W, H, (W - mw) / 2, cy, msg, DIM_TEXT);
if (g.file_count > 0 && g.filter_text[0]) {
char detail[96];
snprintf(detail, sizeof(detail), "Current filter: %s", g.filter_text);
int dw = text_w(detail, FONT_SIZE_SM);
px_text(pixels, W, H, (W - dw) / 2, cy + fh_sm + 6, detail, DIM_TEXT, FONT_SIZE_SM);
}
}
} else {
px_fill(pixels, W, H, content_panel.x, content_panel.y, content_panel.w, content_panel.h, TOOLBAR_BG);
@@ -1060,8 +1281,8 @@ static bool handle_click(int mx, int my) {
// File list click
if (!g.show_visualizer && my >= LIST_TOP && my < lb && mx >= 0 && mx < W) {
int idx = (my - LIST_TOP) / LIST_ITEM_H + g.scroll_y;
if (idx >= 0 && idx < g.file_count) {
int idx = filtered_raw_at((my - LIST_TOP) / LIST_ITEM_H + g.scroll_y);
if (idx >= 0) {
start_track(idx);
return true;
}
@@ -1095,10 +1316,8 @@ static bool handle_click(int mx, int my) {
// Play/Pause
if (mx >= bx && mx < bx + btn_w) {
if (g.play_state == PlayState::Stopped && g.current_track >= 0) {
start_track(g.current_track);
} else if (g.play_state == PlayState::Stopped && g.file_count > 0) {
start_track(0);
if (g.play_state == PlayState::Stopped) {
play_selected_or_default();
} else {
toggle_pause();
}
@@ -1118,6 +1337,94 @@ static bool handle_click(int mx, int my) {
return false;
}
static bool handle_key(const Montauk::KeyEvent& key, bool& quit) {
if (!key.pressed) return false;
if (key.scancode == 0x01) { // Escape
if (g.filter_active) {
if (g.filter_text[0]) clear_filter();
else g.filter_active = false;
return true;
}
quit = true;
return false;
}
if (key.ctrl && (key.ascii == 'f' || key.ascii == 'F' || key.ascii == 6)) {
g.filter_active = true;
return true;
}
if (!key.ctrl && !key.alt && key.ascii == '/') {
g.filter_active = true;
return true;
}
if (!g.show_visualizer && key.scancode == 0x48) { // Up
move_selection(-1);
return true;
}
if (!g.show_visualizer && key.scancode == 0x50) { // Down
move_selection(1);
return true;
}
if (!g.show_visualizer &&
(key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C)) {
if (g.selected_item >= 0) {
start_track(g.selected_item);
g.filter_active = false;
return true;
}
}
if (g.filter_active) {
if (key.ascii == '\b' || key.scancode == 0x0E) {
if (g.filter_text[0]) pop_filter_char();
else g.filter_active = false;
return true;
}
if (!key.ctrl && !key.alt && is_printable_ascii(key.ascii)) {
append_filter_char(key.ascii);
return true;
}
return false;
}
if (key.ascii == ' ') {
if (g.play_state == PlayState::Stopped)
play_selected_or_default();
else
toggle_pause();
return true;
}
if (key.ascii == 's' || key.ascii == 'S') {
stop_playback();
return true;
}
if (key.ascii == 'h' || key.ascii == 'H') {
g.shuffle_enabled = !g.shuffle_enabled;
return true;
}
if (key.ascii == 'r' || key.ascii == 'R') {
cycle_repeat_mode();
return true;
}
if (key.ascii == 'v' || key.ascii == 'V') {
g.show_visualizer = !g.show_visualizer;
return true;
}
if (key.scancode == 0x4D) { // Right arrow
next_track(false);
return true;
}
if (key.scancode == 0x4B) { // Left arrow
prev_track();
return true;
}
return false;
}
// ============================================================================
// Entry point
// ============================================================================
@@ -1131,6 +1438,7 @@ extern "C" void _start() {
g.current_track = -1;
g.play_state = PlayState::Stopped;
g.hovered_item = -1;
g.selected_item = -1;
g.show_visualizer = false;
g.repeat_mode = RepeatMode::Off;
g.rng_state = (uint32_t)montauk::get_milliseconds() ^ 0xC0DEFACEu;
@@ -1263,38 +1571,9 @@ extern "C" void _start() {
// Keyboard
if (ev.type == 0 && ev.key.pressed) {
if (ev.key.scancode == 0x01) { quit = true; break; }
if (ev.key.ascii == ' ') {
if (g.play_state == PlayState::Stopped && g.file_count > 0)
start_track(g.current_track >= 0 ? g.current_track : 0);
else
toggle_pause();
if (handle_key(ev.key, quit))
redraw = true;
}
if (ev.key.ascii == 's' || ev.key.ascii == 'S') {
stop_playback();
redraw = true;
}
if (ev.key.ascii == 'h' || ev.key.ascii == 'H') {
g.shuffle_enabled = !g.shuffle_enabled;
redraw = true;
}
if (ev.key.ascii == 'r' || ev.key.ascii == 'R') {
cycle_repeat_mode();
redraw = true;
}
if (ev.key.ascii == 'v' || ev.key.ascii == 'V') {
g.show_visualizer = !g.show_visualizer;
redraw = true;
}
if (ev.key.scancode == 0x4D) { // Right arrow
next_track(false);
redraw = true;
}
if (ev.key.scancode == 0x4B) { // Left arrow
prev_track();
redraw = true;
}
if (quit) break;
}
// Mouse
@@ -1325,8 +1604,7 @@ extern "C" void _start() {
// Hover tracking for file list
int lb = list_bottom();
if (!g.show_visualizer && ev.mouse.y >= LIST_TOP && ev.mouse.y < lb) {
int new_hover = (ev.mouse.y - LIST_TOP) / LIST_ITEM_H + g.scroll_y;
if (new_hover >= g.file_count) new_hover = -1;
int new_hover = filtered_raw_at((ev.mouse.y - LIST_TOP) / LIST_ITEM_H + g.scroll_y);
if (new_hover != g.hovered_item) {
g.hovered_item = new_hover;
redraw = true;
@@ -1339,10 +1617,7 @@ extern "C" void _start() {
// Scroll
if (!g.show_visualizer && ev.mouse.scroll != 0) {
g.scroll_y -= ev.mouse.scroll * 2;
int max_scroll = g.file_count - visible_items();
if (max_scroll < 0) max_scroll = 0;
if (g.scroll_y < 0) g.scroll_y = 0;
if (g.scroll_y > max_scroll) g.scroll_y = max_scroll;
clamp_list_scroll();
redraw = true;
}
}
@@ -1352,6 +1627,7 @@ extern "C" void _start() {
g.win_w = ev.resize.w;
g.win_h = ev.resize.h;
pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g.win_w, g.win_h);
ensure_selected_visible();
redraw = true;
}
}
+152 -65
View File
@@ -4,6 +4,8 @@ namespace music_visualizer {
using namespace gui;
static constexpr int SLICE_FRAMES = 256;
static void px_fill(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, Color c) {
if (w <= 0 || h <= 0) return;
@@ -30,62 +32,127 @@ static void px_hline(uint32_t* px, int bw, int bh,
}
}
void reset(State& state) {
for (int i = 0; i < BAR_COUNT; i++) {
state.levels[i] = 0;
state.peaks[i] = 0;
state.peak_hold[i] = 0;
static Color mix_color(Color a, Color b, int t) {
t = gui_clamp(t, 0, 255);
int inv_t = 255 - t;
return {
(uint8_t)((a.r * inv_t + b.r * t + 127) / 255),
(uint8_t)((a.g * inv_t + b.g * t + 127) / 255),
(uint8_t)((a.b * inv_t + b.b * t + 127) / 255),
255
};
}
static int ordered_index(const State& state, int order) {
if (state.history_count < WAVE_HISTORY) return order;
int idx = state.write_pos + order;
if (idx >= WAVE_HISTORY) idx -= WAVE_HISTORY;
return idx;
}
static void clear_accumulator(State& state) {
state.accum_frames = 0;
state.accum_abs_sum = 0;
state.accum_min = 32767;
state.accum_max = -32768;
}
static void push_slice(State& state, int min_sample, int max_sample, int avg_abs) {
if (min_sample > max_sample) return;
int idx = state.write_pos;
state.min_samples[idx] = (int16_t)min_sample;
state.max_samples[idx] = (int16_t)max_sample;
int scaled_energy = (avg_abs * 100) / 32768;
scaled_energy = (scaled_energy * 3) / 2;
if (scaled_energy > 100) scaled_energy = 100;
state.energy[idx] = (uint8_t)scaled_energy;
state.write_pos++;
if (state.write_pos >= WAVE_HISTORY) state.write_pos = 0;
if (state.history_count < WAVE_HISTORY) state.history_count++;
}
static void sample_history(const State& state, int column, int total_columns,
int& min_sample, int& max_sample, int& energy) {
if (state.history_count <= 0) {
min_sample = 0;
max_sample = 0;
energy = 0;
return;
}
if (state.history_count == 1 || total_columns <= 1) {
int idx = ordered_index(state, state.history_count - 1);
min_sample = state.min_samples[idx];
max_sample = state.max_samples[idx];
energy = state.energy[idx];
return;
}
int denom = total_columns - 1;
int max_order = state.history_count - 1;
int scaled = column * max_order * 256;
int base = scaled / denom;
int frac = base & 0xFF;
int order0 = base >> 8;
int order1 = order0 < max_order ? order0 + 1 : order0;
int idx0 = ordered_index(state, order0);
int idx1 = ordered_index(state, order1);
int min0 = state.min_samples[idx0];
int min1 = state.min_samples[idx1];
int max0 = state.max_samples[idx0];
int max1 = state.max_samples[idx1];
int e0 = state.energy[idx0];
int e1 = state.energy[idx1];
min_sample = (min0 * (256 - frac) + min1 * frac) / 256;
max_sample = (max0 * (256 - frac) + max1 * frac) / 256;
energy = (e0 * (256 - frac) + e1 * frac) / 256;
}
void reset(State& state) {
for (int i = 0; i < WAVE_HISTORY; i++) {
state.min_samples[i] = 0;
state.max_samples[i] = 0;
state.energy[i] = 0;
}
state.write_pos = 0;
state.history_count = 0;
clear_accumulator(state);
}
void tick(State& state) {
for (int i = 0; i < BAR_COUNT; i++) {
if (state.levels[i] > 0) {
state.levels[i] = state.levels[i] > 3 ? (uint8_t)(state.levels[i] - 3) : 0;
}
if (state.peak_hold[i] < 6) {
state.peak_hold[i]++;
} else if (state.peaks[i] > 0) {
state.peaks[i] = state.peaks[i] > 2 ? (uint8_t)(state.peaks[i] - 2) : 0;
}
}
(void)state;
}
void feed_pcm(State& state, const int16_t* pcm, int frames, int channels) {
if (!pcm || frames <= 0 || channels <= 0) return;
int local_max[BAR_COUNT] = {};
for (int i = 0; i < frames; i++) {
int bar = (i * BAR_COUNT) / frames;
if (bar < 0) bar = 0;
if (bar >= BAR_COUNT) bar = BAR_COUNT - 1;
int amp = 0;
int mono = 0;
for (int ch = 0; ch < channels; ch++) {
int sample = pcm[i * channels + ch];
if (sample < 0) sample = -sample;
amp += sample;
mono += pcm[i * channels + ch];
}
amp /= channels;
if (amp > local_max[bar]) local_max[bar] = amp;
}
mono /= channels;
for (int i = 0; i < BAR_COUNT; i++) {
int scaled = (local_max[i] * 100) / 32768;
scaled = (scaled * 3) / 2;
if (scaled > 100) scaled = 100;
if (mono < state.accum_min) state.accum_min = mono;
if (mono > state.accum_max) state.accum_max = mono;
int blended = scaled;
if (scaled < state.levels[i]) {
blended = (state.levels[i] * 3 + scaled) / 4;
int abs_mono = mono;
if (abs_mono < 0) {
abs_mono = abs_mono == -32768 ? 32768 : -abs_mono;
}
state.accum_abs_sum += abs_mono;
state.accum_frames++;
if (blended > state.levels[i]) {
state.levels[i] = (uint8_t)blended;
}
if (state.levels[i] > state.peaks[i]) {
state.peaks[i] = state.levels[i];
state.peak_hold[i] = 0;
if (state.accum_frames >= SLICE_FRAMES) {
int avg_abs = state.accum_abs_sum / state.accum_frames;
push_slice(state, state.accum_min, state.accum_max, avg_abs);
clear_accumulator(state);
}
}
}
@@ -95,7 +162,13 @@ void render(uint32_t* pixels, int bw, int bh, const Rect& rect,
Color track_bg, Color border) {
if (!pixels || rect.w <= 0 || rect.h <= 0) return;
px_fill(pixels, bw, bh, rect.x, rect.y, rect.w, rect.h, Color::from_rgb(0xFA, 0xFB, 0xFD));
Color panel_bg = Color::from_rgb(0xFA, 0xFB, 0xFD);
Color guide = mix_color(panel_bg, track_bg, 144);
Color guide_faint = mix_color(panel_bg, track_bg, 72);
Color inner_base = mix_color(Color::from_rgb(0xE9, 0xF2, 0xFF), accent, 64);
Color outer_base = mix_color(track_bg, accent_dark, 72);
px_fill(pixels, bw, bh, rect.x, rect.y, rect.w, rect.h, panel_bg);
px_hline(pixels, bw, bh, rect.x, rect.y, rect.w, border);
px_hline(pixels, bw, bh, rect.x, rect.y + rect.h - 1, rect.w, border);
@@ -105,35 +178,49 @@ void render(uint32_t* pixels, int bw, int bh, const Rect& rect,
int inner_h = rect.h - 16;
if (inner_w <= 0 || inner_h <= 0) return;
int gap = 2;
int bar_w = (inner_w - gap * (BAR_COUNT - 1)) / BAR_COUNT;
if (bar_w < 2) bar_w = 2;
int bars_w = bar_w * BAR_COUNT + gap * (BAR_COUNT - 1);
int start_x = inner_x + (inner_w - bars_w) / 2;
int base_y = inner_y + inner_h - 1;
int usable_h = inner_h - 6;
if (usable_h < 8) usable_h = 8;
int center_y = inner_y + inner_h / 2;
int amplitude = inner_h / 2 - 10;
if (amplitude < 12) amplitude = 12;
px_hline(pixels, bw, bh, inner_x, base_y, inner_w, track_bg);
px_hline(pixels, bw, bh, inner_x, center_y, inner_w, guide);
px_hline(pixels, bw, bh, inner_x, center_y - amplitude / 2, inner_w, guide_faint);
px_hline(pixels, bw, bh, inner_x, center_y + amplitude / 2, inner_w, guide_faint);
for (int i = 0; i < BAR_COUNT; i++) {
int x = start_x + i * (bar_w + gap);
int bar_h = (state.levels[i] * usable_h) / 100;
if (bar_h < 2 && state.levels[i] > 0) bar_h = 2;
if (state.history_count <= 0) return;
if (bar_h > 0) {
int y = base_y - bar_h;
int top_h = bar_h / 3;
if (top_h < 2) top_h = 2;
if (top_h > bar_h) top_h = bar_h;
px_fill(pixels, bw, bh, x, y + top_h, bar_w, bar_h - top_h, accent);
px_fill(pixels, bw, bh, x, y, bar_w, top_h, accent_dark);
for (int col = 0; col < inner_w; col++) {
int min_sample, max_sample, energy;
sample_history(state, col, inner_w, min_sample, max_sample, energy);
int top = center_y - (max_sample * amplitude) / 32768;
int bottom = center_y - (min_sample * amplitude) / 32768;
if (top > bottom) {
int swap = top;
top = bottom;
bottom = swap;
}
int peak_h = (state.peaks[i] * usable_h) / 100;
int peak_y = base_y - peak_h;
if (peak_y < inner_y) peak_y = inner_y;
px_fill(pixels, bw, bh, x, peak_y, bar_w, 2, border);
int pad = 1 + energy / 28;
top -= pad;
bottom += pad;
top = gui_clamp(top, inner_y, inner_y + inner_h - 1);
bottom = gui_clamp(bottom, inner_y, inner_y + inner_h - 1);
if (bottom < top) bottom = top;
int outer_t = 48 + (col * 160) / (inner_w > 1 ? (inner_w - 1) : 1);
int inner_t = 80 + (col * 175) / (inner_w > 1 ? (inner_w - 1) : 1);
Color outer = mix_color(outer_base, accent_dark, outer_t);
Color inner = mix_color(inner_base, accent, inner_t);
int x = inner_x + col;
px_fill(pixels, bw, bh, x, top, 1, bottom - top + 1, outer);
int inner_top = top + 1;
int inner_bottom = bottom - 1;
if (inner_bottom >= inner_top) {
px_fill(pixels, bw, bh, x, inner_top, 1, inner_bottom - inner_top + 1, inner);
}
}
}
+10 -4
View File
@@ -5,12 +5,18 @@
namespace music_visualizer {
static constexpr int BAR_COUNT = 24;
static constexpr int WAVE_HISTORY = 192;
struct State {
uint8_t levels[BAR_COUNT];
uint8_t peaks[BAR_COUNT];
uint8_t peak_hold[BAR_COUNT];
int16_t min_samples[WAVE_HISTORY];
int16_t max_samples[WAVE_HISTORY];
uint8_t energy[WAVE_HISTORY];
int write_pos;
int history_count;
int accum_frames;
int accum_abs_sum;
int accum_min;
int accum_max;
};
void reset(State& state);