Android实现蓝牙麦克风录音播放及媒体存储的IoT技术方案咨询
嘿,针对你这个IoT项目里的Android蓝牙音频需求,我来一步步拆解实现方案——都是我在实际项目里踩过坑后总结的靠谱思路:
一、核心功能实现步骤
1. 先搞定权限(重中之重)
Android对音频录制和蓝牙操作管得很严,必须先在Manifest里声明权限,再动态申请。这里列你需要的所有权限:
- 音频录制:
RECORD_AUDIO - 蓝牙连接/扫描:Android 12+需要
BLUETOOTH_CONNECT和BLUETOOTH_SCAN,旧版本用BLUETOOTH和BLUETOOTH_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_CONNECTED、ACTION_ACL_DISCONNECTED事件,及时提示用户或自动重连。 - 延迟优化:如果要低延迟,尽量用PCM格式,调小buffer大小,避免用压缩编码。
- 后台运行:如果需要后台持续录制,必须创建前台服务,不然Android会把你的App杀掉。
- 兼容性:不同厂商的设备对蓝牙协议支持不一样,比如有些设备SCO连接慢,要多测试不同机型。
内容的提问来源于stack exchange,提问作者Ayush Goel




