mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-13 11:20:31 +00:00
同步更新,以及增加asio和独占wasapi的支持
This commit is contained in:
40
osu.Framework.NativeLibs/update-bass.ps1
Normal file
40
osu.Framework.NativeLibs/update-bass.ps1
Normal 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 }
|
||||||
|
}
|
||||||
@@ -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/armeabi-v7a/* -d ../osu.Framework.Android/armeabi-v7a/
|
||||||
unzip -qjo bassmix24-android.zip libs/x86/* -d ../osu.Framework.Android/x86/
|
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
|
# clean up
|
||||||
rm bass*.zip
|
rm bass*.zip
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ namespace osu.Framework.Tests.Audio
|
|||||||
|
|
||||||
private volatile bool simulateLoss;
|
private volatile bool simulateLoss;
|
||||||
|
|
||||||
protected override bool InitBass(int device)
|
protected override bool InitBass(int device, AudioOutputMode outputMode, int? asioDeviceIndex)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (simulateLoss)
|
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
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ namespace osu.Framework.Tests.Clocks
|
|||||||
[TestCase(1)]
|
[TestCase(1)]
|
||||||
[TestCase(10)]
|
[TestCase(10)]
|
||||||
[TestCase(50)]
|
[TestCase(50)]
|
||||||
|
[FlakyTest]
|
||||||
public void TestNoDecoupledDrift(int updateRate)
|
public void TestNoDecoupledDrift(int updateRate)
|
||||||
{
|
{
|
||||||
var stopwatch = new StopwatchClock();
|
var stopwatch = new StopwatchClock();
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ namespace osu.Framework.Tests.Clocks
|
|||||||
[TestCase(1)]
|
[TestCase(1)]
|
||||||
[TestCase(10)]
|
[TestCase(10)]
|
||||||
[TestCase(50)]
|
[TestCase(50)]
|
||||||
|
[FlakyTest]
|
||||||
public void TestNoInterpolationDrift(int updateRate)
|
public void TestNoInterpolationDrift(int updateRate)
|
||||||
{
|
{
|
||||||
var stopwatch = new StopwatchClock();
|
var stopwatch = new StopwatchClock();
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ namespace osu.Framework.Tests.Graphics
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
[FlakyTest]
|
||||||
public void TestReadSaturated()
|
public void TestReadSaturated()
|
||||||
{
|
{
|
||||||
var tripleBuffer = new TripleBuffer<TestObject>();
|
var tripleBuffer = new TripleBuffer<TestObject>();
|
||||||
|
|||||||
@@ -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));
|
public Task<string?> GetAsync(string key, CancellationToken cancellationToken = default) => Task.FromResult(Get(key));
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace osu.Framework.iOS
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base <see cref="UIApplicationDelegate"/> implementation for osu!framework applications.
|
/// Base <see cref="UIApplicationDelegate"/> implementation for osu!framework applications.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class GameApplicationDelegate : UIApplicationDelegate
|
public abstract class GameApplicationDelegate : UIResponder, IUIApplicationDelegate
|
||||||
{
|
{
|
||||||
internal event Action<string>? DragDrop;
|
internal event Action<string>? DragDrop;
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ namespace osu.Framework.iOS
|
|||||||
|
|
||||||
public IOSGameHost Host { get; private set; } = null!;
|
public IOSGameHost Host { get; private set; } = null!;
|
||||||
|
|
||||||
public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
|
public virtual bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
|
||||||
{
|
{
|
||||||
mapLibraryNames();
|
mapLibraryNames();
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ namespace osu.Framework.iOS
|
|||||||
return true;
|
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
|
// 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.
|
// 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;
|
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>
|
/// <summary>
|
||||||
/// Creates the <see cref="Game"/> class to launch.
|
/// Creates the <see cref="Game"/> class to launch.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ namespace osu.Framework.iOS
|
|||||||
|
|
||||||
public override void Create()
|
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();
|
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
|
// 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
|
// frame rate with multi-threaded mode turned on, but it is going to give them worse input latency
|
||||||
// and higher power usage.
|
// and higher power usage.
|
||||||
SDL_SetiOSAnimationCallback(SDLWindowHandle, 1, &runFrame, ObjectHandle.Handle);
|
SDL_SetiOSAnimationCallback(SDLWindowHandle, 1, &runFrame, ObjectHandle.Handle).ThrowIfFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
|
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
|
||||||
|
|||||||
115
osu.Framework/Audio/Asio/BassAsio.cs
Normal file
115
osu.Framework/Audio/Asio/BassAsio.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ using JetBrains.Annotations;
|
|||||||
using ManagedBass;
|
using ManagedBass;
|
||||||
using ManagedBass.Fx;
|
using ManagedBass.Fx;
|
||||||
using ManagedBass.Mix;
|
using ManagedBass.Mix;
|
||||||
|
using osu.Framework.Audio.Asio;
|
||||||
using osu.Framework.Audio.Mixing;
|
using osu.Framework.Audio.Mixing;
|
||||||
using osu.Framework.Audio.Mixing.Bass;
|
using osu.Framework.Audio.Mixing.Bass;
|
||||||
using osu.Framework.Audio.Sample;
|
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.
|
/// Consumers should provide a "Default" audio device entry which sets <see cref="AudioDevice"/> to an empty string.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IEnumerable<string> AudioDeviceNames => audioDeviceNames;
|
public IEnumerable<string> AudioDeviceNames => getAudioDeviceEntries();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is fired whenever a new audio device is discovered and provides its name.
|
/// 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.
|
// Mutated by multiple threads, must be thread safe.
|
||||||
private ImmutableArray<DeviceInfo> audioDevices = ImmutableArray<DeviceInfo>.Empty;
|
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;
|
private Scheduler scheduler => thread.Scheduler;
|
||||||
|
|
||||||
@@ -196,7 +212,15 @@ namespace osu.Framework.Audio
|
|||||||
}
|
}
|
||||||
|
|
||||||
AudioDevice.ValueChanged += _ => scheduler.AddOnce(initCurrentDevice);
|
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).
|
// 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;
|
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"/>.
|
/// Channels removed from this <see cref="AudioMixer"/> fall back to the global <see cref="SampleMixer"/>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="identifier">An identifier displayed on the audio mixer visualiser.</param>
|
/// <param name="identifier">An identifier displayed on the audio mixer visualiser.</param>
|
||||||
public AudioMixer CreateAudioMixer(string identifier = default) =>
|
public AudioMixer CreateAudioMixer(string identifier = default) => createAudioMixer(SampleMixer, !string.IsNullOrEmpty(identifier) ? identifier : $"user #{Interlocked.Increment(ref userMixerID)}");
|
||||||
createAudioMixer(SampleMixer, !string.IsNullOrEmpty(identifier) ? identifier : $"user #{Interlocked.Increment(ref userMixerID)}");
|
|
||||||
|
|
||||||
private AudioMixer createAudioMixer(AudioMixer fallbackMixer, string identifier)
|
private AudioMixer createAudioMixer(AudioMixer fallbackMixer, string identifier)
|
||||||
{
|
{
|
||||||
@@ -332,24 +355,61 @@ namespace osu.Framework.Audio
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void initCurrentDevice()
|
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
|
// try using the specified device
|
||||||
int deviceIndex = audioDeviceNames.FindIndex(d => d == deviceName);
|
if (mode == AudioOutputMode.Asio)
|
||||||
if (deviceIndex >= 0 && trySetDevice(BASS_INTERNAL_DEVICE_COUNT + deviceIndex)) return;
|
{
|
||||||
|
// 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.
|
// 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,
|
// 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".
|
// 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.
|
// 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.
|
// we're boned. even "No sound" device won't initialise.
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bool trySetDevice(int deviceId)
|
bool trySetDevice(int deviceId, AudioOutputMode outputMode, int? asioDeviceIndex)
|
||||||
{
|
{
|
||||||
var device = audioDevices.ElementAtOrDefault(deviceId);
|
var device = audioDevices.ElementAtOrDefault(deviceId);
|
||||||
|
|
||||||
@@ -362,7 +422,7 @@ namespace osu.Framework.Audio
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
// initialize new device
|
// initialize new device
|
||||||
if (!InitBass(deviceId))
|
if (!InitBass(deviceId, outputMode, asioDeviceIndex))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
//we have successfully initialised a new device.
|
//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>
|
/// <summary>
|
||||||
/// This method calls <see cref="Bass.Init(int, int, DeviceInitFlags, IntPtr, IntPtr)"/>.
|
/// This method calls <see cref="Bass.Init(int, int, DeviceInitFlags, IntPtr, IntPtr)"/>.
|
||||||
/// It can be overridden for unit testing.
|
/// It can be overridden for unit testing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="device">The device to initialise.</param>
|
/// <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.
|
// this likely doesn't help us but also doesn't seem to cause any issues or any cpu increase.
|
||||||
Bass.UpdatePeriod = 5;
|
Bass.UpdatePeriod = 5;
|
||||||
@@ -402,25 +503,15 @@ namespace osu.Framework.Audio
|
|||||||
// - This disables support for ItunSMPB tag parsing to match previous expectations.
|
// - 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.
|
// - 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)
|
// (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.
|
// 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.
|
// See https://www.un4seen.com/forum/?topic=19601 for more information.
|
||||||
Bass.Configure((ManagedBass.Configuration)70, false);
|
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 attemptInit()
|
||||||
{
|
{
|
||||||
bool innerSuccess = thread.InitDevice(device, UseExperimentalWasapi.Value);
|
bool innerSuccess = thread.InitDevice(device, toThreadOutputMode(outputMode), asioDeviceIndex);
|
||||||
bool alreadyInitialised = Bass.LastError == Errors.Already;
|
bool alreadyInitialised = Bass.LastError == Errors.Already;
|
||||||
|
|
||||||
if (alreadyInitialised)
|
if (alreadyInitialised)
|
||||||
@@ -449,6 +540,26 @@ namespace osu.Framework.Audio
|
|||||||
|
|
||||||
return true;
|
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()
|
private void syncAudioDevices()
|
||||||
@@ -458,8 +569,8 @@ namespace osu.Framework.Audio
|
|||||||
// Bass should always be providing "No sound" and "Default" device.
|
// 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.");
|
Trace.Assert(audioDevices.Length >= BASS_INTERNAL_DEVICE_COUNT, "Bass did not provide any audio devices.");
|
||||||
|
|
||||||
var oldDeviceNames = audioDeviceNames;
|
var oldDeviceNames = bassDeviceNames;
|
||||||
var newDeviceNames = audioDeviceNames = audioDevices.Skip(BASS_INTERNAL_DEVICE_COUNT).Where(d => d.IsEnabled).Select(d => d.Name).ToImmutableList();
|
var newDeviceNames = bassDeviceNames = audioDevices.Skip(BASS_INTERNAL_DEVICE_COUNT).Where(d => d.IsEnabled).Select(d => d.Name).ToImmutableList();
|
||||||
|
|
||||||
scheduler.Add(() =>
|
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>
|
/// <summary>
|
||||||
/// Check whether any audio device changes have occurred.
|
/// Check whether any audio device changes have occurred.
|
||||||
///
|
///
|
||||||
@@ -535,7 +735,13 @@ namespace osu.Framework.Audio
|
|||||||
protected virtual bool IsCurrentDeviceValid()
|
protected virtual bool IsCurrentDeviceValid()
|
||||||
{
|
{
|
||||||
var device = audioDevices.ElementAtOrDefault(Bass.CurrentDevice);
|
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;
|
return device.IsEnabled && device.IsInitialized && !isFallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using osu.Framework.Graphics.Rendering;
|
using osu.Framework.Graphics.Rendering;
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
@@ -55,7 +56,7 @@ namespace osu.Framework.Graphics.Shaders
|
|||||||
/// <param name="fragment">The fragment shader name.</param>
|
/// <param name="fragment">The fragment shader name.</param>
|
||||||
/// <returns>A cached <see cref="IShader"/> instance, if existing.</returns>
|
/// <returns>A cached <see cref="IShader"/> instance, if existing.</returns>
|
||||||
public virtual IShader? GetCachedShader(string vertex, string fragment)
|
public virtual IShader? GetCachedShader(string vertex, string fragment)
|
||||||
=> shaderCache.TryGetValue((vertex, fragment), out IShader? shader) ? shader : null;
|
=> shaderCache.GetValueOrDefault((vertex, fragment));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to retrieve an already-cached shader part.
|
/// 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>
|
/// <param name="name">The name of the shader part.</param>
|
||||||
/// <returns>A cached <see cref="IShaderPart"/> instance, if existing.</returns>
|
/// <returns>A cached <see cref="IShaderPart"/> instance, if existing.</returns>
|
||||||
public virtual IShaderPart? GetCachedShaderPart(string name)
|
public virtual IShaderPart? GetCachedShaderPart(string name)
|
||||||
=> partCache.TryGetValue(name, out IShaderPart? part) ? part : null;
|
=> partCache.GetValueOrDefault(name);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to retrieve the raw data for a shader file.
|
/// Attempts to retrieve the raw data for a shader file.
|
||||||
|
|||||||
@@ -368,10 +368,7 @@ namespace osu.Framework.Graphics.Transforms
|
|||||||
return min;
|
return min;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastAppliedTransformIndices.TryGetValue(targetMember, out int val))
|
return lastAppliedTransformIndices.GetValueOrDefault(targetMember, 0);
|
||||||
return val;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Input.States;
|
using osu.Framework.Input.States;
|
||||||
using osu.Framework.Lists;
|
using osu.Framework.Lists;
|
||||||
using osu.Framework.Logging;
|
// using osu.Framework.Logging;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Framework.Input.Bindings
|
namespace osu.Framework.Input.Bindings
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// 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 osu.Framework.Input.States;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ namespace osu.Framework.Input
|
|||||||
/// <param name="button">The button to find the manager for.</param>
|
/// <param name="button">The button to find the manager for.</param>
|
||||||
/// <returns>The <see cref="MouseButtonEventManager"/>.</returns>
|
/// <returns>The <see cref="MouseButtonEventManager"/>.</returns>
|
||||||
public MouseButtonEventManager GetButtonEventManagerFor(MouseButton button) =>
|
public MouseButtonEventManager GetButtonEventManagerFor(MouseButton button) =>
|
||||||
mouseButtonEventManagers.TryGetValue(button, out var manager) ? manager : null;
|
mouseButtonEventManagers.GetValueOrDefault(button);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a <see cref="KeyEventManager"/> for a specified key.
|
/// Create a <see cref="KeyEventManager"/> for a specified key.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace osu.Framework.Platform.Linux
|
|||||||
public SDL3LinuxWindow(GraphicsSurfaceType surfaceType, string appName, bool bypassCompositor)
|
public SDL3LinuxWindow(GraphicsSurfaceType surfaceType, string appName, bool bypassCompositor)
|
||||||
: base(surfaceType, appName)
|
: 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
// assume that empty text means no text.
|
// assume that empty text means no text.
|
||||||
public override string? GetText() => SDL_HasClipboardText() ? SDL_GetClipboardText() : null;
|
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>()
|
public override Image<TPixel>? GetImage<TPixel>()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
// this reset is required even on changing from one fullscreen resolution to another.
|
// 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.
|
// 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).
|
// 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_SetWindowBordered(SDLWindowHandle, true).LogErrorIfFailed();
|
||||||
SDL_SetWindowFullscreen(SDLWindowHandle, false);
|
SDL_SetWindowFullscreen(SDLWindowHandle, false).LogErrorIfFailed();
|
||||||
SDL_RestoreWindow(SDLWindowHandle);
|
SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed();
|
||||||
|
|
||||||
base.UpdateWindowStateAndSize(state, display, displayMode);
|
base.UpdateWindowStateAndSize(state, display, displayMode);
|
||||||
}
|
}
|
||||||
|
|||||||
10
osu.Framework/Platform/SDL3/SDL3Exception.cs
Normal file
10
osu.Framework/Platform/SDL3/SDL3Exception.cs
Normal 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})");
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using osu.Framework.Extensions.EnumExtensions;
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.StateChanges;
|
using osu.Framework.Input.StateChanges;
|
||||||
|
using osu.Framework.Logging;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
using SDL;
|
using SDL;
|
||||||
using static SDL.SDL3;
|
using static SDL.SDL3;
|
||||||
@@ -1045,7 +1048,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
{
|
{
|
||||||
int bpp;
|
int bpp;
|
||||||
uint unused;
|
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);
|
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();
|
SDL_ClearError();
|
||||||
return error;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
switch (surfaceType)
|
switch (surfaceType)
|
||||||
{
|
{
|
||||||
case GraphicsSurfaceType.OpenGL:
|
case GraphicsSurfaceType.OpenGL:
|
||||||
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_RED_SIZE, 8);
|
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_RED_SIZE, 8).ThrowIfFailed();
|
||||||
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_GREEN_SIZE, 8);
|
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_GREEN_SIZE, 8).ThrowIfFailed();
|
||||||
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_BLUE_SIZE, 8);
|
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_BLUE_SIZE, 8).ThrowIfFailed();
|
||||||
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_ACCUM_ALPHA_SIZE, 0);
|
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_ACCUM_ALPHA_SIZE, 0).ThrowIfFailed();
|
||||||
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_DEPTH_SIZE, 16);
|
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_DEPTH_SIZE, 16).ThrowIfFailed();
|
||||||
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_STENCIL_SIZE, 8);
|
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_STENCIL_SIZE, 8).ThrowIfFailed();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GraphicsSurfaceType.Vulkan:
|
case GraphicsSurfaceType.Vulkan:
|
||||||
@@ -60,7 +60,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
public Size GetDrawableSize()
|
public Size GetDrawableSize()
|
||||||
{
|
{
|
||||||
int width, height;
|
int width, height;
|
||||||
SDL_GetWindowSizeInPixels(window.SDLWindowHandle, &width, &height);
|
SDL_GetWindowSizeInPixels(window.SDLWindowHandle, &width, &height).ThrowIfFailed();
|
||||||
return new Size(width, height);
|
return new Size(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,19 +70,19 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
{
|
{
|
||||||
if (RuntimeInfo.IsMobile)
|
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:
|
// 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_MAJOR_VERSION, 3).ThrowIfFailed();
|
||||||
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 0).ThrowIfFailed();
|
||||||
}
|
}
|
||||||
else
|
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:
|
// 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_MAJOR_VERSION, 3).ThrowIfFailed();
|
||||||
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 2);
|
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 2).ThrowIfFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
context = SDL_GL_CreateContext(window.SDLWindowHandle);
|
context = SDL_GL_CreateContext(window.SDLWindowHandle);
|
||||||
@@ -90,7 +90,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
if (context == null)
|
if (context == null)
|
||||||
throw new InvalidOperationException($"Failed to create an SDL3 GL context ({SDL_GetError()})");
|
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();
|
loadBindings();
|
||||||
}
|
}
|
||||||
@@ -126,29 +126,13 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
string? str = Marshal.PtrToStringAnsi(new IntPtr(ptr));
|
string? str = Marshal.PtrToStringAnsi(new IntPtr(ptr));
|
||||||
|
|
||||||
Debug.Assert(str != null);
|
Debug.Assert(str != null);
|
||||||
entryPointsInstance[i] = getProcAddress(str);
|
entryPointsInstance[i] = SDL_GL_GetProcAddress(str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pointsInfo.SetValue(bindings, entryPointsInstance);
|
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
|
int? IOpenGLGraphicsSurface.BackbufferFramebuffer
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -182,11 +166,8 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (RuntimeInfo.IsDesktop)
|
SDL_GL_SetSwapInterval(value ? 1 : 0);
|
||||||
{
|
verticalSync = value;
|
||||||
SDL_GL_SetSwapInterval(value ? 1 : 0);
|
|
||||||
verticalSync = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +179,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
void IOpenGLGraphicsSurface.DeleteContext(IntPtr context) => SDL_GL_DestroyContext((SDL_GLContextState*)context);
|
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.MakeCurrent(IntPtr context) => SDL_GL_MakeCurrent(window.SDLWindowHandle, (SDL_GLContextState*)context);
|
||||||
void IOpenGLGraphicsSurface.ClearCurrent() => SDL_GL_MakeCurrent(window.SDLWindowHandle, null);
|
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
|
#endregion
|
||||||
|
|
||||||
@@ -220,7 +201,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
#region Android-specific implementation
|
#region Android-specific implementation
|
||||||
|
|
||||||
[SupportedOSPlatform("android")]
|
[SupportedOSPlatform("android")]
|
||||||
IntPtr IAndroidGraphicsSurface.JniEnvHandle => SDL_GetAndroidJNIEnv();
|
IntPtr IAndroidGraphicsSurface.JniEnvHandle => SDL_GetAndroidJNIEnv().ThrowIfFailed();
|
||||||
|
|
||||||
[SupportedOSPlatform("android")]
|
[SupportedOSPlatform("android")]
|
||||||
IntPtr IAndroidGraphicsSurface.SurfaceHandle => window.SurfaceHandle;
|
IntPtr IAndroidGraphicsSurface.SurfaceHandle => window.SurfaceHandle;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
protected override unsafe void UpdateWindowStateAndSize(WindowState state, Display display, DisplayMode displayMode)
|
protected override unsafe void UpdateWindowStateAndSize(WindowState state, Display display, DisplayMode displayMode)
|
||||||
{
|
{
|
||||||
// This sets the status bar to hidden.
|
// 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.
|
// Don't run base logic at all. Let's keep things simple.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
title = value;
|
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;
|
return IntPtr.Zero;
|
||||||
|
|
||||||
var props = SDL_GetWindowProperties(SDLWindowHandle);
|
var props = SDL_GetWindowProperties(SDLWindowHandle);
|
||||||
|
if (props == 0)
|
||||||
|
return IntPtr.Zero;
|
||||||
|
|
||||||
switch (RuntimeInfo.OS)
|
switch (RuntimeInfo.OS)
|
||||||
{
|
{
|
||||||
@@ -134,6 +136,8 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
return IntPtr.Zero;
|
return IntPtr.Zero;
|
||||||
|
|
||||||
var props = SDL_GetWindowProperties(SDLWindowHandle);
|
var props = SDL_GetWindowProperties(SDLWindowHandle);
|
||||||
|
if (props == 0)
|
||||||
|
return IntPtr.Zero;
|
||||||
|
|
||||||
if (IsWayland)
|
if (IsWayland)
|
||||||
return SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, IntPtr.Zero);
|
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);
|
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))
|
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 Revision: {SDL_GetRevision()}
|
||||||
SDL3 Video driver: {SDL_GetCurrentVideoDriver()}");
|
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_SetLogOutputFunction(&logOutput, IntPtr.Zero);
|
||||||
SDL_SetEventFilter(&eventFilter, ObjectHandle.Handle);
|
SDL_SetEventFilter(&eventFilter, ObjectHandle.Handle);
|
||||||
|
|
||||||
@@ -212,13 +215,13 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
flags |= WindowState.ToFlags();
|
flags |= WindowState.ToFlags();
|
||||||
flags |= graphicsSurface.Type.ToFlags();
|
flags |= graphicsSurface.Type.ToFlags();
|
||||||
|
|
||||||
SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "0"u8);
|
SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "0"u8).LogErrorIfFailed();
|
||||||
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_CENTER, "0"u8);
|
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_CENTER, "0"u8).LogErrorIfFailed();
|
||||||
SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"u8); // disable touch events generating synthetic mouse events on desktop platforms
|
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); // disable mouse events generating synthetic touch events on mobile 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);
|
SDL_SetHint(SDL_HINT_PEN_TOUCH_EVENTS, "0"u8).LogErrorIfFailed();
|
||||||
SDL_SetHint(SDL_HINT_PEN_MOUSE_EVENTS, "0"u8);
|
SDL_SetHint(SDL_HINT_PEN_MOUSE_EVENTS, "0"u8).LogErrorIfFailed();
|
||||||
SDL_SetHint(SDL_HINT_IME_IMPLEMENTED_UI, "composition"u8);
|
SDL_SetHint(SDL_HINT_IME_IMPLEMENTED_UI, "composition"u8).LogErrorIfFailed();
|
||||||
|
|
||||||
SDLWindowHandle = SDL_CreateWindow(title, Size.Width, Size.Height, flags);
|
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.
|
// 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
|
// 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.
|
// so we deactivate it on startup.
|
||||||
SDL_StopTextInput(SDLWindowHandle);
|
SDL_StopTextInput(SDLWindowHandle).LogErrorIfFailed();
|
||||||
|
|
||||||
graphicsSurface.Initialise();
|
graphicsSurface.Initialise();
|
||||||
|
|
||||||
@@ -241,7 +244,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Run()
|
public virtual void Run()
|
||||||
{
|
{
|
||||||
SDL_AddEventWatch(&eventWatch, ObjectHandle.Handle);
|
SDL_AddEventWatch(&eventWatch, ObjectHandle.Handle).LogErrorIfFailed();
|
||||||
|
|
||||||
RunMainLoop();
|
RunMainLoop();
|
||||||
}
|
}
|
||||||
@@ -405,14 +408,14 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
var flags = SDL_GetWindowFlags(SDLWindowHandle);
|
var flags = SDL_GetWindowFlags(SDLWindowHandle);
|
||||||
|
|
||||||
if (flags.HasFlagFast(SDL_WindowFlags.SDL_WINDOW_MINIMIZED))
|
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(() =>
|
public void Hide() => ScheduleCommand(() =>
|
||||||
{
|
{
|
||||||
SDL_HideWindow(SDLWindowHandle);
|
SDL_HideWindow(SDLWindowHandle).LogErrorIfFailed();
|
||||||
});
|
});
|
||||||
|
|
||||||
public void Show() => ScheduleCommand(() =>
|
public void Show() => ScheduleCommand(() =>
|
||||||
@@ -430,7 +433,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
|
|
||||||
SDL_FlashWindow(SDLWindowHandle, flashUntilFocused
|
SDL_FlashWindow(SDLWindowHandle, flashUntilFocused
|
||||||
? SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED
|
? SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED
|
||||||
: SDL_FlashOperation.SDL_FLASH_BRIEFLY);
|
: SDL_FlashOperation.SDL_FLASH_BRIEFLY).LogErrorIfFailed();
|
||||||
});
|
});
|
||||||
|
|
||||||
public void CancelFlash() => ScheduleCommand(() =>
|
public void CancelFlash() => ScheduleCommand(() =>
|
||||||
@@ -438,12 +441,12 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
if (!RuntimeInfo.IsDesktop)
|
if (!RuntimeInfo.IsDesktop)
|
||||||
return;
|
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>
|
/// <summary>
|
||||||
/// Attempts to set the window's icon to the specified image.
|
/// Attempts to set the window's icon to the specified image.
|
||||||
@@ -463,10 +466,12 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
fixed (Rgba32* ptr = pixelSpan)
|
fixed (Rgba32* ptr = pixelSpan)
|
||||||
{
|
{
|
||||||
var pixelFormat = SDL_GetPixelFormatForMasks(32, 0xff, 0xff00, 0xff0000, 0xff000000);
|
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);
|
SDL_DestroySurface(surface);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -495,7 +500,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
|
|
||||||
do
|
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++)
|
for (int i = 0; i < eventsRead; i++)
|
||||||
HandleEvent(events[i]);
|
HandleEvent(events[i]);
|
||||||
} while (eventsRead == events_per_peep);
|
} while (eventsRead == events_per_peep);
|
||||||
|
|||||||
@@ -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)}.");
|
throw new InvalidOperationException($"Cannot set {nameof(RelativeMouseMode)} to true when the cursor is not hidden via {nameof(CursorState)}.");
|
||||||
|
|
||||||
relativeMouseMode = value;
|
relativeMouseMode = value;
|
||||||
ScheduleCommand(() => SDL_SetWindowRelativeMouseMode(SDLWindowHandle, value));
|
ScheduleCommand(() => SDL_SetWindowRelativeMouseMode(SDLWindowHandle, value).LogErrorIfFailed());
|
||||||
updateCursorConfinement();
|
updateCursorConfinement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool MouseAutoCapture
|
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>
|
/// <summary>
|
||||||
@@ -101,9 +101,9 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
ScheduleCommand(() =>
|
ScheduleCommand(() =>
|
||||||
{
|
{
|
||||||
if (cursorVisible)
|
if (cursorVisible)
|
||||||
SDL_ShowCursor();
|
SDL_ShowCursor().LogErrorIfFailed();
|
||||||
else
|
else
|
||||||
SDL_HideCursor();
|
SDL_HideCursor().LogErrorIfFailed();
|
||||||
});
|
});
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -113,7 +113,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
{
|
{
|
||||||
bool confined = CursorState.HasFlagFast(CursorState.Confined);
|
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.
|
// 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.
|
// This is fine for our use case, as UserInputManager will clamp the mouse position.
|
||||||
@@ -122,12 +122,12 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
ScheduleCommand(() =>
|
ScheduleCommand(() =>
|
||||||
{
|
{
|
||||||
var rect = ((RectangleI)(CursorConfineRect / Scale)).ToSDLRect();
|
var rect = ((RectangleI)(CursorConfineRect / Scale)).ToSDLRect();
|
||||||
SDL_SetWindowMouseRect(SDLWindowHandle, &rect);
|
SDL_SetWindowMouseRect(SDLWindowHandle, &rect).LogErrorIfFailed();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
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(() =>
|
public virtual void StartTextInput(TextInputProperties properties) => ScheduleCommand(() =>
|
||||||
{
|
{
|
||||||
currentTextInputProperties ??= SDL_CreateProperties();
|
currentTextInputProperties ??= SDL_CreateProperties().ThrowIfFailed();
|
||||||
|
|
||||||
var props = currentTextInputProperties.Value;
|
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)
|
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
|
else
|
||||||
SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER);
|
SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER).LogErrorIfFailed();
|
||||||
|
|
||||||
if (properties.Type == TextInputType.Code)
|
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
|
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>
|
/// <summary>
|
||||||
/// Resets internal state of the platform-native IME.
|
/// Resets internal state of the platform-native IME.
|
||||||
@@ -218,19 +218,19 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void ResetIme() => ScheduleCommand(() =>
|
public virtual void ResetIme() => ScheduleCommand(() =>
|
||||||
{
|
{
|
||||||
SDL_StopTextInput(SDLWindowHandle);
|
SDL_StopTextInput(SDLWindowHandle).LogErrorIfFailed();
|
||||||
|
|
||||||
if (currentTextInputProperties is SDL_PropertiesID props)
|
if (currentTextInputProperties is SDL_PropertiesID props)
|
||||||
SDL_StartTextInputWithProperties(SDLWindowHandle, props);
|
SDL_StartTextInputWithProperties(SDLWindowHandle, props).LogErrorIfFailed();
|
||||||
else
|
else
|
||||||
SDL_StartTextInput(SDLWindowHandle);
|
SDL_StartTextInput(SDLWindowHandle).LogErrorIfFailed();
|
||||||
});
|
});
|
||||||
|
|
||||||
public void SetTextInputRect(RectangleF rect) => ScheduleCommand(() =>
|
public void SetTextInputRect(RectangleF rect) => ScheduleCommand(() =>
|
||||||
{
|
{
|
||||||
// TODO: SDL3 allows apps to set cursor position through the third parameter of SDL_SetTextInputArea.
|
// TODO: SDL3 allows apps to set cursor position through the third parameter of SDL_SetTextInputArea.
|
||||||
var sdlRect = ((RectangleI)(rect / Scale)).ToSDLRect();
|
var sdlRect = ((RectangleI)(rect / Scale)).ToSDLRect();
|
||||||
SDL_SetTextInputArea(SDLWindowHandle, &sdlRect, 0);
|
SDL_SetTextInputArea(SDLWindowHandle, &sdlRect, 0).LogErrorIfFailed();
|
||||||
});
|
});
|
||||||
|
|
||||||
#region SDL Event Handling
|
#region SDL Event Handling
|
||||||
@@ -359,11 +359,11 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
if (controllers.ContainsKey(instanceID))
|
if (controllers.ContainsKey(instanceID))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
SDL_Joystick* joystick = SDL_OpenJoystick(instanceID);
|
SDL_Joystick* joystick = SDL3Extensions.LogErrorIfFailed(SDL_OpenJoystick(instanceID));
|
||||||
|
|
||||||
SDL_Gamepad* controller = null;
|
SDL_Gamepad* controller = null;
|
||||||
if (SDL_IsGamepad(instanceID))
|
if (SDL_IsGamepad(instanceID))
|
||||||
controller = SDL_OpenGamepad(instanceID);
|
controller = SDL3Extensions.LogErrorIfFailed(SDL_OpenGamepad(instanceID));
|
||||||
|
|
||||||
controllers[instanceID] = new SDL3ControllerBindings(joystick, controller);
|
controllers[instanceID] = new SDL3ControllerBindings(joystick, controller);
|
||||||
}
|
}
|
||||||
@@ -373,7 +373,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void populateJoysticks()
|
private void populateJoysticks()
|
||||||
{
|
{
|
||||||
using var joysticks = SDL_GetJoysticks();
|
using var joysticks = SDL_GetJoysticks().LogErrorIfFailed();
|
||||||
|
|
||||||
if (joysticks == null)
|
if (joysticks == null)
|
||||||
return;
|
return;
|
||||||
@@ -531,7 +531,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
|
|
||||||
private void handleKeymapChangedEvent() => KeymapChanged?.Invoke();
|
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)
|
private void handlePenMotionEvent(SDL_PenMotionEvent evtPenMotion)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
config.BindWith(FrameworkSetting.MinimiseOnFocusLossInFullscreen, minimiseOnFocusLoss);
|
config.BindWith(FrameworkSetting.MinimiseOnFocusLossInFullscreen, minimiseOnFocusLoss);
|
||||||
minimiseOnFocusLoss.BindValueChanged(e =>
|
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);
|
}, true);
|
||||||
|
|
||||||
fetchDisplays();
|
fetchDisplays();
|
||||||
@@ -70,7 +70,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
if (min.Width > sizeWindowed.MaxValue.Width || min.Height > sizeWindowed.MaxValue.Height)
|
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}");
|
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 =>
|
sizeWindowed.MaxValueChanged += max =>
|
||||||
@@ -81,7 +81,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
if (max.Width < sizeWindowed.MinValue.Width || max.Height < sizeWindowed.MinValue.Height)
|
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}");
|
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);
|
config.BindWith(FrameworkSetting.SizeFullscreen, sizeFullscreen);
|
||||||
@@ -165,7 +165,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
position = value;
|
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;
|
return;
|
||||||
|
|
||||||
resizable = value;
|
resizable = value;
|
||||||
ScheduleCommand(() => SDL_SetWindowResizable(SDLWindowHandle, value));
|
ScheduleCommand(() => SDL_SetWindowResizable(SDLWindowHandle, value).LogErrorIfFailed());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,9 +245,9 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
ScheduleCommand(() =>
|
ScheduleCommand(() =>
|
||||||
{
|
{
|
||||||
if (value)
|
if (value)
|
||||||
SDL_ShowWindow(SDLWindowHandle);
|
SDL_ShowWindow(SDLWindowHandle).LogErrorIfFailed();
|
||||||
else
|
else
|
||||||
SDL_HideWindow(SDLWindowHandle);
|
SDL_HideWindow(SDLWindowHandle).LogErrorIfFailed();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,7 +392,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
}
|
}
|
||||||
|
|
||||||
display = new Display(displayIndex,
|
display = new Display(displayIndex,
|
||||||
SDL_GetDisplayName(displayID),
|
SDL_GetDisplayName(displayID).LogErrorIfFailed(),
|
||||||
new Rectangle(rect.x, rect.y, rect.w, rect.h),
|
new Rectangle(rect.x, rect.y, rect.w, rect.h),
|
||||||
new Rectangle(usableBounds.x, usableBounds.y, usableBounds.w, usableBounds.h),
|
new Rectangle(usableBounds.x, usableBounds.y, usableBounds.w, usableBounds.h),
|
||||||
displayModes);
|
displayModes);
|
||||||
@@ -421,7 +421,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
SDL_Rect rect;
|
SDL_Rect rect;
|
||||||
SDL_GetDisplayBounds(displayID, &rect);
|
SDL_GetDisplayBounds(displayID, &rect).LogErrorIfFailed();
|
||||||
return new Rectangle(rect.x, rect.y, rect.w, rect.h);
|
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)
|
private unsafe void fetchWindowSize(bool storeToConfig = true)
|
||||||
{
|
{
|
||||||
int w, h;
|
int w, h;
|
||||||
SDL_GetWindowSize(SDLWindowHandle, &w, &h);
|
SDL_GetWindowSize(SDLWindowHandle, &w, &h).LogErrorIfFailed();
|
||||||
|
|
||||||
int drawableW = graphicsSurface.GetDrawableSize().Width;
|
int drawableW = graphicsSurface.GetDrawableSize().Width;
|
||||||
|
|
||||||
@@ -487,7 +487,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
case SDL_EventType.SDL_EVENT_WINDOW_MOVED:
|
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.
|
// explicitly requery as there are occasions where what SDL has provided us with is not up-to-date.
|
||||||
int x, y;
|
int x, y;
|
||||||
SDL_GetWindowPosition(SDLWindowHandle, &x, &y);
|
SDL_GetWindowPosition(SDLWindowHandle, &x, &y).ThrowIfFailed();
|
||||||
var newPosition = new Point(x, y);
|
var newPosition = new Point(x, y);
|
||||||
|
|
||||||
if (!newPosition.Equals(Position))
|
if (!newPosition.Equals(Position))
|
||||||
@@ -605,7 +605,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
windowMaximised = maximized;
|
windowMaximised = maximized;
|
||||||
}
|
}
|
||||||
|
|
||||||
var newDisplayID = SDL_GetDisplayForWindow(SDLWindowHandle);
|
var newDisplayID = SDL_GetDisplayForWindow(SDLWindowHandle).ThrowIfFailed();
|
||||||
|
|
||||||
if (displayID != newDisplayID)
|
if (displayID != newDisplayID)
|
||||||
{
|
{
|
||||||
@@ -676,9 +676,9 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
case WindowState.Normal:
|
case WindowState.Normal:
|
||||||
Size = sizeWindowed.Value;
|
Size = sizeWindowed.Value;
|
||||||
|
|
||||||
SDL_RestoreWindow(SDLWindowHandle);
|
SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed();
|
||||||
SDL_SetWindowSize(SDLWindowHandle, Size.Width, Size.Height);
|
SDL_SetWindowSize(SDLWindowHandle, Size.Width, Size.Height).LogErrorIfFailed();
|
||||||
SDL_SetWindowResizable(SDLWindowHandle, Resizable);
|
SDL_SetWindowResizable(SDLWindowHandle, Resizable).LogErrorIfFailed();
|
||||||
|
|
||||||
readWindowPositionFromConfig(state, display);
|
readWindowPositionFromConfig(state, display);
|
||||||
break;
|
break;
|
||||||
@@ -690,8 +690,8 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
|
|
||||||
ensureWindowOnDisplay(display);
|
ensureWindowOnDisplay(display);
|
||||||
|
|
||||||
SDL_SetWindowFullscreenMode(SDLWindowHandle, &closestMode);
|
SDL_SetWindowFullscreenMode(SDLWindowHandle, &closestMode).LogErrorIfFailed();
|
||||||
SDL_SetWindowFullscreen(SDLWindowHandle, true);
|
SDL_SetWindowFullscreen(SDLWindowHandle, true).LogErrorIfFailed();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WindowState.FullscreenBorderless:
|
case WindowState.FullscreenBorderless:
|
||||||
@@ -699,16 +699,16 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case WindowState.Maximised:
|
case WindowState.Maximised:
|
||||||
SDL_RestoreWindow(SDLWindowHandle);
|
SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed();
|
||||||
|
|
||||||
ensureWindowOnDisplay(display);
|
ensureWindowOnDisplay(display);
|
||||||
|
|
||||||
SDL_MaximizeWindow(SDLWindowHandle);
|
SDL_MaximizeWindow(SDLWindowHandle).LogErrorIfFailed();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WindowState.Minimised:
|
case WindowState.Minimised:
|
||||||
ensureWindowOnDisplay(display);
|
ensureWindowOnDisplay(display);
|
||||||
SDL_MinimizeWindow(SDLWindowHandle);
|
SDL_MinimizeWindow(SDLWindowHandle).LogErrorIfFailed();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -721,7 +721,8 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
return false;
|
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";
|
string type = windowState == WindowState.Fullscreen ? "fullscreen" : "desktop";
|
||||||
|
|
||||||
if (mode != null)
|
if (mode != null)
|
||||||
@@ -768,7 +769,7 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
{
|
{
|
||||||
if (tryGetDisplayAtIndex(display.Index, out var requestedID))
|
if (tryGetDisplayAtIndex(display.Index, out var requestedID))
|
||||||
{
|
{
|
||||||
if (requestedID == SDL_GetDisplayForWindow(SDLWindowHandle))
|
if (requestedID == SDL_GetDisplayForWindow(SDLWindowHandle).LogErrorIfFailed())
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,8 +845,8 @@ namespace osu.Framework.Platform.SDL3
|
|||||||
ensureWindowOnDisplay(display);
|
ensureWindowOnDisplay(display);
|
||||||
|
|
||||||
// this is a generally sane method of handling borderless, and works well on macOS and linux.
|
// this is a generally sane method of handling borderless, and works well on macOS and linux.
|
||||||
SDL_SetWindowFullscreenMode(SDLWindowHandle, null);
|
SDL_SetWindowFullscreenMode(SDLWindowHandle, null).LogErrorIfFailed();
|
||||||
SDL_SetWindowFullscreen(SDLWindowHandle, true);
|
SDL_SetWindowFullscreen(SDLWindowHandle, true).LogErrorIfFailed();
|
||||||
|
|
||||||
return display.Bounds.Size;
|
return display.Bounds.Size;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ namespace osu.Framework.Platform.Windows
|
|||||||
&& RelativeMouseMode)
|
&& RelativeMouseMode)
|
||||||
{
|
{
|
||||||
var pt = PointToScreen(new Point((int)LastMousePosition.Value.X, (int)LastMousePosition.Value.Y));
|
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)
|
protected override unsafe Size SetBorderless(Display display)
|
||||||
{
|
{
|
||||||
SDL_SetWindowBordered(SDLWindowHandle, false);
|
SDL_SetWindowBordered(SDLWindowHandle, false).LogErrorIfFailed();
|
||||||
|
|
||||||
var newSize = display.Bounds.Size;
|
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
|
// we also trick the game into thinking the window has normal size: see Size setter override
|
||||||
newSize += new Size(windows_borderless_width_hack, 0);
|
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;
|
Position = display.Bounds.Location;
|
||||||
|
|
||||||
return newSize;
|
return newSize;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using System.Linq;
|
|||||||
using ManagedBass;
|
using ManagedBass;
|
||||||
using ManagedBass.Mix;
|
using ManagedBass.Mix;
|
||||||
using ManagedBass.Wasapi;
|
using ManagedBass.Wasapi;
|
||||||
|
using osu.Framework.Audio.Asio;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Development;
|
using osu.Framework.Development;
|
||||||
@@ -124,13 +125,15 @@ namespace osu.Framework.Threading
|
|||||||
private WasapiProcedure? wasapiProcedure;
|
private WasapiProcedure? wasapiProcedure;
|
||||||
private WasapiNotifyProcedure? wasapiNotifyProcedure;
|
private WasapiNotifyProcedure? wasapiNotifyProcedure;
|
||||||
|
|
||||||
|
private BassAsio.AsioProcedure asioProcedure = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If a global mixer is being used, this will be the BASS handle for it.
|
/// 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.
|
/// If non-null, all game mixers should be added to this mixer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Bindable<int?> globalMixerHandle = new Bindable<int?>();
|
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);
|
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.
|
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,10 +142,40 @@ namespace osu.Framework.Threading
|
|||||||
if (!Bass.Init(deviceId, Flags: (DeviceInitFlags)128)) // 128 == BASS_DEVICE_REINIT
|
if (!Bass.Init(deviceId, Flags: (DeviceInitFlags)128)) // 128 == BASS_DEVICE_REINIT
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (useExperimentalWasapi)
|
switch (outputMode)
|
||||||
attemptWasapiInitialisation();
|
{
|
||||||
else
|
case AudioThreadOutputMode.Default:
|
||||||
freeWasapi();
|
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);
|
initialised_devices.Add(deviceId);
|
||||||
return true;
|
return true;
|
||||||
@@ -160,6 +193,7 @@ namespace osu.Framework.Threading
|
|||||||
Bass.Free();
|
Bass.Free();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
freeAsio();
|
||||||
freeWasapi();
|
freeWasapi();
|
||||||
|
|
||||||
if (selectedDevice != deviceId && canSelectDevice(selectedDevice))
|
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)
|
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
|
||||||
return false;
|
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.
|
// To keep things in a sane state let's only keep one device initialised via wasapi.
|
||||||
freeWasapi();
|
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.
|
// This is intentionally initialised inline and stored to a field.
|
||||||
// If we don't do this, it gets GC'd away.
|
// If we don't do this, it gets GC'd away.
|
||||||
@@ -238,11 +274,15 @@ namespace osu.Framework.Threading
|
|||||||
if (notify == WasapiNotificationType.DefaultOutput)
|
if (notify == WasapiNotificationType.DefaultOutput)
|
||||||
{
|
{
|
||||||
freeWasapi();
|
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")}");
|
Logger.Log($"Initialising BassWasapi for device {wasapiDevice}...{(initialised ? "success!" : "FAILED")}");
|
||||||
|
|
||||||
if (!initialised)
|
if (!initialised)
|
||||||
@@ -267,6 +307,93 @@ namespace osu.Framework.Threading
|
|||||||
globalMixerHandle.Value = null;
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user