The Motivation for the Project
I've always been fascinated with the music that can be produced live by a single person using only loopers and sequencers. I own a looper pedal, but the setup involved can be a bit prohibitive for quick practice or jam sessions. I thought that it would be useful to have a simple device that could play drum tracks for me while I practiced playing bass guitar.
The headphone bass amp that I have does include drum tracks with adjustable tempos, but my problem is that none of the premade drum parts on any of the equipment I own is ever particularly to my liking. So I also wanted my device to be able to capture custom drum rhythms.
Finally, the device should be small and easy to use. This would set it apart from most drum machines. While they are often small, their controls are unintuitive, and they have quite the learning curve. Such devices also often place limitations on the ability to reproduce triplets and other more complex rhythms, as they are based around a step sequencer with even divisions of time in powers of two.
Why Only 1-Bit?
One of the reasons that I really enjoy writing software for embedded systems is the way that it challenges me to try to do more with less resources. I started out on 8-bit microcontrollers, many of which do not come with a built-in DAC. However, I also really wanted to apply my skills to creating hardware for music making.
Initially this limited me to creating devices that played monophonic, square wave music. This has its own kind of charm, but it also felt limiting in terms of potential applications. Eventually, in the process of working on another project, I realized that it was possible to create noise by generating a square wave that changes states randomly.
As a lover of gimmicky devices, I saw this as an opportunity to create something interesting. However, it goes beyond just gimmicks. 1-bit audio actually comes with several practical design advantages.
An obvious advantage is that it results in less overhead for the sound output. A DAC is not necessary, which reduces the cost of the system. Not having interface with a DAC also reduces the complexity of the software that produces the output.
Another subtle advantage comes at the amplification stage. Because the audio signal is either high or low at any given time, this means that a single transistor can be used to amplify it instead of an op-amp or similar integrated circuit. This decreases the size, cost, complexity, and possibly power consumption of the system.
Choosing a Microcontroller
Given that this system was designed around explicitly not using costly peripherals, the potential options for microcontrollers are very large. Since this project is mostly for me, I decided to crack open my box of microcontrollers and see what I already had. A while back I'd ordered a few ATmega328Ps when I became frustrated with trying to bit-bang a serial connection on the ATtiny85.
While I didn't need all of the peripherals offered on the 328, I didn't have as much desire to work with UART as I did when I'd bought them, and there wasn't much reason to hold on to them hoping that that would change sometime in the future.
Choosing a 32-bit microcontroller would not be worth it in this situation. I did not have many in my possession at the time, as I was much more comfortable working with 8- and 16-bit devices. Additionally, a 32-bit microcontroller would be overkill for such a simple project.
Another point in the ATmega328P's favor was that I was already fairly familiar working with the AVR architecture. I'd mostly used the ATtiny85 in the past, but a lot of knowledge gained working with those was still useful when working with the ATmega328P.
The only other primary contender was the ATtiny84A, of which I also had several laying around in my parts bin. The main disadvantage of the 84 is the lower pin count. After using up the SPI and RESET pins for connecting the programmer to the device, that left 8 available pins. If it became necessary to run the microcontroller at 16MHz, which has often been the case in previous audio projects, I would need to sacrifice an additional two pins for a crystal.
While it would be possible to construct a bare-bones version of the design using only 6 pins, this could potentially cause problems in the future as I may eventually seek to add more features. If I ended up needing the extra pins, I'd end up having to rewrite significant portions of the code to port it over to a new microcontroller.
The naïve solution would be to store the current state of the audio output and set the audio pin to that value every sample. This works fine for DACs, but when dealing with standard digital GPIO pins, it doesn't work as well, because pins can't be set to a variable in one fell swoop. They can be only be operated on using bitwise operators, or else the whole port will be affected.
The most commonly used bitwise operators are OR and AND, used for setting and clearing bits respectively, but there is another less commonly used operator that could serve me well in this situation: XOR. XORing a value with one inverts its value. This led me to devise a system in which each part kept track of whether or not such a swap needed to be reflected on the output. It just so happens that this method of keeping track of outputs aligned well with the sound generation framework that I'd already implemented.
Partly to simplify development, and partly to save on code space, all of the various percussive sounds were generated with the same sound engine. Essentially, it just generates band-limited noise. Adjusting the frequency band alters the timbre of the sound produced.
A high pass filter was implemented by forcing an audio change if one had not already happened within a given period of time. A low pass filter was implemented by allowing random changes to occur after a period of time had elapsed since the last change. These filters aren't meant to be perfectly accurate. Rather, they're meant to provide a ballpark estimate of the frequency band desired in the output.
The chance of an audio change occurring was represented as a fraction with an 8-bit numerator and an 8-bit denominator. This was adjustable as part of the part's timbre, and it acts as a "center frequency" of sorts for refining the noise generation beyond the filters.
C's standard library provides the
rand() function for
generating random integers. Usually it would be seeded with the
srand() function, but this wasn't necessary for my use case.
It wouldn't matter if every bass drum has the same sequence of audio
samples, since it would all be noise to the listener anyways.
Trouble with Amplitude
Initially, I was planning to use a photodiode paired with an infrared LED as a PWM-able resistor to allow for the amplitude of the outgoing signal to be adjusted. This idea was borrowed from a video by mitxela on YouTube, where he uses an cadmium sulfide photocell to similar ends. The reason I opted for photodiodes over photocells was that I wanted an RoHS compliant solution, even though the project would likely never be mass produced or sold.
However, I soon realized that photodiodes behave differently from photocells. Initially I believed them to be essentially drop-in replacement; both are methods of controlling electrical current with light. Photocells alter their resistance when exposed to light, whereas photodiodes generate current when exposed to light. This means that photodiodes cannot be used as resistors in the manner that I had intended.
Eventually, I decided that this was for the best, in keeping with the initial 1-bit gimmick of the system. The ability to adjust a signal's amplitude would essentially be a DAC, with many of the same disadvantages that that would entail. PWM would add a considerable overhead to the software as well as taking up an additional timer for frequency generation. This design currently only uses one timer, but I would like to leave the other two available for future feature additions if possible.
As mentioned previously, I was intent on having a system of storing rhythms that was less limiting than a traditional step sequencer. My initial naïve solution was to implement a step sequencer with many small steps. However, this would put severe limitations on loop length and waste a large amount of memory on steps where nothing was happening.
Instead, I decided to implement a system similar to how events are stored in MIDI files. Each event would be stored with the relevant data of which parts were hit at that moment as well as the time since the last event. Not only is this a much better use of the limited memory, it also changed the limiting factor on loop length from duration to number of events, allowing for longer, slower loops to also be played.
The First Draft
Below is the schematic of my first working prototype. It is powered with a 9V battery, and it runs at 3.3V.
I thought it would be worth comparing the frequency spectrum produced by my creation to that of some acoustic drum samples I found online. I tried to choose samples that generally matched the sound I was aiming for with the respective 1-bit parts. Below are the results.
Here's my take on the above. The small speaker on the device severely limits its capability to represent low frequencies, so I can't really blame it for not having the best results in that department. However, it seems like my high-pass filter isn't performing that well on the kick drum, as most of its output occurs in the frequency band in which the acoustic kick drum's spectrum fall in amplitude.
Additionally, both the acoustic snare and hi-hat have a peak at just under 300Hz, which I suspect is due to the initial impact sound. The 1-Bit snare and hi-hat both have noticeable dips in their output at this range, so this may need to be considered when improving the sound engine in the future.
If I were to do the project again, I'd use a different voltage regulator. The LD1117 is great at what it does, but it's also fairly large, requires a heat sink, and also provides more current than necessary. A pair of AA or AAA batteries would be a simpler way to produce 3V power while being similarly compact when compared to a 9V battery.
In the future, I'm planning to take advantage of the unused pins to allow for on-the-fly manipulation of the timbre of each part. I'd also like to use the ATmega328P's built in EEPROM to store these timbres as well as user-created loops. Additionally the ability to adjust the tempo of a loop after it is recorded could be extremely useful.