/* * main.cpp * MontaukOS Music Player — MP3 and WAV playback * Copyright (c) 2026 Daniel Hammer */ #include #include #include #include #include #include #include extern "C" { #include #include } #include "minimp3.h" #include "visualizer.hpp" using namespace gui; // ============================================================================ // Constants // ============================================================================ static constexpr int INIT_W = 380; 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 LIST_SB_W = 12; static constexpr int LIST_SB_MIN_TH = 20; static constexpr int INFO_H = 56; static constexpr int PROGRESS_H = 24; static constexpr int TRANSPORT_H = 44; static constexpr int MAX_FILES = 128; static constexpr int MAX_PATH = 256; static constexpr int MAX_FILTER = 64; // Decode buffer: one MP3 frame = up to 1152 samples * 2 channels static constexpr int PCM_BUF_SAMPLES = 1152 * 2; // Audio write chunk size (bytes) — feed in small chunks for responsiveness static constexpr int AUDIO_CHUNK = 4096; static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF); static constexpr Color TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5); static constexpr Color BORDER_COLOR = Color::from_rgb(0xCC, 0xCC, 0xCC); static constexpr Color TEXT_COLOR = Color::from_rgb(0x22, 0x22, 0x22); static constexpr Color DIM_TEXT = Color::from_rgb(0x88, 0x88, 0x88); static constexpr Color TRACK_BG = Color::from_rgb(0xDD, 0xDD, 0xDD); static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF); static constexpr Color BTN_BG = Color::from_rgb(0xE8, 0xE8, 0xE8); static constexpr Color STOP_COLOR = Color::from_rgb(0xCC, 0x33, 0x33); // ============================================================================ // State // ============================================================================ 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; // Font TrueTypeFont* font; // Directory char dir_path[MAX_PATH]; FileEntry files[MAX_FILES]; int file_count; // List UI int scroll_y; int hovered_item; int selected_item; int filtered_indices[MAX_FILES]; int filtered_count; char filter_text[MAX_FILTER]; bool filter_active; // 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; // File data (entire file loaded into memory) uint8_t* file_data; uint64_t file_size; // 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; // WAV state uint64_t wav_data_offset; // offset to PCM data in file uint64_t wav_data_size; // size of PCM data uint64_t wav_pos; // current position in PCM data // Timing uint64_t total_samples; // total decoded samples (per channel) uint64_t played_samples; // samples fed to audio device // PCM buffer for decoded audio int16_t pcm_buf[PCM_BUF_SAMPLES]; int pcm_buf_len; // samples remaining in pcm_buf int pcm_buf_pos; // current read position in pcm_buf // Progress dragging bool dragging_progress; // List scrollbar bool sb_dragging; int sb_drag_offset; bool sb_hovered; int mouse_x; int mouse_y; // Transport icons (dark for normal, white for active/colored backgrounds) SvgIcon ico_rewind; SvgIcon ico_play; SvgIcon ico_pause; SvgIcon ico_stop; SvgIcon ico_forward; 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; static Color g_accent = colors::ACCENT; static Color g_accent_dark = Color::from_rgb(0x2A, 0x62, 0xC8); static Color g_list_hover = Color::from_rgb(0xE8, 0xF0, 0xFD); static Color g_list_playing = Color::from_rgb(0xD0, 0xE2, 0xFB); static Color g_list_selected = Color::from_rgb(0xF0, 0xF5, 0xFC); static int visible_items(); static Color shade(Color c, int delta) { return Color::from_rgb( (uint8_t)gui_clamp((int)c.r + delta, 0, 255), (uint8_t)gui_clamp((int)c.g + delta, 0, 255), (uint8_t)gui_clamp((int)c.b + delta, 0, 255)); } static Color mix(Color a, Color b, int t) { t = gui_clamp(t, 0, 255); int inv = 255 - t; return Color::from_rgb( (uint8_t)((a.r * inv + b.r * t + 127) / 255), (uint8_t)((a.g * inv + b.g * t + 127) / 255), (uint8_t)((a.b * inv + b.b * t + 127) / 255)); } static void music_set_accent(Color accent) { g_accent = accent; g_accent_dark = shade(accent, -28); g_list_hover = mix(accent, WHITE, 220); g_list_playing = mix(accent, WHITE, 196); g_list_selected = mix(accent, WHITE, 236); } static void music_load_accent() { music_set_accent(mtk::load_system_accent()); } // ============================================================================ // Pixel helpers // ============================================================================ static void px_fill(uint32_t* px, int bw, int bh, int x, int y, int w, int h, Color c) { uint32_t v = c.to_pixel(); int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y; int x1 = x + w > bw ? bw : x + w; int y1 = y + h > bh ? bh : y + h; 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 (y < 0 || y >= bh) return; uint32_t v = c.to_pixel(); int x0 = x < 0 ? 0 : x; int x1 = x + w > bw ? bw : x + w; for (int col = x0; col < x1; col++) px[y * bw + col] = v; } static void px_fill_rounded(uint32_t* px, int bw, int bh, int x, int y, int w, int h, int r, Color c) { uint32_t v = c.to_pixel(); for (int row = 0; row < h; row++) { int dy = y + row; if (dy < 0 || dy >= bh) continue; for (int col = 0; col < w; col++) { int dx = x + col; if (dx < 0 || dx >= bw) continue; bool skip = false; int cx, cy; if (col < r && row < r) { cx = r - col - 1; cy = r - row - 1; if (cx*cx + cy*cy >= r*r) skip = true; } else if (col >= w - r && row < r) { cx = col - (w - r); cy = r - row - 1; if (cx*cx + cy*cy >= r*r) skip = true; } else if (col < r && row >= h - r) { cx = r - col - 1; cy = row - (h - r); if (cx*cx + cy*cy >= r*r) skip = true; } else if (col >= w - r && row >= h - r){ cx = col - (w - r); cy = row - (h - r); if (cx*cx + cy*cy >= r*r) skip = true; } if (!skip) px[dy * bw + dx] = v; } } } static void px_circle(uint32_t* px, int bw, int bh, int cx, int cy, int r, Color c) { uint32_t v = c.to_pixel(); for (int dy = -r; dy <= r; dy++) { int py = cy + dy; if (py < 0 || py >= bh) continue; for (int dx = -r; dx <= r; dx++) { int ppx = cx + dx; if (ppx < 0 || ppx >= bw) continue; if (dx * dx + dy * dy <= r * r) px[py * bw + ppx] = v; } } } // Draw a filled triangle (play button shape) static void px_triangle_right(uint32_t* px, int bw, int bh, int x, int y, int w, int h, Color c) { uint32_t v = c.to_pixel(); for (int row = 0; row < h; row++) { int dy = y + row; if (dy < 0 || dy >= bh) continue; // For a right-pointing triangle: width = w * min(row, h-1-row) / (h/2) int half = h / 2; int span; if (half == 0) span = w; else span = w * (row <= half ? row : h - 1 - row) / half; for (int col = 0; col < span; col++) { int dx = x + col; if (dx >= 0 && dx < bw) px[dy * bw + dx] = v; } } } static void px_icon(uint32_t* px, int bw, int bh, int x, int y, const SvgIcon& ic) { if (!ic.pixels) return; for (int row = 0; row < ic.height; row++) { int dy = y + row; if (dy < 0 || dy >= bh) continue; for (int col = 0; col < ic.width; col++) { int dx = x + col; if (dx < 0 || dx >= bw) continue; uint32_t src = ic.pixels[row * ic.width + col]; uint8_t sa = (src >> 24) & 0xFF; if (sa == 0) continue; if (sa == 255) { px[dy * bw + dx] = src; } else { uint32_t dst = px[dy * bw + dx]; uint32_t a = sa, inv_a = 255 - sa; uint32_t rr = (a * ((src >> 16) & 0xFF) + inv_a * ((dst >> 16) & 0xFF) + 128) / 255; uint32_t gg = (a * ((src >> 8) & 0xFF) + inv_a * ((dst >> 8) & 0xFF) + 128) / 255; uint32_t bb = (a * (src & 0xFF) + inv_a * (dst & 0xFF) + 128) / 255; px[dy * bw + dx] = 0xFF000000 | (rr << 16) | (gg << 8) | bb; } } } } static void px_text(uint32_t* px, int bw, int bh, int x, int y, const char* text, Color c, int size = FONT_SIZE) { if (g.font) g.font->draw_to_buffer(px, bw, bh, x, y, text, c, size); } static int text_w(const char* text, int size = FONT_SIZE) { return g.font ? g.font->measure_text(text, size) : 0; } static int font_h(int size = FONT_SIZE) { if (!g.font) return 16; auto* cache = g.font->get_cache(size); return cache->ascent - cache->descent; } 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, int size = FONT_SIZE) { px_fill_rounded(px, bw, bh, x, y, w, h, r, bg); 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, int x, int y, int w, int h, const SvgIcon& ic, Color bg, int r) { px_fill_rounded(px, bw, bh, x, y, w, h, r, bg); int ix = x + (w - ic.width) / 2; int iy = y + (h - ic.height) / 2; px_icon(px, bw, bh, ix, iy, ic); } // ============================================================================ // String helpers // ============================================================================ static bool str_ends_with(const char* s, const char* suffix) { int sl = montauk::slen(s); int xl = montauk::slen(suffix); if (xl > sl) return false; for (int i = 0; i < xl; i++) { char a = s[sl - xl + i]; char b = suffix[i]; // case-insensitive if (a >= 'A' && a <= 'Z') a += 32; if (b >= 'A' && b <= 'Z') b += 32; if (a != b) return false; } 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}; } 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 // ============================================================================ static void scan_directory() { g.file_count = 0; const char* names[256]; int count = montauk::readdir(g.dir_path, names, 256); for (int i = 0; i < count && g.file_count < MAX_FILES; i++) { bool is_mp3 = str_ends_with(names[i], ".mp3"); bool is_wav = str_ends_with(names[i], ".wav"); if (!is_mp3 && !is_wav) continue; FileEntry& f = g.files[g.file_count]; int len = montauk::slen(names[i]); if (len >= (int)sizeof(f.name)) len = sizeof(f.name) - 1; montauk::memcpy(f.name, names[i], len); f.name[len] = 0; f.is_mp3 = is_mp3; g.file_count++; } rebuild_filtered_view(); } // ============================================================================ // WAV parser // ============================================================================ struct WavHeader { uint32_t riff_id; uint32_t file_size; uint32_t wave_id; }; struct WavChunk { uint32_t id; uint32_t size; }; struct WavFmt { uint16_t audio_format; uint16_t channels; uint32_t sample_rate; uint32_t byte_rate; uint16_t block_align; uint16_t bits_per_sample; }; static bool parse_wav(const uint8_t* data, uint64_t size) { if (size < 44) return false; auto* hdr = (const WavHeader*)data; if (hdr->riff_id != 0x46464952) return false; // "RIFF" if (hdr->wave_id != 0x45564157) return false; // "WAVE" uint64_t pos = 12; const WavFmt* fmt = nullptr; while (pos + 8 <= size) { auto* chunk = (const WavChunk*)(data + pos); uint64_t chunk_data = pos + 8; if (chunk->id == 0x20746D66) { // "fmt " if (chunk->size < sizeof(WavFmt)) return false; fmt = (const WavFmt*)(data + chunk_data); } 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; g.wav_data_size = chunk->size; g.wav_pos = 0; int bytes_per_sample = (fmt->bits_per_sample / 8) * fmt->channels; if (bytes_per_sample > 0) g.total_samples = chunk->size / bytes_per_sample; return true; } pos = chunk_data + ((chunk->size + 1) & ~1u); // align to 2 bytes } return false; } // ============================================================================ // MP3 helpers // ============================================================================ 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; } 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; 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; } 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 // ============================================================================ static void stop_playback() { if (g.audio_handle >= 0) { montauk::audio_close(g.audio_handle); g.audio_handle = -1; } if (g.file_data) { 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; g.pcm_buf_pos = 0; g.mp3_offset = 0; g.wav_pos = 0; } static bool start_track(int index) { stop_playback(); if (index < 0 || index >= g.file_count) return false; // Build full path char path[MAX_PATH * 2]; snprintf(path, sizeof(path), "%s/%s", g.dir_path, g.files[index].name); // Open and read entire file int fh = montauk::open(path); if (fh < 0) return false; g.file_size = montauk::getsize(fh); if (g.file_size == 0 || g.file_size > 64 * 1024 * 1024) { // 64 MiB limit montauk::close(fh); return false; } g.file_data = (uint8_t*)montauk::malloc(g.file_size); if (!g.file_data) { montauk::close(fh); return false; } // Read in chunks (syscall may have size limits) uint64_t off = 0; while (off < g.file_size) { uint64_t chunk = g.file_size - off; if (chunk > 32768) chunk = 32768; int rd = montauk::read(fh, g.file_data + off, off, chunk); if (rd <= 0) break; off += rd; } montauk::close(fh); if (off < g.file_size) { montauk::mfree(g.file_data); g.file_data = nullptr; return false; } // Parse format if (g.files[index].is_mp3) { mp3dec_init(&g.mp3dec); g.mp3_offset = 0; // Skip ID3v2 if (g.file_size >= 10 && g.file_data[0] == 'I' && g.file_data[1] == 'D' && g.file_data[2] == '3') { g.mp3_offset = 10 + (((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))); } if (!build_mp3_index(g.mp3_offset)) { montauk::mfree(g.file_data); g.file_data = nullptr; return false; } } else { if (!parse_wav(g.file_data, g.file_size)) { montauk::mfree(g.file_data); g.file_data = nullptr; return false; } } // 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; } g.current_track = index; g.play_state = PlayState::Playing; 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); 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; } static void toggle_pause() { if (g.play_state == PlayState::Playing) { montauk::audio_pause(g.audio_handle); g.play_state = PlayState::Paused; } else if (g.play_state == PlayState::Paused) { montauk::audio_resume(g.audio_handle); g.play_state = PlayState::Playing; } } 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) { 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() { if (g.file_count == 0) return; // If we're more than 3 seconds in, restart current track if (g.sample_rate > 0 && g.played_samples > (uint64_t)g.sample_rate * 3) { start_track(g.current_track); return; } int prev = g.current_track - 1; if (prev < 0) prev = g.file_count - 1; start_track(prev); } // ============================================================================ // Audio feeding — call from the main loop // ============================================================================ static void feed_audio() { if (g.play_state != PlayState::Playing) return; if (g.audio_handle < 0) return; // Try to feed a chunk of audio data for (int iter = 0; iter < 4; iter++) { // If we have leftover PCM in the buffer, write it if (g.pcm_buf_len > 0) { int bytes_avail = g.pcm_buf_len * 2; // 16-bit samples int to_write = bytes_avail > AUDIO_CHUNK ? AUDIO_CHUNK : bytes_avail; int written = montauk::audio_write(g.audio_handle, (const uint8_t*)(g.pcm_buf + g.pcm_buf_pos), to_write); if (written <= 0) return; // device buffer full, try later int samples_written = written / 2; g.pcm_buf_pos += samples_written; g.pcm_buf_len -= samples_written; g.played_samples += samples_written / g.channels; continue; } // Decode more audio g.pcm_buf_pos = 0; g.pcm_buf_len = 0; if (g.current_track >= 0 && g.files[g.current_track].is_mp3) { // MP3 decode if (g.mp3_offset >= g.file_size) { // End of file — advance to next track next_track(true); return; } 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), g.pcm_buf, &info); if (info.frame_bytes > 0) { g.mp3_offset += info.frame_bytes; } else { // Can't decode — end 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, info.hz); } else { g.pcm_buf_pos = 0; } } } else { // WAV decode — just copy PCM data if (g.wav_pos >= g.wav_data_size) { next_track(true); return; } uint64_t remaining = g.wav_data_size - g.wav_pos; uint64_t to_copy = remaining; if (to_copy > sizeof(g.pcm_buf)) to_copy = sizeof(g.pcm_buf); 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, g.sample_rate); } } } } // ============================================================================ // Time formatting // ============================================================================ static void format_time(char* buf, int buf_size, uint64_t samples, int rate) { if (rate <= 0) { snprintf(buf, buf_size, "--:--"); return; } uint64_t secs = samples / rate; int m = (int)(secs / 60); int s = (int)(secs % 60); snprintf(buf, buf_size, "%d:%02d", m, s); } // ============================================================================ // 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 bool list_scrollbar_visible() { return g.filtered_count > visible_items() && visible_items() > 0; } static void list_scrollbar_rects(int W, Rect* track, Rect* thumb) { int list_h = list_bottom() - LIST_TOP; int visible = visible_items(); int tx = W - LIST_SB_W; *track = {tx, LIST_TOP, LIST_SB_W, list_h}; int max_scroll = g.filtered_count - visible; if (max_scroll < 1) max_scroll = 1; int th = (visible * list_h) / g.filtered_count; if (th < LIST_SB_MIN_TH) th = LIST_SB_MIN_TH; if (th > list_h) th = list_h; int range = list_h - th; int ty = LIST_TOP + (g.scroll_y * range) / max_scroll; *thumb = {tx + 1, ty, LIST_SB_W - 2, th}; } 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(); int fh_sm = font_h(FONT_SIZE_SM); // Background px_fill(pixels, W, H, 0, 0, W, H, BG_COLOR); // 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 ? g_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 ? g_accent : BTN_BG, 6); Color repeat_bg = (g.repeat_mode == RepeatMode::Off) ? BTN_BG : g_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 { 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 ? g_list_hover : BTN_BG; Color pill_fg = g.filter_active ? g_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; int dw = text_w(display_dir, FONT_SIZE_SM); 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 - font_h(FONT_SIZE_SM)) / 2, display_dir, TEXT_COLOR, FONT_SIZE_SM); } 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); if (!g.show_visualizer) { bool sb_vis = list_scrollbar_visible(); int row_w = sb_vis ? (W - LIST_SB_W) : W; int tag_right = sb_vis ? (W - LIST_SB_W) : W; for (int i = 0; i < visible_count && (i + g.scroll_y) < g.filtered_count; i++) { int idx = filtered_raw_at(i + g.scroll_y); if (idx < 0) break; int iy = LIST_TOP + i * LIST_ITEM_H; Color bg = BG_COLOR; if (idx == g.current_track && g.play_state != PlayState::Stopped) bg = g_list_playing; else if (idx == g.selected_item) bg = g_list_selected; else if (idx == g.hovered_item) bg = g_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, row_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, g_accent); if (idx == g.current_track && g.play_state == PlayState::Playing) { px_triangle_right(pixels, W, H, 8, iy + 8, 8, 12, g_accent); } else if (idx == g.current_track && g.play_state == PlayState::Paused) { px_fill(pixels, W, H, 8, iy + 8, 3, 12, g_accent); px_fill(pixels, W, H, 13, iy + 8, 3, 12, g_accent); } Color name_color = (idx == g.current_track && g.play_state != PlayState::Stopped) ? g_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, tag_right - tw - 12, iy + (LIST_ITEM_H - fh_sm) / 2, tag, DIM_TEXT, FONT_SIZE_SM); } if (sb_vis) { Rect track, thumb; list_scrollbar_rects(W, &track, &thumb); px_fill(pixels, W, H, track.x, track.y, track.w, track.h, Color::from_rgb(0xF0, 0xF0, 0xF0)); Color tc = (g.sb_hovered || g.sb_dragging) ? Color::from_rgb(0xA0, 0xA0, 0xA0) : Color::from_rgb(0xC0, 0xC0, 0xC0); px_fill(pixels, W, H, thumb.x, thumb.y, thumb.w, thumb.h, tc); } if (g.filtered_count == 0) { const char* msg = g.file_count == 0 ? "No audio files found" : "No matches for filter"; 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); 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 { 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, g_accent, g_accent_dark, TRACK_BG, BORDER_COLOR); } // ---- Now Playing info ---- 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); if (g.current_track >= 0 && g.play_state != PlayState::Stopped) { // Track name px_text(pixels, W, H, 12, info_y + 8, g.files[g.current_track].name, TEXT_COLOR, FONT_SIZE); // Time display char time_cur[16], time_total[16], time_str[40]; format_time(time_cur, sizeof(time_cur), g.played_samples, g.sample_rate); format_time(time_total, sizeof(time_total), g.total_samples, g.sample_rate); snprintf(time_str, sizeof(time_str), "%s / %s", time_cur, time_total); px_text(pixels, W, H, 12, info_y + 8 + fh + 4, time_str, DIM_TEXT, FONT_SIZE_SM); // State indicator on right const char* state_str = (g.play_state == PlayState::Playing) ? "Playing" : "Paused"; int sw = text_w(state_str, FONT_SIZE_SM); px_text(pixels, W, H, W - sw - 12, info_y + 8 + fh + 4, state_str, g_accent, FONT_SIZE_SM); } else { const char* msg = "No track selected"; px_text(pixels, W, H, 12, info_y + (INFO_H - fh) / 2, msg, DIM_TEXT); } // ---- Progress bar ---- int prog_y = info_y + INFO_H; px_fill(pixels, W, H, 0, prog_y, W, PROGRESS_H, Color::from_rgb(0xF0, 0xF0, 0xF0)); int bar_x = 16, bar_w = W - 32, bar_h = 6; int bar_y = prog_y + (PROGRESS_H - bar_h) / 2; px_fill_rounded(pixels, W, H, bar_x, bar_y, bar_w, bar_h, 3, TRACK_BG); if (g.total_samples > 0 && g.play_state != PlayState::Stopped) { int fill = (int)((g.played_samples * bar_w) / g.total_samples); if (fill > bar_w) fill = bar_w; if (fill > 0) px_fill_rounded(pixels, W, H, bar_x, bar_y, fill, bar_h, 3, g_accent); // Knob px_circle(pixels, W, H, bar_x + fill, bar_y + bar_h / 2, 5, g_accent); px_circle(pixels, W, H, bar_x + fill, bar_y + bar_h / 2, 2, WHITE); } // ---- Transport controls ---- int trans_y = prog_y + PROGRESS_H; px_fill(pixels, W, H, 0, trans_y, W, TRANSPORT_H, BG_COLOR); int btn_w = 48, btn_h = 32, btn_r = 6, gap = 8; int total_w = btn_w * 4 + gap * 3; // prev, play, stop, next int bx = (W - total_w) / 2; int by = trans_y + (TRANSPORT_H - btn_h) / 2; // [<<] Prev px_icon_button(pixels, W, H, bx, by, btn_w, btn_h, g.ico_rewind, BTN_BG, btn_r); bx += btn_w + gap; // [Play/Pause] { bool active = (g.play_state == PlayState::Playing); Color bg = active ? g_accent : BTN_BG; const SvgIcon& ic = active ? g.ico_pause_w : g.ico_play; px_icon_button(pixels, W, H, bx, by, btn_w, btn_h, ic, bg, btn_r); } bx += btn_w + gap; // [Stop] { bool active = (g.play_state != PlayState::Stopped); Color bg = active ? STOP_COLOR : BTN_BG; const SvgIcon& ic = active ? g.ico_stop_w : g.ico_stop; px_icon_button(pixels, W, H, bx, by, btn_w, btn_h, ic, bg, btn_r); } bx += btn_w + gap; // [>>] Next px_icon_button(pixels, W, H, bx, by, btn_w, btn_h, g.ico_forward, BTN_BG, btn_r); } // ============================================================================ // Hit testing // ============================================================================ 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 (!g.show_visualizer && my >= LIST_TOP && my < lb && mx >= 0 && mx < W) { int idx = filtered_raw_at((my - LIST_TOP) / LIST_ITEM_H + g.scroll_y); if (idx >= 0) { start_track(idx); return true; } } // Progress bar click if (my >= prog_y && my < prog_y + PROGRESS_H) { if (g.play_state != PlayState::Stopped && g.total_samples > 0) { int bar_x = 16, bar_w = W - 32; int rel = mx - bar_x; if (rel < 0) rel = 0; if (rel > bar_w) rel = bar_w; uint64_t target_samples = (uint64_t)rel * g.total_samples / bar_w; seek_to_samples(target_samples); g.dragging_progress = true; return true; } } // Transport buttons if (my >= trans_y && my < trans_y + TRANSPORT_H) { int btn_w = 48, btn_h = 32, gap = 8; int total_w = btn_w * 4 + gap * 3; int bx = (W - total_w) / 2; int by = trans_y + (TRANSPORT_H - btn_h) / 2; if (my >= by && my < by + btn_h) { // Prev if (mx >= bx && mx < bx + btn_w) { prev_track(); return true; } bx += btn_w + gap; // Play/Pause if (mx >= bx && mx < bx + btn_w) { if (g.play_state == PlayState::Stopped) { play_selected_or_default(); } else { toggle_pause(); } return true; } bx += btn_w + gap; // Stop if (mx >= bx && mx < bx + btn_w) { stop_playback(); return true; } bx += btn_w + gap; // Next if (mx >= bx && mx < bx + btn_w) { next_track(false); return true; } } } return false; } static bool handle_key(const montauk::abi::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 // ============================================================================ extern "C" void _start() { // Init state memset(&g, 0, sizeof(g)); g.win_w = INIT_W; g.win_h = INIT_H; g.audio_handle = -1; g.current_track = -1; g.play_state = PlayState::Stopped; g.hovered_item = -1; g.selected_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 directory auto set_user_home = [&]() { char username[32] = {}; if (montauk::getuser(username, sizeof(username)) > 0 && username[0]) { snprintf(g.dir_path, sizeof(g.dir_path), "0:/users/%s/Music", username); } else { snprintf(g.dir_path, sizeof(g.dir_path), "0:/users"); } }; char arg_file[128] = {}; { char args[256]; int arglen = montauk::getargs(args, sizeof(args)); if (arglen > 0 && args[0]) { if (str_ends_with(args, ".mp3") || str_ends_with(args, ".wav")) { // File path: extract directory and filename int last_slash = -1; for (int i = 0; args[i]; i++) { if (args[i] == '/') last_slash = i; } if (last_slash > 0) { memcpy(g.dir_path, args, last_slash); g.dir_path[last_slash] = '\0'; const char* fname = args + last_slash + 1; int flen = montauk::slen(fname); if (flen > 0 && flen < (int)sizeof(arg_file)) { memcpy(arg_file, fname, flen); arg_file[flen] = '\0'; } } else { set_user_home(); } } else { int len = montauk::slen(args); if (len >= (int)sizeof(g.dir_path)) len = sizeof(g.dir_path) - 1; memcpy(g.dir_path, args, len); g.dir_path[len] = '\0'; } } else { set_user_home(); } } music_load_accent(); // Load font { TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont)); if (f) { montauk::memset(f, 0, sizeof(TrueTypeFont)); if (!f->init("0:/fonts/Roboto-Medium.ttf")) { montauk::mfree(f); f = nullptr; } } g.font = f; } // Load transport icons (dark + white variants for colored backgrounds) { Color ic_color = TEXT_COLOR; g.ico_rewind = svg_load("0:/icons/media-rewind.svg", 16, 16, ic_color); g.ico_play = svg_load("0:/icons/media-play.svg", 16, 16, ic_color); g.ico_pause = svg_load("0:/icons/media-pause.svg", 16, 16, ic_color); g.ico_stop = svg_load("0:/icons/media-stop.svg", 16, 16, ic_color); g.ico_forward = svg_load("0:/icons/media-forward.svg", 16, 16, ic_color); 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 scan_directory(); // Auto-size window width so the longest track name fits without truncation. // Layout per row: 24px left pad + name + 8px gap + tag ("MP3"/"WAV") + 12px // right pad, plus ~10px for the scrollbar gutter. { int max_name_w = 0; for (int i = 0; i < g.file_count; i++) { int w = text_w(g.files[i].name, FONT_SIZE_SM); if (w > max_name_w) max_name_w = w; } int tag_w = text_w("MP3", FONT_SIZE_SM); int wav_w = text_w("WAV", FONT_SIZE_SM); if (wav_w > tag_w) tag_w = wav_w; int needed_w = 24 + max_name_w + 8 + tag_w + 12 + 10; if (needed_w > g.win_w) g.win_w = needed_w; if (g.win_w > 900) g.win_w = 900; } // If a specific file was requested, find it and auto-play int auto_play_idx = -1; if (arg_file[0]) { for (int i = 0; i < g.file_count; i++) { if (str_ends_with(g.files[i].name, arg_file) && montauk::slen(g.files[i].name) == montauk::slen(arg_file)) { auto_play_idx = i; break; } } } // Create window montauk::abi::WinCreateResult wres; if (montauk::win_create("Music", g.win_w, g.win_h, &wres) < 0 || wres.id < 0) montauk::exit(1); int win_id = wres.id; uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa; // Auto-play the requested file if (auto_play_idx >= 0) { start_track(auto_play_idx); } render(pixels); montauk::win_present(win_id); uint64_t last_render = montauk::get_milliseconds(); while (true) { montauk::abi::WinEvent ev; bool redraw = false; bool quit = false; int r; // Drain all pending events before rendering while ((r = montauk::win_poll(win_id, &ev)) > 0) { if (ev.type == 3) { quit = true; break; } // Keyboard if (ev.type == 0 && ev.key.pressed) { if (handle_key(ev.key, quit)) redraw = true; if (quit) break; } // Mouse if (ev.type == 1) { g.mouse_x = ev.mouse.x; g.mouse_y = ev.mouse.y; bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); bool released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1); bool held = (ev.mouse.buttons & 1) != 0; // List scrollbar drag/hover bool sb_consume = false; if (!g.show_visualizer && list_scrollbar_visible()) { Rect sb_track, sb_thumb; list_scrollbar_rects(g.win_w, &sb_track, &sb_thumb); bool in_track = sb_track.contains(ev.mouse.x, ev.mouse.y); if (g.sb_dragging && held) { int list_h = sb_track.h; int range = list_h - sb_thumb.h; int max_scroll = g.filtered_count - visible_items(); if (range > 0 && max_scroll > 0) { int new_ty = ev.mouse.y - g.sb_drag_offset; int rel = new_ty - LIST_TOP; if (rel < 0) rel = 0; if (rel > range) rel = range; int new_scroll = (rel * max_scroll + range / 2) / range; if (new_scroll != g.scroll_y) { g.scroll_y = new_scroll; clamp_list_scroll(); redraw = true; } } sb_consume = true; } else if (g.sb_dragging && released) { g.sb_dragging = false; redraw = true; sb_consume = true; } else if (clicked && in_track) { if (sb_thumb.contains(ev.mouse.x, ev.mouse.y)) { g.sb_dragging = true; g.sb_drag_offset = ev.mouse.y - sb_thumb.y; } else { int visible = visible_items(); int delta = visible > 1 ? visible - 1 : 1; if (ev.mouse.y < sb_thumb.y) g.scroll_y -= delta; else g.scroll_y += delta; clamp_list_scroll(); } redraw = true; sb_consume = true; } if (in_track != g.sb_hovered) { g.sb_hovered = in_track; redraw = true; } } else if (g.sb_hovered) { g.sb_hovered = false; redraw = true; } if (clicked && !sb_consume) { if (handle_click(ev.mouse.x, ev.mouse.y)) redraw = true; } if (released) { g.dragging_progress = false; } // Progress bar drag if (g.dragging_progress && (ev.mouse.buttons & 1)) { int bar_x = 16, bar_w = g.win_w - 32; int rel = ev.mouse.x - bar_x; if (rel < 0) rel = 0; if (rel > bar_w) rel = bar_w; uint64_t target_samples = (uint64_t)rel * g.total_samples / bar_w; seek_to_samples(target_samples); redraw = true; } // Hover tracking for file list int lb = list_bottom(); int hover_right = list_scrollbar_visible() ? (g.win_w - LIST_SB_W) : g.win_w; if (!g.show_visualizer && ev.mouse.y >= LIST_TOP && ev.mouse.y < lb && ev.mouse.x >= 0 && ev.mouse.x < hover_right) { int new_hover = filtered_raw_at((ev.mouse.y - LIST_TOP) / LIST_ITEM_H + g.scroll_y); if (new_hover != g.hovered_item) { g.hovered_item = new_hover; redraw = true; } } else if (g.hovered_item != -1) { g.hovered_item = -1; redraw = true; } // Scroll if (!g.show_visualizer && ev.mouse.scroll != 0) { g.scroll_y -= ev.mouse.scroll * 2; clamp_list_scroll(); redraw = true; } } // Resize if (ev.type == 2) { g.win_w = ev.resize.w; g.win_h = ev.resize.h; pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g.win_w, g.win_h); ensure_selected_visible(); redraw = true; } } if (quit || r < 0) break; // Feed audio data to the device feed_audio(); // Periodic redraw while playing (update time display) uint64_t now = montauk::get_milliseconds(); 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; } // Sleep less when playing to keep audio fed if (g.play_state == PlayState::Playing) montauk::sleep_ms(4); else montauk::sleep_ms(16); } stop_playback(); montauk::win_destroy(win_id); montauk::exit(0); }