Files
MontaukOS/kernel/src/Api/Audio.hpp
T
danielandClaude Fable 5 928fe0bbed fix: make BT A2DP output exclusive to one process
The A2DP output is a single unmixed PCM stream. A second process
opening audio while a stream was active would reconfigure the SBC
encoder and media clock under the owner and interleave both apps' PCM
into one ring, garbling playback (e.g. launching DOOM destabilized
Music). Add ClaimOutput/ReleaseOutput pid ownership: the first opener
gets the BT sink, later openers fall back to the HDA mixer, and only
the owner can tear the stream down. The scheduler releases ownership
on process exit so a killed app cannot leak the claim.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-10 12:41:17 +02:00

133 lines
5.6 KiB
C++

/*
* Audio.hpp
* Audio syscall implementations
* 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 <Terminal/Terminal.hpp>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
#include <Drivers/USB/Bluetooth/A2dp.hpp>
#include <Sched/Scheduler.hpp>
#include "Syscall.hpp"
namespace montauk::abi {
// Audio handle convention:
// 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) {
auto* proc = Sched::GetCurrentProcessPtr();
int pid = proc ? proc->pid : -1;
const char* name = proc ? proc->name : "?";
// Auto-switch: when a Bluetooth A2DP sink is connected and its stream is
// set up (StartSource left it Configured/Open), route audio to the
// headphones -- like a phone does when you plug in BT. Falls back to
// the built-in speakers (HDA) when no BT sink is ready.
if (Drivers::USB::Bluetooth::IsInitialized()) {
auto state = Drivers::USB::Bluetooth::A2dp::GetState();
if (state == Drivers::USB::Bluetooth::A2dp::State::Open ||
state == Drivers::USB::Bluetooth::A2dp::State::Streaming ||
state == Drivers::USB::Bluetooth::A2dp::State::Configured) {
// The BT output is a single unmixed stream, so only the first
// opener gets it. Configuring it again while another stream
// owns it would reset the SBC encoder and media clock under
// that stream and interleave both apps' PCM into one ring
// (garbled playback, dropouts persisting until the owner
// reopens). Later openers fall through to the HDA mixer.
if (Drivers::USB::Bluetooth::A2dp::ClaimOutput(pid)) {
Drivers::USB::Bluetooth::A2dp::ConfigureStream(sampleRate, channels, bitsPerSample);
if (Drivers::USB::Bluetooth::A2dp::StartStream()) {
return AUDIO_HANDLE_BT;
}
// Stream would not start (sink unresponsive / state desync):
// returning the BT handle anyway would make every write fail
// with the app stuck reporting "playing" at 0:00. Fall through
// to the speakers instead.
Drivers::USB::Bluetooth::A2dp::ReleaseOutput(pid);
Kt::KernelLogStream(Kt::WARNING, "Audio")
<< "BT A2DP stream failed to start; falling back to HDA";
}
}
}
// HDA-backed mixer is the default 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::Mixer::Open(sampleRate, channels,
bitsPerSample, pid, name);
}
return -1;
}
static int64_t Sys_AudioClose(int handle) {
if (handle == AUDIO_HANDLE_BT) {
// Stops the stream (dropping the queued tail) only when the caller
// owns the BT output, so a stale handle held by another process
// cannot tear down the owner's stream.
auto* proc = Sched::GetCurrentProcessPtr();
Drivers::USB::Bluetooth::A2dp::ReleaseOutput(proc ? proc->pid : -1);
return 0;
}
Drivers::Audio::Mixer::Close(handle);
return 0;
}
static int64_t Sys_AudioWrite(int handle, const uint8_t* data, uint32_t size) {
if (handle == AUDIO_HANDLE_BT) {
return (int64_t)Drivers::USB::Bluetooth::A2dp::WriteAudio(data, size);
}
return (int64_t)Drivers::Audio::Mixer::Write(handle, data, size);
}
static int64_t Sys_AudioCtl(int handle, int cmd, int value) {
if (handle == AUDIO_HANDLE_BT) {
switch (cmd) {
case AUDIO_CTL_SET_VOLUME:
Drivers::USB::Bluetooth::A2dp::SetVolume(value);
return 0;
case AUDIO_CTL_GET_VOLUME:
return Drivers::USB::Bluetooth::A2dp::GetVolume();
case AUDIO_CTL_PAUSE:
// Pause keeps the queued PCM so resume continues gaplessly.
if (value) Drivers::USB::Bluetooth::A2dp::StopStream(false);
else Drivers::USB::Bluetooth::A2dp::StartStream();
return 0;
case AUDIO_CTL_GET_OUTPUT:
return 1; // Bluetooth
default:
return -1;
}
}
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::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);
}
};