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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 68 additions & 5 deletions ClassIsland/Services/AudioService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class AudioService(ILogger<AudioService> logger) : IAudioService
private ILogger<AudioService> Logger { get; } = logger;

private RefCounted<AudioPlaybackDevice>? _sharedAudioPlaybackDevice;
private Timer? _releaseTimer;

private object _audioPlaybackDeviceInitializeLock = new();

Expand All @@ -49,24 +50,81 @@ public AudioEngine AudioEngine
{
lock (_audioPlaybackDeviceInitializeLock)
{
// 每次获取设备时,重置释放计时器
_releaseTimer?.Change(Timeout.Infinite, Timeout.Infinite);

if (_sharedAudioPlaybackDevice?.IsValueDisposed == false)
{
var lease = _sharedAudioPlaybackDevice.Rent();
Logger.LogDebug("使用了缓存的音频设备 {} (Id={})", lease.Value.Info?.Name, lease.Value.Info?.Id);
return lease;
try
{
var lease = _sharedAudioPlaybackDevice.Rent();
Logger.LogDebug("使用了缓存的音频设备 {} (Id={})", lease.Value.Info?.Name, lease.Value.Info?.Id);
// 成功租借后,重新启动计时器,在 5 秒后释放设备
_releaseTimer?.Change(5000, Timeout.Infinite);
return lease;
}
catch (ObjectDisposedException)
{
// 如果虽然 IsValueDisposed 为 false 但 Rent() 失败(竞态条件),则忽略并重新初始化
}
}

if (TryInitializeDefaultPlaybackDeviceInternal() is not { } device)
{
return null;
}
_sharedAudioPlaybackDevice = new RefCounted<AudioPlaybackDevice>(device);
// 初始化计时器,5秒后自动释放 _sharedAudioPlaybackDevice
if (_releaseTimer == null)
{
_releaseTimer = new Timer(ReleaseTimerCallback, null, 5000, Timeout.Infinite);
}
else
{
_releaseTimer.Change(5000, Timeout.Infinite);
}

var lease2 = _sharedAudioPlaybackDevice.Rent();
_sharedAudioPlaybackDevice.Dispose();
// 注意:此处不再立即调用 _sharedAudioPlaybackDevice.Dispose()
// 而是等待计时器触发后调用,或者在 AudioService Dispose 时调用。
// 这样可以避免频繁初始化/释放设备导致的崩溃。
return lease2;
}
});

private void ReleaseTimerCallback(object? state)
{
lock (_audioPlaybackDeviceInitializeLock)
{
if (_sharedAudioPlaybackDevice != null && !_sharedAudioPlaybackDevice.IsValueDisposed)
{
Logger.LogDebug("音频设备闲置超时,正在释放...");
_sharedAudioPlaybackDevice.Dispose();
// 此时 _sharedAudioPlaybackDevice 只是减少了引用计数。
// 如果仍有 Lease 在使用,设备不会真正关闭。
// 只有当所有 Lease 都释放后,设备才会关闭。
// 我们不需要将 _sharedAudioPlaybackDevice 置为 null,因为 IsValueDisposed 会变成 true (当引用计数归零且 Dispose 真正执行后?? 不对)

// RefCounted.Dispose() 只是 Decrement RefCount。
// 如果 RefCount 归零,Value.Dispose() 被调用,且 Value = null。
// RefCounted.IsValueDisposed => _value == null.

// 如果此时还有 Lease,RefCount > 0。IsValueDisposed 为 false。
// 如果我们下次再来 Rent(),IsValueDisposed 为 false。
// 我们 Rent() 成功。RefCount++。
// 但是我们之前已经调用了一次 Dispose() (即 ReleaseTimerCallback)。
// RefCounted 没有 "AddRef" 给 Creator 的方法。
// 如果我们继续复用这个对象,当 Lease 释放时,RefCount 会减少。
// 如果我们不重新 "拥有" 它,它最终会死掉。

// 所以,为了安全起见,我们应该丢弃这个 _sharedAudioPlaybackDevice 引用,
// 让下次请求创建一个新的。
// 这样旧的 _sharedAudioPlaybackDevice 会在所有 Lease 结束后自动销毁。
_sharedAudioPlaybackDevice = null;
}
}
}

private AudioPlaybackDevice? TryInitializeDefaultPlaybackDeviceInternal()
{
try
Expand Down Expand Up @@ -130,7 +188,12 @@ void OnPlayerOnPlaybackEnded(object? sender, EventArgs args)

public void Dispose()
{
lock (_audioPlaybackDeviceInitializeLock)
{
_releaseTimer?.Dispose();
_sharedAudioPlaybackDevice?.Dispose();
}
AudioEngine.Dispose();
GC.SuppressFinalize(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ namespace ClassIsland.Views.SettingPages;
public partial class NotificationSettingsPage : SettingsPageBase
{
public static readonly List<FilePickerFileType> AudioFileTypes = [

new("Audio Files")
{
Patterns = new[] { "*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.wma", "*.aac" }
},
FilePickerFileTypes.All
];

public NotificationSettingsViewModel ViewModel { get; } = IAppHost.GetService<NotificationSettingsViewModel>();
Expand Down