/* * main.cpp * MontaukOS Audio Control * * Built with the Montauk Toolkit (mtk): * 1. Master output card (large %, slider, mute button) * 2. Per-application mixer rows (name, slider, mute) * * Copyright (c) 2026 Daniel Hammer */ #include #include #include #include #include extern "C" { #include } using namespace gui; // ============================================================================ // Layout constants (in MTK theme units) // ============================================================================ static constexpr int WIN_W = 380; static constexpr int WIN_H = 460; static constexpr int PAD = 22; static constexpr int KNOB_R = 8; static constexpr int SLIDER_TRACK_H = 6; static constexpr int MUTE_BTN_W = 32; static constexpr int MASTER_BUTTON_W = 96; // Vertical rhythm for the master section. Tuned to give the controls room to // breathe without floating in a card frame. static constexpr int MASTER_TOP = 32; static constexpr int MASTER_SUB_GAP = 16; // % → "Master output" static constexpr int MASTER_SLIDER_GAP = 32; // subtitle → slider static constexpr int MASTER_BUTTON_GAP = 36; // slider → mute button static constexpr int MASTER_TAIL_GAP = 36; // mute button → separator static constexpr int APPS_HEADER_GAP = 20; // separator → "APPLICATIONS" static constexpr int APPS_FIRST_ROW_GAP = 16; // header → first row // ============================================================================ // State // ============================================================================ static WsWindow g_win; static int g_master_vol = 80; static bool g_master_muted = false; static montauk::abi::AudioStreamInfo g_streams[8]; static int g_stream_count = 0; // Drag target: -2 = none, -1 = master, 0..N-1 = stream slot in g_streams. static int g_drag_target = -2; // Mixer serial we last synced from. Bumped by the kernel on every state // change; the loop refreshes only when it differs from g_mixer_serial. static uint64_t g_mixer_serial = 0; static int g_mouse_x = -1; static int g_mouse_y = -1; static Color g_accent = colors::ACCENT; // ============================================================================ // Theme // ============================================================================ static mtk::Theme app_theme() { return mtk::make_theme(g_accent); } static void load_accent() { g_accent = mtk::load_system_accent(); } // ============================================================================ // Geometry helpers // ============================================================================ static int big_font_line_height() { if (fonts::system_font && fonts::system_font->valid) return fonts::system_font->get_line_height(fonts::LARGE_SIZE); return system_font_height() * 2; } static int master_big_y() { return MASTER_TOP; } static int master_sub_y() { return master_big_y() + big_font_line_height() + MASTER_SUB_GAP; } static int master_slider_y() { return master_sub_y() + system_font_height() + MASTER_SLIDER_GAP; } static Rect master_slider_rect() { return {PAD, master_slider_y(), g_win.width - PAD * 2, SLIDER_TRACK_H}; } static int master_button_y(const mtk::Theme& theme) { return master_slider_y() + KNOB_R + MASTER_BUTTON_GAP - theme.control_h / 2; } static Rect master_button_rect(const mtk::Theme& theme) { int bx = (g_win.width - MASTER_BUTTON_W) / 2; return {bx, master_button_y(theme), MASTER_BUTTON_W, theme.control_h}; } static int separator_y(const mtk::Theme& theme) { return master_button_y(theme) + theme.control_h + MASTER_TAIL_GAP; } static int apps_header_y(const mtk::Theme& theme) { return separator_y(theme) + APPS_HEADER_GAP; } static int stream_list_y(const mtk::Theme& theme) { return apps_header_y(theme) + system_font_height() + APPS_FIRST_ROW_GAP; } static int stream_row_h(const mtk::Theme& theme) { // Label row + slider row + breathing space below. return system_font_height() + theme.gap_md + theme.control_h + theme.gap_lg; } static Rect stream_row_rect(int idx, const mtk::Theme& theme) { int y = stream_list_y(theme) + idx * stream_row_h(theme); return {PAD, y, g_win.width - PAD * 2, stream_row_h(theme)}; } static Rect stream_slider_rect(int idx, const mtk::Theme& theme) { Rect row = stream_row_rect(idx, theme); int y = row.y + system_font_height() + theme.gap_md + (theme.control_h - SLIDER_TRACK_H) / 2; int x = row.x; int w = row.w - MUTE_BTN_W - theme.gap_md; return {x, y, w, SLIDER_TRACK_H}; } static Rect stream_mute_rect(int idx, const mtk::Theme& theme) { Rect row = stream_row_rect(idx, theme); int y = row.y + system_font_height() + theme.gap_md; int x = row.x + row.w - MUTE_BTN_W; return {x, y, MUTE_BTN_W, theme.control_h}; } static int vol_from_x(const Rect& slider, int mx) { if (slider.w <= 0) return 0; int v = ((mx - slider.x) * 100) / slider.w; if (v < 0) v = 0; if (v > 100) v = 100; return v; } static bool slider_hit(const Rect& slider, int mx, int my) { return my >= slider.y - KNOB_R && my <= slider.y + slider.h + KNOB_R && mx >= slider.x - KNOB_R && mx <= slider.x + slider.w + KNOB_R; } static bool mouse_in_rect(const Rect& r) { return r.contains(g_mouse_x, g_mouse_y); } // ============================================================================ // Backend helpers // ============================================================================ static void refresh_master() { int v = montauk::audio_get_master_volume(); if (v >= 0) g_master_vol = v; g_master_muted = montauk::audio_get_master_mute() == 1; } static void apply_master_volume(int v) { if (v < 0) v = 0; if (v > 100) v = 100; if (v == g_master_vol) return; // No change → skip syscall + HW codec hit. g_master_vol = v; montauk::audio_set_master_volume(v); } static void toggle_master_mute() { g_master_muted = !g_master_muted; montauk::audio_set_master_mute(g_master_muted); } static void refresh_streams() { int n = montauk::audio_list(g_streams, 8); g_stream_count = (n < 0) ? 0 : n; } static void apply_stream_volume(int idx, int v) { if (idx < 0 || idx >= g_stream_count) return; if (v < 0) v = 0; if (v > 100) v = 100; if ((uint8_t)v == g_streams[idx].volume) return; g_streams[idx].volume = (uint8_t)v; montauk::audio_set_volume(g_streams[idx].handle, v); } static void toggle_stream_mute(int idx) { if (idx < 0 || idx >= g_stream_count) return; bool m = !g_streams[idx].muted; g_streams[idx].muted = (uint8_t)(m ? 1 : 0); montauk::audio_set_mute(g_streams[idx].handle, m); } // ============================================================================ // Drawing // ============================================================================ static void draw_slider(Canvas& c, const Rect& slider, int volume, bool muted, const mtk::Theme& theme) { Color track = mtk::mix(theme.surface, theme.border, 80); Color fill = muted ? theme.danger : theme.accent; c.fill_rounded_rect(slider.x, slider.y, slider.w, slider.h, slider.h / 2, track); int show = muted ? 0 : volume; int fw = (show * slider.w) / 100; if (fw > 0) c.fill_rounded_rect(slider.x, slider.y, fw, slider.h, slider.h / 2, fill); int kx = slider.x + fw; int ky = slider.y + slider.h / 2; fill_circle(c, kx, ky, KNOB_R, fill); fill_circle(c, kx, ky, KNOB_R - 3, colors::WHITE); } static void draw_master(Canvas& c, const mtk::Theme& theme) { // Big percentage (no enclosing card). char buf[8]; snprintf(buf, sizeof(buf), "%d%%", g_master_muted ? 0 : g_master_vol); Color vc = g_master_muted ? theme.danger : theme.accent; if (fonts::system_font && fonts::system_font->valid) { int large_w = fonts::system_font->measure_text(buf, fonts::LARGE_SIZE); c.text_2x((g_win.width - large_w) / 2, master_big_y(), buf, vc); } else { int bw = text_width(buf); c.text((g_win.width - bw) / 2, master_big_y(), buf, vc); } // Subtitle. const char* sub = g_master_muted ? "Master output (muted)" : "Master output"; int sw = text_width(sub); c.text((g_win.width - sw) / 2, master_sub_y(), sub, g_master_muted ? theme.danger : theme.text_subtle); // Slider. draw_slider(c, master_slider_rect(), g_master_vol, g_master_muted, theme); // Mute button. PRIMARY when muted so the recovery action stands out. Rect btn = master_button_rect(theme); mtk::WidgetState st = mtk::widget_state(false, mouse_in_rect(btn), true); mtk::draw_button(c, btn, g_master_muted ? "Unmute" : "Mute", g_master_muted ? mtk::BUTTON_PRIMARY : mtk::BUTTON_SECONDARY, st, theme); } static void draw_applications(Canvas& c, const mtk::Theme& theme) { int hy = apps_header_y(theme); c.text(PAD, hy, "APPLICATIONS", theme.text_muted); if (g_stream_count == 0) { int empty_y = stream_list_y(theme) + theme.gap_md; const char* msg = "No applications are playing audio."; int mw = text_width(msg); c.text((g_win.width - mw) / 2, empty_y, msg, theme.text_subtle); return; } int visible = g_stream_count; int rows_room = (g_win.height - stream_list_y(theme) - PAD) / stream_row_h(theme); if (rows_room < 1) rows_room = 1; if (visible > rows_room) visible = rows_room; for (int i = 0; i < visible; i++) { Rect row = stream_row_rect(i, theme); char label[96]; const char* name = g_streams[i].name[0] ? g_streams[i].name : "?"; snprintf(label, sizeof(label), "%s (pid %d)", name, g_streams[i].ownerPid); c.text(row.x, row.y, label, theme.text); char pct[8]; snprintf(pct, sizeof(pct), "%d%%", g_streams[i].muted ? 0 : g_streams[i].volume); int pw = text_width(pct); c.text(row.x + row.w - pw, row.y, pct, theme.text_muted); Rect slider = stream_slider_rect(i, theme); draw_slider(c, slider, g_streams[i].volume, g_streams[i].muted != 0, theme); Rect mute = stream_mute_rect(i, theme); bool m = g_streams[i].muted != 0; mtk::WidgetState st = mtk::widget_state(m, mouse_in_rect(mute), true); mtk::draw_button(c, mute, m ? "M" : "m", m ? mtk::BUTTON_DANGER : mtk::BUTTON_SECONDARY, st, theme); } } static void render() { mtk::StandaloneHost host(&g_win); Canvas c = host.canvas(); mtk::Theme theme = app_theme(); c.fill(theme.window_bg); draw_master(c, theme); mtk::draw_separator(c, PAD, separator_y(theme), g_win.width - PAD * 2, theme); draw_applications(c, theme); host.present(); } // ============================================================================ // Input // ============================================================================ static bool handle_mouse(int mx, int my, uint8_t buttons, uint8_t prev) { mtk::Theme theme = app_theme(); bool clicked = (buttons & 1) && !(prev & 1); bool released = !(buttons & 1) && (prev & 1); bool down = (buttons & 1) != 0; if (clicked) { // Master slider. Rect slider = master_slider_rect(); if (slider_hit(slider, mx, my)) { if (g_master_muted) toggle_master_mute(); apply_master_volume(vol_from_x(slider, mx)); g_drag_target = -1; return true; } // Master mute button. Rect btn = master_button_rect(theme); if (btn.contains(mx, my)) { toggle_master_mute(); return true; } // Per-stream rows. for (int i = 0; i < g_stream_count; i++) { Rect ssl = stream_slider_rect(i, theme); if (slider_hit(ssl, mx, my)) { if (g_streams[i].muted) toggle_stream_mute(i); apply_stream_volume(i, vol_from_x(ssl, mx)); g_drag_target = i; return true; } Rect mute = stream_mute_rect(i, theme); if (mute.contains(mx, my)) { toggle_stream_mute(i); return true; } } } if (g_drag_target != -2 && down) { if (g_drag_target == -1) { Rect slider = master_slider_rect(); if (g_master_muted) toggle_master_mute(); apply_master_volume(vol_from_x(slider, mx)); return true; } else if (g_drag_target >= 0 && g_drag_target < g_stream_count) { Rect ssl = stream_slider_rect(g_drag_target, theme); if (g_streams[g_drag_target].muted) toggle_stream_mute(g_drag_target); apply_stream_volume(g_drag_target, vol_from_x(ssl, mx)); return true; } } if (released) g_drag_target = -2; return false; } static bool handle_key(const montauk::abi::KeyEvent& key) { if (!key.pressed) return false; if (key.scancode == 0x01) { g_win.closed = true; return false; } if (key.scancode == 0x4D || key.ascii == '+' || key.ascii == '=') { if (g_master_muted) toggle_master_mute(); apply_master_volume(g_master_vol + 5); return true; } if (key.scancode == 0x4B || key.ascii == '-') { if (g_master_muted) toggle_master_mute(); apply_master_volume(g_master_vol - 5); return true; } if (key.ascii == 'm' || key.ascii == 'M') { toggle_master_mute(); return true; } return false; } // ============================================================================ // Entry point // ============================================================================ extern "C" void _start() { if (!fonts::init()) { montauk::exit(1); } load_accent(); refresh_master(); refresh_streams(); g_mixer_serial = montauk::audio_wait(0, 0); if (!g_win.create("Audio", WIN_W, WIN_H)) montauk::exit(1); render(); while (g_win.id >= 0 && !g_win.closed) { montauk::abi::WinEvent ev; int r = g_win.poll(&ev); bool redraw = false; // Event-driven sync: the kernel bumps a mixer serial on every state // change. Reading it is one cheap syscall; we only re-snapshot the // full mixer state when the serial has actually moved. Only redraw // when the snapshot differs from what we already show, otherwise // self-driven bumps (e.g. the user dragging our own slider) would // cause a redraw on every drag tick. uint64_t now_serial = montauk::audio_wait(g_mixer_serial, 0); if (now_serial != g_mixer_serial) { g_mixer_serial = now_serial; // Master: only refresh when we're not dragging master ourselves, // otherwise a wake mid-drag could snap our knob to the kernel. if (g_drag_target != -1) { int prev_vol = g_master_vol; bool prev_muted = g_master_muted; refresh_master(); if (g_master_vol != prev_vol || g_master_muted != prev_muted) redraw = true; } // Per-stream: diff before redrawing so dragging the master slider // (which bumps the serial) doesn't trigger redundant repaints. int prev_count = g_stream_count; uint8_t prev_vols[8] = {0}; uint8_t prev_mutes[8] = {0}; int prev_handles[8] = {0}; for (int i = 0; i < prev_count && i < 8; i++) { prev_vols[i] = g_streams[i].volume; prev_mutes[i] = g_streams[i].muted; prev_handles[i] = g_streams[i].handle; } refresh_streams(); if (g_stream_count != prev_count) { redraw = true; } else { for (int i = 0; i < g_stream_count; i++) { if (g_streams[i].handle != prev_handles[i] || g_streams[i].volume != prev_vols[i] || g_streams[i].muted != prev_mutes[i]) { redraw = true; break; } } } } if (r < 0) break; if (r == 0) { if (redraw) render(); // Idle: block for up to 16 ms, waking early if the mixer state // changes. This is the only place we sleep — the next loop will // pick up both the change and any queued window event. montauk::audio_wait(g_mixer_serial, 16); continue; } if (ev.type == 3) break; // close if (ev.type == 2 || ev.type == 4) { // resize / scale change redraw = true; } else if (ev.type == 1) { // Always redraw on motion so hover state tracks the cursor — the // window server stops forwarding events once the cursor leaves // our content area, so the last position before exit is near a // window edge (not on a button), which naturally clears hover. g_mouse_x = ev.mouse.x; g_mouse_y = ev.mouse.y; handle_mouse(ev.mouse.x, ev.mouse.y, ev.mouse.buttons, ev.mouse.prev_buttons); redraw = true; } else if (ev.type == 0) { if (handle_key(ev.key)) redraw = true; } if (redraw) render(); } g_win.destroy(); montauk::exit(0); }