同步更新,以及增加asio和独占wasapi的支持

This commit is contained in:
LA
2025-12-28 14:15:09 +08:00
parent 559b9cef88
commit 8527ffddb7
28 changed files with 773 additions and 173 deletions

View File

@@ -0,0 +1,40 @@
# Ensure we're running from the correct directory (location of this file).
Set-Location -LiteralPath $PSScriptRoot
$ErrorActionPreference = 'Stop'
$zipPath = Join-Path $env:TEMP 'bassasio.zip'
try {
Invoke-WebRequest -Uri 'https://www.un4seen.com/stuff/bassasio.zip' -OutFile $zipPath
$destX64 = Join-Path $PSScriptRoot 'runtimes\win-x64\native'
$destX86 = Join-Path $PSScriptRoot 'runtimes\win-x86\native'
New-Item -ItemType Directory -Force -Path $destX64 | Out-Null
New-Item -ItemType Directory -Force -Path $destX86 | Out-Null
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [System.IO.Compression.ZipFile]::OpenRead($zipPath)
try {
foreach ($entry in $archive.Entries) {
switch ($entry.FullName) {
'x64/bassasio.dll' {
$out = Join-Path $destX64 'bassasio.dll'
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $out, $true)
}
'bassasio.dll' {
$out = Join-Path $destX86 'bassasio.dll'
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $out, $true)
}
}
}
}
finally {
$archive.Dispose()
}
}
finally {
if (Test-Path $zipPath) { Remove-Item -Force $zipPath }
}

View File

@@ -74,5 +74,10 @@ unzip -qjo bassmix24-android.zip libs/arm64-v8a/* -d ../osu.Framework.Android/ar
unzip -qjo bassmix24-android.zip libs/armeabi-v7a/* -d ../osu.Framework.Android/armeabi-v7a/
unzip -qjo bassmix24-android.zip libs/x86/* -d ../osu.Framework.Android/x86/
# bassasio (Windows only)
curl -Lso bassasio.zip https://www.un4seen.com/stuff/bassasio.zip
unzip -qjo bassasio.zip x64/bassasio.dll -d runtimes/win-x64/native/
unzip -qjo bassasio.zip bassasio.dll -d runtimes/win-x86/native/
# clean up
rm bass*.zip

View File

@@ -25,14 +25,14 @@ namespace osu.Framework.Tests.Audio
private volatile bool simulateLoss;
protected override bool InitBass(int device)
protected override bool InitBass(int device, AudioOutputMode outputMode, int? asioDeviceIndex)
{
try
{
if (simulateLoss)
return device == Bass.NoSoundDevice && base.InitBass(device);
return device == Bass.NoSoundDevice && base.InitBass(device, outputMode, asioDeviceIndex);
return base.InitBass(device);
return base.InitBass(device, outputMode, asioDeviceIndex);
}
finally
{

View File

@@ -436,6 +436,7 @@ namespace osu.Framework.Tests.Clocks
[TestCase(1)]
[TestCase(10)]
[TestCase(50)]
[FlakyTest]
public void TestNoDecoupledDrift(int updateRate)
{
var stopwatch = new StopwatchClock();

View File

@@ -254,6 +254,7 @@ namespace osu.Framework.Tests.Clocks
[TestCase(1)]
[TestCase(10)]
[TestCase(50)]
[FlakyTest]
public void TestNoInterpolationDrift(int updateRate)
{
var stopwatch = new StopwatchClock();

View File

@@ -108,6 +108,7 @@ namespace osu.Framework.Tests.Graphics
}
[Test]
[FlakyTest]
public void TestReadSaturated()
{
var tripleBuffer = new TripleBuffer<TestObject>();

View File

@@ -79,7 +79,7 @@ namespace osu.Framework.Tests.Visual.Localisation
{
}
public string? Get(string key) => translations.TryGetValue(key, out string? value) ? value : null;
public string? Get(string key) => translations.GetValueOrDefault(key);
public Task<string?> GetAsync(string key, CancellationToken cancellationToken = default) => Task.FromResult(Get(key));

View File

@@ -17,7 +17,7 @@ namespace osu.Framework.iOS
/// <summary>
/// Base <see cref="UIApplicationDelegate"/> implementation for osu!framework applications.
/// </summary>
public abstract class GameApplicationDelegate : UIApplicationDelegate
public abstract class GameApplicationDelegate : UIResponder, IUIApplicationDelegate
{
internal event Action<string>? DragDrop;
@@ -27,7 +27,7 @@ namespace osu.Framework.iOS
public IOSGameHost Host { get; private set; } = null!;
public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
public virtual bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
mapLibraryNames();
@@ -46,7 +46,7 @@ namespace osu.Framework.iOS
return true;
}
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
public virtual bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
// copied verbatim from SDL: https://github.com/libsdl-org/SDL/blob/d252a8fe126b998bd1b0f4e4cf52312cd11de378/src/video/uikit/SDL_uikitappdelegate.m#L508-L535
// the hope is that the SDL app delegate class does not have such handling exist there, but Apple does not provide a corresponding notification to make that possible.
@@ -55,6 +55,17 @@ namespace osu.Framework.iOS
return true;
}
public override void BuildMenu(IUIMenuBuilder builder)
{
base.BuildMenu(builder);
// Remove useless menus on iPadOS. This makes it almost match macOS, displaying only "Window" and "Help".
builder.RemoveMenu(UIMenuIdentifier.File.GetConstant());
builder.RemoveMenu(UIMenuIdentifier.Edit.GetConstant());
builder.RemoveMenu(UIMenuIdentifier.Format.GetConstant());
builder.RemoveMenu(UIMenuIdentifier.View.GetConstant());
}
/// <summary>
/// Creates the <see cref="Game"/> class to launch.
/// </summary>

View File

@@ -38,7 +38,7 @@ namespace osu.Framework.iOS
public override void Create()
{
SDL_SetHint(SDL_HINT_IOS_HIDE_HOME_INDICATOR, "2"u8);
SDL_SetHint(SDL_HINT_IOS_HIDE_HOME_INDICATOR, "2"u8).LogErrorIfFailed();
base.Create();
@@ -59,7 +59,7 @@ namespace osu.Framework.iOS
// iOS may be a good forward direction if this ever comes up, as a user may see a potentially higher
// frame rate with multi-threaded mode turned on, but it is going to give them worse input latency
// and higher power usage.
SDL_SetiOSAnimationCallback(SDLWindowHandle, 1, &runFrame, ObjectHandle.Handle);
SDL_SetiOSAnimationCallback(SDLWindowHandle, 1, &runFrame, ObjectHandle.Handle).ThrowIfFailed();
}
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]

View File

@@ -0,0 +1,115 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace osu.Framework.Audio.Asio
{
internal static class BassAsio
{
private const string dll = "bassasio";
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
internal delegate int AsioProcedure([MarshalAs(UnmanagedType.Bool)] bool input, int channel, IntPtr buffer, int length, IntPtr user);
[StructLayout(LayoutKind.Sequential)]
private struct AsioDeviceInfoNative
{
public IntPtr Name;
public IntPtr Driver;
public int Flags;
}
internal readonly struct AsioDeviceInfo
{
public AsioDeviceInfo(int index, string name)
{
Index = index;
Name = name;
}
public int Index { get; }
public string Name { get; }
}
[StructLayout(LayoutKind.Sequential)]
internal struct AsioInfo
{
public int Inputs;
public int Outputs;
public int Format;
public double BufferLength;
public double MinBufferLength;
public double MaxBufferLength;
public double PreferredBufferLength;
public double Granularity;
}
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_GetDeviceInfo")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool getDeviceInfo(int device, out AsioDeviceInfoNative info);
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_Init")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool Init(int device, int flags = 0);
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_Free")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool Free();
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_Start")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool Start(int bufferLength, int threads = 0);
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_Stop")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool Stop();
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_GetInfo")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetInfo(out AsioInfo info);
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_GetRate")]
internal static extern double GetRate();
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_SetRate")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetRate(double rate);
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_ChannelEnable")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool ChannelEnable([MarshalAs(UnmanagedType.Bool)] bool input, int channel, AsioProcedure procedure, IntPtr user);
internal static IEnumerable<AsioDeviceInfo> EnumerateDevices(int maxDevices = 64)
{
for (int i = 0; i < maxDevices; i++)
{
if (!getDeviceInfo(i, out var info))
yield break;
string name = Marshal.PtrToStringAnsi(info.Name);
if (!string.IsNullOrEmpty(name))
yield return new AsioDeviceInfo(i, name);
}
}
internal static bool TryFindDeviceIndexByName(string name, out int index)
{
foreach (var d in EnumerateDevices())
{
if (d.Name == name)
{
index = d.Index;
return true;
}
}
index = -1;
return false;
}
}
}

View File

@@ -13,6 +13,7 @@ using JetBrains.Annotations;
using ManagedBass;
using ManagedBass.Fx;
using ManagedBass.Mix;
using osu.Framework.Audio.Asio;
using osu.Framework.Audio.Mixing;
using osu.Framework.Audio.Mixing.Bass;
using osu.Framework.Audio.Sample;
@@ -80,7 +81,7 @@ namespace osu.Framework.Audio
/// Consumers should provide a "Default" audio device entry which sets <see cref="AudioDevice"/> to an empty string.
/// </para>
/// </remarks>
public IEnumerable<string> AudioDeviceNames => audioDeviceNames;
public IEnumerable<string> AudioDeviceNames => getAudioDeviceEntries();
/// <summary>
/// Is fired whenever a new audio device is discovered and provides its name.
@@ -153,7 +154,22 @@ namespace osu.Framework.Audio
// Mutated by multiple threads, must be thread safe.
private ImmutableArray<DeviceInfo> audioDevices = ImmutableArray<DeviceInfo>.Empty;
private ImmutableList<string> audioDeviceNames = ImmutableList<string>.Empty;
private ImmutableList<string> bassDeviceNames = ImmutableList<string>.Empty;
private const string type_bass = "BASS";
private const string type_wasapi_shared = "WASAPI Shared";
private const string type_wasapi_exclusive = "WASAPI Exclusive";
private const string type_asio = "ASIO";
private bool syncingSelection;
protected enum AudioOutputMode
{
Default,
WasapiShared,
WasapiExclusive,
Asio,
}
private Scheduler scheduler => thread.Scheduler;
@@ -196,7 +212,15 @@ namespace osu.Framework.Audio
}
AudioDevice.ValueChanged += _ => scheduler.AddOnce(initCurrentDevice);
UseExperimentalWasapi.ValueChanged += _ => scheduler.AddOnce(initCurrentDevice);
UseExperimentalWasapi.ValueChanged += _ => scheduler.AddOnce(() =>
{
// Keep checkbox meaningful by mapping it onto the dropdown selection when possible.
bool selectionChanged = syncSelectionFromExperimentalFlag();
// If selection didn't change (eg. Default), we still need to reinitialise.
if (!selectionChanged)
initCurrentDevice();
});
// initCurrentDevice not required for changes to `GlobalMixerHandle` as it is only changed when experimental wasapi is toggled (handled above).
GlobalMixerHandle.ValueChanged += handle => usingGlobalMixer.Value = handle.NewValue.HasValue;
@@ -267,8 +291,7 @@ namespace osu.Framework.Audio
/// Channels removed from this <see cref="AudioMixer"/> fall back to the global <see cref="SampleMixer"/>.
/// </remarks>
/// <param name="identifier">An identifier displayed on the audio mixer visualiser.</param>
public AudioMixer CreateAudioMixer(string identifier = default) =>
createAudioMixer(SampleMixer, !string.IsNullOrEmpty(identifier) ? identifier : $"user #{Interlocked.Increment(ref userMixerID)}");
public AudioMixer CreateAudioMixer(string identifier = default) => createAudioMixer(SampleMixer, !string.IsNullOrEmpty(identifier) ? identifier : $"user #{Interlocked.Increment(ref userMixerID)}");
private AudioMixer createAudioMixer(AudioMixer fallbackMixer, string identifier)
{
@@ -332,24 +355,61 @@ namespace osu.Framework.Audio
/// </summary>
private void initCurrentDevice()
{
string deviceName = AudioDevice.Value;
var (mode, deviceName, asioIndex) = parseSelection(AudioDevice.Value);
// keep legacy setting and dropdown selection in sync.
if (!syncingSelection)
{
syncingSelection = true;
try
{
switch (mode)
{
case AudioOutputMode.WasapiShared:
UseExperimentalWasapi.Value = true;
break;
case AudioOutputMode.Default:
case AudioOutputMode.WasapiExclusive:
case AudioOutputMode.Asio:
// Exclusive/ASIO are separate modes; keep the experimental flag off to avoid confusion.
UseExperimentalWasapi.Value = false;
break;
}
}
finally
{
syncingSelection = false;
}
}
// try using the specified device
int deviceIndex = audioDeviceNames.FindIndex(d => d == deviceName);
if (deviceIndex >= 0 && trySetDevice(BASS_INTERNAL_DEVICE_COUNT + deviceIndex)) return;
if (mode == AudioOutputMode.Asio)
{
// ASIO output still requires BASS to be initialised, but output is performed by BassAsio.
// Use the OS default BASS device as a fallback initialisation target.
if (trySetDevice(bass_default_device, mode, asioIndex)) return;
}
else
{
// try using the specified device
int deviceIndex = bassDeviceNames.FindIndex(d => d == deviceName);
if (deviceIndex >= 0 && trySetDevice(BASS_INTERNAL_DEVICE_COUNT + deviceIndex, mode, asioIndex)) return;
}
// try using the system default if there is any device present.
// mobiles are an exception as the built-in speakers may not be provided as an audio device name,
// but they are still provided by BASS under the internal device name "Default".
if ((audioDeviceNames.Count > 0 || RuntimeInfo.IsMobile) && trySetDevice(bass_default_device)) return;
if ((bassDeviceNames.Count > 0 || RuntimeInfo.IsMobile) && trySetDevice(bass_default_device, mode, asioIndex)) return;
// no audio devices can be used, so try using Bass-provided "No sound" device as last resort.
trySetDevice(Bass.NoSoundDevice);
trySetDevice(Bass.NoSoundDevice, AudioOutputMode.Default, null);
// we're boned. even "No sound" device won't initialise.
return;
bool trySetDevice(int deviceId)
bool trySetDevice(int deviceId, AudioOutputMode outputMode, int? asioDeviceIndex)
{
var device = audioDevices.ElementAtOrDefault(deviceId);
@@ -362,7 +422,7 @@ namespace osu.Framework.Audio
return false;
// initialize new device
if (!InitBass(deviceId))
if (!InitBass(deviceId, outputMode, asioDeviceIndex))
return false;
//we have successfully initialised a new device.
@@ -372,12 +432,53 @@ namespace osu.Framework.Audio
}
}
private bool syncSelectionFromExperimentalFlag()
{
if (syncingSelection)
return false;
// Do not attempt to remap explicit exclusive/ASIO selections.
if (tryParseSuffixed(AudioDevice.Value, type_wasapi_exclusive, out _) || tryParseSuffixed(AudioDevice.Value, type_asio, out _))
return false;
if (string.IsNullOrEmpty(AudioDevice.Value))
return false;
string baseName;
if (tryParseSuffixed(AudioDevice.Value, type_bass, out string parsed))
baseName = parsed;
else if (tryParseSuffixed(AudioDevice.Value, type_wasapi_shared, out parsed))
baseName = parsed;
else
// Legacy un-suffixed device name.
baseName = AudioDevice.Value;
string desired = formatEntry(baseName, UseExperimentalWasapi.Value ? type_wasapi_shared : type_bass);
if (AudioDevice.Value == desired)
return false;
syncingSelection = true;
try
{
AudioDevice.Value = desired;
return true;
}
finally
{
syncingSelection = false;
}
}
/// <summary>
/// This method calls <see cref="Bass.Init(int, int, DeviceInitFlags, IntPtr, IntPtr)"/>.
/// It can be overridden for unit testing.
/// </summary>
/// <param name="device">The device to initialise.</param>
protected virtual bool InitBass(int device)
/// <param name="outputMode">The output mode to use for playback.</param>
/// <param name="asioDeviceIndex">When <paramref name="outputMode"/> is ASIO, the selected ASIO device index.</param>
protected virtual bool InitBass(int device, AudioOutputMode outputMode, int? asioDeviceIndex)
{
// this likely doesn't help us but also doesn't seem to cause any issues or any cpu increase.
Bass.UpdatePeriod = 5;
@@ -402,25 +503,15 @@ namespace osu.Framework.Audio
// - This disables support for ItunSMPB tag parsing to match previous expectations.
// - This also disables a change which assumes a 529 sample (2116 byte in stereo 16-bit) delay if the MP3 file doesn't specify one.
// (That was added in Bass for more consistent results across platforms and standard/mp3-free BASS versions, because OSX/iOS's MP3 decoder always removes 529 samples)
Bass.Configure((ManagedBass.Configuration)68, 1);
// Bass.Configure((ManagedBass.Configuration)68, 1);
// Disable BASS_CONFIG_DEV_TIMEOUT flag to keep BASS audio output from pausing on device processing timeout.
// See https://www.un4seen.com/forum/?topic=19601 for more information.
Bass.Configure((ManagedBass.Configuration)70, false);
bool success = attemptInit();
if (success || !UseExperimentalWasapi.Value)
return success;
// in the case we're using experimental WASAPI, give a second chance of initialisation by forcefully disabling it.
Logger.Log($"BASS device {device} failed to initialise with experimental WASAPI, disabling", level: LogLevel.Error);
UseExperimentalWasapi.Value = false;
return attemptInit();
bool attemptInit()
{
bool innerSuccess = thread.InitDevice(device, UseExperimentalWasapi.Value);
bool innerSuccess = thread.InitDevice(device, toThreadOutputMode(outputMode), asioDeviceIndex);
bool alreadyInitialised = Bass.LastError == Errors.Already;
if (alreadyInitialised)
@@ -449,6 +540,26 @@ namespace osu.Framework.Audio
return true;
}
return attemptInit();
}
private static AudioThread.AudioThreadOutputMode toThreadOutputMode(AudioOutputMode mode)
{
switch (mode)
{
case AudioOutputMode.WasapiShared:
return AudioThread.AudioThreadOutputMode.WasapiShared;
case AudioOutputMode.WasapiExclusive:
return AudioThread.AudioThreadOutputMode.WasapiExclusive;
case AudioOutputMode.Asio:
return AudioThread.AudioThreadOutputMode.Asio;
default:
return AudioThread.AudioThreadOutputMode.Default;
}
}
private void syncAudioDevices()
@@ -458,8 +569,8 @@ namespace osu.Framework.Audio
// Bass should always be providing "No sound" and "Default" device.
Trace.Assert(audioDevices.Length >= BASS_INTERNAL_DEVICE_COUNT, "Bass did not provide any audio devices.");
var oldDeviceNames = audioDeviceNames;
var newDeviceNames = audioDeviceNames = audioDevices.Skip(BASS_INTERNAL_DEVICE_COUNT).Where(d => d.IsEnabled).Select(d => d.Name).ToImmutableList();
var oldDeviceNames = bassDeviceNames;
var newDeviceNames = bassDeviceNames = audioDevices.Skip(BASS_INTERNAL_DEVICE_COUNT).Where(d => d.IsEnabled).Select(d => d.Name).ToImmutableList();
scheduler.Add(() =>
{
@@ -485,6 +596,95 @@ namespace osu.Framework.Audio
}
}
private IEnumerable<string> getAudioDeviceEntries()
{
var entries = new List<string>();
// Base BASS devices.
entries.AddRange(bassDeviceNames.Select(d => formatEntry(d, type_bass)));
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
// WASAPI variants for each BASS device.
entries.AddRange(bassDeviceNames.Select(d => formatEntry(d, type_wasapi_shared)));
entries.AddRange(bassDeviceNames.Select(d => formatEntry(d, type_wasapi_exclusive)));
// ASIO drivers.
try
{
foreach (var device in BassAsio.EnumerateDevices())
entries.Add(formatEntry(device.Name, type_asio));
}
catch
{
// ASIO is optional and may not be available.
}
}
return entries;
}
private static string formatEntry(string name, string type) => $"{name} ({type})";
private (AudioOutputMode mode, string deviceName, int? asioDeviceIndex) parseSelection(string selection)
{
// Default device.
if (string.IsNullOrEmpty(selection))
{
return (UseExperimentalWasapi.Value && RuntimeInfo.OS == RuntimeInfo.Platform.Windows
? AudioOutputMode.WasapiShared
: AudioOutputMode.Default, string.Empty, null);
}
if (tryParseSuffixed(selection, type_bass, out string name))
return (AudioOutputMode.Default, name, null);
if (tryParseSuffixed(selection, type_wasapi_shared, out name))
return (AudioOutputMode.WasapiShared, name, null);
if (tryParseSuffixed(selection, type_wasapi_exclusive, out name))
return (AudioOutputMode.WasapiExclusive, name, null);
if (tryParseSuffixed(selection, type_asio, out name))
{
int? index = null;
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
try
{
if (BassAsio.TryFindDeviceIndexByName(name, out int found))
index = found;
}
catch
{
}
}
return (AudioOutputMode.Asio, name, index);
}
// Legacy value (raw BASS device name). Keep old behaviour: the experimental flag decides shared WASAPI.
if (UseExperimentalWasapi.Value && RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
return (AudioOutputMode.WasapiShared, selection, null);
return (AudioOutputMode.Default, selection, null);
}
private static bool tryParseSuffixed(string value, string type, out string baseName)
{
string suffix = $" ({type})";
if (value.EndsWith(suffix, StringComparison.Ordinal))
{
baseName = value[..^suffix.Length];
return true;
}
baseName = string.Empty;
return false;
}
/// <summary>
/// Check whether any audio device changes have occurred.
///
@@ -535,7 +735,13 @@ namespace osu.Framework.Audio
protected virtual bool IsCurrentDeviceValid()
{
var device = audioDevices.ElementAtOrDefault(Bass.CurrentDevice);
bool isFallback = string.IsNullOrEmpty(AudioDevice.Value) ? !device.IsDefault : device.Name != AudioDevice.Value;
var (mode, selectedName, _) = parseSelection(AudioDevice.Value);
// ASIO output selection does not map to a BASS device name; just ensure we're initialised.
if (mode == AudioOutputMode.Asio)
return device.IsEnabled && device.IsInitialized;
bool isFallback = string.IsNullOrEmpty(selectedName) ? !device.IsDefault : device.Name != selectedName;
return device.IsEnabled && device.IsInitialized && !isFallback;
}

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using osu.Framework.Graphics.Rendering;
using osu.Framework.IO.Stores;
@@ -55,7 +56,7 @@ namespace osu.Framework.Graphics.Shaders
/// <param name="fragment">The fragment shader name.</param>
/// <returns>A cached <see cref="IShader"/> instance, if existing.</returns>
public virtual IShader? GetCachedShader(string vertex, string fragment)
=> shaderCache.TryGetValue((vertex, fragment), out IShader? shader) ? shader : null;
=> shaderCache.GetValueOrDefault((vertex, fragment));
/// <summary>
/// Attempts to retrieve an already-cached shader part.
@@ -63,7 +64,7 @@ namespace osu.Framework.Graphics.Shaders
/// <param name="name">The name of the shader part.</param>
/// <returns>A cached <see cref="IShaderPart"/> instance, if existing.</returns>
public virtual IShaderPart? GetCachedShaderPart(string name)
=> partCache.TryGetValue(name, out IShaderPart? part) ? part : null;
=> partCache.GetValueOrDefault(name);
/// <summary>
/// Attempts to retrieve the raw data for a shader file.

View File

@@ -368,10 +368,7 @@ namespace osu.Framework.Graphics.Transforms
return min;
}
if (lastAppliedTransformIndices.TryGetValue(targetMember, out int val))
return val;
return 0;
return lastAppliedTransformIndices.GetValueOrDefault(targetMember, 0);
}
/// <summary>

View File

@@ -12,7 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Input.States;
using osu.Framework.Lists;
using osu.Framework.Logging;
// using osu.Framework.Logging;
using osuTK;
namespace osu.Framework.Input.Bindings

View File

@@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Extensions.TypeExtensions;
// using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Input.States;
using osuTK.Input;

View File

@@ -209,7 +209,7 @@ namespace osu.Framework.Input
/// <param name="button">The button to find the manager for.</param>
/// <returns>The <see cref="MouseButtonEventManager"/>.</returns>
public MouseButtonEventManager GetButtonEventManagerFor(MouseButton button) =>
mouseButtonEventManagers.TryGetValue(button, out var manager) ? manager : null;
mouseButtonEventManagers.GetValueOrDefault(button);
/// <summary>
/// Create a <see cref="KeyEventManager"/> for a specified key.

View File

@@ -11,7 +11,7 @@ namespace osu.Framework.Platform.Linux
public SDL3LinuxWindow(GraphicsSurfaceType surfaceType, string appName, bool bypassCompositor)
: base(surfaceType, appName)
{
SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, bypassCompositor ? "1"u8 : "0"u8);
SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, bypassCompositor ? "1"u8 : "0"u8).LogErrorIfFailed();
}
}
}

View File

@@ -42,7 +42,7 @@ namespace osu.Framework.Platform.SDL3
// assume that empty text means no text.
public override string? GetText() => SDL_HasClipboardText() ? SDL_GetClipboardText() : null;
public override void SetText(string text) => SDL_SetClipboardText(text);
public override void SetText(string text) => SDL_SetClipboardText(text).LogErrorIfFailed();
public override Image<TPixel>? GetImage<TPixel>()
{

View File

@@ -17,9 +17,9 @@ namespace osu.Framework.Platform.SDL3
// this reset is required even on changing from one fullscreen resolution to another.
// if it is not included, the GL context will not get the correct size.
// this is mentioned by multiple sources as an SDL issue, which seems to resolve by similar means (see https://discourse.libsdl.org/t/sdl-setwindowsize-does-not-work-in-fullscreen/20711/4).
SDL_SetWindowBordered(SDLWindowHandle, true);
SDL_SetWindowFullscreen(SDLWindowHandle, false);
SDL_RestoreWindow(SDLWindowHandle);
SDL_SetWindowBordered(SDLWindowHandle, true).LogErrorIfFailed();
SDL_SetWindowFullscreen(SDLWindowHandle, false).LogErrorIfFailed();
SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed();
base.UpdateWindowStateAndSize(state, display, displayMode);
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using static SDL.SDL3;
namespace osu.Framework.Platform.SDL3
{
internal class SDL3Exception(string? expression) : Exception($"{SDL_GetError()} (at {expression})");
}

View File

@@ -1,12 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Drawing;
using System.Runtime.CompilerServices;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.StateChanges;
using osu.Framework.Logging;
using osuTK.Input;
using SDL;
using static SDL.SDL3;
@@ -1045,7 +1048,7 @@ namespace osu.Framework.Platform.SDL3
{
int bpp;
uint unused;
SDL_GetMasksForPixelFormat(mode.format, &bpp, &unused, &unused, &unused, &unused);
SDL_GetMasksForPixelFormat(mode.format, &bpp, &unused, &unused, &unused, &unused).ThrowIfFailed();
return new DisplayMode(SDL_GetPixelFormatName(mode.format), new Size(mode.w, mode.h), bpp, mode.refresh_rate, displayIndex);
}
@@ -1144,5 +1147,100 @@ namespace osu.Framework.Platform.SDL3
SDL_ClearError();
return error;
}
private static void logError(string? expression)
{
Logger.Log($"SDL error: {SDL_GetError()}");
if (!string.IsNullOrEmpty(expression))
Logger.Log($"at {expression}");
}
public static SDLBool LogErrorIfFailed(this SDLBool returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (!returnValue)
logError(expression);
return returnValue;
}
public static void ThrowIfFailed(this SDLBool returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (!returnValue)
throw new SDL3Exception(expression);
}
public static SDL_PropertiesID ThrowIfFailed(this SDL_PropertiesID returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == 0)
throw new SDL3Exception(expression);
return returnValue;
}
public static int LogErrorIfFailed(this int returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == -1)
logError(expression);
return returnValue;
}
public static IntPtr ThrowIfFailed(this IntPtr returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == IntPtr.Zero)
throw new SDL3Exception(expression);
return returnValue;
}
public static SDLArray<T>? LogErrorIfFailed<T>(this SDLArray<T>? returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
where T : unmanaged
{
if (returnValue == null)
logError(expression);
return returnValue;
}
public static SDL_PenDeviceType ThrowIfFailed(this SDL_PenDeviceType returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == SDL_PenDeviceType.SDL_PEN_DEVICE_TYPE_INVALID)
throw new SDL3Exception(expression);
return returnValue;
}
public static string? LogErrorIfFailed(this string? returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == null)
logError(expression);
return returnValue;
}
public static SDL_DisplayID LogErrorIfFailed(this SDL_DisplayID returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == 0)
logError(expression);
return returnValue;
}
public static SDL_DisplayID ThrowIfFailed(this SDL_DisplayID returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == 0)
throw new SDL3Exception(expression);
return returnValue;
}
public static unsafe T* LogErrorIfFailed<T>(T* returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
where T : unmanaged
{
if (returnValue == null)
logError(expression);
return returnValue;
}
}
}

View File

@@ -33,12 +33,12 @@ namespace osu.Framework.Platform.SDL3
switch (surfaceType)
{
case GraphicsSurfaceType.OpenGL:
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_RED_SIZE, 8);
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_GREEN_SIZE, 8);
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_BLUE_SIZE, 8);
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_ACCUM_ALPHA_SIZE, 0);
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_DEPTH_SIZE, 16);
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_RED_SIZE, 8).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_GREEN_SIZE, 8).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_BLUE_SIZE, 8).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_ACCUM_ALPHA_SIZE, 0).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_DEPTH_SIZE, 16).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_STENCIL_SIZE, 8).ThrowIfFailed();
break;
case GraphicsSurfaceType.Vulkan:
@@ -60,7 +60,7 @@ namespace osu.Framework.Platform.SDL3
public Size GetDrawableSize()
{
int width, height;
SDL_GetWindowSizeInPixels(window.SDLWindowHandle, &width, &height);
SDL_GetWindowSizeInPixels(window.SDLWindowHandle, &width, &height).ThrowIfFailed();
return new Size(width, height);
}
@@ -70,19 +70,19 @@ namespace osu.Framework.Platform.SDL3
{
if (RuntimeInfo.IsMobile)
{
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLProfile.SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLProfile.SDL_GL_CONTEXT_PROFILE_ES).ThrowIfFailed();
// Minimum OpenGL version for ES profile:
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 0).ThrowIfFailed();
}
else
{
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLProfile.SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLProfile.SDL_GL_CONTEXT_PROFILE_CORE).ThrowIfFailed();
// Minimum OpenGL version for core profile:
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 2).ThrowIfFailed();
}
context = SDL_GL_CreateContext(window.SDLWindowHandle);
@@ -90,7 +90,7 @@ namespace osu.Framework.Platform.SDL3
if (context == null)
throw new InvalidOperationException($"Failed to create an SDL3 GL context ({SDL_GetError()})");
SDL_GL_MakeCurrent(window.SDLWindowHandle, context);
SDL_GL_MakeCurrent(window.SDLWindowHandle, context).ThrowIfFailed();
loadBindings();
}
@@ -126,29 +126,13 @@ namespace osu.Framework.Platform.SDL3
string? str = Marshal.PtrToStringAnsi(new IntPtr(ptr));
Debug.Assert(str != null);
entryPointsInstance[i] = getProcAddress(str);
entryPointsInstance[i] = SDL_GL_GetProcAddress(str);
}
}
pointsInfo.SetValue(bindings, entryPointsInstance);
}
private IntPtr getProcAddress(string symbol)
{
const SDL_LogCategory error_category = SDL_LogCategory.SDL_LOG_CATEGORY_ERROR;
SDL_LogPriority oldPriority = SDL_GetLogPriority(error_category);
// Prevent logging calls to SDL_GL_GetProcAddress() that fail on systems which don't have the requested symbol (typically macOS).
SDL_SetLogPriority(error_category, SDL_LogPriority.SDL_LOG_PRIORITY_INFO);
IntPtr ret = SDL_GL_GetProcAddress(symbol);
// Reset the logging behaviour.
SDL_SetLogPriority(error_category, oldPriority);
return ret;
}
int? IOpenGLGraphicsSurface.BackbufferFramebuffer
{
get
@@ -181,14 +165,11 @@ namespace osu.Framework.Platform.SDL3
return (verticalSync = interval != 0).Value;
}
set
{
if (RuntimeInfo.IsDesktop)
{
SDL_GL_SetSwapInterval(value ? 1 : 0);
verticalSync = value;
}
}
}
IntPtr IOpenGLGraphicsSurface.WindowContext => (IntPtr)context;
IntPtr IOpenGLGraphicsSurface.CurrentContext => (IntPtr)SDL_GL_GetCurrentContext();
@@ -198,7 +179,7 @@ namespace osu.Framework.Platform.SDL3
void IOpenGLGraphicsSurface.DeleteContext(IntPtr context) => SDL_GL_DestroyContext((SDL_GLContextState*)context);
void IOpenGLGraphicsSurface.MakeCurrent(IntPtr context) => SDL_GL_MakeCurrent(window.SDLWindowHandle, (SDL_GLContextState*)context);
void IOpenGLGraphicsSurface.ClearCurrent() => SDL_GL_MakeCurrent(window.SDLWindowHandle, null);
IntPtr IOpenGLGraphicsSurface.GetProcAddress(string symbol) => getProcAddress(symbol);
IntPtr IOpenGLGraphicsSurface.GetProcAddress(string symbol) => SDL_GL_GetProcAddress(symbol);
#endregion
@@ -220,7 +201,7 @@ namespace osu.Framework.Platform.SDL3
#region Android-specific implementation
[SupportedOSPlatform("android")]
IntPtr IAndroidGraphicsSurface.JniEnvHandle => SDL_GetAndroidJNIEnv();
IntPtr IAndroidGraphicsSurface.JniEnvHandle => SDL_GetAndroidJNIEnv().ThrowIfFailed();
[SupportedOSPlatform("android")]
IntPtr IAndroidGraphicsSurface.SurfaceHandle => window.SurfaceHandle;

View File

@@ -15,7 +15,7 @@ namespace osu.Framework.Platform.SDL3
protected override unsafe void UpdateWindowStateAndSize(WindowState state, Display display, DisplayMode displayMode)
{
// This sets the status bar to hidden.
SDL_SetWindowFullscreen(SDLWindowHandle, true);
SDL_SetWindowFullscreen(SDLWindowHandle, true).LogErrorIfFailed();
// Don't run base logic at all. Let's keep things simple.
}

View File

@@ -75,7 +75,7 @@ namespace osu.Framework.Platform.SDL3
set
{
title = value;
ScheduleCommand(() => SDL_SetWindowTitle(SDLWindowHandle, title));
ScheduleCommand(() => SDL_SetWindowTitle(SDLWindowHandle, title).LogErrorIfFailed());
}
}
@@ -95,6 +95,8 @@ namespace osu.Framework.Platform.SDL3
return IntPtr.Zero;
var props = SDL_GetWindowProperties(SDLWindowHandle);
if (props == 0)
return IntPtr.Zero;
switch (RuntimeInfo.OS)
{
@@ -134,6 +136,8 @@ namespace osu.Framework.Platform.SDL3
return IntPtr.Zero;
var props = SDL_GetWindowProperties(SDLWindowHandle);
if (props == 0)
return IntPtr.Zero;
if (IsWayland)
return SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, IntPtr.Zero);
@@ -161,7 +165,7 @@ namespace osu.Framework.Platform.SDL3
{
ObjectHandle = new ObjectHandle<SDL3Window>(this, GCHandleType.Normal);
SDL_SetHint(SDL_HINT_APP_NAME, appName);
SDL_SetHint(SDL_HINT_APP_NAME, appName).LogErrorIfFailed();
if (!SDL_Init(SDL_InitFlags.SDL_INIT_VIDEO | SDL_InitFlags.SDL_INIT_GAMEPAD))
{
@@ -174,7 +178,6 @@ namespace osu.Framework.Platform.SDL3
SDL3 Revision: {SDL_GetRevision()}
SDL3 Video driver: {SDL_GetCurrentVideoDriver()}");
SDL_SetLogPriority(SDL_LogCategory.SDL_LOG_CATEGORY_ERROR, SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG);
SDL_SetLogOutputFunction(&logOutput, IntPtr.Zero);
SDL_SetEventFilter(&eventFilter, ObjectHandle.Handle);
@@ -212,13 +215,13 @@ namespace osu.Framework.Platform.SDL3
flags |= WindowState.ToFlags();
flags |= graphicsSurface.Type.ToFlags();
SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "0"u8);
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_CENTER, "0"u8);
SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"u8); // disable touch events generating synthetic mouse events on desktop platforms
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"u8); // disable mouse events generating synthetic touch events on mobile platforms
SDL_SetHint(SDL_HINT_PEN_TOUCH_EVENTS, "0"u8);
SDL_SetHint(SDL_HINT_PEN_MOUSE_EVENTS, "0"u8);
SDL_SetHint(SDL_HINT_IME_IMPLEMENTED_UI, "composition"u8);
SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "0"u8).LogErrorIfFailed();
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_CENTER, "0"u8).LogErrorIfFailed();
SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"u8).LogErrorIfFailed(); // disable touch events generating synthetic mouse events on desktop platforms
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"u8).LogErrorIfFailed(); // disable mouse events generating synthetic touch events on mobile platforms
SDL_SetHint(SDL_HINT_PEN_TOUCH_EVENTS, "0"u8).LogErrorIfFailed();
SDL_SetHint(SDL_HINT_PEN_MOUSE_EVENTS, "0"u8).LogErrorIfFailed();
SDL_SetHint(SDL_HINT_IME_IMPLEMENTED_UI, "composition"u8).LogErrorIfFailed();
SDLWindowHandle = SDL_CreateWindow(title, Size.Width, Size.Height, flags);
@@ -228,7 +231,7 @@ namespace osu.Framework.Platform.SDL3
// we want text input to only be active when SDL3DesktopWindowTextInput is active.
// SDL activates it by default on some platforms: https://github.com/libsdl-org/SDL/blob/release-2.0.16/src/video/SDL_video.c#L573-L582
// so we deactivate it on startup.
SDL_StopTextInput(SDLWindowHandle);
SDL_StopTextInput(SDLWindowHandle).LogErrorIfFailed();
graphicsSurface.Initialise();
@@ -241,7 +244,7 @@ namespace osu.Framework.Platform.SDL3
/// </summary>
public virtual void Run()
{
SDL_AddEventWatch(&eventWatch, ObjectHandle.Handle);
SDL_AddEventWatch(&eventWatch, ObjectHandle.Handle).LogErrorIfFailed();
RunMainLoop();
}
@@ -405,14 +408,14 @@ namespace osu.Framework.Platform.SDL3
var flags = SDL_GetWindowFlags(SDLWindowHandle);
if (flags.HasFlagFast(SDL_WindowFlags.SDL_WINDOW_MINIMIZED))
SDL_RestoreWindow(SDLWindowHandle);
SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed();
SDL_RaiseWindow(SDLWindowHandle);
SDL_RaiseWindow(SDLWindowHandle).LogErrorIfFailed();
});
public void Hide() => ScheduleCommand(() =>
{
SDL_HideWindow(SDLWindowHandle);
SDL_HideWindow(SDLWindowHandle).LogErrorIfFailed();
});
public void Show() => ScheduleCommand(() =>
@@ -430,7 +433,7 @@ namespace osu.Framework.Platform.SDL3
SDL_FlashWindow(SDLWindowHandle, flashUntilFocused
? SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED
: SDL_FlashOperation.SDL_FLASH_BRIEFLY);
: SDL_FlashOperation.SDL_FLASH_BRIEFLY).LogErrorIfFailed();
});
public void CancelFlash() => ScheduleCommand(() =>
@@ -438,12 +441,12 @@ namespace osu.Framework.Platform.SDL3
if (!RuntimeInfo.IsDesktop)
return;
SDL_FlashWindow(SDLWindowHandle, SDL_FlashOperation.SDL_FLASH_CANCEL);
SDL_FlashWindow(SDLWindowHandle, SDL_FlashOperation.SDL_FLASH_CANCEL).LogErrorIfFailed();
});
public void EnableScreenSuspension() => ScheduleCommand(() => SDL_EnableScreenSaver());
public void EnableScreenSuspension() => ScheduleCommand(() => SDL_EnableScreenSaver().LogErrorIfFailed());
public void DisableScreenSuspension() => ScheduleCommand(() => SDL_DisableScreenSaver());
public void DisableScreenSuspension() => ScheduleCommand(() => SDL_DisableScreenSaver().LogErrorIfFailed());
/// <summary>
/// Attempts to set the window's icon to the specified image.
@@ -463,10 +466,12 @@ namespace osu.Framework.Platform.SDL3
fixed (Rgba32* ptr = pixelSpan)
{
var pixelFormat = SDL_GetPixelFormatForMasks(32, 0xff, 0xff00, 0xff0000, 0xff000000);
surface = SDL_CreateSurfaceFrom(imageSize.Width, imageSize.Height, pixelFormat, new IntPtr(ptr), imageSize.Width * 4);
surface = SDL3Extensions.LogErrorIfFailed(SDL_CreateSurfaceFrom(imageSize.Width, imageSize.Height, pixelFormat, new IntPtr(ptr), imageSize.Width * 4));
if (surface == null)
return;
}
SDL_SetWindowIcon(SDLWindowHandle, surface);
SDL_SetWindowIcon(SDLWindowHandle, surface).LogErrorIfFailed();
SDL_DestroySurface(surface);
});
}
@@ -495,7 +500,7 @@ namespace osu.Framework.Platform.SDL3
do
{
eventsRead = SDL_PeepEvents(events, SDL_EventAction.SDL_GETEVENT, SDL_EventType.SDL_EVENT_FIRST, SDL_EventType.SDL_EVENT_LAST);
eventsRead = SDL_PeepEvents(events, SDL_EventAction.SDL_GETEVENT, SDL_EventType.SDL_EVENT_FIRST, SDL_EventType.SDL_EVENT_LAST).LogErrorIfFailed();
for (int i = 0; i < eventsRead; i++)
HandleEvent(events[i]);
} while (eventsRead == events_per_peep);

View File

@@ -51,7 +51,7 @@ namespace osu.Framework.Platform.SDL3
throw new InvalidOperationException($"Cannot set {nameof(RelativeMouseMode)} to true when the cursor is not hidden via {nameof(CursorState)}.");
relativeMouseMode = value;
ScheduleCommand(() => SDL_SetWindowRelativeMouseMode(SDLWindowHandle, value));
ScheduleCommand(() => SDL_SetWindowRelativeMouseMode(SDLWindowHandle, value).LogErrorIfFailed());
updateCursorConfinement();
}
}
@@ -69,7 +69,7 @@ namespace osu.Framework.Platform.SDL3
/// </remarks>
public bool MouseAutoCapture
{
set => ScheduleCommand(() => SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, value ? "1"u8 : "0"u8));
set => ScheduleCommand(() => SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, value ? "1"u8 : "0"u8).LogErrorIfFailed());
}
/// <summary>
@@ -101,9 +101,9 @@ namespace osu.Framework.Platform.SDL3
ScheduleCommand(() =>
{
if (cursorVisible)
SDL_ShowCursor();
SDL_ShowCursor().LogErrorIfFailed();
else
SDL_HideCursor();
SDL_HideCursor().LogErrorIfFailed();
});
/// <summary>
@@ -113,7 +113,7 @@ namespace osu.Framework.Platform.SDL3
{
bool confined = CursorState.HasFlagFast(CursorState.Confined);
ScheduleCommand(() => SDL_SetWindowMouseGrab(SDLWindowHandle, confined));
ScheduleCommand(() => SDL_SetWindowMouseGrab(SDLWindowHandle, confined).LogErrorIfFailed());
// Don't use SDL_SetWindowMouseRect when relative mode is enabled, as relative mode already confines the OS cursor to the window.
// This is fine for our use case, as UserInputManager will clamp the mouse position.
@@ -122,12 +122,12 @@ namespace osu.Framework.Platform.SDL3
ScheduleCommand(() =>
{
var rect = ((RectangleI)(CursorConfineRect / Scale)).ToSDLRect();
SDL_SetWindowMouseRect(SDLWindowHandle, &rect);
SDL_SetWindowMouseRect(SDLWindowHandle, &rect).LogErrorIfFailed();
});
}
else
{
ScheduleCommand(() => SDL_SetWindowMouseRect(SDLWindowHandle, null));
ScheduleCommand(() => SDL_SetWindowMouseRect(SDLWindowHandle, null).LogErrorIfFailed());
}
}
@@ -192,25 +192,25 @@ namespace osu.Framework.Platform.SDL3
public virtual void StartTextInput(TextInputProperties properties) => ScheduleCommand(() =>
{
currentTextInputProperties ??= SDL_CreateProperties();
currentTextInputProperties ??= SDL_CreateProperties().ThrowIfFailed();
var props = currentTextInputProperties.Value;
SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_TYPE_NUMBER, (long)properties.Type.ToSDLTextInputType());
SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_TYPE_NUMBER, (long)properties.Type.ToSDLTextInputType()).LogErrorIfFailed();
if (!properties.AutoCapitalisation)
SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER, (long)SDL_Capitalization.SDL_CAPITALIZE_NONE);
SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER, (long)SDL_Capitalization.SDL_CAPITALIZE_NONE).LogErrorIfFailed();
else
SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER);
SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER).LogErrorIfFailed();
if (properties.Type == TextInputType.Code)
SDL_SetBooleanProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN, false);
SDL_SetBooleanProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN, false).LogErrorIfFailed();
else
SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN);
SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN).LogErrorIfFailed();
SDL_StartTextInputWithProperties(SDLWindowHandle, props);
SDL_StartTextInputWithProperties(SDLWindowHandle, props).LogErrorIfFailed();
});
public void StopTextInput() => ScheduleCommand(() => SDL_StopTextInput(SDLWindowHandle));
public void StopTextInput() => ScheduleCommand(() => SDL_StopTextInput(SDLWindowHandle).LogErrorIfFailed());
/// <summary>
/// Resets internal state of the platform-native IME.
@@ -218,19 +218,19 @@ namespace osu.Framework.Platform.SDL3
/// </summary>
public virtual void ResetIme() => ScheduleCommand(() =>
{
SDL_StopTextInput(SDLWindowHandle);
SDL_StopTextInput(SDLWindowHandle).LogErrorIfFailed();
if (currentTextInputProperties is SDL_PropertiesID props)
SDL_StartTextInputWithProperties(SDLWindowHandle, props);
SDL_StartTextInputWithProperties(SDLWindowHandle, props).LogErrorIfFailed();
else
SDL_StartTextInput(SDLWindowHandle);
SDL_StartTextInput(SDLWindowHandle).LogErrorIfFailed();
});
public void SetTextInputRect(RectangleF rect) => ScheduleCommand(() =>
{
// TODO: SDL3 allows apps to set cursor position through the third parameter of SDL_SetTextInputArea.
var sdlRect = ((RectangleI)(rect / Scale)).ToSDLRect();
SDL_SetTextInputArea(SDLWindowHandle, &sdlRect, 0);
SDL_SetTextInputArea(SDLWindowHandle, &sdlRect, 0).LogErrorIfFailed();
});
#region SDL Event Handling
@@ -359,11 +359,11 @@ namespace osu.Framework.Platform.SDL3
if (controllers.ContainsKey(instanceID))
return;
SDL_Joystick* joystick = SDL_OpenJoystick(instanceID);
SDL_Joystick* joystick = SDL3Extensions.LogErrorIfFailed(SDL_OpenJoystick(instanceID));
SDL_Gamepad* controller = null;
if (SDL_IsGamepad(instanceID))
controller = SDL_OpenGamepad(instanceID);
controller = SDL3Extensions.LogErrorIfFailed(SDL_OpenGamepad(instanceID));
controllers[instanceID] = new SDL3ControllerBindings(joystick, controller);
}
@@ -373,7 +373,7 @@ namespace osu.Framework.Platform.SDL3
/// </summary>
private void populateJoysticks()
{
using var joysticks = SDL_GetJoysticks();
using var joysticks = SDL_GetJoysticks().LogErrorIfFailed();
if (joysticks == null)
return;
@@ -531,7 +531,7 @@ namespace osu.Framework.Platform.SDL3
private void handleKeymapChangedEvent() => KeymapChanged?.Invoke();
private static TabletPenDeviceType getPenType(SDL_PenID instanceID) => SDL_GetPenDeviceType(instanceID).ToTabletPenDeviceType();
private static TabletPenDeviceType getPenType(SDL_PenID instanceID) => SDL_GetPenDeviceType(instanceID).ThrowIfFailed().ToTabletPenDeviceType();
private void handlePenMotionEvent(SDL_PenMotionEvent evtPenMotion)
{

View File

@@ -25,7 +25,7 @@ namespace osu.Framework.Platform.SDL3
config.BindWith(FrameworkSetting.MinimiseOnFocusLossInFullscreen, minimiseOnFocusLoss);
minimiseOnFocusLoss.BindValueChanged(e =>
{
ScheduleCommand(() => SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, e.NewValue ? "1"u8 : "0"u8));
ScheduleCommand(() => SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, e.NewValue ? "1"u8 : "0"u8).LogErrorIfFailed());
}, true);
fetchDisplays();
@@ -70,7 +70,7 @@ namespace osu.Framework.Platform.SDL3
if (min.Width > sizeWindowed.MaxValue.Width || min.Height > sizeWindowed.MaxValue.Height)
throw new InvalidOperationException($"Expected a size less than max window size ({sizeWindowed.MaxValue}), got {min}");
ScheduleCommand(() => SDL_SetWindowMinimumSize(SDLWindowHandle, min.Width, min.Height));
ScheduleCommand(() => SDL_SetWindowMinimumSize(SDLWindowHandle, min.Width, min.Height).LogErrorIfFailed());
};
sizeWindowed.MaxValueChanged += max =>
@@ -81,7 +81,7 @@ namespace osu.Framework.Platform.SDL3
if (max.Width < sizeWindowed.MinValue.Width || max.Height < sizeWindowed.MinValue.Height)
throw new InvalidOperationException($"Expected a size greater than min window size ({sizeWindowed.MinValue}), got {max}");
ScheduleCommand(() => SDL_SetWindowMaximumSize(SDLWindowHandle, max.Width, max.Height));
ScheduleCommand(() => SDL_SetWindowMaximumSize(SDLWindowHandle, max.Width, max.Height).LogErrorIfFailed());
};
config.BindWith(FrameworkSetting.SizeFullscreen, sizeFullscreen);
@@ -165,7 +165,7 @@ namespace osu.Framework.Platform.SDL3
set
{
position = value;
ScheduleCommand(() => SDL_SetWindowPosition(SDLWindowHandle, value.X, value.Y));
ScheduleCommand(() => SDL_SetWindowPosition(SDLWindowHandle, value.X, value.Y).LogErrorIfFailed());
}
}
@@ -183,7 +183,7 @@ namespace osu.Framework.Platform.SDL3
return;
resizable = value;
ScheduleCommand(() => SDL_SetWindowResizable(SDLWindowHandle, value));
ScheduleCommand(() => SDL_SetWindowResizable(SDLWindowHandle, value).LogErrorIfFailed());
}
}
@@ -245,9 +245,9 @@ namespace osu.Framework.Platform.SDL3
ScheduleCommand(() =>
{
if (value)
SDL_ShowWindow(SDLWindowHandle);
SDL_ShowWindow(SDLWindowHandle).LogErrorIfFailed();
else
SDL_HideWindow(SDLWindowHandle);
SDL_HideWindow(SDLWindowHandle).LogErrorIfFailed();
});
}
}
@@ -392,7 +392,7 @@ namespace osu.Framework.Platform.SDL3
}
display = new Display(displayIndex,
SDL_GetDisplayName(displayID),
SDL_GetDisplayName(displayID).LogErrorIfFailed(),
new Rectangle(rect.x, rect.y, rect.w, rect.h),
new Rectangle(usableBounds.x, usableBounds.y, usableBounds.w, usableBounds.h),
displayModes);
@@ -421,7 +421,7 @@ namespace osu.Framework.Platform.SDL3
get
{
SDL_Rect rect;
SDL_GetDisplayBounds(displayID, &rect);
SDL_GetDisplayBounds(displayID, &rect).LogErrorIfFailed();
return new Rectangle(rect.x, rect.y, rect.w, rect.h);
}
}
@@ -460,7 +460,7 @@ namespace osu.Framework.Platform.SDL3
private unsafe void fetchWindowSize(bool storeToConfig = true)
{
int w, h;
SDL_GetWindowSize(SDLWindowHandle, &w, &h);
SDL_GetWindowSize(SDLWindowHandle, &w, &h).LogErrorIfFailed();
int drawableW = graphicsSurface.GetDrawableSize().Width;
@@ -487,7 +487,7 @@ namespace osu.Framework.Platform.SDL3
case SDL_EventType.SDL_EVENT_WINDOW_MOVED:
// explicitly requery as there are occasions where what SDL has provided us with is not up-to-date.
int x, y;
SDL_GetWindowPosition(SDLWindowHandle, &x, &y);
SDL_GetWindowPosition(SDLWindowHandle, &x, &y).ThrowIfFailed();
var newPosition = new Point(x, y);
if (!newPosition.Equals(Position))
@@ -605,7 +605,7 @@ namespace osu.Framework.Platform.SDL3
windowMaximised = maximized;
}
var newDisplayID = SDL_GetDisplayForWindow(SDLWindowHandle);
var newDisplayID = SDL_GetDisplayForWindow(SDLWindowHandle).ThrowIfFailed();
if (displayID != newDisplayID)
{
@@ -676,9 +676,9 @@ namespace osu.Framework.Platform.SDL3
case WindowState.Normal:
Size = sizeWindowed.Value;
SDL_RestoreWindow(SDLWindowHandle);
SDL_SetWindowSize(SDLWindowHandle, Size.Width, Size.Height);
SDL_SetWindowResizable(SDLWindowHandle, Resizable);
SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed();
SDL_SetWindowSize(SDLWindowHandle, Size.Width, Size.Height).LogErrorIfFailed();
SDL_SetWindowResizable(SDLWindowHandle, Resizable).LogErrorIfFailed();
readWindowPositionFromConfig(state, display);
break;
@@ -690,8 +690,8 @@ namespace osu.Framework.Platform.SDL3
ensureWindowOnDisplay(display);
SDL_SetWindowFullscreenMode(SDLWindowHandle, &closestMode);
SDL_SetWindowFullscreen(SDLWindowHandle, true);
SDL_SetWindowFullscreenMode(SDLWindowHandle, &closestMode).LogErrorIfFailed();
SDL_SetWindowFullscreen(SDLWindowHandle, true).LogErrorIfFailed();
break;
case WindowState.FullscreenBorderless:
@@ -699,16 +699,16 @@ namespace osu.Framework.Platform.SDL3
break;
case WindowState.Maximised:
SDL_RestoreWindow(SDLWindowHandle);
SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed();
ensureWindowOnDisplay(display);
SDL_MaximizeWindow(SDLWindowHandle);
SDL_MaximizeWindow(SDLWindowHandle).LogErrorIfFailed();
break;
case WindowState.Minimised:
ensureWindowOnDisplay(display);
SDL_MinimizeWindow(SDLWindowHandle);
SDL_MinimizeWindow(SDLWindowHandle).LogErrorIfFailed();
break;
}
}
@@ -721,7 +721,8 @@ namespace osu.Framework.Platform.SDL3
return false;
}
var mode = windowState == WindowState.Fullscreen ? SDL_GetWindowFullscreenMode(windowHandle) : SDL_GetDesktopDisplayMode(displayID);
SDL_ClearError();
var mode = windowState == WindowState.Fullscreen ? SDL_GetWindowFullscreenMode(windowHandle) : SDL3Extensions.LogErrorIfFailed(SDL_GetDesktopDisplayMode(displayID));
string type = windowState == WindowState.Fullscreen ? "fullscreen" : "desktop";
if (mode != null)
@@ -768,7 +769,7 @@ namespace osu.Framework.Platform.SDL3
{
if (tryGetDisplayAtIndex(display.Index, out var requestedID))
{
if (requestedID == SDL_GetDisplayForWindow(SDLWindowHandle))
if (requestedID == SDL_GetDisplayForWindow(SDLWindowHandle).LogErrorIfFailed())
return;
}
@@ -844,8 +845,8 @@ namespace osu.Framework.Platform.SDL3
ensureWindowOnDisplay(display);
// this is a generally sane method of handling borderless, and works well on macOS and linux.
SDL_SetWindowFullscreenMode(SDLWindowHandle, null);
SDL_SetWindowFullscreen(SDLWindowHandle, true);
SDL_SetWindowFullscreenMode(SDLWindowHandle, null).LogErrorIfFailed();
SDL_SetWindowFullscreen(SDLWindowHandle, true).LogErrorIfFailed();
return display.Bounds.Size;
}

View File

@@ -88,7 +88,7 @@ namespace osu.Framework.Platform.Windows
&& RelativeMouseMode)
{
var pt = PointToScreen(new Point((int)LastMousePosition.Value.X, (int)LastMousePosition.Value.Y));
SDL_WarpMouseGlobal(pt.X, pt.Y); // this directly calls the SetCursorPos win32 API
SDL_WarpMouseGlobal(pt.X, pt.Y).LogErrorIfFailed(); // this directly calls the SetCursorPos win32 API
}
}
@@ -121,7 +121,7 @@ namespace osu.Framework.Platform.Windows
protected override unsafe Size SetBorderless(Display display)
{
SDL_SetWindowBordered(SDLWindowHandle, false);
SDL_SetWindowBordered(SDLWindowHandle, false).LogErrorIfFailed();
var newSize = display.Bounds.Size;
@@ -130,7 +130,7 @@ namespace osu.Framework.Platform.Windows
// we also trick the game into thinking the window has normal size: see Size setter override
newSize += new Size(windows_borderless_width_hack, 0);
SDL_SetWindowSize(SDLWindowHandle, newSize.Width, newSize.Height);
SDL_SetWindowSize(SDLWindowHandle, newSize.Width, newSize.Height).LogErrorIfFailed();
Position = display.Bounds.Location;
return newSize;

View File

@@ -9,6 +9,7 @@ using System.Linq;
using ManagedBass;
using ManagedBass.Mix;
using ManagedBass.Wasapi;
using osu.Framework.Audio.Asio;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Development;
@@ -124,13 +125,15 @@ namespace osu.Framework.Threading
private WasapiProcedure? wasapiProcedure;
private WasapiNotifyProcedure? wasapiNotifyProcedure;
private BassAsio.AsioProcedure asioProcedure = null!;
/// <summary>
/// If a global mixer is being used, this will be the BASS handle for it.
/// If non-null, all game mixers should be added to this mixer.
/// </summary>
private readonly Bindable<int?> globalMixerHandle = new Bindable<int?>();
internal bool InitDevice(int deviceId, bool useExperimentalWasapi)
internal bool InitDevice(int deviceId, AudioThreadOutputMode outputMode, int? asioDeviceIndex = null)
{
Debug.Assert(ThreadSafety.IsAudioThread);
Trace.Assert(deviceId != -1); // The real device ID should always be used, as the -1 device has special cases which are hard to work with.
@@ -139,11 +142,41 @@ namespace osu.Framework.Threading
if (!Bass.Init(deviceId, Flags: (DeviceInitFlags)128)) // 128 == BASS_DEVICE_REINIT
return false;
if (useExperimentalWasapi)
attemptWasapiInitialisation();
else
switch (outputMode)
{
case AudioThreadOutputMode.Default:
freeAsio();
freeWasapi();
break;
case AudioThreadOutputMode.WasapiShared:
freeAsio();
attemptWasapiInitialisation(exclusive: false);
break;
case AudioThreadOutputMode.WasapiExclusive:
freeAsio();
attemptWasapiInitialisation(exclusive: true);
break;
case AudioThreadOutputMode.Asio:
freeWasapi();
if (asioDeviceIndex == null)
{
Logger.Log("ASIO output mode selected but no ASIO device index was provided.", level: LogLevel.Error);
return false;
}
if (!initAsio(asioDeviceIndex.Value))
return false;
break;
}
initialised_devices.Add(deviceId);
return true;
}
@@ -160,6 +193,7 @@ namespace osu.Framework.Threading
Bass.Free();
}
freeAsio();
freeWasapi();
if (selectedDevice != deviceId && canSelectDevice(selectedDevice))
@@ -182,7 +216,9 @@ namespace osu.Framework.Threading
}
}
private bool attemptWasapiInitialisation()
private bool attemptWasapiInitialisation() => attemptWasapiInitialisation(exclusive: false);
private bool attemptWasapiInitialisation(bool exclusive)
{
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
return false;
@@ -219,10 +255,10 @@ namespace osu.Framework.Threading
// To keep things in a sane state let's only keep one device initialised via wasapi.
freeWasapi();
return initWasapi(wasapiDevice);
return initWasapi(wasapiDevice, exclusive);
}
private bool initWasapi(int wasapiDevice)
private bool initWasapi(int wasapiDevice, bool exclusive)
{
// This is intentionally initialised inline and stored to a field.
// If we don't do this, it gets GC'd away.
@@ -238,11 +274,15 @@ namespace osu.Framework.Threading
if (notify == WasapiNotificationType.DefaultOutput)
{
freeWasapi();
initWasapi(device);
initWasapi(device, exclusive);
}
});
bool initialised = BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Flags: WasapiInitFlags.EventDriven | WasapiInitFlags.AutoFormat, Buffer: 0f, Period: float.Epsilon);
var flags = WasapiInitFlags.EventDriven | WasapiInitFlags.AutoFormat;
if (exclusive)
flags |= (WasapiInitFlags)16; // WasapiInitFlags.Exclusive (not available in older bindings).
bool initialised = BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Flags: flags, Buffer: 0f, Period: float.Epsilon);
Logger.Log($"Initialising BassWasapi for device {wasapiDevice}...{(initialised ? "success!" : "FAILED")}");
if (!initialised)
@@ -267,6 +307,93 @@ namespace osu.Framework.Threading
globalMixerHandle.Value = null;
}
private bool initAsio(int asioDeviceIndex)
{
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
return false;
Logger.Log($"Attempting BassAsio initialisation for device {asioDeviceIndex}");
freeAsio();
try
{
if (!BassAsio.Init(asioDeviceIndex))
{
Logger.Log($"BassAsio.Init({asioDeviceIndex}) failed", level: LogLevel.Error);
return false;
}
}
catch (DllNotFoundException e)
{
Logger.Log($"bassasio native library not found ({e.Message}). ASIO output will be unavailable.", level: LogLevel.Error);
return false;
}
catch (EntryPointNotFoundException e)
{
Logger.Log($"bassasio native library is incompatible ({e.Message}). ASIO output will be unavailable.", level: LogLevel.Error);
return false;
}
int sampleRate = (int)Math.Round(BassAsio.GetRate());
if (sampleRate <= 0)
sampleRate = 44100;
globalMixerHandle.Value = BassMix.CreateMixerStream(sampleRate, 2, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float);
// If we don't store the procedure, it gets GC'd away.
asioProcedure = (input, channel, buffer, length, user) =>
{
if (globalMixerHandle.Value == null)
return 0;
return Bass.ChannelGetData(globalMixerHandle.Value!.Value, buffer, length);
};
// Enable stereo output (first two output channels) and feed the global mixer stream into it.
// We ignore additional channels for now.
BassAsio.ChannelEnable(false, 0, asioProcedure, IntPtr.Zero);
BassAsio.ChannelEnable(false, 1, asioProcedure, IntPtr.Zero);
BassAsio.GetInfo(out var info);
if (!BassAsio.Start(0))
{
Logger.Log("BassAsio.Start() failed", level: LogLevel.Error);
freeAsio();
return false;
}
Logger.Log($"BassAsio initialised (Rate: {BassAsio.GetRate()}, OutChans: {info.Outputs})");
return true;
}
private void freeAsio()
{
try
{
BassAsio.Stop();
BassAsio.Free();
}
catch
{
}
if (globalMixerHandle.Value != null)
{
Bass.StreamFree(globalMixerHandle.Value.Value);
globalMixerHandle.Value = null;
}
}
internal enum AudioThreadOutputMode
{
Default,
WasapiShared,
WasapiExclusive,
Asio,
}
#endregion
}
}