Skip to content

Commit 6efa4e0

Browse files
committed
wip
1 parent 4a86359 commit 6efa4e0

File tree

5 files changed

+201
-4
lines changed

5 files changed

+201
-4
lines changed

app/src/main/cpp/NativeTrack.cpp

+60-2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ typedef void(*ZN7android11AudioSystem13releaseOutputEi19audio_stream_type_t15aud
8181
static ZN7android11AudioSystem13releaseOutputEi19audio_stream_type_t15audio_session_t_t ZN7android11AudioSystem13releaseOutputEi19audio_stream_type_t15audio_session_t = nullptr;
8282
typedef bool(*ZNK7android10AudioTrack19isOffloadedOrDirectEv_t)(void* thisptr);
8383
static ZNK7android10AudioTrack19isOffloadedOrDirectEv_t ZNK7android10AudioTrack19isOffloadedOrDirectEv = nullptr;
84+
typedef bool(*ZN7android11AudioSystem18isOffloadSupportedERK20audio_offload_info_t_t)(audio_offload_info_t_legacy& offloadInfo);
85+
static ZN7android11AudioSystem18isOffloadSupportedERK20audio_offload_info_t_t ZN7android11AudioSystem18isOffloadSupportedERK20audio_offload_info_t = nullptr;
8486

8587
class MyCallback;
8688
struct track_holder {
@@ -317,7 +319,7 @@ static void callbackAdapter(int event, void* userptr, void* info) {
317319
}
318320

319321
extern "C" JNIEXPORT jboolean JNICALL
320-
Java_org_akanework_gramophone_logic_utils_NativeTrack_initDlsym(JNIEnv* env, jobject) {
322+
Java_org_akanework_gramophone_logic_utils_NativeTrack_00024Companion_initDlsym(JNIEnv* env, jobject) {
321323
if (!initLib(env))
322324
return false;
323325
if (android_get_device_api_level() >= 31) {
@@ -333,7 +335,12 @@ Java_org_akanework_gramophone_logic_utils_NativeTrack_initDlsym(JNIEnv* env, job
333335
DLSYM_OR_RETURN(libutils, ZNK7android7RefBase9decStrongEPKv, false)
334336
DLSYM_OR_RETURN(libutils, ZNK7android7RefBase10createWeakEPKv, false)
335337
DLSYM_OR_RETURN(libutils, ZN7android7RefBase12weakref_type7decWeakEPKv, false)
336-
DLSYM_OR_RETURN(libaudioclient, ZNK7android10AudioTrack19isOffloadedOrDirectEv, false)
338+
if (android_get_device_api_level() <= 23 || android_get_device_api_level() == 28) {
339+
DLSYM_OR_RETURN(libaudioclient, ZN7android11AudioSystem18isOffloadSupportedERK20audio_offload_info_t, false)
340+
}
341+
if (android_get_device_api_level() <= 22) {
342+
DLSYM_OR_RETURN(libaudioclient, ZNK7android10AudioTrack19isOffloadedOrDirectEv, false)
343+
}
337344
if (android_get_device_api_level() == 23) {
338345
DLSYM_OR_RETURN(libaudioclient, ZN7android11AudioSystem16getOutputForAttrEPK18audio_attributes_tPi15audio_session_tP19audio_stream_type_tjj14audio_format_tj20audio_output_flags_tiPK20audio_offload_info_t, false)
339346
DLSYM_OR_RETURN(libaudioclient, ZN7android11AudioSystem10getLatencyEiPj, false)
@@ -688,6 +695,57 @@ Java_org_akanework_gramophone_logic_utils_NativeTrack_dtor(
688695
delete holder;
689696
}
690697

698+
extern "C" JNIEXPORT jboolean JNICALL
699+
Java_org_akanework_gramophone_logic_utils_NativeTrack_00024Companion_isOffloadSupported(
700+
JNIEnv*, jobject, jint sampleRate, jint format,
701+
jint channelMask) {
702+
if (android_get_device_api_level() > 23 && android_get_device_api_level() != 28) {
703+
ALOGE("isOffloadSupported() should only be used on L, M or P");
704+
return false;
705+
}
706+
union {
707+
audio_offload_info_t newInfo = {};
708+
audio_offload_info_t_legacy oldInfo;
709+
} offloadInfo;
710+
if (android_get_device_api_level() >= 28) {
711+
offloadInfo.newInfo = {
712+
.version = AUDIO_MAKE_OFFLOAD_INFO_VERSION(0, 2),
713+
.size = sizeof(audio_offload_info_t),
714+
.sample_rate = (uint32_t)sampleRate,
715+
.channel_mask = (uint32_t)channelMask,
716+
.format = (uint32_t)format,
717+
.stream_type = LEGACY_AUDIO_STREAM_MUSIC, // must be MUSIC
718+
.bit_rate = (uint32_t)0,
719+
.duration_us = 2100 /* 3.5min * 60 */ * 1000 * 1000, // must be >60s
720+
.has_video = (bool)false,
721+
.is_streaming = (bool)false,
722+
.bit_width = (uint32_t)0,
723+
.offload_buffer_size = (uint32_t)0,
724+
.usage = 0,
725+
.encapsulation_mode = 0,
726+
.content_id = 0,
727+
.sync_id = 0
728+
};
729+
} else {
730+
offloadInfo.oldInfo = {
731+
.version = AUDIO_MAKE_OFFLOAD_INFO_VERSION(0, 1),
732+
.size = sizeof(audio_offload_info_t_legacy),
733+
.sample_rate = (uint32_t)sampleRate,
734+
.channel_mask = (uint32_t)channelMask,
735+
.format = (uint32_t)format,
736+
.stream_type = LEGACY_AUDIO_STREAM_MUSIC, // must be MUSIC
737+
.bit_rate = (uint32_t)0,
738+
.duration_us = 2100 /* 3.5min * 60 */ * 1000 * 1000, // must be >60s
739+
.has_video = (bool)false,
740+
.is_streaming = (bool)false,
741+
.bit_width = (uint32_t)0,
742+
.offload_buffer_size = (uint32_t)0,
743+
.usage = 0,
744+
};
745+
}
746+
return ZN7android11AudioSystem18isOffloadSupportedERK20audio_offload_info_t(offloadInfo.oldInfo);
747+
}
748+
691749
// TODO
692750
// void setAudioTrackCallback(const sp<media::IAudioTrackCallback>& callback) {
693751
// mAudioTrackCallback->setAudioTrackCallback(callback);

app/src/main/cpp/aosp_stubs.h

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616

1717
// last observed AOSP size (arm64) was 1312
18+
#include "audio-legacy.h"
19+
1820
#define AUDIO_TRACK_SIZE 5000
1921
// last observed AOSP size (arm64) was 152
2022
#define ATTRIBUTION_SOURCE_SIZE 500

app/src/main/kotlin/org/akanework/gramophone/logic/GramophonePlaybackService.kt

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import android.content.IntentFilter
2828
import android.content.SharedPreferences
2929
import android.graphics.Bitmap
3030
import android.media.AudioDeviceInfo
31+
import android.media.AudioFormat
3132
import android.media.AudioManager
3233
import android.media.audiofx.AudioEffect
3334
import android.net.Uri
@@ -38,6 +39,7 @@ import android.os.HandlerThread
3839
import android.os.Looper
3940
import android.os.Process
4041
import android.util.Log
42+
import android.widget.Toast
4143
import androidx.concurrent.futures.CallbackToFutureAdapter
4244
import androidx.core.app.NotificationChannelCompat
4345
import androidx.core.app.NotificationCompat
@@ -566,6 +568,8 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
566568
} else {
567569
Log.e(TAG, "session id is 0? why????? THIS MIGHT BREAK EQUALIZER")
568570
}
571+
val s = NativeTrack.getDirectPlaybackSupport(this, 192000, AudioFormat.ENCODING_PCM_16BIT, AudioFormat.CHANNEL_OUT_STEREO)
572+
Toast.makeText(this, "direct: ${s.directOrOffload}", Toast.LENGTH_LONG).show()
569573
val track = NativeTrack(this)
570574
track.set()
571575
track.release()

app/src/main/kotlin/org/akanework/gramophone/logic/utils/NativeTrack.kt

+134-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,125 @@
11
package org.akanework.gramophone.logic.utils
22

33
import android.content.Context
4+
import android.media.AudioAttributes
5+
import android.media.AudioFormat
6+
import android.media.AudioManager
7+
import android.media.AudioTrack
48
import android.os.Build
59
import android.os.Parcel
610
import android.util.Log
11+
import androidx.core.content.getSystemService
712
import java.nio.ByteBuffer
813

914
class NativeTrack(context: Context) {
1015
companion object {
1116
private const val TAG = "NativeTrack.kt"
17+
18+
data class DirectPlaybackSupport(val normalOffload: Boolean, val gaplessOffload: Boolean,
19+
val directBitstream: Boolean) {
20+
companion object {
21+
val NONE = DirectPlaybackSupport(false, false, false)
22+
val OFFLOAD = DirectPlaybackSupport(true, false, false)
23+
val GAPLESS_OFFLOAD = DirectPlaybackSupport(false, true, false)
24+
val DIRECT = DirectPlaybackSupport(false, false, true)
25+
}
26+
val offload
27+
get() = normalOffload || gaplessOffload
28+
val directOrOffload
29+
get() = directBitstream || offload
30+
}
31+
32+
fun getDirectPlaybackSupport(context: Context, sampleRate: Int, encoding: Int, channelMask: Int): DirectPlaybackSupport {
33+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
34+
val format = AudioFormat.Builder()
35+
.setSampleRate(sampleRate) // TODO support checking for 384khz
36+
.setEncoding(encoding) // TODO support int24/int32 below S
37+
.setChannelMask(channelMask)
38+
.build()
39+
val attributes = AudioAttributes.Builder()
40+
.setUsage(AudioAttributes.USAGE_MEDIA)
41+
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
42+
.build()
43+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
44+
val d = AudioManager.getDirectPlaybackSupport(format, attributes)
45+
return if (d == AudioManager.DIRECT_PLAYBACK_NOT_SUPPORTED) DirectPlaybackSupport.NONE
46+
else DirectPlaybackSupport(
47+
(d and AudioManager.DIRECT_PLAYBACK_OFFLOAD_SUPPORTED) != 0,
48+
(d and AudioManager.DIRECT_PLAYBACK_OFFLOAD_GAPLESS_SUPPORTED) != 0,
49+
(d and AudioManager.DIRECT_PLAYBACK_BITSTREAM_SUPPORTED) != 0
50+
)
51+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
52+
return when ((@Suppress("deprecation") AudioManager.getPlaybackOffloadSupport(
53+
format,
54+
attributes
55+
))) {
56+
AudioManager.PLAYBACK_OFFLOAD_GAPLESS_SUPPORTED -> DirectPlaybackSupport.GAPLESS_OFFLOAD
57+
AudioManager.PLAYBACK_OFFLOAD_SUPPORTED -> DirectPlaybackSupport.OFFLOAD
58+
else -> if (@Suppress("deprecation") AudioTrack.isDirectPlaybackSupported(
59+
format,
60+
attributes
61+
)
62+
)
63+
DirectPlaybackSupport.DIRECT else DirectPlaybackSupport.NONE
64+
}
65+
} else {
66+
// can't distinguish offload/direct
67+
return if (@Suppress("deprecation") AudioTrack.isDirectPlaybackSupported(format, attributes))
68+
DirectPlaybackSupport.DIRECT else DirectPlaybackSupport.NONE
69+
}
70+
} else {
71+
val encoding = encodingToNative(encoding)
72+
val channelMask = channelMaskToNative(channelMask)
73+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
74+
&& Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1
75+
) {
76+
if (!initDlsym())
77+
throw IllegalStateException("initDlsym() failed")
78+
val bitWidth = when (encoding) {
79+
1, 0x0D000000 -> 16
80+
2 -> 8
81+
3, 4, 5 -> 32
82+
6 -> 24
83+
else -> 0
84+
}
85+
val bitrate = if (bitWidth != 0) {
86+
bitWidth * Integer.bitCount(channelMask) * sampleRate
87+
} else 128 // arbitrary guess for compressed formats
88+
val sessionId = context.getSystemService<AudioManager>()!!.generateAudioSessionId()
89+
return TODO()
90+
} else { // L / M / P
91+
if (!initDlsym())
92+
throw IllegalStateException("initDlsym() failed")
93+
return if (try {
94+
isOffloadSupported(sampleRate, encoding, channelMask)
95+
} catch (t: Throwable) {
96+
Log.e(TAG, Log.getStackTraceString(t)); false
97+
}
98+
)
99+
DirectPlaybackSupport.OFFLOAD
100+
else DirectPlaybackSupport.NONE
101+
}
102+
}
103+
}
104+
private fun encodingToNative(encoding: Int): Int {
105+
return when (encoding) {
106+
AudioFormat.ENCODING_PCM_8BIT -> 2
107+
AudioFormat.ENCODING_PCM_16BIT -> 1
108+
AudioFormat.ENCODING_PCM_24BIT_PACKED -> 6
109+
AudioFormat.ENCODING_PCM_32BIT -> 3
110+
AudioFormat.ENCODING_PCM_FLOAT -> 5
111+
else -> TODO()
112+
}
113+
}
114+
private fun channelMaskToNative(channelMask: Int): Int {
115+
return when (channelMask) {
116+
AudioFormat.CHANNEL_OUT_MONO -> 1
117+
AudioFormat.CHANNEL_OUT_STEREO -> 3
118+
else -> TODO()
119+
}
120+
}
121+
private external fun isOffloadSupported(sampleRate: Int, format: Int, channelMask: Int): Boolean
122+
private external fun initDlsym(): Boolean
12123
}
13124
val ptr: Long
14125
var myState = State.NOT_SET
@@ -43,8 +154,30 @@ class NativeTrack(context: Context) {
43154
throw NativeTrackException("create() returned NULL")
44155
}
45156
}
46-
private external fun initDlsym(): Boolean
47157
private external fun create(@Suppress("unused") parcel: Parcel?): Long
158+
/*
159+
* If used with direct playback before Android 9, it is required to first check format support using
160+
* getDirectPlaybackSupport to avoid invoking bugs in the platform for unsupported formats (ie: requested
161+
* float32 but got int24 instead, createTrack and hence set fails as consequence). Below description assumes
162+
* this when considering possible scenarios.
163+
*
164+
* CAUTION: Until including Android 7.1, direct outputs could be reused even with different session IDs.
165+
* If another app is using a direct (or offload) stream, we might end up with no audio (there can
166+
* only ever be one client). However, this problem is isolated to MediaPlayer using compressed offload
167+
* (or another app like us doing that), and us using hidden API to offload in the same format, sample
168+
* rate and channel mask. Audio focus sadly isn't enough as the track needs to be released to avoid
169+
* this bug, so either avoid other media player apps or using compressed offload on these versions.
170+
*
171+
* CAUTION: From Android 7.0 until Android 8.1, direct outputs with PCM modes int24, int32 or float32 were all
172+
* treated as compatible. To avoid bugs, we should always release our handle to the direct output
173+
* before attempting to switch formats. But if we're unlucky, on Android 7.x only, we may get a busy
174+
* output with a different format anyway because another app has an active direct PCM track - which
175+
* will result in set() failing. In theory, there's another case where set() could fail: on N/O, when
176+
* we request float32 but APM selects int32 instead (but for that, the device would need need to
177+
* support both formats AND have non-deterministic selection, which doesn't seem to happen in AOSP).
178+
* This is because it tries to choose the best profile by bit depth but ignores that int32 and float32
179+
* are different formats.
180+
*/
48181
@Suppress("unused") // for parameters, this method has a few of them
49182
private external fun doSet(ptr: Long, streamType: Int, sampleRate: Int, format: Int, channelMask: Int,
50183
frameCount: Int, trackFlags: Int, sessionId: Int, maxRequiredSpeed: Float,

app/src/main/kotlin/org/akanework/gramophone/ui/adapters/SongAdapter.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ class SongAdapter(
301301
}
302302

303303
override fun getFile(item: MediaItem): File {
304-
return item.getFile()
304+
return item.getFile()!!
305305
}
306306

307307
override fun getTitle(item: MediaItem): String {

0 commit comments

Comments
 (0)