Я хочу использовать гарнитуру Bluetooth (не BLE) для записи аудио, в то же время потоковая музыка из другого приложения (например, YouTube, Spotify). Я знаю, что качество музыки будет низким, но это не проблема. Этот вариант использования работал нормально до Android 15, но с тех пор, кажется, невозможно продолжать играть музыку (на динамиках гарнитуры) во время записи. Есть ли обходной путь?package com.example.android15audio_test
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.*
import android.media.*
import android.os.*
import android.view.KeyEvent
import android.widget.Button
import android.widget.ScrollView
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresPermission
import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout
import java.io.*
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.text.SimpleDateFormat
import java.util.*
import kotlin.system.exitProcess
class MainActivity : ComponentActivity() {
private lateinit var tvLog: TextView
private lateinit var tvStatus: TextView
private lateinit var scroll: ScrollView
private var isRecording = false
private var recordThread: Thread? = null
private lateinit var audioManager: AudioManager
private val logTag = "BTMic"
private val reqPermissions = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { grant ->
val allGranted = grant.values.all { it }
if (allGranted) startRecordingFlow()
else logUi("Permission denied. No mic, no fun.")
}
private fun logUi(msg: String) {
android.util.Log.d(logTag, msg)
runOnUiThread {
tvLog.append("• $msg\n")
scroll.post {
scroll.fullScroll(ScrollView.FOCUS_DOWN)
}
}
}
private fun setStatus(s: String) {
runOnUiThread { tvStatus.text = "Status: $s" }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tvLog = findViewById(R.id.tvLog)
tvStatus = findViewById(R.id.tvStatus)
scroll = findViewById(R.id.scrollView)
findViewById(R.id.btnStart).setOnClickListener { checkPermsAndStart() }
findViewById(R.id.btnStop).setOnClickListener { stopRecordingFlow() }
// Ensure ScrollView starts at bottom after first layout
scroll.doOnLayout { scroll.fullScroll(ScrollView.FOCUS_DOWN) }
audioManager = this.getSystemService(AudioManager::class.java)
// check if device supports bluetooth
val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.getAdapter()
if (bluetoothAdapter == null) {
logUi("Bluetooth is not supported on this device")
exitProcess(0)
}
// check if bluetooth is enabled
if (bluetoothAdapter?.isEnabled == false) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, 42)
}
}
private fun checkPermsAndStart() {
val perms = mutableListOf(Manifest.permission.RECORD_AUDIO)
if (Build.VERSION.SDK_INT >= 31) {
perms += Manifest.permission.BLUETOOTH_CONNECT
}
reqPermissions.launch(perms.toTypedArray())
}
@SuppressLint("MissingPermission")
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
private fun startRecordingFlow() {
if (isRecording) { logUi("Already recording"); return }
val comm = routeToBtScoOrNull() ?: return
// Find the SCO INPUT (match address if possible)
val scoIn = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).firstOrNull {
it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO &&
(it.address == comm.address || comm.address.isNullOrEmpty())
} ?: audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
.firstOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
if (scoIn == null) {
logUi("No BT SCO input device visible.")
return
}
logUi("Using SCO mic input: ${scoIn.productName} addr=${scoIn.address}")
buildAndStart(scoIn) // keep the communication route pinned during the session
}
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
private fun buildAndStart(preferred: AudioDeviceInfo) {
val outFile = File(getExternalFilesDir(null), "sco_recording.wav")
val rate = 16000
val format = AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(rate)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
.build()
var minBuf = AudioRecord.getMinBufferSize(rate, AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT)
if (minBuf
BufferedOutputStream(fos).use { bos ->
// Reserve space for WAV header
val header = ByteArray(44)
bos.write(header)
val buf = ByteArray(2048)
var totalBytes = 0L
while (isRecording) {
val n = ar.read(buf, 0, buf.size)
if (n > 0) {
bos.write(buf, 0, n)
totalBytes += n
}
}
ar.stop()
ar.release()
// Rewrite header with proper sizes
val wav = makeWavHeader(
sampleRate = 16000,
channels = 1,
pcmDataSize = totalBytes.toInt()
)
fos.channel.position(0)
fos.write(wav)
}
}
}
recordThread!!.start()
}
// Many players pause on BECOMING_NOISY. Give them a friendly shove.
private fun resumeExternalPlayback() {
try {
val now = SystemClock.uptimeMillis()
val down = KeyEvent(now, now, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY, 0)
val up = KeyEvent(now, now, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY, 0)
audioManager.dispatchMediaKeyEvent(down)
audioManager.dispatchMediaKeyEvent(up)
logUi("Sent MEDIA_PLAY to resume external playback (HFP).")
} catch (t: Throwable) {
logUi("Couldn't dispatch MEDIA_PLAY: ${t.message}")
}
}
private fun stopRecordingFlow() {
if (!isRecording) { logUi("Nothing to stop."); return }
isRecording = false
try { recordThread?.join(1500) } catch (_: InterruptedException) {}
recordThread = null
setStatus("idle")
logUi("Recording stopped.")
// Return routing to normal
audioManager.clearCommunicationDevice()
audioManager.mode = AudioManager.MODE_NORMAL
}
override fun onStop() {
super.onStop()
logUi("onStop() called")
// Keep it simple: stop when app goes to background.
stopRecordingFlow()
}
private fun makeWavHeader(sampleRate: Int, channels: Int, pcmDataSize: Int): ByteArray {
val byteRate = sampleRate * channels * 2
val totalDataLen = pcmDataSize + 36
return ByteBuffer.allocate(44).order(ByteOrder.LITTLE_ENDIAN).apply {
put("RIFF".toByteArray())
putInt(totalDataLen)
put("WAVE".toByteArray())
put("fmt ".toByteArray())
putInt(16) // PCM header size
putShort(1) // PCM format
putShort(channels.toShort())
putInt(sampleRate)
putInt(byteRate)
putShort((channels * 2).toShort()) // block align
putShort(16) // bits per sample
put("data".toByteArray())
putInt(pcmDataSize)
}.array()
}
@SuppressLint("MissingPermission")
private fun routeToBtScoOrNull(): AudioDeviceInfo? {
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
// Pick a comm device we actually want
val target = audioManager.availableCommunicationDevices.firstOrNull {
it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
it.type == AudioDeviceInfo.TYPE_BLE_HEADSET // only matters for true LE Audio mics
} ?: run {
logUi("No BT comm device (SCO/LE) available.")
return null
}
val ok = audioManager.setCommunicationDevice(target)
logUi("setCommunicationDevice(${target.productName}) = $ok")
// Wait briefly for the route to flip
val deadline = SystemClock.uptimeMillis() + 1500
while (SystemClock.uptimeMillis() < deadline) {
if (audioManager.communicationDevice?.id == target.id) break
Thread.sleep(50)
}
val active = audioManager.communicationDevice
if (active?.id != target.id) {
logUi("SCO route did not activate; communicationDevice=${active?.type}")
return null
}
logUi("SCO route active: ${active.productName} addr=${active.address}")
return active
}
}
Подробнее здесь: https://stackoverflow.com/questions/797 ... on-android
Одновременная запись и потоковая запись музыки на классической гарнитуре BT на Android> = 15 ⇐ Android
-
- Похожие темы
- Ответы
- Просмотры
- Последнее сообщение
-
-
Запись видео и одновременная обработка кадров в Android с помощью CameraX
Гость » » в форуме Android - 0 Ответы
- 45 Просмотры
-
Последнее сообщение Гость
-
-
-
Одновременная запись и чтение в Byte[] для задержки живого звука на произвольное время.
Anonymous » » в форуме JAVA - 0 Ответы
- 34 Просмотры
-
Последнее сообщение Anonymous
-
-
-
Потоковая потоковая передача Polars: Parquet Parquet на основе Shift (-1)
Anonymous » » в форуме Python - 0 Ответы
- 5 Просмотры
-
Последнее сообщение Anonymous
-