Skip to content

fix: 优化了音频逻辑#1031

Merged
fly602 merged 1 commit intolinuxdeepin:masterfrom
fly602:fix-soundEffect
Feb 27, 2026
Merged

fix: 优化了音频逻辑#1031
fly602 merged 1 commit intolinuxdeepin:masterfrom
fly602:fix-soundEffect

Conversation

@fly602
Copy link
Contributor

@fly602 fly602 commented Feb 9, 2026

  1. 优化了音频初始化配置设置
  2. 优化了音频切换端口检查的逻辑
  3. 优化了音频音量静音的设置

Log: 音频优化
PMS: BUG-350057
Influence: audio

Summary by Sourcery

Refine audio initialization and automatic port switching behavior to better handle dynamic device availability and mute/volume settings.

Bug Fixes:

  • Improve auto-switch logic for input and output ports to skip invalid or unavailable devices and fall back to null-sink when needed.
  • Ensure mute state and mono setting are applied consistently without triggering unnecessary re-switching or acting on non-physical devices.
  • Align sink mute behavior when volume reaches zero with stored mute configuration to avoid inconsistent audio states.

Enhancements:

  • Simplify audio startup flow by loading configuration earlier and delegating initial port selection to the unified auto-switch mechanism.
  • Add looping helper in the priority manager to iterate available ports more robustly for both input and output directions.

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 9, 2026

Reviewer's Guide

Refactors audio initialization and auto-switch logic so that configuration is loaded earlier, port selection iterates robustly over available ports, input/output auto-switch checks are unified, and mute/volume handling on sinks is made more consistent and stateful.

Sequence diagram for updated output auto-switch logic

sequenceDiagram
    participant Audio
    participant PriorityManager
    participant Cards
    participant Ctx
    participant PulseCard

    Audio->>Audio: checkAutoSwitchOutputPort()
    Audio->>Audio: canAutoSwitchPort()?
    alt cannot auto switch
        Audio-->>Audio: return false,0,""
    else can auto switch
        Audio->>Audio: read currentCardName,currentPortName from defaultSink
        loop iterate available output ports
            Audio->>PriorityManager: LoopAvaiablePort(DirectionSink,pos)
            PriorityManager-->>Audio: prefer, pos
            alt no more ports or invalid
                Audio-->>Audio: break loop
            else valid prefer
                Audio->>Cards: getByName(prefer.CardName)
                alt card not found
                    Cards-->>Audio: error
                    Audio-->>Audio: continue
                else card found
                    Cards-->>Audio: card
                    Audio->>Ctx: GetCard(card.Id)
                    alt pulse card not found
                        Ctx-->>Audio: error
                        Audio-->>Audio: continue
                    else pulse card found
                        Ctx-->>Audio: pc
                        Audio->>PulseCard: Ports.Get(prefer.PortName,DirectionSink)
                        alt port not found
                            PulseCard-->>Audio: error
                            Audio-->>Audio: continue
                        else port found
                            PulseCard-->>Audio: port
                            Audio->>Audio: mode = ConfigKeeper.GetMode(card,prefer.PortName)
                            alt different from current sink
                                Audio-->>Audio: return true,card.Id,prefer.PortName
                            else same as current sink
                                Audio-->>Audio: return false,0,""
                            end
                        end
                    end
                end
            end
        end
        Audio-->>Audio: return true,0,""  %% exhausted ports without mismatch
    end
Loading

Sequence diagram for updated input auto-switch logic

sequenceDiagram
    participant Audio
    participant PriorityManager
    participant Cards
    participant Ctx
    participant PulseCard

    Audio->>Audio: checkAutoSwitchInputPort()
    Audio->>Audio: canAutoSwitchPort()?
    alt cannot auto switch
        Audio-->>Audio: return false,0,""
    else can auto switch
        Audio->>Audio: read currentCardName,currentPortName from defaultSource
        loop iterate available input ports
            Audio->>PriorityManager: LoopAvaiablePort(DirectionSource,pos)
            PriorityManager-->>Audio: prefer, pos
            alt no more ports or invalid
                Audio-->>Audio: break loop
            else valid prefer
                Audio->>Cards: getByName(prefer.CardName)
                alt card not found
                    Cards-->>Audio: error
                    Audio-->>Audio: continue
                else card found
                    Cards-->>Audio: card
                    Audio->>Ctx: GetCard(card.Id)
                    alt pulse card not found
                        Ctx-->>Audio: error
                        Audio-->>Audio: continue
                    else pulse card found
                        Ctx-->>Audio: pc
                        Audio->>PulseCard: Ports.Get(prefer.PortName,DirectionSource)
                        alt port not found
                            PulseCard-->>Audio: error
                            Audio-->>Audio: continue
                        else port found
                            PulseCard-->>Audio: port
                            Audio->>Audio: check card.ActiveProfile not nil
                            alt profile exists and port supports profile
                                Audio-->>Audio: compare currentCardName,currentPortName
                                alt different from current
                                    Audio-->>Audio: return true,card.Id,prefer.PortName
                                else same as current
                                    Audio-->>Audio: return false,0,""
                                end
                            else profile missing or unsupported
                                Audio-->>Audio: continue
                            end
                        end
                    end
                end
            end
        end
        Audio-->>Audio: return true,0,""  %% exhausted ports without mismatch
    end
Loading

Sequence diagram for updated sink volume and mute handling

sequenceDiagram
    participant Client
    participant Sink
    participant Audio
    participant Ctx
    participant ConfigKeeper

    Client->>Sink: SetVolume(value,isPlay)
    alt value == 0
        Sink->>Sink: value = 0.001
        Sink->>Sink: setMute(true)
    else value > 0
        Sink->>Sink: setMute(ConfigKeeper.Mute.MuteOutput)
    end
    Sink->>Sink: update channel volume (cVolume.SetAvg)
    Sink->>Ctx: SetSinkVolumeByIndex

    Client->>Sink: SetMute(value)
    Sink->>Sink: setMute(value)
    alt setMute returns error
        Sink-->>Client: dbus.Error
    else success
        Sink->>ConfigKeeper: SetMuteOutput(value)
        Sink-->>Client: ok
    end

    rect rgba(200,200,200,0.2)
    note right of Sink: internal setMute(value)
    Sink->>Sink: CheckPort()
    alt error
        Sink-->>Sink: return error
    else ok
        Sink->>Sink: setPropMute(value)
        alt prop changed
            Sink->>Ctx: SetSinkMuteByIndex
            alt value == false
                Sink->>Sink: playFeedback()
            end
        else no change
        end
        Sink-->>Sink: return nil
    end
    end
Loading

Updated class diagram for audio auto-switch and mute logic

classDiagram
    class Audio {
        +init() error
        +autoSwitchPort()
        +autoSwitchOutputPort() bool
        +autoSwitchInputPort() bool
        +checkAutoSwitchOutputPort() (auto bool, cardId uint32, portName string)
        +checkAutoSwitchInputPort() (auto bool, cardId uint32, portName string)
        +resumeSinkConfig(s *Sink)
        +resumeSourceConfig(s *Source)
        +updateDefaultSink(sinkName string)
        +LoadNullSinkModule()
        +moveSinkInputsToSink(s *Sink)
        +refresh()
        +canAutoSwitchPort() bool
        +getCardNameById(id uint32) string
        -defaultSink *Sink
        -defaultSource *Source
        -cards Cards
        -ctx Context
        -Mono bool
        -PropsMu Mutex
        -setPropCards(v string)
    }

    class PriorityManager {
        +SetTheFirstPort(cardName string, portName string, direction int)
        +GetTheFirstPort(direction int) (*PriorityPort, *Position)
        +LoopAvaiablePort(direction int, pos *Position) (*PriorityPort, *Position)
        -availablePort(mapKey string) bool
        -Input PriorityPortList
        -Output PriorityPortList
    }

    class Position {
        +tp int
        +index int
    }

    class PriorityPort {
        +CardName string
        +PortName string
        +PortType int
    }

    class Sink {
        +Name string
        +index uint32
        +SetVolume(value float64, isPlay bool) *dbus.Error
        +SetMute(value bool) *dbus.Error
        +CheckPort() error
        +playFeedback()
        -audio *Audio
        -cVolume CVolume
        -PropsMu Mutex
        -setMute(value bool) error
        -setPropMute(value bool) bool
    }

    class Source {
        +Name string
    }

    class Cards {
        +getByName(name string) (*Card, error)
        +string() string
    }

    class Card {
        +Id uint32
        +ActiveProfile *Profile
        +Ports PortCollection
        -core CoreCard
    }

    class Profile {
        +Name string
    }

    class PortCollection {
        +Get(portName string, direction int) (*Port, error)
    }

    class Port {
        +Profiles ProfileSet
    }

    class ProfileSet {
        +Exists(name string) bool
    }

    class Context {
        +GetCard(id uint32) (*PulseCard, error)
        +GetDefaultSink() string
        +GetDefaultSource() string
        +SetDefaultSink(name string)
        +SetDefaultSource(name string)
        +SetSinkMuteByIndex(index uint32, value bool)
        +SetSinkVolumeByIndex(index uint32, volume CVolume)
    }

    class PulseCard {
        +Ports PortCollection
    }

    class ConfigKeeper {
        +Load()
        +GetMode(card *Card, portName string) string
        +SetMuteOutput(value bool)
        +Mute MuteConfig
    }

    class MuteConfig {
        +MuteOutput bool
    }

    Audio --> PriorityManager : uses for port selection
    Audio --> Cards : uses for logical cards
    Audio --> Context : uses for PulseAudio context
    Audio --> ConfigKeeper : uses for mode and mute
    Audio "1" o-- "many" Sink : manages
    Audio "1" o-- "many" Source : manages

    PriorityManager --> PriorityPort : manages
    PriorityManager --> Position : iteration state

    Cards --> Card
    Card --> Profile
    Card --> PortCollection
    PortCollection --> Port
    Port --> ProfileSet

    Sink --> Audio
    Sink --> Context
    Sink --> ConfigKeeper

    Context --> PulseCard

    ConfigKeeper --> MuteConfig
Loading

File-Level Changes

Change Details Files
Refined auto-switch logic for output ports to iterate through available ports safely and handle missing/invalid devices more robustly.
  • Replaced single GetTheFirstPort-based selection with a LoopAvaiablePort loop for output ports
  • Added runtime checks that the selected card and port exist in the PulseAudio context before using them
  • Adjusted return paths in checkAutoSwitchOutputPort so failures on one candidate port fall through to try others instead of aborting the whole check
  • Updated logging messages to more clearly distinguish output ports and null-sink paths
audio1/audio_events.go
audio1/priority_manager.go
Refined auto-switch logic for input ports to iterate through available ports safely and respect current profiles and availability.
  • Replaced GetTheFirstPort/GetNextPort iteration with LoopAvaiablePort for input ports
  • Changed port lookup to go through the current PulseAudio card (ctx.GetCard) and handle missing cards/ports gracefully
  • Guarded profile checks with nil checks on ActiveProfile and kept auto-switch decisions consistent with current/default source
  • Removed the obsolete needAutoSwitchInputPort helper in favor of the unified checkAutoSwitchInputPort path
audio1/audio_events.go
audio1/priority_manager.go
Simplified and consolidated auto-switch behavior during initialization to rely on autoSwitchPort.
  • Moved config keeper Load() to occur immediately after loading the null-sink module and before refreshing local state
  • Replaced explicit output/input auto-switch and resume calls in init() with a single autoSwitchPort() call
audio1/audio.go
Adjusted sink resume behavior to avoid conflicting with upcoming auto-switches and to decouple mono setting from auto-switch decisions.
  • Changed resumeSinkConfig to always set mute based only on global mute state, not port enabled flag
  • Added a checkAutoSwitchOutputPort call to skip setting mono when an auto switch is pending to avoid multiple switch triggers
  • Relaxed updateDefaultSink so it always resumes sink config for physical devices regardless of auto-switch state
audio1/audio.go
Reworked sink mute and volume interactions so that mute state is centralized and persisted via configuration.
  • Updated SetVolume to call the internal setMute helper instead of the public SetMute when volume hits zero, and to restore mute from config when volume is non-zero
  • Refactored SetMute to delegate actual muting to a new setMute helper, then persist the mute state via GetConfigKeeper().SetMuteOutput
  • Made setMute responsible for port validation via CheckPort, updating the DBus mute property with setPropMute, and calling SetSinkMuteByIndex only when the property actually changes
  • Removed the previous trivial setMute method that only sent mute to the context without updating state
audio1/sink.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In resumeSinkConfig, the mute setting now ignores !portConfig.Enabled and relies only on MuteOutput; if a port is disabled this may leave it unmuted—double‑check whether the disabled state should still influence mute when resuming sink configuration.
  • The new LoopAvaiablePort helper in PriorityManager has a spelling issue in its name (Avaiable), and it might be worth tightening its contract with callers (e.g., documenting or encapsulating the nil/PortTypeInvalid return behavior) to make the termination conditions in the checkAutoSwitch*Port loops clearer.
  • Both checkAutoSwitchOutputPort and checkAutoSwitchInputPort now loop over ports and repeatedly call GetCard/Ports.Get; if this runs frequently, consider caching or reducing the amount/level of debug logging (Debugf) inside the loop to avoid performance and log‑noise issues on systems with many ports.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `resumeSinkConfig`, the mute setting now ignores `!portConfig.Enabled` and relies only on `MuteOutput`; if a port is disabled this may leave it unmuted—double‑check whether the disabled state should still influence mute when resuming sink configuration.
- The new `LoopAvaiablePort` helper in `PriorityManager` has a spelling issue in its name (`Avaiable`), and it might be worth tightening its contract with callers (e.g., documenting or encapsulating the `nil`/`PortTypeInvalid` return behavior) to make the termination conditions in the `checkAutoSwitch*Port` loops clearer.
- Both `checkAutoSwitchOutputPort` and `checkAutoSwitchInputPort` now loop over ports and repeatedly call `GetCard`/`Ports.Get`; if this runs frequently, consider caching or reducing the amount/level of debug logging (`Debugf`) inside the loop to avoid performance and log‑noise issues on systems with many ports.

## Individual Comments

### Comment 1
<location> `audio1/sink.go:107-108` </location>
<code_context>
+		} else {
+			return false, 0, ""
 		}
-	} else {
-		return true, 0, ""
 	}
</code_context>

<issue_to_address>
**issue (bug_risk):** SetVolume now updates hardware mute without updating the stored mute config, which can desync state.

In the non-zero volume path, `SetVolume` now calls `s.setMute(GetConfigKeeper().Mute.MuteOutput)` directly instead of using `SetMute`, so `GetConfigKeeper().SetMuteOutput` is never invoked when mute changes via volume. This can leave `MuteOutput` out of sync with the actual sink mute and cause an incorrect mute state to be reapplied on resume. Please either route this through `SetMute` or explicitly update `MuteOutput` whenever `setMute` is called here.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@fly602 fly602 force-pushed the fix-soundEffect branch 3 times, most recently from 81eb4e6 to fa2fa09 Compare February 26, 2026 07:42
mhduiy
mhduiy previously approved these changes Feb 26, 2026
1. 优化了音频初始化配置设置
2. 优化了音频切换端口检查的逻辑
3. 优化了音频音量静音的设置

Log: 音频优化
PMS: BUG-350057
Influence: audio
@deepin-ci-robot
Copy link

deepin pr auto review

代码审查报告

总体评价

这段代码主要对音频模块的自动切换端口逻辑进行了重构,并优化了静音和音量设置的逻辑。代码整体结构清晰,但存在一些逻辑问题和潜在的性能隐患。

具体问题分析

1. 语法与逻辑问题

audio1/audio.go

  1. 初始化顺序问题

    // 加载module-null-sink,噪音抑制时,将sink-input端口Echo-Cancel Playback引入到null-sink
    a.LoadNullSinkModule()
    GetConfigKeeper().Load()

    GetConfigKeeper().Load()移到LoadNullSinkModule()之后是合理的,但应该在refresh()之前完成配置加载,确保数据一致性。

  2. 自动切换端口逻辑

    // 蓝牙支持的模式
    a.refreshBluetoothOpts()

    删除了蓝牙模式刷新的调用,但未在autoSwitchPort()中补充,可能导致蓝牙设备初始化不完整。

audio1/audio_events.go

  1. 端口检查逻辑

    if card.ActiveProfile != nil && port.Profiles.Exists(card.ActiveProfile.Name) {

    添加了card.ActiveProfile != nil检查是好的,但应该先检查port是否为nil,因为port可能为nil。

  2. 循环逻辑

    for {
        prefer, pos = GetPriorityManager().LoopAvaiablePort(pulse.DirectionSink, pos)
        if prefer == nil || pos == nil || pos.tp == PortTypeInvalid {
            break
        }

    这个循环逻辑可能导致无限循环,如果LoopAvaiablePort返回相同的pos而没有推进。

audio1/sink.go

  1. 音量设置逻辑

    if value == 0 {
        value = 0.001
        s.setMute(true)
    } else {
        s.SetMute(false)
    }

    将音量0改为0.001可能导致用户体验不一致,用户期望静音时音量应为0。

  2. 静音设置逻辑

    func (s *Sink) SetMute(value bool) *dbus.Error {
        if err := s.setMute(value || s.Volume == 0); err != nil {
            return dbusutil.ToError(err)
        }

    当音量为0时强制静音是合理的,但可能导致用户无法设置音量为0而不静音。

2. 代码质量问题

  1. 命名不一致

    • LoopAvaiablePort 应为 LoopAvailablePort(拼写错误)
    • checkAutoSwitchOutputPortcheckAutoSwitchInputPort 返回值命名不一致
  2. 日志级别

    logger.Debugf("loop prefer output port: %+v", prefer)

    使用Debug级别记录端口循环信息是合适的,但应考虑添加更详细的上下文信息。

  3. 错误处理

    if pc, err = a.ctx.GetCard(card.Id); err != nil {
        logger.Warning(err)
        continue
    }

    错误处理使用了continue,但没有记录足够的上下文信息,可能导致调试困难。

3. 性能问题

  1. 重复查询

    var pc *pulse.Card
    if pc, err = a.ctx.GetCard(card.Id); err != nil {
        logger.Warning(err)
        continue
    }

    在循环中重复查询声卡信息可能导致性能问题,应考虑缓存结果。

  2. 锁竞争

    s.PropsMu.Lock()
    cv := s.cVolume.SetAvg(value)
    s.PropsMu.Unlock()

    在频繁操作音量时,锁竞争可能成为瓶颈,应考虑优化锁粒度。

4. 安全问题

  1. 空指针检查

    if card.ActiveProfile != nil && port.Profiles.Exists(card.ActiveProfile.Name) {

    应先检查port是否为nil,避免潜在的空指针解引用。

  2. 并发安全

    func (s *Sink) setMute(value bool) error {
        err := s.CheckPort()
        if err != nil {
            logger.Warning(err.Body...)
            return err
        }

    setMute方法没有使用锁保护,可能在并发调用时导致数据不一致。

改进建议

  1. 初始化顺序优化

    func (a *Audio) init() error {
        // 先加载配置
        GetConfigKeeper().Load()
        // 再初始化模块
        a.LoadNullSinkModule()
        // 最后刷新数据
        a.refresh()
        // ...
    }
  2. 端口检查逻辑优化

    if port == nil {
        logger.Warning("port is nil")
        continue
    }
    if card.ActiveProfile != nil && port.Profiles.Exists(card.ActiveProfile.Name) {
        // ...
    }
  3. 音量设置逻辑优化

    func (s *Sink) SetVolume(value float64, isPlay bool) *dbus.Error {
        // 保存原始值用于配置
        originalValue := value
        
        if value == 0 {
            s.setMute(true)
        } else {
            s.SetMute(false)
        }
        
        // 保存配置时使用原始值
        GetConfigKeeper().SetVolume(card, s.ActivePort.Name, originalValue)
    }
  4. 并发安全优化

    func (s *Sink) setMute(value bool) error {
        s.PropsMu.Lock()
        defer s.PropsMu.Unlock()
        
        err := s.CheckPort()
        if err != nil {
            logger.Warning(err.Body...)
            return err
        }
        
        if !s.setPropMute(value) {
            return nil
        }
        s.audio.context().SetSinkMuteByIndex(s.index, value)
        
        // ...
    }
  5. 循环逻辑优化

    var lastPos *Position
    for {
        prefer, pos = GetPriorityManager().LoopAvaiablePort(pulse.DirectionSink, pos)
        if prefer == nil || pos == nil || pos.tp == PortTypeInvalid {
            break
        }
        if pos == lastPos {
            logger.Warning("loop detected in port iteration")
            break
        }
        lastPos = pos
        // ...
    }

总结

这次重构主要优化了音频模块的自动切换端口逻辑,但引入了一些新的问题。建议在合并前进行充分的测试,特别是针对以下场景:

  1. 多设备切换场景
  2. 静音和音量调节的边界情况
  3. 并发访问场景
  4. 蓝牙设备连接和断开场景

代码整体质量尚可,但需要关注并发安全和性能问题,特别是锁的使用和循环逻辑的优化。

@deepin-ci-robot
Copy link

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: fly602, mhduiy

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@fly602 fly602 merged commit 35cb2b1 into linuxdeepin:master Feb 27, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants