From 24f551a39eae849d891da26cc91f90d354e3a2db Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 12 Jul 2026 02:44:05 +0800
Subject: [PATCH] chore(mas): refresh codebase memory after split

---
 C1.source/privatevoice.src/internal/audio/recorder.go |  254 ++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 231 insertions(+), 23 deletions(-)

diff --git a/C1.source/privatevoice.src/internal/audio/recorder.go b/C1.source/privatevoice.src/internal/audio/recorder.go
index 9cf8b92..44b3a2f 100755
--- a/C1.source/privatevoice.src/internal/audio/recorder.go
+++ b/C1.source/privatevoice.src/internal/audio/recorder.go
@@ -1,6 +1,8 @@
 package audio
 
 import (
+	"errors"
+	"fmt"
 	"math"
 	"sync"
 	"time"
@@ -18,9 +20,12 @@
 	maxPCMBufferBytes     = sampleRate * channels * (bitsPerSample / 8) * maxRecordingSeconds
 	MaxRecordingDuration  = time.Duration(maxRecordingSeconds) * time.Second
 	maxPendingDeviceStops = 2
+	deviceStartWatchDelay = 2 * time.Second
 )
 
 var pendingDeviceStops = make(chan struct{}, maxPendingDeviceStops)
+
+var ErrMicrophonePermission = errors.New("microphone permission not granted")
 
 // InputDevice represents an audio input device.
 type InputDevice struct {
@@ -28,12 +33,41 @@
 	IsDefault bool   `json:"isDefault"`
 }
 
+type recorderAudioContext interface {
+	devices(malgo.DeviceType) ([]malgo.DeviceInfo, error)
+	initDevice(malgo.DeviceConfig, malgo.DeviceCallbacks) (recorderDevice, error)
+	free()
+}
+
+type malgoRecorderContext struct {
+	allocated *malgo.AllocatedContext
+}
+
+func (c *malgoRecorderContext) devices(deviceType malgo.DeviceType) ([]malgo.DeviceInfo, error) {
+	return c.allocated.Context.Devices(deviceType)
+}
+
+func (c *malgoRecorderContext) initDevice(config malgo.DeviceConfig, callbacks malgo.DeviceCallbacks) (recorderDevice, error) {
+	return malgo.InitDevice(c.allocated.Context, config, callbacks)
+}
+
+func (c *malgoRecorderContext) free() {
+	c.allocated.Free()
+}
+
+type recorderDevice interface {
+	Start() error
+	Stop() error
+	Uninit()
+}
+
 // Recorder captures audio from the default input device using malgo (miniaudio).
 type Recorder struct {
-	mu sync.Mutex
+	mu          sync.Mutex
+	lifecycleMu sync.Mutex
 
-	ctx    *malgo.AllocatedContext
-	device *malgo.Device
+	ctx    recorderAudioContext
+	device recorderDevice
 
 	// PCM buffer (16-bit signed, little-endian)
 	pcmBuf []byte
@@ -43,7 +77,11 @@
 
 	// State
 	isRecording    bool
+	startRequested bool
+	isStarting     bool
+	stopAfterStart bool
 	bufferLimitHit bool
+	closing        bool
 	maxVolume      float64
 	currentVolume  float64
 	volumeCallback func(float64)
@@ -58,7 +96,7 @@
 		logger.Error("Failed to init malgo context: %v", err)
 		return &Recorder{}
 	}
-	return &Recorder{ctx: ctx}
+	return &Recorder{ctx: &malgoRecorderContext{allocated: ctx}}
 }
 
 // OnVolume registers a callback for real-time volume updates.
@@ -77,10 +115,14 @@
 
 // ListInputDevices returns all available audio capture devices.
 func (r *Recorder) ListInputDevices() []InputDevice {
-	if r.ctx == nil {
+	r.lifecycleMu.Lock()
+	defer r.lifecycleMu.Unlock()
+
+	ctx := r.ctx
+	if ctx == nil {
 		return nil
 	}
-	infos, err := r.ctx.Context.Devices(malgo.Capture)
+	infos, err := ctx.devices(malgo.Capture)
 	if err != nil {
 		logger.Error("Failed to list devices: %v", err)
 		return nil
@@ -105,10 +147,16 @@
 
 // Start begins recording from the preferred (or default) capture device.
 func (r *Recorder) Start() error {
-	r.mu.Lock()
-	defer r.mu.Unlock()
+	r.lifecycleMu.Lock()
+	defer r.lifecycleMu.Unlock()
 
-	if r.isRecording {
+	r.mu.Lock()
+	if r.closing {
+		r.mu.Unlock()
+		return fmt.Errorf("audio recorder is closed")
+	}
+	if r.isRecording || r.startRequested || r.isStarting {
+		r.mu.Unlock()
 		return nil
 	}
 
@@ -116,6 +164,47 @@
 	r.bufferLimitHit = false
 	r.maxVolume = 0
 	r.currentVolume = 0
+	r.startRequested = true
+	r.stopAfterStart = false
+	ctx := r.ctx
+	preferredDevice := r.preferredDevice
+	r.mu.Unlock()
+	logger.Info("Audio recorder start requested sample_rate=%d channels=%d preferred_device=%q", sampleRate, channels, preferredDevice)
+
+	auth := ensureMicrophoneAuthorization()
+	if !auth.granted {
+		r.mu.Lock()
+		r.startRequested = false
+		r.stopAfterStart = false
+		r.isRecording = false
+		r.mu.Unlock()
+		logger.Error("Microphone permission not granted; audio recorder start blocked state=%s", auth.state)
+		return fmt.Errorf("%w: %s", ErrMicrophonePermission, auth.state)
+	}
+	logger.Info("Microphone permission ready state=%s", auth.state)
+
+	r.mu.Lock()
+	stopBeforeAuthComplete := r.stopAfterStart || r.closing
+	startWasClosed := r.closing
+	if stopBeforeAuthComplete {
+		r.startRequested = false
+		r.stopAfterStart = false
+		r.isRecording = false
+		r.mu.Unlock()
+		if startWasClosed {
+			return fmt.Errorf("audio recorder closed during start")
+		}
+		return nil
+	}
+	r.mu.Unlock()
+
+	if ctx == nil {
+		r.mu.Lock()
+		r.startRequested = false
+		r.stopAfterStart = false
+		r.mu.Unlock()
+		return fmt.Errorf("audio context is not initialized")
+	}
 
 	deviceConfig := malgo.DefaultDeviceConfig(malgo.Capture)
 	deviceConfig.Capture.Format = malgo.FormatS16
@@ -124,11 +213,11 @@
 	deviceConfig.PeriodSizeInMilliseconds = 50
 
 	// Use preferred device if set
-	if r.preferredDevice != "" {
-		infos, err := r.ctx.Context.Devices(malgo.Capture)
+	if preferredDevice != "" {
+		infos, err := ctx.devices(malgo.Capture)
 		if err == nil {
 			for _, info := range infos {
-				if info.Name() == r.preferredDevice {
+				if info.Name() == preferredDevice {
 					deviceConfig.Capture.DeviceID = info.ID.Pointer()
 					break
 				}
@@ -142,18 +231,98 @@
 		},
 	}
 
-	device, err := malgo.InitDevice(r.ctx.Context, deviceConfig, callbacks)
+	device, err := ctx.initDevice(deviceConfig, callbacks)
 	if err != nil {
+		r.mu.Lock()
+		r.startRequested = false
+		r.stopAfterStart = false
+		r.mu.Unlock()
 		return err
 	}
+	logger.Info("Audio device initialized; starting native device")
 
-	if err := device.Start(); err != nil {
+	r.mu.Lock()
+	stopBeforeStart := r.stopAfterStart || r.closing
+	startWasClosed = r.closing
+	if r.isRecording || stopBeforeStart {
+		r.startRequested = false
+		r.stopAfterStart = false
+		r.isRecording = false
+		r.mu.Unlock()
 		device.Uninit()
-		return err
+		if startWasClosed {
+			return fmt.Errorf("audio recorder closed during start")
+		}
+		return nil
 	}
-
 	r.device = device
 	r.isRecording = true
+	r.startRequested = false
+	r.isStarting = true
+	r.stopAfterStart = false
+	r.mu.Unlock()
+
+	deviceStartBegin := time.Now()
+	watchDone := make(chan struct{})
+	go func() {
+		select {
+		case <-watchDone:
+		case <-time.After(deviceStartWatchDelay):
+			logger.Error("Native audio device start still pending after %dms", deviceStartWatchDelay.Milliseconds())
+		}
+	}()
+	if err := device.Start(); err != nil {
+		close(watchDone)
+		r.mu.Lock()
+		stillCurrent := r.device == device
+		if stillCurrent {
+			r.device = nil
+			r.isRecording = false
+			r.startRequested = false
+			r.isStarting = false
+			r.stopAfterStart = false
+		}
+		r.mu.Unlock()
+		if stillCurrent {
+			device.Uninit()
+		}
+		return err
+	}
+	close(watchDone)
+	logger.Info("Native audio device start returned result=success start_ms=%d", time.Since(deviceStartBegin).Milliseconds())
+
+	r.mu.Lock()
+	stillCurrent := r.device == device
+	shouldStop := stillCurrent && (r.stopAfterStart || r.closing || !r.isRecording)
+	startWasClosed = stillCurrent && r.closing
+	if !stillCurrent && r.device == device {
+		r.device = nil
+		r.isRecording = false
+		r.isStarting = false
+		r.stopAfterStart = false
+	}
+	if stillCurrent {
+		r.startRequested = false
+		r.isStarting = false
+	}
+	if shouldStop {
+		r.device = nil
+		r.isRecording = false
+		r.stopAfterStart = false
+	}
+	r.mu.Unlock()
+	if !stillCurrent {
+		stopAndUninitDevice(device)
+		return fmt.Errorf("audio recorder closed during start")
+	}
+	if shouldStop {
+		stopAndUninitDevice(device)
+		if startWasClosed {
+			return fmt.Errorf("audio recorder closed during start")
+		}
+		return nil
+	}
+
 	logger.Info("Recording started (16kHz/16-bit/mono)")
 	return nil
 }
@@ -208,10 +377,14 @@
 
 // GetDeviceName returns the name of the current capture device.
 func (r *Recorder) GetDeviceName() string {
-	if r.ctx == nil {
+	r.lifecycleMu.Lock()
+	defer r.lifecycleMu.Unlock()
+
+	ctx := r.ctx
+	if ctx == nil {
 		return "Default"
 	}
-	devices, err := r.ctx.Context.Devices(malgo.Capture)
+	devices, err := ctx.devices(malgo.Capture)
 	if err != nil || len(devices) == 0 {
 		return "Default"
 	}
@@ -220,32 +393,67 @@
 
 // Close releases all audio resources.
 func (r *Recorder) Close() {
+	r.mu.Lock()
+	r.closing = true
+	r.mu.Unlock()
+
+	r.lifecycleMu.Lock()
+	defer r.lifecycleMu.Unlock()
+
 	device := r.detachDevice()
 	stopAndUninitDevice(device)
 
 	r.mu.Lock()
 	defer r.mu.Unlock()
 	if r.ctx != nil {
-		r.ctx.Free()
+		r.ctx.free()
 		r.ctx = nil
 	}
 }
 
-func (r *Recorder) detachDevice() *malgo.Device {
+func (r *Recorder) detachDevice() recorderDevice {
 	r.mu.Lock()
 	defer r.mu.Unlock()
 
 	device := r.device
+	if r.startRequested && !r.isStarting {
+		logger.Info("Recording stop requested before audio device was published; deferring startup cleanup")
+		r.isRecording = false
+		r.stopAfterStart = true
+		return nil
+	}
+	if r.isStarting {
+		logger.Info("Recording stop requested while audio device start is still pending; deferring native device cleanup")
+		r.isRecording = false
+		r.stopAfterStart = true
+		return nil
+	}
 	r.device = nil
 	r.isRecording = false
 	return device
 }
 
-func (r *Recorder) detachDeviceAndPCM() (*malgo.Device, []byte) {
+func (r *Recorder) detachDeviceAndPCM() (recorderDevice, []byte) {
 	r.mu.Lock()
 	defer r.mu.Unlock()
 
 	device := r.device
+	if r.startRequested && !r.isStarting {
+		logger.Info("Recording samples requested before audio device was published; snapshotting buffered PCM and deferring startup cleanup")
+		r.isRecording = false
+		r.stopAfterStart = true
+		pcm := append([]byte(nil), r.pcmBuf...)
+		r.pcmBuf = nil
+		return nil, pcm
+	}
+	if r.isStarting {
+		logger.Info("Recording samples requested while audio device start is still pending; snapshotting buffered PCM and deferring native device cleanup")
+		r.isRecording = false
+		r.stopAfterStart = true
+		pcm := append([]byte(nil), r.pcmBuf...)
+		r.pcmBuf = nil
+		return nil, pcm
+	}
 	r.device = nil
 	r.isRecording = false
 	pcm := append([]byte(nil), r.pcmBuf...)
@@ -253,7 +461,7 @@
 	return device, pcm
 }
 
-func stopAndUninitDeviceAsync(device *malgo.Device) {
+func stopAndUninitDeviceAsync(device recorderDevice) {
 	if device == nil {
 		return
 	}
@@ -281,7 +489,7 @@
 	}()
 }
 
-func stopAndUninitDevice(device *malgo.Device) {
+func stopAndUninitDevice(device recorderDevice) {
 	if device == nil {
 		return
 	}

--
Gitblit v1.9.3