@@ -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
101106func 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+
944991func (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