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