package audio import ( "os/exec" "fmt" "regexp" "runtime" "io" ) type microphone struct { command *exec.Cmd audioStream io.Reader diagnosticOutput io.Reader Format string Device string Encoding Encoding } func NewMicrophone() (*microphone, error) { if runtime.GOOS != "darwin" { panic(fmt.Errorf("audio.microphone is only supported on macOS")) } format := "avfoundation" device, err := getMicDevice(format) if err != nil { return nil, err } return µphone{ Format: format, Device: device, Encoding: defaultEncoding, }, nil } func (m *microphone) Start() error { m.command = exec.Command("ffmpeg", "-re", "-f", m.Format, "-i", fmt.Sprintf(":%s", m.Device), "-f", m.Encoding.Codec, "-ar", fmt.Sprintf("%d", m.Encoding.Bitrate), "-ac", fmt.Sprintf("%d", m.Encoding.Channels), "-", ) // these should never fail, unless you use the module incorrectly audioStream, err := m.command.StdoutPipe() if err != nil { panic(err) } diagnosticOutput, err := m.command.StderrPipe() if err != nil { panic(err) } m.audioStream, m.diagnosticOutput = audioStream, diagnosticOutput return m.command.Start() } func (m *microphone) Stop() error { return m.command.Process.Kill() } func (m *microphone) AudioStream() io.Reader { if m.audioStream == nil { panic(fmt.Errorf("AudioStream() called before Start()")) } return m.audioStream } func (m *microphone) DiagnosticOutput() io.Reader { if m.diagnosticOutput == nil { panic(fmt.Errorf("DiagnosticOutput() called before Start()")) } return m.diagnosticOutput } func getMicDevice(format string) (string, error) { cmd := exec.Command("ffmpeg", "-f", format, "-list_devices", "true", "-i", "\"\"", ) // ignore error, this ffmpeg command returns exit status 1 outputBytes, _ := cmd.CombinedOutput() output := string(outputBytes) parsedOutput := parseDeviceNumber(output) if parsedOutput == "" { return "", fmt.Errorf("Command: %s\nFailed to parse FFmpeg output:\n%s", cmd, output) } return parsedOutput, nil } func parseDeviceNumber(ffmpegOutput string) string { re := regexp.MustCompile(`\[(\d)\].*?[Mm]icrophone`) matches := re.FindStringSubmatch(ffmpegOutput) if (len(matches) == 2) { return matches[1] } return "" }