Android使用AudioTrack播放WAV音频文件

您所在的位置:网站首页 wav格式视频怎么播放 Android使用AudioTrack播放WAV音频文件

Android使用AudioTrack播放WAV音频文件

2024-05-26 09:53| 来源: 网络整理| 查看: 265

目录

1、wav文件格式

2、wav文件解析

3、wav文件播放

QA:

开始播放wav的时候使用了系统的播放器mediaplayer进行播放,但是无奈mediaplayer支持的实在不好。

好些年前自己做过pcm播放使用的是audiotrack,参考:Android 利用AudioTrack播放 PCM 格式音频_mldxs的博客-CSDN博客

其实WAV和PCM两者之间只差了一个wav文件头而已,所以实现了一套audiotrack播放wav的功能。同时支持本地文件播放和网络文件播放

1、wav文件格式

参考了:wav文件格式解析_全职编码的博客-CSDN博客_wav文件格式

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 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



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3