From 1ad9117fc1a0d9e9ca3f0a15c43a1d87022e2546 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Wed, 25 Mar 2026 19:13:26 +0100 Subject: [PATCH] fix: music app improvements, desktop performance improvements --- programs/src/desktop/apps/app_klog.cpp | 1 + programs/src/desktop/apps/app_procmgr.cpp | 2 + programs/src/desktop/apps/app_terminal.cpp | 5 + programs/src/desktop/apps/apps_common.hpp | 2 +- programs/src/desktop/main.cpp | 100 +++- programs/src/login/main.cpp | 25 +- programs/src/music/Makefile | 2 +- programs/src/music/main.cpp | 518 +++++++++++++------ programs/src/music/visualizer.cpp | 140 +++++ programs/src/music/visualizer.hpp | 23 + scripts/copy_icons.sh | 7 + template/sysroot/include/gui/framebuffer.hpp | 11 +- 12 files changed, 664 insertions(+), 172 deletions(-) create mode 100644 programs/src/music/visualizer.cpp create mode 100644 programs/src/music/visualizer.hpp diff --git a/programs/src/desktop/apps/app_klog.cpp b/programs/src/desktop/apps/app_klog.cpp index 30d10db..dee6d00 100644 --- a/programs/src/desktop/apps/app_klog.cpp +++ b/programs/src/desktop/apps/app_klog.cpp @@ -145,6 +145,7 @@ static void klog_on_poll(Window* win) { klog->last_len = n; if (n > 0) klog->last_tail_byte = klog->klog_buf[n - 1]; + win->dirty = true; } static void klog_on_close(Window* win) { diff --git a/programs/src/desktop/apps/app_procmgr.cpp b/programs/src/desktop/apps/app_procmgr.cpp index 7963c14..abb547f 100644 --- a/programs/src/desktop/apps/app_procmgr.cpp +++ b/programs/src/desktop/apps/app_procmgr.cpp @@ -54,6 +54,8 @@ static void procmgr_on_poll(Window* win) { } } } + + win->dirty = true; } static void procmgr_on_draw(Window* win, Framebuffer& fb) { diff --git a/programs/src/desktop/apps/app_terminal.cpp b/programs/src/desktop/apps/app_terminal.cpp index 03ef628..6df2e0d 100644 --- a/programs/src/desktop/apps/app_terminal.cpp +++ b/programs/src/desktop/apps/app_terminal.cpp @@ -311,12 +311,17 @@ static void terminal_on_poll(Window* win) { if (!terminal_poll(tts->tabs[i])) { // This tab's shell exited term_close_tab(tts, win, i); + win->dirty = true; // term_close_tab may have closed the window (last tab). // desktop_close_window calls on_close which nulls app_data // before the window slot is recycled, so win is still valid here. if (!win->app_data) return; } } + + if (tts->tab_count > 0 && tts->tabs[tts->active_tab]->dirty) { + win->dirty = true; + } } // ============================================================================ diff --git a/programs/src/desktop/apps/apps_common.hpp b/programs/src/desktop/apps/apps_common.hpp index 647bb50..9f2a6b3 100644 --- a/programs/src/desktop/apps/apps_common.hpp +++ b/programs/src/desktop/apps/apps_common.hpp @@ -173,4 +173,4 @@ void open_reboot_dialog(DesktopState* ds); void open_wordprocessor(DesktopState* ds); void open_shutdown_dialog(DesktopState* ds); void open_sleep_dialog(DesktopState* ds); -void desktop_poll_external_windows(DesktopState* ds); +bool desktop_poll_external_windows(DesktopState* ds); diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index f5765a2..d045e03 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -354,7 +354,38 @@ void gui::desktop_init(DesktopState* ds) { // External Window Polling // ============================================================================ -void desktop_poll_external_windows(DesktopState* ds) { +static uint64_t desktop_clock_token() { + Montauk::DateTime dt; + montauk::gettime(&dt); + return ((uint64_t)dt.Year << 32) + | ((uint64_t)dt.Month << 24) + | ((uint64_t)dt.Day << 16) + | ((uint64_t)dt.Hour << 8) + | (uint64_t)dt.Minute; +} + +static bool desktop_has_visible_dirty_window(const DesktopState* ds) { + for (int i = 0; i < ds->window_count; i++) { + const Window& win = ds->windows[i]; + if (win.state == WIN_CLOSED || win.state == WIN_MINIMIZED) continue; + if (win.dirty) return true; + } + return false; +} + +static void desktop_clear_window_dirty(DesktopState* ds) { + for (int i = 0; i < ds->window_count; i++) { + ds->windows[i].dirty = false; + } +} + +static bool desktop_panel_refresh_due(const DesktopState* ds, uint64_t now) { + if (ds->screen_locked) return false; + return (now - ds->net_cfg_last_poll > 5000) || (now - ds->vol_last_poll > 5000); +} + +bool desktop_poll_external_windows(DesktopState* ds) { + bool changed = false; Montauk::WinInfo extWins[8]; int extCount = montauk::win_enumerate(extWins, 8); @@ -370,6 +401,12 @@ void desktop_poll_external_windows(DesktopState* ds) { // Update dirty flag and cursor if (extWins[e].dirty) { ds->windows[i].dirty = true; + if (ds->windows[i].state != WIN_MINIMIZED && ds->windows[i].state != WIN_CLOSED) { + changed = true; + } + } + if (ds->windows[i].ext_cursor != extWins[e].cursor) { + changed = true; } ds->windows[i].ext_cursor = extWins[e].cursor; // Always verify mapping is current. If a window slot was @@ -383,6 +420,7 @@ void desktop_poll_external_windows(DesktopState* ds) { ds->windows[i].content_h = extWins[e].height; ds->windows[i].dirty = true; montauk::strncpy(ds->windows[i].title, extWins[e].title, MAX_TITLE_LEN); + changed = true; // Clear from closing list if the slot was reused for (int c = 0; c < ds->closing_ext_count; c++) { if (ds->closing_ext_ids[c] == extId) { @@ -449,6 +487,7 @@ void desktop_poll_external_windows(DesktopState* ds) { } ds->focused_window = idx; ds->window_count++; + changed = true; } } @@ -469,6 +508,7 @@ void desktop_poll_external_windows(DesktopState* ds) { // Window gone — remove without freeing content (shared memory) ds->windows[i].content = nullptr; // prevent free gui::desktop_close_window(ds, i); + changed = true; } } @@ -482,6 +522,8 @@ void desktop_poll_external_windows(DesktopState* ds) { ds->closing_ext_ids[c] = ds->closing_ext_ids[--ds->closing_ext_count]; } } + + return changed; } // ============================================================================ @@ -489,20 +531,37 @@ void desktop_poll_external_windows(DesktopState* ds) { // ============================================================================ void gui::desktop_run(DesktopState* ds) { + uint64_t lastClockToken = 0; + bool firstFrame = true; + for (;;) { + bool mouseChanged = false; + bool keyboardChanged = false; + bool sceneChanged = false; + // Poll mouse state + int prevMouseX = ds->mouse.x; + int prevMouseY = ds->mouse.y; + uint8_t prevMouseButtons = ds->mouse.buttons; ds->prev_buttons = ds->mouse.buttons; montauk::mouse_state(&ds->mouse); + mouseChanged = ds->mouse.x != prevMouseX + || ds->mouse.y != prevMouseY + || ds->mouse.buttons != prevMouseButtons + || ds->mouse.scrollDelta != 0; + sceneChanged |= mouseChanged; // Poll keyboard events while (montauk::is_key_available()) { Montauk::KeyEvent key; montauk::getkey(&key); desktop_handle_keyboard(ds, key); + keyboardChanged = true; } + sceneChanged |= keyboardChanged; // Poll external windows (discover new, remove dead, update dirty) - desktop_poll_external_windows(ds); + sceneChanged |= desktop_poll_external_windows(ds); if (!ds->screen_locked) { // Poll windows that have a poll callback @@ -516,20 +575,35 @@ void gui::desktop_run(DesktopState* ds) { } // Handle mouse events - desktop_handle_mouse(ds); - - if (!ds->screen_locked) { - // Re-poll external windows so that any killed during mouse/key - // handling are removed before we touch their pixel buffers. - desktop_poll_external_windows(ds); + if (mouseChanged) { + desktop_handle_mouse(ds); } - // Compose and present - desktop_compose(ds); - ds->fb.flip(); + if (!ds->screen_locked && (mouseChanged || keyboardChanged)) { + // Re-poll external windows so that any killed during mouse/key + // handling are removed before we touch their pixel buffers. + sceneChanged |= desktop_poll_external_windows(ds); + } - // Yield to scheduler without artificial frame cap - montauk::sleep_ms(1); + uint64_t now = montauk::get_milliseconds(); + sceneChanged |= desktop_panel_refresh_due(ds, now); + + uint64_t clockToken = desktop_clock_token(); + if (clockToken != lastClockToken) { + lastClockToken = clockToken; + sceneChanged = true; + } + + sceneChanged |= desktop_has_visible_dirty_window(ds); + + if (firstFrame || sceneChanged) { + desktop_compose(ds); + ds->fb.flip(); + desktop_clear_window_dirty(ds); + firstFrame = false; + } + + montauk::sleep_ms(sceneChanged ? 4 : 16); } } diff --git a/programs/src/login/main.cpp b/programs/src/login/main.cpp index d8b378c..e2c6c15 100644 --- a/programs/src/login/main.cpp +++ b/programs/src/login/main.cpp @@ -801,22 +801,41 @@ extern "C" void _start() { } // Main loop + bool first_frame = true; for (;;) { + bool mouse_changed = false; + bool key_changed = false; + // Poll mouse + int prev_mouse_x = ls->mouse.x; + int prev_mouse_y = ls->mouse.y; + uint8_t prev_mouse_buttons = ls->mouse.buttons; ls->prev_buttons = ls->mouse.buttons; montauk::mouse_state(&ls->mouse); + mouse_changed = ls->mouse.x != prev_mouse_x + || ls->mouse.y != prev_mouse_y + || ls->mouse.buttons != prev_mouse_buttons + || ls->mouse.scrollDelta != 0; // Poll keyboard while (montauk::is_key_available()) { Montauk::KeyEvent key; montauk::getkey(&key); handle_key(ls, key); + key_changed = true; } // Handle mouse - handle_mouse(ls); + if (mouse_changed) { + handle_mouse(ls); + } - // Draw - draw_login_screen(ls); + if (first_frame || mouse_changed || key_changed) { + draw_login_screen(ls); + first_frame = false; + montauk::sleep_ms(4); + } else { + montauk::sleep_ms(16); + } } } diff --git a/programs/src/music/Makefile b/programs/src/music/Makefile index e3a156b..78b55b6 100644 --- a/programs/src/music/Makefile +++ b/programs/src/music/Makefile @@ -64,7 +64,7 @@ LDFLAGS := \ # ---- Source files ---- -SRCS := main.cpp minimp3_impl.cpp stb_truetype_impl.cpp +SRCS := main.cpp visualizer.cpp minimp3_impl.cpp stb_truetype_impl.cpp OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- diff --git a/programs/src/music/main.cpp b/programs/src/music/main.cpp index d314f8c..b3819d6 100644 --- a/programs/src/music/main.cpp +++ b/programs/src/music/main.cpp @@ -18,6 +18,7 @@ extern "C" { } #include "minimp3.h" +#include "visualizer.hpp" using namespace gui; @@ -26,15 +27,19 @@ using namespace gui; // ============================================================================ static constexpr int INIT_W = 380; -static constexpr int INIT_H = 460; +static constexpr int INIT_H = 520; static constexpr int TOOLBAR_H = 36; +static constexpr int TOOL_ICON_W = 38; +static constexpr int TOOL_ICON_H = 26; +static constexpr int TOOL_ICON_GAP = 8; +static constexpr int TOOL_ICON_SIZE = 16; static constexpr int FONT_SIZE = 18; static constexpr int FONT_SIZE_SM = 16; static constexpr int FONT_SIZE_LG = 20; static constexpr int LIST_TOP = TOOLBAR_H; static constexpr int LIST_ITEM_H = 32; -static constexpr int INFO_H = 64; +static constexpr int INFO_H = 56; static constexpr int PROGRESS_H = 24; static constexpr int TRANSPORT_H = 44; @@ -66,12 +71,20 @@ static constexpr Color STOP_COLOR = Color::from_rgb(0xCC, 0x33, 0x33); // ============================================================================ enum class PlayState { Stopped, Playing, Paused }; +enum class RepeatMode { Off, All, One }; struct FileEntry { char name[128]; bool is_mp3; // true = mp3, false = wav }; +struct Mp3Frame { + uint64_t samples_before; + uint32_t offset; + uint16_t sample_count; + uint16_t reserved; +}; + struct PlayerState { // Window int win_w, win_h; @@ -91,6 +104,10 @@ struct PlayerState { // Playback PlayState play_state; int current_track; // index into files[], -1 if none + bool show_visualizer; + bool shuffle_enabled; + RepeatMode repeat_mode; + uint32_t rng_state; // Audio handle int audio_handle; @@ -102,6 +119,10 @@ struct PlayerState { // MP3 decoder mp3dec_t mp3dec; uint64_t mp3_offset; // current read position in file_data + Mp3Frame* mp3_frames; + int mp3_frame_count; + int mp3_frame_cap; + uint64_t mp3_seek_discard_samples; int sample_rate; int channels; @@ -131,6 +152,16 @@ struct PlayerState { SvgIcon ico_play_w; SvgIcon ico_pause_w; SvgIcon ico_stop_w; + SvgIcon ico_shuffle; + SvgIcon ico_shuffle_w; + SvgIcon ico_repeat_all; + SvgIcon ico_repeat_all_w; + SvgIcon ico_repeat_one; + SvgIcon ico_repeat_one_w; + SvgIcon ico_visualizer; + SvgIcon ico_visualizer_w; + + music_visualizer::State visualizer; }; static PlayerState g; @@ -258,11 +289,12 @@ static int font_h(int size = FONT_SIZE) { static void px_button(uint32_t* px, int bw, int bh, int x, int y, int w, int h, - const char* label, Color bg, Color fg, int r) { + const char* label, Color bg, Color fg, int r, + int size = FONT_SIZE) { px_fill_rounded(px, bw, bh, x, y, w, h, r, bg); - int tw = text_w(label); - int fh = font_h(); - px_text(px, bw, bh, x + (w - tw) / 2, y + (h - fh) / 2, label, fg); + int tw = text_w(label, size); + int fh = font_h(size); + px_text(px, bw, bh, x + (w - tw) / 2, y + (h - fh) / 2, label, fg, size); } static void px_icon_button(uint32_t* px, int bw, int bh, @@ -293,6 +325,47 @@ static bool str_ends_with(const char* s, const char* suffix) { return true; } +static const char* repeat_mode_label() { + switch (g.repeat_mode) { + case RepeatMode::All: return "Rpt All"; + case RepeatMode::One: return "Rpt 1"; + default: return "Rpt Off"; + } +} + +static void cycle_repeat_mode() { + switch (g.repeat_mode) { + case RepeatMode::Off: g.repeat_mode = RepeatMode::All; break; + case RepeatMode::All: g.repeat_mode = RepeatMode::One; break; + case RepeatMode::One: g.repeat_mode = RepeatMode::Off; break; + } +} + +static uint32_t next_random_u32() { + if (g.rng_state == 0) g.rng_state = 0xA341316Cu; + uint32_t x = g.rng_state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + g.rng_state = x; + return x; +} + +static Rect repeat_button_rect() { + int y = (TOOLBAR_H - TOOL_ICON_H) / 2; + return {g.win_w - 12 - TOOL_ICON_W, y, TOOL_ICON_W, TOOL_ICON_H}; +} + +static Rect shuffle_button_rect() { + Rect repeat = repeat_button_rect(); + return {repeat.x - TOOL_ICON_GAP - TOOL_ICON_W, repeat.y, TOOL_ICON_W, TOOL_ICON_H}; +} + +static Rect visualizer_button_rect() { + Rect shuffle = shuffle_button_rect(); + return {shuffle.x - TOOL_ICON_GAP - TOOL_ICON_W, shuffle.y, TOOL_ICON_W, TOOL_ICON_H}; +} + // ============================================================================ // Directory scanning // ============================================================================ @@ -362,6 +435,7 @@ static bool parse_wav(const uint8_t* data, uint64_t size) { } else if (chunk->id == 0x61746164) { // "data" if (!fmt) return false; if (fmt->audio_format != 1) return false; // PCM only + if (fmt->bits_per_sample != 16) return false; // 16-bit PCM only g.sample_rate = fmt->sample_rate; g.channels = fmt->channels; g.wav_data_offset = chunk_data; @@ -383,50 +457,121 @@ static bool parse_wav(const uint8_t* data, uint64_t size) { // MP3 helpers // ============================================================================ -static bool scan_mp3_info() { - // Scan through the file to estimate total duration - mp3dec_t tmp; - mp3dec_init(&tmp); +static void free_mp3_index() { + if (g.mp3_frames) { + montauk::mfree(g.mp3_frames); + g.mp3_frames = nullptr; + } + g.mp3_frame_count = 0; + g.mp3_frame_cap = 0; + g.mp3_seek_discard_samples = 0; +} - uint64_t offset = 0; +static bool push_mp3_frame(uint32_t offset, uint64_t samples_before, uint16_t sample_count) { + if (g.mp3_frame_count >= g.mp3_frame_cap) { + int new_cap = g.mp3_frame_cap ? g.mp3_frame_cap * 2 : 1024; + auto* new_frames = (Mp3Frame*)montauk::realloc(g.mp3_frames, + (uint64_t)new_cap * sizeof(Mp3Frame)); + if (!new_frames) return false; + g.mp3_frames = new_frames; + g.mp3_frame_cap = new_cap; + } + + Mp3Frame& frame = g.mp3_frames[g.mp3_frame_count++]; + frame.samples_before = samples_before; + frame.offset = offset; + frame.sample_count = sample_count; + frame.reserved = 0; + return true; +} + +static bool build_mp3_index(uint64_t start_offset) { + free_mp3_index(); g.total_samples = 0; g.sample_rate = 0; g.channels = 0; - // Skip ID3v2 tag if present - if (g.file_size >= 10 && g.file_data[0] == 'I' && g.file_data[1] == 'D' && g.file_data[2] == '3') { - uint64_t tag_size = ((uint64_t)(g.file_data[6] & 0x7F) << 21) | - ((uint64_t)(g.file_data[7] & 0x7F) << 14) | - ((uint64_t)(g.file_data[8] & 0x7F) << 7) | - ((uint64_t)(g.file_data[9] & 0x7F)); - offset = tag_size + 10; + mp3dec_t scan_dec; + mp3dec_init(&scan_dec); + + uint64_t offset = start_offset; + while (offset < g.file_size) { + mp3dec_frame_info_t info = {}; + int samples = mp3dec_decode_frame(&scan_dec, + g.file_data + offset, + (int)(g.file_size - offset), + nullptr, &info); + if (info.frame_bytes <= 0) break; + + if (samples > 0 && info.hz > 0 && info.channels > 0) { + uint32_t frame_offset = (uint32_t)(offset + (uint64_t)info.frame_offset); + if (!push_mp3_frame(frame_offset, g.total_samples, (uint16_t)samples)) { + free_mp3_index(); + return false; + } + if (g.sample_rate == 0) { + g.sample_rate = info.hz; + g.channels = info.channels; + } + g.total_samples += (uint64_t)samples; + } + + offset += (uint64_t)info.frame_bytes; } - // Quick scan: decode first frame for format, then estimate from bitrate - mp3dec_frame_info_t info; - int16_t pcm_tmp[MINIMP3_MAX_SAMPLES_PER_FRAME]; - int samples = mp3dec_decode_frame(&tmp, g.file_data + offset, - (int)(g.file_size - offset), pcm_tmp, &info); - if (samples <= 0 || info.hz == 0) return false; - - g.sample_rate = info.hz; - g.channels = info.channels; - - // Estimate total samples from file size and bitrate - if (info.bitrate_kbps > 0) { - uint64_t audio_bytes = g.file_size - offset; - // duration_sec = audio_bytes * 8 / (bitrate_kbps * 1000) - // total_samples = duration_sec * sample_rate - g.total_samples = (audio_bytes * 8 * (uint64_t)g.sample_rate) / - ((uint64_t)info.bitrate_kbps * 1000); - } else { - // VBR with no bitrate info — rough estimate - g.total_samples = g.file_size / 4 * (uint64_t)g.sample_rate / 11025; + if (g.mp3_frame_count <= 0 || g.sample_rate <= 0 || g.channels <= 0) { + free_mp3_index(); + return false; } + g.mp3_offset = g.mp3_frames[0].offset; return true; } +static int pick_shuffled_track(int current) { + if (g.file_count <= 1) return current >= 0 ? current : 0; + int pick = (int)(next_random_u32() % (uint32_t)g.file_count); + if (pick == current) { + pick = (pick + 1 + (int)(next_random_u32() % (uint32_t)(g.file_count - 1))) % g.file_count; + } + return pick; +} + +static void seek_to_samples(uint64_t target_samples) { + if (g.play_state == PlayState::Stopped || g.total_samples == 0) return; + if (target_samples > g.total_samples) target_samples = g.total_samples; + + g.pcm_buf_len = 0; + g.pcm_buf_pos = 0; + + if (g.current_track >= 0 && g.files[g.current_track].is_mp3) { + if (g.mp3_frame_count <= 0) return; + int lo = 0; + int hi = g.mp3_frame_count - 1; + int best = 0; + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + if (g.mp3_frames[mid].samples_before <= target_samples) { + best = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + + const Mp3Frame& frame = g.mp3_frames[best]; + g.mp3_offset = frame.offset; + g.played_samples = frame.samples_before; + g.mp3_seek_discard_samples = target_samples - frame.samples_before; + mp3dec_init(&g.mp3dec); + } else { + int bytes_per_frame = 2 * g.channels; + g.wav_pos = target_samples * (uint64_t)bytes_per_frame; + if (g.wav_pos > g.wav_data_size) g.wav_pos = g.wav_data_size; + g.played_samples = target_samples; + } +} + // ============================================================================ // Playback control // ============================================================================ @@ -440,6 +585,8 @@ static void stop_playback() { montauk::mfree(g.file_data); g.file_data = nullptr; } + free_mp3_index(); + music_visualizer::reset(g.visualizer); g.play_state = PlayState::Stopped; g.played_samples = 0; g.pcm_buf_len = 0; @@ -503,7 +650,7 @@ static bool start_track(int index) { ((uint64_t)(g.file_data[9] & 0x7F))); } - if (!scan_mp3_info()) { + if (!build_mp3_index(g.mp3_offset)) { montauk::mfree(g.file_data); g.file_data = nullptr; return false; @@ -519,6 +666,7 @@ static bool start_track(int index) { // Open audio device g.audio_handle = montauk::audio_open(g.sample_rate, g.channels, 16); if (g.audio_handle < 0) { + free_mp3_index(); montauk::mfree(g.file_data); g.file_data = nullptr; return false; @@ -529,6 +677,8 @@ static bool start_track(int index) { g.played_samples = 0; g.pcm_buf_len = 0; g.pcm_buf_pos = 0; + g.mp3_seek_discard_samples = 0; + music_visualizer::reset(g.visualizer); return true; } @@ -543,10 +693,26 @@ static void toggle_pause() { } } -static void next_track() { - if (g.file_count == 0) return; - int next = (g.current_track + 1) % g.file_count; - start_track(next); +static bool next_track(bool auto_advanced = false) { + if (g.file_count == 0) return false; + if (g.current_track < 0) return start_track(0); + + if (auto_advanced && g.repeat_mode == RepeatMode::One) { + return start_track(g.current_track); + } + + int next = -1; + if (g.shuffle_enabled) { + next = pick_shuffled_track(g.current_track); + } else if (g.current_track + 1 < g.file_count) { + next = g.current_track + 1; + } else if (g.repeat_mode == RepeatMode::All) { + next = 0; + } + + if (next >= 0) return start_track(next); + stop_playback(); + return false; } static void prev_track() { @@ -594,11 +760,11 @@ static void feed_audio() { // MP3 decode if (g.mp3_offset >= g.file_size) { // End of file — advance to next track - next_track(); + next_track(true); return; } - mp3dec_frame_info_t info; + mp3dec_frame_info_t info = {}; int samples = mp3dec_decode_frame(&g.mp3dec, g.file_data + g.mp3_offset, (int)(g.file_size - g.mp3_offset), @@ -607,17 +773,33 @@ static void feed_audio() { g.mp3_offset += info.frame_bytes; } else { // Can't decode — end - next_track(); + next_track(true); return; } if (samples > 0) { + if (g.mp3_seek_discard_samples > 0) { + uint64_t discard = g.mp3_seek_discard_samples; + if (discard > (uint64_t)samples) discard = (uint64_t)samples; + g.pcm_buf_pos = (int)(discard * (uint64_t)info.channels); + g.played_samples += discard; + g.mp3_seek_discard_samples -= discard; + samples -= (int)discard; + } g.pcm_buf_len = samples * info.channels; + if (g.pcm_buf_len > 0) { + music_visualizer::feed_pcm(g.visualizer, + g.pcm_buf + g.pcm_buf_pos, + g.pcm_buf_len / info.channels, + info.channels); + } else { + g.pcm_buf_pos = 0; + } } } else { // WAV decode — just copy PCM data if (g.wav_pos >= g.wav_data_size) { - next_track(); + next_track(true); return; } @@ -628,6 +810,12 @@ static void feed_audio() { memcpy(g.pcm_buf, g.file_data + g.wav_data_offset + g.wav_pos, to_copy); g.wav_pos += to_copy; g.pcm_buf_len = (int)(to_copy / 2); // 16-bit samples + if (g.channels > 0 && g.pcm_buf_len > 0) { + music_visualizer::feed_pcm(g.visualizer, + g.pcm_buf, + g.pcm_buf_len / g.channels, + g.channels); + } } } } @@ -648,6 +836,20 @@ static void format_time(char* buf, int buf_size, uint64_t samples, int rate) { // Render // ============================================================================ +static int list_bottom() { + return g.win_h - INFO_H - PROGRESS_H - TRANSPORT_H; +} + +static int visible_items() { + int count = (list_bottom() - LIST_TOP) / LIST_ITEM_H; + return count > 0 ? count : 0; +} + +static Rect content_panel_rect() { + int y = list_bottom() + 1; + return {0, LIST_TOP, g.win_w, y - LIST_TOP}; +} + static void render(uint32_t* pixels) { int W = g.win_w, H = g.win_h; int fh = font_h(); @@ -659,79 +861,96 @@ static void render(uint32_t* pixels) { // Toolbar px_fill(pixels, W, H, 0, 0, W, TOOLBAR_H, TOOLBAR_BG); px_hline(pixels, W, H, 0, TOOLBAR_H - 1, W, BORDER_COLOR); + Rect visualizer_rect = visualizer_button_rect(); + Rect shuffle_rect = shuffle_button_rect(); + Rect repeat_rect = repeat_button_rect(); + px_icon_button(pixels, W, H, visualizer_rect.x, visualizer_rect.y, + visualizer_rect.w, visualizer_rect.h, + g.show_visualizer ? g.ico_visualizer_w : g.ico_visualizer, + g.show_visualizer ? ACCENT : BTN_BG, 6); + px_icon_button(pixels, W, H, shuffle_rect.x, shuffle_rect.y, shuffle_rect.w, shuffle_rect.h, + g.shuffle_enabled ? g.ico_shuffle_w : g.ico_shuffle, + g.shuffle_enabled ? ACCENT : BTN_BG, 6); + Color repeat_bg = (g.repeat_mode == RepeatMode::Off) ? BTN_BG : ACCENT; + px_icon_button(pixels, W, H, repeat_rect.x, repeat_rect.y, repeat_rect.w, repeat_rect.h, + (g.repeat_mode == RepeatMode::One) ? g.ico_repeat_one_w : + (g.repeat_mode == RepeatMode::All) ? g.ico_repeat_all_w : + g.ico_repeat_all, + repeat_bg, 6); + // Directory path in toolbar { const char* display_dir = g.dir_path; int dw = text_w(display_dir, FONT_SIZE_SM); - int max_dir_w = W - 24; - if (dw > max_dir_w) { + 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; } px_text(pixels, W, H, 12, - (TOOLBAR_H - fh) / 2, display_dir, TEXT_COLOR); + (TOOLBAR_H - font_h(FONT_SIZE_SM)) / 2, display_dir, TEXT_COLOR, FONT_SIZE_SM); } - // ---- File list ---- - int list_bottom = H - INFO_H - PROGRESS_H - TRANSPORT_H; - int visible_items = (list_bottom - LIST_TOP) / LIST_ITEM_H; + Rect content_panel = content_panel_rect(); + int list_end = list_bottom(); + int visible_count = visible_items(); + px_hline(pixels, W, H, 0, list_end, W, BORDER_COLOR); - px_hline(pixels, W, H, 0, list_bottom, 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; + int iy = LIST_TOP + i * LIST_ITEM_H; - for (int i = 0; i < visible_items && (i + g.scroll_y) < g.file_count; i++) { - int idx = i + g.scroll_y; - 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.hovered_item) + bg = LIST_HOVER; - // Highlight - Color bg = BG_COLOR; - if (idx == g.current_track && g.play_state != PlayState::Stopped) - bg = LIST_PLAYING; - 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 (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.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) { + px_fill(pixels, W, H, 8, iy + 8, 3, 12, ACCENT); + px_fill(pixels, W, H, 13, iy + 8, 3, 12, ACCENT); + } - // Playing indicator - 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) { - // Pause bars - px_fill(pixels, W, H, 8, iy + 8, 3, 12, ACCENT); - px_fill(pixels, W, H, 13, iy + 8, 3, 12, ACCENT); + Color name_color = (idx == g.current_track && g.play_state != PlayState::Stopped) + ? ACCENT_DARK : TEXT_COLOR; + px_text(pixels, W, H, 24, iy + (LIST_ITEM_H - fh_sm) / 2, + g.files[idx].name, name_color, FONT_SIZE_SM); + + const char* tag = g.files[idx].is_mp3 ? "MP3" : "WAV"; + int tw = text_w(tag, FONT_SIZE_SM); + px_text(pixels, W, H, W - tw - 12, iy + (LIST_ITEM_H - fh_sm) / 2, + tag, DIM_TEXT, FONT_SIZE_SM); } - // File name - Color name_color = (idx == g.current_track && g.play_state != PlayState::Stopped) - ? ACCENT_DARK : TEXT_COLOR; - px_text(pixels, W, H, 24, iy + (LIST_ITEM_H - fh_sm) / 2, - g.files[idx].name, name_color, FONT_SIZE_SM); + if (g.file_count > visible_count && visible_count > 0) { + int list_h = list_end - LIST_TOP; + int sb_h = (visible_count * list_h) / g.file_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); + px_fill_rounded(pixels, W, H, W - 6, sb_y, 4, sb_h, 2, TRACK_BG); + } - // Format tag - const char* tag = g.files[idx].is_mp3 ? "MP3" : "WAV"; - int tw = text_w(tag, FONT_SIZE_SM); - px_text(pixels, W, H, W - tw - 12, iy + (LIST_ITEM_H - fh_sm) / 2, - tag, DIM_TEXT, FONT_SIZE_SM); - } - - // Scrollbar - if (g.file_count > visible_items && visible_items > 0) { - int list_h = list_bottom - LIST_TOP; - int sb_h = (visible_items * list_h) / g.file_count; - if (sb_h < 20) sb_h = 20; - int sb_y = LIST_TOP + (g.scroll_y * (list_h - sb_h)) / (g.file_count - visible_items); - px_fill_rounded(pixels, W, H, W - 6, sb_y, 4, sb_h, 2, TRACK_BG); - } - - // Empty state - if (g.file_count == 0) { - const char* msg = "No audio files found"; - int mw = text_w(msg); - int cy = LIST_TOP + (list_bottom - LIST_TOP) / 2 - fh / 2; - px_text(pixels, W, H, (W - mw) / 2, cy, msg, DIM_TEXT); + if (g.file_count == 0) { + const char* msg = "No audio files found"; + 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); + } + } else { + px_fill(pixels, W, H, content_panel.x, content_panel.y, content_panel.w, content_panel.h, TOOLBAR_BG); + Rect viz_rect = {12, content_panel.y + 12, W - 24, content_panel.h - 24}; + music_visualizer::render(pixels, W, H, viz_rect, g.visualizer, + ACCENT, ACCENT_DARK, TRACK_BG, BORDER_COLOR); } // ---- Now Playing info ---- - int info_y = list_bottom + 1; + int info_y = list_end + 1; px_fill(pixels, W, H, 0, info_y, W, INFO_H, TOOLBAR_BG); px_hline(pixels, W, H, 0, info_y + INFO_H - 1, W, BORDER_COLOR); @@ -814,23 +1033,33 @@ static void render(uint32_t* pixels) { // Hit testing // ============================================================================ -static int list_bottom() { - return g.win_h - INFO_H - PROGRESS_H - TRANSPORT_H; -} - -static int visible_items() { - return (list_bottom() - LIST_TOP) / LIST_ITEM_H; -} - static bool handle_click(int mx, int my) { int W = g.win_w; int lb = list_bottom(); + Rect visualizer_rect = visualizer_button_rect(); + Rect shuffle_rect = shuffle_button_rect(); + Rect repeat_rect = repeat_button_rect(); int info_y = lb + 1; int prog_y = info_y + INFO_H; int trans_y = prog_y + PROGRESS_H; + if (visualizer_rect.contains(mx, my)) { + g.show_visualizer = !g.show_visualizer; + return true; + } + + if (shuffle_rect.contains(mx, my)) { + g.shuffle_enabled = !g.shuffle_enabled; + return true; + } + + if (repeat_rect.contains(mx, my)) { + cycle_repeat_mode(); + return true; + } + // File list click - if (my >= LIST_TOP && my < lb && mx >= 0 && mx < W) { + 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) { start_track(idx); @@ -845,27 +1074,8 @@ static bool handle_click(int mx, int my) { int rel = mx - bar_x; if (rel < 0) rel = 0; if (rel > bar_w) rel = bar_w; - // Seek: restart track and skip ahead - // For simplicity, we estimate the file offset from the progress ratio uint64_t target_samples = (uint64_t)rel * g.total_samples / bar_w; - - if (g.current_track >= 0 && g.files[g.current_track].is_mp3) { - // MP3 seek: estimate byte offset from ratio - uint64_t target_offset = (g.file_size * rel) / bar_w; - g.mp3_offset = target_offset; - g.played_samples = target_samples; - g.pcm_buf_len = 0; - g.pcm_buf_pos = 0; - mp3dec_init(&g.mp3dec); // reset decoder for clean seek - } else { - // WAV seek: direct offset - int bytes_per_sample = 2 * g.channels; - g.wav_pos = target_samples * bytes_per_sample; - if (g.wav_pos > g.wav_data_size) g.wav_pos = g.wav_data_size; - g.played_samples = target_samples; - g.pcm_buf_len = 0; - g.pcm_buf_pos = 0; - } + seek_to_samples(target_samples); g.dragging_progress = true; return true; } @@ -901,7 +1111,7 @@ static bool handle_click(int mx, int my) { bx += btn_w + gap; // Next - if (mx >= bx && mx < bx + btn_w) { next_track(); return true; } + if (mx >= bx && mx < bx + btn_w) { next_track(false); return true; } } } @@ -921,6 +1131,10 @@ extern "C" void _start() { g.current_track = -1; g.play_state = PlayState::Stopped; g.hovered_item = -1; + g.show_visualizer = false; + g.repeat_mode = RepeatMode::Off; + g.rng_state = (uint32_t)montauk::get_milliseconds() ^ 0xC0DEFACEu; + music_visualizer::reset(g.visualizer); // Parse arguments: accept a directory path or a file path // Helper: set dir_path to current user's home via session config @@ -994,6 +1208,14 @@ extern "C" void _start() { g.ico_play_w = svg_load("0:/icons/media-play.svg", 16, 16, WHITE); g.ico_pause_w = svg_load("0:/icons/media-pause.svg", 16, 16, WHITE); g.ico_stop_w = svg_load("0:/icons/media-stop.svg", 16, 16, WHITE); + g.ico_visualizer = svg_load("0:/icons/view-media-equalizer.svg", TOOL_ICON_SIZE, TOOL_ICON_SIZE, ic_color); + g.ico_visualizer_w = svg_load("0:/icons/view-media-equalizer.svg", TOOL_ICON_SIZE, TOOL_ICON_SIZE, WHITE); + g.ico_shuffle = svg_load("0:/icons/media-playlist-shuffle.svg", TOOL_ICON_SIZE, TOOL_ICON_SIZE, ic_color); + g.ico_shuffle_w = svg_load("0:/icons/media-playlist-shuffle.svg", TOOL_ICON_SIZE, TOOL_ICON_SIZE, WHITE); + g.ico_repeat_all = svg_load("0:/icons/media-playlist-repeat.svg", TOOL_ICON_SIZE, TOOL_ICON_SIZE, ic_color); + g.ico_repeat_all_w = svg_load("0:/icons/media-playlist-repeat.svg", TOOL_ICON_SIZE, TOOL_ICON_SIZE, WHITE); + g.ico_repeat_one = svg_load("0:/icons/media-playlist-repeat-song.svg", TOOL_ICON_SIZE, TOOL_ICON_SIZE, ic_color); + g.ico_repeat_one_w = svg_load("0:/icons/media-playlist-repeat-song.svg", TOOL_ICON_SIZE, TOOL_ICON_SIZE, WHITE); } // Scan for audio files @@ -1053,8 +1275,20 @@ extern "C" void _start() { 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(); + next_track(false); redraw = true; } if (ev.key.scancode == 0x4B) { // Left arrow @@ -1084,26 +1318,13 @@ extern "C" void _start() { if (rel < 0) rel = 0; if (rel > bar_w) rel = bar_w; uint64_t target_samples = (uint64_t)rel * g.total_samples / bar_w; - - if (g.current_track >= 0 && g.files[g.current_track].is_mp3) { - uint64_t target_offset = (g.file_size * rel) / bar_w; - g.mp3_offset = target_offset; - g.played_samples = target_samples; - g.pcm_buf_len = 0; - mp3dec_init(&g.mp3dec); - } else { - int bytes_per_sample = 2 * g.channels; - g.wav_pos = target_samples * bytes_per_sample; - if (g.wav_pos > g.wav_data_size) g.wav_pos = g.wav_data_size; - g.played_samples = target_samples; - g.pcm_buf_len = 0; - } + seek_to_samples(target_samples); redraw = true; } // Hover tracking for file list int lb = list_bottom(); - if (ev.mouse.y >= LIST_TOP && ev.mouse.y < lb) { + 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; if (new_hover != g.hovered_item) { @@ -1116,7 +1337,7 @@ extern "C" void _start() { } // Scroll - if (ev.mouse.scroll != 0) { + 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; @@ -1142,11 +1363,12 @@ extern "C" void _start() { // Periodic redraw while playing (update time display) uint64_t now = montauk::get_milliseconds(); - if (g.play_state == PlayState::Playing && now - last_render >= 250) { + if (g.play_state == PlayState::Playing && now - last_render >= 50) { redraw = true; } if (redraw) { + music_visualizer::tick(g.visualizer); render(pixels); montauk::win_present(win_id); last_render = now; diff --git a/programs/src/music/visualizer.cpp b/programs/src/music/visualizer.cpp new file mode 100644 index 0000000..77306f0 --- /dev/null +++ b/programs/src/music/visualizer.cpp @@ -0,0 +1,140 @@ +#include "visualizer.hpp" + +namespace music_visualizer { + +using namespace gui; + +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; + uint32_t v = c.to_pixel(); + int x0 = gui_clamp(x, 0, bw); + int y0 = gui_clamp(y, 0, bh); + int x1 = gui_clamp(x + w, 0, bw); + int y1 = gui_clamp(y + h, 0, bh); + for (int row = y0; row < y1; row++) { + for (int col = x0; col < x1; col++) { + px[row * bw + col] = v; + } + } +} + +static void px_hline(uint32_t* px, int bw, int bh, + int x, int y, int w, Color c) { + if (w <= 0 || y < 0 || y >= bh) return; + uint32_t v = c.to_pixel(); + int x0 = gui_clamp(x, 0, bw); + int x1 = gui_clamp(x + w, 0, bw); + for (int col = x0; col < x1; col++) { + px[y * bw + col] = v; + } +} + +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; + } +} + +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 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; + for (int ch = 0; ch < channels; ch++) { + int sample = pcm[i * channels + ch]; + if (sample < 0) sample = -sample; + amp += sample; + } + amp /= channels; + if (amp > local_max[bar]) local_max[bar] = amp; + } + + for (int i = 0; i < BAR_COUNT; i++) { + int scaled = (local_max[i] * 100) / 32768; + scaled = (scaled * 3) / 2; + if (scaled > 100) scaled = 100; + + int blended = scaled; + if (scaled < state.levels[i]) { + blended = (state.levels[i] * 3 + scaled) / 4; + } + + 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; + } + } +} + +void render(uint32_t* pixels, int bw, int bh, const Rect& rect, + const State& state, Color accent, Color accent_dark, + 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)); + 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); + + int inner_x = rect.x + 10; + int inner_y = rect.y + 8; + int inner_w = rect.w - 20; + 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; + + px_hline(pixels, bw, bh, inner_x, base_y, inner_w, track_bg); + + 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 (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); + } + + 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); + } +} + +} // namespace music_visualizer diff --git a/programs/src/music/visualizer.hpp b/programs/src/music/visualizer.hpp new file mode 100644 index 0000000..d6a7966 --- /dev/null +++ b/programs/src/music/visualizer.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +namespace music_visualizer { + +static constexpr int BAR_COUNT = 24; + +struct State { + uint8_t levels[BAR_COUNT]; + uint8_t peaks[BAR_COUNT]; + uint8_t peak_hold[BAR_COUNT]; +}; + +void reset(State& state); +void tick(State& state); +void feed_pcm(State& state, const int16_t* pcm, int frames, int channels); +void render(uint32_t* pixels, int bw, int bh, const gui::Rect& rect, + const State& state, gui::Color accent, gui::Color accent_dark, + gui::Color track_bg, gui::Color border); + +} // namespace music_visualizer diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index 9e3fb57..71222f3 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -101,6 +101,13 @@ ICONS=( "actions/16/media-stop.svg" "actions/16/media-forward.svg" "actions/16/media-rewind.svg" + "actions/16/view-media-equalizer.svg" + "actions/16/media-playlist-shuffle.svg" + "actions/16/media-playlist-repeat.svg" + "actions/16/media-playlist-repeat-song.svg" + "panel/media-playlist-shuffle-symbolic.svg" + "panel/media-playlist-repeat-symbolic.svg" + "panel/media-playlist-repeat-one-symbolic.svg" "apps/scalable/multimedia-video-player.svg" "apps/scalable/bluetooth.svg" "panel/volume-level-high.svg" diff --git a/template/sysroot/include/gui/framebuffer.hpp b/template/sysroot/include/gui/framebuffer.hpp index 9a1a4ab..257bf3d 100644 --- a/template/sysroot/include/gui/framebuffer.hpp +++ b/template/sysroot/include/gui/framebuffer.hpp @@ -7,6 +7,7 @@ #pragma once #include #include +#include #include "gui/gui.hpp" namespace gui { @@ -182,13 +183,11 @@ public: inline void flip() { // Copy back buffer to hardware framebuffer, row by row (pitch may differ) - int row_pixels = fb_width; + uint64_t row_bytes = (uint64_t)fb_width * sizeof(uint32_t); for (int y = 0; y < fb_height; y++) { - uint32_t* src = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch); - uint32_t* dst = (uint32_t*)((uint8_t*)hw_fb + y * fb_pitch); - for (int x = 0; x < row_pixels; x++) { - dst[x] = src[x]; - } + void* src = (void*)((uint8_t*)back_buf + y * fb_pitch); + void* dst = (void*)((uint8_t*)hw_fb + y * fb_pitch); + montauk::memcpy(dst, src, row_bytes); } } };