Skip to content

fix: 优化了音频逻辑#1031

Open
fly602 wants to merge 1 commit intolinuxdeepin:masterfrom
fly602:fix-soundEffect
Open

fix: 优化了音频逻辑#1031
fly602 wants to merge 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.

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

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

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: fly602

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

@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

@deepin-ci-robot
Copy link

deepin pr auto review

Git Diff 代码审查报告

总体评价

这次提交主要对音频模块的初始化流程、自动切换端口逻辑以及静音处理进行了重构。代码整体结构有所改进,逻辑更加清晰,但存在一些潜在的性能和并发安全问题。

详细审查意见

1. 语法与逻辑问题

audio1/audio.go

  1. 配置加载顺序问题

    a.LoadNullSinkModule()
    GetConfigKeeper().Load()
    a.refresh()

    GetConfigKeeper().Load()移到a.refresh()之前是合理的,但需要确保LoadNullSinkModule()不依赖于配置加载。建议添加注释说明这一顺序的原因。

  2. 初始化逻辑变更

    - if !a.autoSwitchOutputPort() || a.defaultSink != nil {
    -     a.resumeSinkConfig(a.defaultSink)
    - }
    - if !a.autoSwitchInputPort() || a.defaultSource != nil {
    -     a.resumeSourceConfig(a.defaultSource)
    - }
    + a.autoSwitchPort()

    将两个独立的逻辑合并为一个方法调用,但需要确保autoSwitchPort()内部正确处理了所有情况,包括defaultSinkdefaultSource为nil的情况。

  3. 静音设置逻辑变更

    - s.setMute(GetConfigKeeper().Mute.MuteOutput || !portConfig.Enabled)
    + s.setMute(GetConfigKeeper().Mute.MuteOutput)

    移除了!portConfig.Enabled条件,这可能导致端口禁用时不静音。需要确认这是否是预期行为。

audio1/audio_events.go

  1. 循环逻辑改进

    - prefer, pos := GetPriorityManager().GetTheFirstPort(pulse.DirectionSink)
    - for pos.tp != PortTypeInvalid {
    + var prefer *PriorityPort
    + var pos *Position
    + for {
    +     prefer, pos = GetPriorityManager().LoopAvaiablePort(pulse.DirectionSink, pos)

    使用无限循环配合LoopAvaiablePort方法更清晰,但需要确保循环有明确的退出条件。

  2. 端口存在性检查

    + // 配置同步可能有滞后性,需要查询声卡和端口是否存在
    + var pc *pulse.Card
    + if pc, err = a.ctx.GetCard(card.Id); err != nil {
    +     logger.Warning(err)
    +     continue
    + }
    + if _, err = pc.Ports.Get(prefer.PortName, pulse.DirectionSink); err != nil {
    +     logger.Warning(err)
    +     continue
    + }

    添加了端口存在性检查是好的改进,但频繁调用GetCard可能影响性能。

audio1/sink.go

  1. 静音设置方法重构
    func (s *Sink) setMute(value bool) error {
        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)
        // ...
    }
    将原来的setMute方法改为返回error,并添加了setPropMute检查,改进了错误处理。

2. 代码质量问题

  1. 日志一致性

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

    改进了日志格式,使用了格式化输出,但建议在整个项目中保持一致的日志格式。

  2. 代码重复
    checkAutoSwitchOutputPortcheckAutoSwitchInputPort方法中有大量相似代码,建议提取公共逻辑。

  3. 命名问题

    func (pm *PriorityManager) LoopAvaiablePort(direction int, pos *Position) (*PriorityPort, *Position)

    方法名中的Avaiable拼写错误,应为Available

3. 性能问题

  1. 频繁的配置查询
    checkAutoSwitchOutputPortcheckAutoSwitchInputPort中,每次循环都调用GetCardPorts.Get,可能影响性能。建议考虑缓存这些信息。

  2. 循环中的条件检查

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

    虽然逻辑清晰,但每次循环都进行多个条件检查,可以考虑优化。

4. 安全问题

  1. 并发安全
    setMute方法中,没有看到明显的锁保护,但该方法可能被多个goroutine调用。建议添加适当的锁保护。

  2. 空指针检查

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

    添加了ActiveProfile的空指针检查是好的改进,但需要确保所有类似的地方都进行了检查。

  3. 错误处理
    在多个地方,错误被记录但继续执行:

    if err != nil {
        logger.Warning(err)
        continue
    }

    需要评估这些错误是否可以忽略,或者是否应该中断流程。

改进建议

  1. 添加注释:为复杂的逻辑添加详细注释,特别是autoSwitchPort方法的实现细节。

  2. 错误处理改进:考虑区分可恢复和不可恢复的错误,对于不可恢复的错误应该中断流程。

  3. 性能优化:考虑缓存频繁查询的配置信息,减少重复查询。

  4. 并发安全:为可能并发访问的方法添加适当的锁保护。

  5. 测试覆盖:确保修改后的逻辑有充分的测试覆盖,特别是边界条件。

  6. 代码重构:提取checkAutoSwitchOutputPortcheckAutoSwitchInputPort中的公共逻辑。

  7. 日志改进:统一日志格式和级别,确保关键操作有适当的日志记录。

  8. 命名规范:修正拼写错误,如LoopAvaiablePort应为LoopAvailablePort

总结

这次提交改进了音频模块的初始化流程和自动切换端口逻辑,代码结构更加清晰,错误处理更加完善。但需要注意性能优化、并发安全和错误处理的改进。建议在合并前进行充分的测试,特别是边界条件和并发场景。

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.

Comment on lines +107 to +108
} else {
s.setMute(GetConfigKeeper().Mute.MuteOutput)
Copy link

Choose a reason for hiding this comment

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

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.

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.

2 participants