feat: add Music app features, fix Window Server bug

This commit is contained in:
2026-03-27 15:37:34 +01:00
parent 8f118c7a0f
commit 994c32c09d
5 changed files with 623 additions and 173 deletions
+124 -53
View File
@@ -12,12 +12,24 @@
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Spinlock.hpp> #include <CppLib/Spinlock.hpp>
#include <Sched/Scheduler.hpp> #include <Sched/Scheduler.hpp>
#include <Timekeeping/ApicTimer.hpp>
namespace WinServer { namespace WinServer {
static WindowSlot g_slots[MaxWindows]; static WindowSlot g_slots[MaxWindows];
static int g_uiScale = 1; static int g_uiScale = 1;
static kcp::Mutex wsLock; static kcp::Mutex wsLock;
static constexpr uint64_t RetireGraceMs = 2000;
static constexpr int MaxRetiredBatches = MaxWindows * 4;
struct RetiredBatch {
bool used;
int numPages;
uint64_t freeAfterMs;
uint64_t physPages[MaxPixelPages];
};
static RetiredBatch g_retired[MaxRetiredBatches];
// RAII lock guard for WinServer operations // RAII lock guard for WinServer operations
struct WsGuard { struct WsGuard {
@@ -25,9 +37,85 @@ namespace WinServer {
~WsGuard() { wsLock.Release(); } ~WsGuard() { wsLock.Release(); }
}; };
static void FreePageBatchLocked(uint64_t* physPages, int numPages) {
for (int i = 0; i < numPages; i++) {
if (physPages[i] != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(physPages[i]));
physPages[i] = 0;
}
}
}
static void ProcessRetiredLocked() {
uint64_t now = Timekeeping::GetMilliseconds();
for (int i = 0; i < MaxRetiredBatches; i++) {
if (!g_retired[i].used) continue;
if (now < g_retired[i].freeAfterMs) continue;
FreePageBatchLocked(g_retired[i].physPages, g_retired[i].numPages);
g_retired[i].used = false;
g_retired[i].numPages = 0;
g_retired[i].freeAfterMs = 0;
}
}
static void RetirePageBatchLocked(uint64_t* physPages, int numPages) {
if (numPages <= 0) return;
int retireIdx = -1;
for (int i = 0; i < MaxRetiredBatches; i++) {
if (!g_retired[i].used) {
retireIdx = i;
break;
}
}
if (retireIdx < 0) {
ProcessRetiredLocked();
for (int i = 0; i < MaxRetiredBatches; i++) {
if (!g_retired[i].used) {
retireIdx = i;
break;
}
}
}
if (retireIdx < 0) {
uint64_t oldest = ~0ULL;
for (int i = 0; i < MaxRetiredBatches; i++) {
if (g_retired[i].freeAfterMs < oldest) {
oldest = g_retired[i].freeAfterMs;
retireIdx = i;
}
}
if (retireIdx >= 0) {
Kt::KernelLogStream(Kt::ERROR, "WinServer")
<< "Retire queue full, forcing early free of stale window pages";
FreePageBatchLocked(g_retired[retireIdx].physPages, g_retired[retireIdx].numPages);
g_retired[retireIdx].used = false;
g_retired[retireIdx].numPages = 0;
g_retired[retireIdx].freeAfterMs = 0;
}
}
if (retireIdx < 0) {
FreePageBatchLocked(physPages, numPages);
return;
}
RetiredBatch& batch = g_retired[retireIdx];
batch.used = true;
batch.numPages = numPages;
batch.freeAfterMs = Timekeeping::GetMilliseconds() + RetireGraceMs;
for (int i = 0; i < numPages; i++) {
batch.physPages[i] = physPages[i];
physPages[i] = 0;
}
}
int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h, int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h,
uint64_t& heapNext, uint64_t& outVa) { uint64_t& heapNext, uint64_t& outVa) {
WsGuard guard; WsGuard guard;
ProcessRetiredLocked();
// Find a free slot // Find a free slot
int slotIdx = -1; int slotIdx = -1;
for (int i = 0; i < MaxWindows; i++) { for (int i = 0; i < MaxWindows; i++) {
@@ -102,20 +190,15 @@ namespace WinServer {
int Destroy(int windowId, int callerPid) { int Destroy(int windowId, int callerPid) {
WsGuard guard; WsGuard guard;
ProcessRetiredLocked();
if (windowId < 0 || windowId >= MaxWindows) return -1; if (windowId < 0 || windowId >= MaxWindows) return -1;
WindowSlot& slot = g_slots[windowId]; WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1; if (!slot.used || slot.ownerPid != callerPid) return -1;
// Do NOT unmap snapshot pages from the desktop here -- the desktop
// may be running on another CPU with stale TLB entries. Unmapping
// causes a page fault when the TLB entry is evicted. Just free the
// physical pages (PTEs stay present, pointing to freed memory --
// the desktop reads garbage for at most one frame before its next
// Enumerate sees the window is gone). FreeUserHalf reclaims the
// desktop's page table entries when the desktop eventually exits.
// Unmap LIVE pixel pages from the owner's address space so that // Unmap LIVE pixel pages from the owner's address space so that
// FreeUserHalf() won't double-free them when the process exits. // FreeUserHalf() won't double-free them when the process exits.
// Physical page reclamation is deferred below to avoid stale-TLB
// consumers reading freed memory.
{ {
auto* ownerProc = Sched::GetProcessByPid(slot.ownerPid); auto* ownerProc = Sched::GetProcessByPid(slot.ownerPid);
if (ownerProc) { if (ownerProc) {
@@ -127,23 +210,26 @@ namespace WinServer {
} }
} }
// Free physical pixel pages and snapshot pages // Retire both the owner-facing live pages and the compositor-facing
for (int i = 0; i < slot.pixelNumPages; i++) { // snapshot pages. The desktop can still hold stale mappings to the
if (slot.pixelPhysPages[i] != 0) { // snapshot pages for a short time after Enumerate notices the window
Memory::g_pfa->Free((void*)Memory::HHDM(slot.pixelPhysPages[i])); // is gone, so freeing them immediately turns that stale mapping into
} // a use-after-free.
if (slot.snapshotPhysPages[i] != 0) { RetirePageBatchLocked(slot.pixelPhysPages, slot.pixelNumPages);
Memory::g_pfa->Free((void*)Memory::HHDM(slot.snapshotPhysPages[i])); RetirePageBatchLocked(slot.snapshotPhysPages, slot.pixelNumPages);
}
}
slot.used = false; slot.used = false;
slot.pixelNumPages = 0;
slot.ownerVa = 0;
slot.desktopVa = 0;
slot.desktopPid = 0;
return 0; return 0;
} }
int Present(int windowId, int callerPid) { int Present(int windowId, int callerPid) {
// Validate ownership under the lock (brief) // Validate ownership under the lock (brief)
wsLock.Acquire(); wsLock.Acquire();
ProcessRetiredLocked();
if (windowId < 0 || windowId >= MaxWindows) { wsLock.Release(); return -1; } if (windowId < 0 || windowId >= MaxWindows) { wsLock.Release(); return -1; }
WindowSlot& slot = g_slots[windowId]; WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) { wsLock.Release(); return -1; } if (!slot.used || slot.ownerPid != callerPid) { wsLock.Release(); return -1; }
@@ -170,6 +256,7 @@ namespace WinServer {
int Poll(int windowId, int callerPid, Montauk::WinEvent* outEvent) { int Poll(int windowId, int callerPid, Montauk::WinEvent* outEvent) {
WsGuard guard; WsGuard guard;
ProcessRetiredLocked();
if (windowId < 0 || windowId >= MaxWindows) return -1; if (windowId < 0 || windowId >= MaxWindows) return -1;
WindowSlot& slot = g_slots[windowId]; WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1; if (!slot.used || slot.ownerPid != callerPid) return -1;
@@ -183,6 +270,7 @@ namespace WinServer {
int Enumerate(Montauk::WinInfo* outArray, int maxCount) { int Enumerate(Montauk::WinInfo* outArray, int maxCount) {
WsGuard guard; WsGuard guard;
ProcessRetiredLocked();
int count = 0; int count = 0;
for (int i = 0; i < MaxWindows && count < maxCount; i++) { for (int i = 0; i < MaxWindows && count < maxCount; i++) {
if (!g_slots[i].used) continue; if (!g_slots[i].used) continue;
@@ -202,6 +290,7 @@ namespace WinServer {
uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext) { uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext) {
WsGuard guard; WsGuard guard;
ProcessRetiredLocked();
if (windowId < 0 || windowId >= MaxWindows) return 0; if (windowId < 0 || windowId >= MaxWindows) return 0;
WindowSlot& slot = g_slots[windowId]; WindowSlot& slot = g_slots[windowId];
if (!slot.used) return 0; if (!slot.used) return 0;
@@ -245,12 +334,14 @@ namespace WinServer {
int SendEvent(int windowId, const Montauk::WinEvent* event) { int SendEvent(int windowId, const Montauk::WinEvent* event) {
WsGuard guard; WsGuard guard;
ProcessRetiredLocked();
return SendEventLocked(windowId, event); return SendEventLocked(windowId, event);
} }
int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH, int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH,
uint64_t& heapNext, uint64_t& outVa) { uint64_t& heapNext, uint64_t& outVa) {
WsGuard guard; WsGuard guard;
ProcessRetiredLocked();
if (windowId < 0 || windowId >= MaxWindows) return -1; if (windowId < 0 || windowId >= MaxWindows) return -1;
WindowSlot& slot = g_slots[windowId]; WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1; if (!slot.used || slot.ownerPid != callerPid) return -1;
@@ -264,34 +355,17 @@ namespace WinServer {
int numPages = (int)((bufSize + 0xFFF) / 0x1000); int numPages = (int)((bufSize + 0xFFF) / 0x1000);
if (numPages > MaxPixelPages) return -1; if (numPages > MaxPixelPages) return -1;
// Do NOT unmap/free snapshot pages from the desktop here. // Unmap old LIVE pixel pages from the owner's address space. Actual
// The desktop may be running on another CPU with stale TLB entries // physical page reclamation is deferred below for both live and
// pointing to the old snapshot pages. Unmapping + freeing them // snapshot pages so stale desktop mappings cannot turn into
// causes a page fault on the desktop when the TLB entry is evicted. // use-after-free reads during remap.
// Instead, leave them mapped -- the desktop will get new snapshot
// pages on its next Map call (desktopVa is invalidated below).
// The old snapshot pages remain in the desktop's page tables and
// are reclaimed by FreeUserHalf when the desktop exits.
// Unmap old LIVE pixel pages from owner's address space and free them.
// Free old snapshot pages too (don't leak). The desktop's PTEs still
// point to the old physical addresses, but we don't clear them (no
// UnmapUserIn for desktop) to avoid TLB-eviction page faults. The
// desktop may read one frame of garbage from the freed pages before
// re-mapping -- acceptable during a resize.
int oldNumPages = slot.pixelNumPages; int oldNumPages = slot.pixelNumPages;
for (int i = 0; i < oldNumPages; i++) { for (int i = 0; i < oldNumPages; i++) {
Memory::VMM::Paging::UnmapUserIn( Memory::VMM::Paging::UnmapUserIn(
ownerPml4, slot.ownerVa + (uint64_t)i * 0x1000); ownerPml4, slot.ownerVa + (uint64_t)i * 0x1000);
if (slot.pixelPhysPages[i] != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(slot.pixelPhysPages[i]));
slot.pixelPhysPages[i] = 0;
}
if (slot.snapshotPhysPages[i] != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(slot.snapshotPhysPages[i]));
slot.snapshotPhysPages[i] = 0;
}
} }
RetirePageBatchLocked(slot.pixelPhysPages, oldNumPages);
RetirePageBatchLocked(slot.snapshotPhysPages, oldNumPages);
// Allocate new live + snapshot pages and map live into owner's address space // Allocate new live + snapshot pages and map live into owner's address space
uint64_t userVa = heapNext; uint64_t userVa = heapNext;
@@ -325,6 +399,7 @@ namespace WinServer {
int SetCursor(int windowId, int callerPid, int cursor) { int SetCursor(int windowId, int callerPid, int cursor) {
WsGuard guard; WsGuard guard;
ProcessRetiredLocked();
if (windowId < 0 || windowId >= MaxWindows) return -1; if (windowId < 0 || windowId >= MaxWindows) return -1;
WindowSlot& slot = g_slots[windowId]; WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1; if (!slot.used || slot.ownerPid != callerPid) return -1;
@@ -334,6 +409,7 @@ namespace WinServer {
int SetScale(int scale) { int SetScale(int scale) {
WsGuard guard; WsGuard guard;
ProcessRetiredLocked();
if (scale < 0) scale = 0; if (scale < 0) scale = 0;
if (scale > 2) scale = 2; if (scale > 2) scale = 2;
g_uiScale = scale; g_uiScale = scale;
@@ -357,30 +433,25 @@ namespace WinServer {
void CleanupProcess(int pid) { void CleanupProcess(int pid) {
WsGuard guard; WsGuard guard;
ProcessRetiredLocked();
for (int i = 0; i < MaxWindows; i++) { for (int i = 0; i < MaxWindows; i++) {
if (g_slots[i].used && g_slots[i].ownerPid == pid) { if (g_slots[i].used && g_slots[i].ownerPid == pid) {
Kt::KernelLogStream(Kt::INFO, "WinServer") << "Cleaning up window " Kt::KernelLogStream(Kt::INFO, "WinServer") << "Cleaning up window "
<< i << " for exited PID " << pid; << i << " for exited PID " << pid;
// Do NOT unmap snapshot pages from the desktop here -- the
// desktop may be on another CPU with stale TLB entries.
// Just free the physical pages below (PTEs stay present,
// desktop reads garbage for one frame, then its next
// Enumerate sees the window is gone).
// Do NOT free live pixel pages here -- they are still mapped // Do NOT free live pixel pages here -- they are still mapped
// in the owner's page tables and FreeUserHalf() (called right // in the owner's page tables and FreeUserHalf() (called right
// after CleanupProcess) will free them. // after CleanupProcess) will free them.
// Free snapshot pages (not in the owner's page tables). // Snapshot pages can still be mapped into the desktop, so
for (int p = 0; p < g_slots[i].pixelNumPages; p++) { // retire them instead of freeing them immediately.
if (g_slots[i].snapshotPhysPages[p] != 0) { RetirePageBatchLocked(g_slots[i].snapshotPhysPages, g_slots[i].pixelNumPages);
Memory::g_pfa->Free((void*)Memory::HHDM(g_slots[i].snapshotPhysPages[p]));
g_slots[i].snapshotPhysPages[p] = 0;
}
}
g_slots[i].used = false; g_slots[i].used = false;
g_slots[i].pixelNumPages = 0;
g_slots[i].ownerVa = 0;
g_slots[i].desktopVa = 0;
g_slots[i].desktopPid = 0;
} }
// If this process had windows mapped INTO it (was the desktop viewer), // If this process had windows mapped INTO it (was the desktop viewer),
+10
View File
@@ -398,6 +398,16 @@ bool desktop_poll_external_windows(DesktopState* ds) {
for (int i = 0; i < ds->window_count; i++) { for (int i = 0; i < ds->window_count; i++) {
if (ds->windows[i].external && ds->windows[i].ext_win_id == extId) { if (ds->windows[i].external && ds->windows[i].ext_win_id == extId) {
found = true; found = true;
if (ds->windows[i].content_w != extWins[e].width ||
ds->windows[i].content_h != extWins[e].height) {
ds->windows[i].content_w = extWins[e].width;
ds->windows[i].content_h = extWins[e].height;
ds->windows[i].dirty = true;
if (ds->windows[i].state != WIN_MINIMIZED && ds->windows[i].state != WIN_CLOSED) {
changed = true;
}
}
montauk::strncpy(ds->windows[i].title, extWins[e].title, MAX_TITLE_LEN);
// Update dirty flag and cursor // Update dirty flag and cursor
if (extWins[e].dirty) { if (extWins[e].dirty) {
ds->windows[i].dirty = true; ds->windows[i].dirty = true;
+327 -51
View File
@@ -45,6 +45,7 @@ static constexpr int TRANSPORT_H = 44;
static constexpr int MAX_FILES = 128; static constexpr int MAX_FILES = 128;
static constexpr int MAX_PATH = 256; static constexpr int MAX_PATH = 256;
static constexpr int MAX_FILTER = 64;
// Decode buffer: one MP3 frame = up to 1152 samples * 2 channels // Decode buffer: one MP3 frame = up to 1152 samples * 2 channels
static constexpr int PCM_BUF_SAMPLES = 1152 * 2; static constexpr int PCM_BUF_SAMPLES = 1152 * 2;
@@ -62,6 +63,7 @@ static constexpr Color TRACK_BG = Color::from_rgb(0xDD, 0xDD, 0xDD);
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF); static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr Color LIST_HOVER = Color::from_rgb(0xE8, 0xF0, 0xFD); static constexpr Color LIST_HOVER = Color::from_rgb(0xE8, 0xF0, 0xFD);
static constexpr Color LIST_PLAYING = Color::from_rgb(0xD0, 0xE2, 0xFB); static constexpr Color LIST_PLAYING = Color::from_rgb(0xD0, 0xE2, 0xFB);
static constexpr Color LIST_SELECTED = Color::from_rgb(0xF0, 0xF5, 0xFC);
static constexpr Color BTN_BG = Color::from_rgb(0xE8, 0xE8, 0xE8); static constexpr Color BTN_BG = Color::from_rgb(0xE8, 0xE8, 0xE8);
static constexpr Color BTN_HOVER = Color::from_rgb(0xD8, 0xD8, 0xD8); static constexpr Color BTN_HOVER = Color::from_rgb(0xD8, 0xD8, 0xD8);
static constexpr Color STOP_COLOR = Color::from_rgb(0xCC, 0x33, 0x33); static constexpr Color STOP_COLOR = Color::from_rgb(0xCC, 0x33, 0x33);
@@ -100,6 +102,11 @@ struct PlayerState {
// List UI // List UI
int scroll_y; int scroll_y;
int hovered_item; int hovered_item;
int selected_item;
int filtered_indices[MAX_FILES];
int filtered_count;
char filter_text[MAX_FILTER];
bool filter_active;
// Playback // Playback
PlayState play_state; PlayState play_state;
@@ -166,6 +173,8 @@ struct PlayerState {
static PlayerState g; static PlayerState g;
static int visible_items();
// ============================================================================ // ============================================================================
// Pixel helpers // Pixel helpers
// ============================================================================ // ============================================================================
@@ -366,6 +375,151 @@ static Rect visualizer_button_rect() {
return {shuffle.x - TOOL_ICON_GAP - TOOL_ICON_W, shuffle.y, TOOL_ICON_W, TOOL_ICON_H}; return {shuffle.x - TOOL_ICON_GAP - TOOL_ICON_W, shuffle.y, TOOL_ICON_W, TOOL_ICON_H};
} }
static char ascii_lower(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
}
static bool is_printable_ascii(char c) {
return c >= 32 && c < 127;
}
static bool str_contains_case_insensitive(const char* text, const char* needle) {
if (!needle || !needle[0]) return true;
if (!text || !text[0]) return false;
int text_len = montauk::slen(text);
int needle_len = montauk::slen(needle);
if (needle_len > text_len) return false;
for (int i = 0; i <= text_len - needle_len; i++) {
bool match = true;
for (int j = 0; j < needle_len; j++) {
if (ascii_lower(text[i + j]) != ascii_lower(needle[j])) {
match = false;
break;
}
}
if (match) return true;
}
return false;
}
static int filtered_raw_at(int visible_index) {
if (visible_index < 0 || visible_index >= g.filtered_count) return -1;
return g.filtered_indices[visible_index];
}
static bool filtered_contains(int raw_index) {
if (raw_index < 0) return false;
for (int i = 0; i < g.filtered_count; i++) {
if (g.filtered_indices[i] == raw_index) return true;
}
return false;
}
static int selected_visible_index() {
for (int i = 0; i < g.filtered_count; i++) {
if (g.filtered_indices[i] == g.selected_item) return i;
}
return -1;
}
static void clamp_list_scroll() {
int max_scroll = g.filtered_count - visible_items();
if (max_scroll < 0) max_scroll = 0;
if (g.scroll_y < 0) g.scroll_y = 0;
if (g.scroll_y > max_scroll) g.scroll_y = max_scroll;
}
static void ensure_selected_visible() {
int sel = selected_visible_index();
if (sel < 0) {
clamp_list_scroll();
return;
}
int visible = visible_items();
if (visible <= 0) {
g.scroll_y = 0;
return;
}
if (sel < g.scroll_y) g.scroll_y = sel;
if (sel >= g.scroll_y + visible) g.scroll_y = sel - visible + 1;
clamp_list_scroll();
}
static void rebuild_filtered_view() {
int prev_selected = g.selected_item;
g.filtered_count = 0;
for (int i = 0; i < g.file_count && g.filtered_count < MAX_FILES; i++) {
if (str_contains_case_insensitive(g.files[i].name, g.filter_text)) {
g.filtered_indices[g.filtered_count++] = i;
}
}
if (!filtered_contains(g.hovered_item)) g.hovered_item = -1;
if (g.filtered_count <= 0) {
g.selected_item = -1;
g.scroll_y = 0;
return;
}
if (filtered_contains(prev_selected)) {
g.selected_item = prev_selected;
} else if (filtered_contains(g.current_track)) {
g.selected_item = g.current_track;
} else {
g.selected_item = g.filtered_indices[0];
}
clamp_list_scroll();
ensure_selected_visible();
}
static void append_filter_char(char c) {
int len = montauk::slen(g.filter_text);
if (len >= MAX_FILTER - 1) return;
g.filter_text[len] = c;
g.filter_text[len + 1] = '\0';
rebuild_filtered_view();
}
static void pop_filter_char() {
int len = montauk::slen(g.filter_text);
if (len <= 0) return;
g.filter_text[len - 1] = '\0';
rebuild_filtered_view();
}
static void clear_filter() {
if (!g.filter_text[0]) return;
g.filter_text[0] = '\0';
rebuild_filtered_view();
}
static void move_selection(int delta) {
if (g.filtered_count <= 0) {
g.selected_item = -1;
g.scroll_y = 0;
return;
}
int sel = selected_visible_index();
if (sel < 0) {
sel = delta >= 0 ? 0 : g.filtered_count - 1;
} else {
sel += delta;
if (sel < 0) sel = 0;
if (sel >= g.filtered_count) sel = g.filtered_count - 1;
}
g.selected_item = g.filtered_indices[sel];
ensure_selected_visible();
}
// ============================================================================ // ============================================================================
// Directory scanning // Directory scanning
// ============================================================================ // ============================================================================
@@ -389,6 +543,8 @@ static void scan_directory() {
f.is_mp3 = is_mp3; f.is_mp3 = is_mp3;
g.file_count++; g.file_count++;
} }
rebuild_filtered_view();
} }
// ============================================================================ // ============================================================================
@@ -680,6 +836,13 @@ static bool start_track(int index) {
g.mp3_seek_discard_samples = 0; g.mp3_seek_discard_samples = 0;
music_visualizer::reset(g.visualizer); music_visualizer::reset(g.visualizer);
if (filtered_contains(index)) {
g.selected_item = index;
} else if (g.filtered_count > 0 && selected_visible_index() < 0) {
g.selected_item = g.filtered_indices[0];
}
ensure_selected_visible();
return true; return true;
} }
@@ -693,6 +856,16 @@ static void toggle_pause() {
} }
} }
static bool play_selected_or_default() {
if (g.selected_item >= 0 && filtered_contains(g.selected_item)) return start_track(g.selected_item);
if (g.filtered_count > 0) return start_track(g.filtered_indices[0]);
if (g.filter_text[0]) return false;
if (g.selected_item >= 0) return start_track(g.selected_item);
if (g.current_track >= 0) return start_track(g.current_track);
if (g.file_count > 0) return start_track(0);
return false;
}
static bool next_track(bool auto_advanced = false) { static bool next_track(bool auto_advanced = false) {
if (g.file_count == 0) return false; if (g.file_count == 0) return false;
if (g.current_track < 0) return start_track(0); if (g.current_track < 0) return start_track(0);
@@ -880,9 +1053,45 @@ static void render(uint32_t* pixels) {
// Directory path in toolbar // Directory path in toolbar
{ {
int max_dir_w = visualizer_rect.x - 20;
if (g.filter_active || g.filter_text[0]) {
char filter_query[24];
char filter_label[48];
const char* query = g.filter_text;
int qlen = montauk::slen(query);
if (qlen > 15) {
montauk::memcpy(filter_query, "...", 3);
montauk::memcpy(filter_query + 3, query + qlen - 15, 15);
filter_query[18] = '\0';
query = filter_query;
}
if (g.filter_text[0]) {
snprintf(filter_label, sizeof(filter_label), "%s%s%s",
g.filter_active ? "Find: " : "Filter: ",
query,
g.filter_active ? "_" : "");
} else {
snprintf(filter_label, sizeof(filter_label), "%s",
g.filter_active ? "Find" : "Filter");
}
int pill_h = TOOL_ICON_H;
int pill_w = text_w(filter_label, FONT_SIZE_SM) + 14;
int pill_x = visualizer_rect.x - TOOL_ICON_GAP - pill_w;
int pill_y = (TOOLBAR_H - pill_h) / 2;
Color pill_bg = g.filter_active ? LIST_HOVER : BTN_BG;
Color pill_fg = g.filter_active ? ACCENT_DARK : DIM_TEXT;
if (pill_x > 80) {
px_fill_rounded(pixels, W, H, pill_x, pill_y, pill_w, pill_h, 6, pill_bg);
px_text(pixels, W, H, pill_x + 7, pill_y + (pill_h - fh_sm) / 2,
filter_label, pill_fg, FONT_SIZE_SM);
max_dir_w = pill_x - 12;
}
}
const char* display_dir = g.dir_path; 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 = visualizer_rect.x - 20;
int dir_len = montauk::slen(g.dir_path); int dir_len = montauk::slen(g.dir_path);
if (dw > max_dir_w && dir_len > 20) { 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;
@@ -897,19 +1106,25 @@ static void render(uint32_t* pixels) {
px_hline(pixels, W, H, 0, list_end, W, BORDER_COLOR); px_hline(pixels, W, H, 0, list_end, W, BORDER_COLOR);
if (!g.show_visualizer) { if (!g.show_visualizer) {
for (int i = 0; i < visible_count && (i + g.scroll_y) < g.file_count; i++) { for (int i = 0; i < visible_count && (i + g.scroll_y) < g.filtered_count; i++) {
int idx = i + g.scroll_y; int idx = filtered_raw_at(i + g.scroll_y);
if (idx < 0) break;
int iy = LIST_TOP + i * LIST_ITEM_H; int iy = LIST_TOP + i * LIST_ITEM_H;
Color bg = BG_COLOR; Color bg = BG_COLOR;
if (idx == g.current_track && g.play_state != PlayState::Stopped) if (idx == g.current_track && g.play_state != PlayState::Stopped)
bg = LIST_PLAYING; bg = LIST_PLAYING;
else if (idx == g.selected_item)
bg = LIST_SELECTED;
else if (idx == g.hovered_item) else if (idx == g.hovered_item)
bg = LIST_HOVER; bg = LIST_HOVER;
if (bg.r != BG_COLOR.r || bg.g != BG_COLOR.g || bg.b != BG_COLOR.b) 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); px_fill(pixels, W, H, 0, iy, W, LIST_ITEM_H, bg);
if (idx == g.selected_item && !(idx == g.current_track && g.play_state != PlayState::Stopped))
px_fill(pixels, W, H, 0, iy, 3, LIST_ITEM_H, ACCENT);
if (idx == g.current_track && g.play_state == PlayState::Playing) { if (idx == g.current_track && g.play_state == PlayState::Playing) {
px_triangle_right(pixels, W, H, 8, iy + 8, 8, 12, ACCENT); px_triangle_right(pixels, W, H, 8, iy + 8, 8, 12, ACCENT);
} else if (idx == g.current_track && g.play_state == PlayState::Paused) { } else if (idx == g.current_track && g.play_state == PlayState::Paused) {
@@ -928,19 +1143,25 @@ static void render(uint32_t* pixels) {
tag, DIM_TEXT, FONT_SIZE_SM); tag, DIM_TEXT, FONT_SIZE_SM);
} }
if (g.file_count > visible_count && visible_count > 0) { if (g.filtered_count > visible_count && visible_count > 0) {
int list_h = list_end - LIST_TOP; int list_h = list_end - LIST_TOP;
int sb_h = (visible_count * list_h) / g.file_count; int sb_h = (visible_count * list_h) / g.filtered_count;
if (sb_h < 20) sb_h = 20; if (sb_h < 20) sb_h = 20;
int sb_y = LIST_TOP + (g.scroll_y * (list_h - sb_h)) / (g.file_count - visible_count); int sb_y = LIST_TOP + (g.scroll_y * (list_h - sb_h)) / (g.filtered_count - visible_count);
px_fill_rounded(pixels, W, H, W - 6, sb_y, 4, sb_h, 2, TRACK_BG); px_fill_rounded(pixels, W, H, W - 6, sb_y, 4, sb_h, 2, TRACK_BG);
} }
if (g.file_count == 0) { if (g.filtered_count == 0) {
const char* msg = "No audio files found"; const char* msg = g.file_count == 0 ? "No audio files found" : "No matches for filter";
int mw = text_w(msg); int mw = text_w(msg);
int cy = LIST_TOP + (list_end - LIST_TOP) / 2 - fh / 2; int cy = LIST_TOP + (list_end - LIST_TOP) / 2 - fh / 2;
px_text(pixels, W, H, (W - mw) / 2, cy, msg, DIM_TEXT); px_text(pixels, W, H, (W - mw) / 2, cy, msg, DIM_TEXT);
if (g.file_count > 0 && g.filter_text[0]) {
char detail[96];
snprintf(detail, sizeof(detail), "Current filter: %s", g.filter_text);
int dw = text_w(detail, FONT_SIZE_SM);
px_text(pixels, W, H, (W - dw) / 2, cy + fh_sm + 6, detail, DIM_TEXT, FONT_SIZE_SM);
}
} }
} else { } else {
px_fill(pixels, W, H, content_panel.x, content_panel.y, content_panel.w, content_panel.h, TOOLBAR_BG); px_fill(pixels, W, H, content_panel.x, content_panel.y, content_panel.w, content_panel.h, TOOLBAR_BG);
@@ -1060,8 +1281,8 @@ static bool handle_click(int mx, int my) {
// File list click // File list click
if (!g.show_visualizer && 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 = filtered_raw_at((my - LIST_TOP) / LIST_ITEM_H + g.scroll_y);
if (idx >= 0 && idx < g.file_count) { if (idx >= 0) {
start_track(idx); start_track(idx);
return true; return true;
} }
@@ -1095,10 +1316,8 @@ static bool handle_click(int mx, int my) {
// Play/Pause // Play/Pause
if (mx >= bx && mx < bx + btn_w) { if (mx >= bx && mx < bx + btn_w) {
if (g.play_state == PlayState::Stopped && g.current_track >= 0) { if (g.play_state == PlayState::Stopped) {
start_track(g.current_track); play_selected_or_default();
} else if (g.play_state == PlayState::Stopped && g.file_count > 0) {
start_track(0);
} else { } else {
toggle_pause(); toggle_pause();
} }
@@ -1118,6 +1337,94 @@ static bool handle_click(int mx, int my) {
return false; return false;
} }
static bool handle_key(const Montauk::KeyEvent& key, bool& quit) {
if (!key.pressed) return false;
if (key.scancode == 0x01) { // Escape
if (g.filter_active) {
if (g.filter_text[0]) clear_filter();
else g.filter_active = false;
return true;
}
quit = true;
return false;
}
if (key.ctrl && (key.ascii == 'f' || key.ascii == 'F' || key.ascii == 6)) {
g.filter_active = true;
return true;
}
if (!key.ctrl && !key.alt && key.ascii == '/') {
g.filter_active = true;
return true;
}
if (!g.show_visualizer && key.scancode == 0x48) { // Up
move_selection(-1);
return true;
}
if (!g.show_visualizer && key.scancode == 0x50) { // Down
move_selection(1);
return true;
}
if (!g.show_visualizer &&
(key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C)) {
if (g.selected_item >= 0) {
start_track(g.selected_item);
g.filter_active = false;
return true;
}
}
if (g.filter_active) {
if (key.ascii == '\b' || key.scancode == 0x0E) {
if (g.filter_text[0]) pop_filter_char();
else g.filter_active = false;
return true;
}
if (!key.ctrl && !key.alt && is_printable_ascii(key.ascii)) {
append_filter_char(key.ascii);
return true;
}
return false;
}
if (key.ascii == ' ') {
if (g.play_state == PlayState::Stopped)
play_selected_or_default();
else
toggle_pause();
return true;
}
if (key.ascii == 's' || key.ascii == 'S') {
stop_playback();
return true;
}
if (key.ascii == 'h' || key.ascii == 'H') {
g.shuffle_enabled = !g.shuffle_enabled;
return true;
}
if (key.ascii == 'r' || key.ascii == 'R') {
cycle_repeat_mode();
return true;
}
if (key.ascii == 'v' || key.ascii == 'V') {
g.show_visualizer = !g.show_visualizer;
return true;
}
if (key.scancode == 0x4D) { // Right arrow
next_track(false);
return true;
}
if (key.scancode == 0x4B) { // Left arrow
prev_track();
return true;
}
return false;
}
// ============================================================================ // ============================================================================
// Entry point // Entry point
// ============================================================================ // ============================================================================
@@ -1131,6 +1438,7 @@ 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.selected_item = -1;
g.show_visualizer = false; g.show_visualizer = false;
g.repeat_mode = RepeatMode::Off; g.repeat_mode = RepeatMode::Off;
g.rng_state = (uint32_t)montauk::get_milliseconds() ^ 0xC0DEFACEu; g.rng_state = (uint32_t)montauk::get_milliseconds() ^ 0xC0DEFACEu;
@@ -1263,38 +1571,9 @@ extern "C" void _start() {
// Keyboard // Keyboard
if (ev.type == 0 && ev.key.pressed) { if (ev.type == 0 && ev.key.pressed) {
if (ev.key.scancode == 0x01) { quit = true; break; } if (handle_key(ev.key, quit))
if (ev.key.ascii == ' ') {
if (g.play_state == PlayState::Stopped && g.file_count > 0)
start_track(g.current_track >= 0 ? g.current_track : 0);
else
toggle_pause();
redraw = true; redraw = true;
} if (quit) break;
if (ev.key.ascii == 's' || ev.key.ascii == 'S') {
stop_playback();
redraw = true;
}
if (ev.key.ascii == 'h' || ev.key.ascii == 'H') {
g.shuffle_enabled = !g.shuffle_enabled;
redraw = true;
}
if (ev.key.ascii == 'r' || ev.key.ascii == 'R') {
cycle_repeat_mode();
redraw = true;
}
if (ev.key.ascii == 'v' || ev.key.ascii == 'V') {
g.show_visualizer = !g.show_visualizer;
redraw = true;
}
if (ev.key.scancode == 0x4D) { // Right arrow
next_track(false);
redraw = true;
}
if (ev.key.scancode == 0x4B) { // Left arrow
prev_track();
redraw = true;
}
} }
// Mouse // Mouse
@@ -1325,8 +1604,7 @@ extern "C" void _start() {
// Hover tracking for file list // Hover tracking for file list
int lb = list_bottom(); int lb = list_bottom();
if (!g.show_visualizer && 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 = filtered_raw_at((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) { if (new_hover != g.hovered_item) {
g.hovered_item = new_hover; g.hovered_item = new_hover;
redraw = true; redraw = true;
@@ -1339,10 +1617,7 @@ extern "C" void _start() {
// Scroll // Scroll
if (!g.show_visualizer && 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(); clamp_list_scroll();
if (max_scroll < 0) max_scroll = 0;
if (g.scroll_y < 0) g.scroll_y = 0;
if (g.scroll_y > max_scroll) g.scroll_y = max_scroll;
redraw = true; redraw = true;
} }
} }
@@ -1352,6 +1627,7 @@ extern "C" void _start() {
g.win_w = ev.resize.w; g.win_w = ev.resize.w;
g.win_h = ev.resize.h; g.win_h = ev.resize.h;
pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g.win_w, g.win_h); pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g.win_w, g.win_h);
ensure_selected_visible();
redraw = true; redraw = true;
} }
} }
+152 -65
View File
@@ -4,6 +4,8 @@ namespace music_visualizer {
using namespace gui; using namespace gui;
static constexpr int SLICE_FRAMES = 256;
static void px_fill(uint32_t* px, int bw, int bh, static void px_fill(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, Color c) { int x, int y, int w, int h, Color c) {
if (w <= 0 || h <= 0) return; if (w <= 0 || h <= 0) return;
@@ -30,62 +32,127 @@ static void px_hline(uint32_t* px, int bw, int bh,
} }
} }
void reset(State& state) { static Color mix_color(Color a, Color b, int t) {
for (int i = 0; i < BAR_COUNT; i++) { t = gui_clamp(t, 0, 255);
state.levels[i] = 0; int inv_t = 255 - t;
state.peaks[i] = 0; return {
state.peak_hold[i] = 0; (uint8_t)((a.r * inv_t + b.r * t + 127) / 255),
(uint8_t)((a.g * inv_t + b.g * t + 127) / 255),
(uint8_t)((a.b * inv_t + b.b * t + 127) / 255),
255
};
}
static int ordered_index(const State& state, int order) {
if (state.history_count < WAVE_HISTORY) return order;
int idx = state.write_pos + order;
if (idx >= WAVE_HISTORY) idx -= WAVE_HISTORY;
return idx;
}
static void clear_accumulator(State& state) {
state.accum_frames = 0;
state.accum_abs_sum = 0;
state.accum_min = 32767;
state.accum_max = -32768;
}
static void push_slice(State& state, int min_sample, int max_sample, int avg_abs) {
if (min_sample > max_sample) return;
int idx = state.write_pos;
state.min_samples[idx] = (int16_t)min_sample;
state.max_samples[idx] = (int16_t)max_sample;
int scaled_energy = (avg_abs * 100) / 32768;
scaled_energy = (scaled_energy * 3) / 2;
if (scaled_energy > 100) scaled_energy = 100;
state.energy[idx] = (uint8_t)scaled_energy;
state.write_pos++;
if (state.write_pos >= WAVE_HISTORY) state.write_pos = 0;
if (state.history_count < WAVE_HISTORY) state.history_count++;
}
static void sample_history(const State& state, int column, int total_columns,
int& min_sample, int& max_sample, int& energy) {
if (state.history_count <= 0) {
min_sample = 0;
max_sample = 0;
energy = 0;
return;
} }
if (state.history_count == 1 || total_columns <= 1) {
int idx = ordered_index(state, state.history_count - 1);
min_sample = state.min_samples[idx];
max_sample = state.max_samples[idx];
energy = state.energy[idx];
return;
}
int denom = total_columns - 1;
int max_order = state.history_count - 1;
int scaled = column * max_order * 256;
int base = scaled / denom;
int frac = base & 0xFF;
int order0 = base >> 8;
int order1 = order0 < max_order ? order0 + 1 : order0;
int idx0 = ordered_index(state, order0);
int idx1 = ordered_index(state, order1);
int min0 = state.min_samples[idx0];
int min1 = state.min_samples[idx1];
int max0 = state.max_samples[idx0];
int max1 = state.max_samples[idx1];
int e0 = state.energy[idx0];
int e1 = state.energy[idx1];
min_sample = (min0 * (256 - frac) + min1 * frac) / 256;
max_sample = (max0 * (256 - frac) + max1 * frac) / 256;
energy = (e0 * (256 - frac) + e1 * frac) / 256;
}
void reset(State& state) {
for (int i = 0; i < WAVE_HISTORY; i++) {
state.min_samples[i] = 0;
state.max_samples[i] = 0;
state.energy[i] = 0;
}
state.write_pos = 0;
state.history_count = 0;
clear_accumulator(state);
} }
void tick(State& state) { void tick(State& state) {
for (int i = 0; i < BAR_COUNT; i++) { (void)state;
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) { void feed_pcm(State& state, const int16_t* pcm, int frames, int channels) {
if (!pcm || frames <= 0 || channels <= 0) return; if (!pcm || frames <= 0 || channels <= 0) return;
int local_max[BAR_COUNT] = {};
for (int i = 0; i < frames; i++) { for (int i = 0; i < frames; i++) {
int bar = (i * BAR_COUNT) / frames; int mono = 0;
if (bar < 0) bar = 0;
if (bar >= BAR_COUNT) bar = BAR_COUNT - 1;
int amp = 0;
for (int ch = 0; ch < channels; ch++) { for (int ch = 0; ch < channels; ch++) {
int sample = pcm[i * channels + ch]; mono += pcm[i * channels + ch];
if (sample < 0) sample = -sample;
amp += sample;
} }
amp /= channels; mono /= channels;
if (amp > local_max[bar]) local_max[bar] = amp;
}
for (int i = 0; i < BAR_COUNT; i++) { if (mono < state.accum_min) state.accum_min = mono;
int scaled = (local_max[i] * 100) / 32768; if (mono > state.accum_max) state.accum_max = mono;
scaled = (scaled * 3) / 2;
if (scaled > 100) scaled = 100;
int blended = scaled; int abs_mono = mono;
if (scaled < state.levels[i]) { if (abs_mono < 0) {
blended = (state.levels[i] * 3 + scaled) / 4; abs_mono = abs_mono == -32768 ? 32768 : -abs_mono;
} }
state.accum_abs_sum += abs_mono;
state.accum_frames++;
if (blended > state.levels[i]) { if (state.accum_frames >= SLICE_FRAMES) {
state.levels[i] = (uint8_t)blended; int avg_abs = state.accum_abs_sum / state.accum_frames;
} push_slice(state, state.accum_min, state.accum_max, avg_abs);
if (state.levels[i] > state.peaks[i]) { clear_accumulator(state);
state.peaks[i] = state.levels[i];
state.peak_hold[i] = 0;
} }
} }
} }
@@ -95,7 +162,13 @@ void render(uint32_t* pixels, int bw, int bh, const Rect& rect,
Color track_bg, Color border) { Color track_bg, Color border) {
if (!pixels || rect.w <= 0 || rect.h <= 0) return; if (!pixels || rect.w <= 0 || rect.h <= 0) return;
px_fill(pixels, bw, bh, rect.x, rect.y, rect.w, rect.h, Color::from_rgb(0xFA, 0xFB, 0xFD)); Color panel_bg = Color::from_rgb(0xFA, 0xFB, 0xFD);
Color guide = mix_color(panel_bg, track_bg, 144);
Color guide_faint = mix_color(panel_bg, track_bg, 72);
Color inner_base = mix_color(Color::from_rgb(0xE9, 0xF2, 0xFF), accent, 64);
Color outer_base = mix_color(track_bg, accent_dark, 72);
px_fill(pixels, bw, bh, rect.x, rect.y, rect.w, rect.h, panel_bg);
px_hline(pixels, bw, bh, rect.x, rect.y, rect.w, border); px_hline(pixels, bw, bh, rect.x, rect.y, rect.w, border);
px_hline(pixels, bw, bh, rect.x, rect.y + rect.h - 1, rect.w, border); px_hline(pixels, bw, bh, rect.x, rect.y + rect.h - 1, rect.w, border);
@@ -105,35 +178,49 @@ void render(uint32_t* pixels, int bw, int bh, const Rect& rect,
int inner_h = rect.h - 16; int inner_h = rect.h - 16;
if (inner_w <= 0 || inner_h <= 0) return; if (inner_w <= 0 || inner_h <= 0) return;
int gap = 2; int center_y = inner_y + inner_h / 2;
int bar_w = (inner_w - gap * (BAR_COUNT - 1)) / BAR_COUNT; int amplitude = inner_h / 2 - 10;
if (bar_w < 2) bar_w = 2; if (amplitude < 12) amplitude = 12;
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); px_hline(pixels, bw, bh, inner_x, center_y, inner_w, guide);
px_hline(pixels, bw, bh, inner_x, center_y - amplitude / 2, inner_w, guide_faint);
px_hline(pixels, bw, bh, inner_x, center_y + amplitude / 2, inner_w, guide_faint);
for (int i = 0; i < BAR_COUNT; i++) { if (state.history_count <= 0) return;
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) { for (int col = 0; col < inner_w; col++) {
int y = base_y - bar_h; int min_sample, max_sample, energy;
int top_h = bar_h / 3; sample_history(state, col, inner_w, min_sample, max_sample, energy);
if (top_h < 2) top_h = 2;
if (top_h > bar_h) top_h = bar_h; int top = center_y - (max_sample * amplitude) / 32768;
px_fill(pixels, bw, bh, x, y + top_h, bar_w, bar_h - top_h, accent); int bottom = center_y - (min_sample * amplitude) / 32768;
px_fill(pixels, bw, bh, x, y, bar_w, top_h, accent_dark); if (top > bottom) {
int swap = top;
top = bottom;
bottom = swap;
} }
int peak_h = (state.peaks[i] * usable_h) / 100; int pad = 1 + energy / 28;
int peak_y = base_y - peak_h; top -= pad;
if (peak_y < inner_y) peak_y = inner_y; bottom += pad;
px_fill(pixels, bw, bh, x, peak_y, bar_w, 2, border);
top = gui_clamp(top, inner_y, inner_y + inner_h - 1);
bottom = gui_clamp(bottom, inner_y, inner_y + inner_h - 1);
if (bottom < top) bottom = top;
int outer_t = 48 + (col * 160) / (inner_w > 1 ? (inner_w - 1) : 1);
int inner_t = 80 + (col * 175) / (inner_w > 1 ? (inner_w - 1) : 1);
Color outer = mix_color(outer_base, accent_dark, outer_t);
Color inner = mix_color(inner_base, accent, inner_t);
int x = inner_x + col;
px_fill(pixels, bw, bh, x, top, 1, bottom - top + 1, outer);
int inner_top = top + 1;
int inner_bottom = bottom - 1;
if (inner_bottom >= inner_top) {
px_fill(pixels, bw, bh, x, inner_top, 1, inner_bottom - inner_top + 1, inner);
}
} }
} }
+10 -4
View File
@@ -5,12 +5,18 @@
namespace music_visualizer { namespace music_visualizer {
static constexpr int BAR_COUNT = 24; static constexpr int WAVE_HISTORY = 192;
struct State { struct State {
uint8_t levels[BAR_COUNT]; int16_t min_samples[WAVE_HISTORY];
uint8_t peaks[BAR_COUNT]; int16_t max_samples[WAVE_HISTORY];
uint8_t peak_hold[BAR_COUNT]; uint8_t energy[WAVE_HISTORY];
int write_pos;
int history_count;
int accum_frames;
int accum_abs_sum;
int accum_min;
int accum_max;
}; };
void reset(State& state); void reset(State& state);