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