Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 270 additions & 0 deletions OpenUtau.Core/Audio/SDL3AudioOutput.cs
Original file line number Diff line number Diff line change
@@ -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<AudioOutputDevice> devices = new List<AudioOutputDevice>();
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<AudioOutputDevice> 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
}
}
4 changes: 4 additions & 0 deletions OpenUtau.Core/OpenUtau.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
<PackageReference Include="NLayer.NAudioSupport" Version="1.4.0" />
<PackageReference Include="NumSharp" Version="0.30.0" />
<PackageReference Include="NWaves" Version="0.9.6" />
<PackageReference Include="SDL3-CS" Version="3.4.14" />
<PackageReference Include="SDL3-CS.Linux" Version="3.4.14" />
<PackageReference Include="SDL3-CS.MacOS" Version="3.4.14" />
<PackageReference Include="SDL3-CS.Windows" Version="3.4.14" />
<PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="SharpCompress" Version="0.48.1" />
<PackageReference Include="System.Buffers" Version="4.6.0" />
Expand Down
10 changes: 9 additions & 1 deletion OpenUtau.Core/Util/Preferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -180,7 +187,7 @@ public class SerializablePreferences {
public List<string> FavoriteSingers = new List<string>();
public Dictionary<string, string> SingerPhonemizers = new Dictionary<string, string>();
public List<string> RecentPhonemizers = new List<string>();
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;
Expand Down Expand Up @@ -265,6 +272,7 @@ public class SerializablePreferences {
// Legacy
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public int? Theme;
public bool? PreferPortAudio = false;
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions OpenUtau/Strings/Strings.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ Warning: this option removes custom presets.</system:String>
<system:String x:Key="prefs.playback.backend">Audio Backend</system:String>
<system:String x:Key="prefs.playback.backend.auto">Automatic</system:String>
<system:String x:Key="prefs.playback.backend.mini">MiniAudio</system:String>
<system:String x:Key="prefs.playback.backend.sdl3">SDL 3</system:String>
<system:String x:Key="prefs.playback.cursorposition">Auto-Scroll Margin</system:String>
<system:String x:Key="prefs.playback.device">Playback Device</system:String>
<system:String x:Key="prefs.playback.lockstarttime">On Pausing</system:String>
Expand Down
7 changes: 4 additions & 3 deletions OpenUtau/ViewModels/PreferencesViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion OpenUtau/Views/PreferencesDialog.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@
<ComboBox ItemsSource="{Binding AudioOutputDevices}" SelectedItem="{Binding AudioOutputDevice}" IsEnabled="{Binding !UseSystemDefaultDevice}"/>
<Button Content="{DynamicResource prefs.playback.test}" HorizontalAlignment="Stretch" Command="{Binding TestAudioOutputDevice}"/>
<TextBlock Text="{DynamicResource prefs.playback.backend}" Margin="0,10,0,0"/>
<ComboBox SelectedIndex="{Binding PreferPortAudio}">
<ComboBox SelectedIndex="{Binding AudioBackEnd}">
<ComboBoxItem Content="{DynamicResource prefs.playback.backend.auto}"/>
<ComboBoxItem Content="{DynamicResource prefs.playback.backend.mini}"/>
<ComboBoxItem Content="{DynamicResource prefs.playback.backend.sdl3}"/>
</ComboBox>
<TextBlock Classes="restart"/>
<TextBlock Text="{DynamicResource prefs.playback.lockstarttime}" Margin="0,10,0,0"/>
Expand Down
28 changes: 20 additions & 8 deletions OpenUtau/Views/SplashWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,29 @@ private void Start() {

private static void InitAudio() {
Log.Information("Initializing audio.");
if (!OS.IsWindows() || Core.Util.Preferences.Default.PreferPortAudio) {
if (OS.IsWindows() && Core.Util.Preferences.Default.AudioBackEnd == 0) {
try {
PlaybackManager.Inst.AudioOutput = new Audio.MiniAudioOutput();
} catch (Exception e1) {
Log.Error(e1, "Failed to init MiniAudio");
PlaybackManager.Inst.AudioOutput = new NAudioOutput();
} catch (Exception e0) {
Log.Error(e0, "Failed to init NAudio");
}
} else {
try {
PlaybackManager.Inst.AudioOutput = new NAudioOutput();
} catch (Exception e2) {
Log.Error(e2, "Failed to init NAudio");
switch (Core.Util.Preferences.Default.AudioBackEnd) {
case 0:
case 1:
try {
PlaybackManager.Inst.AudioOutput = new Audio.MiniAudioOutput();
} catch (Exception e1) {
Log.Error(e1, "Failed to init MiniAudio");
}
break;
case 2:
try {
PlaybackManager.Inst.AudioOutput = new Audio.SDL3AudioOutput();
} catch (Exception e2) {
Log.Error(e2, "Failed to init SDL3 Audio");
}
break;
}
}
Log.Information("Initialized audio.");
Expand Down
Loading