diff --git a/audio1/audio.go b/audio1/audio.go index d60ea98e3..5d4cd1b9b 100644 --- a/audio1/audio.go +++ b/audio1/audio.go @@ -9,6 +9,7 @@ import ( "fmt" "math" "os" + "os/exec" "regexp" "sort" "strings" @@ -63,6 +64,8 @@ const ( dsgKeyBluezModeDefault = "bluezModeDefault" dsgKeyMonoEnabled = "monoEnabled" dsgKeyReduceNoiseEnabled = "reduceNoiseEnabled" // 降噪配置,保存用户数据 + dsgKeyAiReduceNoiseEnabled = "aiReduceNoiseEnabled" + rnnoiseSourceName = "effect_output.rnnoise" dsgKeyFirstRun = "firstRun" dsgKeyInputVolume = "inputVolume" @@ -95,7 +98,7 @@ const ( AudioStateChanging = false ) -//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 +//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 //go:generate dbusutil-gen em -type Audio,Sink,SinkInput,Source,Meter func objectPathSliceEqual(v1, v2 []dbus.ObjectPath) bool { @@ -151,6 +154,12 @@ type Audio struct { ReduceNoise bool `prop:"access:rw"` + AiReduceNoise bool `prop:"access:rw"` + + AiNoiseSourcePath dbus.ObjectPath + + aiNoiseSourceName string + defaultPaCfg defaultPaConfig // 最大音量 @@ -240,6 +249,10 @@ func newAudio(service *dbusutil.Service) *Audio { a.PausePlayer = false a.ReduceNoise = false a.emitPropChangedReduceNoise(a.ReduceNoise) + a.AiReduceNoise = false + a.emitPropChangedAiReduceNoise(a.AiReduceNoise) + a.AiNoiseSourcePath = "/" + a.emitPropChangedAiNoiseSourcePath(a.AiNoiseSourcePath) a.CurrentAudioServer = a.getCurrentAudioServer() a.headphoneUnplugAutoPause, err = a.audioDConfig.GetValueBool(dsgKeyHeadphoneUnplugAutoPause) if err != nil { @@ -804,6 +817,10 @@ func (a *Audio) init() error { // 更新本地数据 a.refresh() + + // After refresh, run AI noise-reduction init with both DConfig and + // source list available. + a.initAiReduceNoise() logger.Debug("init cards") a.PropsMu.Lock() a.setPropCards(a.cards.string()) @@ -941,6 +958,29 @@ func (a *Audio) findSource(cardId uint32, activePortName string) *Source { return nil } +// getSourcePathByName returns the D-Bus object path of the source whose Name +// matches, or "/" if not found. Used to expose the physical mic's Source path +// to control center while AI noise reduction is enabled. +func (a *Audio) getSourcePathByName(name string) dbus.ObjectPath { + if name == "" { + return "/" + } + a.mu.Lock() + defer a.mu.Unlock() + for _, source := range a.sources { + if source.Name == name { + return source.getPath() + } + } + return "/" +} + +// updateAiNoiseSourcePath refreshes the AiNoiseSourcePath D-Bus property from +// aiNoiseSourceName. Call under PropsMu. +func (a *Audio) updateAiNoiseSourcePath() { + a.setPropAiNoiseSourcePath(a.getSourcePathByName(a.aiNoiseSourceName)) +} + func (a *Audio) SetPortEnabled(cardId uint32, portName string, enabled bool) *dbus.Error { logger.Infof("dbus call SetPortEnabled with cardId %d, portName %s and enabled %t", cardId, portName, enabled) @@ -1167,6 +1207,23 @@ func (a *Audio) setSourcePortDirectly(cardId uint32, portName string) error { logger.Debugf("set default source: %s", source.Name) a.ctx.SetDefaultSource(source.Name) } + // 如果 AI 降噪已开启,在切换输入设备后重新应用 rnnoise, + // 确保新选中的物理麦克风作为 rnnoise 的 master 源。 + a.PropsMu.RLock() + aiNoise := a.AiReduceNoise + a.PropsMu.RUnlock() + if aiNoise { + card, err := a.cards.get(source.Card) + if err != nil || !isBluezAudio(card.core.Name) { + logger.Debug("AiReduceNoise is on, re-applying after source switch") + if err := a.setAiReduceNoise(true, source.Name); err != nil { + logger.Warning("AiReduceNoise: re-apply after source switch failed:", err) + } + } else { + logger.Debug("AiReduceNoise: skip re-apply for bluetooth device") + a.setPropAiNoiseSourcePath("/") + } + } return nil } @@ -1590,6 +1647,31 @@ func (a *Audio) getMasterNameFromVirtualDevice(device string) string { } } } + if strings.Contains(device, "rnnoise") { + out, err := exec.Command("pw-link", "-l").Output() + if err != nil { + return "" + } + lines := strings.Split(string(out), "\n") + for i, line := range lines { + if !strings.Contains(line, "effect_input.rnnoise:input") { + continue + } + for _, s := range lines[i+1:] { + s = strings.TrimSpace(s) + if strings.HasPrefix(s, "|<-") { + name := strings.Fields(s)[1] + if idx := strings.LastIndex(name, ":"); idx > 0 { + return name[:idx] + } + return name + } + if s == "" || !strings.HasPrefix(s, "|") { + break + } + } + } + } return "" } @@ -1868,6 +1950,19 @@ func (a *Audio) initDsgProp() error { } getReduceNoiseEnabled() + getAiReduceNoiseEnabled := func() { + aiReduceNoiseEnabled, err := a.audioDConfig.GetValueBool(dsgKeyAiReduceNoiseEnabled) + if err != nil { + logger.Warningf("aiReduceNoise DConfig: GetValueBool failed: %v", err) + return + } + logger.Infof("aiReduceNoise: DConfig=%v, calling setAiReduceNoise", aiReduceNoiseEnabled) + a.setAiReduceNoise(aiReduceNoiseEnabled, "") + } + // NOTE: do NOT call getAiReduceNoiseEnabled() during initDsgProp — it runs + // before refresh() and would get an empty default-source name. The actual + // init is done in initAiReduceNoise() after refresh(). + a.audioDConfig.ConnectValueChanged(func(key string) { switch key { case dsgKeyAutoSwitchPort: @@ -1884,6 +1979,8 @@ func (a *Audio) initDsgProp() error { getMonoEnabled() case dsgKeyVolumeIncrease: a.handleVolumeIncrease() + case dsgKeyAiReduceNoiseEnabled: + getAiReduceNoiseEnabled() } }) @@ -2009,3 +2106,148 @@ func (a *Audio) isModuleExist(name string) bool { } return false } + +func (a *Audio) paDefaultSourceIs(target string) bool { + out, err := exec.Command("pactl", "get-default-source").Output() + if err != nil { + return false + } + return strings.TrimSpace(string(out)) == target +} + +func (a *Audio) setAiReduceNoise(enable bool, sourceName string) error { + a.PropsMu.Lock() + if !enable && a.AiReduceNoise == enable { + // Already disabled, nothing to do. + a.PropsMu.Unlock() + return nil + } + + var micName string + if enable { + micName = sourceName + if micName == "" { + micName = a.getDefaultSourceName() + } + if micName == rnnoiseSourceName { + micName = a.getMasterNameFromVirtualDevice(micName) + } + a.aiNoiseSourceName = micName + } else { + micName = a.aiNoiseSourceName + } + a.PropsMu.Unlock() + + // Phase 2: Execute PA operations (outside PropsMu to avoid blocking + // PA event handlers like updateDefaultSource). + if enable { + // Enable rnnoise: rnnoise-toggle saves the current default source + // (the physical mic) and sets effect_output.rnnoise as default. + cmd := exec.Command("rnnoise-toggle", "-q", "on") + err := cmd.Run() + if err != nil { + // rnnoise-toggle -q on exits 1 when default source is already + // effect_output.rnnoise (save_original_source rejects its own). + if a.paDefaultSourceIs(rnnoiseSourceName) { + logger.Warningf("rnnoise-toggle -q on reported failure "+ + "but default source is already %s; continuing", rnnoiseSourceName) + } else { + logger.Warning("failed to enable ai reduce noise:", err) + return err + } + } + } else { + args := []string{"-q", "off"} + if micName != "" { + args = append(args, micName) + } + cmd := exec.Command("rnnoise-toggle", args...) + err := cmd.Run() + if err != nil { + if !a.paDefaultSourceIs(rnnoiseSourceName) { + logger.Warningf("rnnoise-toggle -q off reported failure "+ + "but default source is no longer %s; continuing", rnnoiseSourceName) + } else { + logger.Warning("failed to disable ai reduce noise:", err) + return err + } + } + } + + // Phase 3: Update property and persist DConfig (only on success). + a.PropsMu.Lock() + a.setPropAiReduceNoise(enable) + if enable { + // AiNoiseSourceName was computed in Phase 1; refresh the path so + // control center can bind the volume slider to the physical mic. + a.updateAiNoiseSourcePath() + } else { + // Clear the path — DefaultSource is the physical mic again. + if a.AiNoiseSourcePath != "/" { + a.AiNoiseSourcePath = "/" + a.emitPropChangedAiNoiseSourcePath(a.AiNoiseSourcePath) + } + } + a.PropsMu.Unlock() + if err := a.audioDConfig.SetValue(dsgKeyAiReduceNoiseEnabled, enable); err != nil { + logger.Warning("failed to persist aiReduceNoiseEnabled to dconfig:", err) + } + return nil +} + +func (a *Audio) initAiReduceNoise() { + logger.Debug("initAiReduceNoise") + + // 1. Check if rnnoise source already exists in the source list. + a.mu.Lock() + rnnoiseFound := false + for _, src := range a.sources { + if src.Name == rnnoiseSourceName { + rnnoiseFound = true + break + } + } + a.mu.Unlock() + + // 2. Check DConfig value. + aiReduceNoiseEnabled, err := a.audioDConfig.GetValueBool(dsgKeyAiReduceNoiseEnabled) + if err != nil { + logger.Warningf("initAiReduceNoise: DConfig GetValueBool failed: %v", err) + } + + shouldEnable := aiReduceNoiseEnabled + if err != nil && rnnoiseFound { + shouldEnable = true + } + if !shouldEnable { + logger.Infof("initAiReduceNoise: DConfig=%v (err=%v), rnnoiseFound=%v, nothing to do", + aiReduceNoiseEnabled, err != nil, rnnoiseFound) + return + } + + logger.Infof("initAiReduceNoise: DConfig=%v, rnnoiseFound=%v, calling setAiReduceNoise(true)", + aiReduceNoiseEnabled, rnnoiseFound) + if err := a.setAiReduceNoise(true, ""); err != nil { + logger.Warning("initAiReduceNoise: setAiReduceNoise(true) failed:", err) + return + } + + a.PropsMu.RLock() + micName := a.aiNoiseSourceName + a.PropsMu.RUnlock() + if micName != "" { + a.mu.Lock() + var micSource *Source + for _, s := range a.sources { + if s.Name == micName { + micSource = s + break + } + } + a.mu.Unlock() + if micSource != nil { + logger.Infof("initAiReduceNoise: resumeSourceConfig for %s", micName) + a.resumeSourceConfig(micSource) + } + } +} diff --git a/audio1/audio_dbusutil.go b/audio1/audio_dbusutil.go index b6ac90bdd..c1d51396d 100644 --- a/audio1/audio_dbusutil.go +++ b/audio1/audio_dbusutil.go @@ -1,4 +1,4 @@ -// Code generated by "dbusutil-gen -type Audio,Sink,SinkInput,Source,Meter -import github.com/godbus/dbus audio.go sink.go sinkinput.go source.go meter.go"; DO NOT EDIT. +// Code generated by "dbusutil-gen -type Audio,Sink,SinkInput,Source,Meter -import github.com/godbus/dbus/v5 audio.go sink.go sinkinput.go source.go meter.go"; DO NOT EDIT. package audio @@ -344,6 +344,32 @@ func (v *Audio) emitPropChangedReduceNoise(value bool) error { return v.service.EmitPropertyChanged(v, "ReduceNoise", value) } +func (v *Audio) setPropAiReduceNoise(value bool) (changed bool) { + if v.AiReduceNoise != value { + v.AiReduceNoise = value + v.emitPropChangedAiReduceNoise(value) + return true + } + return false +} + +func (v *Audio) emitPropChangedAiReduceNoise(value bool) error { + return v.service.EmitPropertyChanged(v, "AiReduceNoise", value) +} + +func (v *Audio) setPropAiNoiseSourcePath(value dbus.ObjectPath) (changed bool) { + if v.AiNoiseSourcePath != value { + v.AiNoiseSourcePath = value + v.emitPropChangedAiNoiseSourcePath(value) + return true + } + return false +} + +func (v *Audio) emitPropChangedAiNoiseSourcePath(value dbus.ObjectPath) error { + return v.service.EmitPropertyChanged(v, "AiNoiseSourcePath", value) +} + func (v *Audio) setPropMaxUIVolume(value float64) (changed bool) { if v.MaxUIVolume != value { v.MaxUIVolume = value diff --git a/audio1/audio_events.go b/audio1/audio_events.go index e5ee4fe37..782feeaba 100644 --- a/audio1/audio_events.go +++ b/audio1/audio_events.go @@ -83,6 +83,9 @@ func (a *Audio) handleStateChanged() { select { case state := <-a.stateChan: switch state { + case pulse.ContextStateReady: + logger.Debug("re-checking AI noise reduction") + a.initAiReduceNoise() case pulse.ContextStateFailed: logger.Warning("pulseaudio context state failed") a.destroyCtxRelated() @@ -747,6 +750,7 @@ func isPhysicalDevice(deviceName string) bool { "Echo-Cancel", "remap-sink-mono", "null-sink", + "effect_output.rnnoise", } { if strings.Contains(deviceName, virtualDeviceKey) { return false @@ -781,6 +785,16 @@ func (a *Audio) writeReduceNoise(write *dbusutil.PropertyWrite) *dbus.Error { return nil } +// 外部修改AiReduceNoise时触发回调 +func (a *Audio) writeAiReduceNoise(write *dbusutil.PropertyWrite) *dbus.Error { + reduce, ok := write.Value.(bool) + if !ok { + logger.Warning("type is not bool") + return dbusutil.ToError(errors.New("type is not bool")) + } + return dbusutil.ToError(a.setAiReduceNoise(reduce, "")) +} + func (a *Audio) writeKeyPausePlayer(write *dbusutil.PropertyWrite) *dbus.Error { return dbusutil.ToError(func() error { a.PausePlayer = write.Value.(bool) diff --git a/audio1/module.go b/audio1/module.go index 0b5974af7..c2f3552ef 100644 --- a/audio1/module.go +++ b/audio1/module.go @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2018 - 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2018 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -68,6 +68,11 @@ func (m *Module) start() error { logger.Warning("failed to bind callback for ReduceNoise:", err) } + err = so.SetWriteCallback(m.audio, "AiReduceNoise", m.audio.writeAiReduceNoise) + if err != nil { + logger.Warning("failed to bind callback for AiReduceNoise:", err) + } + err = so.SetWriteCallback(m.audio, "PausePlayer", m.audio.writeKeyPausePlayer) if err != nil { diff --git a/audio1/source.go b/audio1/source.go index 4e410fc36..af7f81806 100644 --- a/audio1/source.go +++ b/audio1/source.go @@ -49,8 +49,14 @@ func newSource(sourceInfo *pulse.Source, audio *Audio) *Source { return s } -// 检测端口是否被禁用 +// 检测端口是否被禁用。 +// 对于虚拟声源(如 AI 降噪 rnnoise 虚拟设备),无声卡/活跃端口,跳过检查。 func (s *Source) CheckPort() *dbus.Error { + if s.Card == 0 && s.ActivePort.Name == "" { + // 虚拟声源没有声卡/活跃端口,跳过端口启用状态检查 + return nil + } + enabled, err := s.audio.IsPortEnabled(s.Card, s.ActivePort.Name) if err != nil { return err diff --git a/misc/dsg-configs/org.deepin.dde.daemon.audio.json b/misc/dsg-configs/org.deepin.dde.daemon.audio.json index 59c40f7c1..1051fb6b9 100644 --- a/misc/dsg-configs/org.deepin.dde.daemon.audio.json +++ b/misc/dsg-configs/org.deepin.dde.daemon.audio.json @@ -95,7 +95,7 @@ }, "reduceNoiseEnabled":{ "value": false, - "serial": 0, + "serial": 1, "flags": ["global"], "name": "reduceNoiseEnabled", "name[zh_CN]": "噪音抑制开关", @@ -179,6 +179,15 @@ "description[zh_CN]": "是否禁用ALSA混音器自动静音模式", "permissions": "readwrite", "visibility": "private" + }, + "aiReduceNoiseEnabled": { + "value": true, + "serial": 0, + "name": "AiReduceNoiseEnabled", + "name[zh_CN]": "AI降噪", + "description": "enable ai reduce noise", + "permissions": "readwrite", + "visibility": "private" } } } \ No newline at end of file