使用 Web Audio API 实现可变占空比方波
使用 Web Audio API 创建可变占空比方波
创建于 2025/4/2 更新于 2025/4/2
最近,我一直在研究 Web Audio API,用于一个我正在做的业余项目。我正在构建一个基于 Web 的 music tracker software,用于创建 Gameboy 原始风格的音频。为了忠实地重现 Gameboy 的声音,我需要可变占空比方波。
标志性的 8-bit 风格的 chiptune 音乐非常依赖方波。Web Audio API 允许你创建 OscillatorNode
,它表示周期性波形,如正弦波、锯齿波、三角波或方波。
const ctx = new AudioContext();
const osc = new OscillatorNode(ctx, { type: "square" });
下面是一个 A4 音符,使用 50% 占空比方波的声音效果:
音量:50%
可能不是你听过的最悦耳的音乐,但你能明白意思。
看起来我们有了方波,很简单,对吧?不幸的是,并不完全是这样。类型为 "square" 的 oscillator 只能允许 50% 的占空比。这意味着在一个周期内,波形有一半时间是高电平,另一半时间是低电平。
Gameboy 的一个巧妙之处在于,它的两个脉冲通道允许可变占空比。具体来说,占空比可以设置为 12.5%、25%、50% 或 75%。这使得游戏开发者能够为他们的游戏创造更丰富、更有层次感的声音。
为了绕过 Web Audio API 的 50% 占空比限制,我必须找到一种方法,从不同类型的周期性波形创建方波。
我们有几个选择。一种方法是使用 Fourier Series(傅里叶级数)。另一种方法是使用 WaveShaperNode
将锯齿波弯曲成所需的占空比方波。首先,让我们看看 Fourier Series。
Fourier Series 方法
简而言之,Fourier Series 是一种将周期性函数或波形表示为无限个正弦和余弦之和的方法。它就像用乐高积木从谐波构建波形一样。基本上,我们可以做一些数学运算来创建一个周期性波形,我们可以用它来塑造 OscillatorNode
的输出。Web Audio API 在 AudioContext
上公开了一个函数,我们可以使用它从一组 Fourier 系数创建一个周期性波形。如下所示。
// 创建 audio context 和 oscillator
const ctx = new AudioContext();
const osc = new OscillatorNode(ctx);
osc.frequency.value = 440; // A4 = 440Hz
// 配置所需的占空比和谐波数
const dutyCycle = 0.25;
const harmonics = 64; // 谐波越多 = 方波越精确
// 创建 Fourier 系数的实部和虚部数组
const real = new Float32Array(harmonics);
const imag = new Float32Array(harmonics);
// DC 偏移 (基于占空比的平均值)
real[0] = 2 * dutyCycle - 1;
imag[0] = 0;
// 计算所需占空比的谐波幅度
for (let n = 1; n < harmonics; n++) {
// 余弦项对于方波为零
real[n] = 0;
// 正弦项遵循以下公式,占空比为 D:
imag[n] = (2 / (n * Math.PI)) * Math.sin(n * dutyCycle * Math.PI * 2);
}
// 从我们的 Fourier 系数创建一个周期性波
const wave = ctx.createPeriodicWave(real, imag, {
disableNormalization: false,
});
// 设置 oscillator 以使用我们的自定义波形
osc.setPeriodicWave(wave);
现在我们有了一个占空比为 25% 或我们想要的任何值的方波振荡器。
以下是使用 Fourier Series 方法创建的可变占空比方波的声音效果。
占空比:
12.5% 25% 50% 75%
音量:50%
这种方法比下一种方法在数学上稍微复杂一些 - 下一种方法更容易掌握。
WaveShaper 方法
Web Audio API 为我们提供了一种扭曲信号的方法。这就是 WaveShaperNode
。在使用 Web Audio API 创建音频时,您可以在图中连接音频节点。通常它会是这样的:
OscillatorNode
→ GainNode
→ AudioDestinationNode
WaveShaperNode
让我们转换来自 OscillatorNode
等节点的输出。我们可以用锯齿波做一个有趣的小事情,我们创建一个阶跃函数,根据它相对于占空比点的位置,将值括在 0 或 1 之间。
const ctx = new AudioContext();
const osc = new OscillatorNode(ctx, { type: "sawtooth" });
const dutyCycle = 0.125;
// 创建 waveshaper
const waveShaper = new WaveShaperNode(ctx);
// 创建 shaping wave 的转换函数
const curveLength = 2048;
const curve = new Float32Array(curveLength);
// 魔法发生在这里 - 在占空比点创建一个阶跃函数
for (let i = 0; i < curveLength; i++) {
const x = i / (curveLength - 1); // 归一化到 0-1 范围
curve[i] = x < dutyCycle ? 1.0 : -1.0; // 阶跃函数
}
waveShaper.curve = curve;
oscillator.connect(waveShaper);
从这里,我们可以将 waveShaper
连接到输出节点,现在我们有了一个占空比为我们想要的任何值的方波。我更喜欢这种方法,因为它简单易懂。
以下是使用 Waveshaper 方法创建的可变占空比方波的声音效果。
占空比:
12.5% 25% 50% 75%
音量:50%
您可能会注意到,使用 Waveshaper 方法创建的方波听起来比 Fourier Series 方法创建的“嗡嗡”声更大。这是因为 Waveshaper 方法创建了一个几乎数学上完美的方波,具有极其锐利的过渡。
每种方法都有优点和缺点。Fourier Series 方法的缺点之一是,它需要大量的谐波才能听起来不错,这在 CPU 周期中代价高昂。如果您的应用程序支持 0% 到 100% 之间的任何占空比并且动态计算曲线,则尤其如此。我正在开发的 music tracker software 的好处是,我只需要支持四个占空比,因此我可以提前计算 waveShaper 曲线一次,并在整个应用程序中重复使用它们。Waveshaper 方法的缺点之一是,您开始遇到 aliasing 和嗡嗡声。
就我的目的而言,Waveshaper 方法是我首选的方法。我喜欢它的简单性,而且我相信它创造了更真实的 Gameboy 声音。这只是与 Web Audio API 相关的冰山一角 - 我真的认为使用这个工具构建很酷的东西有很多潜力,更多的开发者应该看看它。