开始播放wav的时候使用了系统的播放器mediaplayer进行播放,但是无奈mediaplayer支持的实在不好。
其实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)
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)
完整源码已上传:gitee.com/gggl/wav-pl…
本文转自 blog.csdn.net/mldxs/artic…,如有侵权,请联系删除。