- Accueil
- Plugins
- Améliorations UI
- dsh-rail-equalizer
dsh-rail-equalizer
yuanyihy/dsh-rail-equalizer
Visualiseur audio système pour le rail de navigation des tours : réagit en direct à la sortie du périphérique de lecture par défaut ; quatre modes (spectrum/pulse/sway/travelling wave), spectre logarithmique 16 bandes par FFT 2048 points côté hôte et réglages de sensibilité, intensité, impact des basses et vitesse d’onde. L’audio est capté côté hôte via WASAPI loopback : aucune demande d’autorisation, aucun micro ni capture d’écran. La partie navigateur ajoute seulement un attribut, une feuille CSS, des variables et une ligne latérale ; elle ne modifie jamais le composant officiel. La désactivation supprime ces ajouts et rétablit le style par défaut. Windows uniquement.
Installer
dsh plugin --profile web add github:yuanyihy/dsh-rail-equalizerREADME
dsh-rail-equalizer
让 DSH 的**「轮次导航」导轨**(聊天右侧那根刻度条)跟着系统正在播放的声音实时律动。
放音乐、放视频、打游戏 —— 只要声音是从这台电脑的默认播放设备出来的,导轨就会跟着动。 不需要任何授权、不弹任何窗口、不碰麦克风、不录屏,戴耳机照样工作。


仅 Windows。 音频采集层依赖 WASAPI。英文说明见 README.en.md。
它是怎么拿到声音的
这是本插件唯一"重"的地方,值得说清楚。
浏览器的 getDisplayMedia(屏幕共享)做不到"一次授权" —— 这是 Chrome/Edge 刻意的安全设计:每次调用都必须重新选一次共享目标,权限不做持久化,没有任何 API 能绕过。而麦克风方案在戴耳机时完全无效。
所以本插件走的是宿主端 WASAPI 回环:DSH 的宿主进程(Node)通过 koffi 直接调用 Windows Core Audio 的 COM 接口,抓默认播放设备正在渲染的 PCM。
IMMDeviceEnumerator.GetDefaultAudioEndpoint(eRender)
→ IMMDevice.Activate(IAudioClient)
→ IAudioClient.Initialize(AUDCLNT_STREAMFLAGS_LOOPBACK)
→ IAudioClient.GetService(IAudioCaptureClient)
→ 轮询 GetBuffer / ReleaseBuffer
和 OBS 抓桌面音频是同一个机制。它不是"录音",是在读系统混音器已经把声音交给声卡的那份数据,所以:
- 没有任何"权限"概念需要向谁申请
- 耳机 / 音箱 / HDMI / 虚拟声卡,抓的都是默认播放设备(你在系统里切输出,它跟着切)
- 只读,不写入、不改变音量、不影响任何正在播放的程序
- 音频数据只在内存里过一遍算电平,不出这台电脑、不落盘
koffi 通过 optionalDependencies(@koromix/koffi-win32-x64)分发预编译二进制,装包即用,不需要 Visual Studio / node-gyp / 任何编译工具链。
它是怎么做到"不动原组件"的
官方导轨由 @deepseek-ai/dsh-client-ui-chat 渲染。本插件没有 patch、没有 fork、没有替换它的任何代码或组件。
浏览器半只做四件事:
| 做什么 | 具体 |
|---|---|
| 打一个标记 | 往导轨的 <nav> 上加 data-dsh-eq 属性 |
| 注入一张样式表 | 一个 <style> 标签,全部靠 CSS 覆盖(两套构建的横条选择器都覆盖) |
| 写几个变量 | document.documentElement 上的 --dsh-eq-* |
| 画一个控制行 | 侧栏那一行是纯 DOM(insertBefore 插进文档流);展开的设置面板走官方 shell.overlay 插槽 |
为什么这不可能破坏功能:
- 律动只作用于可见的那根横条,而且只叠加
scale/filter/box-shadow三个属性。 官方导轨在不同 DSH 构建里画横条的方式不一样,两种都覆盖:- 新构建:横条是真实的
<span class="…_tick">,它的尺寸/透明度/配色由 React 写成内联样式。 内联样式优先级很高,所以绝不能去改这四个属性 —— 改用独立的scale属性叠加:scale与transform是两个互不覆盖的属性(最终矩阵 = translate × rotate × scale × transform), 元素原有的内联尺寸与四态差异一像素都不会动。 - 旧构建:横条是
button的::before伪元素,那里用transform: … scaleX() scaleY()叠加。 两套选择器各自只在对应构建里命中,互不干扰。
- 新构建:横条是真实的
- 无论哪种构建,都不碰
width/height/opacity/background-color—— 官方四态(当前 / 悬停 / 未加载 / 生成中)的尺寸与配色差异一个都没被改。 - 变形从 1 起算(静止即恒等变换),
filter从brightness(1)起算,无辉光时box-shadow不画 —— 电平静默时视觉效果就是原样。伪元素与scale都不参与布局,也不会出现在getBoundingClientRect()里, 所以导轨的滚动、坐标定位、点击跳转在几何上完全不受影响。 - 只叠加
filter: brightness()与box-shadow,不动background/color,主题 token 原样生效。 - 正在生成的那一轮带
aria-busy="true",样式表显式把它排除在行波动画之外 —— 它自己那条"呼吸"表现一点没被覆盖。 - 每个刻度的行波相位用
nth-child静态生成 CSS(设在行容器上靠继承下发),完全不往 DOM 里写东西,不会和 React 的渲染打架。 - 关闭 = 移除属性 + 移除样式表 + 清空变量,三件事一起做。关掉之后导轨和没见过这个插件完全一样。
安装
仅 Windows。 音频采集层依赖 WASAPI,package.json 里也声明了 os: ["win32"],所以在 macOS / Linux 上会被包管理器直接拒装,而不是装完再崩。
dsh plugin --profile desktop add github:yuanyiHY/dsh-rail-equalizer
装完重启 DSH Desktop(宿主进程要重新加载 bundle 才会注册路由)。
从源码 / 本地目录安装(改代码即时生效)
git clone https://github.com/yuanyiHY/dsh-rail-equalizer
dsh plugin --profile desktop add link:<克隆到的绝对路径>
⚠️ 安装路径里不能有空格 ——
dsh plugin会把参数转给 pnpm,带空格的路径会被切碎成多个包名。本地开发建议放在~/.dsh/local-plugins/这类没有空格的目录下。
不需要编译工具链。
koffi虽然带一个install脚本,但原生二进制是打包在 optional dependency(@koromix/koffi-win32-x64)里的,脚本没执行也能正常加载。pnpm 若提示需要授权构建脚本,跳过不影响使用。
卸载
dsh plugin --profile desktop remove dsh-rail-equalizer
重启后宿主侧的采集与路由完全消失。浏览器侧因为插件不再加载,什么都不会注入。 也可以只点侧栏那一行的开关关掉 —— 效果等同,且立刻生效。
使用
侧栏里、官方「工作区」区块正上方会出现一行:
┌──────────────────────┐
│ ♪ 律动 [ ⬤ ] │ ← 点整行(开关以外)展开 / 收起设置面板
└──────────────────────┘ 点右侧开关 → 直接开 / 关
这一行是插进侧栏文档流的(insertBefore 到 [class*="_regionArea"] 之前),
用和生态里其它插件(如技能中心)一样的做法,并带自愈:React 重渲染把它挤掉就重新插回去。
为什么不注册进官方插槽? 侧栏唯一位置合适的
sidebar.workspaces是kind: "single"—— 往里注册会顶掉官方的工作区区块。而侧栏其余插槽(sidebar.settings/sidebar.brand.*)也都是 single。 所以只能插 DOM,这也是其它侧栏插件共同的做法。
为什么不用浮层(
position: fixed)? 试过,不行 —— 浮层会盖住同样插在这一区域的 其它插件(技能中心那一行就是这么被盖没的)。插进文档流后各占各的位置,谁都不挡谁。
也没有单独的齿轮按钮了 —— 免得和 DSH 自己的设置齿轮撞脸。设置面板只在展开时出现,
定位到这一行的右侧(--dsh-eq-pill-x/y 由 JS 实测写入)。
设置面板里:
| 项目 | 说明 |
|---|---|
| 总开关 | 关掉后导轨完全恢复默认样式(样式表被移除,不是"置零") |
| 运动方式 | 四选一,即时生效,见下表 |
| 灵敏度 | 0.4–3.0。系统音量小、音乐本身轻的时候往上调 |
| 强度 | 0.2–2.0。整体变形幅度 |
| 低频冲击 | 0–2.0。鼓点让刻度纵向"砸"下去多少 |
| 行波 | 只在「行波」模式下有意义:关掉则波浪不跑,刻度原地起伏 |
| 波速 | 行波周期,0.46×–2.4×(滑杆值 2600–500ms) |
四种运动方式
用现有的 N 根刻度当素材,四种完全不同的运动模型:
| 方式 | 观感 | 驱动量 |
|---|---|---|
| 频谱(默认) | 每根刻度长度 = 它负责的那段频率,低音→高音依次排开,像音乐播放器的频谱条 | --dsh-eq-v(该刻度分到的频段) |
| 脉动 | 所有刻度同相位,整排一起伸缩呼吸,最"齐" | --dsh-eq-lvl + 全局动画相位 |
| 摆动 | 刻度本身不变形,整条导轨左右往复摆动 | --dsh-eq-sw × 电平 |
| 行波 | 每根刻度错开相位,一条波沿轨道向上跑 | --dsh-eq-lvl + 每刻度相位 |
频谱模式是唯一的"真频谱":宿主端跑 2048 点 FFT + Hann 窗,把 40Hz→16kHz 按对数切成 16 段(和听感一致),客户端再按实际刻度数把 16 段均匀铺到 N 根刻度上 —— 所以 5 根刻度也能看到从低到高的完整分布,50 根刻度就是一根根连续的频谱。
客户端会热重载、宿主不会,所以可能出现「客户端新版 / 宿主旧版」。此时没有频段数据,频谱模式会自动降级成整排一起动,而不是完全不动。自检里的
bandsAvailable会告诉你当前是哪种。
面板底部有实时电平条和后端状态(设备格式 / 连接状态 / 出错原因)。设置存在浏览器 localStorage(键 dsh-rail-eq:settings:v1)。
CPU 与省电
- 采集是按需的:有浏览器连上 SSE 流才开始抓,最后一个断开后 8 秒自动停。页面没开时这个插件完全不占 CPU。
- 实测解码开销约 2 ms/s CPU(48kHz 立体声 float32),可以忽略。
- 电平以 ~30Hz 经同源 SSE 推送;浏览器侧再用
requestAnimationFrame平滑到屏幕刷新率。 - 尊重系统的**「减少动态效果」**(
prefers-reduced-motion):此时只保留柔和的亮度起伏,不做变形与行波。
出问题怎么办
导轨完全不动
- 点侧栏那一行展开设置面板,看底部状态:
采集失败:...→ 看下面「常见报错」已连接,等待音频…→ 链路是通的,只是系统此刻没在放声音连接中…→ 宿主路由没起来,八成是没重启 DSH当前平台不支持(仅 Windows)→ 预期行为,本实现是 Windows 专用的
- 确认声音确实是从默认播放设备出来的。有些播放器可以指定独立输出设备,如果它绕过了默认设备,就抓不到。
- 确认导轨还在页面上(轮次导航是官方组件,官方改名/移除的话本插件会静默不生效,不影响其它功能)。
- 还是没效果 → 看浏览器半的自检。宿主侧看不到 DOM,所以浏览器半会把自己的状态回传给宿主,
读
GET /rail-eq/status里的clientReport字段,能看到:railFound(找没找到导轨)、matched(CSS 选择器命中没有)、mode/tickCount(当前模式、数到几根刻度)、bandsAvailable(宿主有没有在发频段数据)、vars(电平变量写了多少)、tick.scale/tickRect(横条被拉伸成多大)、uiRendered(标题栏按钮有没有真的被渲染)。 排查「装了但没效果」这一类问题,这个字段通常一眼就能定位。
Initialize(LOOPBACK) 失败 0x88890008 —— 该设备不支持回环(少见,某些独占模式/虚拟声卡)。切一个默认输出设备再试。
不支持的混音格式 tag=... —— 设备的混音格式既不是 IEEE float32 也不是 PCM16。把默认播放设备的格式改成 16bit/24bit、44100/48000Hz 通常能解决。
手动重启采集(换过播放设备之后):
curl -X POST http://127.0.0.1:<DSH端口>/rail-eq/restart
正常情况下不需要 —— 采集出错会自动重试。
调试用的接口
| 路由 | 用途 |
|---|---|
GET /rail-eq/status | 完整状态:是否在采集、音频格式、包数、客户端数、当前电平 |
GET /rail-eq/level | 单发当前电平,不会启动采集 |
GET /rail-eq/stream | SSE 电平流(浏览器半用的就是它) |
POST /rail-eq/restart | 重启采集 |
POST /rail-eq/report | 浏览器半自检回传(排「没效果」用,结果出现在 /status 的 clientReport) |
开发
lib/
index.js 宿主半:SSE 路由 + 采集生命周期(Cordis 插件)
wasapi.js WASAPI 回环采集(koffi 直调 Core Audio COM)
analyzer.js PCM → 4 个时域电平 + 16 段对数频谱(2048 点 radix-2 FFT + Hann 窗,快攻慢放包络)
client.js 浏览器半:导轨绑定 + CSS 变量 + 控制 UI
test/
host-integration.mjs 宿主端集成测试(不需要 DSH 重启)
band-probe.mjs 单点频段探针(验证某个频率落在哪一段)
make-tone.mjs 生成测试音
宿主半是纯 ESM,浏览器半是 window.__ModuleLoader__.load({ id, factory }) 形态的静态 bundle,都不需要构建步骤。改完 lib/*.js 重启 DSH 生效。
跑测试(有声音在放时最有意义):
node test/make-tone.mjs 30
# 另开一个窗口播放 test/tone.wav,然后:
node test/host-integration.mjs
测试会覆盖:路由分发、404、按需采集(单发探活不该启动采集)、SSE 头与推流、status 补发、电平数值健康度、四频段独立性、频谱结构(最强段显著高于中位段)、断开后自动停机、dispose 清理。
koffi 踩过的两个坑
- 输出参数不是返回值数组。 koffi 里
_Out_参数要传一个容器([null])进去,原生代码把结果写回容器,函数返回值就是 C 的返回值(这里是 HRESULT)。 - COM vtable 要手动解引用。
koffi.decode(obj, 'void *')拿到 vtable 指针,再koffi.decode(vtable, index * ptrSize, 'void *')拿到方法指针,最后koffi.call(fn, proto, obj, ...)。proto的第一个参数是this。
已知限制
- 仅 Windows。 音频采集层依赖 WASAPI。macOS 可以用
ScreenCaptureKit或虚拟声卡,没有实现。 - 抓的是默认播放设备。 播放器如果指定了独立输出设备,抓不到。
- 导轨的定位靠语义锚点(
nav+button[class*="_mark"],优先用aria-label),不依赖中英文案也不依赖哈希类名。官方若大改导轨 DOM 结构,findRail()一个函数就能修。 - 没有接入 DSH 的 locale 命名空间,界面文案是中英混合。
Plugins associés
dsh-web (dsh-task-board)
zhu1090093659/dsh-web
dsh-web (dsh-web-all)
zhu1090093659/dsh-web
dsh-TUI
ccch1mneyyy/dsh-tui
dsh-web-ui (dsh-task-board)
zhu1090093659/dsh-web-ui