@@ -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 ())
@@ -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+
944986func (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+ }
0 commit comments