|
1 | 1 | package org.akanework.gramophone.logic.utils
|
2 | 2 |
|
3 | 3 | import android.content.Context
|
| 4 | +import android.media.AudioAttributes |
| 5 | +import android.media.AudioFormat |
| 6 | +import android.media.AudioManager |
| 7 | +import android.media.AudioTrack |
4 | 8 | import android.os.Build
|
5 | 9 | import android.os.Parcel
|
6 | 10 | import android.util.Log
|
| 11 | +import androidx.core.content.getSystemService |
7 | 12 | import java.nio.ByteBuffer
|
8 | 13 |
|
9 | 14 | class NativeTrack(context: Context) {
|
10 | 15 | companion object {
|
11 | 16 | 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 |
12 | 123 | }
|
13 | 124 | val ptr: Long
|
14 | 125 | var myState = State.NOT_SET
|
@@ -43,8 +154,30 @@ class NativeTrack(context: Context) {
|
43 | 154 | throw NativeTrackException("create() returned NULL")
|
44 | 155 | }
|
45 | 156 | }
|
46 |
| - private external fun initDlsym(): Boolean |
47 | 157 | 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 | + */ |
48 | 181 | @Suppress("unused") // for parameters, this method has a few of them
|
49 | 182 | private external fun doSet(ptr: Long, streamType: Int, sampleRate: Int, format: Int, channelMask: Int,
|
50 | 183 | frameCount: Int, trackFlags: Int, sessionId: Int, maxRequiredSpeed: Float,
|
|
0 commit comments