You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android实现蓝牙麦克风录音播放及媒体存储的IoT技术方案咨询

嘿,针对你这个IoT项目里的Android蓝牙音频需求,我来一步步拆解实现方案——都是我在实际项目里踩过坑后总结的靠谱思路:

一、核心功能实现步骤

1. 先搞定权限(重中之重)

Android对音频录制和蓝牙操作管得很严,必须先在Manifest里声明权限,再动态申请。这里列你需要的所有权限:

  • 音频录制:RECORD_AUDIO
  • 蓝牙连接/扫描:Android 12+需要BLUETOOTH_CONNECTBLUETOOTH_SCAN,旧版本用BLUETOOTHBLUETOOTH_ADMIN
  • 存储:Android 10+推荐用MediaStore,不用申请WRITE_EXTERNAL_STORAGE;旧版本需要额外加这个权限

Manifest声明示例:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Android 13+后台运行需要 -->

<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<uses-feature android:name="android.hardware.microphone" android:required="true" />

动态申请权限的Kotlin代码片段:

private val requiredPermissions = arrayOf(
    Manifest.permission.RECORD_AUDIO,
    Manifest.permission.BLUETOOTH_CONNECT,
    Manifest.permission.BLUETOOTH_SCAN
)

private fun checkAndRequestPermissions() {
    val missingPermissions = requiredPermissions.filter {
        ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
    }
    if (missingPermissions.isNotEmpty()) {
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results ->
            // 这里可以处理权限申请结果,比如提示用户必须授权才能使用功能
        }.launch(missingPermissions.toTypedArray())
    }
}

2. 蓝牙设备配对与音频路由配置

要实现蓝牙麦克风采集、配对设备播放,核心是让Android系统把音频输入输出切换到蓝牙设备上。

2.1 找到已配对的蓝牙设备

先获取系统里已配对的蓝牙设备,找到你的目标麦克风和播放设备:

val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
val pairedDevices = bluetoothAdapter.bondedDevices
// 替换成你的设备名称,或者通过MAC地址匹配更可靠
val targetMicDevice = pairedDevices.find { it.name == "My_Bluetooth_Mic" }
val targetPlaybackDevice = pairedDevices.find { it.name == "My_Paired_Phone" }

2.2 切换音频路由到蓝牙

AudioManager来设置音频模式,强制使用蓝牙作为输入输出:

val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager

// 设置为通话模式,支持双向蓝牙音频
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION

// 启动SCO连接,用于蓝牙语音输入
audioManager.isBluetoothScoOn = true
audioManager.startBluetoothSco()

// 开启A2DP,把音频输出到配对的蓝牙设备
audioManager.setBluetoothA2dpOn(true)

注意:startBluetoothSco()需要时间初始化,最好注册广播监听ACTION_SCO_AUDIO_STATE_CHANGED,确认连接成功后再开始录制,避免无声音的问题。

3. 音频录制+实时播放+文件保存

这里分两种方案,按需选择:

3.1 简单方案:用MediaRecorder录制并保存

如果不需要极低延迟,MediaRecorder足够省心,直接生成编码后的音频文件,适合给算法研究用:

private var recorder: MediaRecorder? = null

fun startRecording() {
    recorder = MediaRecorder().apply {
        setAudioSource(MediaRecorder.AudioSource.DEFAULT) // 系统自动用蓝牙麦克风
        setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
        setOutputFile(getOutputFilePath())
        prepare()
        start()
    }
}

fun stopRecording() {
    recorder?.apply {
        stop()
        release()
    }
    recorder = null
}

// Android 10+用MediaStore创建文件,避免存储权限问题
private fun getOutputFilePath(): String {
    val contentValues = ContentValues().apply {
        put(MediaStore.Audio.Media.DISPLAY_NAME, "IoT_Audio_${System.currentTimeMillis()}.m4a")
        put(MediaStore.Audio.Media.MIME_TYPE, "audio/mp4")
        put(MediaStore.Audio.Media.RELATIVE_PATH, Environment.DIRECTORY_MUSIC + "/IoT_Records")
    }
    val uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues)
    return uri?.let { contentResolver.openFileDescriptor(it, "w")?.use { fd ->
        FileDescriptorSync.getPath(fd)
    } } ?: "${externalCacheDir}/temp_audio.m4a"
}

3.2 低延迟方案:AudioRecord+AudioTrack实时流

如果需要实时把录制的音频传到配对设备播放,用AudioRecord采集PCM数据,再用AudioTrack实时播放,同时写入文件:

private var isRecording = false
private var audioRecord: AudioRecord? = null
private var audioTrack: AudioTrack? = null
private var fileOutputStream: FileOutputStream? = null

fun startRealTimeRecordAndPlay() {
    val sampleRate = 44100
    val channelConfig = AudioFormat.CHANNEL_IN_MONO
    val audioFormat = AudioFormat.ENCODING_PCM_16BIT
    val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)

    // 初始化AudioRecord(采集蓝牙麦克风数据)
    audioRecord = AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate, channelConfig, audioFormat, bufferSize)
    // 初始化AudioTrack(输出到蓝牙设备)
    audioTrack = AudioTrack(AudioManager.STREAM_VOICE_CALL, sampleRate, 
        AudioFormat.CHANNEL_OUT_MONO, audioFormat, bufferSize, AudioTrack.MODE_STREAM)
    
    // 打开文件输出流,保存PCM数据(后续可以转成MP4给算法用)
    fileOutputStream = FileOutputStream(getPcmOutputFile())

    isRecording = true
    audioRecord?.startRecording()
    audioTrack?.play()

    // 开启线程处理数据读写
    thread {
        val buffer = ByteArray(bufferSize)
        while (isRecording) {
            val readBytes = audioRecord?.read(buffer, 0, bufferSize) ?: 0
            if (readBytes > 0) {
                // 实时播放
                audioTrack?.write(buffer, 0, readBytes)
                // 保存到文件
                fileOutputStream?.write(buffer, 0, readBytes)
            }
        }
        // 停止后释放资源
        audioRecord?.stop()
        audioRecord?.release()
        audioTrack?.stop()
        audioTrack?.release()
        fileOutputStream?.close()
    }
}

fun stopRealTimeRecord() {
    isRecording = false
}

private fun getPcmOutputFile(): File {
    return File(externalCacheDir, "IoT_Audio_${System.currentTimeMillis()}.pcm")
}

4. 通用实现注意事项

  • 蓝牙稳定性:蓝牙连接容易断,要注册广播监听ACTION_ACL_CONNECTEDACTION_ACL_DISCONNECTED事件,及时提示用户或自动重连。
  • 延迟优化:如果要低延迟,尽量用PCM格式,调小buffer大小,避免用压缩编码。
  • 后台运行:如果需要后台持续录制,必须创建前台服务,不然Android会把你的App杀掉。
  • 兼容性:不同厂商的设备对蓝牙协议支持不一样,比如有些设备SCO连接慢,要多测试不同机型。

内容的提问来源于stack exchange,提问作者Ayush Goel

火山引擎 最新活动