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