1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.pandora
18 
19 import android.bluetooth.BluetoothA2dp
20 import android.bluetooth.BluetoothAdapter
21 import android.bluetooth.BluetoothCodecConfig
22 import android.bluetooth.BluetoothCodecStatus
23 import android.bluetooth.BluetoothCodecType
24 import android.bluetooth.BluetoothManager
25 import android.bluetooth.BluetoothProfile
26 import android.content.Context
27 import android.content.Intent
28 import android.content.IntentFilter
29 import android.media.*
30 import android.util.Log
31 import com.google.protobuf.BoolValue
32 import com.google.protobuf.ByteString
33 import com.google.protobuf.Empty
34 import io.grpc.Status
35 import io.grpc.stub.StreamObserver
36 import java.io.Closeable
37 import java.io.PrintWriter
38 import java.io.StringWriter
39 import kotlin.time.Duration
40 import kotlin.time.Duration.Companion.milliseconds
41 import kotlinx.coroutines.CoroutineScope
42 import kotlinx.coroutines.Dispatchers
43 import kotlinx.coroutines.cancel
44 import kotlinx.coroutines.delay
45 import kotlinx.coroutines.flow.Flow
46 import kotlinx.coroutines.flow.SharingStarted
47 import kotlinx.coroutines.flow.filter
48 import kotlinx.coroutines.flow.first
49 import kotlinx.coroutines.flow.map
50 import kotlinx.coroutines.flow.shareIn
51 import kotlinx.coroutines.withTimeoutOrNull
52 import pandora.A2DPGrpc.A2DPImplBase
53 import pandora.A2DPProto.*
54 
55 @kotlinx.coroutines.ExperimentalCoroutinesApi
56 class A2dp(val context: Context) : A2DPImplBase(), Closeable {
57     private val TAG = "PandoraA2dp"
58 
59     private val scope: CoroutineScope
60     private val flow: Flow<Intent>
61 
62     private val audioManager = context.getSystemService(AudioManager::class.java)!!
63 
64     private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
65     private val bluetoothAdapter = bluetoothManager.adapter
66     private val bluetoothA2dp = getProfileProxy<BluetoothA2dp>(context, BluetoothProfile.A2DP)
67 
68     private var audioTrack: AudioTrack? = null
69 
70     init {
71         scope = CoroutineScope(Dispatchers.Default.limitedParallelism(1))
72         val intentFilter = IntentFilter()
73         intentFilter.addAction(BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED)
74         intentFilter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)
75         intentFilter.addAction(BluetoothA2dp.ACTION_CODEC_CONFIG_CHANGED)
76 
77         flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly)
78     }
79 
closenull80     override fun close() {
81         bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, bluetoothA2dp)
82         scope.cancel()
83     }
84 
openSourcenull85     override fun openSource(
86         request: OpenSourceRequest,
87         responseObserver: StreamObserver<OpenSourceResponse>,
88     ) {
89         grpcUnary<OpenSourceResponse>(scope, responseObserver) {
90             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
91             Log.i(TAG, "openSource: device=$device")
92 
93             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
94                 bluetoothA2dp.connect(device)
95                 val state =
96                     flow
97                         .filter { it.getAction() == BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED }
98                         .filter { it.getBluetoothDeviceExtra() == device }
99                         .map {
100                             it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR)
101                         }
102                         .filter {
103                             it == BluetoothProfile.STATE_CONNECTED ||
104                                 it == BluetoothProfile.STATE_DISCONNECTED
105                         }
106                         .first()
107 
108                 if (state == BluetoothProfile.STATE_DISCONNECTED) {
109                     throw RuntimeException("openSource failed, A2DP has been disconnected")
110                 }
111             }
112 
113             // TODO: b/234891800, AVDTP start request sometimes never sent if playback starts too
114             // early.
115             delay(2000L)
116 
117             val source =
118                 Source.newBuilder().setCookie(ByteString.copyFrom(device.getAddress(), "UTF-8"))
119             OpenSourceResponse.newBuilder().setSource(source).build()
120         }
121     }
122 
waitSourcenull123     override fun waitSource(
124         request: WaitSourceRequest,
125         responseObserver: StreamObserver<WaitSourceResponse>,
126     ) {
127         grpcUnary<WaitSourceResponse>(scope, responseObserver) {
128             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
129             Log.i(TAG, "waitSource: device=$device")
130 
131             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
132                 val state =
133                     flow
134                         .filter { it.getAction() == BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED }
135                         .filter { it.getBluetoothDeviceExtra() == device }
136                         .map {
137                             it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR)
138                         }
139                         .filter {
140                             it == BluetoothProfile.STATE_CONNECTED ||
141                                 it == BluetoothProfile.STATE_DISCONNECTED
142                         }
143                         .first()
144 
145                 if (state == BluetoothProfile.STATE_DISCONNECTED) {
146                     throw RuntimeException("waitSource failed, A2DP has been disconnected")
147                 }
148             }
149 
150             // TODO: b/234891800, AVDTP start request sometimes never sent if playback starts too
151             // early.
152             delay(2000L)
153 
154             val source =
155                 Source.newBuilder().setCookie(ByteString.copyFrom(device.getAddress(), "UTF-8"))
156             WaitSourceResponse.newBuilder().setSource(source).build()
157         }
158     }
159 
startnull160     override fun start(request: StartRequest, responseObserver: StreamObserver<StartResponse>) {
161         grpcUnary<StartResponse>(scope, responseObserver) {
162             if (audioTrack == null) {
163                 audioTrack = buildAudioTrack()
164             }
165             val device = bluetoothAdapter.getRemoteDevice(request.source.cookie.toString("UTF-8"))
166             Log.i(TAG, "start: device=$device")
167 
168             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
169                 throw RuntimeException("Device is not connected, cannot start")
170             }
171 
172             // Configure the selected device as active device if it is not
173             // already.
174             bluetoothA2dp.setActiveDevice(device)
175 
176             // Play an audio track.
177             audioTrack!!.play()
178 
179             // If A2dp is not already playing, wait for it
180             if (!bluetoothA2dp.isA2dpPlaying(device)) {
181                 flow
182                     .filter { it.getAction() == BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED }
183                     .filter { it.getBluetoothDeviceExtra() == device }
184                     .map { it.getIntExtra(BluetoothA2dp.EXTRA_STATE, BluetoothAdapter.ERROR) }
185                     .filter { it == BluetoothA2dp.STATE_PLAYING }
186                     .first()
187             }
188             StartResponse.getDefaultInstance()
189         }
190     }
191 
suspendnull192     override fun suspend(
193         request: SuspendRequest,
194         responseObserver: StreamObserver<SuspendResponse>,
195     ) {
196         grpcUnary<SuspendResponse>(scope, responseObserver) {
197             val device = bluetoothAdapter.getRemoteDevice(request.source.cookie.toString("UTF-8"))
198             val timeoutMillis: Duration = 5000.milliseconds
199 
200             Log.i(TAG, "suspend: device=$device")
201 
202             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
203                 throw RuntimeException("Device is not connected, cannot suspend")
204             }
205 
206             if (!bluetoothA2dp.isA2dpPlaying(device)) {
207                 throw RuntimeException("Device is already suspended, cannot suspend")
208             }
209 
210             val a2dpPlayingStateFlow =
211                 flow
212                     .filter { it.getAction() == BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED }
213                     .filter { it.getBluetoothDeviceExtra() == device }
214                     .map { it.getIntExtra(BluetoothA2dp.EXTRA_STATE, BluetoothAdapter.ERROR) }
215 
216             audioTrack!!.pause()
217             withTimeoutOrNull(timeoutMillis) {
218                 a2dpPlayingStateFlow.filter { it == BluetoothA2dp.STATE_NOT_PLAYING }.first()
219             }
220             SuspendResponse.getDefaultInstance()
221         }
222     }
223 
isSuspendednull224     override fun isSuspended(
225         request: IsSuspendedRequest,
226         responseObserver: StreamObserver<BoolValue>,
227     ) {
228         grpcUnary(scope, responseObserver) {
229             val device = bluetoothAdapter.getRemoteDevice(request.source.cookie.toString("UTF-8"))
230             Log.i(TAG, "isSuspended: device=$device")
231 
232             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
233                 throw RuntimeException("Device is not connected, cannot get suspend state")
234             }
235 
236             val isSuspended = bluetoothA2dp.isA2dpPlaying(device)
237 
238             BoolValue.newBuilder().setValue(isSuspended).build()
239         }
240     }
241 
closenull242     override fun close(request: CloseRequest, responseObserver: StreamObserver<CloseResponse>) {
243         grpcUnary<CloseResponse>(scope, responseObserver) {
244             val device = bluetoothAdapter.getRemoteDevice(request.source.cookie.toString("UTF-8"))
245             Log.i(TAG, "close: device=$device")
246 
247             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
248                 throw RuntimeException("Device is not connected, cannot close")
249             }
250 
251             val a2dpConnectionStateChangedFlow =
252                 flow
253                     .filter { it.getAction() == BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED }
254                     .filter { it.getBluetoothDeviceExtra() == device }
255                     .map { it.getIntExtra(BluetoothA2dp.EXTRA_STATE, BluetoothAdapter.ERROR) }
256 
257             bluetoothA2dp.disconnect(device)
258             a2dpConnectionStateChangedFlow.filter { it == BluetoothA2dp.STATE_DISCONNECTED }.first()
259 
260             CloseResponse.getDefaultInstance()
261         }
262     }
263 
playbackAudionull264     override fun playbackAudio(
265         responseObserver: StreamObserver<PlaybackAudioResponse>
266     ): StreamObserver<PlaybackAudioRequest> {
267         Log.i(TAG, "playbackAudio")
268 
269         if (audioTrack!!.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
270             responseObserver.onError(
271                 Status.UNKNOWN.withDescription("AudioTrack is not started").asException()
272             )
273         }
274 
275         // Volume is maxed out to avoid any amplitude modification of the provided audio data,
276         // enabling the test runner to do comparisons between input and output audio signal.
277         // Any volume modification should be done before providing the audio data.
278         if (audioManager.isVolumeFixed) {
279             Log.w(TAG, "Volume is fixed, cannot max out the volume")
280         } else {
281             val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
282             if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) < maxVolume) {
283                 audioManager.setStreamVolume(
284                     AudioManager.STREAM_MUSIC,
285                     maxVolume,
286                     AudioManager.FLAG_SHOW_UI,
287                 )
288             }
289         }
290 
291         return object : StreamObserver<PlaybackAudioRequest> {
292             override fun onNext(request: PlaybackAudioRequest) {
293                 val data = request.data.toByteArray()
294                 val written = synchronized(audioTrack!!) { audioTrack!!.write(data, 0, data.size) }
295                 if (written != data.size) {
296                     responseObserver.onError(
297                         Status.UNKNOWN.withDescription("AudioTrack write failed").asException()
298                     )
299                 }
300             }
301 
302             override fun onError(t: Throwable) {
303                 t.printStackTrace()
304                 val sw = StringWriter()
305                 t.printStackTrace(PrintWriter(sw))
306                 responseObserver.onError(
307                     Status.UNKNOWN.withCause(t).withDescription(sw.toString()).asException()
308                 )
309             }
310 
311             override fun onCompleted() {
312                 responseObserver.onNext(PlaybackAudioResponse.getDefaultInstance())
313                 responseObserver.onCompleted()
314             }
315         }
316     }
317 
getAudioEncodingnull318     override fun getAudioEncoding(
319         request: GetAudioEncodingRequest,
320         responseObserver: StreamObserver<GetAudioEncodingResponse>,
321     ) {
322         grpcUnary<GetAudioEncodingResponse>(scope, responseObserver) {
323             val device = bluetoothAdapter.getRemoteDevice(request.source.cookie.toString("UTF-8"))
324             Log.i(TAG, "getAudioEncoding: device=$device")
325 
326             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
327                 throw RuntimeException("Device is not connected, cannot getAudioEncoding")
328             }
329 
330             // For now, we only support 44100 kHz sampling rate.
331             GetAudioEncodingResponse.newBuilder()
332                 .setEncoding(AudioEncoding.PCM_S16_LE_44K1_STEREO)
333                 .build()
334         }
335     }
336 
getConfigurationnull337     override fun getConfiguration(
338         request: GetConfigurationRequest,
339         responseObserver: StreamObserver<GetConfigurationResponse>,
340     ) {
341         grpcUnary<GetConfigurationResponse>(scope, responseObserver) {
342             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
343             Log.i(TAG, "getConfiguration: device=$device")
344 
345             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
346                 throw RuntimeException("Device is not connected, cannot getConfiguration")
347             }
348 
349             val codecStatus = bluetoothA2dp.getCodecStatus(device)
350             if (codecStatus == null) {
351                 throw RuntimeException("Codec status is null")
352             }
353 
354             val currentCodecConfig = codecStatus.getCodecConfig()
355             if (currentCodecConfig == null) {
356                 throw RuntimeException("Codec configuration is null")
357             }
358 
359             val supportedCodecTypes = bluetoothA2dp.getSupportedCodecTypes()
360             val configuration =
361                 Configuration.newBuilder()
362                     .setId(getProtoCodecId(currentCodecConfig, supportedCodecTypes))
363                     .setParameters(getProtoCodecParameters(currentCodecConfig))
364                     .build()
365             GetConfigurationResponse.newBuilder().setConfiguration(configuration).build()
366         }
367     }
368 
setConfigurationnull369     override fun setConfiguration(
370         request: SetConfigurationRequest,
371         responseObserver: StreamObserver<SetConfigurationResponse>,
372     ) {
373         grpcUnary<SetConfigurationResponse>(scope, responseObserver) {
374             val timeoutMillis: Duration = 5000.milliseconds
375             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
376             Log.i(TAG, "setConfiguration: device=$device")
377 
378             if (bluetoothA2dp.getConnectionState(device) != BluetoothA2dp.STATE_CONNECTED) {
379                 throw RuntimeException("Device is not connected, cannot getCodecStatus")
380             }
381 
382             val newCodecConfig = getCodecConfigFromProtoConfiguration(request.configuration)
383             if (newCodecConfig == null) {
384                 throw RuntimeException("New codec configuration is null")
385             }
386 
387             val codecId = packCodecId(request.configuration.id)
388 
389             val a2dpCodecConfigChangedFlow =
390                 flow
391                     .filter { it.getAction() == BluetoothA2dp.ACTION_CODEC_CONFIG_CHANGED }
392                     .filter { it.getBluetoothDeviceExtra() == device }
393                     .map {
394                         it.getParcelableExtra(
395                                 BluetoothCodecStatus.EXTRA_CODEC_STATUS,
396                                 BluetoothCodecStatus::class.java,
397                             )
398                             ?.getCodecConfig()
399                     }
400 
401             bluetoothA2dp.setCodecConfigPreference(device, newCodecConfig)
402 
403             val result =
404                 withTimeoutOrNull(timeoutMillis) {
405                     a2dpCodecConfigChangedFlow
406                         .filter { it?.getExtendedCodecType()?.getCodecId() == codecId }
407                         .first()
408                 }
409             Log.i(TAG, "Result=$result")
410             SetConfigurationResponse.newBuilder().setSuccess(result != null).build()
411         }
412     }
413 
unpackCodecIdnull414     private fun unpackCodecId(codecId: Long): CodecId {
415         val codecType = (codecId and 0xFF).toInt()
416         val vendorId = ((codecId shr 8) and 0xFFFF).toInt()
417         val vendorCodecId = ((codecId shr 24) and 0xFFFF).toInt()
418         val codecIdBuilder = CodecId.newBuilder()
419         when (codecType) {
420             0x00 -> {
421                 codecIdBuilder.setSbc(Empty.getDefaultInstance())
422             }
423             0x02 -> {
424                 codecIdBuilder.setMpegAac(Empty.getDefaultInstance())
425             }
426             0xFF -> {
427                 val vendor = Vendor.newBuilder().setId(vendorId).setCodecId(vendorCodecId).build()
428                 codecIdBuilder.setVendor(vendor)
429             }
430             else -> {
431                 throw RuntimeException("Unknown codec type")
432             }
433         }
434         return codecIdBuilder.build()
435     }
436 
packCodecIdnull437     private fun packCodecId(codecId: CodecId): Long {
438         var codecType: Int
439         var vendorId: Int = 0
440         var vendorCodecId: Int = 0
441         when {
442             codecId.hasSbc() -> {
443                 codecType = 0x00
444             }
445             codecId.hasMpegAac() -> {
446                 codecType = 0x02
447             }
448             codecId.hasVendor() -> {
449                 codecType = 0xFF
450                 vendorId = codecId.vendor.id
451                 vendorCodecId = codecId.vendor.codecId
452             }
453             else -> {
454                 throw RuntimeException("Unknown codec type")
455             }
456         }
457         return (codecType.toLong() and 0xFF) or
458             ((vendorId.toLong() and 0xFFFF) shl 8) or
459             ((vendorCodecId.toLong() and 0xFFFF) shl 24)
460     }
461 
getProtoCodecIdnull462     private fun getProtoCodecId(
463         codecConfig: BluetoothCodecConfig,
464         supportedCodecTypes: Collection<BluetoothCodecType>,
465     ): CodecId {
466         var selectedCodecType: BluetoothCodecType? = null
467         for (codecType: BluetoothCodecType in supportedCodecTypes) {
468             if (codecType.getCodecId() == codecConfig.getExtendedCodecType()?.getCodecId()) {
469                 selectedCodecType = codecType
470             }
471         }
472         if (selectedCodecType == null) {
473             Log.e(TAG, "getProtoCodecId: selectedCodecType is null")
474             return CodecId.newBuilder().build()
475         }
476         return unpackCodecId(selectedCodecType.getCodecId())
477     }
478 
getProtoCodecParametersnull479     private fun getProtoCodecParameters(codecConfig: BluetoothCodecConfig): CodecParameters {
480         var channelMode: ChannelMode
481         var samplingFrequencyHz: Int
482         var bitDepth: Int
483         when (codecConfig.getSampleRate()) {
484             BluetoothCodecConfig.SAMPLE_RATE_NONE -> {
485                 samplingFrequencyHz = 0
486             }
487             BluetoothCodecConfig.SAMPLE_RATE_44100 -> {
488                 samplingFrequencyHz = 44100
489             }
490             BluetoothCodecConfig.SAMPLE_RATE_48000 -> {
491                 samplingFrequencyHz = 48000
492             }
493             BluetoothCodecConfig.SAMPLE_RATE_88200 -> {
494                 samplingFrequencyHz = 88200
495             }
496             BluetoothCodecConfig.SAMPLE_RATE_96000 -> {
497                 samplingFrequencyHz = 96000
498             }
499             BluetoothCodecConfig.SAMPLE_RATE_176400 -> {
500                 samplingFrequencyHz = 176400
501             }
502             BluetoothCodecConfig.SAMPLE_RATE_192000 -> {
503                 samplingFrequencyHz = 192000
504             }
505             else -> {
506                 throw RuntimeException("Unknown sample rate")
507             }
508         }
509         when (codecConfig.getBitsPerSample()) {
510             BluetoothCodecConfig.BITS_PER_SAMPLE_NONE -> {
511                 bitDepth = 0
512             }
513             BluetoothCodecConfig.BITS_PER_SAMPLE_16 -> {
514                 bitDepth = 16
515             }
516             BluetoothCodecConfig.BITS_PER_SAMPLE_24 -> {
517                 bitDepth = 24
518             }
519             BluetoothCodecConfig.BITS_PER_SAMPLE_32 -> {
520                 bitDepth = 32
521             }
522             else -> {
523                 throw RuntimeException("Unknown bit depth")
524             }
525         }
526         when (codecConfig.getChannelMode()) {
527             BluetoothCodecConfig.CHANNEL_MODE_NONE -> {
528                 channelMode = ChannelMode.UNKNOWN
529             }
530             BluetoothCodecConfig.CHANNEL_MODE_MONO -> {
531                 channelMode = ChannelMode.MONO
532             }
533             BluetoothCodecConfig.CHANNEL_MODE_STEREO -> {
534                 channelMode = ChannelMode.STEREO
535             }
536             else -> {
537                 throw RuntimeException("Unknown channel mode")
538             }
539         }
540         return CodecParameters.newBuilder()
541             .setSamplingFrequencyHz(samplingFrequencyHz)
542             .setBitDepth(bitDepth)
543             .setChannelMode(channelMode)
544             .build()
545     }
546 
getCodecConfigFromProtoConfigurationnull547     private fun getCodecConfigFromProtoConfiguration(
548         configuration: Configuration
549     ): BluetoothCodecConfig? {
550         var selectedCodecType: BluetoothCodecType? = null
551         val codecTypes = bluetoothA2dp.getSupportedCodecTypes()
552         val codecId = packCodecId(configuration.id)
553         var sampleRate: Int
554         var bitsPerSample: Int
555         var channelMode: Int
556         for (codecType: BluetoothCodecType in codecTypes) {
557             if (codecType.getCodecId() == codecId) {
558                 selectedCodecType = codecType
559             }
560         }
561         if (selectedCodecType == null) {
562             Log.e(TAG, "getCodecConfigFromProtoConfiguration: selectedCodecType is null")
563             return null
564         }
565         when (configuration.parameters.getSamplingFrequencyHz()) {
566             0 -> {
567                 sampleRate = BluetoothCodecConfig.SAMPLE_RATE_NONE
568             }
569             44100 -> {
570                 sampleRate = BluetoothCodecConfig.SAMPLE_RATE_44100
571             }
572             48000 -> {
573                 sampleRate = BluetoothCodecConfig.SAMPLE_RATE_48000
574             }
575             88200 -> {
576                 sampleRate = BluetoothCodecConfig.SAMPLE_RATE_88200
577             }
578             96000 -> {
579                 sampleRate = BluetoothCodecConfig.SAMPLE_RATE_96000
580             }
581             176400 -> {
582                 sampleRate = BluetoothCodecConfig.SAMPLE_RATE_176400
583             }
584             192000 -> {
585                 sampleRate = BluetoothCodecConfig.SAMPLE_RATE_192000
586             }
587             else -> {
588                 throw RuntimeException("Unknown sample rate")
589             }
590         }
591         when (configuration.parameters.getBitDepth()) {
592             0 -> {
593                 bitsPerSample = BluetoothCodecConfig.BITS_PER_SAMPLE_NONE
594             }
595             16 -> {
596                 bitsPerSample = BluetoothCodecConfig.BITS_PER_SAMPLE_16
597             }
598             24 -> {
599                 bitsPerSample = BluetoothCodecConfig.BITS_PER_SAMPLE_24
600             }
601             32 -> {
602                 bitsPerSample = BluetoothCodecConfig.BITS_PER_SAMPLE_32
603             }
604             else -> {
605                 throw RuntimeException("Unknown bit depth")
606             }
607         }
608         when (configuration.parameters.getChannelMode()) {
609             ChannelMode.UNKNOWN -> {
610                 channelMode = BluetoothCodecConfig.CHANNEL_MODE_NONE
611             }
612             ChannelMode.MONO -> {
613                 channelMode = BluetoothCodecConfig.CHANNEL_MODE_MONO
614             }
615             ChannelMode.STEREO -> {
616                 channelMode = BluetoothCodecConfig.CHANNEL_MODE_STEREO
617             }
618             else -> {
619                 throw RuntimeException("Unknown channel mode")
620             }
621         }
622         return BluetoothCodecConfig.Builder()
623             .setExtendedCodecType(selectedCodecType)
624             .setCodecPriority(BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST)
625             .setSampleRate(sampleRate)
626             .setBitsPerSample(bitsPerSample)
627             .setChannelMode(channelMode)
628             .build()
629     }
630 }
631