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