[HN Gopher] Variable duty cycle square waves with the Web Audio API
___________________________________________________________________
Variable duty cycle square waves with the Web Audio API
Author : iamdan
Score : 41 points
Date : 2025-04-07 16:23 UTC (6 hours ago)
(HTM) web link (www.danblack.co)
(TXT) w3m dump (www.danblack.co)
| gmueckl wrote:
| There is ome thing that I don't understand here: wouldn't
| implementing a custom square wave generator as an
| AudioWorkletProcessor be a more straightforward approach in this
| case?
| sapiogenesis wrote:
| I agree. There's a bit of mental overhead related to working
| with audio worklets (you have to load them from an URL or a
| blob URL, etc.) but for square waves in particular the logic
| should be fairly straighforward, the process function just
| needs to output 1 or -1 for any given sample.
| Blackthorn wrote:
| It needs to do a lot more than that if you want to have
| something that doesn't alias like crazy. And significantly
| more if you want it to sound like a game boy.
| gmueckl wrote:
| Well, the output of the wave shaper approach in the article
| is exactly the same sharp digital square wave that you'd
| get from a trivial square wave generator.
|
| But I overlooked the point that the GP mentions that the
| processor source code must be loaded from a separate JS
| file. That's some quite annoying overhead.
| Blackthorn wrote:
| What I was trying to imply was that it's not enough to
| build a square wave generator to sound like a game boy.
| Even if that generator in the game boy was a perfect
| square wave generator, which I'm not sure it is.
|
| The sound created by that generator passes through a
| fairly complicated filter known as The game boy's
| speaker. To properly create a game boy sound, you need to
| find or take an impulse response of that speaker and
| convolve it with the output of your oscillator.
| iamdan wrote:
| I think long term I will be moving in that direction. For this
| initial exploration and experimentation I've been doing with
| the Web Audio API, the sawtooth oscillator + step function
| waveshaper node have been sufficient for my use case but I will
| need investigate whether I can produce a more authentic sound
| with an AudioWorkletProcessor approach
| tashian wrote:
| I did a little synth project recently that uses an
| AudioWorklet processor to morph between single-cycle
| waveforms, and it worked super well. When I tried to do this
| with the Web Audio API, the audio would stutter when I moved
| the controls. Switching to an AudioWorklet thread eliminated
| the stuttering issue. So, if you need real-time sound shaping
| controls, you may find that AudioWorklet is a better fit.
|
| https://waves.tashian.com
| tomduncalf wrote:
| I've got a very simple project demonstrating how to use the
| newish pure C++ Emscripten Audio Worklet API if you're
| interested. It's a bit neater than the old way you'll usually
| come across which also involves writing JS code, but there
| aren't many docs online!
| https://github.com/tomduncalf/emscripten-audio-worklet-
| examp...
| montag wrote:
| Yes... IMO, the WebAudio synthesis primitives are not
| particularly useful. You are so much better off discarding
| these primitives and rolling your own for an audio project of
| any significance.
| Blackthorn wrote:
| That waveshaper version is going to alias pretty badly, isn't it?
| Did the original Gameboy sounds have that aliasing?
| iamdan wrote:
| it does, you definitely "feel" the change. I actually have to
| do some digging to figure that out, I unfortunately do not have
| my childhood GBA anymore so I have to rely on audio clips to
| make that call
| Blackthorn wrote:
| Don't forget to factor in the Gameboy speaker in your
| listening of those audio clips. That's a major factor that
| will change how these waveforms sound very significantly.
| Those clips, and the classic sounds of chiptunes, are never
| "just" the sound coming off the DAC.
| dimatura wrote:
| I'm not an expert on GB chiptune, but from what I've heard from
| enthusiasts is that different GB models sound different, and
| even within the same model there are variations. That said, it
| wouldn't surprise me if the GB waveforms aliasing, at least
| from the digital side, given that it was operating with pretty
| minimal synthesis capabilities. There's probably some extra
| low- and high-pass filtering that shape the sound after the
| waveform itself is generated. Looking at some examples of
| actual GB PWM waveforms, for sure some high-pass would make a
| pure PWM waveform more GB-like. And some low-pass could help a
| bit with aliasing.
| recursive wrote:
| The given examples seem disconnected from the duty cycle radios
| on my browser. I mean changing them changes the sound, but it's
| as if the one I selected is not the one playing.
|
| Anyway, what could have been the purpose for the gameboy hardware
| to provide both 25% and 75% duty cycle? In audio, these sound
| identical to a human, no? They are the same waveform with
| inverted polarity. They have the same overtone content.
| jhedwards wrote:
| I am little confused by the article because it sounds like they
| are describing "pulse width" which is a common parameter on
| analog and digital synthesizers to change the character of the
| square wave. A square wave with a low pulse width will sound
| thinner than one with a high pulse width, and layering square
| waves with different pulse widths gives you a pleasant phasing
| effect.
|
| Based on some cursory research, however, it seems that duty
| cycle is different than pulse width, so now I am unsure if they
| are trying to use duty cycle variation to implement pulse width
| modulation (PWM) or if they are doing something else entirely.
| duped wrote:
| They are synonyms in this context
| em3rgent0rdr wrote:
| More precisely "pulse width" would be a time, while "duty
| cycle" would be a percent.
|
| And while when going from 0% to 50% duty cycle it could be
| said that "a square wave with a low pulse width will sound
| thinner than one with a high pulse width", however, once you
| go past 50% duty cycle the situation reverses. So a 25% duty
| cycle would sound almost identical to a 75% duty cycle...the
| amplitudes of their Fourier transform components would be
| identical.
| hunter2_ wrote:
| > almost identical ... components would be identical
|
| I'm having a tough time reconciling how the former could be
| almost identical while the latter is identical. I guess the
| former involves a human listening through a speaker which
| has asymmetric imperfections (maybe the speaker moves
| outward more easily than it moves inward, or a DC offset in
| the signal leads to compression in the high-excursion side
| that doesn't exist on the low-excursion side, etc.) whereas
| the FFT readout doesn't necessarily have a speaker in the
| system at all.
| recursive wrote:
| Different linearity properties on the positive and
| negative side would be pretty bad for a speaker, but
| possible. In the case of a square wave, non-linearity
| would be identical to a fixed amplitude change though,
| possibly with a DC bias.
|
| Based on the gameboy wiki I looked up, the phase of the
| 25% duty and 75% duty are such that they are inverse of
| each other, seemingly eliminating the possibility of
| combining the two for different waveforms.
| charcircuit wrote:
| >In audio, this sound identical to a human, no?
|
| If it was just that single wave, but there is more than 1 audio
| channel.
| recursive wrote:
| Adding more channels does nothing to the overtone
| information. From what I can find[1], it seems that the phase
| of the 25% and 75% waves are such that the two waves are
| actually inverse of each other. I don't know much about
| Gameboy hardware though. Do you actually know what the point
| of this is?
|
| [1]: https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardwar
| e#Sq...
| sapiogenesis wrote:
| To my ears it sounded like 25% and 75% duty cycles were 50%,
| and 50% sounded like a shorter one, but not sure.
| recursive wrote:
| Yes, that's how it sounded to me as well.
| hunter2_ wrote:
| > The given examples seem disconnected from the duty cycle
| radios on my browser. I mean changing them changes the sound,
| but it's as if the one I selected is not the one playing.
|
| Confirmed. Author, please fix! For example, in the first set of
| radio buttons:
|
| 1. Leave it at the 50% default
|
| 2. Press play
|
| 3. Change it to the 12.5% option -- we continue to hear the
| same sound
|
| 4. Change it back to 50% -- finally we hear a different sound
|
| This is broken. Another example:
|
| 1. Listen to 12.5% after having come directly from 25%
|
| 2. Listen to 12.5% after having come directly from 50%
|
| The 12.5% should sound identical in either case, but it
| erroneously does not.
| neckro23 wrote:
| If you mix a square wave of one duty cycle with another of a
| different duty cycle, they partially cancel each other out and
| you get a new sound.
|
| I'm not sure, but I believe the original NES Castlevania does
| this in some places, like in the "you died" jingle. (It's
| possible I'm misremembering and it's simply two square notes
| separated by an octave.)
| timewizard wrote:
| "Variable duty cycle?"
|
| So just PWM?
|
| Also, if you needed to exceed 50%, you could have just combined
| two different square waves, out of phase, and you'd be there.
| dimatura wrote:
| Yeah, at least in the context of music synthesis, everyone just
| says PWM. (The term even being subject of a meme with a certain
| youtuber who seems to be fond of it ;)).
| iamdan wrote:
| That's true, I'm coming into the audio and signals world as a
| beginner so I'm still picking up the lingo but totally! For my
| purposes I only want to keep track of a single source for a
| channel so I've gone down this wave transformation path rather
| than composition but that also would get the job done
| nayuki wrote:
| > if you needed to exceed 50%, you could have just combined two
| different square waves
|
| I don't think this is possible. A balanced square wave has no
| even harmonics in the frequency domain. Anytime the duty cycle
| is not 0%, 50%, or 100%, you will have non-zero even harmonics.
|
| A linear combination (scaled sum or difference) of two balanced
| square waves will necessarily still have all even harmonics at
| zero, and thus cannot emulate a square wave with a duty cycle
| different from 50%.
| sapiogenesis wrote:
| Web Audio is awesome and IMHO people who got the spec out and
| pushed for implementation deserve lots of respect. You want to
| make a tracker that runs in your browser and you have all the
| primitives at your disposal to make it work? A few years ago I
| wouldn't imagine it can be possible.
|
| Looking forward to seeing the tracker some time!
| iamdan wrote:
| It's super cool! Just from short time that I've been playing
| around with it, it looks like you can push it pretty far.
| Before too long I hope to have a Show HN post with a link to
| the tracker out live in production
| duped wrote:
| I have a hotter take, that the spec and implementations were
| rushed and it makes it hard to push past "toy" quality for web
| apps. I have seen people struggle and wind up moving mountains
| to get what is basic functionality and performance in native
| audio apps.
|
| The other thing is that they have made cardinal sins like
| relying on direct form biquads for basic filters and using an
| array for parameters. It's good enough to make a demo but falls
| apart in the situations that you actually care about, and these
| are things that the pro audio industry (or similar, like
| gaming) have had solved for a very long time (*)
|
| * pro audio software is a disaster, but for other reasons
| meindnoch wrote:
| As others noted, merely switching back and forth between -1 and 1
| will result in heavy aliasing. Also, since you can only switch
| from -1 to 1 at integer sample points, you won't be able to
| accurately generate frequencies that don't divide the sample rate
| evenly. E.g. if your sample rate is 48 kHz, you won't be able to
| generate a 50% duty cycle 11 kHz square wave. Either the duty
| cycle, or the frequency will be different. Like, _audibly_
| different.
|
| The proper way is either via the Fourier series; or look into
| BLIT [1] synthesis.
|
| [1] https://ccrma.stanford.edu/~stilti/papers/blit.pdf
| nkozyra wrote:
| Aliasing seems like a potentially desirable feature for a
| chiptune / retro sound, though.
| iamdan wrote:
| This is the nice thing about constraining myself to a rather
| old and crude sound style, the rough edges can remain a
| little rough
| montag wrote:
| Blargg's Blip Buffer library is a widely-used implementation
| (especially in chip music synthesis), explained in detail here:
| https://www.slack.net/~ant/bl-synth/
| Dave_Rosenthal wrote:
| I was curious to see how audible square wave duty cycle would be
| (I figured not much) but sadly the audio examples are clearly
| broken on this page--choosing the same one multiple times gives
| totally different sounds.
| TonyTrapp wrote:
| Changing the duty cycle of a square wave is called Pulse Width
| Modulation and is an extremely audible and iconic sound. If you
| have ever heared music any on the Commodore 64, you will
| familiar with its sound. PWM is also available in many
| professional synthesizers of the same era.
| iamdan wrote:
| Yes! My dad has an old Commodore 64 and I vaguely remember a
| skiing game. I think it's so cool what folks were able to
| accomplish musically on such limited systems at the time
| iamdan wrote:
| Thank you for calling that out, I am aware that the audio demos
| are a little busted right now and I'll be patching them up when
| I get a little time
| jcims wrote:
| Shot of the output in time and frequency domains going from
| samples 1 through 3 from left to right. Second one is funny.
|
| https://imgur.com/a/PY942sy
|
| (Chrome on mac)
| dlbucci wrote:
| Cool post! I am also making an 8-bit-like tracker with web audio
| (and my name is also Dan B. Weird), but I never found this sort
| of solution to make it work with the native nodes. I just wound
| up manually creating wav buffers. Gives you the ability to
| emulate a lower sample rate and lower pitch resolution (8 bits on
| NES, I believe), plus you can work around the weird scheduling
| quirks of the WebAudio api that didn't really jive with my real
| time game audio use case. It's live at deathbit.okay.tools if you
| wanted to check it out.
| SonOfLilit wrote:
| I'm also working on a gameboy inspired music app! (for Android,
| in Rust)
|
| If you use a perfect square wave, the aliasing is extremely
| audible and sounds terrible.
|
| As far as I can tell, in native everyone uses a nifty algorithm
| called bank-limited audio synthesis, and specifically blargg's
| implementation blip_buf.
|
| https://slack.net/~ant/bl-synth/
|
| If I were OP I'd try to compile the rust port of this library to
| WASM.
| nayuki wrote:
| I also demonstrate square waves synthesized with the Fourier
| approach, along with a duty cycle parameter:
| https://www.nayuki.io/page/band-limited-square-waves
___________________________________________________________________
(page generated 2025-04-07 23:01 UTC)