My initial, cheap and simple, design idea had failed before I even received the boards I had ordered. I had to start over, but the time was running out.
As the first prototype had stopped at the first possible roadblock (I2S streaming) and there were many more unknowns I hadn't bothered checking yet (flash size for sound file and touch-pad sensing, among others) I felt like spending time finding another cheap MCU, with an actual I2S peripheral, would be risky. I instead embraced my inner over engineer and went full overkill.
An RP2040 ought to do it. It actually ought to do it 👏so 👏much that what I had previously failed to do fast enough in C could now probably be done in Python (i.e. by running fast C code written by smarter people than me).
As hinted at in the last log entry, this had always been my plan B. I had routed the touch pads to the SAO connector on the prototype boards and ordered a Xiao RP2040 along with the I2S amplifier breakout board to avoid being delayed too much by the possibility that my bit-banging approach would fail.
Plan A was honestly a long-shot, and mostly done in the hope that a PY32 would be enough. I had to spin a prototype PCB to get my hands on some circular touchpads anyway, and slapping a 25 cent MCU in there wouldn't cost me much additional time or money. You might even have noticed that the prototype didn't have an amplifier on it, as I always planned for a version two.
So with the prototype boards in house I could get to working on a breadboard prototype. This time I would make a functional prototype before ordering another PCB.
Step 1: Touch pad sensing
As revealed above, I hoped to do the code in python. I was now using a mature MCU, and there should be lots of open-source code out there for all the things I needed to do in this project. So, I naturally assumed it would be smooth sailing to get to a prototype.
After a tiny bit of research I opted for using Circuitpython instead of Micropython, as it has better library support and I wasn't going to be doing anything revolutionary here. The Circuitpython filesystem is also way easier to work with than Micropython's (and honestly, this makes a huge difference for any project in my opinion).
For captouch sensing, I didn't even have to install any libraries. The necessary "touchio" library is built-in with the so called "core modules". This library provides both a digital touch/no touch output for each pad, as well as an analog "raw" signal that I assume is a representation of the discharge time. To be honest, the documentation is a bit lacking, so without digging into the source code it's kind of difficult to know exactly what's going on. Luckily, I'm not the first person to use these libraries, and I don't really need to understand what's going on underneath the hood - so looking through some of Todbot's repositories, like the code for his picotouch product series or the code for his Supercon 2023 badge hack, quickly got me going with the syntax.
Converting the raw values to cartesian and polar coordinates was also ridiculously easy, as I was using a 4 touchpad design. Many resources indicate that no more than 3 pads are really necessary for circular pads of this size, but my nervously over-designed layout really paid off in terms of simplifying the code. Here's how I ended up doing the conversion:
y = T1 - T3 # T1 is up, T3 is down
x = T2 - T4 # T2 is left, T4 is right
angle = np.arctan2(y, x)
In the actual program I've first applied a moving average filter to the raw values, but I've simplified the example for clarity.
How to filter the input values is honestly one of the most complex tasks of this project, and still something I need to spend more time fine tuning at the time of writing. Ideally, I want a ridiculously fast response time, so that I can catch the fastest "wacka-wacka" movements, while not letting through any noise. Letting through noise is horrible because changes in the finger position is what drives the playback speed of the audio, and noisy estimates will cause the audio to stutter whenever a finger is placed stationary on the touch wheel. It will also cause jittery playback speeds when the "DJ" is performing a smooth, constant speed, manual disc revolution. As mentioned, this part of the code is what I will spend the most time improving in the future. For now: I'm happy with a working prototype that actually adjusts the audio playback speed.
Step 2: Audio playback speed
I'll gloss over the details in how I found a drum beat without too much deep bass (that I would have trouble representing with my tiny tweeter speaker) on Freesound, cut it to the shortest possible continuous loop, downsampled and converted it to mono in Audacity, and uploaded it to the MCU and loaded it into RAM. The most interesting part of the audio is how the playback speed is controlled (but I'll cover the loading to RAM part, as that was a bit tricky too).
Now, my idea so far had been to use DMA and modify some buffers that the (pio statemachine-based) I2S-peripheral could shuffle out to the amplifier, but at this point I discover that it's actually a bit tricky to do DMA writes in Circuitpython. This is possible in Micropython, as proven by Supercons 2023 Vectorscope-badge, but I wasn't able to find anything similar in Circutpython. Luckily I didn't really have to, as I was about to discover that a lot of the necessary functionality can be sort of hacked together by utilizing/misusing features of the synthio library.
This library is, as the name implies, intended to be used for digital audio synthesizers, but with more help from the aforementioned todbot, who is clearly a synthio power-user, I was able to find a way to play back a wav file instead of a note - which gave me access to the "pitch bend" effect I needed from the library to control the audio playback speed. The relevant section from his immensely helpful circuitpython-synthio-tricks repository is the one named turn wav files into oscillators. Here he describes how to use a library called adafruit_wave that lets you load any wav file (under a certain size) into RAM.
With my beat camouflaging as a "note" in synthio, I was now able to control the playback speed with the Note.bend property. What I had forgot to think about so far, was that using pitch bending would only allow me to reduce or increase the playback speed. For my effect to work I also need to be able to digitally rewind the record, or in other words to reverse the playback direction of the audio sample.
Now, reversing the playback speed is not currently supported in Circuitpython as far as I could tell. So I guess it's time to dive a bit deeper into how Circuitpython does this in the first place. Opening up the Circuitpython repository is more than a little daunting. Just loading it up in git on my 10 year old laptop takes a loooong time. Compiling it? Even worse. But I quickly noticed that the code for buffering up data to the I2S was fairly simple, and tightly linked with the pitch bend effect, so it shouldn't be hard to make a quick modification that would make it read the sample backwards instead of forwards.
What was a bit worse though, was to figure out how to expose the direction as a property that can be accessed from Python. And with compile times of between 10 and 30 minutes I quickly realized that I would have to accept the first working solution. No time for prettification. The result is honestly really bad, but it works, and since adjusting the playback direction of a Note doesn't really make a lot of sense in the upstream version of Circuitpython, I'll just keep my broken code in a fork that does nothing other than this one specific modification.
Step 3: The rest of the f*cking owl
The rest of the proof-of-concept demo didn't bring up too many interesting challenges, so I'll wrap it up here with a video that shows how this works in practice. Sound up! (Because my SAO could only fit a really tiny speaker, so it's barely audible).
Video hosted on Mastodon:
Post by @simenzhor@mastodon.socialView on Mastodon
As mentioned already, this code leaves a lot to be desired, but I'm confident that the right person could make it sound quite cool (even though I still need some convincing that the right person is me).
So even in its current state, this project feels like a good SAO by my standards. With all this on a single PCB it would do something the moment it is plugged in, and there's a lot of room for software hacks/improvements. I guess it's time for a respin!
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.