diff --git a/kernel/src/Api/Audio.hpp b/kernel/src/Api/Audio.hpp index 2638b2d..7aa2c34 100644 --- a/kernel/src/Api/Audio.hpp +++ b/kernel/src/Api/Audio.hpp @@ -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(); diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index b148ae7..4a8b1ef 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 17 +#define MONTAUK_BUILD_NUMBER 20 diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 1b0f34a..87c3d43 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -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; diff --git a/kernel/src/Drivers/Audio/IntelHda.cpp b/kernel/src/Drivers/Audio/IntelHda.cpp index 0ef2fbc..c2bb112 100644 --- a/kernel/src/Drivers/Audio/IntelHda.cpp +++ b/kernel/src/Drivers/Audio/IntelHda.cpp @@ -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; } }; diff --git a/kernel/src/Drivers/Audio/Mixer.cpp b/kernel/src/Drivers/Audio/Mixer.cpp index cc5ca09..f5b3491 100644 --- a/kernel/src/Drivers/Audio/Mixer.cpp +++ b/kernel/src/Drivers/Audio/Mixer.cpp @@ -7,6 +7,8 @@ #include "Mixer.hpp" #include "IntelHda.hpp" +#include +#include #include #include #include @@ -14,6 +16,7 @@ #include #include #include +#include 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 g_switchingOutput{false}; + static std::atomic g_masterVolume{80}; + static std::atomic 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 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); } }; diff --git a/kernel/src/Drivers/Audio/Mixer.hpp b/kernel/src/Drivers/Audio/Mixer.hpp index 312d107..e0cb4dd 100644 --- a/kernel/src/Drivers/Audio/Mixer.hpp +++ b/kernel/src/Drivers/Audio/Mixer.hpp @@ -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(); diff --git a/kernel/src/Drivers/USB/Bluetooth/A2dp.cpp b/kernel/src/Drivers/USB/Bluetooth/A2dp.cpp index 39e5384..10cda25 100644 --- a/kernel/src/Drivers/USB/Bluetooth/A2dp.cpp +++ b/kernel/src/Drivers/USB/Bluetooth/A2dp.cpp @@ -65,9 +65,9 @@ namespace Drivers::USB::Bluetooth::A2dp { // State // ========================================================================= - static State g_state = State::Idle; + static std::atomic 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 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 g_sbcInitialized{false}; + static std::atomic 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 g_ringHead{0}; // producer: WriteAudio static std::atomic g_ringTail{0}; // consumer: PumpMedia static std::atomic g_pumpActive{false}; // single pumper at a time + static std::atomic 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 g_outputOwnerPid{-1}; + static std::atomic g_muted{false}; + static std::atomic 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; } } diff --git a/kernel/src/Drivers/USB/Bluetooth/A2dp.hpp b/kernel/src/Drivers/USB/Bluetooth/A2dp.hpp index 165b3da..9f34c05 100644 --- a/kernel/src/Drivers/USB/Bluetooth/A2dp.hpp +++ b/kernel/src/Drivers/USB/Bluetooth/A2dp.hpp @@ -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); } diff --git a/kernel/src/Drivers/USB/Bluetooth/Avrcp.cpp b/kernel/src/Drivers/USB/Bluetooth/Avrcp.cpp index 46ec4d4..0b97934 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Avrcp.cpp +++ b/kernel/src/Drivers/USB/Bluetooth/Avrcp.cpp @@ -7,6 +7,7 @@ #include "Avrcp.hpp" #include "A2dp.hpp" #include "L2cap.hpp" +#include #include #include #include @@ -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 -> " diff --git a/kernel/src/Drivers/USB/Bluetooth/Bluetooth.cpp b/kernel/src/Drivers/USB/Bluetooth/Bluetooth.cpp index 41de253..42da36a 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Bluetooth.cpp +++ b/kernel/src/Drivers/USB/Bluetooth/Bluetooth.cpp @@ -8,6 +8,7 @@ #include "Hci.hpp" #include "A2dp.hpp" #include "IntelFirmware.hpp" +#include #include #include #include @@ -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 diff --git a/kernel/src/Drivers/USB/Bluetooth/Hci.cpp b/kernel/src/Drivers/USB/Bluetooth/Hci.cpp index f118f01..94c5c64 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Hci.cpp +++ b/kernel/src/Drivers/USB/Bluetooth/Hci.cpp @@ -6,6 +6,7 @@ #include "Hci.hpp" #include "L2cap.hpp" +#include "A2dp.hpp" #include #include #include @@ -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) { diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index b0136b9..9d3d798 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include @@ -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). diff --git a/montaukos.org/docs/man/syscalls.html b/montaukos.org/docs/man/syscalls.html index 3a84807..4712ddc 100644 --- a/montaukos.org/docs/man/syscalls.html +++ b/montaukos.org/docs/man/syscalls.html @@ -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 diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 955a672..ad4af3a 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -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; diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp index abc3534..f4ba59a 100644 --- a/programs/include/gui/desktop.hpp +++ b/programs/include/gui/desktop.hpp @@ -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; diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 954e12b..b4b822b 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -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); } diff --git a/programs/man/syscalls.2 b/programs/man/syscalls.2 index f36a516..b30a06b 100644 --- a/programs/man/syscalls.2 +++ b/programs/man/syscalls.2 @@ -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 diff --git a/programs/src/audio/main.cpp b/programs/src/audio/main.cpp index a506fb7..d13b3e9 100644 --- a/programs/src/audio/main.cpp +++ b/programs/src/audio/main.cpp @@ -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) { diff --git a/programs/src/desktop/input.cpp b/programs/src/desktop/input.cpp index 72b288d..4eaff2e 100644 --- a/programs/src/desktop/input.cpp +++ b/programs/src/desktop/input.cpp @@ -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; diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index a59bff3..00f2760 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -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; } diff --git a/programs/src/desktop/panel.cpp b/programs/src/desktop/panel.cpp index 5abb1bb..715f714 100644 --- a/programs/src/desktop/panel.cpp +++ b/programs/src/desktop/panel.cpp @@ -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); } diff --git a/template/docs/syscalls.md b/template/docs/syscalls.md index 5de2bfd..4bd03b4 100644 --- a/template/docs/syscalls.md +++ b/template/docs/syscalls.md @@ -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 ``` diff --git a/template/sysroot/include/Api/Syscall.hpp b/template/sysroot/include/Api/Syscall.hpp index 19a3f8b..2feb4b2 100644 --- a/template/sysroot/include/Api/Syscall.hpp +++ b/template/sysroot/include/Api/Syscall.hpp @@ -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; diff --git a/template/sysroot/include/gui/desktop.hpp b/template/sysroot/include/gui/desktop.hpp index 5e659d1..c112a29 100644 --- a/template/sysroot/include/gui/desktop.hpp +++ b/template/sysroot/include/gui/desktop.hpp @@ -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; diff --git a/template/sysroot/include/montauk/syscall.h b/template/sysroot/include/montauk/syscall.h index b0b4977..99ad89c 100644 --- a/template/sysroot/include/montauk/syscall.h +++ b/template/sysroot/include/montauk/syscall.h @@ -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); }