fix: music app improvements, desktop performance improvements

This commit is contained in:
2026-03-25 19:13:26 +01:00
parent ea087769f5
commit 1ad9117fc1
12 changed files with 664 additions and 172 deletions
+1
View File
@@ -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;
}
} }
// ============================================================================ // ============================================================================
+1 -1
View File
@@ -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);
+87 -13
View File
@@ -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);
} }
} }
+22 -3
View File
@@ -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);
}
} }
} }
+1 -1
View File
@@ -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
View File
@@ -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;
+140
View File
@@ -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
+23
View File
@@ -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
+7
View File
@@ -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"
+5 -6
View File
@@ -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];
}
} }
} }
}; };