Android AudioTrack 播放封装及测试

20 篇文章 1 订阅
5 篇文章 0 订阅

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 还在 wait
            mPlayStateCondition.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 还在 wait
                mPlayStateCondition.signal();
            }
        } finally {
            mLock.unlock();
        }

        // 等待工作线程结束
        if (mWorkThread != null) {
            try {
                mWorkThread.join(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private class WorkRunnable implements Runnable {

        @Override
        public void run() {
            mAudioTrack.play();
            while (true) {
                boolean isNeedFade = false;
                mLock.lock();
                if (mIsStatusChanged) {
                    mCurStatus = mNewStatus;
                    isNeedFade = true;
                }
                mLock.unlock();

                mDataBuffer.clear();
                if (mDataAvailableListener != null) {
                    // 外层将需要播放的数据放入 mDataBuffer
                    mDataAvailableListener.onDataAvailable(mDataBuffer);
                }

                ByteBuffer byteBuffer = mDataBuffer;
                if (byteBuffer == null || byteBuffer.limit() <= 0) {
                    if (isNeedFade) {
                        mLock.lock();
                        mIsStatusChanged = false;
                        mStatusCondition.signal();
                        mLock.unlock();
                    }
                    continue;
                }
                byteBuffer.rewind();

                // 如果状态发生改变,对播放数据做 Fade
                if (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;

    @Override
    protected 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;

    }

    @Override
    protected void onDestroy() {
        if (mIsStartTest) {
            mIsStartTest = false;
        }
        releaseAudioRes();
        super.onDestroy();
    }

    @Override
    public 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() {
                @Override
                public 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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值