Android 音频录音/播放系列:
《Android AudioRecord 录音封装及测试》
《Android AudioTrack 播放封装及测试》

对 Android AudioTrack 封装,内部管理播放状态,并在暂停播放时做 FadeOut,在恢复播放时做 FadeIn ,避免快速暂停/恢复的播放杂音问题。
并设计成回调的拉模式,通过回调向外层要播放的数据。

AudioPlayer 封装如下:

import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import com.alan.audioio.audio.common.IDataAvailableListener;
import com.alan.audioio.audio.common.IOStatus;
import com.alan.audioio.audio.common.Type;
import com.alan.audioio.audio.exception.AudioException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;/*** Author: AlanWang4523.* Date: 2019-06-14 23:04.* Mail: alanwang4523@gmail.com*/
public class AudioPlayer {private final static String TAG = AudioPlayer.class.getSimpleName();private IDataAvailableListener mDataAvailableListener;private volatile @IOStatus int mNewStatus;private volatile @IOStatus int mCurStatus;private boolean mIsStatusChanged = false;private Thread mWorkThread;private AudioTrack mAudioTrack;private int mChannelCount;private ByteBuffer mDataBuffer;private final ReentrantLock mLock = new ReentrantLock();private final Condition mStatusCondition = mLock.newCondition();private final Condition mPlayStateCondition = mLock.newCondition();/*** 构造函数*/public AudioPlayer() {mNewStatus = IOStatus.UNINITIATED;mCurStatus = IOStatus.UNINITIATED;}/*** 设置 IDataAvailableListener* @param dataAvailableListener 用于获取要播放的数据*/public void setDataAvailableListener(IDataAvailableListener dataAvailableListener) {this.mDataAvailableListener = dataAvailableListener;}/*** 初始化* @param sampleRateInHz 采样率* @param channelCount 通道数* @param bufferSize bufferSize* @throws AudioException AudioException*/public void init(int sampleRateInHz, int channelCount, int bufferSize) throws AudioException {try {if (bufferSize <= 0) {throw new AudioException("The buffer size must be greater than 0!", null);}int channelConfig = channelCount == Type.ChannelCount.Stereo ?AudioFormat.CHANNEL_OUT_STEREO : AudioFormat.CHANNEL_OUT_MONO;int audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 默认采样 short 型格式int minBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,sampleRateInHz, channelConfig, audioFormat, minBufferSize, AudioTrack.MODE_STREAM);mChannelCount = channelCount;mDataBuffer = ByteBuffer.allocateDirect(bufferSize).order(ByteOrder.LITTLE_ENDIAN);mNewStatus = IOStatus.INITIATED;mCurStatus = IOStatus.INITIATED;} catch (Exception e) {throw new AudioException("Init AudioPlayer Failed!", e);}}/*** 开始播放,只能在初始化成功后调用*/public void start() {if (mNewStatus == IOStatus.INITIATED) {mWorkThread = new Thread(null, new WorkRunnable(),TAG + "-" + System.currentTimeMillis());mNewStatus = IOStatus.START;mIsStatusChanged = true;mWorkThread.start();mLock.lock();try {while (mIsStatusChanged) {try {mStatusCondition.await(1000, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {// do nothing}}} finally {mLock.unlock();}} else {throw new IllegalStateException();}}/*** 暂停播放*/public void pause() {if (mNewStatus == IOStatus.PAUSE || mNewStatus == IOStatus.STOP) {return;}if (mNewStatus != IOStatus.START && mNewStatus != IOStatus.RESUME) {return;}mLock.lock();try {mNewStatus = IOStatus.PAUSE;mIsStatusChanged = true;while (mIsStatusChanged) {try {mStatusCondition.await(1000, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {// do nothing}}} finally {mLock.unlock();}mAudioTrack.pause();}/*** 恢复播放*/public void resume() {if (mNewStatus != IOStatus.PAUSE) {return;}mLock.lock();try {mAudioTrack.play();mNewStatus = IOStatus.RESUME;mIsStatusChanged = true;mPlayStateCondition.signal();while (mIsStatusChanged) {try {mStatusCondition.await(1000, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {// do nothing}}} finally {mLock.unlock();}}/*** 停止播放*/public void stop() {if (mNewStatus == IOStatus.STOP || mNewStatus == IOStatus.UNINITIATED|| mNewStatus == IOStatus.INITIATED) {return;}mLock.lock();try {mNewStatus = IOStatus.STOP;mIsStatusChanged = true;// 需要调用 notify,避免在 pause 状态调用 stop 时,work thread 还在 waitmPlayStateCondition.signal();while (mIsStatusChanged) {try {mStatusCondition.await(1000, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {// do nothing}}} finally {mLock.unlock();}}/*** 释放资源*/public void release() {mLock.lock();try {// 如果初始化后还没开始播放则释放资源,否则统一在 WorkRunnable 中释放if (mNewStatus == IOStatus.INITIATED) {mAudioTrack.release();} else {mNewStatus = IOStatus.STOP;mIsStatusChanged = true;// 需要调用 notify,避免在 pause 状态调用 release 时,work thread 还在 waitmPlayStateCondition.signal();}} finally {mLock.unlock();}// 等待工作线程结束if (mWorkThread != null) {try {mWorkThread.join(1000);} catch (InterruptedException e) {e.printStackTrace();}}}private class WorkRunnable implements Runnable {@Overridepublic void run() {mAudioTrack.play();while (true) {boolean isNeedFade = false;mLock.lock();if (mIsStatusChanged) {mCurStatus = mNewStatus;isNeedFade = true;}mLock.unlock();mDataBuffer.clear();if (mDataAvailableListener != null) {// 外层将需要播放的数据放入 mDataBuffermDataAvailableListener.onDataAvailable(mDataBuffer);}ByteBuffer byteBuffer = mDataBuffer;if (byteBuffer == null || byteBuffer.limit() <= 0) {if (isNeedFade) {mLock.lock();mIsStatusChanged = false;mStatusCondition.signal();mLock.unlock();}continue;}byteBuffer.rewind();// 如果状态发生改变,对播放数据做 Fadeif (isNeedFade) {if (mCurStatus == IOStatus.PAUSE || mCurStatus == IOStatus.STOP) {shortFadeOut(byteBuffer, mChannelCount);} else {shortFadeIn(byteBuffer, mChannelCount);}}mAudioTrack.write(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.limit());mLock.lock();try {if (isNeedFade) {mIsStatusChanged = false;mStatusCondition.signal();}while ((mCurStatus == IOStatus.PAUSE) && !mIsStatusChanged) {try {mPlayStateCondition.await();} catch (InterruptedException e) {// do nothing}}if (mCurStatus == IOStatus.STOP || mCurStatus == IOStatus.UNINITIATED) {break;}} finally {mLock.unlock();}}try {mAudioTrack.stop();} catch (IllegalStateException e) {e.printStackTrace();}try {mAudioTrack.release();} catch (IllegalStateException e) {e.printStackTrace();}}}/*** 对音频数据做 fade out* @param byteBuffer byteBuffer* @param channelCount channelCount*/private void shortFadeOut(ByteBuffer byteBuffer, int channelCount) {int shortCount = byteBuffer.limit() / 2;if(1 == channelCount) {for(int i = 0; i < shortCount; i++) {short data = (short) (byteBuffer.getShort(i * 2) * 1.0f * (shortCount - i) / shortCount);byteBuffer.putShort(i * 2, data);}} else {for(int i = 0; i < shortCount; i += 2) {short data = (short) (byteBuffer.getShort(i * 2) * 1.0f * (shortCount - i) / shortCount);byteBuffer.putShort(i * 2, data);data = (short)(byteBuffer.getShort((i + 1) * 2) * 1.0f * (shortCount - i) / shortCount);byteBuffer.putShort((i + 1) * 2, data);}}byteBuffer.rewind();}/*** 对音频数据做 fade in* @param byteBuffer byteBuffer* @param channelCount channelCount*/private void shortFadeIn(ByteBuffer byteBuffer, int channelCount) {int shortCount = byteBuffer.limit() / 2;if(1 == channelCount) {for(int i = 0; i < shortCount; i++) {short data = (short)(byteBuffer.getShort(i * 2) * 1.0f * i / shortCount);byteBuffer.putShort(i * 2, data);}} else {for(int i = 0; i < shortCount; i += 2) {short data = (short)(byteBuffer.getShort(i * 2) * 1.0f * i / shortCount);byteBuffer.putShort(i * 2, data);data = (short)(byteBuffer.getShort((i + 1) * 2) * 1.0f * i / shortCount);byteBuffer.putShort((i + 1) * 2, data);}}byteBuffer.rewind();}
}

IOStatus 状态定义:

import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;/*** Author: AlanWang4523.* Date: 2020/11/10 21:19.* Mail: alanwang4523@gmail.com*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({IOStatus.UNINITIATED, IOStatus.INITIATED,IOStatus.START, IOStatus.PAUSE,IOStatus.RESUME, IOStatus.STOP})
public @interface IOStatus {int UNINITIATED = -1;int INITIATED   = 0;int START       = 1;int PAUSE       = 2;int RESUME      = 3;int STOP        = 4;
}

播放数据回调接口定义:

import java.nio.ByteBuffer;/*** Author: AlanWang4523.* Date: 2020/11/10 21:40.* Mail: alanwang4523@gmail.com*/
public interface IDataAvailableListener {/*** 有数据到来* @param byteBuffer 具体数据*      如果是来自采集端可以从 byteBuffer 读取数据,有效数据长度为 byteBuffer.limit()*      如果是来自播放端可以往 byteBuffer 写入数据,写完后调用 byteBuffer.limit(count) 设置有效数据长度*/void onDataAvailable(ByteBuffer byteBuffer);

IOStatus 状态定义和数据回调接口定义同《Android AudioRecord 录音封装及测试》

测试代码:

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.alan.audioio.R;
import com.alan.audioio.audio.AudioPlayer;
import com.alan.audioio.audio.WavFile;
import com.alan.audioio.audio.common.IDataAvailableListener;
import com.alan.audioio.audio.exception.AudioException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import androidx.appcompat.app.AppCompatActivity;public class TestPlayWavActivity extends AppCompatActivity implements View.OnClickListener {private String mPlayWavPath = "/sdcard/Alan/audio/record_wrapper.wav";private AudioPlayer mAudioPlayer;private WavFile mWavFile;private TextView mBtnCommonTest;private boolean mIsStartTest = false;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_common_test);mBtnCommonTest = findViewById(R.id.btnCommonTest);mBtnCommonTest.setOnClickListener(this);mBtnCommonTest.setText("StarTest");initAudioPlayer();if (mAudioPlayer != null) {mAudioPlayer.start();}mBtnCommonTest.setText("Pause");mIsStartTest = true;}@Overrideprotected void onDestroy() {if (mIsStartTest) {mIsStartTest = false;}releaseAudioRes();super.onDestroy();}@Overridepublic void onClick(View view) {if (view.getId() == R.id.btnCommonTest) {if (!mIsStartTest) {mIsStartTest = true;mBtnCommonTest.setText("Pause");if (mAudioPlayer != null) {mAudioPlayer.resume();}} else {mIsStartTest = false;mBtnCommonTest.setText("Resume");if (mAudioPlayer != null) {mAudioPlayer.pause();}}}}private void initAudioPlayer() {releaseAudioRes();int sampleRate = 44100;int channelCount = 1;int bufferSize = 1024;try {File wavFile = new File(mPlayWavPath);if (!wavFile.exists()) {Toast.makeText(this, "The wav file is not exist.", Toast.LENGTH_SHORT).show();return;}mWavFile = new WavFile(mPlayWavPath);mAudioPlayer = new AudioPlayer();mAudioPlayer.init(sampleRate, channelCount, bufferSize);mAudioPlayer.setDataAvailableListener(new IDataAvailableListener() {@Overridepublic void onDataAvailable(ByteBuffer byteBuffer) {int readLen = -1;try {readLen = mWavFile.read(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.limit());} catch (IOException e) {e.printStackTrace();}if (readLen < 0) {readLen = 0;}byteBuffer.limit(readLen);}});} catch (AudioException | IOException e) {e.printStackTrace();Toast.makeText(this, "init failed.", Toast.LENGTH_SHORT).show();finish();}}public void releaseAudioRes() {if (mAudioPlayer != null) {mAudioPlayer.release();}if (mWavFile != null) {try {mWavFile.close();} catch (IOException e) {e.printStackTrace();}}}
}

Android 音频录音/播放系列:
《Android AudioRecord 录音封装及测试》
《Android AudioTrack 播放封装及测试》

完整代码见:AudioIO

Android AudioTrack 播放封装及测试相关推荐

  1. Android AudioTrack播放PCM文件

    上篇文章写了使用AudioRecord采集音频,为了测试采集音频是否正确,可以通过AudioTrack播放音频试下. AudioTrack只能播放PCM格式的文件.PCM全称是Pulse Code M ...

  2. android AudioTrack 播放 正弦波 方波

    http://blog.csdn.net/songconglai/article/details/8161772

  3. Android多媒体开发(5)————利用Android AudioTrack播放mp3文件

    /********************************************************************************************  * aut ...

  4. AudioTrack播放pcm格式音频

    AudioTrack播放pcm格式音频 package com.zero.demo;import android.content.Context; import android.media.Audio ...

  5. NDK学习笔记:JNI调用Java层方法创建Native的AudioTrack播放PCM(方法签名,CallXXXMethod)

    NDK学习笔记:JNI调用Java层方法创建Native的AudioTrack播放PCM 题目有点复杂,不过确实就是那么回事.这章想记录的内容比较多,先列出来: native static 与 nat ...

  6. Android编程中利用AudioTrack播放PCM数据在音频的最后出现重复回声现象的解决方案

    1 问题描述 今天在进行Android编程时遇到一个很奇怪的问题  重点说一下 这里我的测试机用的是"小米Note"  的确不怎么样 我在"华为P8"上面测试就 ...

  7. audiotrack android,Android 音视频渲染-AudioTrack 播放

    类型 MediaPlayer:原生API中封装最全的 SoundPool:适合播放较短的音频 AudioTrack:底层的音频 API,需要自己解码,只能播放 PCM 裸数据和 WAV AudioTr ...

  8. android audiotrack mp3,播放mp3数据压缩由JLayer和Audiotrack latin mp3在android

    我跟着这个example转换来自AudioRecord原始音频数据为MP3,并且它成功发生,如果我将这个数据存储在一个文件的MP3文件和播放与音乐播放器,然后它是可以听到的.播放mp3数据压缩由JLa ...

  9. android打开wav格式,在Android中使用AudioTrack播放WAV文件

    我偶然发现了这个答案(坦白说,通过尝试& ^ @!我没有想到会工作),如果有人有兴趣-在我的原始代码(这是源自原始帖子中的链接的例子) ,数据从文件中读取如下: InputStream is ...

最新文章

  1. Linux必知必会的目录与启动过程
  2. linux挂载home分区,Linux(CentOS6) 调整 /home 挂载 分区大小
  3. 嘲笑一下SUN科技日开发者大会 O(∩_∩)O~
  4. 第四单元用计算机写作,计算机复习题
  5. linux cpu uuid 查看,Linux下查看UUID方法介绍
  6. 两个维护 提升三服务器,王莉霞:以整改成效践行“两个维护”立足“事要解决”抓好“三访结合”...
  7. 数据结构与算法-时间复杂度
  8. C# 中? 和 ?? 在变量中的使用
  9. atitit.userService 用户系统设计 v4 q316 .doc
  10. java基础,继承类题目:编写一个Java应用程序,该程序包括3个类:Monkey类、People类和主类 E...
  11. php 抓取天气情况 www.weather.com.cn
  12. uC/OS-II系统开发笔记
  13. mybatis plus 动态创建表和字段_mybatis-plus maven代码生成器
  14. c语言转换成delphi程序语言
  15. 【MATLAB】求偏导数
  16. 介绍java中Pair和Map的区别
  17. 白话讲解Dubbo服务
  18. 第十三篇 Python建模库介绍
  19. 南繁水稻国家公园国稻种芯(三亚)水稻节功能性农业产业园
  20. java jdk安装失败_图文解答Java JDK9.0安装失败的原因,附带处理方法

热门文章

  1. Noah Mt4跟单系统制作第二篇 Mt4TradeApi连接服务器篇
  2. mybatis源码分析2 - SqlSessionFactory的创建
  3. 实力爆表,日日新成为AI领航者
  4. 电竞玩家必备的配件|新生代蓝牙——电竞级音频发射器
  5. 情感识别:基于ELM、SOM分类器实现心率变异性信号情感识别,附matlab代码
  6. 苹果应用商店增加新功能 可帮助分发“不适合公开发布应用”
  7. [云炬创业基础笔记]创业机会识别
  8. android xutils json请求,Android Xutils3网络请求的封装详解及实例代码
  9. 数字化转型要避免“唯技术论”!
  10. VS编程中发送Ctrl+V键盘消息实现粘贴功能