fix(linux): avoid audio OSD on startup and exit (#968) (#973)

* fix(linux): avoid audio streams during permission checks (#968)

* fix(ci): remove stale qwen3 submodule gitlink

* fix(ci): restore qwen3 submodule mapping
This commit is contained in:
Chris233 authored and GitHub committed 2026-08-20 22:04:29 +08:00
1 parent 42c1b31754
commit 1ee93576b0
3 files changed
+64 -46

No files matched your search

+31 -17
View File
@@ -335,26 +335,40 @@ mod platform {
/// Windows 的麦克风权限走系统设置 → 隐私 → 麦克风;
/// 这里用 cpal 建立一次短生命周期输入流,避免只查设备格式时误报已授权。
/// Linux 没有对应的应用级权限状态,只检查设备是否存在,避免 PipeWire
/// 因权限探测建立临时输入流而触发桌面音量 OSD(issue #968)。
pub fn check_microphone() -> PermissionStatus {
if windows_microphone_registry_denied() {
log::warn!("[mic] Windows microphone privacy registry is denied");
return PermissionStatus::Denied;
#[cfg(target_os = "linux")]
{
return if has_microphone_input_device() {
PermissionStatus::Granted
} else {
PermissionStatus::NoDevice
};
}
let host = cpal::default_host();
let Some(device) = host.default_input_device() else {
log::warn!("[mic] no default input device");
return PermissionStatus::NoDevice;
};
let supported = match device.default_input_config() {
Ok(config) => config,
Err(err) => return classify_audio_probe_error(err.to_string()),
};
let sample_format = supported.sample_format();
let config: StreamConfig = supported.config();
match probe_input_stream(&device, &config, sample_format) {
Ok(()) => PermissionStatus::Granted,
Err(message) => classify_audio_probe_error(message),
#[cfg(not(target_os = "linux"))]
{
if windows_microphone_registry_denied() {
log::warn!("[mic] Windows microphone privacy registry is denied");
return PermissionStatus::Denied;
}
let host = cpal::default_host();
let Some(device) = host.default_input_device() else {
log::warn!("[mic] no default input device");
return PermissionStatus::NoDevice;
};
let supported = match device.default_input_config() {
Ok(config) => config,
Err(err) => return classify_audio_probe_error(err.to_string()),
};
let sample_format = supported.sample_format();
let config: StreamConfig = supported.config();
match probe_input_stream(&device, &config, sample_format) {
Ok(()) => PermissionStatus::Granted,
Err(message) => classify_audio_probe_error(message),
}
}
}
+5 -3
View File
@@ -1,8 +1,10 @@
// 录音提示音:监听 capsule:state 事件,在"开始录音"边沿播放合成提示音。
// 独立组件,不依赖胶囊窗口显示——Linux 上胶囊隐藏也能正常工作。
// Android Web Audio 输出会触发部分设备的录音输入路由切换,移动端禁用。
// 独立组件,不依赖胶囊窗口显示——胶囊隐藏时也能正常工作。
// Linux 的 Web Audio 输出会被 PipeWire/KDE 当作 sink-input,启动/退出时触发音量 OSD;
// Android 也可能因 Web Audio 输出切换录音输入路由,因此这两个平台禁用。
import { useEffect, useRef } from 'react';
import { detectOS } from './WindowChrome';
import { isAndroid, isTauri } from '../lib/ipc';
import { playRecordStartCue, primeAudioCue, stopAudioCue } from '../lib/audioCue';
import type { CapsuleState, UserPreferences } from '../lib/types';
@@ -18,7 +20,7 @@ interface CapsulePayload {
export function AudioCueListener() {
const audioCueEnabledRef = useRef<boolean>(true);
const prevStateRef = useRef<CapsuleState>('idle' as CapsuleState);
const audioCueRuntimeEnabled = !isAndroid();
const audioCueRuntimeEnabled = !isAndroid() && detectOS() !== 'linux';
// 读取设置(默认开启)
useEffect(() => {
@@ -424,32 +424,34 @@ export function RecordingInputSection() {
<SettingRow label={t('settings.recording.muteDuringRecordingLabel')} desc={t('settings.recording.muteDuringRecordingDesc')}>
<Toggle on={prefs.muteDuringRecording} onToggle={onMuteDuringRecordingChange} />
</SettingRow>
<SettingRow
label={t('settings.recording.audioCueLabel')}
desc={t('settings.recording.audioCueDesc')}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Toggle on={prefs.audioCueOnRecord} onToggle={onAudioCueChange} />
<button
type="button"
onClick={() => playRecordStartCue()}
style={{
padding: '5px 12px',
fontSize: 12,
fontWeight: 500,
fontFamily: 'inherit',
border: '0.5px solid var(--ol-line-strong)',
borderRadius: 8,
background: 'var(--ol-surface-2)',
color: 'var(--ol-ink-2)',
cursor: 'default',
transition: 'background 0.16s var(--ol-motion-quick)',
}}
>
{t('settings.recording.audioCuePreview')}
</button>
</div>
</SettingRow>
{os !== 'linux' && (
<SettingRow
label={t('settings.recording.audioCueLabel')}
desc={t('settings.recording.audioCueDesc')}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Toggle on={prefs.audioCueOnRecord} onToggle={onAudioCueChange} />
<button
type="button"
onClick={() => playRecordStartCue()}
style={{
padding: '5px 12px',
fontSize: 12,
fontWeight: 500,
fontFamily: 'inherit',
border: '0.5px solid var(--ol-line-strong)',
borderRadius: 8,
background: 'var(--ol-surface-2)',
color: 'var(--ol-ink-2)',
cursor: 'default',
transition: 'background 0.16s var(--ol-motion-quick)',
}}
>
{t('settings.recording.audioCuePreview')}
</button>
</div>
</SettingRow>
)}
{os === 'linux' && (
<SettingRow label={t('settings.advanced.streamingInsertLabel')}>
<Toggle