I've finished the main development phase of the new SquishBox Python package and its collection of apps, and I'm pretty happy with where it landed. The biggest goal of this round of development was to make the SquishBox API flexible enough that it wasn't secretly designed around one particular application. The best way to find out whether I'd succeeded was to actually build several very different apps with it.
Hardware and UI
As I worked on the API, I kept coming back to the fact that the hardware has changed quite a bit over the life of the project. I've gone from buttons to a rotary encoder for navigation, added dedicated buttons for sending MIDI messages, and added LEDs. I've also changed my mind about how some of those controls should behave.
For example, I originally used a long press of the encoder to open a menu or confirm an option. It worked, but it made the interface feel sluggish: almost every interaction required waiting to see whether I was going to keep holding the button. Eventually I realized that the more natural behavior was tap to activate, hold to cancel. Most interactions are taps, so the interface feels much snappier.
That led me to an event system, where hardware actions such as "encoder moved left" or "button tapped" can be bound to higher-level UI actions such as "increment this value." It also made it obvious that I needed to separate the hardware layer from the UI layer. The LCD, buttons, encoder and LEDs are hardware; menus, events and text entry are UI.
Once that separation existed, adding another button or LED became a matter of creating an object rather than rewriting the UI. I also moved the configuration out of Python globals and into YAML. The configuration now describes what controls actually exist and what they're supposed to do. That's much closer to the philosophy of SquishBox: users should be able to change the behavior of their instrument without necessarily having to change the code.
MIDI Without Becoming a MIDI Router
MIDI presented an interesting boundary of its own.
SquishBox has a global configuration menu that any application can use for things that are really properties of the instrument: LCD brightness and contrast, Wi-Fi, rebooting, shutting down, and so on. It seemed reasonable to extend that to MIDI connections too. I wanted to be able to say things like "connect my MPK Mini to amsynth" or "automatically connect any MIDI controller to FluidSynth."
I also wanted SquishBox's own buttons to be able to send MIDI messages. A hardware button might, for example, send a CC when it toggles.
But I kept hearing my own earlier argument: FluidPatcher should do FluidPatcher things. And SquishBox shouldn't turn into a general-purpose MIDI router either.
The solution was python-alsa-midi, a Python wrapper around the ALSA sequencer. SquishBox can create its own MIDI port, listen to ALSA's system announcements so it can react when devices appear, manage connections, and send its own occasional MIDI messages. What it doesn't do is route arbitrary MIDI messages between applications. That's the job of an application that actually wants to be a MIDI router.
That distinction has turned out to be pretty useful throughout the redesign: SquishBox can provide infrastructure without trying to become the application that decides how everything should work.
Three Apps to Test the Idea
I deliberately decided to build three substantial audio apps rather than just porting my existing FluidPatcher app. If the API really was general-purpose, these should be different enough to expose the places where I'd accidentally built assumptions about what a SquishBox application was supposed to be.
The first was amsynthbox, a SquishBox interface for amsynth. Unlike FluidSynth, amsynth doesn't expose a convenient C API that I can wrap with ctypes, so I had to launch it as a subprocess and figure out how to connect to its MIDI port. That work fed back into the SquishBox MIDI code and made the general MIDI connection handling considerably better.
The second was trackbox, a simple music player. For audio playback I settled on GStreamer, which turned out to be a great fit: it handles the actual audio playback and all the different file formats without the application having to care much about the details.
I initially got distracted trying to make a clever crossfading system between tracks. Eventually I realized that this was exactly the sort of complexity I was trying not to bake into SquishBox. GStreamer has playbin, and playbin works really well. Sometimes the best design decision is to stop trying to improve something that already works.
Trackbox also pushed the UI event system in useful directions. I wanted playlists that could scroll vertically and be reordered, while also responding to events from the audio player when a track finished. That made me think carefully about how the event loop and get_action() should handle application-specific events such as "end of stream."
It also gave me a useful lesson about what not to put into the SquishBox API. I could have added a special menu_verticalscroll() function, but I decided that this was too application-specific. A SquishBox application can implement that behavior itself using the lower-level event primitives. The API should provide useful building blocks rather than trying to anticipate every kind of application someone might write.
Rebuilding FluidPatcher as an App
The new FluidPatcher app, fpatcherbox, gave me another set of interesting problems.
The obvious functions were straightforward: load and save banks, select patches, create and rename patches, and so on. But I also wanted to be able to browse and change the sounds used by individual channels.
The old FluidPatcher had a solo_soundfont() function that was basically a SquishBox-specific convenience feature. It temporarily put the synth into a single-channel mode so you could browse through a SoundFont and turn one of its presets into a simple patch. Now that FluidPatcher was properly separated from SquishBox, that didn't belong in the library anymore.
The new Bank architecture actually gave me a better solution. Because the app can navigate the bank structure directly, it can work with RouterRule and SFPreset objects and preview how a particular sound will behave inside the current patch. That led to some useful new possibilities, including creating and editing layers directly from the SquishBox interface.
It also exposed a requirement in the new Bank class: these objects couldn't just be convenient ways to read a bank. The application needed to be able to modify them and serialize those changes back to YAML. That's where the copy() functionality came from.
This is one of the things I've really liked about developing the apps alongside the API. Rather than trying to guess what a generic API ought to contain, I can build something with it and let the application expose where the abstractions are wrong or incomplete.
The SquishBox as a Little Computer
I also built a few smaller utility applications, partly because they were useful and partly because they were good tests of whether the API was actually useful outside of audio.
sbedit is a simple line-oriented text editor. The rotary encoder isn't exactly going to replace a keyboard for writing a novel, but it's perfectly adequate for making a quick change to a bank file directly on the SquishBox.
Then I figured out how to use python-evdev to accept input from a USB keyboard. That turned out to be much easier than I expected. The keyboard listener only needs to exist while the text-entry UI is active, so I don't have to keep another input-processing system running all the time. Once I had that working, the text editor became considerably more practical.
I also wrote sbcommander, a little file manager inspired by Midnight Commander. Combined with USB automounting, it makes it easy to move, copy, rename and manage files directly from the SquishBox. This also made me reconsider the old browser-based file manager I'd used in the past. Installing a web server, opening a port, and worrying about the security implications always felt like a lot of machinery just to copy a file.
And sbcommander has one more trick: it can execute shell commands. With a USB keyboard attached, that effectively turns the SquishBox into a tiny Linux console. A power user can get surprisingly far without ever leaving the little LCD interface.
That wasn't really part of the original vision, but I think it's one of the things I like most about where the project has ended up. It's an instrument, but it's also a small computer that happens to be designed to be operated like an instrument.
SquishBox 1.0
At this point, I've exercised the API with a FluidSynth-based synth, an external synth, a music player, a file manager, a text editor, and various little utilities. Each one has exposed something that needed to change, but the resulting API feels general enough that I'm no longer designing it around a particular application.
So I'm calling the main development phase done and publishing SquishBox 1.0.0 to PyPI.
There are still plenty of things I want to improve, of course. But the next problem isn't really about the API anymore. It's about everything surrounding it.
I've now got a Python package, optional Python packages, Debian packages for system dependencies, configuration files, hardware setup, services, audio configuration, and a Raspberry Pi that needs all of those pieces to end up in the right places.
In other words, I've built the thing.
Now I have to figure out how to install it.