4 Bit Blip Box
This is the story of my experience so far making a 4 bit synth on an 8 bit micro running at 16 megahertz.
For context, I've got a big box of electronics stuff that I tinker with on slow weekends. It's a pleasant mixture of educational and enjoyable, and I can leverage some of my other skills along the way on most projects too.
This project in particular is a 4 bit soft-synth, to learn more about all stages of audio generation. I've done things with sampled audio and pre-made synths, and am somewhat familiar with the NES and Gameboy's sound chips, but I haven't ever implemented the whole thing "from scratch" - signal generation to speaker.
Of course, this is still standing on the shoulders of giants, but it's been rewarding nonetheless.
Here's an overhead view of the project as it stands:
I haven't prepared a proper demo or written a full length track, as the tracker is still in its early stages, but here's a vine of what it can do currently:
The full signal chain is described below!
8-key keyboard and a rudimentary tracker.
I'll add a few more keys when/if I get to building a proper LCD/TFT/whatever interface for it. Currently it's just a keyboard mapped to a pre-defined scale (supports major, minor, pentatonic and "blues").
The tracker is a little more interesting but needs a lot of love - and of course needs that interface to allow you to edit the music without editing the firmware! The tech is somewhat inspired by LSDJ for the Gameboy but with one less level of indirection, partly due to memory constraints. There's space for 10 patterns of 4 bars, configured by 16 "layout slots" across 3 synths/voices.
The tracker loops once it hits a completely null line. It's pretty "dumb" at the moment, but it works!
Arduino Nano, ATMega328p at ~16MHz.
Software synthesis and mixing, done at 8 bit, then downsampled. There's currently 3 voices, of 2 classes - noise and PCM.
Using sample-based audio for even things like square waves might seem wasteful, but it's incredibly flexible and results in small code. Particularly for things like a sin synth, it'd have to use a look up table anyway because cause the Arduino isn't cut out for trig - so why not have it generalised that way from the get go?
The noise generator is a 16-bit xorshift that picks a new value twice per note-frequency. This allows altering the percieved pitch of the noise which is great for drums. I went 16 bit over 8 bit to avoid the repetition being audible, and over 32 bit for code size and speed.
I managed to mess up mixing quite a few times. Initially it overflowed as soon as there were 2 voices at once. Then it clipped. Then it used some janky fake compression algorithm that resulted in a lot of distortion... Then I went back to the literature and learned what I was doing wrong: I wasn't leaving any headroom. Don't mix everything at full volume, kids!
There's some dynamic range compression on the loud end of things to extend the effective headroom a little further but it's hard to say whether this is worth doing with 4 bit output or not.
Manually Constructed R2R Resistor ladder - followed by a nice big potentiometer for volume control before amplification.
The resistor ladder is the part that started the whole thing. My resistors are terrible tolerance so 4 bits is about as much resolution as I could realistically expect before error started dominating the actual voltage fractions. I could have metered them and picked "good valued" ones, but the constraint doesn't bother me that much for this project.
I looked into PWM audio (delta-sigma DAC) and it's actually probably the better solution here - but of course, it's about learning and understanding. It seemed wrong to throw out the 4 bit DAC when it had been there since the beginning.
Dual op-amps, TLV 2372I-P for those playing along at home.
This took me a long time to get right, for all the wrong reasons. My initial amp design (visible on the right) was a common emitter amplifier - it was pretty finicky and I couldn't get the power I wanted out of it, plus the gain was unpredictable. This was likely a misunderstanding or bad implementation on my part, but in the end I decided op-amps would probably be a simpler and more accurate solution.
I was initially doing all sorts of over-complicated nonsense trying to get more gain and wondering why the signal was saturating. After reading up on op-amps again and actually looking at the signal (:D) things became a little clearer - I didn't need any more voltage and I wasn't going to get it out of my 5v supply anyway, I just needed more power output (higher current) and to stop loading my poor homebrew DAC.
Wiring them up in parallel as a simple voltage follower works a treat, and requires a lot less circuitry than my previous attempts. I'm considering adding a filter stage before the amp, but for now the "raw" output works pretty well.
A little 8-Ohm Speaker salvaged from this phone that was languishing in my components draw.
The frequency response on it is likely terrible (I haven't measured it and I cant find the part number) but it's going to be crammed inside a plastic enclosure playing 4 bit low sample rate sound, so the main thing is it's loud enough. After getting the amp working properly it seems adequate.
The amp+speaker board in particular is currently a bit of a mess and could use some rework. It's mostly that it isn't breadboard friendly but I imagine it's not going to be great to fit into an enclosure either.
The next "big step" is to build a visual interface for it, and do so without choking the poor CPU.
I've got a tiny (0.96") OLED screen which I could probably drive fast enough, but it's really really incredibly tiny. I worry that it'd be really awful to use. I've also got a "bigger" tiny TFT screen (1.7") that I kind of worry about driving fast enough. In either case it'll be important to optimise memory usage (currently got about 500b of SRAM left) and probably incrementally render stuff to avoid choking out the synth.
The other option for an interface would be a segmented/text LCD - could work but a lot of them are very pin-hungry. I've got 6 pins left so I'd have to find a module that talks some sort of serial protocol or use a separate driver chip. Either way, sounds time intensive.
If I'm really starved for memory I do have external SRAM/EEPROM chips but they'll be much slower to access - for this project it's not a great fit.
Either way, that's all for some other weekend. Get in touch if you need any more info. Hopefully there'll be another post within a month about how well the rest of it went!