|
| 1 | +package starcore.backend.util; |
| 2 | + |
| 3 | +import flixel.sound.FlxSound; |
| 4 | +import flixel.util.FlxSignal; |
| 5 | +import lime.media.AudioBuffer; |
| 6 | +import lime.media.AudioManager; |
| 7 | +import lime.utils.UInt8Array; |
| 8 | +import openfl.media.Sound; |
| 9 | + |
| 10 | +/** |
| 11 | + * Type definition for storing sound regeneration data. |
| 12 | + */ |
| 13 | +#if (windows && cpp) |
| 14 | +typedef RegenSoundData = |
| 15 | +{ |
| 16 | + var sound:FlxSound; |
| 17 | + var isPlaying:Bool; |
| 18 | + var time:Float; |
| 19 | +}; |
| 20 | + |
| 21 | +/** |
| 22 | + * Utility class for handling the game's audio, such as restarting audio on device change. |
| 23 | + * |
| 24 | + * THIS WAS NOT MADE BY ME! Credits go to cyn0x8 for this class. |
| 25 | + * @see https://github.com/cyn0x8 |
| 26 | + * @see https://github.com/FunkinCrew/Funkin/pull/5569 |
| 27 | + */ |
| 28 | +@:buildXml(' |
| 29 | +<target id="haxe"> |
| 30 | + <lib name="ole32.lib" if="windows"/> |
| 31 | +</target> |
| 32 | +') |
| 33 | +@:cppFileCode(' |
| 34 | +#include <string> |
| 35 | +#include "mmdeviceapi.h" |
| 36 | + |
| 37 | +bool _audioDeviceChanged = false; |
| 38 | +class AudioFixClient : public IMMNotificationClient { |
| 39 | + public: |
| 40 | + |
| 41 | + AudioFixClient() : _refCount(1), _pDeviceEnum(nullptr) { |
| 42 | + HRESULT result = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (void**)&_pDeviceEnum); |
| 43 | + if (result == S_OK) _pDeviceEnum->RegisterEndpointNotificationCallback(this); |
| 44 | + updateCurrentDeviceID(); |
| 45 | + } |
| 46 | + |
| 47 | + ~AudioFixClient() { |
| 48 | + if (_pDeviceEnum != nullptr) { |
| 49 | + _pDeviceEnum->UnregisterEndpointNotificationCallback(this); |
| 50 | + _pDeviceEnum->Release(); |
| 51 | + _pDeviceEnum = nullptr; |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDefaultDeviceId) { |
| 56 | + if (flow == eRender && role == eConsole && pwstrDefaultDeviceId != nullptr) { |
| 57 | + if (_currentDeviceID.compare(pwstrDefaultDeviceId) != 0) { |
| 58 | + _audioDeviceChanged = true; |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + return S_OK; |
| 63 | + } |
| 64 | + |
| 65 | + ULONG STDMETHODCALLTYPE AddRef() { |
| 66 | + return InterlockedIncrement(&_refCount); |
| 67 | + } |
| 68 | + |
| 69 | + ULONG STDMETHODCALLTYPE Release() { |
| 70 | + ULONG ulRef = InterlockedDecrement(&_refCount); |
| 71 | + if (0 == ulRef) delete this; |
| 72 | + return ulRef; |
| 73 | + } |
| 74 | + |
| 75 | + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID** ppvInterface) { |
| 76 | + if (IID_IUnknown == riid) { |
| 77 | + AddRef(); |
| 78 | + *ppvInterface = (IUnknown*)this; |
| 79 | + } else if (__uuidof(IMMNotificationClient) == riid) { |
| 80 | + AddRef(); |
| 81 | + *ppvInterface = (IMMNotificationClient*)this; |
| 82 | + } else { |
| 83 | + *ppvInterface = NULL; |
| 84 | + return E_NOINTERFACE; |
| 85 | + } |
| 86 | + |
| 87 | + return S_OK; |
| 88 | + } |
| 89 | + |
| 90 | + HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) { |
| 91 | + return S_OK; |
| 92 | + } |
| 93 | + |
| 94 | + HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) { |
| 95 | + return S_OK; |
| 96 | + } |
| 97 | + |
| 98 | + HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) { |
| 99 | + return S_OK; |
| 100 | + } |
| 101 | + |
| 102 | + HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) { |
| 103 | + return S_OK; |
| 104 | + } |
| 105 | + |
| 106 | + void updateCurrentDeviceID() { |
| 107 | + if (_pDeviceEnum == nullptr) return; |
| 108 | + IMMDevice* _pDevice = nullptr; |
| 109 | + LPWSTR _deviceId = nullptr; |
| 110 | + HRESULT result = _pDeviceEnum->GetDefaultAudioEndpoint(eRender, eConsole, &_pDevice); |
| 111 | + if (SUCCEEDED(result) && _pDevice != nullptr) { |
| 112 | + result = _pDevice->GetId(&_deviceId); |
| 113 | + if (SUCCEEDED(result) && _deviceId != nullptr) { |
| 114 | + _currentDeviceID = _deviceId; |
| 115 | + CoTaskMemFree(_deviceId); |
| 116 | + } |
| 117 | + |
| 118 | + _pDevice->Release(); |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + private: |
| 123 | + |
| 124 | + std::wstring _currentDeviceID; |
| 125 | + IMMDeviceEnumerator* _pDeviceEnum; |
| 126 | + |
| 127 | + LONG _refCount; |
| 128 | +}; |
| 129 | + |
| 130 | +AudioFixClient* curAudioFix; |
| 131 | +') |
| 132 | +#end |
| 133 | +@:nullSafety |
| 134 | +final class AudioUtil |
| 135 | +{ |
| 136 | + #if (windows && cpp) |
| 137 | + /** |
| 138 | + * Signal dispatched when the current audio device is changed, after an attempted restart. |
| 139 | + */ |
| 140 | + public static final audioDeviceChangeSignal:FlxSignal = new FlxSignal(); |
| 141 | + |
| 142 | + /** |
| 143 | + * Whether the current audio device has changed. |
| 144 | + */ |
| 145 | + static var audioDeviceChanged(get, set):Bool; |
| 146 | + |
| 147 | + function new() {} |
| 148 | + |
| 149 | + /** |
| 150 | + * Gets whether the current audio device has changed. |
| 151 | + * |
| 152 | + * @return If the audio device has changed. |
| 153 | + */ |
| 154 | + public static function get_audioDeviceChanged():Bool |
| 155 | + { |
| 156 | + return cast untyped __cpp__('_audioDeviceChanged'); |
| 157 | + } |
| 158 | + |
| 159 | + static function set_audioDeviceChanged(v:Bool):Bool |
| 160 | + { |
| 161 | + untyped __cpp__('_audioDeviceChanged = (bool)v;'); |
| 162 | + return v; |
| 163 | + } |
| 164 | + |
| 165 | + static var initializedAudioFix:Bool = false; |
| 166 | + |
| 167 | + /** |
| 168 | + * Initializes the audio fix client to handle audio device changes. |
| 169 | + * This should be called once at the start of the application. |
| 170 | + */ |
| 171 | + public static function initAudioFix():Void |
| 172 | + { |
| 173 | + if (initializedAudioFix) |
| 174 | + { |
| 175 | + return; |
| 176 | + } |
| 177 | + |
| 178 | + LoggerUtil.log('Initializing audio device change detection'); |
| 179 | + |
| 180 | + untyped __cpp__('if (curAudioFix == nullptr) curAudioFix = new AudioFixClient();'); |
| 181 | + |
| 182 | + FlxG.signals.preUpdate.add(function():Void |
| 183 | + { |
| 184 | + if (audioDeviceChanged) |
| 185 | + { |
| 186 | + LoggerUtil.log('AUDIO DEVICE CHANGE DETECTED', INFO, false); |
| 187 | + LoggerUtil.log('Restarting audio system'); |
| 188 | + restartAudio(); |
| 189 | + } |
| 190 | + }); |
| 191 | + |
| 192 | + initializedAudioFix = true; |
| 193 | + } |
| 194 | + |
| 195 | + /** |
| 196 | + * Restarts the audio system and regenerates all sounds. |
| 197 | + */ |
| 198 | + public static function restartAudio():Void |
| 199 | + { |
| 200 | + final curSounds:Array<FlxSound> = new Array<FlxSound>(); |
| 201 | + |
| 202 | + @:privateAccess |
| 203 | + for (sound in FlxG.sound.list) |
| 204 | + { |
| 205 | + if (sound != null && sound.exists) |
| 206 | + { |
| 207 | + DataUtil.pushUniqueElement(curSounds, sound); |
| 208 | + } |
| 209 | + } |
| 210 | + for (sound in FlxG.sound.list) |
| 211 | + { |
| 212 | + if (sound != null && sound.exists) |
| 213 | + { |
| 214 | + DataUtil.pushUniqueElement(curSounds, sound); |
| 215 | + } |
| 216 | + } |
| 217 | + if (FlxG.sound.music != null && FlxG.sound.music.exists) |
| 218 | + { |
| 219 | + DataUtil.pushUniqueElement(curSounds, FlxG.sound.music); |
| 220 | + } |
| 221 | + |
| 222 | + final regenData:Array<RegenSoundData> = new Array<RegenSoundData>(); |
| 223 | + for (sound in curSounds) |
| 224 | + { |
| 225 | + regenData.push({sound: sound, isPlaying: sound.playing, time: sound.time}); |
| 226 | + sound.pause(); |
| 227 | + } |
| 228 | + |
| 229 | + AudioManager.shutdown(); |
| 230 | + AudioManager.init(); |
| 231 | + |
| 232 | + untyped __cpp__('if (curAudioFix != nullptr) curAudioFix->updateCurrentDeviceID();'); |
| 233 | + |
| 234 | + for (entry in regenData) |
| 235 | + { |
| 236 | + final sound:FlxSound = entry.sound; |
| 237 | + @:privateAccess regenSound(sound._sound); |
| 238 | + |
| 239 | + if (entry.isPlaying) |
| 240 | + { |
| 241 | + sound.play(true, entry.time); |
| 242 | + } |
| 243 | + |
| 244 | + sound.time = entry.time; |
| 245 | + } |
| 246 | + |
| 247 | + // TODO: Do garbage collection here. |
| 248 | + |
| 249 | + audioDeviceChanged = false; |
| 250 | + audioDeviceChangeSignal.dispatch(); |
| 251 | + } |
| 252 | + |
| 253 | + /** |
| 254 | + * Refreshes the sound buffer of a given `Sound`. |
| 255 | + * |
| 256 | + * @param sound The sound to refresh. |
| 257 | + */ |
| 258 | + public static function regenSound(sound:Null<Sound>):Void |
| 259 | + { |
| 260 | + if (sound != null) |
| 261 | + { |
| 262 | + @:privateAccess final curBuffer:Null<AudioBuffer> = sound.__buffer; |
| 263 | + if (curBuffer != null) |
| 264 | + { |
| 265 | + final newBuffer:AudioBuffer = new AudioBuffer(); |
| 266 | + newBuffer.bitsPerSample = curBuffer.bitsPerSample; |
| 267 | + newBuffer.channels = curBuffer.channels; |
| 268 | + newBuffer.data = UInt8Array.fromBytes(curBuffer.data.toBytes()); |
| 269 | + newBuffer.sampleRate = curBuffer.sampleRate; |
| 270 | + newBuffer.src = curBuffer.src; |
| 271 | + @:privateAccess sound.__buffer = newBuffer; |
| 272 | + } |
| 273 | + } |
| 274 | + } |
| 275 | + #end |
| 276 | +} |
0 commit comments