Skip to content

feat(player): volume boost above 100% through the PCM gain stage - #801

Open
gdwengf wants to merge 1 commit into
stackia:mainfrom
gdwengf:feat/volume-boost
Open

gdwengf wants to merge 1 commit into
stackia:mainfrom
gdwengf:feat/volume-boost

Conversation

@gdwengf

@gdwengf gdwengf commented Oct 23, 2026

Copy link
Copy Markdown

为什么

浏览器对 <audio> / <video> 的 volume 有硬上限 1,而这里的直播频道是 MP2 音频,网页不能原生解码,走的是 PCM 软解 + WebAudio 排程那条路——那条路上播放器本来就自己持有 GainNode 直接喂扬声器。

也就是说:想超过 100%,不需要碰元素音量,只要在那个 GainNode 上给增益。但增益超过 1 会削波,削波在语音上是可闻的"爆音",所以增益级后面必须跟一个限幅器,把峰值压住而不是切掉。

改了什么

pcm-audio-player.ts

  • 新增 boost(1..6)与 setBoost(),与 volume 是相乘关系;
  • gainNode 之后串一个 DynamicsCompressorNode(threshold -6dB、ratio 12:1、attack 3ms、release 250ms)当限幅器;iOS 的 MediaStream 旁路和默认输出两条路由都接在限幅器后面;
  • 新增 currentGain(),让所有写增益的地方(updateGain、cancelChain 的淡出)走同一处计算。此前 cancelChain(smooth) 淡出后把增益写回 volume,把倍数冲掉——直播中重同步很频繁,这也是"调了没变大"的原因之一;
  • audioElement.volume 仍然钳到 1(元素上限),超出的部分已经在增益级上给过了。

playback-controller.ts

  • 倍数存在 controller 里而不是 player 里:player 是懒创建的,UI 设置倍数时它往往还不存在,而且每次音频恢复都会重建;ensurePCMPlayer() 创建后重新应用一次。

UI(player-controls.tsx、use-player-touch-gestures.ts、player-gesture-overlay.tsx)

  • 复用原来的音量条,不做第二个控件:下半段 0..100% 是元素音量,上半段 100%..600% 走增益;
  • 画面上右半边上下滑的手势用同一套映射(levelToPos / posToLevel),所以滑过 100% 之后继续进入增益区;
  • 拖动时滑条上方浮出百分比(如 250%),手势指示条也显示同样的百分比,进度条 50/50 分成两段。

player-storage.ts

  • 倍数和音量一样持久化,切台、切槽都保持。

说明

  • 上限取 6 倍(+15.6dB);限幅器阈值留了 6dB 余量,若觉得响度还不够,把 threshold 放宽到 -3dB 会更"响",代价是动态更平。
  • 倍数只对 PCM 软解这条路径生效;原生解码的音频仍受元素音量上限约束,setVolumeBoost 在不具备增益级的后端上是可选方法(no-op)。

Browser media elements cap volume at 1, but live channels here carry MP2 audio that the page cannot decode natively: it goes through the PCM/WebAudio path, which already owns a GainNode feeding the speakers. Loudness above unity therefore only needs gain on that node plus a limiter, because the extra gain would otherwise clip and clipping is audible as crackle on speech.

The PCM player gains a boost factor composed with volume (1x..6x, +15.6dB at the ceiling) and a limiter after the gain stage (threshold -6dB, ratio 12:1, 3ms attack) so peaks are compressed instead of clipped. The level is held by the playback controller rather than the player, because the player is created lazily and rebuilt on every audio recovery.

Both volume controls drive the same two ranges: the lower half of the slider is 0..100% element volume, the upper half is the gain multiplier up to 6x, and the on-picture vertical gesture uses the identical mapping so it can continue past 100% too. A readout shows the applied level (e.g. 250%) while dragging, and the gesture indicator shows the same percentage with the bar split 50/50 between the two ranges.
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.

1 participant