diff --git a/OpenUtau.Core/Audio/SDL3AudioOutput.cs b/OpenUtau.Core/Audio/SDL3AudioOutput.cs new file mode 100644 index 000000000..942f77e4a --- /dev/null +++ b/OpenUtau.Core/Audio/SDL3AudioOutput.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using NAudio.Wave; +using NAudio.Wave.SampleProviders; +using OpenUtau.Core.Util; +using Serilog; +using SDL3; + +namespace OpenUtau.Audio { + public class SDL3AudioOutput : IAudioOutput, IDisposable { + const int channels = 2; + const int sampleRate = 44100; + + public PlaybackState PlaybackState { get; private set; } + public int DeviceNumber { get; private set; } + + private ISampleProvider? sampleProvider; + private double currentTimeMs; + private bool eof; + + private List devices = new List(); + private Guid selectedDevice = Guid.Empty; + private IntPtr stream = IntPtr.Zero; + private readonly SDL.AudioStreamCallback callback; + private bool initializedSdl; + + public SDL3AudioOutput() { + callback = DataCallback; + // Ensure audio subsystem is initialized + var audioFlag = SDL.InitFlags.Audio; + if ((SDL.WasInit(audioFlag) & audioFlag) == 0) { + if (!SDL.Init(audioFlag)) { + Log.Error($"Failed to initialize SDL audio: {SDL.GetError()}"); + } + initializedSdl = true; + } + + UpdateDeviceList(); + if (Preferences.Default.UseSystemDefaultAudioDevice) { + OpenStream(SDL.AudioDeviceDefaultPlayback); + return; + } + + if (Guid.TryParse(Preferences.Default.PlaybackDevice, out var guid) + && devices.Any(d => d.guid == guid)) { + SelectDevice(guid, Preferences.Default.PlaybackDeviceNumber); + return; + } + + bool foundDevice = false; + foreach (var dev in devices) { + try { + SelectDevice(dev.guid, dev.deviceNumber); + foundDevice = true; + break; + } catch (Exception e) { + Log.Warning(e, $"Failed to init audio device {dev}"); + } + } + + if (!foundDevice) { + // Fall back to whatever SDL considers the default device. + OpenStream(SDL.AudioDeviceDefaultPlayback); + } + } + + private void UpdateDeviceList() { + devices.Clear(); + int count; + var arr = SDL.GetAudioPlaybackDevices(out count); + if (arr == null) { + Log.Error($"Failed to get SDL audio playback devices: {SDL.GetError()}"); + } + + for (int i = 0; i < arr.Length; i++) { + uint devid = arr[i]; + string name = SDL.GetAudioDeviceName(devid) ?? $"Device {devid}"; + devices.Add(new AudioOutputDevice { + name = name, + api = "SDL3", + deviceNumber = i, + guid = ToGuid(devid), + }); + } + } + + public void Init(ISampleProvider sampleProvider) { + PlaybackState = PlaybackState.Stopped; + eof = false; + currentTimeMs = 0; + if (sampleRate != sampleProvider.WaveFormat.SampleRate) { + sampleProvider = new WdlResamplingSampleProvider(sampleProvider, sampleRate); + } + this.sampleProvider = sampleProvider.ToStereo(); + } + + public void Play() { + if (stream == IntPtr.Zero) { + return; + } + if (PlaybackState != PlaybackState.Playing) { + if (!SDL.ResumeAudioStreamDevice(stream)) { + Log.Warning($"Failed to resume SDL audio device: {SDL.GetError()}"); + } + } + if (PlaybackState != PlaybackState.Paused) { + currentTimeMs = 0; + } + PlaybackState = PlaybackState.Playing; + eof = false; + } + + public void Pause() { + if (stream != IntPtr.Zero && PlaybackState == PlaybackState.Playing) { + SDL.PauseAudioStreamDevice(stream); + } + PlaybackState = PlaybackState.Paused; + } + + public void Stop() { + if (stream != IntPtr.Zero && PlaybackState == PlaybackState.Playing) { + SDL.PauseAudioStreamDevice(stream); + } + PlaybackState = PlaybackState.Stopped; + } + + float[] temp = new float[0]; + + private unsafe void DataCallback(IntPtr userdata, IntPtr streamPtr, int additionalAmount, int totalAmount) { + if (additionalAmount <= 0) { + return; + } + int samples = additionalAmount / sizeof(float); + if (temp.Length < samples) { + temp = new float[samples]; + } + int n = 0; + if (sampleProvider != null) { + n = sampleProvider.Read(temp, 0, samples); + } + + // If fewer samples read than requested, leave the remainder as zeros + if (n < samples) { + Array.Clear(temp, n, samples - n); + } + if (n == 0) { + eof = true; + } + + // Convert float[] to byte[] (SDL expects native float bytes for AudioF32) + int bytesLen = samples * sizeof(float); + var bytes = new byte[bytesLen]; + Buffer.BlockCopy(temp, 0, bytes, 0, Math.Min(n * sizeof(float), bytesLen)); + + if (!SDL.PutAudioStreamData(streamPtr, bytes, bytesLen)) { + Log.Warning($"Failed to put SDL audio stream data: {SDL.GetError()}"); + } + + currentTimeMs += (double)n / channels * 1000.0 / sampleRate; + } + + public long GetPosition() { + if (eof && PlaybackState == PlaybackState.Playing) { + Stop(); + } + // Return bytes position in the same convention the original used: + return (long)(Math.Max(0, currentTimeMs) / 1000 * sampleRate * 2 /* 16 bit */ * channels); + } + + public void SelectDevice(Guid guid, int deviceNumber) { + if (Preferences.Default.UseSystemDefaultAudioDevice) { + return; + } + if (selectedDevice != Guid.Empty && selectedDevice == guid) { + return; + } + for (int i = 0; i < devices.Count; i++) { + if (devices[i].guid == guid) { + deviceNumber = i; + break; + } + if (i == devices.Count - 1 && devices.Count > 0) { + guid = devices[0].guid; + deviceNumber = devices[0].deviceNumber; + } + } + bool wasPlaying = PlaybackState == PlaybackState.Playing; + OpenStream(FromGuid(guid)); + if (wasPlaying) { + SDL.ResumeAudioStreamDevice(stream); + } + selectedDevice = guid; + DeviceNumber = deviceNumber; + if (Preferences.Default.PlaybackDevice != guid.ToString()) { + Preferences.Default.PlaybackDevice = guid.ToString(); + Preferences.Default.PlaybackDeviceNumber = deviceNumber; + Preferences.Save(); + } + } + + public List GetOutputDevices() { + return devices; + } + + private void OpenStream(uint devid) { + CloseStream(); + + var spec = new SDL.AudioSpec { + Format = BitConverter.IsLittleEndian ? SDL.AudioFormat.AudioF32LE : SDL.AudioFormat.AudioF32BE, + Channels = channels, + Freq = sampleRate, + }; + + stream = SDL.OpenAudioDeviceStream(devid, in spec, callback, IntPtr.Zero); + if (stream == IntPtr.Zero) { + Log.Error($"Failed to open SDL audio device: {SDL.GetError()}"); + } + } + + private void CloseStream() { + if (stream != IntPtr.Zero) { + SDL.DestroyAudioStream(stream); + stream = IntPtr.Zero; + } + } + + private static Guid ToGuid(uint devid) { + var bytes = new byte[16]; + BitConverter.GetBytes(devid).CopyTo(bytes, 0); + return new Guid(bytes); + } + + private static uint FromGuid(Guid guid) { + var bytes = guid.ToByteArray(); + return BitConverter.ToUInt32(bytes, 0); + } + + + #region disposable + + private bool disposedValue; + + protected virtual void Dispose(bool disposing) { + if (!disposedValue) { + if (disposing) { + // dispose managed state (managed objects) + } + CloseStream(); + if (initializedSdl) { + SDL.QuitSubSystem(SDL.InitFlags.Audio); + initializedSdl = false; + } + disposedValue = true; + } + } + + ~SDL3AudioOutput() { + Dispose(disposing: false); + } + + public void Dispose() { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + } +} diff --git a/OpenUtau.Core/OpenUtau.Core.csproj b/OpenUtau.Core/OpenUtau.Core.csproj index 7b5ab3cae..6afd25d2c 100644 --- a/OpenUtau.Core/OpenUtau.Core.csproj +++ b/OpenUtau.Core/OpenUtau.Core.csproj @@ -22,6 +22,10 @@ + + + + diff --git a/OpenUtau.Core/Util/Preferences.cs b/OpenUtau.Core/Util/Preferences.cs index 79de981f3..faabe1350 100644 --- a/OpenUtau.Core/Util/Preferences.cs +++ b/OpenUtau.Core/Util/Preferences.cs @@ -120,6 +120,13 @@ private static void Load() { }; Default.Theme = null; } + if (Default.PreferPortAudio != null) { + Default.AudioBackEnd = Default.PreferPortAudio switch { + false => 0, + true => 1 + }; + Default.PreferPortAudio = null; + } } else { Reset(); } @@ -180,7 +187,7 @@ public class SerializablePreferences { public List FavoriteSingers = new List(); public Dictionary SingerPhonemizers = new Dictionary(); public List RecentPhonemizers = new List(); - public bool PreferPortAudio = false; + public uint AudioBackEnd = 0; // 0 = Automatic, 1 = MiniAudio, 2 = SDL public bool UseSystemDefaultAudioDevice = true; public double PlayPosMarkerMargin = 0.9; public int LockStartTime = 0; @@ -265,6 +272,7 @@ public class SerializablePreferences { // Legacy [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? Theme; + public bool? PreferPortAudio = false; } /// diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml index fe9830797..8a9fcb112 100644 --- a/OpenUtau/Strings/Strings.axaml +++ b/OpenUtau/Strings/Strings.axaml @@ -610,6 +610,7 @@ Warning: this option removes custom presets. Audio Backend Automatic MiniAudio + SDL 3 Auto-Scroll Margin Playback Device On Pausing diff --git a/OpenUtau/ViewModels/PreferencesViewModel.cs b/OpenUtau/ViewModels/PreferencesViewModel.cs index 30033cc38..57b026a75 100644 --- a/OpenUtau/ViewModels/PreferencesViewModel.cs +++ b/OpenUtau/ViewModels/PreferencesViewModel.cs @@ -56,6 +56,7 @@ public AudioOutputDevice? AudioOutputDevice { } [Reactive] public bool UseSystemDefaultDevice { get; set; } [Reactive] public int PreferPortAudio { get; set; } + [Reactive] public uint AudioBackEnd { get; set; } [Reactive] public int LockStartTime { get; set; } [Reactive] public int PlaybackAutoScroll { get; set; } [Reactive] public double PlayPosMarkerMargin { get; set; } @@ -140,7 +141,7 @@ public PreferencesViewModel() { } } UseSystemDefaultDevice = Preferences.Default.UseSystemDefaultAudioDevice; - PreferPortAudio = Preferences.Default.PreferPortAudio ? 1 : 0; + AudioBackEnd = Preferences.Default.AudioBackEnd; PlaybackAutoScroll = Preferences.Default.PlaybackAutoScroll; PlayPosMarkerMargin = Preferences.Default.PlayPosMarkerMargin; LockStartTime = Preferences.Default.LockStartTime; @@ -216,9 +217,9 @@ public PreferencesViewModel() { } } }); - this.WhenAnyValue(vm => vm.PreferPortAudio) + this.WhenAnyValue(vm => vm.AudioBackEnd) .Subscribe(index => { - Preferences.Default.PreferPortAudio = index > 0; + Preferences.Default.AudioBackEnd = index; Preferences.Save(); }); this.WhenAnyValue(vm => vm.PlaybackAutoScroll) diff --git a/OpenUtau/Views/PreferencesDialog.axaml b/OpenUtau/Views/PreferencesDialog.axaml index 661cff5b5..28063d585 100644 --- a/OpenUtau/Views/PreferencesDialog.axaml +++ b/OpenUtau/Views/PreferencesDialog.axaml @@ -100,9 +100,10 @@