Я хочу использовать гарнитуру 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
1755963715
Anonymous
Я хочу использовать гарнитуру 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
}
}
Подробнее здесь: [url]https://stackoverflow.com/questions/79744383/simultaneous-recording-and-music-streaming-on-a-classic-bt-headset-on-android[/url]
Ответить
1 сообщение
• Страница 1 из 1
Перейти
- Кемерово-IT
- ↳ Javascript
- ↳ C#
- ↳ JAVA
- ↳ Elasticsearch aggregation
- ↳ Python
- ↳ Php
- ↳ Android
- ↳ Html
- ↳ Jquery
- ↳ C++
- ↳ IOS
- ↳ CSS
- ↳ Excel
- ↳ Linux
- ↳ Apache
- ↳ MySql
- Детский мир
- Для души
- ↳ Музыкальные инструменты даром
- ↳ Печатная продукция даром
- Внешняя красота и здоровье
- ↳ Одежда и обувь для взрослых даром
- ↳ Товары для здоровья
- ↳ Физкультура и спорт
- Техника - даром!
- ↳ Автомобилистам
- ↳ Компьютерная техника
- ↳ Плиты: газовые и электрические
- ↳ Холодильники
- ↳ Стиральные машины
- ↳ Телевизоры
- ↳ Телефоны, смартфоны, плашеты
- ↳ Швейные машинки
- ↳ Прочая электроника и техника
- ↳ Фототехника
- Ремонт и интерьер
- ↳ Стройматериалы, инструмент
- ↳ Мебель и предметы интерьера даром
- ↳ Cантехника
- Другие темы
- ↳ Разное даром
- ↳ Давай меняться!
- ↳ Отдам\возьму за копеечку
- ↳ Работа и подработка в Кемерове
- ↳ Давай с тобой поговорим...
Мобильная версия