142 lines
3.1 KiB
Go
142 lines
3.1 KiB
Go
package audiocap
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
SinkName = "cheater_mic"
|
|
SourceName = "CheaterMic"
|
|
RecordingSink = "recording"
|
|
)
|
|
|
|
type Handle struct {
|
|
// virtmic modules
|
|
sinkModuleID string
|
|
sourceModuleID string
|
|
|
|
// recording modules
|
|
recordingSinkID string
|
|
callLoopbackID string
|
|
ttsLoopbackID string
|
|
}
|
|
|
|
func Setup() (*Handle, error) {
|
|
h := &Handle{}
|
|
|
|
// 1. virtmic null-sink
|
|
sinkID, err := pactlLoad("module-null-sink",
|
|
"sink_name="+SinkName,
|
|
"sink_properties=device.description=CheaterSink",
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("null-sink: %w", err)
|
|
}
|
|
h.sinkModuleID = sinkID
|
|
|
|
// 2. virtmic remap-source
|
|
sourceID, err := pactlLoad("module-remap-source",
|
|
"source_name="+SourceName,
|
|
"master="+SinkName+".monitor",
|
|
"source_properties=device.description=CheaterMic",
|
|
)
|
|
if err != nil {
|
|
h.Teardown()
|
|
return nil, fmt.Errorf("remap-source: %w", err)
|
|
}
|
|
h.sourceModuleID = sourceID
|
|
|
|
// 3. recording null-sink
|
|
recID, err := pactlLoad("module-null-sink",
|
|
"sink_name="+RecordingSink,
|
|
"sink_properties=device.description=Recording",
|
|
)
|
|
if err != nil {
|
|
h.Teardown()
|
|
return nil, fmt.Errorf("recording sink: %w", err)
|
|
}
|
|
h.recordingSinkID = recID
|
|
|
|
// 4. Call-audio loopback: resolve default sink monitor explicitly
|
|
defaultMonitor, err := defaultSinkMonitor()
|
|
if err != nil {
|
|
h.Teardown()
|
|
return nil, fmt.Errorf("resolve default monitor: %w", err)
|
|
}
|
|
callID, err := pactlLoad("module-loopback",
|
|
"source="+defaultMonitor,
|
|
"sink="+RecordingSink,
|
|
"latency_msec=20",
|
|
)
|
|
if err != nil {
|
|
h.Teardown()
|
|
return nil, fmt.Errorf("call loopback: %w", err)
|
|
}
|
|
h.callLoopbackID = callID
|
|
|
|
// 5. TTS loopback: cheater_mic monitor → recording
|
|
ttsID, err := pactlLoad("module-loopback",
|
|
"source="+SinkName+".monitor",
|
|
"sink="+RecordingSink,
|
|
"latency_msec=20",
|
|
)
|
|
if err != nil {
|
|
h.Teardown()
|
|
return nil, fmt.Errorf("tts loopback: %w", err)
|
|
}
|
|
h.ttsLoopbackID = ttsID
|
|
|
|
return h, nil
|
|
}
|
|
|
|
// Teardown unloads in reverse order. Safe to call even after partial Setup.
|
|
func (h *Handle) Teardown() {
|
|
if h.ttsLoopbackID != "" {
|
|
_ = pactlUnload(h.ttsLoopbackID)
|
|
}
|
|
if h.callLoopbackID != "" {
|
|
_ = pactlUnload(h.callLoopbackID)
|
|
}
|
|
if h.recordingSinkID != "" {
|
|
_ = pactlUnload(h.recordingSinkID)
|
|
}
|
|
if h.sourceModuleID != "" {
|
|
_ = pactlUnload(h.sourceModuleID)
|
|
}
|
|
if h.sinkModuleID != "" {
|
|
_ = pactlUnload(h.sinkModuleID)
|
|
}
|
|
}
|
|
|
|
func (h *Handle) SinkForPlayback() string {
|
|
return SinkName
|
|
}
|
|
|
|
// defaultSinkMonitor returns "<default-sink-name>.monitor"
|
|
func defaultSinkMonitor() (string, error) {
|
|
out, err := exec.Command("pactl", "get-default-sink").Output()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sink := strings.TrimSpace(string(out))
|
|
if sink == "" {
|
|
return "", fmt.Errorf("empty default sink")
|
|
}
|
|
return sink + ".monitor", nil
|
|
}
|
|
|
|
func pactlLoad(module string, args ...string) (string, error) {
|
|
cmdArgs := append([]string{"load-module", module}, args...)
|
|
out, err := exec.Command("pactl", cmdArgs...).Output()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(string(out)), nil
|
|
}
|
|
|
|
func pactlUnload(id string) error {
|
|
return exec.Command("pactl", "unload-module", id).Run()
|
|
}
|