feat: audio mixer, Audio app, fix Installer crash

This commit is contained in:
2026-05-19 07:48:09 +02:00
parent 77a536bcc3
commit c6ca17984b
26 changed files with 1272 additions and 373 deletions
+8 -8
View File
@@ -59,7 +59,7 @@ BINDIR := bin
PROGRAMS := $(notdir $(wildcard src/*))
# Programs with custom Makefiles (built separately).
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth network terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs test_dialogs test_dl libloader libhello crashpad
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs test_dialogs test_dl libloader libhello crashpad
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
@@ -86,9 +86,9 @@ CONFIGDST := $(patsubst $(CONFIGDIR)/%,$(BINDIR)/config/%,$(CONFIGSRC))
# Common shared assets (wallpapers, etc.)
COMMONKEEP := $(BINDIR)/common/.keep
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth network terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs bearssl libc tls libjpeg libjpegwrite install-apps libloader libs libhello test_dialogs test_dl crashpad
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs bearssl libc tls libjpeg libjpegwrite install-apps libloader libs libhello test_dialogs test_dl crashpad
all: bearssl libc libjpeg libjpegwrite tls libloader libs libhello $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth network terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell icons fonts install-apps test_dialogs test_dl crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(COMMONKEEP)
all: bearssl libc libjpeg libjpegwrite tls libloader libs libhello $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell icons fonts install-apps test_dialogs test_dl crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(COMMONKEEP)
# Build BearSSL static library (cross-compiled for freestanding x86_64).
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
@@ -180,9 +180,9 @@ devexplorer: libc
installer: libc
$(MAKE) -C src/installer
# Build volume control standalone GUI tool (depends on libc).
volume: libc
$(MAKE) -C src/volume
# Build audio control standalone GUI tool (depends on libc).
audio: libc
$(MAKE) -C src/audio
# Build music player standalone GUI tool (depends on libc).
music: libc
@@ -309,7 +309,7 @@ test_dialogs: libc libloader dialogs
$(MAKE) -C src/test_dialogs
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth network terminal klog procmgr calculator screenshot texteditor mandelbrot printers timezone crashpad
install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer audio music video bluetooth network terminal klog procmgr calculator screenshot texteditor mandelbrot printers timezone crashpad
../scripts/install_apps.sh
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
@@ -365,7 +365,7 @@ clean:
$(MAKE) -C src/disks clean
$(MAKE) -C src/devexplorer clean
$(MAKE) -C src/installer clean
$(MAKE) -C src/volume clean
$(MAKE) -C src/audio clean
$(MAKE) -C src/music clean
$(MAKE) -C src/video clean
$(MAKE) -C src/bluetooth clean
+36 -8
View File
@@ -162,6 +162,11 @@ namespace Montauk {
static constexpr uint64_t SYS_NETSTATUS = 125;
static constexpr uint64_t SYS_DRIVEKIND = 127;
// Audio mixer enumeration
static constexpr uint64_t SYS_AUDIOLIST = 128;
// Block until the mixer state serial differs from the caller's snapshot.
static constexpr uint64_t SYS_AUDIOWAIT = 129;
static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024;
static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0;
@@ -170,14 +175,23 @@ namespace Montauk {
static constexpr uint32_t IPC_SIGNAL_EXITED = 1u << 3;
static constexpr uint32_t IPC_SIGNAL_READY = 1u << 4;
// Audio control commands (for SYS_AUDIOCTL)
static constexpr int AUDIO_CTL_SET_VOLUME = 0;
static constexpr int AUDIO_CTL_GET_VOLUME = 1;
static constexpr int AUDIO_CTL_GET_POS = 2;
static constexpr int AUDIO_CTL_PAUSE = 3;
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
static constexpr int AUDIO_CTL_BT_STATUS = 6; // Get Bluetooth connection status
// Audio control commands (for SYS_AUDIOCTL).
//
// Commands 0..3 act on the stream named by the handle argument.
// Commands 7..12 act on the global master and ignore the handle.
static constexpr int AUDIO_CTL_SET_VOLUME = 0;
static constexpr int AUDIO_CTL_GET_VOLUME = 1;
static constexpr int AUDIO_CTL_GET_POS = 2;
static constexpr int AUDIO_CTL_PAUSE = 3;
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
static constexpr int AUDIO_CTL_BT_STATUS = 6; // Get Bluetooth status
static constexpr int AUDIO_CTL_SET_MASTER_VOLUME = 7; // 0-100
static constexpr int AUDIO_CTL_GET_MASTER_VOLUME = 8;
static constexpr int AUDIO_CTL_SET_MUTE = 9; // 0/1, per-stream
static constexpr int AUDIO_CTL_GET_MUTE = 10;
static constexpr int AUDIO_CTL_SET_MASTER_MUTE = 11;
static constexpr int AUDIO_CTL_GET_MASTER_MUTE = 12;
static constexpr int SOCK_TCP = 1;
static constexpr int SOCK_UDP = 2;
@@ -252,6 +266,20 @@ namespace Montauk {
uint64_t serial;
};
// One entry per active mixer stream; returned by SYS_AUDIOLIST.
struct AudioStreamInfo {
int32_t handle;
int32_t ownerPid;
char name[64];
uint32_t sampleRate;
uint8_t channels;
uint8_t bitsPerSample;
uint8_t volume; // 0-100
uint8_t muted; // 0/1
uint8_t paused; // 0/1
uint8_t _pad[7];
};
// Window server shared types
struct WinEvent {
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
+1
View File
@@ -171,6 +171,7 @@ struct DesktopState {
int vol_pre_mute; // volume before mute
bool vol_dragging; // slider drag in progress
uint64_t vol_last_poll;
uint64_t vol_serial; // last seen mixer state serial
// Temperature monitoring
static constexpr int MAX_THERMAL_ZONES = 8;
+27
View File
@@ -477,6 +477,33 @@ namespace montauk {
inline int audio_bt_status(int handle) {
return audio_ctl(handle, Montauk::AUDIO_CTL_BT_STATUS, 0);
}
inline int audio_set_master_volume(int percent) {
return audio_ctl(-1, Montauk::AUDIO_CTL_SET_MASTER_VOLUME, percent);
}
inline int audio_get_master_volume() {
return audio_ctl(-1, Montauk::AUDIO_CTL_GET_MASTER_VOLUME, 0);
}
inline int audio_set_master_mute(bool muted) {
return audio_ctl(-1, Montauk::AUDIO_CTL_SET_MASTER_MUTE, muted ? 1 : 0);
}
inline int audio_get_master_mute() {
return audio_ctl(-1, Montauk::AUDIO_CTL_GET_MASTER_MUTE, 0);
}
inline int audio_set_mute(int handle, bool muted) {
return audio_ctl(handle, Montauk::AUDIO_CTL_SET_MUTE, muted ? 1 : 0);
}
inline int audio_get_mute(int handle) {
return audio_ctl(handle, Montauk::AUDIO_CTL_GET_MUTE, 0);
}
inline int audio_list(Montauk::AudioStreamInfo* buf, int maxCount) {
return (int)syscall2(Montauk::SYS_AUDIOLIST, (uint64_t)buf, (uint64_t)maxCount);
}
// Returns the current mixer state serial. With timeoutMs > 0 the call
// blocks until the serial differs from `prevSerial` or the timeout
// elapses. With timeoutMs == 0 it returns immediately (cheap snapshot).
inline uint64_t audio_wait(uint64_t prevSerial, uint64_t timeoutMs) {
return syscall2(Montauk::SYS_AUDIOWAIT, prevSerial, timeoutMs);
}
// Bluetooth
inline int bt_scan(Montauk::BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
@@ -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
+487
View File
@@ -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]
+7 -6
View File
@@ -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;
}
+25 -7
View File
@@ -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;
}
+3
View File
@@ -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)
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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
-309
View File
@@ -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);
}