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 ".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() }