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