From 853d590d97bec5b292a258d5d19db1f12f7780a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20T=C3=B3th?= Date: Wed, 29 Apr 2020 20:48:55 +0200 Subject: [PATCH] Partially refactor stuff --- audio/audio.go | 53 ---------------------- audio/encoding.go | 13 ++++++ audio/ffmpeg.go | 102 +++++++++++++++++++++++++++++++++++++++++++ audio/pulsectl.go | 50 +++++++++++++++++++++ main.go | 55 +++++++++-------------- pulsectl/pulsectl.go | 32 -------------- 6 files changed, 186 insertions(+), 119 deletions(-) delete mode 100644 audio/audio.go create mode 100644 audio/encoding.go create mode 100644 audio/ffmpeg.go create mode 100644 audio/pulsectl.go delete mode 100644 pulsectl/pulsectl.go diff --git a/audio/audio.go b/audio/audio.go deleted file mode 100644 index 04e3a98..0000000 --- a/audio/audio.go +++ /dev/null @@ -1,53 +0,0 @@ -package audio - -import ( - "os/exec" - "fmt" - "regexp" -) - - -type MicStreamConfig struct { - Format string - Device string - Encoding string - Bitrate int - Channels int -} - -func StreamMic(config MicStreamConfig) *exec.Cmd { - cmd := exec.Command("ffmpeg", "-re", - "-f", config.Format, - "-i", config.Device, - "-f", config.Encoding, - "-ar", fmt.Sprintf("%d", config.Bitrate), - "-ac", fmt.Sprintf("%d", config.Channels), - "-", - ) - return cmd -} - -func GetMicDevice() (string, error) { - cmd := exec.Command("ffmpeg", - "-f", "avfoundation", - "-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 "" -} diff --git a/audio/encoding.go b/audio/encoding.go new file mode 100644 index 0000000..cfc4c29 --- /dev/null +++ b/audio/encoding.go @@ -0,0 +1,13 @@ +package audio + +type Encoding struct { + Codec string + Bitrate int + Channels int +} + +var defaultEncoding = Encoding{ + Codec: "s16le", + Bitrate: 44100, + Channels: 2, +} diff --git a/audio/ffmpeg.go b/audio/ffmpeg.go new file mode 100644 index 0000000..b2e63aa --- /dev/null +++ b/audio/ffmpeg.go @@ -0,0 +1,102 @@ +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", 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 "" +} diff --git a/audio/pulsectl.go b/audio/pulsectl.go new file mode 100644 index 0000000..f8074af --- /dev/null +++ b/audio/pulsectl.go @@ -0,0 +1,50 @@ +package audio + +import ( + "os/exec" + "fmt" + "path" + "runtime" +) + + +type pulsectl struct { + PipeName string + PipeDir string + Encoding Encoding +} + +func NewPulsectl() *pulsectl { + if runtime.GOOS != "linux" { + panic(fmt.Errorf("audio.pulsectl is only supported on Linux")) + } + return &pulsectl{ + PipeName: "remote-mic", + PipeDir: "/dev/shm", + Encoding: defaultEncoding, + } +} + +func (p *pulsectl) LoadPipeSourceModule() error { + cmd := exec.Command("pactl", "load-module", "module-pipe-source", + fmt.Sprintf("source_name=%s", p.PipeName), + fmt.Sprintf("file=%s", path.Join(p.PipeDir, p.PipeName)), + fmt.Sprintf("format=%s", p.Encoding.Codec), + fmt.Sprintf("rate=%d", p.Encoding.Bitrate), + fmt.Sprintf("channels=%d", p.Encoding.Channels), + ) + outBytes, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to load pipe source module:\n%s", string(outBytes)) + } + return nil +} + +func (p *pulsectl) UnloadPipeSourceModule() error { + cmd := exec.Command("pactl", "unload-module", "module-pipe-source") + outBytes, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to unload pipe source module:\n%s", string(outBytes)) + } + return nil +} diff --git a/main.go b/main.go index 4d2cfb5..85af05e 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,6 @@ import ( "io" "time" "remote-mic/socketops" - "remote-mic/pulsectl" "remote-mic/audio" ) @@ -28,43 +27,31 @@ func socketExample() { } func pulsectlExample() { - pulsectl.LoadPipeSource(pulsectl.PipeSourceConfig{ - "remote-mic", - "/dev/shm", - "s16le", - 44100, - 2, - }) - time.Sleep(10 * time.Second) - pulsectl.UnloadPipeSource() -} - -func micStreamExample() { - cmd := audio.StreamMic(audio.MicStreamConfig{ - "avfoundation", - ":0", - "s16le", - 44100, - 2, - }) - - pipe, _ := cmd.StdoutPipe() - - cmd.Start() - - for { - io.Copy(os.Stdout, pipe) - } -} - -func getDeviceExample() { - device, err := audio.GetMicDevice() + pulsectl := audio.NewPulsectl() + err := pulsectl.LoadPipeSourceModule() if err != nil { panic(err) } - fmt.Println(device) + time.Sleep(10 * time.Second) + pulsectl.UnloadPipeSourceModule() +} + +func micStreamExample() { + mic, err := audio.NewMicrophone() + if err != nil { + panic(err) + } + err = mic.Start() + if err != nil { + panic(err) + } + defer mic.Stop() + + for { + io.Copy(os.Stdout, mic.AudioStream()) + } } func main() { - getDeviceExample() + pulsectlExample() } diff --git a/pulsectl/pulsectl.go b/pulsectl/pulsectl.go deleted file mode 100644 index 4f04d8b..0000000 --- a/pulsectl/pulsectl.go +++ /dev/null @@ -1,32 +0,0 @@ -package pulsectl - -import ( - "os/exec" - "fmt" - "path" -) - - -type PipeSourceConfig struct { - PipeName string - PipeDir string - Encoding string - Bitrate int - Channels int -} - -func LoadPipeSource(config PipeSourceConfig) error { - cmd := exec.Command("pactl", "load-module", "module-pipe-source", - fmt.Sprintf("source_name=%s", config.PipeName), - fmt.Sprintf("file=%s", path.Join(config.PipeDir, config.PipeName)), - fmt.Sprintf("format=%s", config.Encoding), - fmt.Sprintf("rate=%d", config.Bitrate), - fmt.Sprintf("channels=%d", config.Channels), - ) - return cmd.Run() -} - -func UnloadPipeSource() error { - cmd := exec.Command("pactl", "unload-module", "module-pipe-source") - return cmd.Run() -}