feat: audio - add concurrent mixing, output switching, and device-aware UI
This commit is contained in:
+13
-72
@@ -19,52 +19,19 @@
|
||||
|
||||
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()) {
|
||||
// Every application gets a mixer stream regardless of the selected
|
||||
// device. The mixer performs one shared resample/mix pass and routes it
|
||||
// to HDA or A2DP, allowing concurrent Bluetooth playback and live
|
||||
// switching without invalidating application handles.
|
||||
if (Drivers::Audio::Mixer::IsOutputAvailable(
|
||||
Drivers::Audio::Mixer::Output::Hda) ||
|
||||
Drivers::Audio::Mixer::IsOutputAvailable(
|
||||
Drivers::Audio::Mixer::Output::Bluetooth)) {
|
||||
return (int64_t)Drivers::Audio::Mixer::Open(sampleRate, channels,
|
||||
bitsPerSample, pid, name);
|
||||
}
|
||||
@@ -73,46 +40,20 @@ namespace montauk::abi {
|
||||
}
|
||||
|
||||
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_GET_OUTPUT)
|
||||
return (int)Drivers::Audio::Mixer::GetOutput();
|
||||
if (cmd == AUDIO_CTL_SET_OUTPUT)
|
||||
return Drivers::Audio::Mixer::SetOutput(
|
||||
(Drivers::Audio::Mixer::Output)value);
|
||||
if (cmd == AUDIO_CTL_BT_STATUS) {
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
||||
return (int64_t)Drivers::USB::Bluetooth::A2dp::GetState();
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 17
|
||||
#define MONTAUK_BUILD_NUMBER 20
|
||||
|
||||
@@ -174,6 +174,8 @@ namespace montauk::abi {
|
||||
static constexpr int AUDIO_CTL_GET_MUTE = 10;
|
||||
static constexpr int AUDIO_CTL_SET_MASTER_MUTE = 11;
|
||||
static constexpr int AUDIO_CTL_GET_MASTER_MUTE = 12;
|
||||
static constexpr int AUDIO_OUTPUT_HDA = 0;
|
||||
static constexpr int AUDIO_OUTPUT_BLUETOOTH = 1;
|
||||
|
||||
/* Bluetooth.hpp */
|
||||
static constexpr uint64_t SYS_BTSCAN = 84;
|
||||
|
||||
@@ -26,6 +26,10 @@ namespace Drivers::Audio::IntelHda {
|
||||
|
||||
static bool g_initialized = false;
|
||||
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 uint8_t g_bus, g_dev, g_func;
|
||||
@@ -852,6 +856,7 @@ namespace Drivers::Audio::IntelHda {
|
||||
bool bufferCompleted = false;
|
||||
|
||||
// Handle stream interrupts (bits 0-29 correspond to stream descriptors)
|
||||
g_streamLock.Acquire();
|
||||
if (g_stream.Active) {
|
||||
uint8_t si = g_stream.StreamIndex;
|
||||
if (intsts & (1u << si)) {
|
||||
@@ -860,6 +865,7 @@ namespace Drivers::Audio::IntelHda {
|
||||
WriteSD8(si, SD_STS, sts);
|
||||
}
|
||||
}
|
||||
g_streamLock.Release();
|
||||
|
||||
// Handle RIRB interrupt (controller interrupt enable bit 30)
|
||||
// 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) {
|
||||
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 (bitsPerSample != 8 && bitsPerSample != 16 && bitsPerSample != 20
|
||||
&& 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)
|
||||
uint8_t streamIndex = g_numInputStreams;
|
||||
uint8_t streamTag = 1;
|
||||
@@ -1044,7 +1055,10 @@ namespace Drivers::Audio::IntelHda {
|
||||
ConfigureOutputPath(fmt, streamTag);
|
||||
|
||||
// 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
|
||||
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)channels << "ch";
|
||||
|
||||
g_streamLock.Release();
|
||||
return 0; // Handle 0
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -1080,10 +1100,16 @@ namespace Drivers::Audio::IntelHda {
|
||||
g_stream.Active = false;
|
||||
|
||||
KernelLogStream(OK, "HDA") << "Stream closed";
|
||||
g_streamLock.Release();
|
||||
}
|
||||
|
||||
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 writePos = g_stream.WritePos;
|
||||
@@ -1095,12 +1121,18 @@ namespace Drivers::Audio::IntelHda {
|
||||
}
|
||||
if (available > 64) available -= 64;
|
||||
else available = 0;
|
||||
g_streamLock.Release();
|
||||
return available;
|
||||
}
|
||||
|
||||
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;
|
||||
g_streamLock.Acquire();
|
||||
if (!g_stream.Active) {
|
||||
g_streamLock.Release();
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Drain unsolicited responses from the RIRB — during playback no
|
||||
// CodecCommands are sent, so ReadResponse() never runs and jack
|
||||
@@ -1135,7 +1167,10 @@ namespace Drivers::Audio::IntelHda {
|
||||
else available = 0;
|
||||
|
||||
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)
|
||||
uint32_t firstChunk = TOTAL_BUFFER_SIZE - writePos;
|
||||
@@ -1148,36 +1183,45 @@ namespace Drivers::Audio::IntelHda {
|
||||
|
||||
g_stream.WritePos = (writePos + size) % TOTAL_BUFFER_SIZE;
|
||||
|
||||
g_streamLock.Release();
|
||||
return (int)size;
|
||||
}
|
||||
|
||||
int Control(int handle, int cmd, int value) {
|
||||
if (handle != 0) return -1;
|
||||
|
||||
g_streamLock.Acquire();
|
||||
int result = -1;
|
||||
switch (cmd) {
|
||||
case AUDIO_CTL_SET_VOLUME:
|
||||
if (!g_initialized) { g_volume = value; return 0; }
|
||||
if (!g_initialized) { g_volume = value; result = 0; break; }
|
||||
SetOutputVolume(value);
|
||||
return 0;
|
||||
result = 0;
|
||||
break;
|
||||
|
||||
case AUDIO_CTL_GET_VOLUME:
|
||||
return g_volume;
|
||||
result = g_volume;
|
||||
break;
|
||||
|
||||
case AUDIO_CTL_GET_POS:
|
||||
if (!g_stream.Active) return 0;
|
||||
return (int)g_dmaPos[g_stream.StreamIndex * 2];
|
||||
result = !g_stream.Active ? 0 :
|
||||
(int)g_dmaPos[g_stream.StreamIndex * 2];
|
||||
break;
|
||||
|
||||
case AUDIO_CTL_PAUSE:
|
||||
if (!g_stream.Active) return -1;
|
||||
if (!g_stream.Active) break;
|
||||
if (value)
|
||||
StopStream(g_stream.StreamIndex);
|
||||
else
|
||||
StartStream(g_stream.StreamIndex);
|
||||
return 0;
|
||||
result = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
return -1;
|
||||
break;
|
||||
}
|
||||
g_streamLock.Release();
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include "Mixer.hpp"
|
||||
#include "IntelHda.hpp"
|
||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
@@ -14,6 +16,7 @@
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <atomic>
|
||||
|
||||
namespace Drivers::Audio::Mixer {
|
||||
|
||||
@@ -34,9 +37,12 @@ namespace Drivers::Audio::Mixer {
|
||||
|
||||
// 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_assert((MAX_STREAMS & (MAX_STREAMS - 1)) == 0,
|
||||
"handle slot mask requires a power-of-two stream count");
|
||||
|
||||
struct VirtualStream {
|
||||
bool active;
|
||||
int handle;
|
||||
int ownerPid;
|
||||
char name[64];
|
||||
|
||||
@@ -66,20 +72,24 @@ namespace Drivers::Audio::Mixer {
|
||||
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 bool g_btOpened = 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 uint32_t g_nextGeneration = 1;
|
||||
static uint64_t g_masterHwSeq = 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;
|
||||
static std::atomic<uint64_t> g_serial{0};
|
||||
|
||||
// Caller must hold g_lock.
|
||||
static void BumpSerialLocked() {
|
||||
g_serial++;
|
||||
g_serial.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
// Scratch mix buffer (int32 stereo, to avoid clipping during accumulation).
|
||||
@@ -104,11 +114,19 @@ namespace Drivers::Audio::Mixer {
|
||||
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) {
|
||||
if (!ring) return;
|
||||
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() {
|
||||
if (g_hdaOpened) return true;
|
||||
if (!IntelHda::IsInitialized()) return false;
|
||||
@@ -116,17 +134,26 @@ namespace Drivers::Audio::Mixer {
|
||||
if (g_hdaHandle < 0) return false;
|
||||
// Master volume is applied in software during mixdown. Keep the codec
|
||||
// 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;
|
||||
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() {
|
||||
for (;;) {
|
||||
g_lock.Acquire();
|
||||
bool hdaOpen = g_hdaOpened;
|
||||
int handle = g_hdaHandle;
|
||||
bool muted = g_masterMute;
|
||||
bool muted = g_masterMute.load(std::memory_order_acquire);
|
||||
uint64_t seq = g_masterHwSeq;
|
||||
g_lock.Release();
|
||||
|
||||
@@ -204,8 +231,25 @@ namespace Drivers::Audio::Mixer {
|
||||
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() {
|
||||
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
|
||||
// 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
|
||||
// unrelated chunks. Clamp to MAX_PUMP_FRAMES so the scratch buffers
|
||||
// are bounded.
|
||||
uint32_t freeBytes = IntelHda::GetWriteSpace(g_hdaHandle);
|
||||
uint32_t freeBytes = BackendWriteSpace();
|
||||
uint32_t frames = freeBytes / 4;
|
||||
if (frames == 0) return;
|
||||
if (frames > MAX_PUMP_FRAMES) frames = MAX_PUMP_FRAMES;
|
||||
@@ -240,7 +284,7 @@ namespace Drivers::Audio::Mixer {
|
||||
frames = streamFrames;
|
||||
} else if (g_activeCount == 0 || !hasAudible || !hasUnpaused) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -304,7 +348,10 @@ namespace Drivers::Audio::Mixer {
|
||||
}
|
||||
|
||||
// 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++) {
|
||||
int32_t l = (g_mixScratch[f * 2 + 0] * 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
|
||||
// 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();
|
||||
if (!ring) return -1;
|
||||
|
||||
retry_after_switch:
|
||||
g_lock.Acquire();
|
||||
|
||||
if (!EnsureHdaOpen()) {
|
||||
if (g_switchingOutput.load(std::memory_order_acquire)) {
|
||||
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);
|
||||
return -1;
|
||||
}
|
||||
@@ -348,12 +433,18 @@ namespace Drivers::Audio::Mixer {
|
||||
}
|
||||
if (slot < 0) {
|
||||
g_lock.Release();
|
||||
if (wakeSwitchWaiters)
|
||||
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
|
||||
FreeRing(ring);
|
||||
return -1;
|
||||
}
|
||||
|
||||
VirtualStream& s = g_streams[slot];
|
||||
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;
|
||||
int n = 0;
|
||||
if (ownerName) {
|
||||
@@ -377,14 +468,17 @@ namespace Drivers::Audio::Mixer {
|
||||
BumpSerialLocked();
|
||||
|
||||
g_lock.Release();
|
||||
if (wakeSwitchWaiters)
|
||||
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
|
||||
Sched::WakeObjectWaiters((void*)&g_serial);
|
||||
return slot;
|
||||
return s.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();
|
||||
VirtualStream& s = g_streams[handle];
|
||||
VirtualStream& s = g_streams[slot];
|
||||
bool changed = false;
|
||||
// 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
|
||||
@@ -393,10 +487,11 @@ namespace Drivers::Audio::Mixer {
|
||||
// spinning on g_lock while ReallocConsecutive walks the free list.
|
||||
int16_t* ringToFree = nullptr;
|
||||
bool lastStream = false;
|
||||
if (s.active) {
|
||||
if (s.active && s.handle == handle) {
|
||||
ringToFree = s.ring;
|
||||
s.ring = nullptr;
|
||||
s.active = false;
|
||||
s.handle = -1;
|
||||
s.ownerPid = 0;
|
||||
s.name[0] = '\0';
|
||||
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
|
||||
// next Open() reopens the HDA stream via EnsureHdaOpen().
|
||||
bool closeHda = lastStream && g_hdaOpened;
|
||||
bool closeBt = lastStream && g_btOpened;
|
||||
int hdaHandle = g_hdaHandle;
|
||||
if (closeHda) {
|
||||
g_hdaOpened = false;
|
||||
g_hdaHandle = -1;
|
||||
}
|
||||
if (closeBt) g_btOpened = false;
|
||||
g_lock.Release();
|
||||
if (closeHda) IntelHda::Close(hdaHandle);
|
||||
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
|
||||
if (ringToFree) FreeRing(ringToFree);
|
||||
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;
|
||||
int slot = SlotFromHandle(handle);
|
||||
if (slot < 0 || slot >= MAX_STREAMS || !data || size == 0) return -1;
|
||||
g_lock.Acquire();
|
||||
VirtualStream& s = g_streams[handle];
|
||||
if (!s.active) { g_lock.Release(); return -1; }
|
||||
VirtualStream& s = g_streams[slot];
|
||||
if (!s.active || s.handle != handle) { g_lock.Release(); return -1; }
|
||||
|
||||
uint32_t written = ConvertAndPush(s, data, size);
|
||||
|
||||
// Run a pump cycle so the HDA buffer stays fed.
|
||||
Pump();
|
||||
|
||||
bool serviceBluetooth = g_output == Output::Bluetooth;
|
||||
g_lock.Release();
|
||||
if (serviceBluetooth)
|
||||
Drivers::USB::Bluetooth::A2dp::ServiceMedia();
|
||||
return (int)written;
|
||||
}
|
||||
|
||||
@@ -449,11 +551,12 @@ namespace Drivers::Audio::Mixer {
|
||||
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();
|
||||
VirtualStream& s = g_streams[handle];
|
||||
if (!s.active) { g_lock.Release(); return -1; }
|
||||
VirtualStream& s = g_streams[slot];
|
||||
if (!s.active || s.handle != handle) { g_lock.Release(); return -1; }
|
||||
|
||||
int rv = -1;
|
||||
bool changed = false;
|
||||
@@ -498,7 +601,7 @@ namespace Drivers::Audio::Mixer {
|
||||
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].handle = s.handle;
|
||||
buf[count].ownerPid = s.ownerPid;
|
||||
int j = 0;
|
||||
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;
|
||||
s.ring = nullptr;
|
||||
s.active = false;
|
||||
s.handle = -1;
|
||||
s.ownerPid = 0;
|
||||
s.name[0] = '\0';
|
||||
if (g_activeCount > 0) g_activeCount--;
|
||||
@@ -534,14 +638,17 @@ namespace Drivers::Audio::Mixer {
|
||||
}
|
||||
}
|
||||
bool closeHda = changed && (g_activeCount == 0) && g_hdaOpened;
|
||||
bool closeBt = changed && (g_activeCount == 0) && g_btOpened;
|
||||
int hdaHandle = g_hdaHandle;
|
||||
if (closeHda) {
|
||||
g_hdaOpened = false;
|
||||
g_hdaHandle = -1;
|
||||
}
|
||||
if (closeBt) g_btOpened = false;
|
||||
if (changed) BumpSerialLocked();
|
||||
g_lock.Release();
|
||||
if (closeHda) IntelHda::Close(hdaHandle);
|
||||
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
|
||||
for (int i = 0; i < ringCount; i++) FreeRing(ringsToFree[i]);
|
||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||
}
|
||||
@@ -551,8 +658,9 @@ namespace Drivers::Audio::Mixer {
|
||||
if (percent > 100) percent = 100;
|
||||
|
||||
g_lock.Acquire();
|
||||
bool changed = (g_masterVolume != percent);
|
||||
g_masterVolume = percent;
|
||||
bool changed =
|
||||
g_masterVolume.load(std::memory_order_relaxed) != percent;
|
||||
g_masterVolume.store(percent, std::memory_order_release);
|
||||
if (changed) BumpSerialLocked();
|
||||
g_lock.Release();
|
||||
|
||||
@@ -560,13 +668,13 @@ namespace Drivers::Audio::Mixer {
|
||||
}
|
||||
|
||||
int GetMasterVolume() {
|
||||
return g_masterVolume;
|
||||
return g_masterVolume.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void SetMasterMute(bool muted) {
|
||||
g_lock.Acquire();
|
||||
bool changed = (g_masterMute != muted);
|
||||
g_masterMute = muted;
|
||||
bool changed = g_masterMute.load(std::memory_order_relaxed) != muted;
|
||||
g_masterMute.store(muted, std::memory_order_release);
|
||||
if (changed) {
|
||||
g_masterHwSeq++;
|
||||
BumpSerialLocked();
|
||||
@@ -574,11 +682,142 @@ namespace Drivers::Audio::Mixer {
|
||||
g_lock.Release();
|
||||
|
||||
if (changed) SyncHdaMasterMute();
|
||||
if (changed)
|
||||
Drivers::USB::Bluetooth::A2dp::SetMuted(muted);
|
||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -586,12 +825,12 @@ namespace Drivers::Audio::Mixer {
|
||||
// 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();
|
||||
if (g_output == Output::Hda) Pump();
|
||||
g_lock.Release();
|
||||
}
|
||||
|
||||
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
|
||||
@@ -600,18 +839,20 @@ namespace Drivers::Audio::Mixer {
|
||||
// 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;
|
||||
return g_serial.load(std::memory_order_acquire) ==
|
||||
((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;
|
||||
uint64_t serial = g_serial.load(std::memory_order_acquire);
|
||||
if (serial != prevSerial) return serial;
|
||||
if (timeoutMs == 0) return serial;
|
||||
|
||||
WaitCtx ctx{prevSerial};
|
||||
Sched::BlockOnObjectIf((void*)&g_serial, timeoutMs,
|
||||
WaitShouldBlock, &ctx);
|
||||
return g_serial;
|
||||
return g_serial.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
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.
|
||||
// Maximum simultaneous virtual streams. Handles include a generation, so
|
||||
// a stale handle cannot affect a different stream after slot reuse.
|
||||
constexpr int MAX_STREAMS = 8;
|
||||
|
||||
// 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_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 ownerPid, const char* ownerName);
|
||||
void Close(int handle);
|
||||
@@ -41,6 +47,19 @@ namespace Drivers::Audio::Mixer {
|
||||
void SetMasterMute(bool muted);
|
||||
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,
|
||||
// refill the HW ring so audio doesn't loop stale data.
|
||||
void OnHdaBufferComplete();
|
||||
|
||||
@@ -65,9 +65,9 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
// 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_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_remoteSeid = 0; // Remote stream endpoint ID
|
||||
static uint8_t g_localSeid = 1; // Our local SEID
|
||||
@@ -92,7 +92,8 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
|
||||
// SBC encoder
|
||||
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
|
||||
// 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_ringTail{0}; // consumer: PumpMedia
|
||||
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 uint64_t g_clockBase = 0; // ms timestamp of the media clock zero
|
||||
static uint64_t g_sentSamples = 0; // per-channel samples sent since reset
|
||||
@@ -132,12 +134,8 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
}
|
||||
|
||||
// Volume
|
||||
static int g_volume = 80;
|
||||
|
||||
// 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};
|
||||
static std::atomic<bool> g_muted{false};
|
||||
static std::atomic<int> g_requestedVolume{-1};
|
||||
|
||||
// AVDTP response tracking
|
||||
static volatile bool g_avdtpResponseReady = false;
|
||||
@@ -1313,6 +1311,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
// with no kernel log output at all).
|
||||
case AVDTP_CLOSE: {
|
||||
g_state = State::Idle;
|
||||
g_routeChanged.store(true, std::memory_order_release);
|
||||
SendAvdtpResponse(txLabel, AVDTP_CLOSE, nullptr, 0);
|
||||
KernelLogStream(WARNING, "BT-A2DP") << "Remote CLOSED stream";
|
||||
break;
|
||||
@@ -1327,6 +1326,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
|
||||
case AVDTP_ABORT: {
|
||||
g_state = State::Idle;
|
||||
g_routeChanged.store(true, std::memory_order_release);
|
||||
SendAvdtpResponse(txLabel, AVDTP_ABORT, nullptr, 0);
|
||||
KernelLogStream(WARNING, "BT-A2DP") << "Remote ABORTED stream";
|
||||
break;
|
||||
@@ -1353,6 +1353,13 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
// =========================================================================
|
||||
|
||||
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);
|
||||
// Override with the SBC parameters actually negotiated in
|
||||
// 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)channels << "ch";
|
||||
|
||||
g_pumpActive.store(false, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1380,19 +1388,40 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
// 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() {
|
||||
if (!AcquireMediaService()) return false;
|
||||
bool result = false;
|
||||
if (g_state == State::Open || 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;
|
||||
ResetMediaClock();
|
||||
return true;
|
||||
if (AvdtpStart()) {
|
||||
ResetMediaClock();
|
||||
result = true;
|
||||
}
|
||||
} else {
|
||||
result = (g_state == State::Streaming);
|
||||
}
|
||||
return (g_state == State::Streaming);
|
||||
g_serviceActive.store(false, std::memory_order_release);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool StopStream(bool flushQueued) {
|
||||
if (!AcquireMediaService()) return false;
|
||||
if (g_state == State::Streaming) {
|
||||
uint8_t payload[1] = {(uint8_t)(g_remoteSeid << 2)};
|
||||
SendAvdtpCommand(AVDTP_SUSPEND, payload, 1);
|
||||
@@ -1406,6 +1435,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
g_ringTail.store(g_ringHead.load(std::memory_order_relaxed),
|
||||
std::memory_order_release);
|
||||
}
|
||||
g_serviceActive.store(false, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1521,9 +1551,8 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
bytesPerFrame - firstPart);
|
||||
g_ringTail.store(tail + bytesPerFrame, std::memory_order_release);
|
||||
|
||||
uint32_t numSamples = samplesPerFrame * g_sbcEncoder.Channels;
|
||||
for (uint32_t i = 0; i < numSamples; i++) {
|
||||
framePcm[i] = (int16_t)(((int32_t)framePcm[i] * g_volume) / 100);
|
||||
if (g_muted.load(std::memory_order_acquire)) {
|
||||
memset(framePcm, 0, bytesPerFrame);
|
||||
}
|
||||
|
||||
frameLen = Sbc::Encode(&g_sbcEncoder, framePcm, &mediaPkt[off]);
|
||||
@@ -1556,10 +1585,17 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
static uint32_t rejCount = 0;
|
||||
rejCount++;
|
||||
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 #"
|
||||
<< (uint64_t)rejCount << ": sbc=" << (uint64_t)(g_sbcInitialized ? 1 : 0)
|
||||
<< " state=" << (uint64_t)(int)g_state
|
||||
<< " mediaCid=" << base::hex << (uint64_t)g_mediaCid << base::dec;
|
||||
<< (uint64_t)rejCount << ": sbc="
|
||||
<< (uint64_t)(sbcInitialized ? 1 : 0)
|
||||
<< " state=" << (uint64_t)(int)state
|
||||
<< " mediaCid=" << base::hex << (uint64_t)mediaCid
|
||||
<< base::dec;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -1579,14 +1615,39 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
memcpy(&g_pcmRing[0], pcmData + firstPart, n - firstPart);
|
||||
g_ringHead.store(head + n, std::memory_order_release);
|
||||
|
||||
// Reap events (NOCP credits, inbound traffic) and feed the link from
|
||||
// syscall context too, so streaming keeps moving even when no core
|
||||
// is idle.
|
||||
// Event processing and SBC encoding deliberately happen in
|
||||
// ServiceMedia(), after the mixer releases its lock.
|
||||
return (int)n;
|
||||
}
|
||||
|
||||
void ServiceMedia() {
|
||||
if (!AcquireMediaService()) return;
|
||||
Xhci::PollEvents();
|
||||
Hci::DrainEvents();
|
||||
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() {
|
||||
return g_state;
|
||||
return g_state.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool IsStreaming() {
|
||||
return (g_state == State::Streaming);
|
||||
return g_state.load(std::memory_order_acquire) == State::Streaming;
|
||||
}
|
||||
|
||||
int GetVolume() {
|
||||
return g_volume;
|
||||
}
|
||||
|
||||
void SetVolume(int percent) {
|
||||
void RequestMasterVolume(int percent) {
|
||||
if (percent < 0) percent = 0;
|
||||
if (percent > 100) percent = 100;
|
||||
g_volume = percent;
|
||||
g_requestedVolume.store(percent, std::memory_order_release);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Output ownership (one process at a time; see header)
|
||||
// =========================================================================
|
||||
|
||||
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 SetMuted(bool muted) {
|
||||
g_muted.store(muted, std::memory_order_release);
|
||||
}
|
||||
|
||||
void ReleaseOutput(int pid) {
|
||||
if (pid < 0) return;
|
||||
if (g_outputOwnerPid.load(std::memory_order_acquire) != pid) return;
|
||||
// Stop (suspend + flush queued PCM) BEFORE freeing ownership, so a
|
||||
// concurrent Open cannot configure the stream while it is being
|
||||
// torn down.
|
||||
StopStream(true);
|
||||
g_outputOwnerPid.store(-1, std::memory_order_release);
|
||||
bool ConsumeVolumeRequest(int* percent) {
|
||||
int value = g_requestedVolume.exchange(-1, std::memory_order_acq_rel);
|
||||
if (value < 0) return false;
|
||||
if (percent) *percent = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,36 +59,36 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
// of bytes accepted (0 = ring full, retry later).
|
||||
int WriteAudio(const uint8_t* pcmData, uint32_t pcmLen);
|
||||
|
||||
// The A2DP output is a single unmixed PCM stream, so at most one process
|
||||
// may own the Bluetooth audio handle at a time. A second opener sharing
|
||||
// it would reconfigure the SBC encoder and media clock under the first
|
||||
// stream and interleave its raw PCM into the same ring (audible garble
|
||||
// and dropouts), and its close would suspend the owner's stream.
|
||||
//
|
||||
// ClaimOutput returns true if `pid` now owns the output; false if it is
|
||||
// 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);
|
||||
// Free bytes in the PCM queue, aligned to complete stereo frames.
|
||||
uint32_t GetWriteSpace();
|
||||
|
||||
// Tear down local media state after the ACL link disappears. The actual
|
||||
// mixer notification is deferred out of the nested HCI receive path.
|
||||
void OnDisconnected(uint16_t aclHandle);
|
||||
bool ConsumeRouteChange();
|
||||
|
||||
// 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;
|
||||
// self-serializing, cheap no-op when not streaming.
|
||||
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
|
||||
State GetState();
|
||||
|
||||
// Check if currently streaming
|
||||
bool IsStreaming();
|
||||
|
||||
// Get volume (0-100)
|
||||
int GetVolume();
|
||||
// Queue a headset AVRCP absolute-volume request for the system mixer.
|
||||
void RequestMasterVolume(int percent);
|
||||
void SetMuted(bool muted);
|
||||
|
||||
// Set volume (0-100)
|
||||
void SetVolume(int percent);
|
||||
// AVRCP receive runs nested inside the transport event pump. Defer its
|
||||
// master-volume request until the top-level Bluetooth service context.
|
||||
bool ConsumeVolumeRequest(int* percent);
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "Avrcp.hpp"
|
||||
#include "A2dp.hpp"
|
||||
#include "L2cap.hpp"
|
||||
#include <Drivers/Audio/Mixer.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
@@ -183,7 +184,7 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
||||
// follow-up on actual change is a later feature.)
|
||||
if (p[0] == 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,
|
||||
pdu, rp, sizeof(rp));
|
||||
} 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) {
|
||||
uint8_t vol = p[0] & 0x7F;
|
||||
A2dp::SetVolume(((int)vol * 100) / 127);
|
||||
A2dp::RequestMasterVolume(((int)vol * 100) / 127);
|
||||
SendVendorRsp(localCid, transaction, AVC_RSP_ACCEPTED,
|
||||
pdu, &vol, 1);
|
||||
KernelLogStream(INFO, "BT-AVRCP") << "absolute volume -> "
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "Hci.hpp"
|
||||
#include "A2dp.hpp"
|
||||
#include "IntelFirmware.hpp"
|
||||
#include <Drivers/Audio/Mixer.hpp>
|
||||
#include <Drivers/USB/Xhci.hpp>
|
||||
#include <Drivers/USB/UsbDevice.hpp>
|
||||
#include <Fs/Vfs.hpp>
|
||||
@@ -419,10 +420,14 @@ namespace Drivers::USB::Bluetooth {
|
||||
void ServiceEvents() {
|
||||
if (!g_initialized) return;
|
||||
if (Xhci::InPollContext()) return; // never nest under PollEvents
|
||||
Xhci::PollEvents();
|
||||
Hci::DrainEvents();
|
||||
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");
|
||||
}
|
||||
A2dp::StartSource();
|
||||
Drivers::Audio::Mixer::OnBluetoothStateChanged();
|
||||
}
|
||||
// Persist any new link key now (process context), even if
|
||||
// the link later dropped, so the disk write never stalls
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "Hci.hpp"
|
||||
#include "L2cap.hpp"
|
||||
#include "A2dp.hpp"
|
||||
#include <atomic>
|
||||
#include <Fs/Vfs.hpp>
|
||||
#include <Drivers/USB/Xhci.hpp>
|
||||
@@ -795,6 +796,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
|
||||
KernelLogStream(INFO, "BT-HCI") << "Disconnection: handle="
|
||||
<< (uint64_t)handle << " reason=" << (uint64_t)reason;
|
||||
A2dp::OnDisconnected(handle);
|
||||
|
||||
for (int i = 0; i < MAX_CONNECTIONS; i++) {
|
||||
if (g_connections[i].Active && g_connections[i].Handle == handle) {
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
#include <Api/Heap.hpp>
|
||||
#include <Api/LibSyscall.hpp>
|
||||
#include <Drivers/Audio/Mixer.hpp>
|
||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
||||
#include <Drivers/Graphics/IntelGPU.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.
|
||||
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
|
||||
// flips, so the next fullscreen client and the kernel terminal are
|
||||
// never stranded on the invisible buffer (no-op for non-owners).
|
||||
|
||||
@@ -656,6 +656,7 @@
|
||||
audio_get_pos AUDIO_CTL_GET_POS (2)
|
||||
audio_pause, audio_resume AUDIO_CTL_PAUSE (3)
|
||||
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
|
||||
audio_bt_status AUDIO_CTL_BT_STATUS (6)
|
||||
audio_set_master_volume, _get_ AUDIO_CTL_{SET,GET}_MASTER_VOLUME (7/8), 0-100
|
||||
|
||||
@@ -259,6 +259,8 @@ namespace montauk::abi {
|
||||
static constexpr int AUDIO_CTL_GET_MUTE = 10;
|
||||
static constexpr int AUDIO_CTL_SET_MASTER_MUTE = 11;
|
||||
static constexpr int AUDIO_CTL_GET_MASTER_MUTE = 12;
|
||||
static constexpr int AUDIO_OUTPUT_HDA = 0;
|
||||
static constexpr int AUDIO_OUTPUT_BLUETOOTH = 1;
|
||||
|
||||
static constexpr int SOCK_TCP = 1;
|
||||
static constexpr int SOCK_UDP = 2;
|
||||
|
||||
@@ -188,6 +188,8 @@ struct DesktopState {
|
||||
bool vol_dragging; // slider drag in progress
|
||||
uint64_t vol_last_poll;
|
||||
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
|
||||
static constexpr int MAX_THERMAL_ZONES = 8;
|
||||
|
||||
@@ -514,6 +514,9 @@ namespace montauk {
|
||||
inline int audio_get_output(int handle) {
|
||||
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) {
|
||||
return audio_ctl(handle, montauk::abi::AUDIO_CTL_BT_STATUS, 0);
|
||||
}
|
||||
|
||||
@@ -548,6 +548,7 @@
|
||||
audio_get_pos AUDIO_CTL_GET_POS (2)
|
||||
audio_pause, audio_resume AUDIO_CTL_PAUSE (3)
|
||||
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
|
||||
audio_bt_status AUDIO_CTL_BT_STATUS (6)
|
||||
audio_set_master_volume, _get_ AUDIO_CTL_{SET,GET}_MASTER_VOLUME (7/8), 0-100
|
||||
|
||||
@@ -26,7 +26,7 @@ using namespace gui;
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int WIN_W = 380;
|
||||
static constexpr int WIN_H = 460;
|
||||
static constexpr int WIN_H = 590;
|
||||
|
||||
static constexpr int PAD = 22;
|
||||
static constexpr int KNOB_R = 8;
|
||||
@@ -43,6 +43,7 @@ static constexpr int MASTER_BUTTON_GAP = 36; // slider → mute button
|
||||
static constexpr int MASTER_TAIL_GAP = 36; // mute button → separator
|
||||
static constexpr int APPS_HEADER_GAP = 20; // separator → "APPLICATIONS"
|
||||
static constexpr int APPS_FIRST_ROW_GAP = 16; // header → first row
|
||||
static constexpr int OUTPUT_ROW_H = 44;
|
||||
|
||||
// ============================================================================
|
||||
// State
|
||||
@@ -52,6 +53,8 @@ static WsWindow g_win;
|
||||
|
||||
static int g_master_vol = 80;
|
||||
static bool g_master_muted = false;
|
||||
static int g_output = montauk::abi::AUDIO_OUTPUT_HDA;
|
||||
static int g_bt_status = 0; // A2DP State enum
|
||||
|
||||
static montauk::abi::AudioStreamInfo g_streams[8];
|
||||
static int g_stream_count = 0;
|
||||
@@ -116,10 +119,31 @@ static int separator_y(const mtk::Theme& theme) {
|
||||
return master_button_y(theme) + theme.control_h + MASTER_TAIL_GAP;
|
||||
}
|
||||
|
||||
static int apps_header_y(const mtk::Theme& theme) {
|
||||
static int output_header_y(const mtk::Theme& theme) {
|
||||
return separator_y(theme) + APPS_HEADER_GAP;
|
||||
}
|
||||
|
||||
static int output_list_y(const mtk::Theme& theme) {
|
||||
return output_header_y(theme) + system_font_height() + 12;
|
||||
}
|
||||
|
||||
static Rect hda_output_rect(const mtk::Theme& theme) {
|
||||
return {PAD, output_list_y(theme), g_win.width - PAD * 2, OUTPUT_ROW_H};
|
||||
}
|
||||
|
||||
static Rect bt_output_rect(const mtk::Theme& theme) {
|
||||
Rect hda = hda_output_rect(theme);
|
||||
return {hda.x, hda.y + hda.h, hda.w, hda.h};
|
||||
}
|
||||
|
||||
static int apps_separator_y(const mtk::Theme& theme) {
|
||||
return output_list_y(theme) + OUTPUT_ROW_H * 2 + 18;
|
||||
}
|
||||
|
||||
static int apps_header_y(const mtk::Theme& theme) {
|
||||
return apps_separator_y(theme) + APPS_HEADER_GAP;
|
||||
}
|
||||
|
||||
static int stream_list_y(const mtk::Theme& theme) {
|
||||
return apps_header_y(theme) + system_font_height() + APPS_FIRST_ROW_GAP;
|
||||
}
|
||||
@@ -177,6 +201,21 @@ static void refresh_master() {
|
||||
g_master_muted = montauk::audio_get_master_mute() == 1;
|
||||
}
|
||||
|
||||
static void refresh_output() {
|
||||
int output = montauk::audio_get_output(-1);
|
||||
if (output == montauk::abi::AUDIO_OUTPUT_HDA ||
|
||||
output == montauk::abi::AUDIO_OUTPUT_BLUETOOTH) g_output = output;
|
||||
g_bt_status = montauk::audio_bt_status(-1);
|
||||
}
|
||||
|
||||
static bool switch_output(int output) {
|
||||
if (output == g_output) return false;
|
||||
if (montauk::audio_set_output(output) < 0) return false;
|
||||
g_output = output;
|
||||
refresh_output();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void apply_master_volume(int v) {
|
||||
if (v < 0) v = 0;
|
||||
if (v > 100) v = 100;
|
||||
@@ -265,6 +304,33 @@ static void draw_master(Canvas& c, const mtk::Theme& theme) {
|
||||
st, theme);
|
||||
}
|
||||
|
||||
static void draw_outputs(Canvas& c, const mtk::Theme& theme) {
|
||||
c.text(PAD, output_header_y(theme), "OUTPUT DEVICE", theme.text_muted);
|
||||
|
||||
Rect hda = hda_output_rect(theme);
|
||||
Rect bt = bt_output_rect(theme);
|
||||
bool bt_ready = g_bt_status >= 2;
|
||||
bool hda_selected = g_output == montauk::abi::AUDIO_OUTPUT_HDA;
|
||||
bool bt_selected = g_output == montauk::abi::AUDIO_OUTPUT_BLUETOOTH;
|
||||
|
||||
// Keep the choices directly on the page, consistent with the rest of the
|
||||
// app's unboxed sections.
|
||||
Rect hda_radio = {hda.x, hda.y + 6, 18, hda.h - 12};
|
||||
Rect bt_radio = {bt.x, bt.y + 6, 18, bt.h - 12};
|
||||
mtk::draw_radio(c, hda_radio, "", hda_selected, theme);
|
||||
mtk::draw_radio(c, bt_radio, "", bt_selected, theme);
|
||||
|
||||
int text_x = hda.x + 30;
|
||||
c.text(text_x, hda.y + 4, "Built-in Speakers", theme.text);
|
||||
c.text(text_x, hda.y + 23, "High Definition Audio", theme.text_subtle);
|
||||
|
||||
Color bt_text = bt_ready ? theme.text : theme.text_muted;
|
||||
c.text(text_x, bt.y + 4, "Bluetooth", bt_text);
|
||||
c.text(text_x, bt.y + 23,
|
||||
bt_ready ? "Connected audio device" : "No device connected",
|
||||
bt_ready ? theme.text_subtle : theme.text_muted);
|
||||
}
|
||||
|
||||
static void draw_applications(Canvas& c, const mtk::Theme& theme) {
|
||||
int hy = apps_header_y(theme);
|
||||
c.text(PAD, hy, "APPLICATIONS", theme.text_muted);
|
||||
@@ -278,7 +344,8 @@ static void draw_applications(Canvas& c, const mtk::Theme& theme) {
|
||||
}
|
||||
|
||||
int visible = g_stream_count;
|
||||
int rows_room = (g_win.height - stream_list_y(theme) - PAD) / stream_row_h(theme);
|
||||
int rows_room = (g_win.height - stream_list_y(theme) - PAD)
|
||||
/ stream_row_h(theme);
|
||||
if (rows_room < 1) rows_room = 1;
|
||||
if (visible > rows_room) visible = rows_room;
|
||||
|
||||
@@ -320,6 +387,9 @@ static void render() {
|
||||
draw_master(c, theme);
|
||||
mtk::draw_separator(c, PAD, separator_y(theme),
|
||||
g_win.width - PAD * 2, theme);
|
||||
draw_outputs(c, theme);
|
||||
mtk::draw_separator(c, PAD, apps_separator_y(theme),
|
||||
g_win.width - PAD * 2, theme);
|
||||
draw_applications(c, theme);
|
||||
|
||||
host.present();
|
||||
@@ -336,6 +406,14 @@ static bool handle_mouse(int mx, int my, uint8_t buttons, uint8_t prev) {
|
||||
bool down = (buttons & 1) != 0;
|
||||
|
||||
if (clicked) {
|
||||
if (hda_output_rect(theme).contains(mx, my)) {
|
||||
switch_output(montauk::abi::AUDIO_OUTPUT_HDA);
|
||||
return true;
|
||||
}
|
||||
if (bt_output_rect(theme).contains(mx, my) && g_bt_status >= 2) {
|
||||
switch_output(montauk::abi::AUDIO_OUTPUT_BLUETOOTH);
|
||||
return true;
|
||||
}
|
||||
// Master slider.
|
||||
Rect slider = master_slider_rect();
|
||||
if (slider_hit(slider, mx, my)) {
|
||||
@@ -418,6 +496,7 @@ extern "C" void _start() {
|
||||
}
|
||||
|
||||
load_accent();
|
||||
refresh_output();
|
||||
refresh_master();
|
||||
refresh_streams();
|
||||
g_mixer_serial = montauk::audio_wait(0, 0);
|
||||
@@ -443,6 +522,12 @@ extern "C" void _start() {
|
||||
if (now_serial != g_mixer_serial) {
|
||||
g_mixer_serial = now_serial;
|
||||
|
||||
int prev_output = g_output;
|
||||
int prev_bt_status = g_bt_status;
|
||||
refresh_output();
|
||||
if (g_output != prev_output || g_bt_status != prev_bt_status)
|
||||
redraw = true;
|
||||
|
||||
// Master: only refresh when we're not dragging master ourselves,
|
||||
// otherwise a wake mid-drag could snap our knob to the kernel.
|
||||
if (g_drag_target != -1) {
|
||||
|
||||
@@ -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_y = PANEL_HEIGHT + 2;
|
||||
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
|
||||
if (ds->vol_dragging) {
|
||||
@@ -482,6 +482,23 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
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
|
||||
} else if (!ds->vol_icon_rect.contains(mx, my)) {
|
||||
ds->vol_popup_open = false;
|
||||
|
||||
@@ -401,6 +401,8 @@ void gui::desktop_init(DesktopState* ds) {
|
||||
ds->vol_dragging = false;
|
||||
ds->vol_last_poll = montauk::get_milliseconds();
|
||||
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;
|
||||
|
||||
@@ -491,6 +493,8 @@ static bool desktop_refresh_panel_state(DesktopState* ds, uint64_t now) {
|
||||
ds->vol_serial = serial;
|
||||
int v = montauk::audio_get_master_volume();
|
||||
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) {
|
||||
ds->vol_level = v;
|
||||
changed = true;
|
||||
@@ -499,6 +503,14 @@ static bool desktop_refresh_panel_state(DesktopState* ds, uint64_t now) {
|
||||
ds->vol_muted = muted;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -125,6 +125,17 @@ void gui::desktop_draw_panel(DesktopState* ds) {
|
||||
tinted[p] = ((uint32_t)a << 24) | 0x00CC3333;
|
||||
}
|
||||
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 {
|
||||
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_H = 120;
|
||||
static constexpr int VOL_POPUP_H = 202;
|
||||
static constexpr int VOL_SLIDER_X = 16;
|
||||
static constexpr int VOL_SLIDER_W = VOL_POPUP_W - 32;
|
||||
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);
|
||||
tw = text_width("Mute");
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -417,6 +417,7 @@ int audio_pause(int handle);
|
||||
int audio_resume(int handle);
|
||||
int audio_get_pos(int handle); // Playback position
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
@@ -240,6 +240,8 @@ namespace montauk::abi {
|
||||
static constexpr int AUDIO_CTL_GET_MUTE = 10;
|
||||
static constexpr int AUDIO_CTL_SET_MASTER_MUTE = 11;
|
||||
static constexpr int AUDIO_CTL_GET_MASTER_MUTE = 12;
|
||||
static constexpr int AUDIO_OUTPUT_HDA = 0;
|
||||
static constexpr int AUDIO_OUTPUT_BLUETOOTH = 1;
|
||||
|
||||
static constexpr int SOCK_TCP = 1;
|
||||
static constexpr int SOCK_UDP = 2;
|
||||
|
||||
@@ -189,6 +189,8 @@ struct DesktopState {
|
||||
bool vol_dragging; // slider drag in progress
|
||||
uint64_t vol_last_poll;
|
||||
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
|
||||
static constexpr int MAX_THERMAL_ZONES = 8;
|
||||
|
||||
@@ -498,6 +498,9 @@ namespace montauk {
|
||||
inline int audio_get_output(int handle) {
|
||||
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) {
|
||||
return audio_ctl(handle, montauk::abi::AUDIO_CTL_BT_STATUS, 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user