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
+86
View File
@@ -0,0 +1,86 @@
# Makefile for audio (standalone Audio Control) on MontaukOS
# Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
# ---- Toolchain ----
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CXX := $(TOOLCHAIN_PREFIX)g++
else
CXX := g++
endif
# ---- Paths ----
PROG_INC := ../../include
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
LIBDIR := ../../lib
# ---- Compiler flags ----
CXXFLAGS := \
-std=gnu++20 \
-g -O2 -pipe \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-unused-function \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-fno-rtti \
-fno-exceptions \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-I $(PROG_INC) \
-isystem $(PROG_INC)/libc \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
# ---- Linker flags ----
LDFLAGS := \
-nostdlib \
-static \
-Wl,--build-id=none \
-Wl,--gc-sections \
-Wl,-m,elf_x86_64 \
-z max-page-size=0x1000 \
-T $(LINK_LD)
# ---- Source files ----
SRCS := main.cpp stb_truetype_impl.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
TARGET := $(BINDIR)/apps/audio/audio.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/apps/audio
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
+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);
}
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Audio"
binary = "audio.elf"
icon = "pavucontrol.svg"
[menu]
category = "System"
visible = true
+35
View File
@@ -0,0 +1,35 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <gui/stb_math.h>
// Override all stb_truetype dependencies before including the implementation
#define STBTT_ifloor(x) ((int) stb_floor(x))
#define STBTT_iceil(x) ((int) stb_ceil(x))
#define STBTT_sqrt(x) stb_sqrt(x)
#define STBTT_pow(x,y) stb_pow(x,y)
#define STBTT_fmod(x,y) stb_fmod(x,y)
#define STBTT_cos(x) stb_cos(x)
#define STBTT_acos(x) stb_acos(x)
#define STBTT_fabs(x) stb_fabs(x)
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
#define STBTT_strlen(x) montauk::slen(x)
#define STBTT_assert(x) ((void)(x))
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>