2020-04-29 18:48:55 +00:00
|
|
|
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,
|
2020-04-29 19:12:33 +00:00
|
|
|
"-i", fmt.Sprintf(":%s", m.Device),
|
2020-04-29 18:48:55 +00:00
|
|
|
"-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 ""
|
|
|
|
}
|