Day1 初识AndroidAudio

devtools/2025/2/24 7:20:09/

今日目标

  1. 搭建Android Audio开发环境
  2. 理解音频基础概念
  3. 实现第一个音频播放/录制Demo
  4. 了解车载音频的特殊性

上午:环境搭建与理论学习

步骤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.myapplicationimport android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack
import kotlin.math.PI
import kotlin.math.sinclass AudioPlayer {private var audioTrack: AudioTrack? = nullprivate var isPaused = falsefun play(sampleRate: Int = 44100, duration: Int = 5, frequency: Double = 440.0) {try {if (isPaused) {audioTrack?.play()isPaused = falsereturn}val numSamples = sampleRate * durationval sample = ShortArray(numSamples)for (i in 0 until numSamples) {val angle = 2.0 * PI * frequency * i / sampleRateval 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 = 1024for (i in 0 until numSamples step bufferSize) {val length = if (i + bufferSize > numSamples) numSamples - i else bufferSizeaudioTrack?.write(sample, i, length)}} catch (e: Exception) {e.printStackTrace()}}fun stop() {try {audioTrack?.stop()audioTrack?.release()audioTrack = nullisPaused = false} catch (e: Exception) {e.printStackTrace()}}fun pause() {try {audioTrack?.pause()isPaused = true} catch (e: Exception) {e.printStackTrace()}}
}
步骤5:实现简单录音
package com.example.myapplicationimport 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.IOExceptionclass AudioRecorder(private val context: Context) {private var audioRecord: AudioRecord? = nullprivate var isRecording = falseprivate var recordingThread: Thread? = null// 采样率private val sampleRate = 44100// 声道配置,单声道private val channelConfig = AudioFormat.CHANNEL_IN_MONO// 音频编码格式,16 位 PCMprivate 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 = truerecordingThread = 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 = falseaudioRecord?.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导入验证)
✅ 初步感知车载导航类音频的特殊处理逻辑

请添加图片描述

音频格式对比表格

格式类型编码方式是否压缩文件大小音质损失延迟要求典型应用场景车载开发注意事项
WAVPCM无损原始音频录制/播放优先用于低延迟实时音频
MP3有损压缩明显音乐存储/流媒体避免用于需要精确控制的场景
AAC有损压缩较少较少蓝牙音频传输需处理编解码延迟问题
FLAC无损压缩中等高保真音乐存储车载存储空间充足时使用
OPUS有损/无损/可选择可变可调极低实时语音传输/VoIP适合车载通话降噪系统
AMR有损极小严重语音录制(如电话录音)仅限语音场景,不推荐音乐

关键参数详解

  1. 编码方式
    • PCM(脉冲编码调制):原始音频数据,Android AudioTrack可直接处理
    • 压缩编码(如MP3/AAC):需通过MediaPlayerMediaCodec解码
  2. 是否压缩
    • 无损压缩(FLAC):保留全部音质,但解码消耗资源
    • 有损压缩(MP3):牺牲音质换取体积减小
  3. 车载开发注意事项
    • 低延迟要求:车载语音控制需优先选择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)
    

车载场景选择建议

  1. 导航提示音:优先使用WAV(低延迟)
  2. 蓝牙音乐传输:强制使用AAC/SBC(协议限制)
  3. 多声道环绕声:必须用PCM或FLAC(保留空间信息)
  4. 语音助手交互:推荐OPUS(网络传输友好)

http://www.ppmy.cn/devtools/161312.html

相关文章

2025年网络安全(黑客技术)三个月自学手册

🤟 基于入门网络安全/黑客打造的:👉黑客&网络安全入门&进阶学习资源包 前言 什么是网络安全 网络安全可以基于攻击和防御视角来分类,我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术,而“蓝队”、“…

常用Git命令

1、初始化本地git仓库(创建新仓库) git init2、配置用户名和邮件 git config --global user.name "xxx" git config --global user.email "xxxxxx.com"3、自动着色 git status git config --global color.ui true git config --g…

(面试经典问题之连接池篇)连接池构成、作用及其基本原理详解

一、什么是连接池 连接池一般指的是数据库连接池(connection pooling),是指程序启动时建立足够的数据库连接,并将这些连接组成一个连接池,由程序动态的对池中的连接进行申请,使用,释放&#xf…

蓝桥每日打卡

#蓝桥#JAVA#奇怪的捐赠 题目描述 本题为填空题,只需要算出结果后,在代码中使用输出语句将所填结果输出即可。 地产大亨 Q 先生临终的遗愿是:拿出100万元给 X 社区的居民抽奖,以稍慰藉心中愧疚。 麻烦的是,他有个很…

51单片机-串口通信编程

串行口工作之前,应对其进行初始化,主要是设置产生波特率的定时器1、串行口控制盒中断控制。具体步骤如下: 确定T1的工作方式(编程TMOD寄存器)计算T1的初值,装载TH1\TL1启动T1(编程TCON中的TR1位…

Oracle中补全时间的处理

在实际数据处理的过程中,存在日期不连续的问题,可能会导致数据传到前后端出现异常,为了避免这种问题,通常会从数据端进行日期不全的处理: 以下为补全年份的案例: with x as (select 开始年份 (…

easyexcel和poi同时存在版本问题,使用easyexcel导出excel设置日期格式

这两天在使用easyexcel导出excel的时候日期格式全都是字符串导致导出的excel列无法筛选 后来调整了一下终于弄好了,看一下最终效果 这里涉及到easyexcel和poi版本冲突的问题,一直没搞定,最后狠下心来把所有的都升级到了最新版,然…

Effective Objective-C 2.0 读书笔记——协议和分类

Effective Objective-C 2.0 读书笔记——协议和分类 文章目录 Effective Objective-C 2.0 读书笔记——协议和分类在分类中添加属性使用 “class-continuation分类” 隐藏实现细节通过协议提供匿名对象 在分类中添加属性 尽管从技术上说,分类里也可以声明属性&…