feat: audio mixer, Audio app, fix Installer crash
This commit is contained in:
+26
-10
@@ -1,33 +1,42 @@
|
||||
/*
|
||||
* Audio.hpp
|
||||
* 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
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Drivers/Audio/IntelHda.hpp>
|
||||
#include <Drivers/Audio/Mixer.hpp>
|
||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
|
||||
#include "Syscall.hpp"
|
||||
|
||||
namespace Montauk {
|
||||
|
||||
// Audio handle convention:
|
||||
// 0x00 - 0x0F : Intel HDA handles
|
||||
// 0x100 : Bluetooth A2DP audio output
|
||||
// 0x00 - 0x07 : Mixer virtual streams (one per opened audio handle)
|
||||
// 0x100 : Bluetooth A2DP audio output (bypasses mixer for now)
|
||||
|
||||
static constexpr int AUDIO_HANDLE_BT = 0x100;
|
||||
|
||||
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()) {
|
||||
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()) {
|
||||
auto state = Drivers::USB::Bluetooth::A2dp::GetState();
|
||||
if (state == Drivers::USB::Bluetooth::A2dp::State::Open ||
|
||||
@@ -47,7 +56,7 @@ namespace Montauk {
|
||||
Drivers::USB::Bluetooth::A2dp::StopStream();
|
||||
return 0;
|
||||
}
|
||||
Drivers::Audio::IntelHda::Close(handle);
|
||||
Drivers::Audio::Mixer::Close(handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -55,7 +64,7 @@ namespace Montauk {
|
||||
if (handle == AUDIO_HANDLE_BT) {
|
||||
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) {
|
||||
@@ -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_BT_STATUS) {
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
||||
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];
|
||||
|
||||
out->pid = rep->pid;
|
||||
for (int i = 0; i < 64 && rep->processName[i]; i++) out->processName[i] = rep->processName[i];
|
||||
out->processName[63] = '\0';
|
||||
int pn;
|
||||
for (pn = 0; pn < 63 && rep->processName[pn]; pn++) out->processName[pn] = rep->processName[pn];
|
||||
out->processName[pn] = '\0';
|
||||
out->exceptionVector = rep->exceptionVector;
|
||||
for (int i = 0; i < 32 && rep->exceptionName[i]; i++) out->exceptionName[i] = rep->exceptionName[i];
|
||||
out->exceptionName[31] = '\0';
|
||||
int en;
|
||||
for (en = 0; en < 31 && rep->exceptionName[en]; en++) out->exceptionName[en] = rep->exceptionName[en];
|
||||
out->exceptionName[en] = '\0';
|
||||
out->faultingAddress = rep->faultingAddress;
|
||||
out->instructionPointer = rep->instructionPointer;
|
||||
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);
|
||||
case SYS_AUDIOCTL:
|
||||
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:
|
||||
if ((int64_t)frame->arg2 < 0) 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_AUDIOCTL = 83;
|
||||
|
||||
// Audio control commands (for SYS_AUDIOCTL)
|
||||
static constexpr int AUDIO_CTL_SET_VOLUME = 0;
|
||||
static constexpr int AUDIO_CTL_GET_VOLUME = 1;
|
||||
static constexpr int AUDIO_CTL_GET_POS = 2;
|
||||
static constexpr int AUDIO_CTL_PAUSE = 3;
|
||||
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
|
||||
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
|
||||
static constexpr int AUDIO_CTL_BT_STATUS = 6; // Get Bluetooth connection status
|
||||
// Audio control commands (for SYS_AUDIOCTL).
|
||||
//
|
||||
// Commands 0..3 act on the stream named by the handle argument.
|
||||
// Commands 7..10 act on the global master and ignore the handle.
|
||||
static constexpr int AUDIO_CTL_SET_VOLUME = 0;
|
||||
static constexpr int AUDIO_CTL_GET_VOLUME = 1;
|
||||
static constexpr int AUDIO_CTL_GET_POS = 2;
|
||||
static constexpr int AUDIO_CTL_PAUSE = 3;
|
||||
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
|
||||
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
|
||||
static constexpr int AUDIO_CTL_BT_STATUS = 6; // Get Bluetooth status
|
||||
static constexpr int AUDIO_CTL_SET_MASTER_VOLUME = 7; // 0-100
|
||||
static constexpr int AUDIO_CTL_GET_MASTER_VOLUME = 8;
|
||||
static constexpr int AUDIO_CTL_SET_MUTE = 9; // 0/1, per-stream
|
||||
static constexpr int AUDIO_CTL_GET_MUTE = 10;
|
||||
static constexpr int AUDIO_CTL_SET_MASTER_MUTE = 11;
|
||||
static constexpr int AUDIO_CTL_GET_MASTER_MUTE = 12;
|
||||
|
||||
/* Bluetooth.hpp */
|
||||
static constexpr uint64_t SYS_BTSCAN = 84;
|
||||
@@ -230,6 +239,10 @@ namespace Montauk {
|
||||
/* Net.hpp */
|
||||
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 IPC_SIGNAL_READABLE = 1u << 0;
|
||||
@@ -311,6 +324,20 @@ namespace Montauk {
|
||||
uint64_t serial;
|
||||
};
|
||||
|
||||
// One entry per active mixer stream; returned by SYS_AUDIOLIST.
|
||||
struct AudioStreamInfo {
|
||||
int32_t handle;
|
||||
int32_t ownerPid;
|
||||
char name[64];
|
||||
uint32_t sampleRate;
|
||||
uint8_t channels;
|
||||
uint8_t bitsPerSample;
|
||||
uint8_t volume; // 0-100
|
||||
uint8_t muted; // 0/1
|
||||
uint8_t paused; // 0/1
|
||||
uint8_t _pad[7];
|
||||
};
|
||||
|
||||
// Window server shared types
|
||||
struct WinEvent {
|
||||
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
#include "IntelHda.hpp"
|
||||
#include "Mixer.hpp"
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
@@ -840,12 +841,14 @@ namespace Drivers::Audio::IntelHda {
|
||||
|
||||
static void HandleInterrupt(uint8_t /*irq*/) {
|
||||
uint32_t intsts = Read32(REG_INTSTS);
|
||||
bool bufferCompleted = false;
|
||||
|
||||
// Handle stream interrupts (bits 0-29 correspond to stream descriptors)
|
||||
if (g_stream.Active) {
|
||||
uint8_t si = g_stream.StreamIndex;
|
||||
if (intsts & (1u << si)) {
|
||||
uint8_t sts = ReadSD8(si, SD_STS);
|
||||
if (sts & SD_STS_BCIS) bufferCompleted = true;
|
||||
WriteSD8(si, SD_STS, sts);
|
||||
}
|
||||
}
|
||||
@@ -858,6 +861,12 @@ namespace Drivers::Audio::IntelHda {
|
||||
uint8_t rirbSts = Read8(REG_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";
|
||||
|
||||
// Capture crash report
|
||||
CrashReport::Report rep;
|
||||
CrashReport::Report rep{};
|
||||
rep.valid = true;
|
||||
rep.pid = proc->pid;
|
||||
for (int j = 0; j < 64 && proc->name[j]; j++) rep.processName[j] = proc->name[j];
|
||||
rep.processName[63] = '\0';
|
||||
int pn;
|
||||
for (pn = 0; pn < 63 && proc->name[pn]; pn++) rep.processName[pn] = proc->name[pn];
|
||||
rep.processName[pn] = '\0';
|
||||
rep.exceptionVector = i;
|
||||
for (int j = 0; j < 32 && ExceptionStrings[i][j]; j++) rep.exceptionName[j] = ExceptionStrings[i][j];
|
||||
rep.exceptionName[31] = '\0';
|
||||
int en;
|
||||
for (en = 0; en < 31 && ExceptionStrings[i][en]; en++) rep.exceptionName[en] = ExceptionStrings[i][en];
|
||||
rep.exceptionName[en] = '\0';
|
||||
rep.instructionPointer = regs->IP;
|
||||
rep.codeSegment = regs->CS;
|
||||
rep.flags = regs->Flags;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <Api/WinServer.hpp>
|
||||
#include <Api/Heap.hpp>
|
||||
#include <Api/LibSyscall.hpp>
|
||||
#include <Drivers/Audio/Mixer.hpp>
|
||||
#include <Ipc/Ipc.hpp>
|
||||
|
||||
// Assembly: context switch with CR3 and FPU state parameters
|
||||
@@ -715,6 +716,10 @@ namespace Sched {
|
||||
// Clean up any windows owned by this process
|
||||
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.
|
||||
Ipc::CleanupProcessSlot(slot, exitingPid, proc.pml4Phys);
|
||||
Montauk::CleanupHeapForSlot(slot, proc.pml4Phys);
|
||||
@@ -873,6 +878,7 @@ namespace Sched {
|
||||
|
||||
// Safe to clean up resources now -- process is not running anywhere.
|
||||
WinServer::CleanupProcess(killedPid);
|
||||
Drivers::Audio::Mixer::CleanupProcess(killedPid);
|
||||
Ipc::CleanupProcessSlot(slot, killedPid, proc.pml4Phys);
|
||||
Montauk::CleanupHeapForSlot(slot, proc.pml4Phys);
|
||||
Montauk::CleanupLibTable(slot);
|
||||
|
||||
Reference in New Issue
Block a user