diff --git a/ClassIsland/Services/AudioService.cs b/ClassIsland/Services/AudioService.cs index 0bfcd470a..2395b160b 100644 --- a/ClassIsland/Services/AudioService.cs +++ b/ClassIsland/Services/AudioService.cs @@ -23,6 +23,7 @@ public class AudioService(ILogger logger) : IAudioService private ILogger Logger { get; } = logger; private RefCounted? _sharedAudioPlaybackDevice; + private Timer? _releaseTimer; private object _audioPlaybackDeviceInitializeLock = new(); @@ -49,11 +50,23 @@ 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) @@ -61,12 +74,57 @@ public AudioEngine AudioEngine return null; } _sharedAudioPlaybackDevice = new RefCounted(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 @@ -130,7 +188,12 @@ void OnPlayerOnPlaybackEnded(object? sender, EventArgs args) public void Dispose() { + lock (_audioPlaybackDeviceInitializeLock) + { + _releaseTimer?.Dispose(); + _sharedAudioPlaybackDevice?.Dispose(); + } AudioEngine.Dispose(); GC.SuppressFinalize(this); } -} \ No newline at end of file +} diff --git a/ClassIsland/Views/SettingPages/NotificationSettingsPage.axaml.cs b/ClassIsland/Views/SettingPages/NotificationSettingsPage.axaml.cs index 4c4d93f42..d27590d31 100644 --- a/ClassIsland/Views/SettingPages/NotificationSettingsPage.axaml.cs +++ b/ClassIsland/Views/SettingPages/NotificationSettingsPage.axaml.cs @@ -37,7 +37,11 @@ namespace ClassIsland.Views.SettingPages; public partial class NotificationSettingsPage : SettingsPageBase { public static readonly List AudioFileTypes = [ - + new("Audio Files") + { + Patterns = new[] { "*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.wma", "*.aac" } + }, + FilePickerFileTypes.All ]; public NotificationSettingsViewModel ViewModel { get; } = IAppHost.GetService();