<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tahskajuha</title>
    <description>The latest articles on DEV Community by Tahskajuha (@tahskajuha).</description>
    <link>https://dev.to/tahskajuha</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3983182%2F208069f3-94ef-4e80-ba94-b245a37b2c23.png</url>
      <title>DEV Community: Tahskajuha</title>
      <link>https://dev.to/tahskajuha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tahskajuha"/>
    <language>en</language>
    <item>
      <title>I Hijacked Android's Forgotten Audio Sink to Capture System-wide Audio</title>
      <dc:creator>Tahskajuha</dc:creator>
      <pubDate>Mon, 15 Jun 2026 18:11:39 +0000</pubDate>
      <link>https://dev.to/tahskajuha/i-hijacked-androids-forgotten-audio-sink-to-capture-system-wide-audio-3h2k</link>
      <guid>https://dev.to/tahskajuha/i-hijacked-androids-forgotten-audio-sink-to-capture-system-wide-audio-3h2k</guid>
      <description>&lt;p&gt;It was early 2024 when I got my Samsung Galaxy Tab A9+ 5G. I still remember rooting it almost instantly while keeping the stock ROM because I am a heavy Samsung DeX user. Being somewhat of an audiophile, I also installed Viper4Android and gradually turned the tablet into my personal media hub for music, podcasts, radio, YouTube, and Twitch.&lt;/p&gt;

&lt;p&gt;Thing is, I spend much more of my time on both my laptop and my unrooted phone than on the tablet. Controlling playback is simple enough with VLC's remote control feature and this is the part where I thought to myself: &lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Wouldn't it be cool if I could stream the tablet's audio output, after all processing and effects from V4A, to my laptop and phone?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Initially it sounds like an easy problem. What follows is a record of a project that grew into three separate repositories, and a solution that I still use every day.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclaimer: This article documents my personal experimentation on a rooted Samsung Galaxy A9+ 5G (SM-X216B). This is not a guide or a recommendation but rather just a record of what I tried, what broke, what I understood (mostly, what I misunderstood), the compromises I had to accept, and what ended up working for me under specific constraints.&lt;/p&gt;

&lt;p&gt;Any code, patches, configuration changes, or binaries mentioned here are provided strictly for reference. If you choose to experiment with similar modifications, you do so entirely on your own risk.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;⠀&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Let's Start With The Existing Solutions
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
The most obvious solution was Bluetooth. The problem with it is that it never really fit my workflow. Most of the time my headphones were already connected to either my phone or laptop because that's where I was actually working. A2DP works for streaming audio from the tablet to the laptop which is actually what I used right until I deployed my own solution; but, you can't stream from Android to Android with it. On top of that, I was never particularly fond of the codec compression and general RF congestion that comes with University life.&lt;/p&gt;

&lt;p&gt;The next place I looked was Android's own capture APIs. Applications like Zoom and Google Meet can capture both audio and video during screen sharing through the MediaProjection API. Unfortunately, MediaProjection comes with a deliberate limitation as applications can just opt out of being captured by setting FLAG_SECURE. This was suboptimal, especially because A2DP does not have this caveat and I really did not want to downgrade my system to the point where any app could just make my life miserable by adding one line into its code at any point.&lt;/p&gt;

&lt;p&gt;The solution needed to be application-agnostic and it was clear that achieving that on this layer was not feasible. I needed to go deeper.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  I Might Have Gone a Little Too Deep
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This next section is just a bunch of me trying to come up with a very questionable hardware solution and inevitably achieving absolutely nothing useful.&lt;/p&gt;

&lt;p&gt;I just included this part for historical accuracy. You can go ahead and skip it entirely without missing anything other than the gradual escalation of my misery.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
Alright, given the constraints, I needed to think outside the box. I just decided that the box in this case was the entire tablet. I mean, if the software is gonna stop me from getting a hold of the audio output then I will just take it from where the output is actually supposed to go: the audio jack.&lt;/p&gt;

&lt;p&gt;The idea started simple-ish. Take the output from the audio jack, make it look like something a mic would produce and then just send it back through the same jack it came from. Once the output became a physical input, it would no longer be subject to Android's playback capture restrictions.&lt;/p&gt;

&lt;p&gt;Unfortunately, the Devil's in the details. Headphone outputs operate at volt-level signals while microphone inputs expect millivolt-level signals, so the signal had to be attenuated, filtered, AC-coupled, and conditioned before it could even be considered safe to connect.&lt;/p&gt;

&lt;p&gt;This is what I ended up with after several iterations in LTSpice:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsrv9u08md7bbowja77f4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsrv9u08md7bbowja77f4.png" alt="LTSpice schematic of a proposed hardware loopback circuit. The design mixes the left and right headphone channels into a mono signal, filters and attenuates it to microphone levels, generates a 4.5V virtual ground using a TL072 op-amp powered by a 9V battery, and AC-couples the conditioned signal into a simulated microphone input. The microphone detection network and bias circuitry are also modeled to test whether the device would simultaneously recognize a microphone and accept the injected audio signal." width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This circuit solves the signal-conditioning part. Unfortunately, microphone detection completely breaks it.&lt;/p&gt;

&lt;p&gt;See, modern devices don't just accept microphone input because a signal appears on the microphone pin. They first verify that something resembling a microphone is connected by measuring the resistance presented to the jack. The resistor needed for microphone detection ended up fighting with the already tiny audio signal I was trying to inject.&lt;/p&gt;

&lt;p&gt;Surprisingly, I had a moment of lucidity at this point as I realized that the amount of time-and-money required for building a mono hardware solution that wasn't even guaranteed to work had made the cost-to-benefit ratio touch grass.&lt;/p&gt;

&lt;p&gt;And hence, the "hardware approach" failed entirely.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  It Ain't Over Just Yet
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
Ok, the hardware route did not work out. That's a long amount of work down the drain but, whatever. There's still a problem to solve. At this point, I had explored both ends from hardware to high level API; the only sensible option left was something in-between. So, I decided to go diving into the Android audio stack. This led me to looking through audio sinks and if any of them could be intercepted. During that, I came across &lt;code&gt;remote_submix&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's circa Android 4, before MediaProjection API was even conceived. Prime time, if not a little behind its competitors for Android to introduce features such as Miracast, Screen Capture, and other stuff requiring audio output capturing. Back then, to implement that stuff, a special audio sink was designed to act as a pipe that the output could be routed through. Thus, remote_submix was born.&lt;/p&gt;

&lt;p&gt;As of present day, on modern Android, remote_submix has been almost entirely phased out in favor of the MediaProjection API. But it is still just sitting there as a free audio sink. In fact, android actually still allows access to it if you use either root or ADB shell, as long as you still use the MediaProjection API as the entry point to interact with (which is an oversimplification of what &lt;code&gt;scrcpy&lt;/code&gt; does to capture audio).&lt;/p&gt;

&lt;p&gt;First things first, I wanted to see what would happen if I made remote_submix always available. So, I pulled &lt;code&gt;/vendor/etc/r_submix_audio_policy_configuration.xml&lt;/code&gt; to modify and make a Magisk patch out of it. All I had to do was just add one line really:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&amp;lt;module name="r_submix" halVersion="2.0"&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;    &amp;lt;attachedDevices&amp;gt;
    &amp;lt;item&amp;gt;Remote Submix In&amp;lt;/item&amp;gt;
&lt;span class="gi"&gt;+   &amp;lt;item&amp;gt;Remote Submix Out&amp;lt;/item&amp;gt;
&lt;/span&gt;    &amp;lt;/attachedDevices&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;    &amp;lt;mixPorts&amp;gt;
        &amp;lt;mixPort name="r_submix output" role="source"&amp;gt;
            &amp;lt;profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
                     samplingRates="48000"
                     channelMasks="AUDIO_CHANNEL_OUT_STEREO"/&amp;gt;
        &amp;lt;/mixPort&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;        &amp;lt;mixPort name="r_submix input" role="sink"&amp;gt;
           &amp;lt;profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
                    samplingRates="48000"
                    channelMasks="AUDIO_CHANNEL_IN_STEREO"/&amp;gt;
        &amp;lt;/mixPort&amp;gt;
   &amp;lt;/mixPorts&amp;gt;
.
.
.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I applied the patch and realized that things had worked a bit too well.&lt;/p&gt;

&lt;p&gt;See, remote_submix is special in a way that it is the only virtual sink that does not interface with any hardware on a device. A benefit of that is several times faster initialization compared to any other audio sink. Since Magisk patches are applied at boot which also happens to be the time when Android decides priority of every audio output, remote_submix always wins and becomes default.&lt;/p&gt;

&lt;p&gt;Not an issue since I can probably use Tasker or just a basic shell script to change which audio sink to use as long as it is available. I am also personally not the type to complain when my beer is too cold. &lt;/p&gt;

&lt;p&gt;So, let's go forward and try to capture audio from it using an the AudioRecord API to see if this sink can be captured. And, this is what I got:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;permission denied: capture not allowed&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So even remote_submix was a subject to Android's capture restrictions. I suppose it was a bit naive of me to expect otherwise. Now, I am just back to square one.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  I Am Really Starting to Get Sick of This
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
Time to directly tackle the layer causing all of this. While the sink configuration is defined in an XML file, all the policy is enforced via binaries written in C++. If I were on a Pixel device, I would have gone directly to AOSP long ago. Unfortunately, OneUI source, while derived from AOSP is proprietary. I have no idea what changes are made to the binaries I want to target.&lt;/p&gt;

&lt;p&gt;With no other option left, I decided to open up Ghidra. I pulled &lt;code&gt;/system/lib64/libaudiopolicyservice.so&lt;/code&gt; from the tablet and imported it into the program. I then searched for the string "permission denied: capture not allowed" and found this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh4wyuy03jf1lde3kr124.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh4wyuy03jf1lde3kr124.png" alt="Screenshot of Ghidra decompiling Samsung's libaudiopolicyservice.so. Two highlighted error strings show the audio policy enforcement paths: " width="799" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This looked promising at first, but what really convinced me I had found the correct enforcement point was the Knox branch sitting right underneath the capture checks.&lt;/p&gt;

&lt;p&gt;Knox is Samsung's enterprise security framework. A dedicated whitelist being implemented here strongly suggested that this was the most optimal spot to do so.&lt;/p&gt;

&lt;p&gt;But something was off. There is a specific case for remote_submix and even though I am using it, that is not the branch I am hitting. At that point, though I just decided to patch both branches and call it a day. I just had to apply this minor change in both branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- 00150c84 88 06 00 37        tbnz       w8,#0x0,switchD_00150ac8::caseD_0
&lt;/span&gt;&lt;span class="gi"&gt;+ 00150c84 88 06 00 37        b       switchD_00150ac8::caseD_0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I rebuilt the library, updated the Magisk module and tested AudioRecord again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it worked!&lt;/strong&gt; AudioRecord started running without errors and printed PCM data that actually looked valid!&lt;/p&gt;

&lt;p&gt;Just as a sort of recap, let's go through what I had at this point. For starters, what I did not have was the time I spent on this entire ordeal spanning across 2 years. What I gained in return however, was the ability to capture, modify or stream my tablet's output post V4A, completely app-agnostic.&lt;/p&gt;

&lt;p&gt;There was one caveat here too though: I had patched both branches enforcing the rules. This allowed me to also be able to capture audio from every other audio sink on the device as the switch-case was basically entirely neutered.&lt;/p&gt;

&lt;p&gt;Basically, in my quest to reroute audio without restrictions, I had scorched earth and opened the Pandora's Box of security concerns. Honestly though, I didn't even care as this was something you would have to write custom code for if you wanted to attack it and as far as I know I am not on someone's hit list... yet...&lt;/p&gt;

&lt;p&gt;So, I won. This is the part where I get to live happily ever after, right? Except...&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  I Got Curious
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
Ok, that switch-case was clearly modified Samsung and not created by them. I had to know if I could find it in AOSP just because I really wanted to know what was going on.&lt;/p&gt;

&lt;p&gt;So, I got my hands on the &lt;code&gt;av&lt;/code&gt; repository from AOSP, did some digging and found that my tablet's software version corresponded to the &lt;code&gt;android-platform-14.0.0_r24&lt;/code&gt; branch. After that I again just searched for the "permission denied: capture not allowed" string (God bless &lt;code&gt;ripgrep&lt;/code&gt;) and found the implementation in &lt;code&gt;AudioPolicyInterfaceImpl.cpp&lt;/code&gt;. Using that I was able to map the decompiled cases and important variables back to their original meanings:&lt;/p&gt;

&lt;p&gt;local_434 -&amp;gt; inputType&lt;br&gt;
case 0 -&amp;gt; AudioPolicyInterface::API_INPUT_LEGACY&lt;br&gt;
case 1 -&amp;gt; AudioPolicyInterface::API_INPUT_MIX_CAPTURE&lt;br&gt;
case 2 -&amp;gt; AudioPolicyInterface::API_INPUT_MIX_EXT_POLICY_REROUTE&lt;br&gt;
case 3 -&amp;gt; AudioPolicyInterface::API_INPUT_MIX_PUBLIC_CAPTURE_PLAYBACK&lt;/p&gt;

&lt;p&gt;But things still weren't adding up. I &lt;em&gt;was&lt;/em&gt; using remote_submix but I wasn't hitting AudioPolicyInterface::API_INPUT_MIX_EXT_POLICY_REROUTE.&lt;/p&gt;

&lt;p&gt;So, I decided to look deeper by tracing &lt;code&gt;inputType&lt;/code&gt; to where it was being assigned. I found what I was looking for in &lt;code&gt;AudioPolicyManager.cpp&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;status_t&lt;/span&gt; &lt;span class="n"&gt;AudioPolicyManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;getInputForAttr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;audio_attributes_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="n"&gt;audio_io_handle_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="n"&gt;audio_unique_id_t&lt;/span&gt; &lt;span class="n"&gt;riid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="n"&gt;audio_session_t&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AttributionSourceState&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;attributionSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="n"&gt;audio_config_base_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="n"&gt;audio_input_flags_t&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="n"&gt;audio_port_handle_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;selectedDeviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="n"&gt;input_type_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inputType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="n"&gt;audio_port_handle_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;portId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AUDIO_IO_HANDLE_NONE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inputType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;API_INPUT_INVALID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;AUDIO_SOURCE_REMOTE_SUBMIX&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="n"&gt;extractAddressFromAudioAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;has_value&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mPolicyMixes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getInputMixForAttr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;policyMix&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;NO_ERROR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ALOGW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s could not find input mix for attr %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;__func__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;c_str&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
            &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mAvailableInputDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AUDIO_DEVICE_IN_REMOTE_SUBMIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                  &lt;span class="n"&gt;String8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"addr="&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                                                  &lt;span class="n"&gt;AUDIO_FORMAT_DEFAULT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ALOGW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s could not find in Remote Submix device for source %d, tags %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;__func__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BAD_VALUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_mix_loopback_render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policyMix&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mRouteFlags&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inputType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;API_INPUT_MIX_PUBLIC_CAPTURE_PLAYBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inputType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;API_INPUT_MIX_EXT_POLICY_REROUTE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;explicitRoutingDevice&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;explicitRoutingDevice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Prevent from storing invalid requested device id in clients&lt;/span&gt;
            &lt;span class="n"&gt;requestedDeviceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AUDIO_PORT_HANDLE_NONE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mEngine&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;getInputDeviceForAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;policyMix&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;ALOGV_IF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"%s found device type is 0x%X"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;__FUNCTION__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ALOGW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"getInputForAttr() could not find device for source %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BAD_VALUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;AUDIO_DEVICE_IN_ECHO_REFERENCE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inputType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;API_INPUT_MIX_CAPTURE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policyMix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ALOG_ASSERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policyMix&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mMixType&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MIX_TYPE_RECORDERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Invalid Mix Type"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// there is an external policy, but this input is attached to a mix of recorders,&lt;/span&gt;
            &lt;span class="c1"&gt;// meaning it receives audio injected into the framework, so the recorder doesn't&lt;/span&gt;
            &lt;span class="c1"&gt;// know about it and is therefore considered "legacy"&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inputType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;API_INPUT_LEGACY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_is_remote_submix_device&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inputType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;API_INPUT_MIX_CAPTURE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;AUDIO_DEVICE_IN_TELEPHONY_RX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inputType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;API_INPUT_TELEPHONY_RX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inputType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;API_INPUT_LEGACY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, if the source is remote_submix, Android also expects an address describing where the other end of the pipe is. In retrospect, this makes perfect sense. remote_submix was designed as a routing mechanism rather than a standalone endpoint. If no destination is declared, Android effectively treats the pipe as terminating immediately and falls back to the generic capture path.&lt;/p&gt;

&lt;p&gt;The problem is that making remote_submix always available since boot means I don't get to provide an address. Besides, providing an address seems to be possible only via the MediaProjection API and I am so not dealing with that.&lt;/p&gt;

&lt;p&gt;So, instead of dealing with the address handling, I decided to just patch this library similar to how I had patched the other library such that the &lt;code&gt;source = remote_submix &amp;amp;&amp;amp; address == NULL&lt;/code&gt; path assigns &lt;code&gt;*inputType = API_INPUT_MIX_EXT_POLICY_REROUTE&lt;/code&gt;. This way I can leave the API_INPUT_MIX_CAPTURE branch untouched, thus significantly reducing the blast radius to just the remote_submix pipe rather than weakening capture restrictions across the entire system. Like using TNT instead of a nuke to get the job done!&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Here We Go Again
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
This time I pulled &lt;code&gt;/system/lib64/libaudiopolicymanagerdefault.so&lt;/code&gt; from my tablet and imported it into Ghidra. While there was no clean string to search for this time, I did manage to find the function by its name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* android::AudioPolicyManager::getInputForAttr(audio_attributes_t const*, int*, int,
   audio_session_t, android::content::AttributionSourceState const&amp;amp;, audio_config_base*,
   audio_input_flags_t, int*, android::AudioPolicyInterface::input_type_t*, int*) */&lt;/span&gt;

&lt;span class="n"&gt;ulong&lt;/span&gt; &lt;span class="n"&gt;__thiscall&lt;/span&gt;
&lt;span class="n"&gt;android&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AudioPolicyManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;getInputForAttr&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AudioPolicyManager&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;audio_attributes_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;param_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;param_2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;undefined4&lt;/span&gt; &lt;span class="n"&gt;param_3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;undefined4&lt;/span&gt; &lt;span class="n"&gt;param_5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;param_6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;param_7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;param_8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;uint&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;param_9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;param_10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;param_11&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, all I needed to find was everywhere &lt;code&gt;param_10&lt;/code&gt; was being assigned in the function. Then I filtered those to find only the assignments where &lt;code&gt;param_10&lt;/code&gt; was being assigned to 1 and this is what I got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight armasm"&gt;&lt;code&gt;&lt;span class="err"&gt;.&lt;/span&gt;
&lt;span class="err"&gt;.&lt;/span&gt;
&lt;span class="err"&gt;.&lt;/span&gt;
&lt;span class="nl"&gt;001c9f24&lt;/span&gt; &lt;span class="err"&gt;28&lt;/span&gt; &lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;        &lt;span class="nv"&gt;mov&lt;/span&gt;        &lt;span class="nv"&gt;w8&lt;/span&gt;&lt;span class="o"&gt;,#&lt;/span&gt;&lt;span class="mh"&gt;0x1&lt;/span&gt;
&lt;span class="err"&gt;.&lt;/span&gt;
&lt;span class="err"&gt;.&lt;/span&gt;
&lt;span class="err"&gt;.&lt;/span&gt;
&lt;span class="nl"&gt;001ca228&lt;/span&gt; &lt;span class="err"&gt;28&lt;/span&gt; &lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;        &lt;span class="nv"&gt;mov&lt;/span&gt;        &lt;span class="nv"&gt;w8&lt;/span&gt;&lt;span class="o"&gt;,#&lt;/span&gt;&lt;span class="mh"&gt;0x1&lt;/span&gt;
&lt;span class="err"&gt;.&lt;/span&gt;
&lt;span class="err"&gt;.&lt;/span&gt;
&lt;span class="err"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now truthfully, at this point I just decided to just change each one at a time and see if that works and wouldn't you know it, patching the second one to assign the value 2 worked perfectly alongside the new &lt;code&gt;libaudiopolicyservice.so&lt;/code&gt; that only had its API_INPUT_MIX_EXT_POLICY_REROUTE branch altered!&lt;/p&gt;

&lt;p&gt;Well, I actually have a proof of concept now: a controlled and reliable way to capture and reroute system audio post-processing without relying on fragile APIs or hardware hacks. It isn't scalable to every device out there or even safe enough to do so but it is surprisingly elegant for personal use.&lt;/p&gt;

&lt;p&gt;Now all I need is something to actually capture and send the audio over the network and something to receive it that can work both on Android and Linux.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building The Actual Streaming Stack
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
Alright, since basically no app out there assumes unfettered access to remote_submix, I had to write my own app that used the AudioRecord API to capture raw PCM, then encode it and send it over the network. This is the part where I decided to create a second repository separate from the Magisk module.&lt;/p&gt;

&lt;p&gt;I decided to keep the streamer app intentionally boring: OPUS encoding, RTP over UDP transfer to whatever IP and Port is specified by the user, and defaults that made sense. You can check out the app and its specs &lt;a href="https://github.com/Tahskajuha/remote-submix-streamer" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As for the receiver part: while I could have gone with WebRTC or GStreamer, I suppose; I wanted something lightweight, cross-platform and something that gave me control over stuff like latency, stuttering, buffer-sizes, etc. More importantly, I wanted something I could just run in the background and forget no matter if it receives packets to decode or not. I decided to therefore make a third repository: a configurable receiver library that any audio framework could call and get raw PCM from. I decided to write it in C and later JNI wrap it to run on Android.&lt;/p&gt;

&lt;p&gt;It's currently pretty buggy and won't exactly be competing with the existing giants in this lifetime; but, in my case it gets the job done. In case anyone's interested, you can check the library out &lt;a href="https://github.com/Tahskajuha/libopusrx" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Talking about the performance, the current implementation streams stereo 48 kHz audio encoded with OPUS using fixed 20 ms frames.&lt;/p&gt;

&lt;p&gt;CPU usage has been negligible in daily use. On Linux, the receiver generally consumes less CPU than applications I routinely keep open anyway, such as Firefox or Hyprland.&lt;/p&gt;

&lt;p&gt;Latency depends mostly on the buffering required to avoid underruns. In practice I observe approximately 120-150 ms over USB tethering and roughly 1 second over Tailscale.&lt;/p&gt;

&lt;p&gt;I guess a final architecture diagram is in order:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq928phdngz8khx185j50.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq928phdngz8khx185j50.png" alt="Architecture diagram of the final audio streaming pipeline. Audio from any Android application flows through AudioFlinger and Viper4Android, then through a patched AudioPolicy layer into the remote_submix virtual audio sink. The AudioRecord API captures the resulting PCM stream, which is encoded with Opus, packetized into RTP, and transmitted over UDP. On the receiving side, packets are parsed, buffered by a jitter buffer, decoded back to PCM, and played through ALSA or another audio framework before reaching the listener." width="589" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Epilogue
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
Well, I finally have everything I set out to achieve here. I have been using the entire pipeline for over a month as of writing this. Was it worth it? From a practical perspective, probably not.&lt;/p&gt;

&lt;p&gt;Honestly, I am not gonna end this by pretending it was actually about understanding Android better. NOPE. Bluetooth A2DP is still as horrible as when I started this journey, and I am glad to never have to deal with that curse ever again!&lt;/p&gt;

&lt;p&gt;I'm off!&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PS: for the sake of making this piece of writing somewhat bearable, I decided to cut out and compress several details that I felt weren't relevant. You can find an abridged, albeit slightly outdated at the end version of this thing along with the Magisk module and LTSpice schematic &lt;a href="https://github.com/Tahskajuha/sm-x216b-audio-reroute" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>android</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
