当前位置: 首页 > article >正文

Day1 初识AndroidAudio

今日目标

  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.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导入验证)
✅ 初步感知车载导航类音频的特殊处理逻辑

请添加图片描述

音频格式对比表格

格式类型编码方式是否压缩文件大小音质损失延迟要求典型应用场景车载开发注意事项
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.kler.cn/a/558975.html

相关文章:

  • OpenSSL实验
  • 网络安全研究
  • Python常用的函数和功能
  • 黑马点评 面试话术
  • 蓝桥杯 Java B 组之背包问题(01背包、完全背包)
  • Pytorch使用手册-音频特征提取(专题二十一)
  • [Android] GKD v1.10.0 β1—— 开屏 及 内部信息流 广告跳过工具
  • 鸿蒙5.0实战案例:基于ArkUI的验证码实现
  • 菜鸟养成记--Java篇(一)类型转换
  • Docker入门及基本概念
  • 基于ffmpeg+openGL ES实现的视频编辑工具-添加背景音乐(十一)
  • C1车证学习笔记
  • DeepSeek 助力 Vue 开发:打造丝滑的表单验证(Form Validation)
  • Spring Boot集成Redis + Lua脚本实现原子性操作:小白入门指南
  • 如何让大模型理解变量,扣子(coze)智能体中变量描述起着啥作用?程序员看了集体惊呆!扣子免费系列教程(19)
  • 梯度计算(MATLAB和pytorch实例)
  • Github 2025-02-21 Java开源项目日报Top7
  • Unity制作游戏——前期准备:Unity2023和VS2022下载和安装配置——附安装包
  • netty十八罗汉之——挖耳罗汉(Decoder)
  • 一文弄懂RSA算法中的TLS握手流程