From b68d1e6b96f66b4fd791db5b2ac111de817b84b7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:43:55 +0000 Subject: [PATCH 1/2] Fix audio notification crash and missing file types in settings This change addresses a critical bug where the application would crash when attempting to play a notification sound due to a race condition in `AudioService.cs` when renting a shared `AudioPlaybackDevice`. It adds a `try-catch` block for `ObjectDisposedException` to safely handle disposed devices and re-initialize them. Additionally, it fixes an issue where users could not select custom notification sound files because the `AudioFileTypes` list in `NotificationSettingsPage.axaml.cs` was empty. Common audio file extensions and an "All files" option have been added. Fixes #1485 Co-authored-by: kaokao221 <88539021+kaokao221@users.noreply.github.com> --- ClassIsland/Services/AudioService.cs | 12 +++++++++--- .../SettingPages/NotificationSettingsPage.axaml.cs | 6 +++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/ClassIsland/Services/AudioService.cs b/ClassIsland/Services/AudioService.cs index 0bfcd470a..b70dea2e5 100644 --- a/ClassIsland/Services/AudioService.cs +++ b/ClassIsland/Services/AudioService.cs @@ -51,9 +51,15 @@ public AudioEngine AudioEngine { 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); + return lease; + } + catch (ObjectDisposedException) + { + } } if (TryInitializeDefaultPlaybackDeviceInternal() is not { } device) 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(); From 8749214e15106dfe8065006f8172291ed9e11a52 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:18:21 +0000 Subject: [PATCH 2/2] Fix audio crash with debounce and add audio file types in settings This commit addresses a critical application crash (Access Violation) caused by rapid initialization and disposal of the audio device (miniaudio backend) when playing notification sounds. Changes: - Implemented a debounce mechanism in `AudioService.cs` using `System.Threading.Timer` to keep the shared `AudioPlaybackDevice` alive for 5 seconds after the last use. This prevents rapid re-initialization cycles that were triggering the native crash. - Added a `try-catch` block for `ObjectDisposedException` when renting the audio device as an additional safety measure. - Populated `AudioFileTypes` in `NotificationSettingsPage.axaml.cs` with common audio file extensions (*.wav, *.mp3, *.ogg, *.flac, *.m4a, *.wma, *.aac) and `FilePickerFileTypes.All` to allow users to select custom sound files, fixing an issue where the file picker filter was empty. Fixes #1485 Co-authored-by: kaokao221 <88539021+kaokao221@users.noreply.github.com> --- ClassIsland/Services/AudioService.cs | 61 +++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/ClassIsland/Services/AudioService.cs b/ClassIsland/Services/AudioService.cs index b70dea2e5..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,16 +50,22 @@ public AudioEngine AudioEngine { lock (_audioPlaybackDeviceInitializeLock) { + // 每次获取设备时,重置释放计时器 + _releaseTimer?.Change(Timeout.Infinite, Timeout.Infinite); + if (_sharedAudioPlaybackDevice?.IsValueDisposed == false) { 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() 失败(竞态条件),则忽略并重新初始化 } } @@ -67,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 @@ -136,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 +}