Day1 初识AndroidAudio
今日目标
- 搭建Android Audio开发环境
- 理解音频基础概念
- 实现第一个音频播放/录制Demo
- 了解车载音频的特殊性
上午:环境搭建与理论学习
步骤1:开发环境配置
- 安装Android Studio(最新稳定版)
- 创建新项目(选择Kotlin语言,Minimum SDK API 21+)
- 连接测试设备:
- 使用手机(推荐Android 10+)
- 或配置Android Automotive模拟器(官方教程)
步骤2:核心概念学习
- 必读文档:
- Android Audio概览(精读前3节)
- 理解关键术语:
- AudioTrack(播放) vs AudioRecord(录制)
- Stream Type(STREAM_MUSIC/STREAM_NAVIGATION)
- Audio Focus(多应用音频抢占机制)
- 车载场景关联:
- 思考:当导航语音播报时,音乐应用该如何响应?(记录疑问)
步骤3:音频格式认知
- 用Audacity软件(下载)生成测试音频:
- 创建1kHz正弦波WAV文件(采样率44.1kHz,16bit PCM)
- 对比MP3与WAV文件大小/频谱差异(理解有损vs无损压缩)
步骤4:实现基础音频播放
package com.example.myapplication
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack
import kotlin.math.PI
import kotlin.math.sin
class AudioPlayer {
private var audioTrack: AudioTrack? = null
private var isPaused = false
fun play(sampleRate: Int = 44100, duration: Int = 5, frequency: Double = 440.0) {
try {
if (isPaused) {
audioTrack?.play()
isPaused = false
return
}
val numSamples = sampleRate * duration
val sample = ShortArray(numSamples)
for (i in 0 until numSamples) {
val angle = 2.0 * PI * frequency * i / sampleRate
val value = (sin(angle) * Short.MAX_VALUE).toInt()
sample[i] = value.toShort()
}
val minBufferSize = AudioTrack.getMinBufferSize(
sampleRate,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT
)
audioTrack = AudioTrack(
AudioManager.STREAM_MUSIC,
sampleRate,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
minBufferSize,
AudioTrack.MODE_STREAM
)
if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) {
throw IllegalStateException("AudioTrack 初始化失败")
}
audioTrack?.play()
val bufferSize = 1024
for (i in 0 until numSamples step bufferSize) {
val length = if (i + bufferSize > numSamples) numSamples - i else bufferSize
audioTrack?.write(sample, i, length)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun stop() {
try {
audioTrack?.stop()
audioTrack?.release()
audioTrack = null
isPaused = false
} catch (e: Exception) {
e.printStackTrace()
}
}
fun pause() {
try {
audioTrack?.pause()
isPaused = true
} catch (e: Exception) {
e.printStackTrace()
}
}
}
步骤5:实现简单录音
package com.example.myapplication
import android.content.Context
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.os.Build
import android.os.Environment
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
class AudioRecorder(private val context: Context) {
private var audioRecord: AudioRecord? = null
private var isRecording = false
private var recordingThread: Thread? = null
// 采样率
private val sampleRate = 44100
// 声道配置,单声道
private val channelConfig = AudioFormat.CHANNEL_IN_MONO
// 音频编码格式,16 位 PCM
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
// 缓冲区大小
private val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
/**
* 开始录音
* @param filePath 录音文件保存的路径
*/
fun startRecording(filePath: String) {
try {
audioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
audioFormat,
bufferSize
)
audioRecord?.let {
it.startRecording()
isRecording = true
recordingThread = Thread {
val data = ByteArray(bufferSize)
val file = getOutputFile(filePath)
val outputStream = FileOutputStream(file)
try {
while (isRecording) {
val readSize = it.read(data, 0, bufferSize)
if (readSize > 0) {
outputStream.write(data, 0, readSize)
}
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
try {
outputStream.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
recordingThread?.start()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* 停止录音
*/
fun stopRecording() {
isRecording = false
audioRecord?.let {
if (it.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
it.stop()
}
it.release()
audioRecord = null
}
recordingThread?.join()
recordingThread = null
}
/**
* 根据传入的文件名获取输出文件
* @param fileName 文件名
* @return 输出文件对象
*/
private fun getOutputFile(fileName: String): File {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10 及以上使用应用专属外部存储目录
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)
if (!storageDir?.exists()!!) {
storageDir.mkdirs()
}
File(storageDir, fileName)
} else {
// Android 9 及以下使用传统外部存储路径
val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
if (!storageDir.exists()) {
storageDir.mkdirs()
}
File(storageDir, fileName)
}
}
}
⚠️ 音频资源加载:
- 避免直接将大文件放入
res/raw
(可能OOM),生产环境应使用文件流逐步读取 - WAV文件需确保无压缩(PCM格式),否则AudioTrack可能无法直接播放
⚠️ 延迟优化预知:
- 今日使用的AudioTrack默认模式为
MODE_STREAM
,后续学习会对比MODE_STATIC
模式差异
学习成果检验
✅ 成功播放自定义生成的测试音频
✅ 录制并保存PCM原始数据文件(可用Audacity导入验证)
✅ 初步感知车载导航类音频的特殊处理逻辑
音频格式对比表格
格式类型 | 编码方式 | 是否压缩 | 文件大小 | 音质损失 | 延迟要求 | 典型应用场景 | 车载开发注意事项 |
---|---|---|---|---|---|---|---|
WAV | PCM无损 | 否 | 大 | 无 | 低 | 原始音频录制/播放 | 优先用于低延迟实时音频 |
MP3 | 有损压缩 | 是 | 小 | 明显 | 中 | 音乐存储/流媒体 | 避免用于需要精确控制的场景 |
AAC | 有损压缩 | 是 | 较少 | 较少 | 中 | 蓝牙音频传输 | 需处理编解码延迟问题 |
FLAC | 无损压缩 | 是 | 中等 | 无 | 高 | 高保真音乐存储 | 车载存储空间充足时使用 |
OPUS | 有损/无损/可选择 | 是 | 可变 | 可调 | 极低 | 实时语音传输/VoIP | 适合车载通话降噪系统 |
AMR | 有损 | 是 | 极小 | 严重 | 低 | 语音录制(如电话录音) | 仅限语音场景,不推荐音乐 |
关键参数详解
- 编码方式
- PCM(脉冲编码调制):原始音频数据,Android AudioTrack可直接处理
- 压缩编码(如MP3/AAC):需通过
MediaPlayer
或MediaCodec
解码
- 是否压缩
- 无损压缩(FLAC):保留全部音质,但解码消耗资源
- 有损压缩(MP3):牺牲音质换取体积减小
- 车载开发注意事项
- 低延迟要求:车载语音控制需优先选择WAV/OPUS
- 存储限制:导航提示音可使用高压缩比的AAC
- 硬件兼容性:部分车机芯片对特定格式(如OPUS)支持有限
-
直接播放PCM数据(适合WAV):
// 使用AudioTrack播放原始PCM数据 audioTrack.write(pcmData, 0, pcmData.size)
-
解码压缩格式(如MP3/AAC):
// 使用MediaPlayer解码 mediaPlayer.setDataSource("audio.mp3") mediaPlayer.prepare() mediaPlayer.start()
-
实时编码(如语音传输):
// 使用MediaCodec进行OPUS编码 val codec = MediaCodec.createEncoderByType("audio/opus") codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
车载场景选择建议
- 导航提示音:优先使用WAV(低延迟)
- 蓝牙音乐传输:强制使用AAC/SBC(协议限制)
- 多声道环绕声:必须用PCM或FLAC(保留空间信息)
- 语音助手交互:推荐OPUS(网络传输友好)