Years before I started writing about Android, I was an avid user of the XDA forums, like many of my colleagues. While there were many different things that drew people to root and mod their Android phones, one of the more popular reasons was to install Viper4Android, an incredibly powerful audio enhancement tool. It not only offered a multi-band equalizer but also supported importing headphone correction files, applying various audio effects, controlling the gain, and more. I loved V4A for what it did, and not having it was one of the main things that made me want to root every new phone I got.
Then came Wavelet in 2020. The app, created by developer Thomas de Witt, made a splash in the Android community when people saw what it had to offer. It wasn’t quite on the level of V4A, but what it featured without needing root access was remarkable. AutoEQ profiles for thousands of headphone models, a 9 band graphic equalizer, effects such as reverberation, virtualizer, and bass tuner, and finally a limiter and channel balance feature were all included as part of the package.
Sound quality is subjective, of course, so whether or not apps like Wavelet actually make your songs sound better is up to the listener. What’s not up for debate is whether they work, as in they do actually tune the audio as described. I don’t know about you, but seeing Wavelet in action got me wondering how music equalizer apps are even able to apply audio effects to music sessions started by YouTube Music, Spotify, and other media players. It always felt a bit hacky to me, as if these apps are using an API in a way that’s unintended by Google, because they often don’t work with many media players. The fact that Wavelet asks the user to grant it a special permission via ADB in order to use “enhanced session detection” solidified the hackiness in my mind.
I don’t see that as a bad thing, though. In fact, I love these kinds of workarounds from an academic standpoint. That’s why in this edition of Android Dessert Bites, I’m going to dive into how music equalizer apps work on Android.
AudioFX - Android's Audio Effects API
The way audio works in Android (and, well, many operating systems) is quite complex, but it starts with an app using Android’s audio-specific framework APIs in the android.media package. At the lower level, Android’s audio hardware abstraction layer (HAL) defines the interface between the higher-level framework APIs and the underlying audio driver and hardware.
There’s a subset of classes in Android’s media framework that manage audio effects. These are Android’s Audio Effects APIs, or AudioFX for short, and they enable input (pre-processing) effects like acoustic echo cancellation, automatic gain control, and noise suppression as well as output (post-processing) effects like equalization, virtualization, bass boosting, and so on. Under the hood, these APIs call into the Effects HAL, which is part of the audio HAL, to control audio effects. For each audio effect that a device supports, the OEM has to supply a native library in the vendor image and list the effect in the audio_effects.xml configuration file.
Fun fact: this is what Viper4Android does when it installs its driver. It pushes the libv4a_fx.so audio effect library to /vendor/lib/soundfx and patches audio_effects.xml in /vendor/etc to define the v4a_fx audio effect.
OEMs have the option to define a default preprocessing effect that is applied to a particular audio source, eg. acoustic echo cancellation applied to the voice communication stream, or they can enable certain audio effects for specific audio devices that are attached. App developers, on the other hand, have the option to attach an audio effect of their choice to a specific AudioTrack or MediaPlayer instance by specifying the audio session ID of that instance. AudioTrack and MediaPlayer are two commonly used APIs to play audio files, while an audio session ID is a “system-wide unique identifier for a set of audio streams” that is primarily used “to associate audio effects to audio players.” By supplying the audio session ID to the AudioEffect API, an app like your typical music player can attach a multi-band equalizer effect to whatever audio it’s playing.
The AudioEffect API and most post-processing effects were introduced all the way back in Android 2.3 Gingerbread, and most pre-processing effects in Android 4.4 KitKat, so they’re very old. Some post-processing effects are newer, like DynamicsProcessing (introduced in Android 9 Pie) and HapticGenerator (introduced in Android 12). The Spatializer effect for spatial audio is the newest one, having been introduced in Android 12L and further refined in Android 13, but it doesn’t appear as if apps can attach this effect to audio sessions in the same way that they can with other effects.
As an aside, the Wavelet app makes heavy use of the DynamicsProcessing effect, which is why it requires Android 9+ to run in contrast to other, less powerful system-wide equalizer apps that support older Android versions. I haven’t seen any apps use the HapticGenerator effect since that cool demo of audio-coupled haptics I showed off last year. Android’s Ringtone API seems to support enabling this effect for ringtones, but it’s seemingly unused by most apps including Google’s own (in favor of haptic waveforms hardcoded for each stock ringtone in the Pixel’s case). Kdrag0n, the developer behind last year’s audio-coupled haptics demo, privately showed me a Wavelet-like app he was developing called PhantomAmp. That app was ultimately never released, but it supported using the HapticGenerator to generate audio-coupled haptics for any currently playing audio. The haptics part felt off in many tracks (in my experience), which I think is a shame because last year’s demo made it seem really promising. It also didn’t work with some media player apps because they don’t request the VIBRATE permission.
One of the biggest challenges kdrag0n had while developing PhantomAmp is actually something that plagues most system-wide equalizer apps on Android, including Wavelet, and it’s the challenge of getting it to work with every media player app. The problem is that while it’s quite straightforward for an app to get the audio session ID if that app is the one that started the audio session in the first place, it’s not so simple to grab the ID if the audio session was started by another app. For apps that create their own AudioTrack or MediaPlayer instances, they can grab the audio session ID from their respective getAudioSessionId() methods. There isn’t an equivalent, one size fits all approach that system-wide equalizer apps can use to grab the ID for audio sessions started by other apps, if one even exists (which it may not for apps with custom audio stacks). That’s important because system-wide music equalizer apps can only apply equalization and other effects to, say, songs playing from YouTube Music or Spotify if they can get the ID for the sessions those apps started, because that’s what the various AudioEffect APIs require.
Hunting for IDs: Why so many music equalizer apps fail to work
In the very early days of Android, system-wide equalizer apps had it easy: just request the MODIFY_AUDIO_SETTINGS permission (which is granted at installation) and apply effects to the global audio output mix which is session ID 0! That way, equalizer apps wouldn’t have to hunt down the session ID, they could just apply effects to the global audio output and be done with it. Unfortunately, this method was short-lived: Google deprecated the ability to attach audio effects to the global audio output mix less than a year after Gingerbread’s initial release. There were “too many problems with supporting global effects,” according to a Googler on an Issue Tracker post, though those problems were never specified, even in the commit description. Although applying effects to session 0 has been deprecated for nearly 11 years now, the method was never fully removed, and some devices still support it. In fact, the Wavelet developer tells me that the app’s “Legacy mode” still attempts this method.
Instead of the global approach, most equalizer apps instead rely on media player apps to broadcast the session ID for any audio sessions they create. The way this works is that media player apps opening a new audio session are expected to broadcast an intent that contains the session ID and the package name. The equalizer app registers a broadcast receiver to receive that intent and then uses the session ID to attach an audio effect to the audio session, with the package name optionally letting the equalizer app know what media player app it’s dealing with.
However, there are two problems with this approach. First, the equalizer app has to be running (either in the foreground or with a foreground service) in order to receive these broadcasts on Android 8.0+. This is because Android 8.0+ won’t deliver implicit broadcasts (broadcasts that don’t target a specific app) to receivers that are defined in the manifest. The workaround is to use a context-registered receiver that listens for the broadcast while the app is running. This isn’t a huge deal, but it does mean the equalizer app has to be running at all times. The second and more significant limitation is the fact that media player apps aren’t required to send this broadcast when opening an audio session, meaning equalizer apps won’t be receiving the session ID they need to work. Many media players don’t send this broadcast at all, while some require additional configuration to do so (as outlined in Wavelet’s documentation).
This is where most music equalizer apps give up* and where Wavelet shines. Wavelet is able to grab the session ID for most media players even if they don’t broadcast it, thanks to the use of the system dump tool. By requesting the DUMP permission, which can be manually granted by the user on production builds through the ‘pm grant’ command in ADB shell, it’s possible to read information from the audio system service. The AudioPlaybackConfiguration part of the output contains information on all audio sessions, and it lists the session IDs for each.
Wavelet calls this approach its “enhanced session detection” mode, and it enables it to support far more media player apps than most equalizer apps. It doesn’t work with YouTube which, from what I’m told, only logs its audio session ID for debugging purposes, but it does with most media players I’ve tried. I’m glad it does, because music equalizers are a great tool to tune the audio output to your liking. Many people don’t have high-end or even semi-decent audio gear, so these apps can go a long way in improving the audio. Audiophiles who aren’t happy with the stock sound signature of their headphones can equalize their frequency responses. There are other use cases outside of music, too, as shown by the AudioClean app which uses the DynamicsProcessing effect to clean up audio in Google Meet calls.
*Kdrag0n’s PhantomAmp app adds a rather unique approach to this problem: record the device audio using Android’s screen recording API and then apply effects to that audio. It does this as well as grabbing the session ID to apply effects in order to support even more apps. However, doing this requires Android 10+ and the microphone permission and blocks the user from recording the screen while the app is active.
This article was updated at 2:45 PM PT to correct how PhantomAmp detects and applies audio effects.