From 14f801be6ea478678209c381fd188d7a58eadf93 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Mon, 18 May 2026 01:29:40 +0800
Subject: [PATCH] fix: avoid recorder stop deadlock
---
TODO.md | 9 ++++
VoiceSnapGo/app.go | 2
VoiceSnapGo/internal/audio/recorder.go | 59 ++++++++++++++++++-----------
VoiceSnapGo/build/darwin/Info.plist | 2
4 files changed, 48 insertions(+), 24 deletions(-)
diff --git a/TODO.md b/TODO.md
index 22ecd19..2a89645 100644
--- a/TODO.md
+++ b/TODO.md
@@ -17,6 +17,15 @@
## Done
+- [2026-05-18] 修复停止录音时 `ma_device_stop` 与音频回调互等导致 App 假死的问题。
+ - 根因: `Recorder.StopAndGetSamples()` 持有录音互斥锁调用 `device.Stop()`;底层 `ma_device_stop` 等待音频回调结束,而音频回调同时等待同一把锁,形成互等。
+ - 修复: 停止录音时先在锁内摘除设备并标记非录音,释放锁后再调用 `Stop/Uninit`,避免底层停止设备时阻塞音频回调。
+ - 增加停止设备过程日志: `Stopping audio device`、`Audio device stopped`、`Audio device uninitialized`。
+ - build: `20260518.0125`
+ - 输出: `VoiceSnapGo/build/local/arm64/VoiceSnap.app`
+ - 输出: `VoiceSnapGo/build/local/arm64/VoiceSnap-2.1.1-build20260518.0125-arm64-local.dmg`
+ - 兼容输出: `VoiceSnapGo/build/local/arm64/VoiceSnap-2.1.1-arm64-local.dmg` 已替换为同一份新包。
+ - 已安装到 `/Applications/VoiceSnap.app`,并在日志中验证一次完整停止链路。
- [2026-05-17] 放大菜单栏图标的有效绘制区域,改善深色菜单栏里图标过小的问题。
- `VoiceSnapGo/build/darwin/tray_icon.png` 的有效内容区域从约 `156x156` 放大到约 `220x220`。
- build: `20260517.2306`
diff --git a/VoiceSnapGo/app.go b/VoiceSnapGo/app.go
index 4422ddb..c296b47 100755
--- a/VoiceSnapGo/app.go
+++ b/VoiceSnapGo/app.go
@@ -27,7 +27,7 @@
const (
appVersion = "2.1.1"
- appBuild = "20260517.2306"
+ appBuild = "20260518.0125"
appDisplayVersion = appVersion + " (build " + appBuild + ")"
appName = "VoiceSnap"
diff --git a/VoiceSnapGo/build/darwin/Info.plist b/VoiceSnapGo/build/darwin/Info.plist
index cbbc452..82b1146 100755
--- a/VoiceSnapGo/build/darwin/Info.plist
+++ b/VoiceSnapGo/build/darwin/Info.plist
@@ -17,7 +17,7 @@
<key>CFBundleShortVersionString</key>
<string>2.1.1</string>
<key>CFBundleVersion</key>
- <string>20260517.2306</string>
+ <string>20260518.0125</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSMicrophoneUsageDescription</key>
diff --git a/VoiceSnapGo/internal/audio/recorder.go b/VoiceSnapGo/internal/audio/recorder.go
index 94bdd10..43dc995 100755
--- a/VoiceSnapGo/internal/audio/recorder.go
+++ b/VoiceSnapGo/internal/audio/recorder.go
@@ -9,9 +9,9 @@
)
const (
- sampleRate = 16000
- channels = 1
- bitsPerSample = 16
+ sampleRate = 16000
+ channels = 1
+ bitsPerSample = 16
silenceThreshold = 0.05
)
@@ -25,8 +25,8 @@
type Recorder struct {
mu sync.Mutex
- ctx *malgo.AllocatedContext
- device *malgo.Device
+ ctx *malgo.AllocatedContext
+ device *malgo.Device
// PCM buffer (16-bit signed, little-endian)
pcmBuf []byte
@@ -35,11 +35,11 @@
preferredDevice string
// State
- isRecording bool
- maxVolume float64
- currentVolume float64
- volumeCallback func(float64)
- deviceChangeCb func(string)
+ isRecording bool
+ maxVolume float64
+ currentVolume float64
+ volumeCallback func(float64)
+ deviceChangeCb func(string)
}
// NewRecorder creates a new audio recorder backed by malgo.
@@ -151,19 +151,21 @@
// Stop stops recording and discards audio data.
func (r *Recorder) Stop() {
+ device := r.detachDevice()
+ stopAndUninitDevice(device)
+
r.mu.Lock()
defer r.mu.Unlock()
-
- r.stopDevice()
r.pcmBuf = nil
}
// StopAndGetSamples stops recording and returns the captured audio as float32 samples.
func (r *Recorder) StopAndGetSamples() []float32 {
+ device := r.detachDevice()
+ stopAndUninitDevice(device)
+
r.mu.Lock()
defer r.mu.Unlock()
-
- r.stopDevice()
if len(r.pcmBuf) < 2 {
return nil
@@ -203,23 +205,36 @@
// Close releases all audio resources.
func (r *Recorder) Close() {
+ device := r.detachDevice()
+ stopAndUninitDevice(device)
+
r.mu.Lock()
defer r.mu.Unlock()
-
- r.stopDevice()
if r.ctx != nil {
r.ctx.Free()
r.ctx = nil
}
}
-func (r *Recorder) stopDevice() {
- if r.device != nil {
- r.device.Stop()
- r.device.Uninit()
- r.device = nil
- }
+func (r *Recorder) detachDevice() *malgo.Device {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ device := r.device
+ r.device = nil
r.isRecording = false
+ return device
+}
+
+func stopAndUninitDevice(device *malgo.Device) {
+ if device == nil {
+ return
+ }
+ logger.Info("Stopping audio device")
+ device.Stop()
+ logger.Info("Audio device stopped")
+ device.Uninit()
+ logger.Info("Audio device uninitialized")
}
func (r *Recorder) onData(input []byte) {
--
Gitblit v1.9.3