Skip to content

feat: 均衡器和变速 - #12

Merged
imsyy merged 5 commits into
devfrom
feat/equalizer
Apr 28, 2026
Merged

imsyy merged 5 commits into
devfrom
feat/equalizer

Conversation

@imsyy

@imsyy imsyy commented Apr 28, 2026

Copy link
Copy Markdown
Member

No description provided.

Copilot AI review requested due to automatic review settings April 28, 2026 07:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

该 PR 为播放器新增「均衡器(10 频段 + 前级)」与「变速/变调(含音调同步)」能力,并在 UI 中提供对应弹窗入口与交互控件,同时完善歌词加载在“解析为空”场景下的处理。

Changes:

  • 新增均衡器系统设置(默认值、类型、启动时下发、运行时配置联动)与对应 UI 弹窗
  • 新增变速/变调能力:渲染端状态与时间插值同步、主进程 IPC、原生音频引擎 StretchProcessor 接入
  • 改造通用滑块组件 SSlider:支持垂直模式与中心填充,用于均衡器/音调调整

Reviewed changes

Copilot reviewed 33 out of 36 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/utils/color.ts 调整 surfaceBright 调色板与注释
src/stores/status.ts 增加 speed/pitch/pitchSync 运行时状态(不持久化)
src/stores/media.ts setLyric:解析为空时将其视为无歌词并清空内容/来源
src/services/playback.ts 插值时间源引入 speed,并新增 setSpeed/reset 行为
src/services/lyricLoader.ts 在线歌词提交与本地回退/TTML 升级流程重构
src/i18n/locales/zh-CN.json 新增均衡器/变速相关文案与 common.close
src/i18n/locales/en-US.json 新增均衡器/变速相关文案与 common.close
src/core/player.ts 新增 setSpeed/setPitch/setPitchSync;initPlayer 启动时下发均衡器配置
src/components/ui/STooltip.vue Tooltip 样式调整(去边框)
src/components/ui/SSlider.vue Slider 支持 vertical/centerFill,调整默认 thumb 展示等
src/components/ui/SSelect.vue Select 样式调整(边框/阴影)
src/components/ui/SPopselect.vue Popselect 样式调整(去边框)
src/components/ui/SPopover.vue Popover 样式调整(去边框)
src/components/ui/SDropdownMenu.vue DropdownMenu 内容样式调整(去边框)
src/components/ui/SDialog.vue Dialog 非 cover 背景改为 surface-alt
src/components/ui/SContextMenu.vue ContextMenu 内容样式调整(去边框)
src/components/player/FullPlayer/index.vue Seek Slider 显式关闭 alwaysShowThumb
src/components/player/FullPlayer/PlayerBar.vue 播放条新增“更多”菜单入口,打开均衡器/变速弹窗
src/components/modals/SpeedDialog.vue 新增播放速度/音调同步/音调调整弹窗
src/components/modals/EqualizerDialog.vue 新增均衡器弹窗(预设、前级、10 频段)
shared/types/settings.ts 增加 EqualizerPreset/EqualizerSettings,并挂到 PlayerSettings
shared/types/player.ts PlayerApi 增加均衡器/变速/变调相关方法声明
shared/defaults/settings.ts system.player 默认值新增 equalizer 配置
native/audio-engine/src/tempo.rs 新增 StretchProcessor(变速/变调核心处理)
native/audio-engine/src/source.rs DecoderSource 接入 EQ + tempo 处理链
native/audio-engine/src/player.rs InnerPlayer 增加跨曲目共享的 equalizer/tempo,并在 load/seek reset 状态
native/audio-engine/src/lib.rs NAPI 暴露均衡器/变速/变调相关方法
native/audio-engine/src/equalizer.rs 新增 10 频段参数均衡器实现
native/audio-engine/index.d.ts 补充原生模块 TS 类型声明(均衡器/变速/变调)
native/audio-engine/Cargo.toml 引入 signalsmith-stretch 依赖
electron/preload/index.ts preload 暴露 player.setEqualizer*/setSpeed/setPitch/setPitchSync
electron/main/services/engine.ts 增加均衡器相关同步方法(供 config 副作用调用)
electron/main/ipc/player.ts 新增均衡器/变速/变调 IPC handler
electron/main/ipc/config.ts 系统配置变更副作用:把均衡器配置同步到引擎
components.d.ts 注册新增组件类型(EqualizerDialog/SpeedDialog)
Cargo.lock 锁文件更新以包含新增 Rust 依赖

Comment thread src/services/playback.ts Outdated
Comment on lines +111 to +118
/** 重置所有状态 */
export const reset = (): void => {
currentTimeMs = 0;
totalDurationMs = 0;
lastSyncAt = 0;
playing = false;
seeking = false;
speed = 1.0;

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

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

playback.reset() 里把 speed 重置为 1.0,但 status.speed 并未在 stop/onQueueEnded 时同步重置;同时引擎侧 tempo 参数也不会在 stop_internal 里清空。结果是:用户曾设置变速后执行 stop/队列结束,再次播放时引擎仍按旧 speed 播放,但前端插值时间源变回 1.0,歌词高亮/进度显示会漂。建议:reset 不要重置 speed(让其随会话延续),或在 stop/onQueueEnded 时同时把引擎、status、playback 三方都重置到 1.0。

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +173
const applyOnline = async (
token: number,
track: Track,
online: OnlineResult,
fallbackLocal: LocalLyric | null,
): Promise<void> => {
commit(token, online.source, online.input);
if (token !== currentToken) return;
if (useMediaStore().parsedLyric.length === 0) {
if (fallbackLocal) commitLocal(token, fallbackLocal);
return;
}
if (shouldTryTTML(online.source.platform, online.source.format)) {
await tryTTMLOverlay(token, track, online.source.platform);
}

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

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

applyOnline 的控制流会导致“在线歌词解析为空”时永远不会触发 TTML overlay:parsedLyric.length===0 分支里无论是否存在 fallbackLocal 都直接 return;且 loadForTrack/refreshPreference 还会把 local(即便解析为空)作为 fallbackLocal 传入,进一步提前 return。建议:仅在“本地解析后确实有可用行”时才回退本地;若在线解析为空且无可用本地,则仍应按 shouldTryTTML 尝试 TTML overlay(与函数注释一致)。

Copilot uses AI. Check for mistakes.
Comment thread src/core/player.ts
Comment on lines +198 to +217
/**
* 设置播放速度
* @param v - 速度(0.5 ~ 2.0),引擎侧自动 clamp
*/
export const setSpeed = async (v: number): Promise<void> => {
const result = await window.api.player.setSpeed(v);
if (result.success) {
useStatusStore().speed = v;
// 同步给 playback 时间源,让墙钟插值正确换算到源时间(影响歌词高亮)
playback.setSpeed(v);
}
};

/**
* 设置音调偏移(半音 -12 ~ 12)
*/
export const setPitch = async (n: number): Promise<void> => {
const result = await window.api.player.setPitch(n);
if (result.success) useStatusStore().pitch = n;
};

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

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

setSpeed/setPitch 注释写“引擎侧自动 clamp”,但这里在 IPC 成功后直接把传入值写入 status store,并同步给 playback 时间源;如果调用方传入越界或 NaN(哪怕引擎内部 clamp/处理),前端状态与实际播放会不一致,歌词插值也会错。建议在调用 window.api.player 前先在渲染端 clamp/过滤到合法范围(speed: [0.5,2] 且 finite;pitch: [-12,12] 且整数),并用 clamp 后的值更新 store/playback。

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +59
const BAND_MIN = -12;
const BAND_MAX = 12;
const PREAMP_MIN = -12;
const PREAMP_MAX = 12;

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

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

这里 BAND_MIN/BAND_MAX 设为 ±12,但共享类型注释(shared/types/settings.ts:25)以及引擎侧 clamp(native/audio-engine/src/equalizer.rs)都是按 ±15 设计;会造成“规格/持久化/引擎能力”和 UI 可调范围不一致。建议统一:要么把 UI 调整到 ±15,要么同步修改类型注释与引擎限制(并说明为何限制为 ±12)。

Suggested change
const BAND_MIN = -12;
const BAND_MAX = 12;
const PREAMP_MIN = -12;
const PREAMP_MAX = 12;
const BAND_MIN = -15;
const BAND_MAX = 15;
const PREAMP_MIN = -15;
const PREAMP_MAX = 15;

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +112
const bands = [...EQ_PRESETS[key]];
localBands.value = bands;
window.api.player.setEqualizerBands(bands).catch(() => {});
settings.setSystem("player.equalizer.bands", bands);
settings.setSystem("player.equalizer.preset", key);
};

const onBandChange = (index: number, value: number): void => {
localBands.value[index] = value;
};

const onBandCommit = (): void => {
const bands = [...localBands.value];
const matched = matchPreset(bands);
window.api.player.setEqualizerBands(bands).catch(() => {});
settings.setSystem("player.equalizer.bands", bands);
settings.setSystem("player.equalizer.preset", matched);
};

const onPreampChange = (value: number): void => {
localPreamp.value = value;
};

const onPreampCommit = (): void => {
window.api.player.setPreampGain(localPreamp.value).catch(() => {});
settings.setSystem("player.equalizer.preamp", localPreamp.value);
};

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

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

EqualizerDialog 里对 bands/preamp 的修改同时调用了 window.api.player.*(直接走 player IPC)和 settings.setSystem(走 config IPC → main/applyConfigChange → engine 服务)。这会导致同一操作产生两次 IPC/两次下发,且有潜在竞态(后到的 config 同步可能覆盖先到的直接调用)。建议统一副作用入口:优先只用 settings.setSystem 触发主进程 applyConfigChange;如果确实需要“立即生效”,考虑把后处理放到 settings.setSystem(类似 fadeDuration 的处理),避免在 UI 层同时走两条链路。

Copilot uses AI. Check for mistakes.
Comment thread native/audio-engine/src/tempo.rs Outdated
Comment on lines +69 to +76
/// 设置播放速度,自动 clamp 到 [0.5, 2.0]
pub(crate) fn set_speed(&mut self, speed: f32) {
self.speed = speed.clamp(0.5, 2.0);
// sync=OFF 时 transpose 跟着 speed 变
if !self.pitch_sync {
self.sync_transpose_to_stretch();
}
}

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

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

StretchProcessor::set_speed 直接对 speed 做 clamp,但 f32::clamp 遇到 NaN 会返回 NaN,随后 effective_transpose/log2 和 process 里的 output_frames 计算都会产生 NaN,可能导致整段音频不产出(甚至出现异常行为)。建议在这里(或更上游 NAPI/IPC 边界)先对 speed 做 is_finite 检查;非有限值可回退到 1.0,再 clamp 到 [0.5, 2.0]。

Copilot uses AI. Check for mistakes.
Comment thread native/audio-engine/src/equalizer.rs Outdated
Comment on lines +135 to +150
/// 更新所有频段增益(dB),长度不等于 EQ_BAND_COUNT 时按短端截取
pub fn set_band_gains(&mut self, gains_db: &[f32]) {
for (i, gain) in gains_db.iter().take(EQ_BAND_COUNT).enumerate() {
self.band_gains_db[i] = gain.clamp(-BAND_GAIN_LIMIT_DB, BAND_GAIN_LIMIT_DB);
}
self.recompute_coefficients();
}

pub fn band_gains_db(&self) -> [f32; EQ_BAND_COUNT] {
self.band_gains_db
}

pub fn set_preamp_db(&mut self, db: f32) {
let clamped = db.clamp(-PREAMP_LIMIT_DB, PREAMP_LIMIT_DB);
self.preamp_linear = db_to_linear(clamped);
}

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

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

Equalizer::set_band_gains / set_preamp_db 里对输入直接 clamp,但 clamp 遇到 NaN/Inf 会保留 NaN,随后系数计算/对数会传播 NaN,最终把输出样本变成 NaN,可能引发播放异常。建议在写入 band_gains_db / preamp_linear 前对输入做 is_finite 过滤(非有限值当作 0dB),再进行范围 clamp;同时 recompute_coefficients 也应避免用 NaN gain 生成系数。

Copilot uses AI. Check for mistakes.
- 变速 reset 不再清 speed,避免 stop 后会话内速度漂移
- 在线歌词解析为空且无本地时,仍按需触发 TTML overlay
- EQ band 范围 ±12 → ±15,与引擎和类型注释对齐
- EQ 弹窗只走 settings.setSystem,去掉直连 player IPC 的双链路
- core/player.ts setSpeed/setPitch 入口加 finite + clamp
- tempo.rs / equalizer.rs 对 NaN/Inf 守卫,避免污染 DSP 状态

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@imsyy
imsyy merged commit 291eaf9 into dev Apr 28, 2026
3 checks passed
@imsyy
imsyy deleted the feat/equalizer branch April 28, 2026 09:23
roarrabbit pushed a commit to roarrabbit/SPlayer-Next that referenced this pull request Aug 27, 2026
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