Exploring audio DACs in simulation – Part 1 – 2021-03-21
When I was developing EightThirtyTwo I made good use of GHDL, which allowed me to run the CPU’s VHDL code in simulation and output traces of internal signals for viewing in GTKWave.
Now I’ve started exploring Verilator for simulating Verilog code, and it’s safe to say I’m very impressed!
So a little back-story is in order: [Ok, it turns out there’s so much back-story I didn’t actually talk about Verilator – next time, I promise!]
Some years ago I spent some time trying to improve the audio output on Minimig, and encountered some strange counter-intuitive problems. Originally the DAC used by Minimig was a simple 1st-order Sigma Delta DAC. These have pros and cons:
On the plus side, they’re very simple, easy to understand, have predictable behaviour over their entire input range and have a very small footprint.
On the minus side they need a very high oversampling ratio, otherwise they develop audible tones as volume levels reduce, since the pulses they create become regularly spaced further and further apart. Each of the Amiga’s audio channels requires 14 bits to adequately represent the 8-bit sample resolution multiplied by the six bits of volume adjustment, and since there are two channels per stereo… well… channel, we need to think in terms of 15-bit signals.
Minimig’s DAC originally ran at 7.09MHz and the slowest cycle a 15-bit signal will produce with a 1st-order DAC is 7.09MHz / 32768 = about 216Hz. Yeah, that’s not good. If we aim to keep that cycle above 20KHz, so that it’s definitely not audible, we only have an effective resolution of between 8 and 9 bits.
Before I started tinkering with the DAC problem, someone had addressed the idle tone problem by clearing the DAC’s accumulator on a fixed schedule. This has a similar effect to truncating input bits, but it’s theoretically finer-grained – the more often you do it, the more detail you discard, but you’re not necessarily discarding a whole number of bits.
This, naturally, had some unpleasant side-effects – so the first thing I tried was moving the DAC onto the 28MHz clock. This helped in some ways, but made other side-effects worse! It’s only now, with Verilator’s help, that I’ve come to understand why.
On the now-defunct Minimig.net forum those of us interested in the subject used three standard tests for the various DAC variants we tried:
- The classic Bitmap Brothers game, Gods – the footstep sound should reverberate and fade away smoothly, and not sound like “toc-shhhhh….”.
- In the same game, jumping and landing shouldn’t result in a constant high-pitched tone. (I believe after that sound effect is triggered the audio hardware is “parked” on an almost-but-not-quite-zero value, which can cause an idle tone, as described above.)
- Tim Follin’s excellent title music from Gauntlet III ends with an arpeggio which fades out to nothing (technically it plays for ever, but at zero volume!) and if there’s any bit truncation, noise or idle tones happening, then the last few seconds of that arpeggio will sound bizarre.
- Finally, booting the ProTracker 3.15 ADF, loading the module “Stardust Memories”, setting the volume slider to one step above zero, and recording the song on a PC at maximum input gain. On a well-performing DAC the song should sound almost normal. (Spoiler: this was not a well-performing DAC!)
We tried various approaches to creating a DAC which would give adequate results for all those tests, with limited results. The one thing that seemed to happen consistently is that if the DAC was run at 28MHz it would be noisier than if it was run at 7MHz. I could never adequately explain why, but figured it might have something to do with narrower pulses being generated less accurately (a bit like dot gain in printing, which makes halftoned prints muddy if not corrected for) .
In fact since my background is in printing and graphics I readily drew an analogy between the sigma-delta DAC and Floyd-Steinberg halftoning in graphics – where the error from each sample is distributed to subsequent samples. I knew this method of halftoning was much more prone to dot gain effects than a traditional litho-printing dot screen, where the spacing of the dots is constant and their size is varied – this would be analogous to pulse-width-modulation in audio.
The problem with pulse width modulation is that the cycle length is fixed and must be long enough to accommodate as many pulses as there are unique levels in the input data – so we’re back to a cycle 32768 pulses long and only 216Hz. In the end I tried a hybrid approach, where the input data was first processed by a sigma-delta with a 5-bit output, then that 5-bit output drives a PWM. I still needed to employ the dump-the-accumulator trick in order to prevent idle tones developing – however, despite this, the hybrid DAC performed measurably better than any other approach we tried.
But darned if I knew why!
So I revisited this recently, hoping to remove the reliance on dumping the accumulator and generally improve the DAC. I’ll describe that process next time – and then I will actually talk about Verilator, I promise!