Enabling the FM radio in Ubuntu Touch

I recently realized that my Xiaomi Redmi Note 7 Pro, on which I installed Ubuntu Touch not so long ago, has a working FM radio. One of the many psychological bugs of mine is the irrational urge I feel of having my hardware, no matter whether I use it or not, supported by Linux. So, the fact that I never listen to the radio is unfortunately not a reason to dissuade me from wasting time on getting the FM radio working in Ubuntu Touch.

This post is a quick summary of my investigation, which should serve as a note keeper for when I'll actually get to implement the feature.

The Android story

It was a bit surprising to find out that Android does not offer an API to use the FM radio, meaning that we cannot simply expose an API via libhybris. Every manufacturer is therefore choosing their own API, and Android applications are most often developed for a specific chipset family, or need to carry the code to support all the various devices.

For example, I found the RFM Radio Android application which supports a few Qualcomm Snapdragon processors, for which the FM radio functionality is exposed by a kernel driver via the V4L API. This means that the FM radio is probably working out of the box with QtMultimedia's QRadioTuner. Unfortunately the Note 7 Pro uses a newer Snapdragon, and even after some days of investigations I couldn't find out how the radio driver communicates to the userspace; but more on this below. Other chipsets offer other APIs, and I was glad to find that someone already wrote a Ubuntu Touch FM radio application for the Volla phone, which has a Mediatek board.

Anyway, the lack of a unified FM radio API is probably the reason why most of the so-called “FM radio” applications on Android are not really using the FM radio but rather streaming audio from the internet.

The FM radio in the Note 7 Pro

Before I start talking about a phone that no one cares about, let me say that what I'm going to write applies to several other Snapdragon chipsets and could be relevant to other phones. For one, the Redmi Note 9 Pro uses the very same bluetooth chipset as the Note 7 Pro (and in case you are wondering why I mentioned bluetooth, it's because the FM radio functionality is delivered by the same BT chip), so all what I'm going to write here is also relevant for that phone.

In order to figure out how the radio worked on this device, I took the drastic decision to reflash the stock Android (well, MIUI), started the preinstalled FM radio application, and meanwhile looked at the logcat messages (I'm not sure if this is needed, but before doing so I went to the “Developer options” in the system settings and set the debugging level to the maximum). Among a lot of noise, this showed me lines like these:

I android_hardware_fm: Opened fm_helium.so shared object library successfully
I android_hardware_fm: Obtaining handle: 'FM_HELIUM_LIB_INTERFACE' to the shared object library...
D FmReceiverJNI: init native called
D android_hardware_fm: BT soc is cherokee
I android_hardware_fm: Init native called
I android_hardware_fm: Initializing the FM HAL module & registering the JNI callback functions...
D radio_helium: ++hal_init
D fm_hci  : ++fm_hci_init
I fm_hci  : hci_initialize

Well, even without knowing nothing about all what these lines meant, I had something I could search the internet for. So I found the Qualcomm FM code in CodeAurora, and search for the code relative to my Snapdragon 675 (aka sm6150). I quickly gave up on trying to make some sense out of the git tag naming in that repository, and just tried to search for a tag which could be referring to my device. I found one, and started browsing its source tree.

It turns out that Qualcomm provides a Java package which applications can use, and which internally dlopen()s the fm_helium.so library, which in turn depends on the libfm_hci.so library. I had a quick look at the source code of these libraries, which are also present in the repository, but decided that I would have had more chances of success if I just tried to follow the JNI code, and in particular the android_hardware_fm.cpp file. I'm not sure why this code is not using the C structure types defined in the headers provided by the helium library, and instead redefines all the constants and accesses the character buffers by offsets — it might be just for historical reasons — but in any case I decided to follow along.

The fm-bridge program

Since we have a rather net separation between the Ubuntu Touch and the Android worlds (the Android services are running inside an LXC container, with all their Android libs and dependencies), one should not attempt to write an Ubuntu process that loads the Android libraries, because the libc used in Android is different, so things are likely not to work. But we can have Ubuntu and Android processes communicate over a socket or other kind of IPC; so, what I decided to go for, is writing a small C program that will live in the Android side, it will talk to the FM radio (via helium_fm), and accept commands / give replies via its stdin / stdout.

I unimaginatively called it “fm-bridge”, and you can look at its horrible code here. Really, I just said it was terrible, so why did you look at it? I definitely need to rewrite it from scratch, possibly using the helium headers, but as a proof of concept this also works. Then I carefully examined the logcat output while using the MIUI FM radio application in Android, and figured out what was the command sequence I had to input into fm-bridge's standard input in order to have it tune onto a given frequency. I'm publishing the commands here too, should I ever lose my notes:

enableSlimbus 1
setControl 0x8000004 1
enableSoftMute 1
setControl 0x8000029 0
setControl 0x800000c 1
setControl 0x800000d 1
setControl 0x800000e 1
setControl 0x800002b 0
setControl 0x8000007 4
setControl 0x8000006 0x40
setControl 0x8000006 0x40
setControl 0x8000011 0
setControl 0x800000f 1
getControl 0x8000010
setControl 0x8000010 0xef
setControl 0x800000f 1
setControl 0x800001b 1
setControl 0x8000012 0
setFreq 89300
setMonoStereo 1

I'm sure that not all of them are needed, but I'll figure out the optimal sequence later. In order to use this program on Ubuntu Touch, I had to alter the vendor partition to add this program, but also the fm_helium.so and libfm_hci.so libraries (more on that below).

When feeding the above commands to the fm-bridge in Ubuntu, I saw that I was getting a logcat output similar to the one from Android, which was mildly comforting. No sound was comint out of the speaker or out of the earplugs, but I was hardly expecting it all to work at the first try. And I got convinced that the FM tuner was indeed working, because typing the command “startSearch 1” made a new frequency appear in the logs, proving that the tuner had found another station and tuned onto it.

Getting the sound out

This was actually the easiest of the steps, thanks to the Ubuntu Touch FM radio application we have for the Volla: its source code mentions a few pulseaudio commands that worked perfectly in the Note 7 Pro too, despite the fact that the underlying chipset is totally different. This should not be as surprising as it might sound like, given that Android has a common audio API.

Just for my future reference, the commands are these:

pacmd set-source-port 1 input-fm_tuner
pactl load-module module-loopback source=1 sink=0

Ta-daaa! The radio was now playing from the phone loudspeakers! It was indeed quite loud, and the volume buttons did not seem to have any effect on it, but the volume can be controlled with pulseaudio:

pactl set-source-volume 1 50%

Of course, if we ever manage to make this into an Ubuntu Touch feature, we'll have to find a way to make the volume respond to the volume buttons.

Addind the needed files to the vendor partition

The simplest approach (and the one I took initially) is that of downloading the vendor.img into your PC, loop-mounting it, adding the fm_helium.so, libfm_hci.so and fm-bridge files to it and then umount the partition and reflash it (remembering to converting it from/to a sparse image before downloading/uploading it). This approach works flawlessly, but I'm wondering if one might incur into issues if the version of the NDK used to compile fm-bridge is different from the one that was used to compile the other vendor binaries, so I decided to give it a try to build the whole vendor partition myself.

This turned out to be a non trivial process, because I was using the Halium tree to build the vendor image, and not the LineageOS which was used to build the vendor image for my device: I could make an image, but it took some time before I figured out which were the needed packages that somehow got lost because of the Halium changes and that had to be added to the Makefile.

To help my weak memory, I expanded the README file in the violet port with the steps needed in order to build the vendor image.

A system service for the FM radio

While it could be possible for Ubuntu Touch applications to directly access the FM radio device in the same way that Android applications do, this is suboptimal for a few reasons. Even if we provided a shared library to deal with the various radio chipset implementations, the application would either need to be unconfined, or we'd had to provide an ever-changing AppArmor profile that peeks new holes every time that a new device implementation is added (and what if this implemenation uses a generic kernel device, which could be used for other goals too?) and in any case we'd have to make this policy restricted, since the RDS data provided by the radio stations would reveal the user location (well, the city at least) to the application. Not to talk about concurrent access to the radio device if two applications attempt to use it.

Therefore, my proposition (and what I'll implement, if I'll live long enough or if someone doesn't beat me to it) is to have a system service deal with the various hardware differences and expose a D-Bus API that will be hooked up as a QRadioTunerControl plugin, so that Qt applications will be able to just use the QtMultimedia APIs to access the radio.

The service would also need to talk to the trust-store, to let the user decide whether the application should really be granted access to the FM receiver (and when using the turst-store, this decision is remembered, and revocable from the System Settings's Security panel). Of course we'll also need to add a fm-radio AppArmor policy to let applications use this service.

Comments

There's also webmention support.