Android使用AudioTrack播放WAV音频文件
Posted mldxs
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android使用AudioTrack播放WAV音频文件相关的知识,希望对你有一定的参考价值。
目录
开始播放wav的时候使用了系统的播放器mediaplayer进行播放,但是无奈mediaplayer支持的实在不好。
好些年前自己做过pcm播放使用的是audiotrack,参考:Android 利用AudioTrack播放 PCM 格式音频_mldxs的博客-CSDN博客
其实WAV和PCM两者之间只差了一个wav文件头而已,所以实现了一套audiotrack播放wav的功能。同时支持本地文件播放和网络文件播放
1、wav文件格式
参考了:wav文件格式解析_全职编码的博客-CSDN博客_wav文件格式
其中对我们比较重要的字段:
- NumChannels : 声道(一般1-8)
- SampleRate:采样频率(常见的有8000,16000,44100,48000)
- BitsPerSample:采样精度(常见的有8、16、32,分别代表着一个采样占据1、2、4个字节)
其余字段解释,详见wav文件格式解析_全职编码的博客-CSDN博客_wav文件格式
wav文件头共44个字节,文件头后紧跟着的就是pcm数据,也就是真正的播放数据了。
2、wav文件解析
package com.macoli.wav_player
import java.io.DataInputStream
import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
class Wav(private val inputStream : InputStream)
val wavHeader : WavHeader = WavHeader()
init
parseHeader()
private fun parseHeader()
val dataInputStream : DataInputStream = DataInputStream(inputStream)
val intValue = ByteArray(4)
val shortValue = ByteArray(2)
try
wavHeader.mChunkID = "" + Char(dataInputStream.readByte().toUShort()) + Char(
dataInputStream.readByte().toUShort()
) + Char(dataInputStream.readByte().toUShort()) + Char(
dataInputStream.readByte().toUShort()
)
dataInputStream.read(intValue)
wavHeader.mChunkSize = byteArrayToInt(intValue)
wavHeader.mFormat = "" + Char(dataInputStream.readByte().toUShort()) + Char(
dataInputStream.readByte().toUShort()
) + Char(dataInputStream.readByte().toUShort()) + Char(
dataInputStream.readByte().toUShort()
)
wavHeader.mSubChunk1ID = "" + Char(dataInputStream.readByte().toUShort()) + Char(
dataInputStream.readByte().toUShort()
) + Char(dataInputStream.readByte().toUShort()) + Char(
dataInputStream.readByte().toUShort()
)
dataInputStream.read(intValue)
wavHeader.mSubChunk1Size = byteArrayToInt(intValue)
dataInputStream.read(shortValue)
wavHeader.mAudioFormat = byteArrayToShort(shortValue)
dataInputStream.read(shortValue)
wavHeader.mNumChannel = byteArrayToShort(shortValue)
dataInputStream.read(intValue)
wavHeader.mSampleRate = byteArrayToInt(intValue)
dataInputStream.read(intValue)
wavHeader.mByteRate = byteArrayToInt(intValue)
dataInputStream.read(shortValue)
wavHeader.mBlockAlign = byteArrayToShort(shortValue)
dataInputStream.read(shortValue)
wavHeader.mBitsPerSample = byteArrayToShort(shortValue)
wavHeader.mSubChunk2ID = "" + Char(dataInputStream.readByte().toUShort()) + Char(
dataInputStream.readByte().toUShort()
) + Char(dataInputStream.readByte().toUShort()) + Char(
dataInputStream.readByte().toUShort()
)
dataInputStream.read(intValue)
wavHeader.mSubChunk2Size = byteArrayToInt(intValue)
catch (e: Exception)
e.printStackTrace()
private fun byteArrayToShort(b: ByteArray): Short
return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).short
private fun byteArrayToInt(b: ByteArray): Int
return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).int
/**
* WAV文件头
* */
class WavHeader
var mChunkID = "RIFF"
var mChunkSize = 0
var mFormat = "WAVE"
var mSubChunk1ID = "fmt "
var mSubChunk1Size = 16
var mAudioFormat: Short = 1
var mNumChannel: Short = 1
var mSampleRate = 8000
var mByteRate = 0
var mBlockAlign: Short = 0
var mBitsPerSample: Short = 8
var mSubChunk2ID = "data"
var mSubChunk2Size = 0
constructor()
constructor(chunkSize: Int, sampleRateInHz: Int, channels: Int, bitsPerSample: Int)
mChunkSize = chunkSize
mSampleRate = sampleRateInHz
mBitsPerSample = bitsPerSample.toShort()
mNumChannel = channels.toShort()
mByteRate = mSampleRate * mNumChannel * mBitsPerSample / 8
mBlockAlign = (mNumChannel * mBitsPerSample / 8).toShort()
override fun toString(): String
return "WavFileHeader" +
"mChunkID='" + mChunkID + '\\'' +
", mChunkSize=" + mChunkSize +
", mFormat='" + mFormat + '\\'' +
", mSubChunk1ID='" + mSubChunk1ID + '\\'' +
", mSubChunk1Size=" + mSubChunk1Size +
", mAudioFormat=" + mAudioFormat +
", mNumChannel=" + mNumChannel +
", mSampleRate=" + mSampleRate +
", mByteRate=" + mByteRate +
", mBlockAlign=" + mBlockAlign +
", mBitsPerSample=" + mBitsPerSample +
", mSubChunk2ID='" + mSubChunk2ID + '\\'' +
", mSubChunk2Size=" + mSubChunk2Size +
''
3、wav文件播放
使用audiotrack播放wav一般有3个步骤:
- 下载wav文件
- 初始化audiotrack(初始化audiotrack依赖刚刚解析wav文件头的信息)
private void initAudioTracker()
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
AudioFormat audioFormat = new AudioFormat.Builder()
.setEncoding(getEncoding())
.setSampleRate(mWav.getWavHeader().getMSampleRate())
.build();
mAudioTrack = new AudioTrack(audioAttributes, audioFormat, getMiniBufferSize()
, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);
- audiotrack.write播放音频
下面是真正播放wav的代码了,代码很简单,不做过多介绍了:
使用单独的Downloader线程对wav文件进行下载,加快缓冲速度,避免播放出现卡顿杂音现象。
使用RealPlayer线程对wav文件进行播放。
其中Downloader线程对应生产者,RealPlayer对应消费者。mSoundData则是生产者消费者之间的缓冲区。
package com.macoli.wav_player;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import java.util.Arrays;
import java.util.concurrent.LinkedBlockingQueue;
public class WavPlayer
public volatile boolean isPlaying = false ;
private final LinkedBlockingQueue<byte[]> mSoundData = new LinkedBlockingQueue<>() ;
private volatile Wav mWav ;
private volatile int mDownloadComplete = -1 ;
private final byte[] mWavReady = new byte[1] ;
public WavPlayer()
public void play(String urlStr , boolean local)
isPlaying = true ;
mSoundData.clear();
mDownloadComplete = -1 ;
mWav = null ;
new Thread(new Downloader(urlStr , local)).start();
new Thread(new RealPlayer()).start();
private int getChannel()
return mWav.getWavHeader().getMNumChannel() == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO;
private int getEncoding()
int ENCODING = AudioFormat.ENCODING_DEFAULT;
if (mWav.getWavHeader().getMBitsPerSample() == 8)
ENCODING = AudioFormat.ENCODING_PCM_8BIT;
else if (mWav.getWavHeader().getMBitsPerSample() == 16)
ENCODING = AudioFormat.ENCODING_PCM_16BIT;
else if (mWav.getWavHeader().getMBitsPerSample() == 32)
ENCODING = AudioFormat.ENCODING_PCM_FLOAT;
return ENCODING ;
private int getMiniBufferSize()
return AudioTrack.getMinBufferSize(
mWav.getWavHeader().getMSampleRate(), getChannel(), getEncoding());
private WavOnCompletionListener onCompletionListener ;
public void setOnCompletionListener(WavOnCompletionListener onCompletionListener)
this.onCompletionListener = onCompletionListener ;
public interface WavOnCompletionListener
void onCompletion(int status) ;
private class Downloader implements Runnable
private final String mUrlStr ;
private final boolean isLocal ;
private Downloader(String urlStr , boolean local)
mUrlStr = urlStr ;
isLocal = local ;
@Override
public void run()
mDownloadComplete = -1 ;
InputStream in = null ;
try
if (!isLocal)
URL url = new URL(mUrlStr);
URLConnection urlConnection = url.openConnection() ;
in = new BufferedInputStream(urlConnection.getInputStream()) ;
else
in = new BufferedInputStream(new FileInputStream(mUrlStr)) ;
if (in == null)
mDownloadComplete = -2 ;
isPlaying = false ;
onCompletionListener.onCompletion(-2);
synchronized (mWavReady)
mWavReady.notifyAll();
return ;
synchronized (mWavReady)
mWav = new Wav(in) ;
mWavReady.notifyAll();
catch (Exception e)
mDownloadComplete = -2 ;
isPlaying = false ;
onCompletionListener.onCompletion(-2);
synchronized (mWavReady)
mWavReady.notifyAll();
return ;
int iniBufferSize = getMiniBufferSize() ;
byte[] buffer = new byte[iniBufferSize] ;
int read = 0 ;
long startTime = System.currentTimeMillis() ;
try
int bufferFilledCount = 0 ;
while ((read = in.read(buffer , bufferFilledCount , iniBufferSize - bufferFilledCount)) != -1)
bufferFilledCount += read ;
if (bufferFilledCount >= iniBufferSize)
byte[] newBuffer = Arrays.copyOf(buffer , iniBufferSize) ;
mSoundData.put(newBuffer) ;
read = 0 ;
bufferFilledCount = 0 ;
mDownloadComplete = 1 ;
catch (IOException | InterruptedException e)
mDownloadComplete = -2 ;
isPlaying = false ;
onCompletionListener.onCompletion(-2);
finally
if (in != null)
try
in.close();
catch (IOException e)
e.printStackTrace();
private class RealPlayer implements Runnable
private AudioTrack mAudioTrack;
private void initAudioTracker()
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
AudioFormat audioFormat = new AudioFormat.Builder()
.setEncoding(getEncoding())
.setSampleRate(mWav.getWavHeader().getMSampleRate())
.build();
mAudioTrack = new AudioTrack(audioAttributes, audioFormat, getMiniBufferSize()
, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);
public void play()
mAudioTrack.play() ;
byte[] buffer ;
try
while(true)
buffer = mSoundData.take();
if (mWav.getWavHeader().getMBitsPerSample() == 8)
try
mAudioTrack.write(buffer, 0, buffer.length, AudioTrack.WRITE_BLOCKING);
catch (Exception e)
else if (mWav.getWavHeader().getMBitsPerSample() == 16)
try
ShortBuffer sb = ByteBuffer.wrap(buffer, 0, buffer.length).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
short[] out = new short[sb.capacity()];
sb.get(out);
mAudioTrack.write(out, 0, out.length, AudioTrack.WRITE_BLOCKING);
catch (Exception e)
else if (mWav.getWavHeader().getMBitsPerSample() == 32)
try
FloatBuffer fb = ByteBuffer.wrap(buffer, 0, buffer.length).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer();
float[] out = new float[fb.capacity()];
fb.get(out);
mAudioTrack.write(out, 0, out.length, AudioTrack.WRITE_BLOCKING);
// mAudioTrack.write(mBuffer, 0, read , AudioTrack.WRITE_BLOCKING);
catch (Exception e)
if ((1 == mDownloadComplete && mSoundData.isEmpty()) || -2 == mDownloadComplete)
break ;
catch (Exception e)
isPlaying = false ;
onCompletionListener.onCompletion(-2);
return ;
finally
mAudioTrack.stop();
mAudioTrack.release();
mAudioTrack = null;
isPlaying = false ;
onCompletionListener.onCompletion(1);
@Override
public void run()
synchronized (mWavReady)
if (mWav == null)
try
mWavReady.wait();
if (mWav == null)
return ;
catch (InterruptedException e)
e.printStackTrace();
initAudioTracker() ;
play();
调用wavplayer播放wav:
wavplayer.play(url , 是否是本地wav文件)
val wavPlayer = WavPlayer()
wavPlayer.play("/sdcard/Music/3.wav" , true)
QA:
Q:1、播放wav第一帧有爆音。
A: 由于wav文件有44字节的文件头,在读取文件的时候需要跳过wav文件头再向AudioTrack.write中进行写入。
Q:2、播放网络wav有杂音。
A:由于网络读取wav文件每次读取的字节数会远远小于我们设置的minbuffer,所以每次读取网络流的时候我们都要等待minbuffer填充满的时候再使用AudioTrack.write进行写入。
int bufferFilledCount = 0 ;
while ((read = in.read(buffer , bufferFilledCount , iniBufferSize - bufferFilledCount)) != -1)
bufferFilledCount += read ;
if (bufferFilledCount >= iniBufferSize)
byte[] newBuffer = Arrays.copyOf(buffer , iniBufferSize) ;
mSoundData.put(newBuffer) ;
read = 0 ;
bufferFilledCount = 0 ;
Q:3、播放wav失败,全部都是杂音。
A:查看wav文件头,看看wav的采样精度,如果采样精度是32的话,必须使用write(float[]),否则肯定播放失败。
public int write(@NonNull float[] audioData, int offsetInFloats, int sizeInFloats,
@WriteMode int writeMode)
完整源码已上传:https://gitee.com/gggl/wav-player
Android音频系统AudioTrack使用方法详解
以上是关于Android使用AudioTrack播放WAV音频文件的主要内容,如果未能解决你的问题,请参考以下文章
Android AudioTrack 在声音开始和结束时点击