Dirtywave M8 client for Android
Recently I purchased an awesome tracker called Dirtywave M8. I have a few pieces or hardware (and software) that can be used to create music, but it’s pretty hard to find a piece of hardware that is both powerful and has a small form factor. I think M8 fits this description nicely: it’s a hugely powerful (you can create full albums on it) music creation device that you can use even when riding on a bus.
Seems like because of the global chip shortage it’s pretty hard to manufacture this device in the quantities that would fulfill the demand so the creator of M8, Timothy Lamb, decided to release a version of the M8 firmware that you can install on a Teensy yourself, provided you can get your hands on one. Then you just need to connect it to a device that can run a version of a client software that will provide the GUI and input/output for the device. The most popular headless clients are m8c and the web based M8WebDisplay. These are very useful if you plan on using your headless M8 with your computer or if you have an Anbernic or similar retro emulation console already.
This got me thinking though: what if you can use your phone as a client for the M8 since you probably have one already? Unfortunately iPhones are ruled out completely because they don’t allow you to access USB devices. Not all iPads though because newer iPads with M1 processors do support DriverKit which means you can write your own driver to communicate with USB devices.
This leaves us with Android. Yes, you can use M8WebDisplay on Android devices as well, but you will not have audio.
M8C for Android
M8C is a headless M8 client that uses SDL to provide a GUI and libserialport to communicate with the device. Since SDL is ported to a lot of platforms, one of them being Android, it seemed like it would be quite easy to get it to run (spoiler: it was not).
To get the SDL part of the m8c running on Android was actually quite simple with Android NDK. There’s even a skeleton of a working Android project included in SDL that you can copy/paste and start modifying for your own purposes.
GUI
First hurdle was libserialport because it assumes you have direct access to serial devices ( under /dev/tty*
) which you do not under an unrooted Android device. I needed another way to access a USB device that is connected to the Android phone with an OTG cable. Luckily such a project exists. It’s called libusb. Unfortunately, libusb can’t be used directly, because in Android you need to first ask for a permission to access the USB device from the Java side of the code, open a connection to the device, and then you can pass a file descriptor to the native code that can use it to communicate. You can’t do it by just trying to open a USB device from native code, it would just fail.
Luckily libusb supports this flow by having a libusb_wrap_sys_device
which accepts a file descriptor and wraps it to a libusb_device_handle
that you can use to call all the libusb API’s. Since libusb is not Android specific, it seemed like a good idea to contribute it to upstream and open a PR on m8c to support different ways to communicate with an M8 device.
At this point we can see the GUI on the phone:
Input
This was pretty cool already, but not that useful because you can’t interact with the device in any way. For that we need to add some buttons to the screen layout and pass the touch events to the input event processing mechanism of m8c.
The button logic itself is quite simple: every button maps to a bit in an 8-bit byte and since any button can be a modifier for any other button, you need to keep track of all the buttons currently pressed. Enum for buttons in Java can look something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.Set;
enum M8Key {
EDIT(1),
OPTION(1 << 1),
RIGHT(1 << 2),
PLAY(1 << 3),
SHIFT(1 << 4),
DOWN(1 << 5),
UP(1 << 6),
LEFT(1 << 7);
private final int code;
M8Key(int code) {
this.code = code;
}
public char getCode(Set<M8Key> modifiers) {
return (char) modifiers.stream()
.map(t -> t.code)
.reduce(code, (in, out) -> in | out)
.intValue();
}
}
When button goes down you add it to a list of modifiers and submit the code to m8c. When button goes up you send a 0 code.
This is what it looks like with added buttons:
Audio
Now to the hardest part: audio. Theoretically it should be quite simple to get the audio to work since M8 exposes itself as a class compliant USB audio device, but there is no audio when you connect the device and whatever I have tried there is no easy way to get it to work. It might be my lack of Android knowledge. Current theory is that since the M8 has both audio output and input, the output will just get redirected to the input, and you will not hear anything. I’m not 100% sure that’s the case, but it would fit the symptoms.
The way I did get it to work was to use libusb to connect to the audio interface of the M8 device (thanks to this project) and pass raw audio data from it to the SDL Audio functionality by opening the default audio output device using SDL_OpenAudio and SDL_QueueAudio. At this point audio worked on my test device. I built a pre-release version and published it to GitHub and tried to get some feedback from M8 Discord.
It turned out that the audio was still not working for some people, so I tested with another (older) Android device and I also didn’t have any audio. After a bit of hacking I found out that instead of using the default output, I had to set a preferred device to be the speakerphone to get the audio to work. Luckily SDL has another function called SDL_OpenAudioDevice that can be used to choose a specific audio device and not just the default one.
Unfortunately for me, it turned out that none of the Android drivers (SDL has three: AAudio, openSLES and Android) actually supported setting a preferred audio device! After a bit of digging in SDL code I decided to go all in and try to add the possibility to choose an audio device using Android drivers to SDL. I don’t have much C experience, but I found the abstractions in SDL pretty easy to follow. I managed to throw something together and opened a PR which was merged pretty quickly, but there are still some issues to be solved.
And now with audio:
Audio still doesn’t work on some devices, but I hope it is due to my bad understanding of C and if we fix the issues in the SDL audio drivers then it might start to work eventually.
Conclusion
Trying to build what seemed to be a simple wrapper for an existing application turned out to be a multi week journey going down different rabbit holes, but at the same time I have been learning quite a lot and I managed to contribute back to a few open source projects.