Android音频数据的采集和播放

  音频与视频数据的采集和播放,是多媒体开发的两个基本要素,Android平台上,音频与视频数据的采集和播放是分开的,采集到原始的数据后,经过压缩、编码等处理,后续把音视频资源同步播放。

  Android 提供了两套用于录制和播放音频的 API,分别是 MediaRecorder & MediaPlayer 和 AudioRecord & AudioTrack。MediaRecorder & MediaPlayer 是偏上层的API,使用简单,对想做基本的音视频录制和播放的用户很友好。AudioRecord & AudioTrack 是偏底层的API, 可以对原始的音频数据做混音、变音等处理,灵活度更高。

  首先介绍 MediaRecorder & MediaPlayer。

  MediaRecorder 既可以用来录制音频,也可以用来录制视频,它内部使用一个状态机控制录制过程,如下图所示:

media_recorder_state

  录制音频前,需要先申请录制音频的权限。使用 MeidaRecorder 录制音频的工作流如下:

1
MediaRecorder recorder = new MediaRecorder();
2
 recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
3
 recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
4
 recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
5
 recorder.setOutputFile(PATH_NAME);
6
 recorder.prepare();
7
 // 开始录制
8
 recorder.start();   
9
 ...
10
 recorder.stop();
11
 //reset 以后可以从 setAudioSource() 开始重用 recorder 实例
12
 recorder.reset();  
13
 //release 以后recorder实例不能被重用
14
 recorder.release();

  MediaPlayer 类是 Android 媒体框架最重要的组成部分之一,只需极少量的配置,就能获取录制、解码以及播放音频和视频。它支持本地资源、内部 URI 和外部流式传输的网址这三种不同的媒体源。MediaPlayer 内部同样使用状态机来控制和管理媒体播放,Android 另一个 View 级别用来播放视频的组件 VideoView 内部使用的MediaPlayer 来播放视频。MediaPlayer 播放音频的方法非常简单,需要注意的是 MediaPlayer 占用了宝贵的系统资源,使用后需及时释放资源,示例代码如下:

1
private lateinit var mMediaPlayer: MediaPlayer
2
3
  fun startPlaying(fileName: String) {
4
      mMediaPlayer = MediaPlayer().apply {
5
          try {
6
              setDataSource(fileName)
7
              prepare()
8
              start()
9
          } catch (e: IOException) {
10
              Log.e(LOG_TAG, "prepare() failed")
11
          }
12
      }
13
  }
14
15
  fun stopPlaying() {
16
      mMediaPlayer.release()
17
  }

  开篇提到 MediaRecorder 是偏上层录制音频的API,它的内部也是使用偏底层的 API, AudioRecord 实现的,下面介绍 AudioRecord & AudioTrack。

  AudioRecord 运行过程中,会不断记录硬件中输入的音频资源,需要循环读取这些数据,存储为原始的 PCM 格式的文件。PCM,全称 Pulse Code Modulation,脉冲编码调制,是信号处理专业的内容,自然界中的声音波形比较复杂,通常我们采用脉冲编码调制,即PCM编码,通过抽样、量化、编码三个步骤将连续变化的模拟信号转换为数字信号,PCM 格式的数据是一种未经压缩和处理的音频采样原始数据流。

  使用 AudioRecord 录制音频的详细操作如下:

1
private lateinit var mAudioRecordRecord: Button
2
private var mIsRecording = false
3
private val mExecutorService = Executors.newSingleThreadExecutor()
4
 private var mPCMFileName: String = ""
5
//采样率
6
private val mSampleRate = 44100
7
 //音频格式
8
 private val mAudioFormat = AudioFormat.ENCODING_PCM_16BIT
9
10
 private fun initView(){
11
   //path in device: sdcard/Android/data/packageName/cache/RecorderTest/audiorecordtest.pcm
12
   mPCMFileName = "${externalCacheDir?.absolutePath}/RecorderTest/audiorecordtest.pcm"
13
   
14
   mAudioRecordRecord = findViewById(R.id.audio_record_record)
15
         mAudioRecordRecord.setOnClickListener {
16
             if (mIsRecording) {
17
                 mIsRecording = false
18
                 mAudioRecordRecord.text = "audiorecord start record"
19
             } else {
20
                 mIsRecording = true
21
               	//开启新的线程,循环录制音频
22
                 mExecutorService.submit {
23
                     if (!startAudioRecord(File(mPCMFileName))) {
24
                         recordFail()
25
                     }
26
                 }
27
                 mAudioRecordRecord.text = "audiorecord stop record"
28
             }
29
         }
30
 }
31
32
private fun startAudioRecord(audioFile: File): Boolean {
33
       return try {
34
           audioFile.parentFile?.mkdirs()
35
           audioFile.createNewFile()
36
         	//创建输出流
37
           mFileOutputStream = FileOutputStream(audioFile)
38
         	//设置音频输入源
39
           val audioSource = MediaRecorder.AudioSource.MIC
40
         	//设置声道
41
           val channelConfig = AudioFormat.CHANNEL_IN_MONO
42
           val minBufferSize =
43
               AudioRecord.getMinBufferSize(mSampleRate, channelConfig, mAudioFormat)
44
           mAudioRecord = AudioRecord(
45
               audioSource, mSampleRate, channelConfig,
46
               mAudioFormat, max(minBufferSize, 2048)
47
           )
48
           mAudioRecord.startRecording()
49
           while (mIsRecording) {
50
               val read = mAudioRecord.read(mBuffer, 0, 2048)
51
               if (read > 0) {
52
                   mFileOutputStream.write(mBuffer, 0, read)
53
               } else {
54
                   return false
55
               }
56
           }
57
           stopAudioRecord()
58
       } catch (e: IOException) {
59
           Log.e(TAG, e.toString())
60
           false
61
       } catch (e: java.lang.RuntimeException) {
62
           Log.e(TAG, e.toString())
63
           false
64
       } finally {
65
           mAudioRecord.release()
66
       }
67
  }
68
69
  private fun stopAudioRecord(): Boolean {
70
       try {
71
           mIsRecording = false
72
         	//释放资源
73
           mAudioRecord.stop()
74
           mAudioRecord.release()
75
           mFileOutputStream.close()
76
       } catch (e: IOException) {
77
           Log.e(TAG, e.toString())
78
           return false
79
       }
80
       return true
81
  }
82
83
private fun recordFail() {
84
       mIsRecording = false
85
   		// UI操作回到主线程
86
       runOnUiThread {
87
           Toast.makeText(this, "record fail", Toast.LENGTH_SHORT).show()
88
       }
89
 }

  AudioTrack 类管理和播放单个音频资源,可以在两种模式下运行,静态模式 MODE_STATIC 和流模式MODE_STREAM。

  当音频资源需要以最小延迟播放,并且占用内存不会太大时,应选择静态模式。对于经常播放的 UI 和游戏声音,静态模式是首选方式,并且开销可能最小。静态模式下,需要在调用 play() 函数播放前,先调用 write() 函数写入数据。

  流模式适合处理较大的音频资源,推荐在调用 play() 函数播放前调用 write() 函数写入数据,否则因为没有充足的数据,调用 play() 函数不会立即播放。这种模式下需要连续不断的往 AudioTrack 中写入数据。

 使用 AndioTrack 流模式播放音频的示例代码如下:

1
  private val mSampleRate = 44100
2
  private val mAudioFormat = AudioFormat.ENCODING_PCM_16BIT
3
private var mIsPlaying = false
4
private val mExecutorService = Executors.newSingleThreadExecutor()
5
private val mBuffer = ByteArray(2048)
6
7
private fun initView(){
8
    //文件在设备中的存储路径: sdcard/Android/data/packageName/cache/RecorderTest/audiorecordtest.pcm
9
      mPCMFileName = "${externalCacheDir?.absolutePath}/RecorderTest/audiorecordtest.pcm"
10
    
11
      val mAudioTrackPlay = findViewById(R.id.audio_track_play)
12
      mAudioTrackPlay.setOnClickListener {
13
          if (!mIsPlaying) {
14
              mIsPlaying = true
15
              mExecutorService.submit {
16
                  audioTrackPlay(File(mPCMFileName))
17
              }
18
          }
19
      }
20
  }
21
22
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
23
  private fun audioTrackPlay(audioFile: File) {
24
      val streamType = AudioManager.STREAM_MUSIC
25
      //设置声道
26
      val channelConfig = AudioFormat.CHANNEL_OUT_MONO
27
    	//设置流模式
28
      val mode = AudioTrack.MODE_STREAM
29
    	//设置最小缓冲大小
30
      val minBufferSize = AudioTrack.getMinBufferSize(mSampleRate, channelConfig, mAudioFormat)
31
32
      val attributesBuilder = AudioAttributes.Builder()
33
          .setLegacyStreamType(streamType)
34
          .build()
35
      val formatBuilder = AudioFormat.Builder()
36
          .setChannelMask(channelConfig)
37
          .setEncoding(mAudioFormat)
38
          .setSampleRate(mSampleRate)
39
          .build()
40
      val bufferSize = max(minBufferSize, 2048)
41
      val sessionId = AudioManager.AUDIO_SESSION_ID_GENERATE
42
43
      val audioTrack = AudioTrack(attributesBuilder, formatBuilder, bufferSize, mode, sessionId)
44
45
      var mFileInputStream: FileInputStream? = null
46
      try {
47
          mFileInputStream = FileInputStream(audioFile)
48
          audioTrack.play()
49
          var read: Int
50
          while (mFileInputStream.read(mBuffer).also { read = it } > 0) {
51
              when (audioTrack.write(mBuffer, 0, read)) {
52
                  AudioTrack.ERROR_BAD_VALUE, AudioTrack.ERROR_INVALID_OPERATION, AudioManager.ERROR_DEAD_OBJECT -> audioTrackPlayFail()
53
                  else -> {
54
                      //play success
55
                  }
56
              }
57
          }
58
      } catch (e: RuntimeException) {
59
          Log.e(TAG, e.toString())
60
          audioTrackPlayFail()
61
      } catch (e: IOException) {
62
          Log.e(TAG, e.toString())
63
          audioTrackPlayFail()
64
      } finally {
65
          mIsPlaying = false
66
          mFileInputStream?.let { closeQuietly(it) }
67
          audioTrack.stop()
68
          audioTrack.release()
69
      }
70
  }
71
72
  private fun closeQuietly(mFileInputStream: FileInputStream) {
73
      try {
74
          mFileInputStream.close()
75
      } catch (e: IOException) {
76
          Log.e(TAG, e.toString())
77
      }
78
  }
79
80
  private fun audioTrackPlayFail() {
81
      runOnUiThread { Toast.makeText(this, "play fail", Toast.LENGTH_SHORT).show() }
82
  }

  Android 音频数据的采集和播放就介绍到这里,文章中使用到的详细代码示例请移步 github-StreamingTour