feat: audio - add concurrent mixing, output switching, and device-aware UI

This commit is contained in:
2026-07-29 20:03:35 +01:00
parent d99dab45e5
commit 86de3400a9
25 changed files with 688 additions and 209 deletions
+88 -3
View File
@@ -26,7 +26,7 @@ using namespace gui;
// ============================================================================
static constexpr int WIN_W = 380;
static constexpr int WIN_H = 460;
static constexpr int WIN_H = 590;
static constexpr int PAD = 22;
static constexpr int KNOB_R = 8;
@@ -43,6 +43,7 @@ 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
static constexpr int OUTPUT_ROW_H = 44;
// ============================================================================
// State
@@ -52,6 +53,8 @@ static WsWindow g_win;
static int g_master_vol = 80;
static bool g_master_muted = false;
static int g_output = montauk::abi::AUDIO_OUTPUT_HDA;
static int g_bt_status = 0; // A2DP State enum
static montauk::abi::AudioStreamInfo g_streams[8];
static int g_stream_count = 0;
@@ -116,10 +119,31 @@ 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) {
static int output_header_y(const mtk::Theme& theme) {
return separator_y(theme) + APPS_HEADER_GAP;
}
static int output_list_y(const mtk::Theme& theme) {
return output_header_y(theme) + system_font_height() + 12;
}
static Rect hda_output_rect(const mtk::Theme& theme) {
return {PAD, output_list_y(theme), g_win.width - PAD * 2, OUTPUT_ROW_H};
}
static Rect bt_output_rect(const mtk::Theme& theme) {
Rect hda = hda_output_rect(theme);
return {hda.x, hda.y + hda.h, hda.w, hda.h};
}
static int apps_separator_y(const mtk::Theme& theme) {
return output_list_y(theme) + OUTPUT_ROW_H * 2 + 18;
}
static int apps_header_y(const mtk::Theme& theme) {
return apps_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;
}
@@ -177,6 +201,21 @@ static void refresh_master() {
g_master_muted = montauk::audio_get_master_mute() == 1;
}
static void refresh_output() {
int output = montauk::audio_get_output(-1);
if (output == montauk::abi::AUDIO_OUTPUT_HDA ||
output == montauk::abi::AUDIO_OUTPUT_BLUETOOTH) g_output = output;
g_bt_status = montauk::audio_bt_status(-1);
}
static bool switch_output(int output) {
if (output == g_output) return false;
if (montauk::audio_set_output(output) < 0) return false;
g_output = output;
refresh_output();
return true;
}
static void apply_master_volume(int v) {
if (v < 0) v = 0;
if (v > 100) v = 100;
@@ -265,6 +304,33 @@ static void draw_master(Canvas& c, const mtk::Theme& theme) {
st, theme);
}
static void draw_outputs(Canvas& c, const mtk::Theme& theme) {
c.text(PAD, output_header_y(theme), "OUTPUT DEVICE", theme.text_muted);
Rect hda = hda_output_rect(theme);
Rect bt = bt_output_rect(theme);
bool bt_ready = g_bt_status >= 2;
bool hda_selected = g_output == montauk::abi::AUDIO_OUTPUT_HDA;
bool bt_selected = g_output == montauk::abi::AUDIO_OUTPUT_BLUETOOTH;
// Keep the choices directly on the page, consistent with the rest of the
// app's unboxed sections.
Rect hda_radio = {hda.x, hda.y + 6, 18, hda.h - 12};
Rect bt_radio = {bt.x, bt.y + 6, 18, bt.h - 12};
mtk::draw_radio(c, hda_radio, "", hda_selected, theme);
mtk::draw_radio(c, bt_radio, "", bt_selected, theme);
int text_x = hda.x + 30;
c.text(text_x, hda.y + 4, "Built-in Speakers", theme.text);
c.text(text_x, hda.y + 23, "High Definition Audio", theme.text_subtle);
Color bt_text = bt_ready ? theme.text : theme.text_muted;
c.text(text_x, bt.y + 4, "Bluetooth", bt_text);
c.text(text_x, bt.y + 23,
bt_ready ? "Connected audio device" : "No device connected",
bt_ready ? theme.text_subtle : theme.text_muted);
}
static void draw_applications(Canvas& c, const mtk::Theme& theme) {
int hy = apps_header_y(theme);
c.text(PAD, hy, "APPLICATIONS", theme.text_muted);
@@ -278,7 +344,8 @@ static void draw_applications(Canvas& c, const mtk::Theme& theme) {
}
int visible = g_stream_count;
int rows_room = (g_win.height - stream_list_y(theme) - PAD) / stream_row_h(theme);
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;
@@ -320,6 +387,9 @@ static void render() {
draw_master(c, theme);
mtk::draw_separator(c, PAD, separator_y(theme),
g_win.width - PAD * 2, theme);
draw_outputs(c, theme);
mtk::draw_separator(c, PAD, apps_separator_y(theme),
g_win.width - PAD * 2, theme);
draw_applications(c, theme);
host.present();
@@ -336,6 +406,14 @@ static bool handle_mouse(int mx, int my, uint8_t buttons, uint8_t prev) {
bool down = (buttons & 1) != 0;
if (clicked) {
if (hda_output_rect(theme).contains(mx, my)) {
switch_output(montauk::abi::AUDIO_OUTPUT_HDA);
return true;
}
if (bt_output_rect(theme).contains(mx, my) && g_bt_status >= 2) {
switch_output(montauk::abi::AUDIO_OUTPUT_BLUETOOTH);
return true;
}
// Master slider.
Rect slider = master_slider_rect();
if (slider_hit(slider, mx, my)) {
@@ -418,6 +496,7 @@ extern "C" void _start() {
}
load_accent();
refresh_output();
refresh_master();
refresh_streams();
g_mixer_serial = montauk::audio_wait(0, 0);
@@ -443,6 +522,12 @@ extern "C" void _start() {
if (now_serial != g_mixer_serial) {
g_mixer_serial = now_serial;
int prev_output = g_output;
int prev_bt_status = g_bt_status;
refresh_output();
if (g_output != prev_output || g_bt_status != prev_bt_status)
redraw = true;
// 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) {