Partially refactor stuff
This commit is contained in:
parent
86d6751754
commit
853d590d97
@ -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 ""
|
|
||||||
}
|
|
13
audio/encoding.go
Normal file
13
audio/encoding.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package audio
|
||||||
|
|
||||||
|
type Encoding struct {
|
||||||
|
Codec string
|
||||||
|
Bitrate int
|
||||||
|
Channels int
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultEncoding = Encoding{
|
||||||
|
Codec: "s16le",
|
||||||
|
Bitrate: 44100,
|
||||||
|
Channels: 2,
|
||||||
|
}
|
102
audio/ffmpeg.go
Normal file
102
audio/ffmpeg.go
Normal file
@ -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 ""
|
||||||
|
}
|
50
audio/pulsectl.go
Normal file
50
audio/pulsectl.go
Normal file
@ -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
|
||||||
|
}
|
55
main.go
55
main.go
@ -6,7 +6,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
"remote-mic/socketops"
|
"remote-mic/socketops"
|
||||||
"remote-mic/pulsectl"
|
|
||||||
"remote-mic/audio"
|
"remote-mic/audio"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,43 +27,31 @@ func socketExample() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func pulsectlExample() {
|
func pulsectlExample() {
|
||||||
pulsectl.LoadPipeSource(pulsectl.PipeSourceConfig{
|
pulsectl := audio.NewPulsectl()
|
||||||
"remote-mic",
|
err := pulsectl.LoadPipeSourceModule()
|
||||||
"/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()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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() {
|
func main() {
|
||||||
getDeviceExample()
|
pulsectlExample()
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user