☰
StemDeck前端变调引擎实现:SoundTouch AudioWorklet、13路输入路由与浏览器Secure Context陷阱
2026/9/26 4:50:35 网站建设 项目流程

StemDeck前端变调引擎实现:SoundTouch AudioWorklet、13路输入路由与浏览器Secure Context陷阱

【免费下载链接】stemdeckStemdeck is an modern stem extraction platform for musicians,producers and hobbyists, designed to isolate vocals, drums, bass, piano and guitar for practice, transcription, remixing, and creative audio workflows through a modern and interactive interface项目地址: https://gitcode.com/gh_mirrors/st/stemdeck

StemDeck 是一款面向乐手与制作人的音轨分离(stem extraction)平台,能把一首歌拆成人声、鼓、贝斯等独立轨道。它的前端变调引擎基于 SoundTouch 算法实现了 AudioWorklet 实时移调:±6 个半音、13 路输入路由、单一输出时钟,还要绕过浏览器 Secure Context 这道"陷阱"。本文带你完整看懂这套浏览器变调引擎的设计与实现。

一、为什么是"一路一个输入",而不是"一个 worklet 一个参数"?

常规思路是:一个 AudioWorklet 节点暴露一个pitch参数,想移多少调就填多少。但 StemDeck 支持逐轨独立移调——同一首歌里,人声停在原位、吉他升 2 个半音、贝斯降 3 个半音,还要全程和鼓点对齐。

问题在于:WSOLA 类移调的延迟是随移调量变化的。源码里记录了一组实测数据(soundtouch-processor.js):

参数端到端延迟
pitch 0,tempo 10 ms(直通)
pitch +2,tempo 1115 ms
pitch −5,tempo 1144 ms
pitch 0,tempo 0.75437 ms

如果每个轨道各跑一条独立链路,鼓组会和乐队最多差出 144 ms——直接"脱拍"。

StemDeck 的解法是用"连接关系"表达移调量:worklet 提供 13 路输入,第 k 路输入固定对应k − 6个半音。轨道想升 2 个半音,不是改参数,而是把它的总线接到第 8 路上。所有链路最后汇入同一个 tempo 阶段、同一个输出时钟,无论多少种调式同时播放,对齐永远成立。

13 路输入 ├─ 输入 0 → 移调 −6 半音 ├─ 输入 1 → 移调 −5 半音 ├─ ... ├─ 输入 6 → 半音 0:鼓组、节拍器、原调轨道(普通延迟线) ├─ ... └─ 输入 12 → 移调 +6 半音 ↓ 共享 tempo 阶段 → 单一输出

这个映射在 pitchBus.js 中定义,主线程引擎和 worklet 共用同一份常量,避免两边各写一份然后"漂移":

export const PITCH_MIN = -6; export const PITCH_MAX = 6; export const INPUT_COUNT = PITCH_MAX - PITCH_MIN + 1; // 13 export const ZERO_INPUT = -PITCH_MIN; // 6

范围定在 ±6 也有讲究:超过 ±5 半音音质明显劣化,与其提供一个"听起来破掉"的八度,不如在控制面板上就停住(soundtouch-processor.js)。

二、鼓组永不移调,但必须跟乐队一起变速

一个反直觉的细节:鼓永远不移调。用重采样把军鼓抬一个调,它不会"变成另一个调的鼓",只会变成另一种鼓(pitchBus.js)。所以鼓组固定走第 6 路(半音 0 的普通延迟线)。

但鼓不能跳过时间伸缩:tempo 0.75× 时,鼓组必须跟着乐队一起变慢。这正是"单一共享 tempo 阶段"设计的关键收益:

  • 音乐:WSOLA(1/r) → 重采样(r) → 共享 WSOLA(s)
  • 鼓:等倍率直通 → 共享 WSOLA(s)

两条链路的时长在构造上就相同(都是1/s),只有音乐被乘了移调比 r(soundtouch-processor.js)。

节拍器点击也接在同样的"未移调总线"上,与鼓样本在同一帧进入音频图,共用同一个 tempo 阶段(metronome.js)。

三、13 路总线如何接线:懒加载与优雅降级

主线程侧为每一路输入建一个 Gain 总线,然后按numberOfInputs: 13创建 worklet 节点(audioEngine.js):

const buses = Array.from({ length: INPUT_COUNT }, () => ctx.createGain()); // ... stNode = new AudioWorkletNode(ctx, 'soundtouch-processor', { numberOfInputs: INPUT_COUNT, numberOfOutputs: 1, outputChannelCount: [2], });

轨道换调时走routeTrack():算出目标总线,播放中则先做 6 ms 淡出、保持 20 ms、再淡入,两条总线延迟相同,听起来是一次短促的"音量下探"而不是两个调式硬拼接的咔哒声(audioEngine.js)。

工程细节上还有几个值得学的设计:

  • 懒建链/留尾释放:worklet 里只有当某路输入真正接上信号时才创建对应的 PitchChain;轨道离开后多保留 256 个 block(约几百毫秒),把乐句尾巴放完才释放,避免"掐头去尾"(soundtouch-processor.js)。
  • 静音预充:每条链路启动时先喂一段静音,让它表现得像"从播放一开始就在运行"。这样轨道中途换调,旧链路的尾巴和新链路能无缝衔接,也不会卡住其他轨道 130 ms(soundtouch-processor.js)。
  • 抗混叠只在上移时生效:只有升调是抽取(decimation),Nyquist 以上会折叠回可听频段形成"金属渣"感,所以链路在重采样前挂了一组 8 阶 Butterworth 级联低通;降调是内插,无需滤波(soundtouch-processor.js)。
  • 直通旁路:没有任何移调也没有变速时,worklet 逐样本原样回传,零额外延迟(soundtouch-processor.js)。
  • 延迟自报:worklet 算好内部缓冲深度后主动postMessage给主线程,用于摆放播放头——常量只维护一份(soundtouch-processor.js)。
  • 磁带式兜底:worklet 加载失败时,总线直连 master,改用playbackRate重采样——变速可用但会连音高一起变,功能降级而不是罢工(audioEngine.js)。

数值一致性还有单测兜底:测试会直接解析 worklet 源码,断言其 ±6 半音范围和主线程pitchBus.js里的常量严格相等(pitch-shift.test.mjs),端到端行为由 transpose.spec.mjs 覆盖。

四、Secure Context 陷阱:为什么手机上"移调"按钮是死的

这是最容易踩的坑。StemDeck 常以"局域网服务"方式运行:电脑上起服务,手机/平板通过http://192.168.x.x打开。但AudioWorklet 是 Secure Context API——浏览器对http://<局域网IP>一律不开放,于是 SoundTouch 阶段整个建不起来,移调功能直接不可用。

更隐蔽的是误导性的报错。早期 UI 只显示"需要 Web Audio",可 Web Audio 明明工作正常,真正的问题出在源(origin)不安全上(pitchBus.js)。现在的做法是区分两种失败:

export function pitchBlockedKey() { return window.isSecureContext === false ? "pitch.insecureOrigin" // "通过网络连接时无法使用移调" : "pitch.unavailable"; // 真正的 worklet 故障 }

多语言文案在 i18n.js("通过网络连接时无法使用移调")。

而项目给出的正解在 tls_listener.py:桌面版同时跑两个 uvicorn 监听器——

  1. http 回环口:给自家 webview 用。http://127.0.0.1按浏览器规则本身就是安全上下文,而且自签名证书会在 Tauri 窗口里弹出无法点击的证书拦截页,只能走 http。
  2. https 局域网口:给手机用。只有 TLS 能让192.168.x.x这个源变成 secure context,AudioWorklet 才肯开放。

两个监听器共享同一个 FastAPI 应用对象、同一个队列,只是入口协议不同——一个 registry 服务两种受众,是这个模块最聪明的地方。

五、小结

StemDeck 的变调引擎给了三个可以迁移到自己项目里的经验:

  1. 用拓扑代替参数:当处理延迟依赖参数时,"接到哪个输入"比"填哪个数值"更利于多路对齐;
  2. 单一时钟是底线:多路音频只要延迟不一致就脱拍,共享末端 tempo 阶段 + 对齐垫音(alignment pad)是保证;
  3. 先想清楚源的安全性:AudioWorklet 在局域网 http 下不可用是浏览器规则而非 bug,要么上 TLS,要么提供诚实的降级提示(isSecureContext检查 + 精确文案)。

想继续深挖,可以从 soundtouch-processor.js(WSOLA 核心)与 audioEngine.js(总线接线与时钟映射)两个文件入手。

【免费下载链接】stemdeckStemdeck is an modern stem extraction platform for musicians,producers and hobbyists, designed to isolate vocals, drums, bass, piano and guitar for practice, transcription, remixing, and creative audio workflows through a modern and interactive interface项目地址: https://gitcode.com/gh_mirrors/st/stemdeck

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询