From 7fd98374e924b326fd55cae8b76afeead6090386 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 3 Jul 2026 22:12:55 +0200 Subject: [PATCH] feat: follow audio output device changes (reopen on disconnect) OpenAL does not re-route on its own, so unplugging the active output device (e.g. headphones) mid-game left the sim silent until restart. Poll ALC_CONNECTED (ALC_EXT_disconnect) at ~1 Hz and, when the device is lost, reopen playback on the current default output via alcReopenDeviceSOFT, preserving the context and all sources. Tokens are declared locally since the bundled AL headers ship no alext.h; the entry point is resolved at runtime, so this is a no-op (with a log warning) on OpenAL Soft builds too old to provide ALC_SOFT_reopen_device. --- audio/audiorenderer.cpp | 39 +++++++++++++++++++++++++++++++++++++++ audio/audiorenderer.h | 6 ++++++ 2 files changed, 45 insertions(+) diff --git a/audio/audiorenderer.cpp b/audio/audiorenderer.cpp index eceeb346..eb18058e 100644 --- a/audio/audiorenderer.cpp +++ b/audio/audiorenderer.cpp @@ -17,6 +17,12 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "vehicle/Train.h" +// ALC_EXT_disconnect / ALC_SOFT_reopen_device tokens; the bundled AL headers ship no alext.h, +// but OpenAL Soft provides these at runtime (resolved via alcGetProcAddress). +#ifndef ALC_CONNECTED +#define ALC_CONNECTED 0x313 +#endif + namespace audio { openal_renderer renderer; @@ -363,6 +369,32 @@ openal_renderer::update( double const Deltatime ) { if (alcDeviceResumeSOFT) alcDeviceResumeSOFT(m_device); + // follow audio output device changes (OpenAL won't on its own): if the active device is + // gone (e.g. headphones unplugged) reopen playback on the current default output. Polled at + // ~1 Hz to avoid per-frame ALC round-trips. + if( m_candetectdisconnect ) { + m_devicechecktime += Deltatime; + if( m_devicechecktime >= 1.0 ) { + m_devicechecktime = 0.0; + ALCint connected{ ALC_TRUE }; + ::alcGetIntegerv( m_device, ALC_CONNECTED, 1, &connected ); + if( connected == ALC_FALSE ) { + if( alcReopenDeviceSOFT != nullptr ) { + // NULL device name selects the current default output; context and sources are preserved + if( alcReopenDeviceSOFT( m_device, nullptr, m_contextattributes ) == ALC_TRUE ) { + WriteLog( "sound: active audio device lost, reopened on the current default output" ); + } + else { + ErrorLog( "sound: active audio device lost, reopening on the default output failed (retrying)" ); + } + } + else { + ErrorLog( "sound: active audio device lost and ALC_SOFT_reopen_device is unavailable; cannot recover" ); + } + } + } + } + // update listener // gain ::alListenerf( AL_GAIN, Global.AudioVolume ); @@ -544,6 +576,7 @@ openal_renderer::init_caps() { WriteLog( "Supported extensions: " + std::string{ (char *)::alcGetString( m_device, ALC_EXTENSIONS ) } ); ALCint attr[3] = { ALC_MONO_SOURCES, Global.audio_max_sources, 0 }; // request more sounds + std::copy( std::begin( attr ), std::end( attr ), std::begin( m_contextattributes ) ); // cached for device reopen m_context = ::alcCreateContext( m_device, attr ); if( m_context == nullptr ) { @@ -573,6 +606,12 @@ openal_renderer::init_caps() { if (!alcDevicePauseSOFT || !alcDeviceResumeSOFT) WriteLog("sound: warning: extension ALC_SOFT_pause_device not found"); + m_candetectdisconnect = ( alcIsExtensionPresent( m_device, "ALC_EXT_disconnect" ) == ALC_TRUE ); + if( alcIsExtensionPresent( m_device, "ALC_SOFT_reopen_device" ) == ALC_TRUE ) + alcReopenDeviceSOFT = (ALCboolean(*)(ALCdevice*, ALCchar const*, ALCint const*))alcGetProcAddress( m_device, "alcReopenDeviceSOFT" ); + if( !alcReopenDeviceSOFT ) + WriteLog( "sound: warning: extension ALC_SOFT_reopen_device not found; audio output device changes won't be followed" ); + return true; } diff --git a/audio/audiorenderer.h b/audio/audiorenderer.h index 13978fd0..53c9c394 100644 --- a/audio/audiorenderer.h +++ b/audio/audiorenderer.h @@ -176,6 +176,12 @@ private: void (*alProcessUpdatesSOFT)() = nullptr; void (*alcDevicePauseSOFT)(ALCdevice*) = nullptr; void (*alcDeviceResumeSOFT)(ALCdevice*) = nullptr; + // ALC_SOFT_reopen_device: move playback to the current default output when the active + // device disappears (e.g. headphones unplugged mid-game), keeping context and sources + ALCboolean (*alcReopenDeviceSOFT)(ALCdevice*, ALCchar const*, ALCint const*) = nullptr; + bool m_candetectdisconnect{ false }; // ALC_EXT_disconnect present, ALC_CONNECTED is queryable + ALCint m_contextattributes[3]{ 0, 0, 0 }; // cached context attribs, reused when reopening the device + double m_devicechecktime{ 0.0 }; // accumulates dt to throttle the ALC_CONNECTED poll to ~1 Hz }; extern openal_renderer renderer;