feat: audio mixer, Audio app, fix Installer crash
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Makefile for volume (standalone Volume Control) on MontaukOS
|
||||
# Makefile for audio (standalone Audio Control) on MontaukOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
@@ -68,14 +68,14 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := $(BINDIR)/apps/volume/volume.elf
|
||||
TARGET := $(BINDIR)/apps/audio/audio.elf
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/apps/volume
|
||||
mkdir -p $(BINDIR)/apps/audio
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
@@ -0,0 +1,487 @@
|
||||
/*
|
||||
* 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 <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/mtk.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
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::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;
|
||||
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;
|
||||
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::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::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. The master
|
||||
// value is left alone while the user is dragging the master slider
|
||||
// so a mid-drag wakeup can't snap the knob to the kernel value.
|
||||
uint64_t now_serial = montauk::audio_wait(g_mixer_serial, 0);
|
||||
if (now_serial != g_mixer_serial) {
|
||||
g_mixer_serial = now_serial;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
refresh_streams();
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[app]
|
||||
name = "Volume"
|
||||
binary = "volume.elf"
|
||||
name = "Audio"
|
||||
binary = "audio.elf"
|
||||
icon = "pavucontrol.svg"
|
||||
|
||||
[menu]
|
||||
@@ -401,7 +401,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
if (v > 100) v = 100;
|
||||
ds->vol_muted = false;
|
||||
ds->vol_level = v;
|
||||
montauk::audio_set_volume(0, v);
|
||||
montauk::audio_set_master_volume(v);
|
||||
return;
|
||||
}
|
||||
if (left_released) {
|
||||
@@ -422,7 +422,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
if (v > 100) v = 100;
|
||||
ds->vol_muted = false;
|
||||
ds->vol_level = v;
|
||||
montauk::audio_set_volume(0, v);
|
||||
montauk::audio_set_master_volume(v);
|
||||
ds->vol_dragging = true;
|
||||
return;
|
||||
}
|
||||
@@ -441,7 +441,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
int v = ds->vol_level - 5;
|
||||
if (v < 0) v = 0;
|
||||
ds->vol_level = v;
|
||||
montauk::audio_set_volume(0, v);
|
||||
montauk::audio_set_master_volume(v);
|
||||
return;
|
||||
}
|
||||
bx += minus_w + gap;
|
||||
@@ -451,7 +451,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
int v = ds->vol_level + 5;
|
||||
if (v > 100) v = 100;
|
||||
ds->vol_level = v;
|
||||
montauk::audio_set_volume(0, v);
|
||||
montauk::audio_set_master_volume(v);
|
||||
return;
|
||||
}
|
||||
bx += plus_w + gap;
|
||||
@@ -459,12 +459,13 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
if (mx >= bx && mx < bx + mute_w) {
|
||||
if (ds->vol_muted) {
|
||||
ds->vol_muted = false;
|
||||
montauk::audio_set_volume(0, ds->vol_pre_mute);
|
||||
montauk::audio_set_master_mute(false);
|
||||
montauk::audio_set_master_volume(ds->vol_pre_mute);
|
||||
ds->vol_level = ds->vol_pre_mute;
|
||||
} else {
|
||||
ds->vol_pre_mute = ds->vol_level;
|
||||
ds->vol_muted = true;
|
||||
montauk::audio_set_volume(0, 0);
|
||||
montauk::audio_set_master_mute(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -311,12 +311,13 @@ void gui::desktop_init(DesktopState* ds) {
|
||||
|
||||
ds->vol_popup_open = false;
|
||||
ds->vol_icon_rect = {0, 0, 0, 0};
|
||||
int vol = montauk::audio_get_volume(0);
|
||||
int vol = montauk::audio_get_master_volume();
|
||||
ds->vol_level = vol >= 0 ? vol : 80;
|
||||
ds->vol_muted = false;
|
||||
ds->vol_muted = montauk::audio_get_master_mute() == 1;
|
||||
ds->vol_pre_mute = ds->vol_level;
|
||||
ds->vol_dragging = false;
|
||||
ds->vol_last_poll = montauk::get_milliseconds();
|
||||
ds->vol_serial = montauk::audio_wait(0, 0);
|
||||
|
||||
ds->closing_ext_count = 0;
|
||||
|
||||
@@ -393,11 +394,28 @@ static bool desktop_refresh_panel_state(DesktopState* ds, uint64_t now) {
|
||||
ds->net_cfg_last_poll = now;
|
||||
}
|
||||
|
||||
if (now - ds->vol_last_poll > 5000) {
|
||||
int v = montauk::audio_get_volume(0);
|
||||
if (v >= 0 && !ds->vol_muted && v != ds->vol_level) {
|
||||
ds->vol_level = v;
|
||||
changed = true;
|
||||
// Event-driven sync: the kernel mixer bumps a serial on every state
|
||||
// change. Reading it is one cheap syscall; refresh only when the serial
|
||||
// moved since we last looked. The audio app (and any other client) will
|
||||
// bump this serial when they change volume/mute, so the popup and the
|
||||
// panel speaker icon stay in sync without polling them blindly.
|
||||
//
|
||||
// Skip the refresh while our own slider is being dragged so a kernel
|
||||
// snapshot mid-drag can't clobber the in-flight value.
|
||||
if (!ds->vol_dragging) {
|
||||
uint64_t serial = montauk::audio_wait(ds->vol_serial, 0);
|
||||
if (serial != ds->vol_serial) {
|
||||
ds->vol_serial = serial;
|
||||
int v = montauk::audio_get_master_volume();
|
||||
bool muted = montauk::audio_get_master_mute() == 1;
|
||||
if (v >= 0 && v != ds->vol_level) {
|
||||
ds->vol_level = v;
|
||||
changed = true;
|
||||
}
|
||||
if (muted != ds->vol_muted) {
|
||||
ds->vol_muted = muted;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ds->vol_last_poll = now;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ CXXFLAGS := \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-MMD -MP \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
@@ -82,5 +83,7 @@ $(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
-include $(OBJS:.o=.d)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
|
||||
@@ -40,7 +40,7 @@ static constexpr int SWATCH_Y = 8;
|
||||
static constexpr int MAX_CANVAS_W = 2048;
|
||||
static constexpr int MAX_CANVAS_H = 2048;
|
||||
|
||||
static constexpr int FONT_SIZE = 16;
|
||||
static constexpr int FONT_SIZE = 18;
|
||||
|
||||
// UI colors
|
||||
static constexpr Color BG_COLOR = Color::from_rgb(0xE0, 0xE0, 0xE0);
|
||||
|
||||
@@ -41,7 +41,7 @@ static constexpr int INIT_CAP = 4096;
|
||||
static constexpr int MAX_CAP = 262144; // 256KB
|
||||
static constexpr int MAX_LINES = 16384;
|
||||
static constexpr int TAB_WIDTH = 4;
|
||||
static constexpr int FONT_SIZE = 16;
|
||||
static constexpr int FONT_SIZE = 18;
|
||||
static constexpr int SYN_LINE_BUF_CAP = 1024;
|
||||
|
||||
// Colors
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS Volume Control
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int WIN_W = 280;
|
||||
static constexpr int WIN_H = 164;
|
||||
static constexpr int FONT_SIZE = 16;
|
||||
static constexpr int FONT_SIZE_LG = 28;
|
||||
|
||||
static constexpr int SLIDER_X = 24;
|
||||
static constexpr int SLIDER_Y = 78;
|
||||
static constexpr int SLIDER_H = 8;
|
||||
static constexpr int KNOB_R = 10;
|
||||
|
||||
static constexpr int BTN_W = 48;
|
||||
static constexpr int BTN_H = 28;
|
||||
static constexpr int BTN_RAD = 6;
|
||||
|
||||
static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr Color TEXT_COLOR = Color::from_rgb(0x22, 0x22, 0x22);
|
||||
static constexpr Color ACCENT = Color::from_rgb(0x36, 0x7B, 0xF0);
|
||||
static constexpr Color TRACK_BG = Color::from_rgb(0xDD, 0xDD, 0xDD);
|
||||
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr Color MUTE_COLOR = Color::from_rgb(0xCC, 0x33, 0x33);
|
||||
|
||||
// ============================================================================
|
||||
// State
|
||||
// ============================================================================
|
||||
|
||||
static TrueTypeFont* g_font = nullptr;
|
||||
static int g_win_w = WIN_W;
|
||||
static int g_win_h = WIN_H;
|
||||
static int g_volume = 80;
|
||||
static bool g_muted = false;
|
||||
static int g_pre_mute_vol = 80;
|
||||
static bool g_dragging = false;
|
||||
|
||||
// ============================================================================
|
||||
// Volume helpers
|
||||
// ============================================================================
|
||||
|
||||
static void apply_volume(int vol) {
|
||||
if (vol < 0) vol = 0;
|
||||
if (vol > 100) vol = 100;
|
||||
g_volume = vol;
|
||||
montauk::audio_set_volume(0, g_volume);
|
||||
}
|
||||
|
||||
static void refresh_volume() {
|
||||
int v = montauk::audio_get_volume(0);
|
||||
if (v >= 0) g_volume = v;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Layout helpers
|
||||
// ============================================================================
|
||||
|
||||
static int slider_w() {
|
||||
return g_win_w - 48;
|
||||
}
|
||||
|
||||
static int button_y() {
|
||||
return g_win_h - BTN_H - 18;
|
||||
}
|
||||
|
||||
static void get_button_rects(Rect* minus_btn, Rect* plus_btn, Rect* mute_btn) {
|
||||
int total_btn_w = BTN_W * 2 + 60 + 12 * 2;
|
||||
int bx = (g_win_w - total_btn_w) / 2;
|
||||
int by = button_y();
|
||||
if (minus_btn) *minus_btn = {bx, by, BTN_W, BTN_H};
|
||||
bx += BTN_W + 12;
|
||||
if (plus_btn) *plus_btn = {bx, by, BTN_W, BTN_H};
|
||||
bx += BTN_W + 12;
|
||||
if (mute_btn) *mute_btn = {bx, by, 60, BTN_H};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Render
|
||||
// ============================================================================
|
||||
|
||||
static void render(Canvas& canvas) {
|
||||
int fh_lg = text_height(g_font, FONT_SIZE_LG);
|
||||
int slider_fill_w = slider_w();
|
||||
|
||||
// Background
|
||||
canvas.fill(BG_COLOR);
|
||||
|
||||
// Volume percentage (large, centered)
|
||||
char vol_str[8];
|
||||
snprintf(vol_str, sizeof(vol_str), "%d%%", g_muted ? 0 : g_volume);
|
||||
int vw = text_width(g_font, vol_str, FONT_SIZE_LG);
|
||||
Color vol_color = g_muted ? MUTE_COLOR : ACCENT;
|
||||
draw_text(canvas, g_font, (g_win_w - vw) / 2, 20, vol_str, vol_color, FONT_SIZE_LG);
|
||||
|
||||
// "Muted" label
|
||||
if (g_muted) {
|
||||
const char* muted_label = "Muted";
|
||||
int mw = text_width(g_font, muted_label, FONT_SIZE);
|
||||
draw_text(canvas, g_font, (g_win_w - mw) / 2,
|
||||
20 + fh_lg + 4, muted_label, MUTE_COLOR, FONT_SIZE);
|
||||
}
|
||||
|
||||
// Slider track
|
||||
canvas.fill_rounded_rect(SLIDER_X, SLIDER_Y, slider_fill_w, SLIDER_H, 4, TRACK_BG);
|
||||
|
||||
// Filled portion
|
||||
int fill_w = ((g_muted ? 0 : g_volume) * slider_fill_w) / 100;
|
||||
if (fill_w > 0)
|
||||
canvas.fill_rounded_rect(SLIDER_X, SLIDER_Y, fill_w, SLIDER_H, 4, ACCENT);
|
||||
|
||||
// Knob
|
||||
int knob_x = SLIDER_X + fill_w;
|
||||
int knob_y = SLIDER_Y + SLIDER_H / 2;
|
||||
fill_circle(canvas, knob_x, knob_y, KNOB_R, ACCENT);
|
||||
fill_circle(canvas, knob_x, knob_y, KNOB_R - 3, WHITE);
|
||||
|
||||
// Buttons: [-] and [+] and [Mute]
|
||||
Rect minus_btn, plus_btn, mute_btn;
|
||||
get_button_rects(&minus_btn, &plus_btn, &mute_btn);
|
||||
draw_button(canvas, g_font, minus_btn.x, minus_btn.y, minus_btn.w, minus_btn.h,
|
||||
"-", Color::from_rgb(0xE0, 0xE0, 0xE0), TEXT_COLOR, BTN_RAD, FONT_SIZE);
|
||||
draw_button(canvas, g_font, plus_btn.x, plus_btn.y, plus_btn.w, plus_btn.h,
|
||||
"+", Color::from_rgb(0xE0, 0xE0, 0xE0), TEXT_COLOR, BTN_RAD, FONT_SIZE);
|
||||
Color mute_bg = g_muted ? MUTE_COLOR : Color::from_rgb(0xE0, 0xE0, 0xE0);
|
||||
Color mute_fg = g_muted ? WHITE : TEXT_COLOR;
|
||||
draw_button(canvas, g_font, mute_btn.x, mute_btn.y, mute_btn.w, mute_btn.h,
|
||||
"Mute", mute_bg, mute_fg, BTN_RAD, FONT_SIZE);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hit testing
|
||||
// ============================================================================
|
||||
|
||||
static int vol_from_slider_x(int mx) {
|
||||
int v = ((mx - SLIDER_X) * 100) / slider_w();
|
||||
if (v < 0) v = 0;
|
||||
if (v > 100) v = 100;
|
||||
return v;
|
||||
}
|
||||
|
||||
static bool handle_click(int mx, int my) {
|
||||
// Slider area (generous vertical hit zone)
|
||||
if (my >= SLIDER_Y - KNOB_R && my <= SLIDER_Y + SLIDER_H + KNOB_R &&
|
||||
mx >= SLIDER_X - KNOB_R && mx <= SLIDER_X + slider_w() + KNOB_R) {
|
||||
g_muted = false;
|
||||
apply_volume(vol_from_slider_x(mx));
|
||||
g_dragging = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Rect minus_btn, plus_btn, mute_btn;
|
||||
get_button_rects(&minus_btn, &plus_btn, &mute_btn);
|
||||
|
||||
int by = button_y();
|
||||
if (my >= by && my < by + BTN_H) {
|
||||
// [-] button
|
||||
if (minus_btn.contains(mx, my)) {
|
||||
g_muted = false;
|
||||
apply_volume(g_volume - 5);
|
||||
return true;
|
||||
}
|
||||
|
||||
// [+] button
|
||||
if (plus_btn.contains(mx, my)) {
|
||||
g_muted = false;
|
||||
apply_volume(g_volume + 5);
|
||||
return true;
|
||||
}
|
||||
|
||||
// [Mute] button
|
||||
if (mute_btn.contains(mx, my)) {
|
||||
if (g_muted) {
|
||||
g_muted = false;
|
||||
apply_volume(g_pre_mute_vol);
|
||||
} else {
|
||||
g_pre_mute_vol = g_volume;
|
||||
g_muted = true;
|
||||
montauk::audio_set_volume(0, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Entry point
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void _start() {
|
||||
// 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;
|
||||
}
|
||||
|
||||
refresh_volume();
|
||||
|
||||
// Create window
|
||||
WsWindow win;
|
||||
if (!win.create("Volume", WIN_W, WIN_H))
|
||||
montauk::exit(1);
|
||||
|
||||
g_win_w = win.width;
|
||||
g_win_h = win.height;
|
||||
|
||||
{
|
||||
Canvas canvas = win.canvas();
|
||||
render(canvas);
|
||||
}
|
||||
win.present();
|
||||
|
||||
while (win.id >= 0 && !win.closed) {
|
||||
Montauk::WinEvent ev;
|
||||
int r = win.poll(&ev);
|
||||
|
||||
if (r < 0) break;
|
||||
if (r == 0) { montauk::sleep_ms(16); continue; }
|
||||
|
||||
bool redraw = false;
|
||||
|
||||
if (ev.type == 3) break; // close
|
||||
if (ev.type == 2) {
|
||||
g_win_w = win.width;
|
||||
g_win_h = win.height;
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
// Keyboard
|
||||
if (ev.type == 0 && ev.key.pressed) {
|
||||
if (ev.key.scancode == 0x01) break; // Escape
|
||||
if (ev.key.scancode == 0x4D || ev.key.ascii == '+' || ev.key.ascii == '=') { // Right / +
|
||||
g_muted = false;
|
||||
apply_volume(g_volume + 5);
|
||||
redraw = true;
|
||||
} else if (ev.key.scancode == 0x4B || ev.key.ascii == '-') { // Left / -
|
||||
g_muted = false;
|
||||
apply_volume(g_volume - 5);
|
||||
redraw = true;
|
||||
} else if (ev.key.ascii == 'm' || ev.key.ascii == 'M') {
|
||||
if (g_muted) {
|
||||
g_muted = false;
|
||||
apply_volume(g_pre_mute_vol);
|
||||
} else {
|
||||
g_pre_mute_vol = g_volume;
|
||||
g_muted = true;
|
||||
montauk::audio_set_volume(0, 0);
|
||||
}
|
||||
redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse
|
||||
if (ev.type == 1) {
|
||||
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
bool released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1);
|
||||
|
||||
if (clicked) {
|
||||
if (handle_click(ev.mouse.x, ev.mouse.y))
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
if (g_dragging && (ev.mouse.buttons & 1)) {
|
||||
g_muted = false;
|
||||
apply_volume(vol_from_slider_x(ev.mouse.x));
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
if (released) {
|
||||
g_dragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (redraw) {
|
||||
Canvas canvas = win.canvas();
|
||||
render(canvas);
|
||||
win.present();
|
||||
}
|
||||
}
|
||||
|
||||
win.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
Reference in New Issue
Block a user