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

This commit is contained in:
2026-07-29 20:03:35 +01:00
parent d99dab45e5
commit 86de3400a9
25 changed files with 688 additions and 209 deletions
+13 -72
View File
@@ -19,52 +19,19 @@
namespace montauk::abi { 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) { static int64_t Sys_AudioOpen(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
int pid = proc ? proc->pid : -1; int pid = proc ? proc->pid : -1;
const char* name = proc ? proc->name : "?"; const char* name = proc ? proc->name : "?";
// Auto-switch: when a Bluetooth A2DP sink is connected and its stream is // Every application gets a mixer stream regardless of the selected
// set up (StartSource left it Configured/Open), route audio to the // device. The mixer performs one shared resample/mix pass and routes it
// headphones -- like a phone does when you plug in BT. Falls back to // to HDA or A2DP, allowing concurrent Bluetooth playback and live
// the built-in speakers (HDA) when no BT sink is ready. // switching without invalidating application handles.
if (Drivers::USB::Bluetooth::IsInitialized()) { if (Drivers::Audio::Mixer::IsOutputAvailable(
auto state = Drivers::USB::Bluetooth::A2dp::GetState(); Drivers::Audio::Mixer::Output::Hda) ||
if (state == Drivers::USB::Bluetooth::A2dp::State::Open || Drivers::Audio::Mixer::IsOutputAvailable(
state == Drivers::USB::Bluetooth::A2dp::State::Streaming || Drivers::Audio::Mixer::Output::Bluetooth)) {
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, return (int64_t)Drivers::Audio::Mixer::Open(sampleRate, channels,
bitsPerSample, pid, name); bitsPerSample, pid, name);
} }
@@ -73,46 +40,20 @@ namespace montauk::abi {
} }
static int64_t Sys_AudioClose(int handle) { 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); Drivers::Audio::Mixer::Close(handle);
return 0; return 0;
} }
static int64_t Sys_AudioWrite(int handle, const uint8_t* data, uint32_t size) { 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); 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) {
if (handle == AUDIO_HANDLE_BT) { if (cmd == AUDIO_CTL_GET_OUTPUT)
switch (cmd) { return (int)Drivers::Audio::Mixer::GetOutput();
case AUDIO_CTL_SET_VOLUME: if (cmd == AUDIO_CTL_SET_OUTPUT)
Drivers::USB::Bluetooth::A2dp::SetVolume(value); return Drivers::Audio::Mixer::SetOutput(
return 0; (Drivers::Audio::Mixer::Output)value);
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 (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();
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 17 #define MONTAUK_BUILD_NUMBER 20
+2
View File
@@ -174,6 +174,8 @@ namespace montauk::abi {
static constexpr int AUDIO_CTL_GET_MUTE = 10; static constexpr int AUDIO_CTL_GET_MUTE = 10;
static constexpr int AUDIO_CTL_SET_MASTER_MUTE = 11; static constexpr int AUDIO_CTL_SET_MASTER_MUTE = 11;
static constexpr int AUDIO_CTL_GET_MASTER_MUTE = 12; static constexpr int AUDIO_CTL_GET_MASTER_MUTE = 12;
static constexpr int AUDIO_OUTPUT_HDA = 0;
static constexpr int AUDIO_OUTPUT_BLUETOOTH = 1;
/* Bluetooth.hpp */ /* Bluetooth.hpp */
static constexpr uint64_t SYS_BTSCAN = 84; static constexpr uint64_t SYS_BTSCAN = 84;
+58 -14
View File
@@ -26,6 +26,10 @@ namespace Drivers::Audio::IntelHda {
static bool g_initialized = false; static bool g_initialized = false;
static kcp::Spinlock g_codecLock; static kcp::Spinlock g_codecLock;
// Serializes stream lifecycle, DMA write-pointer updates, and IRQ-side
// stream inspection. The IRQ releases it before calling the mixer to keep
// the lock order consistently Mixer -> HDA stream.
static kcp::Spinlock g_streamLock;
static volatile uint8_t* g_mmioBase = nullptr; static volatile uint8_t* g_mmioBase = nullptr;
static uint8_t g_bus, g_dev, g_func; static uint8_t g_bus, g_dev, g_func;
@@ -852,6 +856,7 @@ namespace Drivers::Audio::IntelHda {
bool bufferCompleted = false; bool bufferCompleted = false;
// Handle stream interrupts (bits 0-29 correspond to stream descriptors) // Handle stream interrupts (bits 0-29 correspond to stream descriptors)
g_streamLock.Acquire();
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)) {
@@ -860,6 +865,7 @@ namespace Drivers::Audio::IntelHda {
WriteSD8(si, SD_STS, sts); WriteSD8(si, SD_STS, sts);
} }
} }
g_streamLock.Release();
// Handle RIRB interrupt (controller interrupt enable bit 30) // Handle RIRB interrupt (controller interrupt enable bit 30)
// Do NOT advance g_rirbReadPtr here — ReadResponse() owns it. // Do NOT advance g_rirbReadPtr here — ReadResponse() owns it.
@@ -1028,11 +1034,16 @@ namespace Drivers::Audio::IntelHda {
int Open(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) { int Open(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) {
if (!g_initialized) return -1; if (!g_initialized) return -1;
if (g_stream.Active) return -1; // Only one stream at a time
if (channels < 1 || channels > 8) return -1; if (channels < 1 || channels > 8) return -1;
if (bitsPerSample != 8 && bitsPerSample != 16 && bitsPerSample != 20 if (bitsPerSample != 8 && bitsPerSample != 16 && bitsPerSample != 20
&& bitsPerSample != 24 && bitsPerSample != 32) return -1; && bitsPerSample != 24 && bitsPerSample != 32) return -1;
g_streamLock.Acquire();
if (g_stream.Active) {
g_streamLock.Release();
return -1; // Only one physical stream at a time
}
// Output stream index = numInputStreams (first output stream) // Output stream index = numInputStreams (first output stream)
uint8_t streamIndex = g_numInputStreams; uint8_t streamIndex = g_numInputStreams;
uint8_t streamTag = 1; uint8_t streamTag = 1;
@@ -1044,7 +1055,10 @@ namespace Drivers::Audio::IntelHda {
ConfigureOutputPath(fmt, streamTag); ConfigureOutputPath(fmt, streamTag);
// Set up the output stream DMA // Set up the output stream DMA
if (!SetupOutputStream(streamIndex, fmt)) return -1; if (!SetupOutputStream(streamIndex, fmt)) {
g_streamLock.Release();
return -1;
}
// Zero the DMA buffer // Zero the DMA buffer
memset(g_dmaBuffer, 0, TOTAL_BUFFER_SIZE); memset(g_dmaBuffer, 0, TOTAL_BUFFER_SIZE);
@@ -1065,11 +1079,17 @@ namespace Drivers::Audio::IntelHda {
<< (uint64_t)sampleRate << "Hz " << (uint64_t)bitsPerSample << "-bit " << (uint64_t)sampleRate << "Hz " << (uint64_t)bitsPerSample << "-bit "
<< (uint64_t)channels << "ch"; << (uint64_t)channels << "ch";
g_streamLock.Release();
return 0; // Handle 0 return 0; // Handle 0
} }
void Close(int handle) { void Close(int handle) {
if (handle != 0 || !g_stream.Active) return; if (handle != 0) return;
g_streamLock.Acquire();
if (!g_stream.Active) {
g_streamLock.Release();
return;
}
StopStream(g_stream.StreamIndex); StopStream(g_stream.StreamIndex);
@@ -1080,10 +1100,16 @@ namespace Drivers::Audio::IntelHda {
g_stream.Active = false; g_stream.Active = false;
KernelLogStream(OK, "HDA") << "Stream closed"; KernelLogStream(OK, "HDA") << "Stream closed";
g_streamLock.Release();
} }
uint32_t GetWriteSpace(int handle) { uint32_t GetWriteSpace(int handle) {
if (handle != 0 || !g_stream.Active) return 0; if (handle != 0) return 0;
g_streamLock.Acquire();
if (!g_stream.Active) {
g_streamLock.Release();
return 0;
}
uint32_t hwPos = g_dmaPos[g_stream.StreamIndex * 2]; uint32_t hwPos = g_dmaPos[g_stream.StreamIndex * 2];
uint32_t writePos = g_stream.WritePos; uint32_t writePos = g_stream.WritePos;
@@ -1095,12 +1121,18 @@ namespace Drivers::Audio::IntelHda {
} }
if (available > 64) available -= 64; if (available > 64) available -= 64;
else available = 0; else available = 0;
g_streamLock.Release();
return available; return available;
} }
int Write(int handle, const uint8_t* data, uint32_t size) { int Write(int handle, const uint8_t* data, uint32_t size) {
if (handle != 0 || !g_stream.Active || !data || size == 0) if (handle != 0 || !data || size == 0)
return -1; return -1;
g_streamLock.Acquire();
if (!g_stream.Active) {
g_streamLock.Release();
return -1;
}
// Drain unsolicited responses from the RIRB — during playback no // Drain unsolicited responses from the RIRB — during playback no
// CodecCommands are sent, so ReadResponse() never runs and jack // CodecCommands are sent, so ReadResponse() never runs and jack
@@ -1135,7 +1167,10 @@ namespace Drivers::Audio::IntelHda {
else available = 0; else available = 0;
if (size > available) size = available; if (size > available) size = available;
if (size == 0) return 0; if (size == 0) {
g_streamLock.Release();
return 0;
}
// Write data to DMA buffer (handle wrap-around) // Write data to DMA buffer (handle wrap-around)
uint32_t firstChunk = TOTAL_BUFFER_SIZE - writePos; uint32_t firstChunk = TOTAL_BUFFER_SIZE - writePos;
@@ -1148,36 +1183,45 @@ namespace Drivers::Audio::IntelHda {
g_stream.WritePos = (writePos + size) % TOTAL_BUFFER_SIZE; g_stream.WritePos = (writePos + size) % TOTAL_BUFFER_SIZE;
g_streamLock.Release();
return (int)size; return (int)size;
} }
int Control(int handle, int cmd, int value) { int Control(int handle, int cmd, int value) {
if (handle != 0) return -1; if (handle != 0) return -1;
g_streamLock.Acquire();
int result = -1;
switch (cmd) { switch (cmd) {
case AUDIO_CTL_SET_VOLUME: case AUDIO_CTL_SET_VOLUME:
if (!g_initialized) { g_volume = value; return 0; } if (!g_initialized) { g_volume = value; result = 0; break; }
SetOutputVolume(value); SetOutputVolume(value);
return 0; result = 0;
break;
case AUDIO_CTL_GET_VOLUME: case AUDIO_CTL_GET_VOLUME:
return g_volume; result = g_volume;
break;
case AUDIO_CTL_GET_POS: case AUDIO_CTL_GET_POS:
if (!g_stream.Active) return 0; result = !g_stream.Active ? 0 :
return (int)g_dmaPos[g_stream.StreamIndex * 2]; (int)g_dmaPos[g_stream.StreamIndex * 2];
break;
case AUDIO_CTL_PAUSE: case AUDIO_CTL_PAUSE:
if (!g_stream.Active) return -1; if (!g_stream.Active) break;
if (value) if (value)
StopStream(g_stream.StreamIndex); StopStream(g_stream.StreamIndex);
else else
StartStream(g_stream.StreamIndex); StartStream(g_stream.StreamIndex);
return 0; result = 0;
break;
default: default:
return -1; break;
} }
g_streamLock.Release();
return result;
} }
}; };
+277 -36
View File
@@ -7,6 +7,8 @@
#include "Mixer.hpp" #include "Mixer.hpp"
#include "IntelHda.hpp" #include "IntelHda.hpp"
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
#include <Drivers/USB/Bluetooth/A2dp.hpp>
#include <Memory/PageFrameAllocator.hpp> #include <Memory/PageFrameAllocator.hpp>
#include <Memory/HHDM.hpp> #include <Memory/HHDM.hpp>
#include <Sched/Scheduler.hpp> #include <Sched/Scheduler.hpp>
@@ -14,6 +16,7 @@
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
#include <CppLib/Spinlock.hpp> #include <CppLib/Spinlock.hpp>
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
#include <atomic>
namespace Drivers::Audio::Mixer { namespace Drivers::Audio::Mixer {
@@ -34,9 +37,12 @@ namespace Drivers::Audio::Mixer {
// Cap one pump cycle to keep loop bounded under heavy write bursts. // 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 static constexpr uint32_t MAX_PUMP_FRAMES = 4096; // ~85 ms at 48 kHz
static_assert((MAX_STREAMS & (MAX_STREAMS - 1)) == 0,
"handle slot mask requires a power-of-two stream count");
struct VirtualStream { struct VirtualStream {
bool active; bool active;
int handle;
int ownerPid; int ownerPid;
char name[64]; char name[64];
@@ -66,20 +72,24 @@ namespace Drivers::Audio::Mixer {
static VirtualStream g_streams[MAX_STREAMS] = {}; static VirtualStream g_streams[MAX_STREAMS] = {};
static bool g_hdaOpened = false; static bool g_hdaOpened = false;
static int g_hdaHandle = -1; static int g_hdaHandle = -1;
static int g_masterVolume = 80; static bool g_btOpened = false;
static bool g_masterMute = false; static Output g_output = Output::Hda;
static std::atomic<bool> g_switchingOutput{false};
static std::atomic<int> g_masterVolume{80};
static std::atomic<bool> g_masterMute{false};
static int g_activeCount = 0; static int g_activeCount = 0;
static uint32_t g_nextGeneration = 1;
static uint64_t g_masterHwSeq = 0; static uint64_t g_masterHwSeq = 0;
// Monotonically increasing serial. Bumped (and waiters woken) on every // Monotonically increasing serial. Bumped (and waiters woken) on every
// mutation of mixer state. Clients use it to detect changes without // mutation of mixer state. Clients use it to detect changes without
// re-reading the whole snapshot. The address of the serial doubles as the // re-reading the whole snapshot. The address of the serial doubles as the
// wait-object passed to BlockOnObject / WakeObjectWaiters. // wait-object passed to BlockOnObject / WakeObjectWaiters.
static volatile uint64_t g_serial = 0; static std::atomic<uint64_t> g_serial{0};
// Caller must hold g_lock. // Caller must hold g_lock.
static void BumpSerialLocked() { static void BumpSerialLocked() {
g_serial++; g_serial.fetch_add(1, std::memory_order_release);
} }
// Scratch mix buffer (int32 stereo, to avoid clipping during accumulation). // Scratch mix buffer (int32 stereo, to avoid clipping during accumulation).
@@ -104,11 +114,19 @@ namespace Drivers::Audio::Mixer {
return (int16_t*)p; return (int16_t*)p;
} }
static int SlotFromHandle(int handle) {
if (handle < 0) return -1;
return handle & (MAX_STREAMS - 1);
}
static void FreeRing(int16_t* ring) { static void FreeRing(int16_t* ring) {
if (!ring) return; if (!ring) return;
Memory::g_pfa->ReallocConsecutive(ring, 0); Memory::g_pfa->ReallocConsecutive(ring, 0);
} }
// Caller holds g_lock. HDA setup is non-blocking and serialized by the
// mixer; Bluetooth setup is performed outside g_lock by SetOutput because
// AVDTP commands may wait for the peer.
static bool EnsureHdaOpen() { static bool EnsureHdaOpen() {
if (g_hdaOpened) return true; if (g_hdaOpened) return true;
if (!IntelHda::IsInitialized()) return false; if (!IntelHda::IsInitialized()) return false;
@@ -116,17 +134,26 @@ namespace Drivers::Audio::Mixer {
if (g_hdaHandle < 0) return false; if (g_hdaHandle < 0) return false;
// Master volume is applied in software during mixdown. Keep the codec // Master volume is applied in software during mixdown. Keep the codec
// amp at unity and use it only as an immediate hard-mute gate. // amp at unity and use it only as an immediate hard-mute gate.
IntelHda::Control(g_hdaHandle, IntelHda::AUDIO_CTL_SET_VOLUME, g_masterMute ? 0 : 100); IntelHda::Control(g_hdaHandle, IntelHda::AUDIO_CTL_SET_VOLUME,
g_masterMute.load(std::memory_order_acquire) ? 0 : 100);
g_hdaOpened = true; g_hdaOpened = true;
return true; return true;
} }
static bool BluetoothReady() {
if (!Drivers::USB::Bluetooth::IsInitialized()) return false;
auto state = Drivers::USB::Bluetooth::A2dp::GetState();
return state == Drivers::USB::Bluetooth::A2dp::State::Configured ||
state == Drivers::USB::Bluetooth::A2dp::State::Open ||
state == Drivers::USB::Bluetooth::A2dp::State::Streaming;
}
static void SyncHdaMasterMute() { static void SyncHdaMasterMute() {
for (;;) { for (;;) {
g_lock.Acquire(); g_lock.Acquire();
bool hdaOpen = g_hdaOpened; bool hdaOpen = g_hdaOpened;
int handle = g_hdaHandle; int handle = g_hdaHandle;
bool muted = g_masterMute; bool muted = g_masterMute.load(std::memory_order_acquire);
uint64_t seq = g_masterHwSeq; uint64_t seq = g_masterHwSeq;
g_lock.Release(); g_lock.Release();
@@ -204,8 +231,25 @@ namespace Drivers::Audio::Mixer {
return frames; return frames;
} }
static uint32_t BackendWriteSpace() {
if (g_output == Output::Bluetooth) {
return g_btOpened
? Drivers::USB::Bluetooth::A2dp::GetWriteSpace() : 0;
}
return g_hdaOpened ? IntelHda::GetWriteSpace(g_hdaHandle) : 0;
}
static int BackendWrite(const uint8_t* data, uint32_t size) {
if (g_output == Output::Bluetooth) {
return g_btOpened
? Drivers::USB::Bluetooth::A2dp::WriteAudio(data, size) : -1;
}
return g_hdaOpened ? IntelHda::Write(g_hdaHandle, data, size) : -1;
}
static void Pump() { static void Pump() {
if (!g_hdaOpened) return; if ((g_output == Output::Hda && !g_hdaOpened) ||
(g_output == Output::Bluetooth && !g_btOpened)) return;
// Produce only as many frames as the HDA DMA ring has room for right // Produce only as many frames as the HDA DMA ring has room for right
// now. Producing more would mean the surplus is silently dropped by // now. Producing more would mean the surplus is silently dropped by
@@ -213,7 +257,7 @@ namespace Drivers::Audio::Mixer {
// advanced — that's what scrambles speech into a sequence of // advanced — that's what scrambles speech into a sequence of
// unrelated chunks. Clamp to MAX_PUMP_FRAMES so the scratch buffers // unrelated chunks. Clamp to MAX_PUMP_FRAMES so the scratch buffers
// are bounded. // are bounded.
uint32_t freeBytes = IntelHda::GetWriteSpace(g_hdaHandle); uint32_t freeBytes = BackendWriteSpace();
uint32_t frames = freeBytes / 4; uint32_t frames = freeBytes / 4;
if (frames == 0) return; if (frames == 0) return;
if (frames > MAX_PUMP_FRAMES) frames = MAX_PUMP_FRAMES; if (frames > MAX_PUMP_FRAMES) frames = MAX_PUMP_FRAMES;
@@ -240,7 +284,7 @@ namespace Drivers::Audio::Mixer {
frames = streamFrames; frames = streamFrames;
} else if (g_activeCount == 0 || !hasAudible || !hasUnpaused) { } else if (g_activeCount == 0 || !hasAudible || !hasUnpaused) {
memset(g_outScratch, 0, frames * 2 * sizeof(int16_t)); memset(g_outScratch, 0, frames * 2 * sizeof(int16_t));
IntelHda::Write(g_hdaHandle, (const uint8_t*)g_outScratch, frames * 4); BackendWrite((const uint8_t*)g_outScratch, frames * 4);
return; return;
} }
@@ -304,7 +348,10 @@ namespace Drivers::Audio::Mixer {
} }
// Saturate, apply master volume + mute, and emit s16 stereo. // Saturate, apply master volume + mute, and emit s16 stereo.
int32_t masterGain = g_masterMute ? 0 : g_masterVolume; // 0..100 bool masterMuted = g_masterMute.load(std::memory_order_acquire);
int32_t masterGain = masterMuted
? 0
: g_masterVolume.load(std::memory_order_acquire);
for (uint32_t f = 0; f < frames; f++) { for (uint32_t f = 0; f < frames; f++) {
int32_t l = (g_mixScratch[f * 2 + 0] * masterGain) / 100; int32_t l = (g_mixScratch[f * 2 + 0] * masterGain) / 100;
int32_t r = (g_mixScratch[f * 2 + 1] * masterGain) / 100; int32_t r = (g_mixScratch[f * 2 + 1] * masterGain) / 100;
@@ -314,7 +361,7 @@ namespace Drivers::Audio::Mixer {
// Hand off to HDA. IntelHda::Write returns the number of bytes // Hand off to HDA. IntelHda::Write returns the number of bytes
// actually accepted (limited by free space in the DMA ring). // actually accepted (limited by free space in the DMA ring).
IntelHda::Write(g_hdaHandle, (const uint8_t*)g_outScratch, frames * 4); BackendWrite((const uint8_t*)g_outScratch, frames * 4);
} }
// ========================================================================= // =========================================================================
@@ -334,10 +381,48 @@ namespace Drivers::Audio::Mixer {
int16_t* ring = AllocRing(); int16_t* ring = AllocRing();
if (!ring) return -1; if (!ring) return -1;
retry_after_switch:
g_lock.Acquire(); g_lock.Acquire();
if (g_switchingOutput.load(std::memory_order_acquire)) {
if (!EnsureHdaOpen()) {
g_lock.Release(); g_lock.Release();
Sched::BlockOnObject((void*)&g_switchingOutput, 1000);
goto retry_after_switch;
}
// The selected backend is opened lazily. A remembered Bluetooth
// selection whose sink is no longer ready falls back to HDA.
bool wakeSwitchWaiters = false;
if (g_output == Output::Bluetooth && !BluetoothReady())
g_output = Output::Hda;
if (g_output == Output::Hda && !IntelHda::IsInitialized() &&
BluetoothReady())
g_output = Output::Bluetooth;
if (g_output == Output::Bluetooth && !g_btOpened && BluetoothReady()) {
g_switchingOutput = true;
g_lock.Release();
bool btOk = Drivers::USB::Bluetooth::A2dp::ConfigureStream(
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
Drivers::USB::Bluetooth::A2dp::StartStream();
g_lock.Acquire();
g_btOpened = btOk;
if (!btOk) g_output = Output::Hda;
if (btOk)
Drivers::USB::Bluetooth::A2dp::SetMuted(
g_masterMute.load(std::memory_order_acquire));
g_switchingOutput = false;
wakeSwitchWaiters = true;
}
if (g_output == Output::Hda && !EnsureHdaOpen()) {
g_lock.Release();
if (wakeSwitchWaiters)
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
FreeRing(ring);
return -1;
}
if (g_output == Output::Bluetooth && !g_btOpened) {
g_lock.Release();
if (wakeSwitchWaiters)
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
FreeRing(ring); FreeRing(ring);
return -1; return -1;
} }
@@ -348,12 +433,18 @@ namespace Drivers::Audio::Mixer {
} }
if (slot < 0) { if (slot < 0) {
g_lock.Release(); g_lock.Release();
if (wakeSwitchWaiters)
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
FreeRing(ring); FreeRing(ring);
return -1; return -1;
} }
VirtualStream& s = g_streams[slot]; VirtualStream& s = g_streams[slot];
s.active = true; s.active = true;
uint32_t generation = g_nextGeneration;
g_nextGeneration = (g_nextGeneration == 0x0FFFFFFFu)
? 1 : g_nextGeneration + 1;
s.handle = (int)((generation << 3) | (uint32_t)slot);
s.ownerPid = ownerPid; s.ownerPid = ownerPid;
int n = 0; int n = 0;
if (ownerName) { if (ownerName) {
@@ -377,14 +468,17 @@ namespace Drivers::Audio::Mixer {
BumpSerialLocked(); BumpSerialLocked();
g_lock.Release(); g_lock.Release();
if (wakeSwitchWaiters)
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
Sched::WakeObjectWaiters((void*)&g_serial); Sched::WakeObjectWaiters((void*)&g_serial);
return slot; return s.handle;
} }
void Close(int handle) { void Close(int handle) {
if (handle < 0 || handle >= MAX_STREAMS) return; int slot = SlotFromHandle(handle);
if (slot < 0 || slot >= MAX_STREAMS) return;
g_lock.Acquire(); g_lock.Acquire();
VirtualStream& s = g_streams[handle]; VirtualStream& s = g_streams[slot];
bool changed = false; bool changed = false;
// Pull the ring pointer out of the slot before freeing it. Once // Pull the ring pointer out of the slot before freeing it. Once
// s.active=false and s.ring=nullptr are visible under the lock no // s.active=false and s.ring=nullptr are visible under the lock no
@@ -393,10 +487,11 @@ namespace Drivers::Audio::Mixer {
// spinning on g_lock while ReallocConsecutive walks the free list. // spinning on g_lock while ReallocConsecutive walks the free list.
int16_t* ringToFree = nullptr; int16_t* ringToFree = nullptr;
bool lastStream = false; bool lastStream = false;
if (s.active) { if (s.active && s.handle == handle) {
ringToFree = s.ring; ringToFree = s.ring;
s.ring = nullptr; s.ring = nullptr;
s.active = false; s.active = false;
s.handle = -1;
s.ownerPid = 0; s.ownerPid = 0;
s.name[0] = '\0'; s.name[0] = '\0';
if (g_activeCount > 0) g_activeCount--; if (g_activeCount > 0) g_activeCount--;
@@ -408,29 +503,36 @@ namespace Drivers::Audio::Mixer {
// the HDA DMA ring stops looping its last 32 KiB of samples. The // the HDA DMA ring stops looping its last 32 KiB of samples. The
// next Open() reopens the HDA stream via EnsureHdaOpen(). // next Open() reopens the HDA stream via EnsureHdaOpen().
bool closeHda = lastStream && g_hdaOpened; bool closeHda = lastStream && g_hdaOpened;
bool closeBt = lastStream && g_btOpened;
int hdaHandle = g_hdaHandle; int hdaHandle = g_hdaHandle;
if (closeHda) { if (closeHda) {
g_hdaOpened = false; g_hdaOpened = false;
g_hdaHandle = -1; g_hdaHandle = -1;
} }
if (closeBt) g_btOpened = false;
g_lock.Release(); g_lock.Release();
if (closeHda) IntelHda::Close(hdaHandle); if (closeHda) IntelHda::Close(hdaHandle);
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
if (ringToFree) FreeRing(ringToFree); if (ringToFree) FreeRing(ringToFree);
if (changed) Sched::WakeObjectWaiters((void*)&g_serial); if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
} }
int Write(int handle, const uint8_t* data, uint32_t size) { int Write(int handle, const uint8_t* data, uint32_t size) {
if (handle < 0 || handle >= MAX_STREAMS || !data || size == 0) return -1; int slot = SlotFromHandle(handle);
if (slot < 0 || slot >= MAX_STREAMS || !data || size == 0) return -1;
g_lock.Acquire(); g_lock.Acquire();
VirtualStream& s = g_streams[handle]; VirtualStream& s = g_streams[slot];
if (!s.active) { g_lock.Release(); return -1; } if (!s.active || s.handle != handle) { g_lock.Release(); return -1; }
uint32_t written = ConvertAndPush(s, data, size); uint32_t written = ConvertAndPush(s, data, size);
// Run a pump cycle so the HDA buffer stays fed. // Run a pump cycle so the HDA buffer stays fed.
Pump(); Pump();
bool serviceBluetooth = g_output == Output::Bluetooth;
g_lock.Release(); g_lock.Release();
if (serviceBluetooth)
Drivers::USB::Bluetooth::A2dp::ServiceMedia();
return (int)written; return (int)written;
} }
@@ -449,11 +551,12 @@ namespace Drivers::Audio::Mixer {
return GetMasterMute() ? 1 : 0; return GetMasterMute() ? 1 : 0;
} }
if (handle < 0 || handle >= MAX_STREAMS) return -1; int slot = SlotFromHandle(handle);
if (slot < 0 || slot >= MAX_STREAMS) return -1;
g_lock.Acquire(); g_lock.Acquire();
VirtualStream& s = g_streams[handle]; VirtualStream& s = g_streams[slot];
if (!s.active) { g_lock.Release(); return -1; } if (!s.active || s.handle != handle) { g_lock.Release(); return -1; }
int rv = -1; int rv = -1;
bool changed = false; bool changed = false;
@@ -498,7 +601,7 @@ namespace Drivers::Audio::Mixer {
for (int i = 0; i < MAX_STREAMS && count < maxCount; i++) { for (int i = 0; i < MAX_STREAMS && count < maxCount; i++) {
VirtualStream& s = g_streams[i]; VirtualStream& s = g_streams[i];
if (!s.active) continue; if (!s.active) continue;
buf[count].handle = i; buf[count].handle = s.handle;
buf[count].ownerPid = s.ownerPid; buf[count].ownerPid = s.ownerPid;
int j = 0; int j = 0;
for (; j < 63 && s.name[j]; j++) buf[count].name[j] = s.name[j]; for (; j < 63 && s.name[j]; j++) buf[count].name[j] = s.name[j];
@@ -527,6 +630,7 @@ namespace Drivers::Audio::Mixer {
ringsToFree[ringCount++] = s.ring; ringsToFree[ringCount++] = s.ring;
s.ring = nullptr; s.ring = nullptr;
s.active = false; s.active = false;
s.handle = -1;
s.ownerPid = 0; s.ownerPid = 0;
s.name[0] = '\0'; s.name[0] = '\0';
if (g_activeCount > 0) g_activeCount--; if (g_activeCount > 0) g_activeCount--;
@@ -534,14 +638,17 @@ namespace Drivers::Audio::Mixer {
} }
} }
bool closeHda = changed && (g_activeCount == 0) && g_hdaOpened; bool closeHda = changed && (g_activeCount == 0) && g_hdaOpened;
bool closeBt = changed && (g_activeCount == 0) && g_btOpened;
int hdaHandle = g_hdaHandle; int hdaHandle = g_hdaHandle;
if (closeHda) { if (closeHda) {
g_hdaOpened = false; g_hdaOpened = false;
g_hdaHandle = -1; g_hdaHandle = -1;
} }
if (closeBt) g_btOpened = false;
if (changed) BumpSerialLocked(); if (changed) BumpSerialLocked();
g_lock.Release(); g_lock.Release();
if (closeHda) IntelHda::Close(hdaHandle); if (closeHda) IntelHda::Close(hdaHandle);
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
for (int i = 0; i < ringCount; i++) FreeRing(ringsToFree[i]); for (int i = 0; i < ringCount; i++) FreeRing(ringsToFree[i]);
if (changed) Sched::WakeObjectWaiters((void*)&g_serial); if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
} }
@@ -551,8 +658,9 @@ namespace Drivers::Audio::Mixer {
if (percent > 100) percent = 100; if (percent > 100) percent = 100;
g_lock.Acquire(); g_lock.Acquire();
bool changed = (g_masterVolume != percent); bool changed =
g_masterVolume = percent; g_masterVolume.load(std::memory_order_relaxed) != percent;
g_masterVolume.store(percent, std::memory_order_release);
if (changed) BumpSerialLocked(); if (changed) BumpSerialLocked();
g_lock.Release(); g_lock.Release();
@@ -560,13 +668,13 @@ namespace Drivers::Audio::Mixer {
} }
int GetMasterVolume() { int GetMasterVolume() {
return g_masterVolume; return g_masterVolume.load(std::memory_order_acquire);
} }
void SetMasterMute(bool muted) { void SetMasterMute(bool muted) {
g_lock.Acquire(); g_lock.Acquire();
bool changed = (g_masterMute != muted); bool changed = g_masterMute.load(std::memory_order_relaxed) != muted;
g_masterMute = muted; g_masterMute.store(muted, std::memory_order_release);
if (changed) { if (changed) {
g_masterHwSeq++; g_masterHwSeq++;
BumpSerialLocked(); BumpSerialLocked();
@@ -574,11 +682,142 @@ namespace Drivers::Audio::Mixer {
g_lock.Release(); g_lock.Release();
if (changed) SyncHdaMasterMute(); if (changed) SyncHdaMasterMute();
if (changed)
Drivers::USB::Bluetooth::A2dp::SetMuted(muted);
if (changed) Sched::WakeObjectWaiters((void*)&g_serial); if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
} }
bool GetMasterMute() { bool GetMasterMute() {
return g_masterMute; return g_masterMute.load(std::memory_order_acquire);
}
int SetOutput(Output output) {
if (output != Output::Hda && output != Output::Bluetooth) return -1;
g_lock.Acquire();
if (g_output == output &&
!g_switchingOutput.load(std::memory_order_acquire)) {
bool ready = g_activeCount == 0 ||
(output == Output::Hda ? g_hdaOpened : g_btOpened);
if (ready) {
g_lock.Release();
return 0;
}
}
if (g_switchingOutput.load(std::memory_order_acquire)) {
g_lock.Release();
return -1;
}
g_switchingOutput = true;
bool needBackend = g_activeCount != 0;
g_lock.Release();
bool prepared = true;
int newHdaHandle = -1;
if (needBackend && output == Output::Hda) {
if (!IntelHda::IsInitialized()) {
prepared = false;
} else {
newHdaHandle = IntelHda::Open(MIX_RATE, MIX_CHANNELS, MIX_BITS);
prepared = newHdaHandle >= 0;
if (prepared) {
IntelHda::Control(
newHdaHandle, IntelHda::AUDIO_CTL_SET_VOLUME,
g_masterMute.load(std::memory_order_acquire) ? 0 : 100);
}
}
} else if (needBackend && output == Output::Bluetooth) {
prepared = BluetoothReady() &&
Drivers::USB::Bluetooth::A2dp::ConfigureStream(
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
Drivers::USB::Bluetooth::A2dp::StartStream();
if (prepared)
Drivers::USB::Bluetooth::A2dp::SetMuted(
g_masterMute.load(std::memory_order_acquire));
} else if (output == Output::Hda) {
prepared = IntelHda::IsInitialized();
} else {
prepared = BluetoothReady();
}
g_lock.Acquire();
if (!prepared) {
g_switchingOutput = false;
g_lock.Release();
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
return -1;
}
Output oldOutput = g_output;
bool closeHda = oldOutput == Output::Hda && g_hdaOpened;
bool closeBt = oldOutput == Output::Bluetooth && g_btOpened;
int oldHdaHandle = g_hdaHandle;
g_output = output;
if (output == Output::Hda && needBackend) {
g_hdaHandle = newHdaHandle;
g_hdaOpened = true;
}
if (output == Output::Bluetooth && needBackend) g_btOpened = true;
if (closeHda) {
g_hdaOpened = false;
if (output != Output::Hda) g_hdaHandle = -1;
}
if (closeBt) g_btOpened = false;
// Prime the new device immediately from data already queued by
// applications, avoiding a full hardware-buffer interval of silence
// after a switch.
Pump();
g_switchingOutput = false;
BumpSerialLocked();
g_lock.Release();
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
if (closeHda) IntelHda::Close(oldHdaHandle);
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
Sched::WakeObjectWaiters((void*)&g_serial);
return 0;
}
Output GetOutput() {
g_lock.Acquire();
Output output = g_output;
g_lock.Release();
return output;
}
bool IsOutputAvailable(Output output) {
if (output == Output::Hda) return IntelHda::IsInitialized();
if (output == Output::Bluetooth) return BluetoothReady();
return false;
}
void OnBluetoothStateChanged() {
bool wake = false;
bool stopBt = false;
g_lock.Acquire();
if (g_output == Output::Bluetooth && !BluetoothReady()) {
stopBt = g_btOpened;
g_btOpened = false;
if (g_activeCount == 0 || EnsureHdaOpen()) {
g_output = Output::Hda;
if (g_hdaOpened) Pump();
}
BumpSerialLocked();
wake = true;
} else {
BumpSerialLocked();
wake = true;
}
g_lock.Release();
if (stopBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
if (wake) Sched::WakeObjectWaiters((void*)&g_serial);
}
void OnBluetoothWritable() {
g_lock.Acquire();
if (g_output == Output::Bluetooth && g_btOpened) Pump();
g_lock.Release();
} }
void OnHdaBufferComplete() { void OnHdaBufferComplete() {
@@ -586,12 +825,12 @@ namespace Drivers::Audio::Mixer {
// HDA register access are both serialized through g_lock (which // HDA register access are both serialized through g_lock (which
// disables interrupts on acquire), so this is safe to call from IRQ. // disables interrupts on acquire), so this is safe to call from IRQ.
g_lock.Acquire(); g_lock.Acquire();
Pump(); if (g_output == Output::Hda) Pump();
g_lock.Release(); g_lock.Release();
} }
uint64_t GetSerial() { uint64_t GetSerial() {
return g_serial; return g_serial.load(std::memory_order_acquire);
} }
// BlockOnObjectIf callback: returns true (i.e. "do block") only if the // BlockOnObjectIf callback: returns true (i.e. "do block") only if the
@@ -600,18 +839,20 @@ namespace Drivers::Audio::Mixer {
// read of g_serial and the scheduler dropping the process to Blocked. // read of g_serial and the scheduler dropping the process to Blocked.
struct WaitCtx { uint64_t expected; }; struct WaitCtx { uint64_t expected; };
static bool WaitShouldBlock(void* ctx) { static bool WaitShouldBlock(void* ctx) {
return g_serial == ((WaitCtx*)ctx)->expected; return g_serial.load(std::memory_order_acquire) ==
((WaitCtx*)ctx)->expected;
} }
uint64_t Wait(uint64_t prevSerial, uint64_t timeoutMs) { uint64_t Wait(uint64_t prevSerial, uint64_t timeoutMs) {
// Fast path: state already moved on, no need to enter the scheduler. // Fast path: state already moved on, no need to enter the scheduler.
if (g_serial != prevSerial) return g_serial; uint64_t serial = g_serial.load(std::memory_order_acquire);
if (timeoutMs == 0) return g_serial; if (serial != prevSerial) return serial;
if (timeoutMs == 0) return serial;
WaitCtx ctx{prevSerial}; WaitCtx ctx{prevSerial};
Sched::BlockOnObjectIf((void*)&g_serial, timeoutMs, Sched::BlockOnObjectIf((void*)&g_serial, timeoutMs,
WaitShouldBlock, &ctx); WaitShouldBlock, &ctx);
return g_serial; return g_serial.load(std::memory_order_acquire);
} }
}; };
+22 -3
View File
@@ -11,8 +11,8 @@
namespace Drivers::Audio::Mixer { namespace Drivers::Audio::Mixer {
// Maximum simultaneous virtual streams. Each open audio handle owned by a // Maximum simultaneous virtual streams. Handles include a generation, so
// process consumes one slot. Slot index doubles as the user-visible handle. // a stale handle cannot affect a different stream after slot reuse.
constexpr int MAX_STREAMS = 8; constexpr int MAX_STREAMS = 8;
// Fixed hardware mix format. Streams opened at other rates / channel // Fixed hardware mix format. Streams opened at other rates / channel
@@ -21,7 +21,13 @@ namespace Drivers::Audio::Mixer {
constexpr uint8_t MIX_CHANNELS = 2; constexpr uint8_t MIX_CHANNELS = 2;
constexpr uint8_t MIX_BITS = 16; constexpr uint8_t MIX_BITS = 16;
// Lazy-init: opens the underlying HDA stream on first virtual Open(). enum class Output : int {
Hda = 0,
Bluetooth = 1
};
// Opens a virtual stream. All streams are mixed together before being
// handed to the selected HDA or Bluetooth backend.
int Open(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample, int Open(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample,
int ownerPid, const char* ownerName); int ownerPid, const char* ownerName);
void Close(int handle); void Close(int handle);
@@ -41,6 +47,19 @@ namespace Drivers::Audio::Mixer {
void SetMasterMute(bool muted); void SetMasterMute(bool muted);
bool GetMasterMute(); bool GetMasterMute();
// Global output routing. SetOutput prepares the new backend before making
// it visible, so existing virtual streams survive a device switch.
int SetOutput(Output output);
Output GetOutput();
bool IsOutputAvailable(Output output);
// Bluetooth link state changed. Wake userspace and fall back to HDA if the
// selected sink disappeared.
void OnBluetoothStateChanged();
// Called by the Bluetooth service loop after it frees PCM queue space.
void OnBluetoothWritable();
// Called from the HDA BCIS interrupt: a buffer segment finished playing, // Called from the HDA BCIS interrupt: a buffer segment finished playing,
// refill the HW ring so audio doesn't loop stale data. // refill the HW ring so audio doesn't loop stale data.
void OnHdaBufferComplete(); void OnHdaBufferComplete();
+95 -48
View File
@@ -65,9 +65,9 @@ namespace Drivers::USB::Bluetooth::A2dp {
// State // State
// ========================================================================= // =========================================================================
static State g_state = State::Idle; static std::atomic<State> g_state{State::Idle};
static uint16_t g_sigCid = 0; // L2CAP CID for AVDTP signaling static uint16_t g_sigCid = 0; // L2CAP CID for AVDTP signaling
static uint16_t g_mediaCid = 0; // L2CAP CID for AVDTP media transport static std::atomic<uint16_t> g_mediaCid{0}; // L2CAP CID for AVDTP media transport
static uint8_t g_txLabel = 1; static uint8_t g_txLabel = 1;
static uint8_t g_remoteSeid = 0; // Remote stream endpoint ID static uint8_t g_remoteSeid = 0; // Remote stream endpoint ID
static uint8_t g_localSeid = 1; // Our local SEID static uint8_t g_localSeid = 1; // Our local SEID
@@ -92,7 +92,8 @@ namespace Drivers::USB::Bluetooth::A2dp {
// SBC encoder // SBC encoder
static Sbc::SbcEncoder g_sbcEncoder = {}; static Sbc::SbcEncoder g_sbcEncoder = {};
static bool g_sbcInitialized = false; static std::atomic<bool> g_sbcInitialized{false};
static std::atomic<bool> g_routeChanged{false};
// SBC capability negotiation. An A2DP source must SetConfiguration with a // SBC capability negotiation. An A2DP source must SetConfiguration with a
// subset of what the sink advertised in GetCapabilities -- asserting a fixed // subset of what the sink advertised in GetCapabilities -- asserting a fixed
@@ -120,6 +121,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
static std::atomic<uint32_t> g_ringHead{0}; // producer: WriteAudio static std::atomic<uint32_t> g_ringHead{0}; // producer: WriteAudio
static std::atomic<uint32_t> g_ringTail{0}; // consumer: PumpMedia static std::atomic<uint32_t> g_ringTail{0}; // consumer: PumpMedia
static std::atomic<bool> g_pumpActive{false}; // single pumper at a time static std::atomic<bool> g_pumpActive{false}; // single pumper at a time
static std::atomic<bool> g_serviceActive{false}; // serialize USB event reap too
static uint32_t g_pcmRate = 48000; static uint32_t g_pcmRate = 48000;
static uint64_t g_clockBase = 0; // ms timestamp of the media clock zero static uint64_t g_clockBase = 0; // ms timestamp of the media clock zero
static uint64_t g_sentSamples = 0; // per-channel samples sent since reset static uint64_t g_sentSamples = 0; // per-channel samples sent since reset
@@ -132,12 +134,8 @@ namespace Drivers::USB::Bluetooth::A2dp {
} }
// Volume // Volume
static int g_volume = 80; static std::atomic<bool> g_muted{false};
static std::atomic<int> g_requestedVolume{-1};
// Exclusive owner (pid) of the A2DP audio output, -1 = free. See
// ClaimOutput/ReleaseOutput in the header: the output is one unmixed
// stream, so a second process sharing the handle would corrupt it.
static std::atomic<int> g_outputOwnerPid{-1};
// AVDTP response tracking // AVDTP response tracking
static volatile bool g_avdtpResponseReady = false; static volatile bool g_avdtpResponseReady = false;
@@ -1313,6 +1311,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
// with no kernel log output at all). // with no kernel log output at all).
case AVDTP_CLOSE: { case AVDTP_CLOSE: {
g_state = State::Idle; g_state = State::Idle;
g_routeChanged.store(true, std::memory_order_release);
SendAvdtpResponse(txLabel, AVDTP_CLOSE, nullptr, 0); SendAvdtpResponse(txLabel, AVDTP_CLOSE, nullptr, 0);
KernelLogStream(WARNING, "BT-A2DP") << "Remote CLOSED stream"; KernelLogStream(WARNING, "BT-A2DP") << "Remote CLOSED stream";
break; break;
@@ -1327,6 +1326,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
case AVDTP_ABORT: { case AVDTP_ABORT: {
g_state = State::Idle; g_state = State::Idle;
g_routeChanged.store(true, std::memory_order_release);
SendAvdtpResponse(txLabel, AVDTP_ABORT, nullptr, 0); SendAvdtpResponse(txLabel, AVDTP_ABORT, nullptr, 0);
KernelLogStream(WARNING, "BT-A2DP") << "Remote ABORTED stream"; KernelLogStream(WARNING, "BT-A2DP") << "Remote ABORTED stream";
break; break;
@@ -1353,6 +1353,13 @@ namespace Drivers::USB::Bluetooth::A2dp {
// ========================================================================= // =========================================================================
bool ConfigureStream(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) { bool ConfigureStream(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) {
// Encoder configuration and PumpMedia both mutate the SBC encoder.
// Device switching normally configures an Open stream, but explicitly
// exclude a pumper that was already in flight.
bool expected = false;
if (!g_pumpActive.compare_exchange_strong(expected, true,
std::memory_order_acquire))
return false;
Sbc::Init(&g_sbcEncoder, sampleRate, channels, bitsPerSample); Sbc::Init(&g_sbcEncoder, sampleRate, channels, bitsPerSample);
// Override with the SBC parameters actually negotiated in // Override with the SBC parameters actually negotiated in
// SetConfiguration so the encoded frame headers match what the sink // SetConfiguration so the encoded frame headers match what the sink
@@ -1373,6 +1380,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
<< (uint64_t)sampleRate << "Hz " << (uint64_t)bitsPerSample << "-bit " << (uint64_t)sampleRate << "Hz " << (uint64_t)bitsPerSample << "-bit "
<< (uint64_t)channels << "ch"; << (uint64_t)channels << "ch";
g_pumpActive.store(false, std::memory_order_release);
return true; return true;
} }
@@ -1380,19 +1388,40 @@ namespace Drivers::USB::Bluetooth::A2dp {
// StartStream / StopStream // StartStream / StopStream
// ========================================================================= // =========================================================================
static bool AcquireMediaService() {
for (int spin = 0; spin < 100000; spin++) {
bool expected = false;
if (g_serviceActive.compare_exchange_weak(expected, true,
std::memory_order_acquire))
return true;
asm volatile("pause" ::: "memory");
}
return false;
}
bool StartStream() { bool StartStream() {
if (!AcquireMediaService()) return false;
bool result = false;
if (g_state == State::Open || g_state == State::Configured) { if (g_state == State::Open || g_state == State::Configured) {
if (g_state == State::Configured) { if (g_state == State::Configured) {
if (!AvdtpOpen()) return false; if (!AvdtpOpen()) {
g_serviceActive.store(false, std::memory_order_release);
return false;
} }
if (!AvdtpStart()) return false; }
if (AvdtpStart()) {
ResetMediaClock(); ResetMediaClock();
return true; result = true;
} }
return (g_state == State::Streaming); } else {
result = (g_state == State::Streaming);
}
g_serviceActive.store(false, std::memory_order_release);
return result;
} }
bool StopStream(bool flushQueued) { bool StopStream(bool flushQueued) {
if (!AcquireMediaService()) return false;
if (g_state == State::Streaming) { if (g_state == State::Streaming) {
uint8_t payload[1] = {(uint8_t)(g_remoteSeid << 2)}; uint8_t payload[1] = {(uint8_t)(g_remoteSeid << 2)};
SendAvdtpCommand(AVDTP_SUSPEND, payload, 1); SendAvdtpCommand(AVDTP_SUSPEND, payload, 1);
@@ -1406,6 +1435,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
g_ringTail.store(g_ringHead.load(std::memory_order_relaxed), g_ringTail.store(g_ringHead.load(std::memory_order_relaxed),
std::memory_order_release); std::memory_order_release);
} }
g_serviceActive.store(false, std::memory_order_release);
return true; return true;
} }
@@ -1521,9 +1551,8 @@ namespace Drivers::USB::Bluetooth::A2dp {
bytesPerFrame - firstPart); bytesPerFrame - firstPart);
g_ringTail.store(tail + bytesPerFrame, std::memory_order_release); g_ringTail.store(tail + bytesPerFrame, std::memory_order_release);
uint32_t numSamples = samplesPerFrame * g_sbcEncoder.Channels; if (g_muted.load(std::memory_order_acquire)) {
for (uint32_t i = 0; i < numSamples; i++) { memset(framePcm, 0, bytesPerFrame);
framePcm[i] = (int16_t)(((int32_t)framePcm[i] * g_volume) / 100);
} }
frameLen = Sbc::Encode(&g_sbcEncoder, framePcm, &mediaPkt[off]); frameLen = Sbc::Encode(&g_sbcEncoder, framePcm, &mediaPkt[off]);
@@ -1556,10 +1585,17 @@ namespace Drivers::USB::Bluetooth::A2dp {
static uint32_t rejCount = 0; static uint32_t rejCount = 0;
rejCount++; rejCount++;
if (rejCount <= 2 || (rejCount & 0x3FF) == 0) { if (rejCount <= 2 || (rejCount & 0x3FF) == 0) {
bool sbcInitialized =
g_sbcInitialized.load(std::memory_order_acquire);
State state = g_state.load(std::memory_order_acquire);
uint16_t mediaCid =
g_mediaCid.load(std::memory_order_acquire);
KernelLogStream(WARNING, "BT-A2DP") << "WriteAudio rejected #" KernelLogStream(WARNING, "BT-A2DP") << "WriteAudio rejected #"
<< (uint64_t)rejCount << ": sbc=" << (uint64_t)(g_sbcInitialized ? 1 : 0) << (uint64_t)rejCount << ": sbc="
<< " state=" << (uint64_t)(int)g_state << (uint64_t)(sbcInitialized ? 1 : 0)
<< " mediaCid=" << base::hex << (uint64_t)g_mediaCid << base::dec; << " state=" << (uint64_t)(int)state
<< " mediaCid=" << base::hex << (uint64_t)mediaCid
<< base::dec;
} }
return -1; return -1;
} }
@@ -1579,14 +1615,39 @@ namespace Drivers::USB::Bluetooth::A2dp {
memcpy(&g_pcmRing[0], pcmData + firstPart, n - firstPart); memcpy(&g_pcmRing[0], pcmData + firstPart, n - firstPart);
g_ringHead.store(head + n, std::memory_order_release); g_ringHead.store(head + n, std::memory_order_release);
// Reap events (NOCP credits, inbound traffic) and feed the link from // Event processing and SBC encoding deliberately happen in
// syscall context too, so streaming keeps moving even when no core // ServiceMedia(), after the mixer releases its lock.
// is idle. return (int)n;
}
void ServiceMedia() {
if (!AcquireMediaService()) return;
Xhci::PollEvents(); Xhci::PollEvents();
Hci::DrainEvents(); Hci::DrainEvents();
PumpMedia(); PumpMedia();
g_serviceActive.store(false, std::memory_order_release);
}
return (int)n; uint32_t GetWriteSpace() {
if (!g_sbcInitialized || g_state != State::Streaming || g_mediaCid == 0)
return 0;
uint32_t head = g_ringHead.load(std::memory_order_relaxed);
uint32_t tail = g_ringTail.load(std::memory_order_acquire);
return (PCM_RING_SIZE - (head - tail)) & ~3u;
}
void OnDisconnected(uint16_t aclHandle) {
if (aclHandle != L2cap::GetAclHandle()) return;
g_state.store(State::Idle, std::memory_order_release);
g_mediaCid.store(0, std::memory_order_release);
g_sbcInitialized.store(false, std::memory_order_release);
g_ringTail.store(g_ringHead.load(std::memory_order_relaxed),
std::memory_order_release);
g_routeChanged.store(true, std::memory_order_release);
}
bool ConsumeRouteChange() {
return g_routeChanged.exchange(false, std::memory_order_acq_rel);
} }
// ========================================================================= // =========================================================================
@@ -1594,42 +1655,28 @@ namespace Drivers::USB::Bluetooth::A2dp {
// ========================================================================= // =========================================================================
State GetState() { State GetState() {
return g_state; return g_state.load(std::memory_order_acquire);
} }
bool IsStreaming() { bool IsStreaming() {
return (g_state == State::Streaming); return g_state.load(std::memory_order_acquire) == State::Streaming;
} }
int GetVolume() { void RequestMasterVolume(int percent) {
return g_volume;
}
void SetVolume(int percent) {
if (percent < 0) percent = 0; if (percent < 0) percent = 0;
if (percent > 100) percent = 100; if (percent > 100) percent = 100;
g_volume = percent; g_requestedVolume.store(percent, std::memory_order_release);
} }
// ========================================================================= void SetMuted(bool muted) {
// Output ownership (one process at a time; see header) g_muted.store(muted, std::memory_order_release);
// =========================================================================
bool ClaimOutput(int pid) {
if (pid < 0) return false;
int expected = -1;
return g_outputOwnerPid.compare_exchange_strong(expected, pid,
std::memory_order_acq_rel);
} }
void ReleaseOutput(int pid) { bool ConsumeVolumeRequest(int* percent) {
if (pid < 0) return; int value = g_requestedVolume.exchange(-1, std::memory_order_acq_rel);
if (g_outputOwnerPid.load(std::memory_order_acquire) != pid) return; if (value < 0) return false;
// Stop (suspend + flush queued PCM) BEFORE freeing ownership, so a if (percent) *percent = value;
// concurrent Open cannot configure the stream while it is being return true;
// torn down.
StopStream(true);
g_outputOwnerPid.store(-1, std::memory_order_release);
} }
} }
+18 -18
View File
@@ -59,36 +59,36 @@ namespace Drivers::USB::Bluetooth::A2dp {
// of bytes accepted (0 = ring full, retry later). // of bytes accepted (0 = ring full, retry later).
int WriteAudio(const uint8_t* pcmData, uint32_t pcmLen); int WriteAudio(const uint8_t* pcmData, uint32_t pcmLen);
// The A2DP output is a single unmixed PCM stream, so at most one process // Free bytes in the PCM queue, aligned to complete stereo frames.
// may own the Bluetooth audio handle at a time. A second opener sharing uint32_t GetWriteSpace();
// it would reconfigure the SBC encoder and media clock under the first
// stream and interleave its raw PCM into the same ring (audible garble // Tear down local media state after the ACL link disappears. The actual
// and dropouts), and its close would suspend the owner's stream. // mixer notification is deferred out of the nested HCI receive path.
// void OnDisconnected(uint16_t aclHandle);
// ClaimOutput returns true if `pid` now owns the output; false if it is bool ConsumeRouteChange();
// already owned (the caller should fall back to the HDA mixer).
// ReleaseOutput stops the stream (dropping queued PCM) and frees the
// output when `pid` is the current owner; no-op otherwise. The
// scheduler also calls it on process exit so a killed app cannot leak
// ownership.
bool ClaimOutput(int pid);
void ReleaseOutput(int pid);
// Encode + send queued PCM, paced to the audio clock and gated on ACL TX // Encode + send queued PCM, paced to the audio clock and gated on ACL TX
// readiness. Called from the idle-loop event pump and from WriteAudio; // readiness. Called from the idle-loop event pump and from WriteAudio;
// self-serializing, cheap no-op when not streaming. // self-serializing, cheap no-op when not streaming.
void PumpMedia(); void PumpMedia();
// Reap controller events and pump media from process context. Kept
// separate from WriteAudio so the mixer never holds its spinlock across
// USB event processing or SBC encoding.
void ServiceMedia();
// Get current state // Get current state
State GetState(); State GetState();
// Check if currently streaming // Check if currently streaming
bool IsStreaming(); bool IsStreaming();
// Get volume (0-100) // Queue a headset AVRCP absolute-volume request for the system mixer.
int GetVolume(); void RequestMasterVolume(int percent);
void SetMuted(bool muted);
// Set volume (0-100) // AVRCP receive runs nested inside the transport event pump. Defer its
void SetVolume(int percent); // master-volume request until the top-level Bluetooth service context.
bool ConsumeVolumeRequest(int* percent);
} }
+3 -2
View File
@@ -7,6 +7,7 @@
#include "Avrcp.hpp" #include "Avrcp.hpp"
#include "A2dp.hpp" #include "A2dp.hpp"
#include "L2cap.hpp" #include "L2cap.hpp"
#include <Drivers/Audio/Mixer.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
@@ -183,7 +184,7 @@ namespace Drivers::USB::Bluetooth::Avrcp {
// follow-up on actual change is a later feature.) // follow-up on actual change is a later feature.)
if (p[0] == EVT_VOLUME_CHANGED) { if (p[0] == EVT_VOLUME_CHANGED) {
uint8_t rp[2] = {EVT_VOLUME_CHANGED, uint8_t rp[2] = {EVT_VOLUME_CHANGED,
(uint8_t)((A2dp::GetVolume() * 127) / 100)}; (uint8_t)((Drivers::Audio::Mixer::GetMasterVolume() * 127) / 100)};
SendVendorRsp(localCid, transaction, AVC_RSP_INTERIM, SendVendorRsp(localCid, transaction, AVC_RSP_INTERIM,
pdu, rp, sizeof(rp)); pdu, rp, sizeof(rp));
} else if (p[0] == EVT_PLAYBACK_STATUS) { } else if (p[0] == EVT_PLAYBACK_STATUS) {
@@ -203,7 +204,7 @@ namespace Drivers::USB::Bluetooth::Avrcp {
} }
} else if (pdu == PDU_SET_ABS_VOLUME && ctype == AVC_CTYPE_CONTROL && plen >= 1) { } else if (pdu == PDU_SET_ABS_VOLUME && ctype == AVC_CTYPE_CONTROL && plen >= 1) {
uint8_t vol = p[0] & 0x7F; uint8_t vol = p[0] & 0x7F;
A2dp::SetVolume(((int)vol * 100) / 127); A2dp::RequestMasterVolume(((int)vol * 100) / 127);
SendVendorRsp(localCid, transaction, AVC_RSP_ACCEPTED, SendVendorRsp(localCid, transaction, AVC_RSP_ACCEPTED,
pdu, &vol, 1); pdu, &vol, 1);
KernelLogStream(INFO, "BT-AVRCP") << "absolute volume -> " KernelLogStream(INFO, "BT-AVRCP") << "absolute volume -> "
@@ -8,6 +8,7 @@
#include "Hci.hpp" #include "Hci.hpp"
#include "A2dp.hpp" #include "A2dp.hpp"
#include "IntelFirmware.hpp" #include "IntelFirmware.hpp"
#include <Drivers/Audio/Mixer.hpp>
#include <Drivers/USB/Xhci.hpp> #include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/UsbDevice.hpp> #include <Drivers/USB/UsbDevice.hpp>
#include <Fs/Vfs.hpp> #include <Fs/Vfs.hpp>
@@ -419,10 +420,14 @@ namespace Drivers::USB::Bluetooth {
void ServiceEvents() { void ServiceEvents() {
if (!g_initialized) return; if (!g_initialized) return;
if (Xhci::InPollContext()) return; // never nest under PollEvents if (Xhci::InPollContext()) return; // never nest under PollEvents
Xhci::PollEvents();
Hci::DrainEvents();
Hci::ProcessPendingCommands(); Hci::ProcessPendingCommands();
A2dp::PumpMedia(); // feed queued media to the link (no-op when idle) A2dp::ServiceMedia(); // reap events and feed queued media
Drivers::Audio::Mixer::OnBluetoothWritable();
int requestedVolume;
if (A2dp::ConsumeVolumeRequest(&requestedVolume))
Drivers::Audio::Mixer::SetMasterVolume(requestedVolume);
if (A2dp::ConsumeRouteChange())
Drivers::Audio::Mixer::OnBluetoothStateChanged();
} }
// ========================================================================= // =========================================================================
@@ -605,6 +610,7 @@ namespace Drivers::USB::Bluetooth {
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory"); for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
} }
A2dp::StartSource(); A2dp::StartSource();
Drivers::Audio::Mixer::OnBluetoothStateChanged();
} }
// Persist any new link key now (process context), even if // Persist any new link key now (process context), even if
// the link later dropped, so the disk write never stalls // the link later dropped, so the disk write never stalls
+2
View File
@@ -6,6 +6,7 @@
#include "Hci.hpp" #include "Hci.hpp"
#include "L2cap.hpp" #include "L2cap.hpp"
#include "A2dp.hpp"
#include <atomic> #include <atomic>
#include <Fs/Vfs.hpp> #include <Fs/Vfs.hpp>
#include <Drivers/USB/Xhci.hpp> #include <Drivers/USB/Xhci.hpp>
@@ -795,6 +796,7 @@ namespace Drivers::USB::Bluetooth::Hci {
KernelLogStream(INFO, "BT-HCI") << "Disconnection: handle=" KernelLogStream(INFO, "BT-HCI") << "Disconnection: handle="
<< (uint64_t)handle << " reason=" << (uint64_t)reason; << (uint64_t)handle << " reason=" << (uint64_t)reason;
A2dp::OnDisconnected(handle);
for (int i = 0; i < MAX_CONNECTIONS; i++) { for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (g_connections[i].Active && g_connections[i].Handle == handle) { if (g_connections[i].Active && g_connections[i].Handle == handle) {
-6
View File
@@ -24,7 +24,6 @@
#include <Api/Heap.hpp> #include <Api/Heap.hpp>
#include <Api/LibSyscall.hpp> #include <Api/LibSyscall.hpp>
#include <Drivers/Audio/Mixer.hpp> #include <Drivers/Audio/Mixer.hpp>
#include <Drivers/USB/Bluetooth/A2dp.hpp>
#include <Drivers/Graphics/IntelGPU.hpp> #include <Drivers/Graphics/IntelGPU.hpp>
#include <Ipc/Ipc.hpp> #include <Ipc/Ipc.hpp>
@@ -1241,11 +1240,6 @@ namespace Sched {
// and its ring buffer don't leak when an app forgets to audio_close. // and its ring buffer don't leak when an app forgets to audio_close.
Drivers::Audio::Mixer::CleanupProcess(exitingPid); Drivers::Audio::Mixer::CleanupProcess(exitingPid);
// Release the Bluetooth A2DP output if this process owned it, so a
// killed app cannot leave the output claimed forever (no-op when the
// process was not the owner).
Drivers::USB::Bluetooth::A2dp::ReleaseOutput(exitingPid);
// Restore scanout to buffer 0 if the exiting process owned page // Restore scanout to buffer 0 if the exiting process owned page
// flips, so the next fullscreen client and the kernel terminal are // flips, so the next fullscreen client and the kernel terminal are
// never stranded on the invisible buffer (no-op for non-owners). // never stranded on the invisible buffer (no-op for non-owners).
+1
View File
@@ -656,6 +656,7 @@
audio_get_pos AUDIO_CTL_GET_POS (2) audio_get_pos AUDIO_CTL_GET_POS (2)
audio_pause, audio_resume AUDIO_CTL_PAUSE (3) audio_pause, audio_resume AUDIO_CTL_PAUSE (3)
audio_get_output AUDIO_CTL_GET_OUTPUT (4): 0=HDA, 1=Bluetooth audio_get_output AUDIO_CTL_GET_OUTPUT (4): 0=HDA, 1=Bluetooth
audio_set_output AUDIO_CTL_SET_OUTPUT (5): switch all streams
(SET_OUTPUT, 5) switch a stream's output route (SET_OUTPUT, 5) switch a stream's output route
audio_bt_status AUDIO_CTL_BT_STATUS (6) audio_bt_status AUDIO_CTL_BT_STATUS (6)
audio_set_master_volume, _get_ AUDIO_CTL_{SET,GET}_MASTER_VOLUME (7/8), 0-100 audio_set_master_volume, _get_ AUDIO_CTL_{SET,GET}_MASTER_VOLUME (7/8), 0-100
+2
View File
@@ -259,6 +259,8 @@ namespace montauk::abi {
static constexpr int AUDIO_CTL_GET_MUTE = 10; static constexpr int AUDIO_CTL_GET_MUTE = 10;
static constexpr int AUDIO_CTL_SET_MASTER_MUTE = 11; static constexpr int AUDIO_CTL_SET_MASTER_MUTE = 11;
static constexpr int AUDIO_CTL_GET_MASTER_MUTE = 12; static constexpr int AUDIO_CTL_GET_MASTER_MUTE = 12;
static constexpr int AUDIO_OUTPUT_HDA = 0;
static constexpr int AUDIO_OUTPUT_BLUETOOTH = 1;
static constexpr int SOCK_TCP = 1; static constexpr int SOCK_TCP = 1;
static constexpr int SOCK_UDP = 2; static constexpr int SOCK_UDP = 2;
+2
View File
@@ -188,6 +188,8 @@ struct DesktopState {
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 uint64_t vol_serial; // last seen mixer state serial
int vol_output; // 0=HDA, 1=Bluetooth
int vol_bt_status; // A2DP state (>=2 means selectable)
// Temperature monitoring // Temperature monitoring
static constexpr int MAX_THERMAL_ZONES = 8; static constexpr int MAX_THERMAL_ZONES = 8;
+3
View File
@@ -514,6 +514,9 @@ namespace montauk {
inline int audio_get_output(int handle) { inline int audio_get_output(int handle) {
return audio_ctl(handle, montauk::abi::AUDIO_CTL_GET_OUTPUT, 0); return audio_ctl(handle, montauk::abi::AUDIO_CTL_GET_OUTPUT, 0);
} }
inline int audio_set_output(int output) {
return audio_ctl(-1, montauk::abi::AUDIO_CTL_SET_OUTPUT, output);
}
inline int audio_bt_status(int handle) { inline int audio_bt_status(int handle) {
return audio_ctl(handle, montauk::abi::AUDIO_CTL_BT_STATUS, 0); return audio_ctl(handle, montauk::abi::AUDIO_CTL_BT_STATUS, 0);
} }
+1
View File
@@ -548,6 +548,7 @@
audio_get_pos AUDIO_CTL_GET_POS (2) audio_get_pos AUDIO_CTL_GET_POS (2)
audio_pause, audio_resume AUDIO_CTL_PAUSE (3) audio_pause, audio_resume AUDIO_CTL_PAUSE (3)
audio_get_output AUDIO_CTL_GET_OUTPUT (4): 0=HDA, 1=Bluetooth audio_get_output AUDIO_CTL_GET_OUTPUT (4): 0=HDA, 1=Bluetooth
audio_set_output AUDIO_CTL_SET_OUTPUT (5): switch all streams
(SET_OUTPUT, 5) switch a stream's output route (SET_OUTPUT, 5) switch a stream's output route
audio_bt_status AUDIO_CTL_BT_STATUS (6) audio_bt_status AUDIO_CTL_BT_STATUS (6)
audio_set_master_volume, _get_ AUDIO_CTL_{SET,GET}_MASTER_VOLUME (7/8), 0-100 audio_set_master_volume, _get_ AUDIO_CTL_{SET,GET}_MASTER_VOLUME (7/8), 0-100
+88 -3
View File
@@ -26,7 +26,7 @@ using namespace gui;
// ============================================================================ // ============================================================================
static constexpr int WIN_W = 380; static constexpr int WIN_W = 380;
static constexpr int WIN_H = 460; static constexpr int WIN_H = 590;
static constexpr int PAD = 22; static constexpr int PAD = 22;
static constexpr int KNOB_R = 8; static constexpr int KNOB_R = 8;
@@ -43,6 +43,7 @@ static constexpr int MASTER_BUTTON_GAP = 36; // slider → mute button
static constexpr int MASTER_TAIL_GAP = 36; // mute button → separator static constexpr int MASTER_TAIL_GAP = 36; // mute button → separator
static constexpr int APPS_HEADER_GAP = 20; // separator → "APPLICATIONS" static constexpr int APPS_HEADER_GAP = 20; // separator → "APPLICATIONS"
static constexpr int APPS_FIRST_ROW_GAP = 16; // header → first row static constexpr int APPS_FIRST_ROW_GAP = 16; // header → first row
static constexpr int OUTPUT_ROW_H = 44;
// ============================================================================ // ============================================================================
// State // State
@@ -52,6 +53,8 @@ static WsWindow g_win;
static int g_master_vol = 80; static int g_master_vol = 80;
static bool g_master_muted = false; static bool g_master_muted = false;
static int g_output = montauk::abi::AUDIO_OUTPUT_HDA;
static int g_bt_status = 0; // A2DP State enum
static montauk::abi::AudioStreamInfo g_streams[8]; static montauk::abi::AudioStreamInfo g_streams[8];
static int g_stream_count = 0; static int g_stream_count = 0;
@@ -116,10 +119,31 @@ static int separator_y(const mtk::Theme& theme) {
return master_button_y(theme) + theme.control_h + MASTER_TAIL_GAP; return master_button_y(theme) + theme.control_h + MASTER_TAIL_GAP;
} }
static int apps_header_y(const mtk::Theme& theme) { static int output_header_y(const mtk::Theme& theme) {
return separator_y(theme) + APPS_HEADER_GAP; return separator_y(theme) + APPS_HEADER_GAP;
} }
static int output_list_y(const mtk::Theme& theme) {
return output_header_y(theme) + system_font_height() + 12;
}
static Rect hda_output_rect(const mtk::Theme& theme) {
return {PAD, output_list_y(theme), g_win.width - PAD * 2, OUTPUT_ROW_H};
}
static Rect bt_output_rect(const mtk::Theme& theme) {
Rect hda = hda_output_rect(theme);
return {hda.x, hda.y + hda.h, hda.w, hda.h};
}
static int apps_separator_y(const mtk::Theme& theme) {
return output_list_y(theme) + OUTPUT_ROW_H * 2 + 18;
}
static int apps_header_y(const mtk::Theme& theme) {
return apps_separator_y(theme) + APPS_HEADER_GAP;
}
static int stream_list_y(const mtk::Theme& theme) { static int stream_list_y(const mtk::Theme& theme) {
return apps_header_y(theme) + system_font_height() + APPS_FIRST_ROW_GAP; return apps_header_y(theme) + system_font_height() + APPS_FIRST_ROW_GAP;
} }
@@ -177,6 +201,21 @@ static void refresh_master() {
g_master_muted = montauk::audio_get_master_mute() == 1; g_master_muted = montauk::audio_get_master_mute() == 1;
} }
static void refresh_output() {
int output = montauk::audio_get_output(-1);
if (output == montauk::abi::AUDIO_OUTPUT_HDA ||
output == montauk::abi::AUDIO_OUTPUT_BLUETOOTH) g_output = output;
g_bt_status = montauk::audio_bt_status(-1);
}
static bool switch_output(int output) {
if (output == g_output) return false;
if (montauk::audio_set_output(output) < 0) return false;
g_output = output;
refresh_output();
return true;
}
static void apply_master_volume(int v) { static void apply_master_volume(int v) {
if (v < 0) v = 0; if (v < 0) v = 0;
if (v > 100) v = 100; if (v > 100) v = 100;
@@ -265,6 +304,33 @@ static void draw_master(Canvas& c, const mtk::Theme& theme) {
st, theme); st, theme);
} }
static void draw_outputs(Canvas& c, const mtk::Theme& theme) {
c.text(PAD, output_header_y(theme), "OUTPUT DEVICE", theme.text_muted);
Rect hda = hda_output_rect(theme);
Rect bt = bt_output_rect(theme);
bool bt_ready = g_bt_status >= 2;
bool hda_selected = g_output == montauk::abi::AUDIO_OUTPUT_HDA;
bool bt_selected = g_output == montauk::abi::AUDIO_OUTPUT_BLUETOOTH;
// Keep the choices directly on the page, consistent with the rest of the
// app's unboxed sections.
Rect hda_radio = {hda.x, hda.y + 6, 18, hda.h - 12};
Rect bt_radio = {bt.x, bt.y + 6, 18, bt.h - 12};
mtk::draw_radio(c, hda_radio, "", hda_selected, theme);
mtk::draw_radio(c, bt_radio, "", bt_selected, theme);
int text_x = hda.x + 30;
c.text(text_x, hda.y + 4, "Built-in Speakers", theme.text);
c.text(text_x, hda.y + 23, "High Definition Audio", theme.text_subtle);
Color bt_text = bt_ready ? theme.text : theme.text_muted;
c.text(text_x, bt.y + 4, "Bluetooth", bt_text);
c.text(text_x, bt.y + 23,
bt_ready ? "Connected audio device" : "No device connected",
bt_ready ? theme.text_subtle : theme.text_muted);
}
static void draw_applications(Canvas& c, const mtk::Theme& theme) { static void draw_applications(Canvas& c, const mtk::Theme& theme) {
int hy = apps_header_y(theme); int hy = apps_header_y(theme);
c.text(PAD, hy, "APPLICATIONS", theme.text_muted); c.text(PAD, hy, "APPLICATIONS", theme.text_muted);
@@ -278,7 +344,8 @@ static void draw_applications(Canvas& c, const mtk::Theme& theme) {
} }
int visible = g_stream_count; int visible = g_stream_count;
int rows_room = (g_win.height - stream_list_y(theme) - PAD) / stream_row_h(theme); int rows_room = (g_win.height - stream_list_y(theme) - PAD)
/ stream_row_h(theme);
if (rows_room < 1) rows_room = 1; if (rows_room < 1) rows_room = 1;
if (visible > rows_room) visible = rows_room; if (visible > rows_room) visible = rows_room;
@@ -320,6 +387,9 @@ static void render() {
draw_master(c, theme); draw_master(c, theme);
mtk::draw_separator(c, PAD, separator_y(theme), mtk::draw_separator(c, PAD, separator_y(theme),
g_win.width - PAD * 2, theme); g_win.width - PAD * 2, theme);
draw_outputs(c, theme);
mtk::draw_separator(c, PAD, apps_separator_y(theme),
g_win.width - PAD * 2, theme);
draw_applications(c, theme); draw_applications(c, theme);
host.present(); host.present();
@@ -336,6 +406,14 @@ static bool handle_mouse(int mx, int my, uint8_t buttons, uint8_t prev) {
bool down = (buttons & 1) != 0; bool down = (buttons & 1) != 0;
if (clicked) { if (clicked) {
if (hda_output_rect(theme).contains(mx, my)) {
switch_output(montauk::abi::AUDIO_OUTPUT_HDA);
return true;
}
if (bt_output_rect(theme).contains(mx, my) && g_bt_status >= 2) {
switch_output(montauk::abi::AUDIO_OUTPUT_BLUETOOTH);
return true;
}
// Master slider. // Master slider.
Rect slider = master_slider_rect(); Rect slider = master_slider_rect();
if (slider_hit(slider, mx, my)) { if (slider_hit(slider, mx, my)) {
@@ -418,6 +496,7 @@ extern "C" void _start() {
} }
load_accent(); load_accent();
refresh_output();
refresh_master(); refresh_master();
refresh_streams(); refresh_streams();
g_mixer_serial = montauk::audio_wait(0, 0); g_mixer_serial = montauk::audio_wait(0, 0);
@@ -443,6 +522,12 @@ extern "C" void _start() {
if (now_serial != g_mixer_serial) { if (now_serial != g_mixer_serial) {
g_mixer_serial = now_serial; g_mixer_serial = now_serial;
int prev_output = g_output;
int prev_bt_status = g_bt_status;
refresh_output();
if (g_output != prev_output || g_bt_status != prev_bt_status)
redraw = true;
// Master: only refresh when we're not dragging master ourselves, // Master: only refresh when we're not dragging master ourselves,
// otherwise a wake mid-drag could snap our knob to the kernel. // otherwise a wake mid-drag could snap our knob to the kernel.
if (g_drag_target != -1) { if (g_drag_target != -1) {
+18 -1
View File
@@ -390,7 +390,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
int popup_x = ds->vol_icon_rect.x + ds->vol_icon_rect.w - 200; int popup_x = ds->vol_icon_rect.x + ds->vol_icon_rect.w - 200;
int popup_y = PANEL_HEIGHT + 2; int popup_y = PANEL_HEIGHT + 2;
if (popup_x < 4) popup_x = 4; if (popup_x < 4) popup_x = 4;
Rect vol_rect = {popup_x, popup_y, 200, 120}; Rect vol_rect = {popup_x, popup_y, 200, 202};
// Handle drag continuity // Handle drag continuity
if (ds->vol_dragging) { if (ds->vol_dragging) {
@@ -482,6 +482,23 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
return; return;
} }
} }
// Output device radio rows.
Rect hda_r = {popup_x + 12, popup_y + 137, 200 - 24, 25};
Rect bt_r = {hda_r.x, hda_r.y + hda_r.h, hda_r.w, hda_r.h};
if (hda_r.contains(mx, my) &&
ds->vol_output != montauk::abi::AUDIO_OUTPUT_HDA) {
if (montauk::audio_set_output(montauk::abi::AUDIO_OUTPUT_HDA) == 0)
ds->vol_output = montauk::abi::AUDIO_OUTPUT_HDA;
return;
}
if (bt_r.contains(mx, my) && ds->vol_bt_status >= 2 &&
ds->vol_output != montauk::abi::AUDIO_OUTPUT_BLUETOOTH) {
if (montauk::audio_set_output(
montauk::abi::AUDIO_OUTPUT_BLUETOOTH) == 0)
ds->vol_output = montauk::abi::AUDIO_OUTPUT_BLUETOOTH;
return;
}
return; // click inside popup but not on any control return; // click inside popup but not on any control
} else if (!ds->vol_icon_rect.contains(mx, my)) { } else if (!ds->vol_icon_rect.contains(mx, my)) {
ds->vol_popup_open = false; ds->vol_popup_open = false;
+12
View File
@@ -401,6 +401,8 @@ void gui::desktop_init(DesktopState* ds) {
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->vol_serial = montauk::audio_wait(0, 0);
ds->vol_output = montauk::audio_get_output(-1);
ds->vol_bt_status = montauk::audio_bt_status(-1);
ds->closing_ext_count = 0; ds->closing_ext_count = 0;
@@ -491,6 +493,8 @@ static bool desktop_refresh_panel_state(DesktopState* ds, uint64_t now) {
ds->vol_serial = serial; ds->vol_serial = serial;
int v = montauk::audio_get_master_volume(); int v = montauk::audio_get_master_volume();
bool muted = montauk::audio_get_master_mute() == 1; bool muted = montauk::audio_get_master_mute() == 1;
int output = montauk::audio_get_output(-1);
int btStatus = montauk::audio_bt_status(-1);
if (v >= 0 && v != ds->vol_level) { if (v >= 0 && v != ds->vol_level) {
ds->vol_level = v; ds->vol_level = v;
changed = true; changed = true;
@@ -499,6 +503,14 @@ static bool desktop_refresh_panel_state(DesktopState* ds, uint64_t now) {
ds->vol_muted = muted; ds->vol_muted = muted;
changed = true; changed = true;
} }
if (output >= 0 && output != ds->vol_output) {
ds->vol_output = output;
changed = true;
}
if (btStatus != ds->vol_bt_status) {
ds->vol_bt_status = btStatus;
changed = true;
}
} }
ds->vol_last_poll = now; ds->vol_last_poll = now;
} }
+52 -1
View File
@@ -125,6 +125,17 @@ void gui::desktop_draw_panel(DesktopState* ds) {
tinted[p] = ((uint32_t)a << 24) | 0x00CC3333; tinted[p] = ((uint32_t)a << 24) | 0x00CC3333;
} }
fb.blit_alpha(vol_icon_x, vol_icon_y, 16, 16, tinted); fb.blit_alpha(vol_icon_x, vol_icon_y, 16, 16, tinted);
} else if (ds->vol_output == montauk::abi::AUDIO_OUTPUT_BLUETOOTH) {
// The speaker glyph remains recognizable while the accent tint
// communicates that audio is routed wirelessly.
uint32_t* src = ds->icon_volume.pixels;
uint32_t tinted[256];
uint32_t rgb = ds->settings.accent_color.to_pixel() & 0x00FFFFFF;
for (int p = 0; p < 16 * 16; p++) {
uint8_t a = (src[p] >> 24) & 0xFF;
tinted[p] = ((uint32_t)a << 24) | rgb;
}
fb.blit_alpha(vol_icon_x, vol_icon_y, 16, 16, tinted);
} else { } else {
fb.blit_alpha(vol_icon_x, vol_icon_y, ds->icon_volume.width, ds->icon_volume.height, ds->icon_volume.pixels); fb.blit_alpha(vol_icon_x, vol_icon_y, ds->icon_volume.width, ds->icon_volume.height, ds->icon_volume.pixels);
} }
@@ -337,7 +348,7 @@ void desktop_draw_net_popup(DesktopState* ds) {
// ============================================================================ // ============================================================================
static constexpr int VOL_POPUP_W = 200; static constexpr int VOL_POPUP_W = 200;
static constexpr int VOL_POPUP_H = 120; static constexpr int VOL_POPUP_H = 202;
static constexpr int VOL_SLIDER_X = 16; static constexpr int VOL_SLIDER_X = 16;
static constexpr int VOL_SLIDER_W = VOL_POPUP_W - 32; static constexpr int VOL_SLIDER_W = VOL_POPUP_W - 32;
static constexpr int VOL_SLIDER_H = 8; static constexpr int VOL_SLIDER_H = 8;
@@ -455,4 +466,44 @@ void desktop_draw_vol_popup(DesktopState* ds) {
fill_rounded_rect(fb, bx, btn_y, mute_w, btn_h, btn_rad, mute_bg); fill_rounded_rect(fb, bx, btn_y, mute_w, btn_h, btn_rad, mute_bg);
tw = text_width("Mute"); tw = text_width("Mute");
draw_text(fb, bx + (mute_w - tw) / 2, btn_y + (btn_h - system_font_height()) / 2, "Mute", mute_fg); draw_text(fb, bx + (mute_w - tw) / 2, btn_y + (btn_h - system_font_height()) / 2, "Mute", mute_fg);
// Output is a persistent selection, so present it as a conventional radio
// group rather than a second bank of action buttons.
int output_label_y = popup_y + 116;
draw_text(fb, popup_x + 16, output_label_y, "Output",
Color::from_rgb(0x77, 0x77, 0x77));
auto draw_device_row = [&](const Rect& row, const char* label,
bool selected, bool enabled) {
bool hovered = enabled && row.contains(ds->mouse.x, ds->mouse.y);
if (hovered)
fill_rounded_rect(fb, row.x, row.y, row.w, row.h, 5,
Color::from_rgb(0xF0, 0xF3, 0xF6));
int radio_size = 14;
int rx = row.x + 4;
int ry = row.y + (row.h - radio_size) / 2;
fill_rounded_rect(fb, rx, ry, radio_size, radio_size,
radio_size / 2, Color::from_rgb(0xAA, 0xAA, 0xAA));
fill_rounded_rect(fb, rx + 1, ry + 1, radio_size - 2,
radio_size - 2, (radio_size - 2) / 2, colors::WHITE);
if (selected)
fill_rounded_rect(fb, rx + 4, ry + 4, radio_size - 8,
radio_size - 8, (radio_size - 8) / 2,
ds->settings.accent_color);
Color fg = enabled ? colors::TEXT_COLOR
: Color::from_rgb(0x99, 0x99, 0x99);
draw_text(fb, rx + radio_size + 8,
row.y + (row.h - system_font_height()) / 2, label, fg);
};
Rect hda_r = {popup_x + 12, popup_y + 137, VOL_POPUP_W - 24, 25};
Rect bt_r = {hda_r.x, hda_r.y + hda_r.h, hda_r.w, hda_r.h};
bool bt_ready = ds->vol_bt_status >= 2;
draw_device_row(hda_r, "Built-in Speakers",
ds->vol_output == montauk::abi::AUDIO_OUTPUT_HDA, true);
draw_device_row(bt_r, bt_ready ? "Bluetooth" : "Bluetooth (offline)",
ds->vol_output == montauk::abi::AUDIO_OUTPUT_BLUETOOTH,
bt_ready);
} }
+1
View File
@@ -417,6 +417,7 @@ int audio_pause(int handle);
int audio_resume(int handle); int audio_resume(int handle);
int audio_get_pos(int handle); // Playback position int audio_get_pos(int handle); // Playback position
int audio_get_output(int handle); // Current output device int audio_get_output(int handle); // Current output device
int audio_set_output(int output); // 0=HDA, 1=Bluetooth
int audio_bt_status(int handle); // Bluetooth audio status int audio_bt_status(int handle); // Bluetooth audio status
``` ```
+2
View File
@@ -240,6 +240,8 @@ namespace montauk::abi {
static constexpr int AUDIO_CTL_GET_MUTE = 10; static constexpr int AUDIO_CTL_GET_MUTE = 10;
static constexpr int AUDIO_CTL_SET_MASTER_MUTE = 11; static constexpr int AUDIO_CTL_SET_MASTER_MUTE = 11;
static constexpr int AUDIO_CTL_GET_MASTER_MUTE = 12; static constexpr int AUDIO_CTL_GET_MASTER_MUTE = 12;
static constexpr int AUDIO_OUTPUT_HDA = 0;
static constexpr int AUDIO_OUTPUT_BLUETOOTH = 1;
static constexpr int SOCK_TCP = 1; static constexpr int SOCK_TCP = 1;
static constexpr int SOCK_UDP = 2; static constexpr int SOCK_UDP = 2;
+2
View File
@@ -189,6 +189,8 @@ struct DesktopState {
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 uint64_t vol_serial; // last seen mixer state serial
int vol_output; // 0=HDA, 1=Bluetooth
int vol_bt_status; // A2DP state (>=2 means selectable)
// Temperature monitoring // Temperature monitoring
static constexpr int MAX_THERMAL_ZONES = 8; static constexpr int MAX_THERMAL_ZONES = 8;
@@ -498,6 +498,9 @@ namespace montauk {
inline int audio_get_output(int handle) { inline int audio_get_output(int handle) {
return audio_ctl(handle, montauk::abi::AUDIO_CTL_GET_OUTPUT, 0); return audio_ctl(handle, montauk::abi::AUDIO_CTL_GET_OUTPUT, 0);
} }
inline int audio_set_output(int output) {
return audio_ctl(-1, montauk::abi::AUDIO_CTL_SET_OUTPUT, output);
}
inline int audio_bt_status(int handle) { inline int audio_bt_status(int handle) {
return audio_ctl(handle, montauk::abi::AUDIO_CTL_BT_STATUS, 0); return audio_ctl(handle, montauk::abi::AUDIO_CTL_BT_STATUS, 0);
} }