Skip to content

fix(audio): honor manual output selection#1146

Merged
mhduiy merged 1 commit into
linuxdeepin:masterfrom
mhduiy:sink
Jun 17, 2026
Merged

fix(audio): honor manual output selection#1146
mhduiy merged 1 commit into
linuxdeepin:masterfrom
mhduiy:sink

Conversation

@mhduiy

@mhduiy mhduiy commented Jun 15, 2026

Copy link
Copy Markdown
Contributor
  1. Track pending manual port switches when a profile change is required.
  2. Complete manual output device selection after the card becomes ready.
  3. Avoid losing manual port selection when automatic port switching is disabled.

Log: Fix manual output device selection not taking effect when automatic port switching is disabled.

fix(audio): 修复手动输出设备选择失效

  1. 在需要切换配置文件时记录待处理的手动端口切换。
  2. 在声卡就绪后继续完成手动输出设备选择。
  3. 避免关闭自动端口切换后丢失手动端口选择。

Log: 修复关闭自动端口切换后设置输出设备不生效的问题。

Summary by Sourcery

Ensure manual audio output selection is correctly applied even when profile changes are required and automatic port switching is disabled.

Bug Fixes:

  • Preserve and apply pending manual port switches across profile changes so user-selected outputs take effect once the card is ready.
  • Prevent loss of manual port selections when automatic port switching is turned off by prioritizing completion of pending manual switches before auto switching.

@sourcery-ai

sourcery-ai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements tracking and completion of pending manual audio port changes across card profile switches, ensuring manual output selections are honored even when automatic port switching is disabled, by adding state for pending manual switches and routing card-ready event handling through a new completion helper.

Sequence diagram for manual port switch across card profile change

sequenceDiagram
    actor User
    participant Audio
    participant Card

    User ->> Audio: setPort(cardId, portName, direction, auto=false)
    Audio ->> Card: ActiveProfile
    alt profile matches
        Audio ->> Audio: setPortDirectly(cardId, portName, direction)
        Audio ->> Audio: clearPendingManualPort(cardId, portName, direction)
    else profile differs
        Audio ->> Audio: setPendingManualPort(cardId, portName, direction)
        Audio ->> Card: SetProfile(profile)
    end

    Note over Card,Audio: After profile switch and card becomes ready
    Card ->> Audio: handleCardEvent/handleSinkAdded(...)
    Audio ->> Audio: checkCardIsReady(cardId)
    Audio ->> Audio: switchPortAfterCardReady(cardId)
    alt pending manual switch exists
        Audio ->> Audio: completePendingManualPort(cardId)
        Audio ->> Audio: setPortDirectly(cardId, portName, direction)
    else no pending manual switch
        Audio ->> Audio: autoSwitchPort()
    end
Loading

File-Level Changes

Change Details Files
Track and clear pending manual manual port selections when profile switching is required before applying a port change.
  • Add a pendingManualPort field and struct to store cardId, portName, and direction for a single pending manual port switch.
  • Update setPort to record pending manual port switches when a profile change is needed and clear them once setPortDirectly succeeds for manual operations.
  • Implement helper methods to set, clear, and complete pending manual port switches with appropriate locking and error logging.
audio1/audio.go
Integrate completion of pending manual switches into card readiness event flow while preserving existing auto-switch behavior.
  • Introduce switchPortAfterCardReady to prefer completing a pending manual port switch for a given card, falling back to autoSwitchPort otherwise.
  • Update card-, sink-, and source-related event handlers to call switchPortAfterCardReady instead of autoSwitchPort once a card is ready, ensuring manual selections are applied after device graph changes.
audio1/audio_events.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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The Audio struct only tracks a single pendingManualPort, so concurrent/manual switches on different cards will overwrite each other; consider storing pending switches keyed by cardId to avoid losing earlier requests when multiple cards are involved.
  • In completePendingManualPort, there is a gap between unlocking and later relocking a.mu where pendingManualPort can be updated by another goroutine; although you guard the final clear with a pointer comparison, it may be clearer and less error‑prone to capture the values you need under the lock and have a separate method to perform the switch without touching shared state.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `Audio` struct only tracks a single `pendingManualPort`, so concurrent/manual switches on different cards will overwrite each other; consider storing pending switches keyed by `cardId` to avoid losing earlier requests when multiple cards are involved.
- In `completePendingManualPort`, there is a gap between unlocking and later relocking `a.mu` where `pendingManualPort` can be updated by another goroutine; although you guard the final clear with a pointer comparison, it may be clearer and less error‑prone to capture the values you need under the lock and have a separate method to perform the switch without touching shared state.

## Individual Comments

### Comment 1
<location path="audio1/audio.go" line_range="1071-1081" />
<code_context>
+	}
+}
+
+func (a *Audio) clearPendingManualPort(cardId uint32, portName string, direction int) {
+	a.mu.Lock()
+	defer a.mu.Unlock()
+
+	if a.pendingManualPort != nil &&
+		a.pendingManualPort.cardId == cardId &&
+		a.pendingManualPort.portName == portName &&
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Clearing the pending manual port only on full match may leave stale state around.

`clearPendingManualPort` only clears when `cardId`, `portName`, and `direction` all match. If a user quickly issues multiple manual switches on the same card with different ports/directions, an older `pendingManualPort` can remain and later be applied by `completePendingManualPort`, which only checks `cardId`. To avoid using stale choices, either clear by `cardId` only, or also match `portName`/`direction` in `completePendingManualPort`.

```suggestion
func (a *Audio) clearPendingManualPort(cardId uint32, portName string, direction int) {
	a.mu.Lock()
	defer a.mu.Unlock()

	// Keep parameters in the signature for compatibility; ignore them for clearing logic.
	_ = portName
	_ = direction

	if a.pendingManualPort != nil &&
		a.pendingManualPort.cardId == cardId {
		a.pendingManualPort = nil
	}
}
```
</issue_to_address>

### Comment 2
<location path="audio1/audio.go" line_range="1083-1092" />
<code_context>
+	}
+}
+
+func (a *Audio) completePendingManualPort(cardId uint32) bool {
+	a.mu.Lock()
+	pending := a.pendingManualPort
+	if pending == nil || pending.cardId != cardId {
+		a.mu.Unlock()
+		return false
+	}
+	a.mu.Unlock()
+
+	err := a.setPortDirectly(pending.cardId, pending.portName, pending.direction)
+	if err != nil {
+		logger.Warning(err)
+		return false
+	}
+
+	a.mu.Lock()
+	if a.pendingManualPort == pending {
+		a.pendingManualPort = nil
+	}
</code_context>
<issue_to_address>
**issue (bug_risk):** Using a copied pointer without re-validating the intended port/direction can apply a stale manual selection.

In `completePendingManualPort`, you snapshot `pending := a.pendingManualPort`, unlock, then call `setPortDirectly` with that snapshot. If another goroutine calls `setPendingManualPort` for the same `cardId` but a different `portName`/`direction` after the snapshot, this function will still apply the old selection, so the final state may not match the latest user choice. Consider either re-validating that `pending` still matches the current desired port/direction under the lock immediately before calling `setPortDirectly`, or attaching a monotonic sequence/timestamp to `pendingManualPort` and only acting on the newest entry.
</issue_to_address>

### Comment 3
<location path="audio1/audio_events.go" line_range="277-279" />
<code_context>
 	a.autoSwitchInputPort()
 }

+func (a *Audio) switchPortAfterCardReady(cardId uint32) {
+	if !a.completePendingManualPort(cardId) {
+		a.autoSwitchPort()
+	}
+}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Falling back to auto-switch after a failed manual completion may override user intent.

In `switchPortAfterCardReady`, if `completePendingManualPort` returns false because `setPortDirectly` failed, we immediately call `a.autoSwitchPort()`. This turns a transient failure in applying the manual choice into an automatic override of explicit user intent. It would be better to distinguish "no pending manual for this card" from "pending exists but failed to apply" and, in the latter case, avoid `autoSwitchPort` (e.g., leave the current port or retry the manual selection).

Suggested implementation:

```golang
func (a *Audio) switchPortAfterCardReady(cardId uint32) {
	handledPending, applied := a.completePendingManualPort(cardId)

	// If there is no pending manual selection for this card, fall back to auto.
	if !handledPending {
		a.autoSwitchPort()
		return
	}

	// If there *was* a pending manual selection but it failed to apply,
	// do not auto-switch: keep the current port to respect user intent.
	if !applied {
		return
	}
}

```

To support this change you will also need to:
1. Update the signature of `completePendingManualPort` to return two booleans, e.g.:
   - `handledPending bool` – true if there was a pending manual selection for this `cardId` (regardless of success in applying it).
   - `applied bool` – true if the pending manual selection was successfully applied.
   Example: `func (a *Audio) completePendingManualPort(cardId uint32) (handledPending bool, applied bool)`.
2. Adjust the implementation of `completePendingManualPort` to:
   - Return `(false, false)` when there is no pending manual port for the given `cardId`.
   - Return `(true, true)` when a pending manual selection exists and `setPortDirectly` (or equivalent) succeeds.
   - Return `(true, false)` when a pending manual selection exists but applying it fails.
3. Update all other call sites of `completePendingManualPort` in the codebase to handle the new `(bool, bool)` return values appropriately (most likely checking `handledPending` or both flags depending on context).
</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 thread audio1/audio.go Outdated
Comment thread audio1/audio.go Outdated
Comment thread audio1/audio_events.go
@mhduiy mhduiy force-pushed the sink branch 2 times, most recently from 2a250db to 6cd54af Compare June 15, 2026 10:14
@mhduiy

mhduiy commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

/test all

1. Track pending manual port switches when a profile change is required.
2. Complete manual output device selection after the card becomes ready.
3. Avoid losing manual port selection when automatic port switching is disabled.

Log: Fix manual output device selection not taking effect when automatic port switching is disabled.

fix(audio): 修复手动输出设备选择失效

1. 在需要切换配置文件时记录待处理的手动端口切换。
2. 在声卡就绪后继续完成手动输出设备选择。
3. 避免关闭自动端口切换后丢失手动端口选择。

Log: 修复关闭自动端口切换后设置输出设备不生效的问题。
@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

★ 总体评分:95分(0-100之间的整数)

■ 【总体评价】

代码通过引入pending机制优雅解决了手动切换端口被自动切换覆盖的逻辑缺陷
逻辑基本正确但存在失败状态下pending残留导致自动切换被阻塞的瑕疵扣5分

■ 【详细分析】

  • 1.语法逻辑基本正确✓

代码在setPort中正确区分了自动与手动切换的分支,通过pendingManualPort暂存意图,并在switchPortAfterCardReady中优先消费。但completePendingManualPort函数在执行setPortDirectly失败时仅打印警告并返回true, false,未清除pendingManualPort状态。这导致后续事件触发switchPortAfterCardReady时,会持续读取到该失败的暂存记录,且由于applied为false直接跳过autoSwitchPort()调用,造成音频端口卡死在错误状态且无法自动恢复。
潜在问题:失败状态下pending记录永久残留阻塞自动切换恢复机制
建议:在completePendingManualPort中,当setPortDirectly返回错误时,应清除对应的pendingManualPort状态,并在switchPortAfterCardReady中当!applied时降级执行autoSwitchPort()作为兜底

  • 2.代码质量良好✓

新增的pendingManualPortSwitch结构体及相关的setPendingManualPortclearPendingManualPortcompletePendingManualPort函数职责划分清晰,命名表意明确。completePendingManualPort采用两次加锁并在锁外执行耗时操作的设计,有效避免了锁竞争。对齐方式的调整提升了代码可读性。
潜在问题:部分新增函数缺少注释说明其并发安全设计意图
建议:为completePendingManualPort补充注释说明为何采用两次加锁以及二次校验的防并发覆盖目的

  • 3.代码性能无性能问题✓

新增的sync.Mutex锁粒度极小,仅用于保护指针的读写赋值操作,不存在在锁内执行耗时I/O或复杂计算的情况。事件处理逻辑的修改仅是在原有调用链前增加一次极轻量的锁检查,对系统运行时开销几乎无影响。
建议:无需优化

  • 4.代码安全存在0个安全漏洞✓

漏洞对比统计:新增漏洞 0 个,减少漏洞 0 个,持平 0 个
代码修改仅涉及内部状态管理与PulseAudio端口切换逻辑,未引入外部不可信输入处理、命令拼接、文件路径操作等危险行为。互斥锁的正确使用保障了并发状态下的内存安全,不存在数据竞争或越界访问风险。

  • 建议:保持现有的内部状态隔离与锁保护机制

■ 【改进建议代码示例】

diff --git a/audio1/audio_events.go b/audio1/audio_events.go
index e5ee4fe37..c12345678 100644
--- a/audio1/audio_events.go
+++ b/audio1/audio_events.go
@@ -277,8 +277,9 @@ func (a *Audio) switchPortAfterCardReady(cardId uint32) {
 	if !handledPending {
 		a.autoSwitchPort()
 		return
 	}
 	if !applied {
 		logger.Warningf("pending manual switch for cardId=%d was not applied, fallback to auto switch", cardId)
+		a.autoSwitchPort()
 		return
 	}
 }
diff --git a/audio1/audio.go b/audio1/audio.go
index d60ea98e3..d87654321 100644
--- a/audio1/audio.go
+++ b/audio1/audio.go
@@ -1083,6 +1083,14 @@ func (a *Audio) completePendingManualPort(cardId uint32) (handled bool, applied
 	if err != nil {
 		logger.Warningf("failed to complete pending manual port: cardId=%d, portName=%s, direction=%d, err=%v",
 			pending.cardId, pending.portName, pending.direction, err)
+
+		// 执行失败时清除残留状态,防止后续事件被永久阻塞
+		a.pendingManualPortMu.Lock()
+		if a.pendingManualPort != nil &&
+			a.pendingManualPort.cardId == pending.cardId &&
+			a.pendingManualPort.portName == pending.portName &&
+			a.pendingManualPort.direction == pending.direction {
+			a.pendingManualPort = nil
+		}
+		a.pendingManualPortMu.Unlock()
 		return true, false
 	}

@deepin-ci-robot

Copy link
Copy Markdown

[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

@mhduiy mhduiy merged commit d773796 into linuxdeepin:master Jun 17, 2026
17 of 18 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