diff --git a/OpenUtau.Core/Audio/DummyAudioOutput.cs b/OpenUtau.Core/Audio/DummyAudioOutput.cs index aa5dbbb11..f755137e2 100644 --- a/OpenUtau.Core/Audio/DummyAudioOutput.cs +++ b/OpenUtau.Core/Audio/DummyAudioOutput.cs @@ -6,6 +6,7 @@ namespace OpenUtau.Audio { public class DummyAudioOutput : IAudioOutput { public PlaybackState PlaybackState => PlaybackState.Stopped; public int DeviceNumber => 0; + public event EventHandler DevicesChanged; public List GetOutputDevices() => new List(); public long GetPosition() => 0; public void Init(ISampleProvider sampleProvider) { } diff --git a/OpenUtau.Core/Audio/IAudioOutput.cs b/OpenUtau.Core/Audio/IAudioOutput.cs index 1b46aa23d..237ace918 100644 --- a/OpenUtau.Core/Audio/IAudioOutput.cs +++ b/OpenUtau.Core/Audio/IAudioOutput.cs @@ -16,6 +16,9 @@ public interface IAudioOutput { PlaybackState PlaybackState { get; } int DeviceNumber { get; } + // Raised when available output devices list changes (plug/unplug). + event EventHandler DevicesChanged; + void SelectDevice(Guid guid, int deviceNumber); void Init(ISampleProvider sampleProvider); void Pause(); diff --git a/OpenUtau.Core/Audio/MiniAudioOutput.cs b/OpenUtau.Core/Audio/MiniAudioOutput.cs index 378337cb9..df751bc89 100644 --- a/OpenUtau.Core/Audio/MiniAudioOutput.cs +++ b/OpenUtau.Core/Audio/MiniAudioOutput.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; using NAudio.Wave; using NAudio.Wave.SampleProviders; +using System.Threading; using OpenUtau.Core.Util; using Serilog; @@ -20,12 +21,24 @@ public class MiniAudioOutput : IAudioOutput, IDisposable { private bool eof; private List devices = new List(); + private readonly object devicesLock = new object(); + private Timer devicePollTimer = null; + public event EventHandler DevicesChanged; private IntPtr callbackPtr = IntPtr.Zero; private IntPtr nativeContext = IntPtr.Zero; private Guid selectedDevice = Guid.Empty; public MiniAudioOutput() { UpdateDeviceList(); + // Start device polling timer to detect plug/unplug on platforms + // where native library does not emit events. + devicePollTimer = new Timer(_ => { + try { + PollDeviceChange(); + } catch (Exception e) { + Log.Warning(e, "Device poll error"); + } + }, null, 2000, 2000); unsafe { var f = (ou_audio_data_callback_t)DataCallback; GCHandle.Alloc(f); @@ -49,9 +62,8 @@ public MiniAudioOutput() { } } } - - private void UpdateDeviceList() { - devices.Clear(); + private List GetDeviceListFromNative() { + var list = new List(); unsafe { const int kMaxCount = 128; ou_audio_device_info_t* device_infos = stackalloc ou_audio_device_info_t[kMaxCount]; @@ -73,7 +85,7 @@ private void UpdateDeviceList() { string name = (OS.IsWindows() && api != "WASAPI") ? Marshal.PtrToStringAnsi(device_infos[i].name) : Marshal.PtrToStringUTF8(device_infos[i].name); - devices.Add(new AudioOutputDevice { + list.Add(new AudioOutputDevice { name = name, api = api, deviceNumber = i, @@ -82,6 +94,37 @@ private void UpdateDeviceList() { } ou_free_audio_device_infos(device_infos, count); } + return list; + } + + private void UpdateDeviceList() { + var newList = GetDeviceListFromNative(); + lock (devicesLock) { + devices = newList; + } + } + + private void PollDeviceChange() { + var newList = GetDeviceListFromNative(); + bool changed = false; + lock (devicesLock) { + if (newList.Count != devices.Count) { + changed = true; + } else { + for (int i = 0; i < newList.Count; i++) { + if (newList[i].guid != devices[i].guid || newList[i].name != devices[i].name) { + changed = true; + break; + } + } + } + if (changed) { + devices = newList; + } + } + if (changed) { + DevicesChanged?.Invoke(this, EventArgs.Empty); + } } public void Init(ISampleProvider sampleProvider) { @@ -234,6 +277,12 @@ protected virtual void Dispose(bool disposing) { } // free unmanaged resources (unmanaged objects) and override finalizer + if (devicePollTimer != null) { + try { + devicePollTimer.Dispose(); + } catch { } + devicePollTimer = null; + } if (nativeContext != IntPtr.Zero) { ou_free_audio_device(nativeContext); nativeContext = IntPtr.Zero; diff --git a/OpenUtau/NAudioOutput.cs b/OpenUtau/NAudioOutput.cs index 695e36a8c..c9bf25f7b 100644 --- a/OpenUtau/NAudioOutput.cs +++ b/OpenUtau/NAudioOutput.cs @@ -9,6 +9,7 @@ namespace OpenUtau.App { public class NAudioOutput : DummyAudioOutput { } #else public class NAudioOutput : IAudioOutput { + public event EventHandler DevicesChanged; const int Channels = 2; private readonly object lockObj = new object(); diff --git a/OpenUtau/ViewModels/PreferencesViewModel.cs b/OpenUtau/ViewModels/PreferencesViewModel.cs index c7e987830..344f10528 100644 --- a/OpenUtau/ViewModels/PreferencesViewModel.cs +++ b/OpenUtau/ViewModels/PreferencesViewModel.cs @@ -133,6 +133,23 @@ public PreferencesViewModel() { if (device != null) { AudioOutputDevice = device; } + // Subscribe to device list changes to refresh UI when devices are plugged/unplugged. + try { + audioOutput.DevicesChanged += (s, e) => { + Avalonia.Threading.Dispatcher.UIThread.Post(() => { + try { + AudioOutputDevices = PlaybackManager.Inst.AudioOutput.GetOutputDevices(); + int curDeviceNumber = PlaybackManager.Inst.AudioOutput.DeviceNumber; + var cur = AudioOutputDevices.FirstOrDefault(d => d.deviceNumber == curDeviceNumber); + if (cur != null) { + AudioOutputDevice = cur; + } + } catch (Exception ex) { + Log.Warning(ex, "Failed to update audio device list on DevicesChanged"); + } + }); + }; + } catch { } } PreferPortAudio = Preferences.Default.PreferPortAudio ? 1 : 0; PlaybackAutoScroll = Preferences.Default.PlaybackAutoScroll;