feat: audio mixer, Audio app, fix Installer crash
This commit is contained in:
+26
-10
@@ -1,33 +1,42 @@
|
|||||||
/*
|
/*
|
||||||
* Audio.hpp
|
* Audio.hpp
|
||||||
* Audio syscall implementations
|
* Audio syscall implementations
|
||||||
* Routes audio to Intel HDA or Bluetooth A2DP based on handle
|
* Routes audio through the software mixer to the active hardware backend
|
||||||
|
* (Intel HDA or Bluetooth A2DP).
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <Drivers/Audio/IntelHda.hpp>
|
#include <Drivers/Audio/IntelHda.hpp>
|
||||||
|
#include <Drivers/Audio/Mixer.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
||||||
|
#include <Sched/Scheduler.hpp>
|
||||||
|
|
||||||
#include "Syscall.hpp"
|
#include "Syscall.hpp"
|
||||||
|
|
||||||
namespace Montauk {
|
namespace Montauk {
|
||||||
|
|
||||||
// Audio handle convention:
|
// Audio handle convention:
|
||||||
// 0x00 - 0x0F : Intel HDA handles
|
// 0x00 - 0x07 : Mixer virtual streams (one per opened audio handle)
|
||||||
// 0x100 : Bluetooth A2DP audio output
|
// 0x100 : Bluetooth A2DP audio output (bypasses mixer for now)
|
||||||
|
|
||||||
static constexpr int AUDIO_HANDLE_BT = 0x100;
|
static constexpr int AUDIO_HANDLE_BT = 0x100;
|
||||||
|
|
||||||
static int64_t Sys_AudioOpen(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) {
|
static int64_t Sys_AudioOpen(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) {
|
||||||
// If HDA is available, use it (primary output)
|
// HDA-backed mixer is the preferred output. The mixer keeps the HDA
|
||||||
|
// hardware stream open across virtual streams, so multiple apps can
|
||||||
|
// play simultaneously.
|
||||||
if (Drivers::Audio::IntelHda::IsInitialized()) {
|
if (Drivers::Audio::IntelHda::IsInitialized()) {
|
||||||
return (int64_t)Drivers::Audio::IntelHda::Open(sampleRate, channels, bitsPerSample);
|
auto* proc = Sched::GetCurrentProcessPtr();
|
||||||
|
int pid = proc ? proc->pid : -1;
|
||||||
|
const char* name = proc ? proc->name : "?";
|
||||||
|
return (int64_t)Drivers::Audio::Mixer::Open(sampleRate, channels,
|
||||||
|
bitsPerSample, pid, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: try Bluetooth audio if available
|
// Fallback: Bluetooth A2DP when no HDA controller is present.
|
||||||
if (Drivers::USB::Bluetooth::IsInitialized()) {
|
if (Drivers::USB::Bluetooth::IsInitialized()) {
|
||||||
auto state = Drivers::USB::Bluetooth::A2dp::GetState();
|
auto state = Drivers::USB::Bluetooth::A2dp::GetState();
|
||||||
if (state == Drivers::USB::Bluetooth::A2dp::State::Open ||
|
if (state == Drivers::USB::Bluetooth::A2dp::State::Open ||
|
||||||
@@ -47,7 +56,7 @@ namespace Montauk {
|
|||||||
Drivers::USB::Bluetooth::A2dp::StopStream();
|
Drivers::USB::Bluetooth::A2dp::StopStream();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Drivers::Audio::IntelHda::Close(handle);
|
Drivers::Audio::Mixer::Close(handle);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +64,7 @@ namespace Montauk {
|
|||||||
if (handle == AUDIO_HANDLE_BT) {
|
if (handle == AUDIO_HANDLE_BT) {
|
||||||
return (int64_t)Drivers::USB::Bluetooth::A2dp::WriteAudio(data, size);
|
return (int64_t)Drivers::USB::Bluetooth::A2dp::WriteAudio(data, size);
|
||||||
}
|
}
|
||||||
return (int64_t)Drivers::Audio::IntelHda::Write(handle, data, size);
|
return (int64_t)Drivers::Audio::Mixer::Write(handle, data, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int64_t Sys_AudioCtl(int handle, int cmd, int value) {
|
static int64_t Sys_AudioCtl(int handle, int cmd, int value) {
|
||||||
@@ -77,14 +86,21 @@ namespace Montauk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional control commands for audio routing
|
|
||||||
if (cmd == AUDIO_CTL_GET_OUTPUT) return 0; // HDA
|
if (cmd == AUDIO_CTL_GET_OUTPUT) return 0; // HDA
|
||||||
if (cmd == AUDIO_CTL_BT_STATUS) {
|
if (cmd == AUDIO_CTL_BT_STATUS) {
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
||||||
return (int64_t)Drivers::USB::Bluetooth::A2dp::GetState();
|
return (int64_t)Drivers::USB::Bluetooth::A2dp::GetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int64_t)Drivers::Audio::IntelHda::Control(handle, cmd, value);
|
return (int64_t)Drivers::Audio::Mixer::Control(handle, cmd, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int64_t Sys_AudioList(AudioStreamInfo* buf, int maxCount) {
|
||||||
|
return (int64_t)Drivers::Audio::Mixer::List(buf, maxCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int64_t Sys_AudioWait(uint64_t prevSerial, uint64_t timeoutMs) {
|
||||||
|
return (int64_t)Drivers::Audio::Mixer::Wait(prevSerial, timeoutMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ namespace Montauk {
|
|||||||
auto* rep = &CrashReport::g_reports[oldestIdx];
|
auto* rep = &CrashReport::g_reports[oldestIdx];
|
||||||
|
|
||||||
out->pid = rep->pid;
|
out->pid = rep->pid;
|
||||||
for (int i = 0; i < 64 && rep->processName[i]; i++) out->processName[i] = rep->processName[i];
|
int pn;
|
||||||
out->processName[63] = '\0';
|
for (pn = 0; pn < 63 && rep->processName[pn]; pn++) out->processName[pn] = rep->processName[pn];
|
||||||
|
out->processName[pn] = '\0';
|
||||||
out->exceptionVector = rep->exceptionVector;
|
out->exceptionVector = rep->exceptionVector;
|
||||||
for (int i = 0; i < 32 && rep->exceptionName[i]; i++) out->exceptionName[i] = rep->exceptionName[i];
|
int en;
|
||||||
out->exceptionName[31] = '\0';
|
for (en = 0; en < 31 && rep->exceptionName[en]; en++) out->exceptionName[en] = rep->exceptionName[en];
|
||||||
|
out->exceptionName[en] = '\0';
|
||||||
out->faultingAddress = rep->faultingAddress;
|
out->faultingAddress = rep->faultingAddress;
|
||||||
out->instructionPointer = rep->instructionPointer;
|
out->instructionPointer = rep->instructionPointer;
|
||||||
out->stackPointer = rep->stackPointer;
|
out->stackPointer = rep->stackPointer;
|
||||||
|
|||||||
@@ -334,6 +334,12 @@ namespace Montauk {
|
|||||||
return Sys_AudioWrite((int)frame->arg1, (const uint8_t*)frame->arg2, (uint32_t)frame->arg3);
|
return Sys_AudioWrite((int)frame->arg1, (const uint8_t*)frame->arg2, (uint32_t)frame->arg3);
|
||||||
case SYS_AUDIOCTL:
|
case SYS_AUDIOCTL:
|
||||||
return Sys_AudioCtl((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
|
return Sys_AudioCtl((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
|
||||||
|
case SYS_AUDIOLIST:
|
||||||
|
if ((int64_t)frame->arg2 < 0) return -1;
|
||||||
|
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(AudioStreamInfo), true)) return -1;
|
||||||
|
return Sys_AudioList((AudioStreamInfo*)frame->arg1, (int)frame->arg2);
|
||||||
|
case SYS_AUDIOWAIT:
|
||||||
|
return Sys_AudioWait(frame->arg1, frame->arg2);
|
||||||
case SYS_BTSCAN:
|
case SYS_BTSCAN:
|
||||||
if ((int64_t)frame->arg2 < 0) return -1;
|
if ((int64_t)frame->arg2 < 0) return -1;
|
||||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtScanResult), true)) return -1;
|
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtScanResult), true)) return -1;
|
||||||
|
|||||||
@@ -157,14 +157,23 @@ namespace Montauk {
|
|||||||
static constexpr uint64_t SYS_AUDIOWRITE = 82;
|
static constexpr uint64_t SYS_AUDIOWRITE = 82;
|
||||||
static constexpr uint64_t SYS_AUDIOCTL = 83;
|
static constexpr uint64_t SYS_AUDIOCTL = 83;
|
||||||
|
|
||||||
// Audio control commands (for SYS_AUDIOCTL)
|
// Audio control commands (for SYS_AUDIOCTL).
|
||||||
static constexpr int AUDIO_CTL_SET_VOLUME = 0;
|
//
|
||||||
static constexpr int AUDIO_CTL_GET_VOLUME = 1;
|
// Commands 0..3 act on the stream named by the handle argument.
|
||||||
static constexpr int AUDIO_CTL_GET_POS = 2;
|
// Commands 7..10 act on the global master and ignore the handle.
|
||||||
static constexpr int AUDIO_CTL_PAUSE = 3;
|
static constexpr int AUDIO_CTL_SET_VOLUME = 0;
|
||||||
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
|
static constexpr int AUDIO_CTL_GET_VOLUME = 1;
|
||||||
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
|
static constexpr int AUDIO_CTL_GET_POS = 2;
|
||||||
static constexpr int AUDIO_CTL_BT_STATUS = 6; // Get Bluetooth connection status
|
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;
|
||||||
|
|
||||||
/* Bluetooth.hpp */
|
/* Bluetooth.hpp */
|
||||||
static constexpr uint64_t SYS_BTSCAN = 84;
|
static constexpr uint64_t SYS_BTSCAN = 84;
|
||||||
@@ -230,6 +239,10 @@ namespace Montauk {
|
|||||||
/* Net.hpp */
|
/* Net.hpp */
|
||||||
static constexpr uint64_t SYS_NETSTATUS = 125;
|
static constexpr uint64_t SYS_NETSTATUS = 125;
|
||||||
|
|
||||||
|
/* Audio.hpp */
|
||||||
|
static constexpr uint64_t SYS_AUDIOLIST = 128;
|
||||||
|
static constexpr uint64_t SYS_AUDIOWAIT = 129;
|
||||||
|
|
||||||
static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024;
|
static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024;
|
||||||
|
|
||||||
static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0;
|
static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0;
|
||||||
@@ -311,6 +324,20 @@ namespace Montauk {
|
|||||||
uint64_t serial;
|
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
|
// Window server shared types
|
||||||
struct WinEvent {
|
struct WinEvent {
|
||||||
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "IntelHda.hpp"
|
#include "IntelHda.hpp"
|
||||||
|
#include "Mixer.hpp"
|
||||||
#include <Memory/PageFrameAllocator.hpp>
|
#include <Memory/PageFrameAllocator.hpp>
|
||||||
#include <Memory/HHDM.hpp>
|
#include <Memory/HHDM.hpp>
|
||||||
#include <Memory/Paging.hpp>
|
#include <Memory/Paging.hpp>
|
||||||
@@ -840,12 +841,14 @@ namespace Drivers::Audio::IntelHda {
|
|||||||
|
|
||||||
static void HandleInterrupt(uint8_t /*irq*/) {
|
static void HandleInterrupt(uint8_t /*irq*/) {
|
||||||
uint32_t intsts = Read32(REG_INTSTS);
|
uint32_t intsts = Read32(REG_INTSTS);
|
||||||
|
bool bufferCompleted = false;
|
||||||
|
|
||||||
// Handle stream interrupts (bits 0-29 correspond to stream descriptors)
|
// Handle stream interrupts (bits 0-29 correspond to stream descriptors)
|
||||||
if (g_stream.Active) {
|
if (g_stream.Active) {
|
||||||
uint8_t si = g_stream.StreamIndex;
|
uint8_t si = g_stream.StreamIndex;
|
||||||
if (intsts & (1u << si)) {
|
if (intsts & (1u << si)) {
|
||||||
uint8_t sts = ReadSD8(si, SD_STS);
|
uint8_t sts = ReadSD8(si, SD_STS);
|
||||||
|
if (sts & SD_STS_BCIS) bufferCompleted = true;
|
||||||
WriteSD8(si, SD_STS, sts);
|
WriteSD8(si, SD_STS, sts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -858,6 +861,12 @@ namespace Drivers::Audio::IntelHda {
|
|||||||
uint8_t rirbSts = Read8(REG_RIRBSTS);
|
uint8_t rirbSts = Read8(REG_RIRBSTS);
|
||||||
Write8(REG_RIRBSTS, rirbSts);
|
Write8(REG_RIRBSTS, rirbSts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify the mixer so it can refill the DMA ring with the next mix
|
||||||
|
// window. Done after clearing status bits so re-entry can't latch.
|
||||||
|
if (bufferCompleted) {
|
||||||
|
Mixer::OnHdaBufferComplete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -0,0 +1,517 @@
|
|||||||
|
/*
|
||||||
|
* Mixer.cpp
|
||||||
|
* Software audio mixer: routes multiple per-process audio streams to a single
|
||||||
|
* hardware output (Intel HDA), with per-stream volume, mute, and pause.
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Mixer.hpp"
|
||||||
|
#include "IntelHda.hpp"
|
||||||
|
#include <Memory/PageFrameAllocator.hpp>
|
||||||
|
#include <Memory/HHDM.hpp>
|
||||||
|
#include <Sched/Scheduler.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
#include <Libraries/Memory.hpp>
|
||||||
|
|
||||||
|
namespace Drivers::Audio::Mixer {
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Per-stream state
|
||||||
|
// =========================================================================
|
||||||
|
//
|
||||||
|
// Each virtual stream stores its input as 16-bit stereo at the *input*
|
||||||
|
// sample rate. The pump consumes input frames at the input rate, linearly
|
||||||
|
// interpolates between adjacent frames using a Q32 fractional cursor, and
|
||||||
|
// produces output frames at MIX_RATE.
|
||||||
|
|
||||||
|
static constexpr uint32_t INPUT_RING_FRAMES = 16384; // ~341 ms at 48 kHz
|
||||||
|
static constexpr int INPUT_RING_BYTES = INPUT_RING_FRAMES * 4;
|
||||||
|
static constexpr int INPUT_RING_PAGES = (INPUT_RING_BYTES + 0xFFF) / 0x1000;
|
||||||
|
|
||||||
|
// Cap one pump cycle to keep loop bounded under heavy write bursts.
|
||||||
|
static constexpr uint32_t MAX_PUMP_FRAMES = 4096; // ~85 ms at 48 kHz
|
||||||
|
|
||||||
|
struct VirtualStream {
|
||||||
|
bool active;
|
||||||
|
int ownerPid;
|
||||||
|
char name[64];
|
||||||
|
|
||||||
|
uint32_t inputRate;
|
||||||
|
uint8_t inputChannels; // 1 or 2 (mono is upmixed at conversion time)
|
||||||
|
uint8_t inputBits; // currently must be 16
|
||||||
|
uint8_t volume; // 0-100
|
||||||
|
bool muted;
|
||||||
|
bool paused;
|
||||||
|
|
||||||
|
// Input ring: int16 stereo at inputRate.
|
||||||
|
int16_t* ring;
|
||||||
|
uint32_t writeFrame; // monotonically increasing, modulo'd at access
|
||||||
|
uint32_t readFrame;
|
||||||
|
uint64_t consumedInputBytes; // total input bytes consumed by pump
|
||||||
|
|
||||||
|
// Resample cursor (Q32 fixed point, fractional position within next input frame).
|
||||||
|
uint64_t posQ32;
|
||||||
|
uint64_t stepQ32; // (inputRate << 32) / MIX_RATE
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Module state
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static kcp::Spinlock g_lock;
|
||||||
|
static VirtualStream g_streams[MAX_STREAMS] = {};
|
||||||
|
static bool g_hdaOpened = false;
|
||||||
|
static int g_hdaHandle = -1;
|
||||||
|
static int g_masterVolume = 80;
|
||||||
|
static bool g_masterMute = false;
|
||||||
|
static int g_activeCount = 0;
|
||||||
|
|
||||||
|
// Monotonically increasing serial. Bumped (and waiters woken) on every
|
||||||
|
// mutation of mixer state. Clients use it to detect changes without
|
||||||
|
// re-reading the whole snapshot. The address of the serial doubles as the
|
||||||
|
// wait-object passed to BlockOnObject / WakeObjectWaiters.
|
||||||
|
static volatile uint64_t g_serial = 0;
|
||||||
|
|
||||||
|
// Caller must hold g_lock.
|
||||||
|
static void BumpSerialLocked() {
|
||||||
|
g_serial++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scratch mix buffer (int32 stereo, to avoid clipping during accumulation).
|
||||||
|
// Sized for MAX_PUMP_FRAMES * 2 channels.
|
||||||
|
static int32_t g_mixScratch[MAX_PUMP_FRAMES * 2];
|
||||||
|
static int16_t g_outScratch[MAX_PUMP_FRAMES * 2];
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helpers
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static inline int16_t SatI16(int32_t v) {
|
||||||
|
if (v > 32767) return 32767;
|
||||||
|
if (v < -32768) return -32768;
|
||||||
|
return (int16_t)v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int16_t* AllocRing() {
|
||||||
|
void* p = Memory::g_pfa->ReallocConsecutive(nullptr, INPUT_RING_PAGES);
|
||||||
|
if (!p) return nullptr;
|
||||||
|
memset(p, 0, INPUT_RING_PAGES * 0x1000);
|
||||||
|
return (int16_t*)p;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void FreeRing(int16_t* ring) {
|
||||||
|
if (!ring) return;
|
||||||
|
Memory::g_pfa->ReallocConsecutive(ring, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool EnsureHdaOpen() {
|
||||||
|
if (g_hdaOpened) return true;
|
||||||
|
if (!IntelHda::IsInitialized()) return false;
|
||||||
|
g_hdaHandle = IntelHda::Open(MIX_RATE, MIX_CHANNELS, MIX_BITS);
|
||||||
|
if (g_hdaHandle < 0) return false;
|
||||||
|
IntelHda::Control(g_hdaHandle, IntelHda::AUDIO_CTL_SET_VOLUME, g_masterMute ? 0 : g_masterVolume);
|
||||||
|
g_hdaOpened = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert one chunk of raw input bytes from a stream into int16 stereo at
|
||||||
|
// the stream's native rate, then push into its ring. Caller holds g_lock.
|
||||||
|
// Returns the number of *input bytes* successfully ingested.
|
||||||
|
static uint32_t ConvertAndPush(VirtualStream& s, const uint8_t* data, uint32_t size) {
|
||||||
|
// Only 16-bit input is supported. Caller validates this in Open().
|
||||||
|
const uint32_t inFrameBytes = (uint32_t)s.inputChannels * 2;
|
||||||
|
uint32_t inFrames = size / inFrameBytes;
|
||||||
|
if (inFrames == 0) return 0;
|
||||||
|
|
||||||
|
// Clamp to whatever space the ring has left without growing past the
|
||||||
|
// configured capacity. Excess input is dropped — callers know to write
|
||||||
|
// again later.
|
||||||
|
uint32_t inFlight = s.writeFrame - s.readFrame;
|
||||||
|
uint32_t free = (inFlight >= INPUT_RING_FRAMES) ? 0 : (INPUT_RING_FRAMES - inFlight);
|
||||||
|
if (inFrames > free) inFrames = free;
|
||||||
|
if (inFrames == 0) return 0;
|
||||||
|
|
||||||
|
const int16_t* src = (const int16_t*)data;
|
||||||
|
for (uint32_t i = 0; i < inFrames; i++) {
|
||||||
|
int16_t l, r;
|
||||||
|
if (s.inputChannels == 1) {
|
||||||
|
l = r = src[i];
|
||||||
|
} else {
|
||||||
|
l = src[i * 2 + 0];
|
||||||
|
r = src[i * 2 + 1];
|
||||||
|
}
|
||||||
|
uint32_t idx = (s.writeFrame + i) % INPUT_RING_FRAMES;
|
||||||
|
s.ring[idx * 2 + 0] = l;
|
||||||
|
s.ring[idx * 2 + 1] = r;
|
||||||
|
}
|
||||||
|
s.writeFrame += inFrames;
|
||||||
|
return inFrames * inFrameBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Mixer pump
|
||||||
|
// =========================================================================
|
||||||
|
//
|
||||||
|
// Compute the number of frames we can safely write to the HDA DMA buffer
|
||||||
|
// (free space in the ring, minus a small guard), then for each active
|
||||||
|
// stream resample and mix it into the scratch buffer. Finally hand the
|
||||||
|
// result to IntelHda::Write().
|
||||||
|
|
||||||
|
static void Pump() {
|
||||||
|
if (!g_hdaOpened) return;
|
||||||
|
|
||||||
|
// Ask IntelHda how much room there is. The driver clamps writes to free
|
||||||
|
// space, so we feed it a generous buffer and let it consume what it can.
|
||||||
|
uint32_t frames = MAX_PUMP_FRAMES;
|
||||||
|
|
||||||
|
// Always write to HDA, even when no streams are active, so the
|
||||||
|
// hardware ring stays filled with silence instead of looping the
|
||||||
|
// last mixed audio. Without this, closing the last app would leave
|
||||||
|
// its tail playing on repeat.
|
||||||
|
if (g_activeCount == 0) {
|
||||||
|
memset(g_outScratch, 0, frames * 2 * sizeof(int16_t));
|
||||||
|
IntelHda::Write(g_hdaHandle, (const uint8_t*)g_outScratch, frames * 4);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero the scratch accumulator.
|
||||||
|
memset(g_mixScratch, 0, frames * 2 * sizeof(int32_t));
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_STREAMS; i++) {
|
||||||
|
VirtualStream& s = g_streams[i];
|
||||||
|
if (!s.active || s.paused || s.muted || s.volume == 0) continue;
|
||||||
|
|
||||||
|
// posQ32 is the fractional read cursor; integer part is the index
|
||||||
|
// of the *next* input frame to consume.
|
||||||
|
uint64_t pos = s.posQ32;
|
||||||
|
uint64_t step = s.stepQ32;
|
||||||
|
uint32_t available = s.writeFrame - s.readFrame;
|
||||||
|
if (available == 0) continue;
|
||||||
|
|
||||||
|
// Per-stream gain in Q15 (post-master mix).
|
||||||
|
int32_t gain = (int32_t)s.volume; // 0..100
|
||||||
|
|
||||||
|
for (uint32_t f = 0; f < frames; f++) {
|
||||||
|
uint64_t intPart = pos >> 32;
|
||||||
|
if (intPart + 1 >= available) {
|
||||||
|
break; // would read past end of ring; stop early
|
||||||
|
}
|
||||||
|
uint32_t i0 = (s.readFrame + (uint32_t)intPart) % INPUT_RING_FRAMES;
|
||||||
|
uint32_t i1 = (s.readFrame + (uint32_t)intPart + 1) % INPUT_RING_FRAMES;
|
||||||
|
|
||||||
|
int32_t l0 = s.ring[i0 * 2 + 0];
|
||||||
|
int32_t r0 = s.ring[i0 * 2 + 1];
|
||||||
|
int32_t l1 = s.ring[i1 * 2 + 0];
|
||||||
|
int32_t r1 = s.ring[i1 * 2 + 1];
|
||||||
|
|
||||||
|
// 16-bit fraction is the high half of the Q32 fractional part.
|
||||||
|
int32_t frac = (int32_t)((pos & 0xFFFFFFFFull) >> 16); // 0..65535
|
||||||
|
int32_t inv = 65536 - frac;
|
||||||
|
int32_t l = (l0 * inv + l1 * frac) >> 16;
|
||||||
|
int32_t r = (r0 * inv + r1 * frac) >> 16;
|
||||||
|
|
||||||
|
// Apply per-stream volume.
|
||||||
|
l = (l * gain) / 100;
|
||||||
|
r = (r * gain) / 100;
|
||||||
|
|
||||||
|
g_mixScratch[f * 2 + 0] += l;
|
||||||
|
g_mixScratch[f * 2 + 1] += r;
|
||||||
|
|
||||||
|
pos += step;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance the ring read cursor by the integer part of `pos`, and
|
||||||
|
// keep only the leftover fractional bit for next pump.
|
||||||
|
uint32_t consumedFrames = (uint32_t)(pos >> 32);
|
||||||
|
if (consumedFrames > available - 1) consumedFrames = available - 1;
|
||||||
|
s.readFrame += consumedFrames;
|
||||||
|
s.posQ32 = pos - ((uint64_t)consumedFrames << 32);
|
||||||
|
s.consumedInputBytes += (uint64_t)consumedFrames *
|
||||||
|
(uint64_t)((s.inputChannels == 1) ? 2 : 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saturate, apply master volume + mute, and emit s16 stereo.
|
||||||
|
int32_t masterGain = g_masterMute ? 0 : g_masterVolume; // 0..100
|
||||||
|
for (uint32_t f = 0; f < frames; f++) {
|
||||||
|
int32_t l = (g_mixScratch[f * 2 + 0] * masterGain) / 100;
|
||||||
|
int32_t r = (g_mixScratch[f * 2 + 1] * masterGain) / 100;
|
||||||
|
g_outScratch[f * 2 + 0] = SatI16(l);
|
||||||
|
g_outScratch[f * 2 + 1] = SatI16(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hand off to HDA. IntelHda::Write returns the number of bytes
|
||||||
|
// actually accepted (limited by free space in the DMA ring).
|
||||||
|
IntelHda::Write(g_hdaHandle, (const uint8_t*)g_outScratch, frames * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Public API
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
int Open(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample,
|
||||||
|
int ownerPid, const char* ownerName) {
|
||||||
|
if (channels < 1 || channels > 2) return -1;
|
||||||
|
if (bitsPerSample != 16) return -1;
|
||||||
|
if (sampleRate < 8000 || sampleRate > 192000) return -1;
|
||||||
|
|
||||||
|
g_lock.Acquire();
|
||||||
|
|
||||||
|
if (!EnsureHdaOpen()) {
|
||||||
|
g_lock.Release();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int slot = -1;
|
||||||
|
for (int i = 0; i < MAX_STREAMS; i++) {
|
||||||
|
if (!g_streams[i].active) { slot = i; break; }
|
||||||
|
}
|
||||||
|
if (slot < 0) {
|
||||||
|
g_lock.Release();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualStream& s = g_streams[slot];
|
||||||
|
int16_t* ring = AllocRing();
|
||||||
|
if (!ring) {
|
||||||
|
g_lock.Release();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
s.active = true;
|
||||||
|
s.ownerPid = ownerPid;
|
||||||
|
int n = 0;
|
||||||
|
if (ownerName) {
|
||||||
|
for (; n < 63 && ownerName[n]; n++) s.name[n] = ownerName[n];
|
||||||
|
}
|
||||||
|
s.name[n] = '\0';
|
||||||
|
s.inputRate = sampleRate;
|
||||||
|
s.inputChannels = channels;
|
||||||
|
s.inputBits = bitsPerSample;
|
||||||
|
s.volume = 100;
|
||||||
|
s.muted = false;
|
||||||
|
s.paused = false;
|
||||||
|
s.ring = ring;
|
||||||
|
s.writeFrame = 0;
|
||||||
|
s.readFrame = 0;
|
||||||
|
s.consumedInputBytes = 0;
|
||||||
|
s.posQ32 = 0;
|
||||||
|
// step = inputRate / MIX_RATE in Q32.
|
||||||
|
s.stepQ32 = ((uint64_t)sampleRate << 32) / MIX_RATE;
|
||||||
|
g_activeCount++;
|
||||||
|
BumpSerialLocked();
|
||||||
|
|
||||||
|
g_lock.Release();
|
||||||
|
Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Close(int handle) {
|
||||||
|
if (handle < 0 || handle >= MAX_STREAMS) return;
|
||||||
|
g_lock.Acquire();
|
||||||
|
VirtualStream& s = g_streams[handle];
|
||||||
|
bool changed = false;
|
||||||
|
if (s.active) {
|
||||||
|
FreeRing(s.ring);
|
||||||
|
s.ring = nullptr;
|
||||||
|
s.active = false;
|
||||||
|
s.ownerPid = 0;
|
||||||
|
s.name[0] = '\0';
|
||||||
|
if (g_activeCount > 0) g_activeCount--;
|
||||||
|
BumpSerialLocked();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
g_lock.Release();
|
||||||
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Write(int handle, const uint8_t* data, uint32_t size) {
|
||||||
|
if (handle < 0 || handle >= MAX_STREAMS || !data || size == 0) return -1;
|
||||||
|
g_lock.Acquire();
|
||||||
|
VirtualStream& s = g_streams[handle];
|
||||||
|
if (!s.active) { g_lock.Release(); return -1; }
|
||||||
|
|
||||||
|
uint32_t written = ConvertAndPush(s, data, size);
|
||||||
|
|
||||||
|
// Run a pump cycle so the HDA buffer stays fed.
|
||||||
|
Pump();
|
||||||
|
|
||||||
|
g_lock.Release();
|
||||||
|
return (int)written;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Control(int handle, int cmd, int value) {
|
||||||
|
// Master-scope commands ignore the handle.
|
||||||
|
switch (cmd) {
|
||||||
|
case Montauk::AUDIO_CTL_SET_MASTER_VOLUME:
|
||||||
|
SetMasterVolume(value);
|
||||||
|
return 0;
|
||||||
|
case Montauk::AUDIO_CTL_GET_MASTER_VOLUME:
|
||||||
|
return GetMasterVolume();
|
||||||
|
case Montauk::AUDIO_CTL_SET_MASTER_MUTE:
|
||||||
|
SetMasterMute(value != 0);
|
||||||
|
return 0;
|
||||||
|
case Montauk::AUDIO_CTL_GET_MASTER_MUTE:
|
||||||
|
return GetMasterMute() ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle < 0 || handle >= MAX_STREAMS) return -1;
|
||||||
|
|
||||||
|
g_lock.Acquire();
|
||||||
|
VirtualStream& s = g_streams[handle];
|
||||||
|
if (!s.active) { g_lock.Release(); return -1; }
|
||||||
|
|
||||||
|
int rv = -1;
|
||||||
|
bool changed = false;
|
||||||
|
switch (cmd) {
|
||||||
|
case Montauk::AUDIO_CTL_SET_VOLUME: {
|
||||||
|
int v = value; if (v < 0) v = 0; if (v > 100) v = 100;
|
||||||
|
if (s.volume != (uint8_t)v) { s.volume = (uint8_t)v; changed = true; }
|
||||||
|
rv = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Montauk::AUDIO_CTL_GET_VOLUME:
|
||||||
|
rv = s.volume;
|
||||||
|
break;
|
||||||
|
case Montauk::AUDIO_CTL_GET_POS:
|
||||||
|
rv = (int)(s.consumedInputBytes & 0x7FFFFFFF);
|
||||||
|
break;
|
||||||
|
case Montauk::AUDIO_CTL_PAUSE:
|
||||||
|
if (s.paused != (value != 0)) { s.paused = (value != 0); changed = true; }
|
||||||
|
rv = 0;
|
||||||
|
break;
|
||||||
|
case Montauk::AUDIO_CTL_SET_MUTE:
|
||||||
|
if (s.muted != (value != 0)) { s.muted = (value != 0); changed = true; }
|
||||||
|
rv = 0;
|
||||||
|
break;
|
||||||
|
case Montauk::AUDIO_CTL_GET_MUTE:
|
||||||
|
rv = s.muted ? 1 : 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
rv = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (changed) BumpSerialLocked();
|
||||||
|
g_lock.Release();
|
||||||
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
int List(Montauk::AudioStreamInfo* buf, int maxCount) {
|
||||||
|
if (!buf || maxCount <= 0) return 0;
|
||||||
|
g_lock.Acquire();
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < MAX_STREAMS && count < maxCount; i++) {
|
||||||
|
VirtualStream& s = g_streams[i];
|
||||||
|
if (!s.active) continue;
|
||||||
|
buf[count].handle = i;
|
||||||
|
buf[count].ownerPid = s.ownerPid;
|
||||||
|
int j = 0;
|
||||||
|
for (; j < 63 && s.name[j]; j++) buf[count].name[j] = s.name[j];
|
||||||
|
buf[count].name[j] = '\0';
|
||||||
|
buf[count].sampleRate = s.inputRate;
|
||||||
|
buf[count].channels = s.inputChannels;
|
||||||
|
buf[count].bitsPerSample = s.inputBits;
|
||||||
|
buf[count].volume = s.volume;
|
||||||
|
buf[count].muted = s.muted ? 1 : 0;
|
||||||
|
buf[count].paused = s.paused ? 1 : 0;
|
||||||
|
for (int k = 0; k < 7; k++) buf[count]._pad[k] = 0;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
g_lock.Release();
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CleanupProcess(int pid) {
|
||||||
|
g_lock.Acquire();
|
||||||
|
bool changed = false;
|
||||||
|
for (int i = 0; i < MAX_STREAMS; i++) {
|
||||||
|
VirtualStream& s = g_streams[i];
|
||||||
|
if (s.active && s.ownerPid == pid) {
|
||||||
|
FreeRing(s.ring);
|
||||||
|
s.ring = nullptr;
|
||||||
|
s.active = false;
|
||||||
|
s.ownerPid = 0;
|
||||||
|
s.name[0] = '\0';
|
||||||
|
if (g_activeCount > 0) g_activeCount--;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) BumpSerialLocked();
|
||||||
|
g_lock.Release();
|
||||||
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetMasterVolume(int percent) {
|
||||||
|
if (percent < 0) percent = 0;
|
||||||
|
if (percent > 100) percent = 100;
|
||||||
|
g_lock.Acquire();
|
||||||
|
bool changed = (g_masterVolume != percent);
|
||||||
|
g_masterVolume = percent;
|
||||||
|
if (g_hdaOpened) {
|
||||||
|
IntelHda::Control(g_hdaHandle, IntelHda::AUDIO_CTL_SET_VOLUME,
|
||||||
|
g_masterMute ? 0 : g_masterVolume);
|
||||||
|
}
|
||||||
|
if (changed) BumpSerialLocked();
|
||||||
|
g_lock.Release();
|
||||||
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
int GetMasterVolume() {
|
||||||
|
return g_masterVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetMasterMute(bool muted) {
|
||||||
|
g_lock.Acquire();
|
||||||
|
bool changed = (g_masterMute != muted);
|
||||||
|
g_masterMute = muted;
|
||||||
|
if (g_hdaOpened) {
|
||||||
|
IntelHda::Control(g_hdaHandle, IntelHda::AUDIO_CTL_SET_VOLUME,
|
||||||
|
g_masterMute ? 0 : g_masterVolume);
|
||||||
|
}
|
||||||
|
if (changed) BumpSerialLocked();
|
||||||
|
g_lock.Release();
|
||||||
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetMasterMute() {
|
||||||
|
return g_masterMute;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnHdaBufferComplete() {
|
||||||
|
// Called from the HDA buffer-completion interrupt. Mixer state and
|
||||||
|
// HDA register access are both serialized through g_lock (which
|
||||||
|
// disables interrupts on acquire), so this is safe to call from IRQ.
|
||||||
|
g_lock.Acquire();
|
||||||
|
Pump();
|
||||||
|
g_lock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t GetSerial() {
|
||||||
|
return g_serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockOnObjectIf callback: returns true (i.e. "do block") only if the
|
||||||
|
// current serial still matches the caller's snapshot. Avoids the classic
|
||||||
|
// wakeup-before-block race: state could change between the caller's first
|
||||||
|
// read of g_serial and the scheduler dropping the process to Blocked.
|
||||||
|
struct WaitCtx { uint64_t expected; };
|
||||||
|
static bool WaitShouldBlock(void* ctx) {
|
||||||
|
return g_serial == ((WaitCtx*)ctx)->expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t Wait(uint64_t prevSerial, uint64_t timeoutMs) {
|
||||||
|
// Fast path: state already moved on, no need to enter the scheduler.
|
||||||
|
if (g_serial != prevSerial) return g_serial;
|
||||||
|
if (timeoutMs == 0) return g_serial;
|
||||||
|
|
||||||
|
WaitCtx ctx{prevSerial};
|
||||||
|
Sched::BlockOnObjectIf((void*)&g_serial, timeoutMs,
|
||||||
|
WaitShouldBlock, &ctx);
|
||||||
|
return g_serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Mixer.hpp
|
||||||
|
* Software audio mixer: routes multiple per-process audio streams to a single
|
||||||
|
* hardware output (Intel HDA), with per-stream volume, mute, and pause.
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <Api/Syscall.hpp>
|
||||||
|
|
||||||
|
namespace Drivers::Audio::Mixer {
|
||||||
|
|
||||||
|
// Maximum simultaneous virtual streams. Each open audio handle owned by a
|
||||||
|
// process consumes one slot. Slot index doubles as the user-visible handle.
|
||||||
|
constexpr int MAX_STREAMS = 8;
|
||||||
|
|
||||||
|
// Fixed hardware mix format. Streams opened at other rates / channel
|
||||||
|
// counts / bit depths are converted at write time.
|
||||||
|
constexpr uint32_t MIX_RATE = 48000;
|
||||||
|
constexpr uint8_t MIX_CHANNELS = 2;
|
||||||
|
constexpr uint8_t MIX_BITS = 16;
|
||||||
|
|
||||||
|
// Lazy-init: opens the underlying HDA stream on first virtual Open().
|
||||||
|
int Open(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample,
|
||||||
|
int ownerPid, const char* ownerName);
|
||||||
|
void Close(int handle);
|
||||||
|
int Write(int handle, const uint8_t* data, uint32_t size);
|
||||||
|
int Control(int handle, int cmd, int value);
|
||||||
|
|
||||||
|
// Enumerate all currently-active streams.
|
||||||
|
int List(Montauk::AudioStreamInfo* buf, int maxCount);
|
||||||
|
|
||||||
|
// Close every stream owned by `pid`. Called from the scheduler on process
|
||||||
|
// exit / kill so leaked handles do not occupy mixer slots.
|
||||||
|
void CleanupProcess(int pid);
|
||||||
|
|
||||||
|
// Master-volume control (applies post-mix, before HW DAC).
|
||||||
|
void SetMasterVolume(int percent);
|
||||||
|
int GetMasterVolume();
|
||||||
|
void SetMasterMute(bool muted);
|
||||||
|
bool GetMasterMute();
|
||||||
|
|
||||||
|
// Called from the HDA BCIS interrupt: a buffer segment finished playing,
|
||||||
|
// refill the HW ring so audio doesn't loop stale data.
|
||||||
|
void OnHdaBufferComplete();
|
||||||
|
|
||||||
|
// Event-driven sync for clients (panel popup, Audio app, level meters).
|
||||||
|
//
|
||||||
|
// A monotonically increasing serial is bumped on every state change
|
||||||
|
// (open/close, per-stream and master volume/mute, pause). Wait() blocks
|
||||||
|
// until the serial differs from `prevSerial` or `timeoutMs` elapses, and
|
||||||
|
// returns the current serial. timeoutMs == 0 returns immediately
|
||||||
|
// (non-blocking poll). Callers should refresh their local snapshot
|
||||||
|
// whenever the serial changes.
|
||||||
|
uint64_t GetSerial();
|
||||||
|
uint64_t Wait(uint64_t prevSerial, uint64_t timeoutMs);
|
||||||
|
};
|
||||||
@@ -99,14 +99,16 @@ namespace Hal {
|
|||||||
<< ") - process terminated";
|
<< ") - process terminated";
|
||||||
|
|
||||||
// Capture crash report
|
// Capture crash report
|
||||||
CrashReport::Report rep;
|
CrashReport::Report rep{};
|
||||||
rep.valid = true;
|
rep.valid = true;
|
||||||
rep.pid = proc->pid;
|
rep.pid = proc->pid;
|
||||||
for (int j = 0; j < 64 && proc->name[j]; j++) rep.processName[j] = proc->name[j];
|
int pn;
|
||||||
rep.processName[63] = '\0';
|
for (pn = 0; pn < 63 && proc->name[pn]; pn++) rep.processName[pn] = proc->name[pn];
|
||||||
|
rep.processName[pn] = '\0';
|
||||||
rep.exceptionVector = i;
|
rep.exceptionVector = i;
|
||||||
for (int j = 0; j < 32 && ExceptionStrings[i][j]; j++) rep.exceptionName[j] = ExceptionStrings[i][j];
|
int en;
|
||||||
rep.exceptionName[31] = '\0';
|
for (en = 0; en < 31 && ExceptionStrings[i][en]; en++) rep.exceptionName[en] = ExceptionStrings[i][en];
|
||||||
|
rep.exceptionName[en] = '\0';
|
||||||
rep.instructionPointer = regs->IP;
|
rep.instructionPointer = regs->IP;
|
||||||
rep.codeSegment = regs->CS;
|
rep.codeSegment = regs->CS;
|
||||||
rep.flags = regs->Flags;
|
rep.flags = regs->Flags;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include <Api/WinServer.hpp>
|
#include <Api/WinServer.hpp>
|
||||||
#include <Api/Heap.hpp>
|
#include <Api/Heap.hpp>
|
||||||
#include <Api/LibSyscall.hpp>
|
#include <Api/LibSyscall.hpp>
|
||||||
|
#include <Drivers/Audio/Mixer.hpp>
|
||||||
#include <Ipc/Ipc.hpp>
|
#include <Ipc/Ipc.hpp>
|
||||||
|
|
||||||
// Assembly: context switch with CR3 and FPU state parameters
|
// Assembly: context switch with CR3 and FPU state parameters
|
||||||
@@ -715,6 +716,10 @@ namespace Sched {
|
|||||||
// Clean up any windows owned by this process
|
// Clean up any windows owned by this process
|
||||||
WinServer::CleanupProcess(exitingPid);
|
WinServer::CleanupProcess(exitingPid);
|
||||||
|
|
||||||
|
// Release any mixer virtual streams owned by this process so the slot
|
||||||
|
// and its ring buffer don't leak when an app forgets to audio_close.
|
||||||
|
Drivers::Audio::Mixer::CleanupProcess(exitingPid);
|
||||||
|
|
||||||
// Release process-scoped IPC handles/mappings before tearing down the address space.
|
// Release process-scoped IPC handles/mappings before tearing down the address space.
|
||||||
Ipc::CleanupProcessSlot(slot, exitingPid, proc.pml4Phys);
|
Ipc::CleanupProcessSlot(slot, exitingPid, proc.pml4Phys);
|
||||||
Montauk::CleanupHeapForSlot(slot, proc.pml4Phys);
|
Montauk::CleanupHeapForSlot(slot, proc.pml4Phys);
|
||||||
@@ -873,6 +878,7 @@ namespace Sched {
|
|||||||
|
|
||||||
// Safe to clean up resources now -- process is not running anywhere.
|
// Safe to clean up resources now -- process is not running anywhere.
|
||||||
WinServer::CleanupProcess(killedPid);
|
WinServer::CleanupProcess(killedPid);
|
||||||
|
Drivers::Audio::Mixer::CleanupProcess(killedPid);
|
||||||
Ipc::CleanupProcessSlot(slot, killedPid, proc.pml4Phys);
|
Ipc::CleanupProcessSlot(slot, killedPid, proc.pml4Phys);
|
||||||
Montauk::CleanupHeapForSlot(slot, proc.pml4Phys);
|
Montauk::CleanupHeapForSlot(slot, proc.pml4Phys);
|
||||||
Montauk::CleanupLibTable(slot);
|
Montauk::CleanupLibTable(slot);
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ BINDIR := bin
|
|||||||
PROGRAMS := $(notdir $(wildcard src/*))
|
PROGRAMS := $(notdir $(wildcard src/*))
|
||||||
|
|
||||||
# Programs with custom Makefiles (built separately).
|
# 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))
|
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||||
|
|
||||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
# 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.)
|
# Common shared assets (wallpapers, etc.)
|
||||||
COMMONKEEP := $(BINDIR)/common/.keep
|
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).
|
# 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)
|
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
|
installer: libc
|
||||||
$(MAKE) -C src/installer
|
$(MAKE) -C src/installer
|
||||||
|
|
||||||
# Build volume control standalone GUI tool (depends on libc).
|
# Build audio control standalone GUI tool (depends on libc).
|
||||||
volume: libc
|
audio: libc
|
||||||
$(MAKE) -C src/volume
|
$(MAKE) -C src/audio
|
||||||
|
|
||||||
# Build music player standalone GUI tool (depends on libc).
|
# Build music player standalone GUI tool (depends on libc).
|
||||||
music: libc
|
music: libc
|
||||||
@@ -309,7 +309,7 @@ test_dialogs: libc libloader dialogs
|
|||||||
$(MAKE) -C src/test_dialogs
|
$(MAKE) -C src/test_dialogs
|
||||||
|
|
||||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
# 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
|
../scripts/install_apps.sh
|
||||||
|
|
||||||
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
# 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/disks clean
|
||||||
$(MAKE) -C src/devexplorer clean
|
$(MAKE) -C src/devexplorer clean
|
||||||
$(MAKE) -C src/installer clean
|
$(MAKE) -C src/installer clean
|
||||||
$(MAKE) -C src/volume clean
|
$(MAKE) -C src/audio clean
|
||||||
$(MAKE) -C src/music clean
|
$(MAKE) -C src/music clean
|
||||||
$(MAKE) -C src/video clean
|
$(MAKE) -C src/video clean
|
||||||
$(MAKE) -C src/bluetooth clean
|
$(MAKE) -C src/bluetooth clean
|
||||||
|
|||||||
@@ -162,6 +162,11 @@ namespace Montauk {
|
|||||||
static constexpr uint64_t SYS_NETSTATUS = 125;
|
static constexpr uint64_t SYS_NETSTATUS = 125;
|
||||||
static constexpr uint64_t SYS_DRIVEKIND = 127;
|
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 CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024;
|
||||||
|
|
||||||
static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0;
|
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_EXITED = 1u << 3;
|
||||||
static constexpr uint32_t IPC_SIGNAL_READY = 1u << 4;
|
static constexpr uint32_t IPC_SIGNAL_READY = 1u << 4;
|
||||||
|
|
||||||
// Audio control commands (for SYS_AUDIOCTL)
|
// Audio control commands (for SYS_AUDIOCTL).
|
||||||
static constexpr int AUDIO_CTL_SET_VOLUME = 0;
|
//
|
||||||
static constexpr int AUDIO_CTL_GET_VOLUME = 1;
|
// Commands 0..3 act on the stream named by the handle argument.
|
||||||
static constexpr int AUDIO_CTL_GET_POS = 2;
|
// Commands 7..12 act on the global master and ignore the handle.
|
||||||
static constexpr int AUDIO_CTL_PAUSE = 3;
|
static constexpr int AUDIO_CTL_SET_VOLUME = 0;
|
||||||
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
|
static constexpr int AUDIO_CTL_GET_VOLUME = 1;
|
||||||
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
|
static constexpr int AUDIO_CTL_GET_POS = 2;
|
||||||
static constexpr int AUDIO_CTL_BT_STATUS = 6; // Get Bluetooth connection status
|
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_TCP = 1;
|
||||||
static constexpr int SOCK_UDP = 2;
|
static constexpr int SOCK_UDP = 2;
|
||||||
@@ -252,6 +266,20 @@ namespace Montauk {
|
|||||||
uint64_t serial;
|
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
|
// Window server shared types
|
||||||
struct WinEvent {
|
struct WinEvent {
|
||||||
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ struct DesktopState {
|
|||||||
int vol_pre_mute; // volume before mute
|
int vol_pre_mute; // volume before mute
|
||||||
bool vol_dragging; // slider drag in progress
|
bool vol_dragging; // slider drag in progress
|
||||||
uint64_t vol_last_poll;
|
uint64_t vol_last_poll;
|
||||||
|
uint64_t vol_serial; // last seen mixer state serial
|
||||||
|
|
||||||
// Temperature monitoring
|
// Temperature monitoring
|
||||||
static constexpr int MAX_THERMAL_ZONES = 8;
|
static constexpr int MAX_THERMAL_ZONES = 8;
|
||||||
|
|||||||
@@ -477,6 +477,33 @@ namespace montauk {
|
|||||||
inline int audio_bt_status(int handle) {
|
inline int audio_bt_status(int handle) {
|
||||||
return audio_ctl(handle, Montauk::AUDIO_CTL_BT_STATUS, 0);
|
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
|
// Bluetooth
|
||||||
inline int bt_scan(Montauk::BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
|
inline int bt_scan(Montauk::BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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
|
# Copyright (c) 2026 Daniel Hammer
|
||||||
|
|
||||||
MAKEFLAGS += -rR
|
MAKEFLAGS += -rR
|
||||||
@@ -68,14 +68,14 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
|||||||
|
|
||||||
# ---- Target ----
|
# ---- Target ----
|
||||||
|
|
||||||
TARGET := $(BINDIR)/apps/volume/volume.elf
|
TARGET := $(BINDIR)/apps/audio/audio.elf
|
||||||
|
|
||||||
.PHONY: all clean
|
.PHONY: all clean
|
||||||
|
|
||||||
all: $(TARGET)
|
all: $(TARGET)
|
||||||
|
|
||||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||||
mkdir -p $(BINDIR)/apps/volume
|
mkdir -p $(BINDIR)/apps/audio
|
||||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||||
|
|
||||||
$(OBJDIR)/%.o: %.cpp Makefile
|
$(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]
|
[app]
|
||||||
name = "Volume"
|
name = "Audio"
|
||||||
binary = "volume.elf"
|
binary = "audio.elf"
|
||||||
icon = "pavucontrol.svg"
|
icon = "pavucontrol.svg"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
@@ -401,7 +401,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
if (v > 100) v = 100;
|
if (v > 100) v = 100;
|
||||||
ds->vol_muted = false;
|
ds->vol_muted = false;
|
||||||
ds->vol_level = v;
|
ds->vol_level = v;
|
||||||
montauk::audio_set_volume(0, v);
|
montauk::audio_set_master_volume(v);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (left_released) {
|
if (left_released) {
|
||||||
@@ -422,7 +422,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
if (v > 100) v = 100;
|
if (v > 100) v = 100;
|
||||||
ds->vol_muted = false;
|
ds->vol_muted = false;
|
||||||
ds->vol_level = v;
|
ds->vol_level = v;
|
||||||
montauk::audio_set_volume(0, v);
|
montauk::audio_set_master_volume(v);
|
||||||
ds->vol_dragging = true;
|
ds->vol_dragging = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -441,7 +441,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
int v = ds->vol_level - 5;
|
int v = ds->vol_level - 5;
|
||||||
if (v < 0) v = 0;
|
if (v < 0) v = 0;
|
||||||
ds->vol_level = v;
|
ds->vol_level = v;
|
||||||
montauk::audio_set_volume(0, v);
|
montauk::audio_set_master_volume(v);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bx += minus_w + gap;
|
bx += minus_w + gap;
|
||||||
@@ -451,7 +451,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
int v = ds->vol_level + 5;
|
int v = ds->vol_level + 5;
|
||||||
if (v > 100) v = 100;
|
if (v > 100) v = 100;
|
||||||
ds->vol_level = v;
|
ds->vol_level = v;
|
||||||
montauk::audio_set_volume(0, v);
|
montauk::audio_set_master_volume(v);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bx += plus_w + gap;
|
bx += plus_w + gap;
|
||||||
@@ -459,12 +459,13 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
if (mx >= bx && mx < bx + mute_w) {
|
if (mx >= bx && mx < bx + mute_w) {
|
||||||
if (ds->vol_muted) {
|
if (ds->vol_muted) {
|
||||||
ds->vol_muted = false;
|
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;
|
ds->vol_level = ds->vol_pre_mute;
|
||||||
} else {
|
} else {
|
||||||
ds->vol_pre_mute = ds->vol_level;
|
ds->vol_pre_mute = ds->vol_level;
|
||||||
ds->vol_muted = true;
|
ds->vol_muted = true;
|
||||||
montauk::audio_set_volume(0, 0);
|
montauk::audio_set_master_mute(true);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,12 +311,13 @@ void gui::desktop_init(DesktopState* ds) {
|
|||||||
|
|
||||||
ds->vol_popup_open = false;
|
ds->vol_popup_open = false;
|
||||||
ds->vol_icon_rect = {0, 0, 0, 0};
|
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_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_pre_mute = ds->vol_level;
|
||||||
ds->vol_dragging = false;
|
ds->vol_dragging = false;
|
||||||
ds->vol_last_poll = montauk::get_milliseconds();
|
ds->vol_last_poll = montauk::get_milliseconds();
|
||||||
|
ds->vol_serial = montauk::audio_wait(0, 0);
|
||||||
|
|
||||||
ds->closing_ext_count = 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;
|
ds->net_cfg_last_poll = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (now - ds->vol_last_poll > 5000) {
|
// Event-driven sync: the kernel mixer bumps a serial on every state
|
||||||
int v = montauk::audio_get_volume(0);
|
// change. Reading it is one cheap syscall; refresh only when the serial
|
||||||
if (v >= 0 && !ds->vol_muted && v != ds->vol_level) {
|
// moved since we last looked. The audio app (and any other client) will
|
||||||
ds->vol_level = v;
|
// bump this serial when they change volume/mute, so the popup and the
|
||||||
changed = true;
|
// 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;
|
ds->vol_last_poll = now;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ CXXFLAGS := \
|
|||||||
-msse2 \
|
-msse2 \
|
||||||
-mno-red-zone \
|
-mno-red-zone \
|
||||||
-mcmodel=small \
|
-mcmodel=small \
|
||||||
|
-MMD -MP \
|
||||||
-I $(PROG_INC) \
|
-I $(PROG_INC) \
|
||||||
-isystem $(PROG_INC)/libc \
|
-isystem $(PROG_INC)/libc \
|
||||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||||
@@ -82,5 +83,7 @@ $(OBJDIR)/%.o: %.cpp Makefile
|
|||||||
mkdir -p $(OBJDIR)
|
mkdir -p $(OBJDIR)
|
||||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
-include $(OBJS:.o=.d)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(OBJDIR) $(TARGET)
|
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_W = 2048;
|
||||||
static constexpr int MAX_CANVAS_H = 2048;
|
static constexpr int MAX_CANVAS_H = 2048;
|
||||||
|
|
||||||
static constexpr int FONT_SIZE = 16;
|
static constexpr int FONT_SIZE = 18;
|
||||||
|
|
||||||
// UI colors
|
// UI colors
|
||||||
static constexpr Color BG_COLOR = Color::from_rgb(0xE0, 0xE0, 0xE0);
|
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_CAP = 262144; // 256KB
|
||||||
static constexpr int MAX_LINES = 16384;
|
static constexpr int MAX_LINES = 16384;
|
||||||
static constexpr int TAB_WIDTH = 4;
|
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;
|
static constexpr int SYN_LINE_BUF_CAP = 1024;
|
||||||
|
|
||||||
// Colors
|
// 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);
|
|
||||||
}
|
|
||||||
@@ -26,7 +26,7 @@ APPS=(
|
|||||||
"disks|apps/scalable/gparted.svg"
|
"disks|apps/scalable/gparted.svg"
|
||||||
"devexplorer|apps/scalable/hardware.svg"
|
"devexplorer|apps/scalable/hardware.svg"
|
||||||
"installer|mimetypes/scalable/text-x-install.svg"
|
"installer|mimetypes/scalable/text-x-install.svg"
|
||||||
"volume|apps/scalable/pavucontrol.svg"
|
"audio|apps/scalable/pavucontrol.svg"
|
||||||
"music|apps/scalable/audio-player.svg"
|
"music|apps/scalable/audio-player.svg"
|
||||||
"video|apps/scalable/video-player.svg"
|
"video|apps/scalable/video-player.svg"
|
||||||
"bluetooth|apps/scalable/bluetooth.svg"
|
"bluetooth|apps/scalable/bluetooth.svg"
|
||||||
|
|||||||
Reference in New Issue
Block a user