Skip to content

Commit c5f2cf6

Browse files
committed
feat(audio1): add AI noise reduction support
1. Added AiReduceNoise and AiNoiseSourcePath D-Bus properties for toggling AI noise reduction 2. Implemented writeAiReduceNoise callback and DConfig persistence for AI noise reduction state 3. Added rnnoise virtual source handling: skip port check, prevent auto-switch when active 4. Added GetDefaultInputPortAnyCard() helper for AI noise recovery across cards 5. Registered CheckPort method for Meter and SinkInput in D-Bus export 6. Added aiReduceNoiseEnabled DConfig key for global AI noise reduction setting Log: Add AI noise reduction feature with D-Bus properties, DConfig persistence, and rnnoise virtual source handling feat(audio1): 添加 AI 降噪功能支持 1. 添加 AiReduceNoise 和 AiNoiseSourcePath D-Bus 属性用于开关 AI 降噪 2. 实现 writeAiReduceNoise 回调及 DConfig 持久化 AI 降噪状态 3. 添加 rnnoise 虚拟声源处理:跳过端口检查,开启时阻止自动切换 4. 添加 GetDefaultInputPortAnyCard() 辅助方法用于跨声卡恢复 AI 降噪 5. 为 Meter 和 SinkInput 注册 CheckPort 方法到 D-Bus 导出 6. 添加 aiReduceNoiseEnabled DConfig 键用于全局 AI 降噪设置 Log: 新增 AI 降噪功能,包含 D-Bus 属性、DConfig 持久化及 rnnoise 虚拟声源处理 PMS: STORY-41229
1 parent 70ebba1 commit c5f2cf6

7 files changed

Lines changed: 386 additions & 3 deletions

File tree

audio1/audio.go

Lines changed: 288 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import (
99
"fmt"
1010
"math"
1111
"os"
12+
"os/exec"
1213
"regexp"
1314
"sort"
15+
"strconv"
1416
"strings"
1517
"sync"
1618
"time"
@@ -63,6 +65,9 @@ const (
6365
dsgKeyBluezModeDefault = "bluezModeDefault"
6466
dsgKeyMonoEnabled = "monoEnabled"
6567
dsgKeyReduceNoiseEnabled = "reduceNoiseEnabled" // 降噪配置,保存用户数据
68+
dsgKeyAiReduceNoiseEnabled = "aiReduceNoiseEnabled"
69+
dsgKeyAiNoiseSourceName = "aiNoisePhysicalSourceName" // 开启AI降噪时持久化的物理麦克风名
70+
rnnoiseSourceName = "effect_output.rnnoise"
6671

6772
dsgKeyFirstRun = "firstRun"
6873
dsgKeyInputVolume = "inputVolume"
@@ -95,7 +100,7 @@ const (
95100
AudioStateChanging = false
96101
)
97102

98-
//go:generate dbusutil-gen -type Audio,Sink,SinkInput,Source,Meter -import github.com/godbus/dbus audio.go sink.go sinkinput.go source.go meter.go
103+
//go:generate dbusutil-gen -type Audio,Sink,SinkInput,Source,Meter -import github.com/godbus/dbus/v5 audio.go sink.go sinkinput.go source.go meter.go
99104
//go:generate dbusutil-gen em -type Audio,Sink,SinkInput,Source,Meter
100105

101106
func objectPathSliceEqual(v1, v2 []dbus.ObjectPath) bool {
@@ -151,6 +156,12 @@ type Audio struct {
151156

152157
ReduceNoise bool `prop:"access:rw"`
153158

159+
AiReduceNoise bool `prop:"access:rw"`
160+
161+
AiNoiseSourcePath dbus.ObjectPath
162+
163+
aiNoiseSourceName string
164+
154165
defaultPaCfg defaultPaConfig
155166

156167
// 最大音量
@@ -240,6 +251,10 @@ func newAudio(service *dbusutil.Service) *Audio {
240251
a.PausePlayer = false
241252
a.ReduceNoise = false
242253
a.emitPropChangedReduceNoise(a.ReduceNoise)
254+
a.AiReduceNoise = false
255+
a.emitPropChangedAiReduceNoise(a.AiReduceNoise)
256+
a.AiNoiseSourcePath = "/"
257+
a.emitPropChangedAiNoiseSourcePath(a.AiNoiseSourcePath)
243258
a.CurrentAudioServer = a.getCurrentAudioServer()
244259
a.headphoneUnplugAutoPause, err = a.audioDConfig.GetValueBool(dsgKeyHeadphoneUnplugAutoPause)
245260
if err != nil {
@@ -804,6 +819,10 @@ func (a *Audio) init() error {
804819

805820
// 更新本地数据
806821
a.refresh()
822+
823+
// After refresh, run AI noise-reduction init with both DConfig and
824+
// source list available.
825+
a.initAiReduceNoise()
807826
logger.Debug("init cards")
808827
a.PropsMu.Lock()
809828
a.setPropCards(a.cards.string())
@@ -822,6 +841,11 @@ func (a *Audio) init() error {
822841

823842
a.autoSwitchPort()
824843

844+
// Retry initAiReduceNoise in case the PA context was already connected
845+
// before stateChan was registered (the ContextStateReady event would
846+
// have been consumed by pulse internals and never reach our handler).
847+
a.initAiReduceNoise()
848+
825849
return nil
826850
}
827851

@@ -941,6 +965,29 @@ func (a *Audio) findSource(cardId uint32, activePortName string) *Source {
941965
return nil
942966
}
943967

968+
// getSourcePathByName returns the D-Bus object path of the source whose Name
969+
// matches, or "/" if not found. Used to expose the physical mic's Source path
970+
// to control center while AI noise reduction is enabled.
971+
func (a *Audio) getSourcePathByName(name string) dbus.ObjectPath {
972+
if name == "" {
973+
return "/"
974+
}
975+
a.mu.Lock()
976+
defer a.mu.Unlock()
977+
for _, source := range a.sources {
978+
if source.Name == name {
979+
return source.getPath()
980+
}
981+
}
982+
return "/"
983+
}
984+
985+
// updateAiNoiseSourcePath refreshes the AiNoiseSourcePath D-Bus property from
986+
// aiNoiseSourceName. Call under PropsMu.
987+
func (a *Audio) updateAiNoiseSourcePath() {
988+
a.setPropAiNoiseSourcePath(a.getSourcePathByName(a.aiNoiseSourceName))
989+
}
990+
944991
func (a *Audio) SetPortEnabled(cardId uint32, portName string, enabled bool) *dbus.Error {
945992
logger.Infof("dbus call SetPortEnabled with cardId %d, portName %s and enabled %t", cardId, portName, enabled)
946993

@@ -1868,6 +1915,19 @@ func (a *Audio) initDsgProp() error {
18681915
}
18691916
getReduceNoiseEnabled()
18701917

1918+
getAiReduceNoiseEnabled := func() {
1919+
aiReduceNoiseEnabled, err := a.audioDConfig.GetValueBool(dsgKeyAiReduceNoiseEnabled)
1920+
if err != nil {
1921+
logger.Warningf("aiReduceNoise DConfig: GetValueBool failed: %v", err)
1922+
return
1923+
}
1924+
logger.Infof("aiReduceNoise: DConfig=%v, calling setAiReduceNoise", aiReduceNoiseEnabled)
1925+
a.setAiReduceNoise(aiReduceNoiseEnabled)
1926+
}
1927+
// NOTE: do NOT call getAiReduceNoiseEnabled() during initDsgProp — it runs
1928+
// before refresh() and would get an empty default-source name. The actual
1929+
// init is done in initAiReduceNoise() after refresh().
1930+
18711931
a.audioDConfig.ConnectValueChanged(func(key string) {
18721932
switch key {
18731933
case dsgKeyAutoSwitchPort:
@@ -1884,6 +1944,8 @@ func (a *Audio) initDsgProp() error {
18841944
getMonoEnabled()
18851945
case dsgKeyVolumeIncrease:
18861946
a.handleVolumeIncrease()
1947+
case dsgKeyAiReduceNoiseEnabled:
1948+
getAiReduceNoiseEnabled()
18871949
}
18881950
})
18891951

@@ -2009,3 +2071,228 @@ func (a *Audio) isModuleExist(name string) bool {
20092071
}
20102072
return false
20112073
}
2074+
2075+
func (a *Audio) paDefaultSourceIs(target string) bool {
2076+
out, err := exec.Command("pactl", "get-default-source").Output()
2077+
if err != nil {
2078+
return false
2079+
}
2080+
return strings.TrimSpace(string(out)) == target
2081+
}
2082+
2083+
func (a *Audio) recoverPhysicalSourceName() string {
2084+
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
2085+
if runtimeDir == "" {
2086+
runtimeDir = "/run/user/" + strconv.Itoa(os.Getuid())
2087+
}
2088+
2089+
// 1) rnnoise-toggle's saved original source (tmpfs, may be gone after reboot).
2090+
stateFile := runtimeDir + "/rnnoise-toggle-original-source"
2091+
if data, err := os.ReadFile(stateFile); err == nil {
2092+
name := strings.TrimSpace(string(data))
2093+
if name != "" && name != rnnoiseSourceName {
2094+
return name
2095+
}
2096+
}
2097+
2098+
// 2) ConfigKeeper: find the source whose ActivePort matches the user's
2099+
// saved DefaultInputPort. This is the most reliable source across reboots
2100+
// because it records the port the user manually selected.
2101+
defaultPort := GetConfigKeeper().GetDefaultInputPortAnyCard()
2102+
if defaultPort != "" {
2103+
a.mu.Lock()
2104+
for _, s := range a.sources {
2105+
if s.ActivePort.Name == defaultPort && s.Name != rnnoiseSourceName {
2106+
name := s.Name
2107+
a.mu.Unlock()
2108+
return name
2109+
}
2110+
}
2111+
a.mu.Unlock()
2112+
}
2113+
2114+
// 3) DConfig — survives reboot but can be stale/wrong on multi-mic systems.
2115+
if name, err := a.audioDConfig.GetValueString(dsgKeyAiNoiseSourceName); err == nil {
2116+
name = strings.TrimSpace(name)
2117+
if name != "" && name != rnnoiseSourceName {
2118+
return name
2119+
}
2120+
}
2121+
2122+
// 4) Fallback: scan pactl source list for a non-rnnoise, non-monitor source.
2123+
out, err := exec.Command("pactl", "list", "sources", "short").Output()
2124+
if err != nil {
2125+
return ""
2126+
}
2127+
for _, line := range strings.Split(string(out), "\n") {
2128+
fields := strings.Fields(line)
2129+
if len(fields) >= 2 {
2130+
name := fields[1]
2131+
if name != rnnoiseSourceName && !strings.Contains(name, ".monitor") {
2132+
return name
2133+
}
2134+
}
2135+
}
2136+
return ""
2137+
}
2138+
2139+
func (a *Audio) setAiReduceNoise(enable bool) error {
2140+
a.PropsMu.Lock()
2141+
sameAsCurrent := a.AiReduceNoise == enable
2142+
if sameAsCurrent {
2143+
// Already in the requested state.
2144+
a.PropsMu.Unlock()
2145+
return nil
2146+
}
2147+
2148+
// Compute aiNoiseSourceName under lock before rnnoise-toggle changes
2149+
// the PA default source (which would trigger updateDefaultSource on
2150+
// the PA goroutine and race on aiNoiseSourceName).
2151+
var micName string
2152+
var micNameRecovered bool // true if micName came from recoverPhysicalSourceName()
2153+
if enable {
2154+
if defaultPort := GetConfigKeeper().GetDefaultInputPortAnyCard(); defaultPort != "" {
2155+
a.mu.Lock()
2156+
for _, s := range a.sources {
2157+
if s.ActivePort.Name == defaultPort && s.Name != rnnoiseSourceName {
2158+
micName = s.Name
2159+
break
2160+
}
2161+
}
2162+
a.mu.Unlock()
2163+
}
2164+
if micName == "" {
2165+
micName = a.getDefaultSourceName()
2166+
if micName == rnnoiseSourceName || micName == "" {
2167+
micName = a.recoverPhysicalSourceName()
2168+
micNameRecovered = true
2169+
}
2170+
}
2171+
a.aiNoiseSourceName = micName
2172+
} else {
2173+
micName = a.aiNoiseSourceName
2174+
}
2175+
a.PropsMu.Unlock()
2176+
2177+
// Phase 2: Execute PA operations (outside PropsMu to avoid blocking
2178+
// PA event handlers like updateDefaultSource).
2179+
if enable {
2180+
// Enable rnnoise: rnnoise-toggle saves the current default source
2181+
// (the physical mic) and sets effect_output.rnnoise as default.
2182+
cmd := exec.Command("rnnoise-toggle", "-q", "on")
2183+
err := cmd.Run()
2184+
if err != nil {
2185+
// rnnoise-toggle -q on exits 1 when default source is already
2186+
// effect_output.rnnoise (save_original_source rejects its own).
2187+
if a.paDefaultSourceIs(rnnoiseSourceName) {
2188+
logger.Warningf("rnnoise-toggle -q on reported failure "+
2189+
"but default source is already %s; continuing", rnnoiseSourceName)
2190+
} else {
2191+
logger.Warning("failed to enable ai reduce noise:", err)
2192+
return err
2193+
}
2194+
}
2195+
} else {
2196+
args := []string{"-q", "off"}
2197+
if micName != "" {
2198+
args = append(args, micName)
2199+
}
2200+
cmd := exec.Command("rnnoise-toggle", args...)
2201+
err := cmd.Run()
2202+
if err != nil {
2203+
if !a.paDefaultSourceIs(rnnoiseSourceName) {
2204+
logger.Warningf("rnnoise-toggle -q off reported failure "+
2205+
"but default source is no longer %s; continuing", rnnoiseSourceName)
2206+
} else {
2207+
logger.Warning("failed to disable ai reduce noise:", err)
2208+
return err
2209+
}
2210+
}
2211+
}
2212+
2213+
// Phase 3: Update property and persist DConfig (only on success).
2214+
a.PropsMu.Lock()
2215+
a.setPropAiReduceNoise(enable)
2216+
if enable {
2217+
// AiNoiseSourceName was computed in Phase 1; refresh the path so
2218+
// control center can bind the volume slider to the physical mic.
2219+
a.updateAiNoiseSourcePath()
2220+
} else {
2221+
// Clear the path — DefaultSource is the physical mic again.
2222+
if a.AiNoiseSourcePath != "/" {
2223+
a.AiNoiseSourcePath = "/"
2224+
a.emitPropChangedAiNoiseSourcePath(a.AiNoiseSourcePath)
2225+
}
2226+
}
2227+
a.PropsMu.Unlock()
2228+
if err := a.audioDConfig.SetValue(dsgKeyAiReduceNoiseEnabled, enable); err != nil {
2229+
logger.Warning("failed to persist aiReduceNoiseEnabled to dconfig:", err)
2230+
}
2231+
if enable && !micNameRecovered {
2232+
if err := a.audioDConfig.SetValue(dsgKeyAiNoiseSourceName, micName); err != nil {
2233+
logger.Warning("failed to persist aiNoisePhysicalSourceName to dconfig:", err)
2234+
}
2235+
} else if !enable {
2236+
if err := a.audioDConfig.SetValue(dsgKeyAiNoiseSourceName, ""); err != nil {
2237+
logger.Warning("failed to clear aiNoisePhysicalSourceName in dconfig:", err)
2238+
}
2239+
}
2240+
return nil
2241+
}
2242+
2243+
func (a *Audio) initAiReduceNoise() {
2244+
logger.Debug("initAiReduceNoise")
2245+
2246+
// 1. Check if rnnoise source already exists in the source list.
2247+
a.mu.Lock()
2248+
rnnoiseFound := false
2249+
for _, src := range a.sources {
2250+
if src.Name == rnnoiseSourceName {
2251+
rnnoiseFound = true
2252+
break
2253+
}
2254+
}
2255+
a.mu.Unlock()
2256+
2257+
// 2. Check DConfig value.
2258+
aiReduceNoiseEnabled, err := a.audioDConfig.GetValueBool(dsgKeyAiReduceNoiseEnabled)
2259+
if err != nil {
2260+
logger.Warningf("initAiReduceNoise: DConfig GetValueBool failed: %v", err)
2261+
}
2262+
2263+
shouldEnable := aiReduceNoiseEnabled
2264+
if err != nil && rnnoiseFound {
2265+
shouldEnable = true
2266+
}
2267+
if !shouldEnable {
2268+
logger.Infof("initAiReduceNoise: DConfig=%v (err=%v), rnnoiseFound=%v, nothing to do",
2269+
aiReduceNoiseEnabled, err != nil, rnnoiseFound)
2270+
return
2271+
}
2272+
2273+
logger.Infof("initAiReduceNoise: DConfig=%v, rnnoiseFound=%v, calling setAiReduceNoise(true)",
2274+
aiReduceNoiseEnabled, rnnoiseFound)
2275+
if err := a.setAiReduceNoise(true); err != nil {
2276+
logger.Warning("initAiReduceNoise: setAiReduceNoise(true) failed:", err)
2277+
return
2278+
}
2279+
2280+
a.PropsMu.RLock()
2281+
micName := a.aiNoiseSourceName
2282+
a.PropsMu.RUnlock()
2283+
if micName != "" {
2284+
a.mu.Lock()
2285+
var micSource *Source
2286+
for _, s := range a.sources {
2287+
if s.Name == micName {
2288+
micSource = s
2289+
break
2290+
}
2291+
}
2292+
a.mu.Unlock()
2293+
if micSource != nil {
2294+
logger.Infof("initAiReduceNoise: resumeSourceConfig for %s", micName)
2295+
a.resumeSourceConfig(micSource)
2296+
}
2297+
}
2298+
}

0 commit comments

Comments
 (0)