使用Web Audio API生成可变占空比方波
Variable duty cycle square waves with the Web Audio API

原始链接: https://www.danblack.co/blog/variable-duty-cycle-square-wave

我正在构建一个基于网页的音乐追踪器,它类似于Gameboy的音频,需要可变占空比的方波。Web Audio API的默认方波OscillatorNode仅限于50%的占空比,这与Gameboy灵活的脉冲声道(12.5%、25%、50%、75%)不同。 为了克服这个问题,我探索了两种解决方法。第一种方法涉及傅里叶级数,它通过对正弦和余弦波进行求和来构建波形。这种方法需要计算傅里叶系数并创建PeriodicWave,这对于实时应用来说可能计算量很大,但可以生成精确的方波。 第二种方法利用WaveShaperNode来扭曲锯齿波。通过创建一个“阶跃函数”曲线,锯齿波被转换为具有特定占空比的方波。这种方法更易于理解和实现。 虽然WaveShaper方法可能会引入混叠或“嗡嗡”声,但我更喜欢它,因为它简单,并且产生的声音更接近Gameboy的真实声音。此外,我只需要预先计算几个WaveShaper曲线,而不是一系列值。Web Audio API提供了强大的工具来进行创造性的音频处理,我鼓励其他开发者探索它的潜力。

Hacker News 最新 | 往期 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 使用 Web Audio API 生成可变占空比方波 (danblack.co) 12 分 iamdan 35 分钟前 | 隐藏 | 往期 | 收藏 | 讨论 加入我们,参加 6 月 16-17 日在旧金山举办的 AI 初创公司学校! 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系我们 搜索:

原文

Created on 4/2/2025Updated on 4/2/2025

Lately, I've been playing around with the Web Audio API for a side project I'm working on. I am building a web-based music tracker software for creating audio in the original style of the Gameboy. To faithfully recreate the sounds of the Gameboy, I need variable duty cycle square waves.

The iconic 8-bit, chiptune style of music is heavily reliant on square waves. The Web Audio API allows you to create OscillatorNodes that represent periodic waveforms like a sine, sawtooth, triangle, or square waves.

const ctx = new AudioContext(); const osc = new OscillatorNode(ctx, { type: "square", });

Here is what an A4 sounds like with a 50% duty cycle square wave:

Not the most pleasant musical thing you've ever heard but you get the gist.

Seems like we have our square wave, pretty easy right? Unfortunately no, not quite. Oscillators with the type "square" only allow for a duty cycle of 50%. This means that for one cycle, the wave is high for half the time and low for the other half.

One of the neat things about the Gameboy was that its two pulse channels allowed for variable duty cycles. Specifically, the duty cycle could be set to 12.5%, 25%, 50% or 75%. This allowed game developers to create richer, more textured sounds for their games.

To get around this 50%-duty-cycle limitation of the Web Audio API, I had to find a way to create square waves from a different type of periodic waveform.

We have a couple of options. One approach is to use the Fourier Series. Another is to use a WaveShaperNode to bend a sawtooth wave into the desired duty cycle square wave. First, let's take a look at the Fourier Series.

The Fourier Series Approach

In short, the Fourier Series is a way to represent a periodic function or waveform as an infinite sum of sines and cosines. It is like building up waves from harmonics like lego bricks. Basically, we can do a little bit of math to create a periodic waveform that we can shape an OscillatorNode's output with. The Web Audio API exposes a function on the AudioContext that we can use to create a periodic waveform from a set of Fourier coefficients. This is how that looks.

// Create your audio context & oscillator const ctx = new AudioContext(); const osc = new OscillatorNode(ctx); osc.frequency.value = 440; // A4 = 440Hz // Configure your desired duty cycle & number of harmonics const dutyCycle = 0.25; const harmonics = 64; // More harmonics = more accurate square wave // Create arrays for real and imaginary parts of Fourier coefficients const real = new Float32Array(harmonics); const imag = new Float32Array(harmonics); // DC offset (average value based on duty cycle) real[0] = 2 * dutyCycle - 1; imag[0] = 0; // Calculate harmonic amplitudes for desired duty cycle for (let n = 1; n < harmonics; n++) { // Cosine terms are zero for square waves real[n] = 0; // Sine terms follow this formula for duty cycle D: imag[n] = (2 / (n * Math.PI)) * Math.sin(n * dutyCycle * Math.PI * 2); } // Create a periodic wave from our Fourier coefficients const wave = ctx.createPeriodicWave(real, imag, { disableNormalization: false }); // Set the oscillator to use our custom wave osc.setPeriodicWave(wave);

Now we have a square wave oscillator with a duty cycle of 25% or whatever we would like to set it as.

Here is what square waves with variable duty cycles sound like when created with the Fourier Series approach.

This approach is a little more math heavy than the next - the next method is a little bit more intuitive to grasp.

The WaveShaper Approach

The Web Audio API provides us with a way to distort signals. This is the WaveShaperNode. When creating audio with the Web Audio API, you connect audio nodes in a graph. Typically it will be something like this:

OscillatorNodeGainNode AudioDestinationNode

The WaveShaperNode lets us transform the output from something like an OscillatorNode. We can do a fun little thing with a sawtooth wave where we create a step function to bracket the value to either 0 or 1 depending on where it falls in relation to the duty cycle point.

const ctx = new AudioContext(); const osc = new OscillatorNode(ctx, { type: "sawtooth", }); const dutyCycle = 0.125; // Create the waveshaper const waveShaper = new WaveShaperNode(ctx); // Create the transfer function that shapes the wave const curveLength = 2048; const curve = new Float32Array(curveLength); // The magic happens here - create a step function at the duty cycle point for (let i = 0; i < curveLength; i++) { const x = i / (curveLength - 1); // Normalize to 0-1 range curve[i] = x < dutyCycle ? 1.0 : -1.0; // Step function } waveShaper.curve = curve; oscillator.connect(waveShaper);

From here, we can connect the waveShaper to an output node and now we have a square wave of whatever duty cycle we like. I prefer this approach because of how simple and easy it is to grasp.

Here is what square waves with variable duty cycles sound like when created with the Waveshaper approach.

You might notice that the square waves created with the Waveshaper approach sound "buzzier" than the Fourier Series approach. This is because the Waveshaper approach creates an almost mathematically perfect square wave with extremely sharp transitions.

There are pros and cons to each approach. One of the cons of the Fourier Series approach is that you need a lot of harmonics for it to sound decent, which is costly in CPU cycles. This is especially true if your application supports any duty cycle between 0% and 100% and calculates the curve on the fly. The nice thing about the music tracker software that I am working on is that I only need to support four duty cycles so I can compute my waveShaper curves once ahead of time and reuse them throughout the application. One of the downsides of the Waveshaper approach is that you start running into aliasing and buzziness.

For my purposes, the Waveshaper approach is my preferred method. I like the simplicity of it and I also believe that it creates a more authentic Gameboy sound. This is just the tip of the iceberg as it relates to the Web Audio API - I really do think there is a lot of potential for building cool things with this tool and more devs should check it out.

联系我们 contact @ memedata.com