Skip to content

Commit f8fc315

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 29c125b commit f8fc315

7 files changed

Lines changed: 381 additions & 3 deletions

File tree

audio1/audio.go

Lines changed: 283 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())
@@ -941,6 +960,29 @@ func (a *Audio) findSource(cardId uint32, activePortName string) *Source {
941960
return nil
942961
}
943962

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

@@ -1868,6 +1910,19 @@ func (a *Audio) initDsgProp() error {
18681910
}
18691911
getReduceNoiseEnabled()
18701912

1913+
getAiReduceNoiseEnabled := func() {
1914+
aiReduceNoiseEnabled, err := a.audioDConfig.GetValueBool(dsgKeyAiReduceNoiseEnabled)
1915+
if err != nil {
1916+
logger.Warningf("aiReduceNoise DConfig: GetValueBool failed: %v", err)
1917+
return
1918+
}
1919+
logger.Infof("aiReduceNoise: DConfig=%v, calling setAiReduceNoise", aiReduceNoiseEnabled)
1920+
a.setAiReduceNoise(aiReduceNoiseEnabled)
1921+
}
1922+
// NOTE: do NOT call getAiReduceNoiseEnabled() during initDsgProp — it runs
1923+
// before refresh() and would get an empty default-source name. The actual
1924+
// init is done in initAiReduceNoise() after refresh().
1925+
18711926
a.audioDConfig.ConnectValueChanged(func(key string) {
18721927
switch key {
18731928
case dsgKeyAutoSwitchPort:
@@ -1884,6 +1939,8 @@ func (a *Audio) initDsgProp() error {
18841939
getMonoEnabled()
18851940
case dsgKeyVolumeIncrease:
18861941
a.handleVolumeIncrease()
1942+
case dsgKeyAiReduceNoiseEnabled:
1943+
getAiReduceNoiseEnabled()
18871944
}
18881945
})
18891946

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

audio1/audio_dbusutil.go

Lines changed: 27 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)