fix: music app improvements, desktop performance improvements
This commit is contained in:
@@ -145,6 +145,7 @@ static void klog_on_poll(Window* win) {
|
|||||||
|
|
||||||
klog->last_len = n;
|
klog->last_len = n;
|
||||||
if (n > 0) klog->last_tail_byte = klog->klog_buf[n - 1];
|
if (n > 0) klog->last_tail_byte = klog->klog_buf[n - 1];
|
||||||
|
win->dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void klog_on_close(Window* win) {
|
static void klog_on_close(Window* win) {
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ static void procmgr_on_poll(Window* win) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
win->dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void procmgr_on_draw(Window* win, Framebuffer& fb) {
|
static void procmgr_on_draw(Window* win, Framebuffer& fb) {
|
||||||
|
|||||||
@@ -311,12 +311,17 @@ static void terminal_on_poll(Window* win) {
|
|||||||
if (!terminal_poll(tts->tabs[i])) {
|
if (!terminal_poll(tts->tabs[i])) {
|
||||||
// This tab's shell exited
|
// This tab's shell exited
|
||||||
term_close_tab(tts, win, i);
|
term_close_tab(tts, win, i);
|
||||||
|
win->dirty = true;
|
||||||
// term_close_tab may have closed the window (last tab).
|
// term_close_tab may have closed the window (last tab).
|
||||||
// desktop_close_window calls on_close which nulls app_data
|
// desktop_close_window calls on_close which nulls app_data
|
||||||
// before the window slot is recycled, so win is still valid here.
|
// before the window slot is recycled, so win is still valid here.
|
||||||
if (!win->app_data) return;
|
if (!win->app_data) return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tts->tab_count > 0 && tts->tabs[tts->active_tab]->dirty) {
|
||||||
|
win->dirty = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -173,4 +173,4 @@ void open_reboot_dialog(DesktopState* ds);
|
|||||||
void open_wordprocessor(DesktopState* ds);
|
void open_wordprocessor(DesktopState* ds);
|
||||||
void open_shutdown_dialog(DesktopState* ds);
|
void open_shutdown_dialog(DesktopState* ds);
|
||||||
void open_sleep_dialog(DesktopState* ds);
|
void open_sleep_dialog(DesktopState* ds);
|
||||||
void desktop_poll_external_windows(DesktopState* ds);
|
bool desktop_poll_external_windows(DesktopState* ds);
|
||||||
|
|||||||
@@ -354,7 +354,38 @@ void gui::desktop_init(DesktopState* ds) {
|
|||||||
// External Window Polling
|
// 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];
|
Montauk::WinInfo extWins[8];
|
||||||
int extCount = montauk::win_enumerate(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
|
// Update dirty flag and cursor
|
||||||
if (extWins[e].dirty) {
|
if (extWins[e].dirty) {
|
||||||
ds->windows[i].dirty = true;
|
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;
|
ds->windows[i].ext_cursor = extWins[e].cursor;
|
||||||
// Always verify mapping is current. If a window slot was
|
// 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].content_h = extWins[e].height;
|
||||||
ds->windows[i].dirty = true;
|
ds->windows[i].dirty = true;
|
||||||
montauk::strncpy(ds->windows[i].title, extWins[e].title, MAX_TITLE_LEN);
|
montauk::strncpy(ds->windows[i].title, extWins[e].title, MAX_TITLE_LEN);
|
||||||
|
changed = true;
|
||||||
// Clear from closing list if the slot was reused
|
// Clear from closing list if the slot was reused
|
||||||
for (int c = 0; c < ds->closing_ext_count; c++) {
|
for (int c = 0; c < ds->closing_ext_count; c++) {
|
||||||
if (ds->closing_ext_ids[c] == extId) {
|
if (ds->closing_ext_ids[c] == extId) {
|
||||||
@@ -449,6 +487,7 @@ void desktop_poll_external_windows(DesktopState* ds) {
|
|||||||
}
|
}
|
||||||
ds->focused_window = idx;
|
ds->focused_window = idx;
|
||||||
ds->window_count++;
|
ds->window_count++;
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,6 +508,7 @@ void desktop_poll_external_windows(DesktopState* ds) {
|
|||||||
// Window gone — remove without freeing content (shared memory)
|
// Window gone — remove without freeing content (shared memory)
|
||||||
ds->windows[i].content = nullptr; // prevent free
|
ds->windows[i].content = nullptr; // prevent free
|
||||||
gui::desktop_close_window(ds, i);
|
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];
|
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) {
|
void gui::desktop_run(DesktopState* ds) {
|
||||||
|
uint64_t lastClockToken = 0;
|
||||||
|
bool firstFrame = true;
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
bool mouseChanged = false;
|
||||||
|
bool keyboardChanged = false;
|
||||||
|
bool sceneChanged = false;
|
||||||
|
|
||||||
// Poll mouse state
|
// 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;
|
ds->prev_buttons = ds->mouse.buttons;
|
||||||
montauk::mouse_state(&ds->mouse);
|
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
|
// Poll keyboard events
|
||||||
while (montauk::is_key_available()) {
|
while (montauk::is_key_available()) {
|
||||||
Montauk::KeyEvent key;
|
Montauk::KeyEvent key;
|
||||||
montauk::getkey(&key);
|
montauk::getkey(&key);
|
||||||
desktop_handle_keyboard(ds, key);
|
desktop_handle_keyboard(ds, key);
|
||||||
|
keyboardChanged = true;
|
||||||
}
|
}
|
||||||
|
sceneChanged |= keyboardChanged;
|
||||||
|
|
||||||
// Poll external windows (discover new, remove dead, update dirty)
|
// Poll external windows (discover new, remove dead, update dirty)
|
||||||
desktop_poll_external_windows(ds);
|
sceneChanged |= desktop_poll_external_windows(ds);
|
||||||
|
|
||||||
if (!ds->screen_locked) {
|
if (!ds->screen_locked) {
|
||||||
// Poll windows that have a poll callback
|
// Poll windows that have a poll callback
|
||||||
@@ -516,20 +575,35 @@ void gui::desktop_run(DesktopState* ds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle mouse events
|
// Handle mouse events
|
||||||
desktop_handle_mouse(ds);
|
if (mouseChanged) {
|
||||||
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compose and present
|
if (!ds->screen_locked && (mouseChanged || keyboardChanged)) {
|
||||||
desktop_compose(ds);
|
// Re-poll external windows so that any killed during mouse/key
|
||||||
ds->fb.flip();
|
// handling are removed before we touch their pixel buffers.
|
||||||
|
sceneChanged |= desktop_poll_external_windows(ds);
|
||||||
|
}
|
||||||
|
|
||||||
// Yield to scheduler without artificial frame cap
|
uint64_t now = montauk::get_milliseconds();
|
||||||
montauk::sleep_ms(1);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -801,22 +801,41 @@ extern "C" void _start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Main loop
|
// Main loop
|
||||||
|
bool first_frame = true;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
bool mouse_changed = false;
|
||||||
|
bool key_changed = false;
|
||||||
|
|
||||||
// Poll mouse
|
// 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;
|
ls->prev_buttons = ls->mouse.buttons;
|
||||||
montauk::mouse_state(&ls->mouse);
|
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
|
// Poll keyboard
|
||||||
while (montauk::is_key_available()) {
|
while (montauk::is_key_available()) {
|
||||||
Montauk::KeyEvent key;
|
Montauk::KeyEvent key;
|
||||||
montauk::getkey(&key);
|
montauk::getkey(&key);
|
||||||
handle_key(ls, key);
|
handle_key(ls, key);
|
||||||
|
key_changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle mouse
|
// Handle mouse
|
||||||
handle_mouse(ls);
|
if (mouse_changed) {
|
||||||
|
handle_mouse(ls);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw
|
if (first_frame || mouse_changed || key_changed) {
|
||||||
draw_login_screen(ls);
|
draw_login_screen(ls);
|
||||||
|
first_frame = false;
|
||||||
|
montauk::sleep_ms(4);
|
||||||
|
} else {
|
||||||
|
montauk::sleep_ms(16);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ LDFLAGS := \
|
|||||||
|
|
||||||
# ---- Source files ----
|
# ---- 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))
|
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||||
|
|
||||||
# ---- Target ----
|
# ---- Target ----
|
||||||
|
|||||||
+370
-148
@@ -18,6 +18,7 @@ extern "C" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#include "minimp3.h"
|
#include "minimp3.h"
|
||||||
|
#include "visualizer.hpp"
|
||||||
|
|
||||||
using namespace gui;
|
using namespace gui;
|
||||||
|
|
||||||
@@ -26,15 +27,19 @@ using namespace gui;
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
static constexpr int INIT_W = 380;
|
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 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 = 18;
|
||||||
static constexpr int FONT_SIZE_SM = 16;
|
static constexpr int FONT_SIZE_SM = 16;
|
||||||
static constexpr int FONT_SIZE_LG = 20;
|
static constexpr int FONT_SIZE_LG = 20;
|
||||||
|
|
||||||
static constexpr int LIST_TOP = TOOLBAR_H;
|
static constexpr int LIST_TOP = TOOLBAR_H;
|
||||||
static constexpr int LIST_ITEM_H = 32;
|
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 PROGRESS_H = 24;
|
||||||
static constexpr int TRANSPORT_H = 44;
|
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 PlayState { Stopped, Playing, Paused };
|
||||||
|
enum class RepeatMode { Off, All, One };
|
||||||
|
|
||||||
struct FileEntry {
|
struct FileEntry {
|
||||||
char name[128];
|
char name[128];
|
||||||
bool is_mp3; // true = mp3, false = wav
|
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 {
|
struct PlayerState {
|
||||||
// Window
|
// Window
|
||||||
int win_w, win_h;
|
int win_w, win_h;
|
||||||
@@ -91,6 +104,10 @@ struct PlayerState {
|
|||||||
// Playback
|
// Playback
|
||||||
PlayState play_state;
|
PlayState play_state;
|
||||||
int current_track; // index into files[], -1 if none
|
int current_track; // index into files[], -1 if none
|
||||||
|
bool show_visualizer;
|
||||||
|
bool shuffle_enabled;
|
||||||
|
RepeatMode repeat_mode;
|
||||||
|
uint32_t rng_state;
|
||||||
|
|
||||||
// Audio handle
|
// Audio handle
|
||||||
int audio_handle;
|
int audio_handle;
|
||||||
@@ -102,6 +119,10 @@ struct PlayerState {
|
|||||||
// MP3 decoder
|
// MP3 decoder
|
||||||
mp3dec_t mp3dec;
|
mp3dec_t mp3dec;
|
||||||
uint64_t mp3_offset; // current read position in file_data
|
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 sample_rate;
|
||||||
int channels;
|
int channels;
|
||||||
|
|
||||||
@@ -131,6 +152,16 @@ struct PlayerState {
|
|||||||
SvgIcon ico_play_w;
|
SvgIcon ico_play_w;
|
||||||
SvgIcon ico_pause_w;
|
SvgIcon ico_pause_w;
|
||||||
SvgIcon ico_stop_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;
|
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,
|
static void px_button(uint32_t* px, int bw, int bh,
|
||||||
int x, int y, int w, int h,
|
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);
|
px_fill_rounded(px, bw, bh, x, y, w, h, r, bg);
|
||||||
int tw = text_w(label);
|
int tw = text_w(label, size);
|
||||||
int fh = font_h();
|
int fh = font_h(size);
|
||||||
px_text(px, bw, bh, x + (w - tw) / 2, y + (h - fh) / 2, label, fg);
|
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,
|
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;
|
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
|
// Directory scanning
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -362,6 +435,7 @@ static bool parse_wav(const uint8_t* data, uint64_t size) {
|
|||||||
} else if (chunk->id == 0x61746164) { // "data"
|
} else if (chunk->id == 0x61746164) { // "data"
|
||||||
if (!fmt) return false;
|
if (!fmt) return false;
|
||||||
if (fmt->audio_format != 1) return false; // PCM only
|
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.sample_rate = fmt->sample_rate;
|
||||||
g.channels = fmt->channels;
|
g.channels = fmt->channels;
|
||||||
g.wav_data_offset = chunk_data;
|
g.wav_data_offset = chunk_data;
|
||||||
@@ -383,50 +457,121 @@ static bool parse_wav(const uint8_t* data, uint64_t size) {
|
|||||||
// MP3 helpers
|
// MP3 helpers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
static bool scan_mp3_info() {
|
static void free_mp3_index() {
|
||||||
// Scan through the file to estimate total duration
|
if (g.mp3_frames) {
|
||||||
mp3dec_t tmp;
|
montauk::mfree(g.mp3_frames);
|
||||||
mp3dec_init(&tmp);
|
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.total_samples = 0;
|
||||||
g.sample_rate = 0;
|
g.sample_rate = 0;
|
||||||
g.channels = 0;
|
g.channels = 0;
|
||||||
|
|
||||||
// Skip ID3v2 tag if present
|
mp3dec_t scan_dec;
|
||||||
if (g.file_size >= 10 && g.file_data[0] == 'I' && g.file_data[1] == 'D' && g.file_data[2] == '3') {
|
mp3dec_init(&scan_dec);
|
||||||
uint64_t tag_size = ((uint64_t)(g.file_data[6] & 0x7F) << 21) |
|
|
||||||
((uint64_t)(g.file_data[7] & 0x7F) << 14) |
|
uint64_t offset = start_offset;
|
||||||
((uint64_t)(g.file_data[8] & 0x7F) << 7) |
|
while (offset < g.file_size) {
|
||||||
((uint64_t)(g.file_data[9] & 0x7F));
|
mp3dec_frame_info_t info = {};
|
||||||
offset = tag_size + 10;
|
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
|
if (g.mp3_frame_count <= 0 || g.sample_rate <= 0 || g.channels <= 0) {
|
||||||
mp3dec_frame_info_t info;
|
free_mp3_index();
|
||||||
int16_t pcm_tmp[MINIMP3_MAX_SAMPLES_PER_FRAME];
|
return false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.mp3_offset = g.mp3_frames[0].offset;
|
||||||
return true;
|
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
|
// Playback control
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -440,6 +585,8 @@ static void stop_playback() {
|
|||||||
montauk::mfree(g.file_data);
|
montauk::mfree(g.file_data);
|
||||||
g.file_data = nullptr;
|
g.file_data = nullptr;
|
||||||
}
|
}
|
||||||
|
free_mp3_index();
|
||||||
|
music_visualizer::reset(g.visualizer);
|
||||||
g.play_state = PlayState::Stopped;
|
g.play_state = PlayState::Stopped;
|
||||||
g.played_samples = 0;
|
g.played_samples = 0;
|
||||||
g.pcm_buf_len = 0;
|
g.pcm_buf_len = 0;
|
||||||
@@ -503,7 +650,7 @@ static bool start_track(int index) {
|
|||||||
((uint64_t)(g.file_data[9] & 0x7F)));
|
((uint64_t)(g.file_data[9] & 0x7F)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scan_mp3_info()) {
|
if (!build_mp3_index(g.mp3_offset)) {
|
||||||
montauk::mfree(g.file_data);
|
montauk::mfree(g.file_data);
|
||||||
g.file_data = nullptr;
|
g.file_data = nullptr;
|
||||||
return false;
|
return false;
|
||||||
@@ -519,6 +666,7 @@ static bool start_track(int index) {
|
|||||||
// Open audio device
|
// Open audio device
|
||||||
g.audio_handle = montauk::audio_open(g.sample_rate, g.channels, 16);
|
g.audio_handle = montauk::audio_open(g.sample_rate, g.channels, 16);
|
||||||
if (g.audio_handle < 0) {
|
if (g.audio_handle < 0) {
|
||||||
|
free_mp3_index();
|
||||||
montauk::mfree(g.file_data);
|
montauk::mfree(g.file_data);
|
||||||
g.file_data = nullptr;
|
g.file_data = nullptr;
|
||||||
return false;
|
return false;
|
||||||
@@ -529,6 +677,8 @@ static bool start_track(int index) {
|
|||||||
g.played_samples = 0;
|
g.played_samples = 0;
|
||||||
g.pcm_buf_len = 0;
|
g.pcm_buf_len = 0;
|
||||||
g.pcm_buf_pos = 0;
|
g.pcm_buf_pos = 0;
|
||||||
|
g.mp3_seek_discard_samples = 0;
|
||||||
|
music_visualizer::reset(g.visualizer);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -543,10 +693,26 @@ static void toggle_pause() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void next_track() {
|
static bool next_track(bool auto_advanced = false) {
|
||||||
if (g.file_count == 0) return;
|
if (g.file_count == 0) return false;
|
||||||
int next = (g.current_track + 1) % g.file_count;
|
if (g.current_track < 0) return start_track(0);
|
||||||
start_track(next);
|
|
||||||
|
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() {
|
static void prev_track() {
|
||||||
@@ -594,11 +760,11 @@ static void feed_audio() {
|
|||||||
// MP3 decode
|
// MP3 decode
|
||||||
if (g.mp3_offset >= g.file_size) {
|
if (g.mp3_offset >= g.file_size) {
|
||||||
// End of file — advance to next track
|
// End of file — advance to next track
|
||||||
next_track();
|
next_track(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mp3dec_frame_info_t info;
|
mp3dec_frame_info_t info = {};
|
||||||
int samples = mp3dec_decode_frame(&g.mp3dec,
|
int samples = mp3dec_decode_frame(&g.mp3dec,
|
||||||
g.file_data + g.mp3_offset,
|
g.file_data + g.mp3_offset,
|
||||||
(int)(g.file_size - g.mp3_offset),
|
(int)(g.file_size - g.mp3_offset),
|
||||||
@@ -607,17 +773,33 @@ static void feed_audio() {
|
|||||||
g.mp3_offset += info.frame_bytes;
|
g.mp3_offset += info.frame_bytes;
|
||||||
} else {
|
} else {
|
||||||
// Can't decode — end
|
// Can't decode — end
|
||||||
next_track();
|
next_track(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (samples > 0) {
|
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;
|
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 {
|
} else {
|
||||||
// WAV decode — just copy PCM data
|
// WAV decode — just copy PCM data
|
||||||
if (g.wav_pos >= g.wav_data_size) {
|
if (g.wav_pos >= g.wav_data_size) {
|
||||||
next_track();
|
next_track(true);
|
||||||
return;
|
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);
|
memcpy(g.pcm_buf, g.file_data + g.wav_data_offset + g.wav_pos, to_copy);
|
||||||
g.wav_pos += to_copy;
|
g.wav_pos += to_copy;
|
||||||
g.pcm_buf_len = (int)(to_copy / 2); // 16-bit samples
|
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
|
// 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) {
|
static void render(uint32_t* pixels) {
|
||||||
int W = g.win_w, H = g.win_h;
|
int W = g.win_w, H = g.win_h;
|
||||||
int fh = font_h();
|
int fh = font_h();
|
||||||
@@ -659,79 +861,96 @@ static void render(uint32_t* pixels) {
|
|||||||
// Toolbar
|
// Toolbar
|
||||||
px_fill(pixels, W, H, 0, 0, W, TOOLBAR_H, TOOLBAR_BG);
|
px_fill(pixels, W, H, 0, 0, W, TOOLBAR_H, TOOLBAR_BG);
|
||||||
px_hline(pixels, W, H, 0, TOOLBAR_H - 1, W, BORDER_COLOR);
|
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
|
// Directory path in toolbar
|
||||||
{
|
{
|
||||||
const char* display_dir = g.dir_path;
|
const char* display_dir = g.dir_path;
|
||||||
int dw = text_w(display_dir, FONT_SIZE_SM);
|
int dw = text_w(display_dir, FONT_SIZE_SM);
|
||||||
int max_dir_w = W - 24;
|
int max_dir_w = visualizer_rect.x - 20;
|
||||||
if (dw > max_dir_w) {
|
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;
|
display_dir = g.dir_path + montauk::slen(g.dir_path) - 20;
|
||||||
}
|
}
|
||||||
px_text(pixels, W, H, 12,
|
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 ----
|
Rect content_panel = content_panel_rect();
|
||||||
int list_bottom = H - INFO_H - PROGRESS_H - TRANSPORT_H;
|
int list_end = list_bottom();
|
||||||
int visible_items = (list_bottom - LIST_TOP) / LIST_ITEM_H;
|
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++) {
|
Color bg = BG_COLOR;
|
||||||
int idx = i + g.scroll_y;
|
if (idx == g.current_track && g.play_state != PlayState::Stopped)
|
||||||
int iy = LIST_TOP + i * LIST_ITEM_H;
|
bg = LIST_PLAYING;
|
||||||
|
else if (idx == g.hovered_item)
|
||||||
|
bg = LIST_HOVER;
|
||||||
|
|
||||||
// Highlight
|
if (bg.r != BG_COLOR.r || bg.g != BG_COLOR.g || bg.b != BG_COLOR.b)
|
||||||
Color bg = BG_COLOR;
|
px_fill(pixels, W, H, 0, iy, W, LIST_ITEM_H, bg);
|
||||||
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)
|
if (idx == g.current_track && g.play_state == PlayState::Playing) {
|
||||||
px_fill(pixels, W, H, 0, iy, W, LIST_ITEM_H, bg);
|
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
|
Color name_color = (idx == g.current_track && g.play_state != PlayState::Stopped)
|
||||||
if (idx == g.current_track && g.play_state == PlayState::Playing) {
|
? ACCENT_DARK : TEXT_COLOR;
|
||||||
px_triangle_right(pixels, W, H, 8, iy + 8, 8, 12, ACCENT);
|
px_text(pixels, W, H, 24, iy + (LIST_ITEM_H - fh_sm) / 2,
|
||||||
} else if (idx == g.current_track && g.play_state == PlayState::Paused) {
|
g.files[idx].name, name_color, FONT_SIZE_SM);
|
||||||
// Pause bars
|
|
||||||
px_fill(pixels, W, H, 8, iy + 8, 3, 12, ACCENT);
|
const char* tag = g.files[idx].is_mp3 ? "MP3" : "WAV";
|
||||||
px_fill(pixels, W, H, 13, iy + 8, 3, 12, ACCENT);
|
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
|
if (g.file_count > visible_count && visible_count > 0) {
|
||||||
Color name_color = (idx == g.current_track && g.play_state != PlayState::Stopped)
|
int list_h = list_end - LIST_TOP;
|
||||||
? ACCENT_DARK : TEXT_COLOR;
|
int sb_h = (visible_count * list_h) / g.file_count;
|
||||||
px_text(pixels, W, H, 24, iy + (LIST_ITEM_H - fh_sm) / 2,
|
if (sb_h < 20) sb_h = 20;
|
||||||
g.files[idx].name, name_color, FONT_SIZE_SM);
|
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
|
if (g.file_count == 0) {
|
||||||
const char* tag = g.files[idx].is_mp3 ? "MP3" : "WAV";
|
const char* msg = "No audio files found";
|
||||||
int tw = text_w(tag, FONT_SIZE_SM);
|
int mw = text_w(msg);
|
||||||
px_text(pixels, W, H, W - tw - 12, iy + (LIST_ITEM_H - fh_sm) / 2,
|
int cy = LIST_TOP + (list_end - LIST_TOP) / 2 - fh / 2;
|
||||||
tag, DIM_TEXT, FONT_SIZE_SM);
|
px_text(pixels, W, H, (W - mw) / 2, cy, msg, DIM_TEXT);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Scrollbar
|
px_fill(pixels, W, H, content_panel.x, content_panel.y, content_panel.w, content_panel.h, TOOLBAR_BG);
|
||||||
if (g.file_count > visible_items && visible_items > 0) {
|
Rect viz_rect = {12, content_panel.y + 12, W - 24, content_panel.h - 24};
|
||||||
int list_h = list_bottom - LIST_TOP;
|
music_visualizer::render(pixels, W, H, viz_rect, g.visualizer,
|
||||||
int sb_h = (visible_items * list_h) / g.file_count;
|
ACCENT, ACCENT_DARK, TRACK_BG, BORDER_COLOR);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Now Playing info ----
|
// ---- 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_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);
|
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
|
// 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) {
|
static bool handle_click(int mx, int my) {
|
||||||
int W = g.win_w;
|
int W = g.win_w;
|
||||||
int lb = list_bottom();
|
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 info_y = lb + 1;
|
||||||
int prog_y = info_y + INFO_H;
|
int prog_y = info_y + INFO_H;
|
||||||
int trans_y = prog_y + PROGRESS_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
|
// 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;
|
int idx = (my - LIST_TOP) / LIST_ITEM_H + g.scroll_y;
|
||||||
if (idx >= 0 && idx < g.file_count) {
|
if (idx >= 0 && idx < g.file_count) {
|
||||||
start_track(idx);
|
start_track(idx);
|
||||||
@@ -845,27 +1074,8 @@ static bool handle_click(int mx, int my) {
|
|||||||
int rel = mx - bar_x;
|
int rel = mx - bar_x;
|
||||||
if (rel < 0) rel = 0;
|
if (rel < 0) rel = 0;
|
||||||
if (rel > bar_w) rel = bar_w;
|
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;
|
uint64_t target_samples = (uint64_t)rel * g.total_samples / bar_w;
|
||||||
|
seek_to_samples(target_samples);
|
||||||
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;
|
|
||||||
}
|
|
||||||
g.dragging_progress = true;
|
g.dragging_progress = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -901,7 +1111,7 @@ static bool handle_click(int mx, int my) {
|
|||||||
bx += btn_w + gap;
|
bx += btn_w + gap;
|
||||||
|
|
||||||
// Next
|
// 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.current_track = -1;
|
||||||
g.play_state = PlayState::Stopped;
|
g.play_state = PlayState::Stopped;
|
||||||
g.hovered_item = -1;
|
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
|
// Parse arguments: accept a directory path or a file path
|
||||||
// Helper: set dir_path to current user's home via session config
|
// 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_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_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_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
|
// Scan for audio files
|
||||||
@@ -1053,8 +1275,20 @@ extern "C" void _start() {
|
|||||||
stop_playback();
|
stop_playback();
|
||||||
redraw = true;
|
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
|
if (ev.key.scancode == 0x4D) { // Right arrow
|
||||||
next_track();
|
next_track(false);
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
if (ev.key.scancode == 0x4B) { // Left arrow
|
if (ev.key.scancode == 0x4B) { // Left arrow
|
||||||
@@ -1084,26 +1318,13 @@ extern "C" void _start() {
|
|||||||
if (rel < 0) rel = 0;
|
if (rel < 0) rel = 0;
|
||||||
if (rel > bar_w) rel = bar_w;
|
if (rel > bar_w) rel = bar_w;
|
||||||
uint64_t target_samples = (uint64_t)rel * g.total_samples / bar_w;
|
uint64_t target_samples = (uint64_t)rel * g.total_samples / bar_w;
|
||||||
|
seek_to_samples(target_samples);
|
||||||
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;
|
|
||||||
}
|
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hover tracking for file list
|
// Hover tracking for file list
|
||||||
int lb = list_bottom();
|
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;
|
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.file_count) new_hover = -1;
|
||||||
if (new_hover != g.hovered_item) {
|
if (new_hover != g.hovered_item) {
|
||||||
@@ -1116,7 +1337,7 @@ extern "C" void _start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scroll
|
// Scroll
|
||||||
if (ev.mouse.scroll != 0) {
|
if (!g.show_visualizer && ev.mouse.scroll != 0) {
|
||||||
g.scroll_y -= ev.mouse.scroll * 2;
|
g.scroll_y -= ev.mouse.scroll * 2;
|
||||||
int max_scroll = g.file_count - visible_items();
|
int max_scroll = g.file_count - visible_items();
|
||||||
if (max_scroll < 0) max_scroll = 0;
|
if (max_scroll < 0) max_scroll = 0;
|
||||||
@@ -1142,11 +1363,12 @@ extern "C" void _start() {
|
|||||||
|
|
||||||
// Periodic redraw while playing (update time display)
|
// Periodic redraw while playing (update time display)
|
||||||
uint64_t now = montauk::get_milliseconds();
|
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;
|
redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redraw) {
|
if (redraw) {
|
||||||
|
music_visualizer::tick(g.visualizer);
|
||||||
render(pixels);
|
render(pixels);
|
||||||
montauk::win_present(win_id);
|
montauk::win_present(win_id);
|
||||||
last_render = now;
|
last_render = now;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <gui/gui.hpp>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -101,6 +101,13 @@ ICONS=(
|
|||||||
"actions/16/media-stop.svg"
|
"actions/16/media-stop.svg"
|
||||||
"actions/16/media-forward.svg"
|
"actions/16/media-forward.svg"
|
||||||
"actions/16/media-rewind.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/multimedia-video-player.svg"
|
||||||
"apps/scalable/bluetooth.svg"
|
"apps/scalable/bluetooth.svg"
|
||||||
"panel/volume-level-high.svg"
|
"panel/volume-level-high.svg"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
|
#include <montauk/string.h>
|
||||||
#include "gui/gui.hpp"
|
#include "gui/gui.hpp"
|
||||||
|
|
||||||
namespace gui {
|
namespace gui {
|
||||||
@@ -182,13 +183,11 @@ public:
|
|||||||
|
|
||||||
inline void flip() {
|
inline void flip() {
|
||||||
// Copy back buffer to hardware framebuffer, row by row (pitch may differ)
|
// 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++) {
|
for (int y = 0; y < fb_height; y++) {
|
||||||
uint32_t* src = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch);
|
void* src = (void*)((uint8_t*)back_buf + y * fb_pitch);
|
||||||
uint32_t* dst = (uint32_t*)((uint8_t*)hw_fb + y * fb_pitch);
|
void* dst = (void*)((uint8_t*)hw_fb + y * fb_pitch);
|
||||||
for (int x = 0; x < row_pixels; x++) {
|
montauk::memcpy(dst, src, row_bytes);
|
||||||
dst[x] = src[x];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user