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