今天咱们学习音频的收集、编码、生成文件、转码等操作,咱们生成三种格局的文件格局,pcm、wav、aac 三种格局,而且咱们用 AudioStack 来播映音频,最后咱们播映这个音频。
运用 AudioRecord 完结录音生成PCM 文件
AudioRecord 是 Android 体系供给的用于完结录音的功用类,要想了解这个类的详细的说明和用法,咱们能够去看一下官方的文档:
AndioRecord类的主要功用是让各种 Java 运用能够办理音频资源,以便它们经过此类能够录制声响相关的硬件所收集的声响。此功用的完结便是经过”pulling”(读取)AudioRecord目标的声响数据来完结的。在录音进程中,运用所需求做的便是经过后边三个类办法中的一个去及时地获取AudioRecord目标的录音数据. AudioRecord类供给的三个获取声响数据的办法分别是read(byte[], int, int), read(short[], int, int), read(ByteBuffer, int). 不管挑选运用那一个办法都必须事前设定方便用户的声响数据的存储格局。
开端录音的时分,AudioRecord需求初始化一个相关联的声响buffer, 这个buffer主要是用来保存新的声响数据。这个buffer的巨细,咱们能够在目标构造期间去指定。它标明一个AudioRecord目标还没有被读取(同步)声响数据前能录多长的音(即一次能够录制的声响容量)。声响数据从音频硬件中被读出,数据巨细不超过整个录音数据的巨细(能够分多次读出),即每次读取初始化buffer容量的数据。
1.1 首先要声明一些全局的变量和常量参数
主要是声明一些用到的参数,详细解说能够看注释。
//指定音频源 这个和MediaRecorder是相同的 MediaRecorder.AudioSource.MIC指的是麦克风
private static final int mAudioSource = MediaRecorder.AudioSource.MIC;
//指定采样率 (MediaRecoder 的采样率一般是8000Hz AAC的一般是44100Hz。 设置采样率为44100,目前为常用的采样率,官方文档表明这个值能够兼容一切的设置)
private static final int mSampleRateInHz = 44100;
//指定捕获音频的声道数目。在AudioFormat类中指定用于此的常量,单声道
private static final int mChannelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;
//指定音频量化位数 ,在AudioFormaat类中指定了以下各种或许的常量。一般咱们挑选ENCODING_PCM_16BIT和ENCODING_PCM_8BIT PCM代表的是脉冲编码调制,它实际上是原始音频样本。
//因而能够设置每个样本的分辨率为16位或许8位,16位将占用更多的空间和处理才能,表明的音频也愈加接近真实。
private static final int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
//指定缓冲区巨细。调用AudioRecord类的getMinBufferSize办法能够获得。
private int mBufferSizeInBytes;
// 声明 AudioRecord 目标
private AudioRecord mAudioRecord = null;
1.2 获取buffer的巨细并创立AudioRecord
//初始化数据,核算最小缓冲区
mBufferSizeInBytes = AudioRecord.getMinBufferSize(mSampleRateInHz, mChannelConfig, mAudioFormat);
//创立AudioRecorder目标mAudioRecord = new AudioRecord(mAudioSource, mSampleRateInHz, mChannelConfig,
mAudioFormat, mBufferSizeInBytes);
1.3 创立一个子线程开启线程录音,并写入文件文件
@Override
public void run() {
//标记为开端收集状况
isRecording = true;
//创立文件
createFile();
try {
//判别AudioRecord未初始化,中止录音的时分开释了,状况就为STATE_UNINITIALIZED
if (mAudioRecord.getState() == mAudioRecord.STATE_UNINITIALIZED) {
initData();
}
//最小缓冲区
byte[] buffer = new byte[mBufferSizeInBytes];
//获取到文件的数据流
mDataOutputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(mRecordingFile)));
//开端录音
mAudioRecord.startRecording();
//getRecordingState获取当前AudioReroding是否正在收集数据的状况
while (isRecording && mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
int bufferReadResult = mAudioRecord.read(buffer, 0, mBufferSizeInBytes);
for (int i = 0; i < bufferReadResult; i++) {
mDataOutputStream.write(buffer[i]);
}
}
} catch (Exception e) {
Log.e(TAG, "Recording Failed");
} finally {
// 中止录音
stopRecord();
IOUtil.close(mDataOutputStream);
}
}
1.4 权限和收集小结
留意:权限需求:WRITE_EXTERNAL_STORAGE、RECORD_AUDIO 到现在根本的录音的流程就介绍完了,可是这时分问题来了:
- 我按照流程,把音频数据都输出到文件里边了,中止录音后,翻开此文件,发现不能播映,到底是为什么呢?
答:按照流程走完了,数据是进去了,可是现在的文件里边的内容仅仅是最原始的音频数据,术语称为raw(中文解说是“原材料”或“未经处理的东西”),这时分,你让播映器去翻开,它既不知道保存的格局是什么,又不知道怎么进行解码操作。当然播映不了。
- 那怎么才干在播映器中播映我录制的内容呢?
答: 在文件的数据最初参加AAC HEAD 或许 AAC 数据即可,也便是文件头。只要加上文件头部的数据,播映器才干正确的知道里边的内容到底是什么,从而能够正常的解析并播映里边的内容。
PCM 、WAV、AAC 的文件头介绍
我这儿简略的介绍一下这三种的格局的根本介绍,详细我增加了详细的访问链接,详细点击详情查看,我这儿点到为止。
PCM: PCM(Pulse Code Modulation—-脉码调制录音)。所谓PCM录音便是将声响等模拟信号变成符号化的脉冲列,再予以记录。PCM信号是由[1]、[0]等符号构成的数字信号,而未经过任何编码和紧缩处理。与模拟信号比,它不易受传送体系的杂波及失真的影响。动态规模宽,可得到音质相当好的影响效果。
WAV : wav是一种无损的音频文件格局,WAV符合 PIFF(Resource Interchange File Format)规范。一切的WAV都有一个文件头,这个文件头音频流的编码参数。WAV对音频流的编码没有硬性规定,除了PCM之外,还有简直一切支撑ACM规范的编码都能够为WAV的音频流进行编码。 简略来说:WAV 是一种无损的音频文件格局,PCM是没有紧缩的编码办法
AAC : AAC(Advanced Audio Coding),中文称为“高级音频编码”,呈现于1997年,基于 MPEG-2的音频编码技能。由Fraunhofer IIS、杜比实验室、AT&T、Sony(索尼)等公司共同开发,意图是取代MP3格局。2000年,MPEG-4规范呈现后,AAC 重新集成了其特性,参加了SBR技能和PS技能,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。他是一种专为声响数据设计的文件紧缩格局,与Mp3相似。利用AAC格局,可使声响文件明显减小,而不会让人感觉声响质量有所降低 。
PCM 转化为 WAV
在文件的数据最初参加WAVE HEAD 或许 AAC 数据即可,也便是文件头。只要加上文件头部的数据,播映器才干正确的知道里边的内容到底是什么,从而能够正常的解析并播映里边的内容。详细的头文件的描述,在Play a WAV file on an AudioTrack里边能够进行了解。
public class WAVUtil {
/**
* PCM文件转WAV文件
*
* @param inPcmFilePath 输入PCM文件途径
* @param outWavFilePath 输出WAV文件途径
* @param sampleRate 采样率,例如44100
* @param channels 声道数 单声道:1或双声道:2
* @param bitNum 采样位数,8或16
*/
public static void convertPcm2Wav(String inPcmFilePath, String outWavFilePath, int sampleRate,int channels, int bitNum) {
FileInputStream in = null;
FileOutputStream out = null;
byte[] data = new byte[1024];
try {
//采样字节byte率
long byteRate = sampleRate * channels * bitNum / 8;
in = new FileInputStream(inPcmFilePath);
out = new FileOutputStream(outWavFilePath);
//PCM文件巨细
long totalAudioLen = in.getChannel().size();
//总巨细,因为不包括RIFF和WAV,所以是44 - 8 = 36,在加上PCM文件巨细
long totalDataLen = totalAudioLen + 36;
writeWaveFileHeader(out, totalAudioLen, totalDataLen, sampleRate, channels, byteRate);
int length = 0;
while ((length = in.read(data)) > 0) {
out.write(data, 0, length);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
IOUtil.close(in,out);
}
}
/**
* 输出WAV文件
*
* @param out WAV输出文件流
* @param totalAudioLen 整个音频PCM数据巨细
* @param totalDataLen 整个数据巨细
* @param sampleRate 采样率
* @param channels 声道数
* @param byteRate 采样字节byte率
* @throws IOException
*/
private static void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,long totalDataLen, int sampleRate, int channels, long byteRate) throws IOException {
byte[] header = new byte[44];
header[0] = 'R'; // RIFF
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
header[4] = (byte) (totalDataLen & 0xff);//数据巨细
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);
header[8] = 'W';//WAVE
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
//FMT Chunk
header[12] = 'f'; // 'fmt '
header[13] = 'm';
header[14] = 't';
header[15] = ' ';//过渡字节
//数据巨细
header[16] = 16;
// 4 bytes: size of 'fmt ' chunk
header[17] = 0;
header[18] = 0;
header[19] = 0;
//编码办法 10H为PCM编码格局
header[20] = 1; // format = 1
header[21] = 0;
//通道数
header[22] = (byte) channels;
header[23] = 0;
//采样率,每个通道的播映速度
header[24] = (byte) (sampleRate & 0xff);
header[25] = (byte) ((sampleRate >> 8) & 0xff);
header[26] = (byte) ((sampleRate >> 16) & 0xff);
header[27] = (byte) ((sampleRate >> 24) & 0xff);
//音频数据传送速率,采样率*通道数*采样深度/8
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
// 确认体系一次要处理多少个这样字节的数据,确认缓冲区,通道数*采样位数
header[32] = (byte) (channels * 16 / 8);
header[33] = 0;
//每个样本的数据位数
header[34] = 16;
header[35] = 0;
//Data chunk
header[36] = 'd';//data
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
out.write(header, 0, 44);
}
}
看到下图咱们生成了相对的 wav 文件,咱们用用本机自带播映器翻开此刻就能正常播映,可是咱们发现他的巨细比较大,咱们看到便是几分钟就这么大,咱们平常用的是 mp3 、aac 格局的,咱们怎么办到的呢,这儿咱们继续看一下 mp3 格局怎么能生成 。
PCM 转化为 AAC 文件格局
生成 aac 文件播映
public class AACUtil {
...
/**
* 初始化AAC编码器
*/ private void initAACMediaEncode() {
try {
//参数对应-> mime type、采样率、声道数
MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 16000, 1);
encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 64000);//比特率
encodeFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
encodeFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, AudioFormat.CHANNEL_IN_MONO);
encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1024);//作用于inputBuffer的巨细
mediaEncode = MediaCodec.createEncoderByType(encodeType);
mediaEncode.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (IOException e) {
e.printStackTrace();
}
if (mediaEncode == null) {
LogUtil.e("create mediaEncode failed");
return;
}
mediaEncode.start();
encodeInputBuffers = mediaEncode.getInputBuffers();
encodeOutputBuffers = mediaEncode.getOutputBuffers();
encodeBufferInfo = new MediaCodec.BufferInfo();
}
private boolean codeOver = false;
/**
* 开端转码
* 音频数据{@link #srcPath}先解码成PCM PCM数据在编码成MediaFormat.MIMETYPE_AUDIO_AAC音频格局
* mp3->PCM->aac
*/ public void startAsync() {
LogUtil.w("start");
new Thread(new DecodeRunnable()).start();
}
/**
* 解码{@link #srcPath}音频文件 得到PCM数据块
*
* @return 是否解码完一切数据
*/
private void srcAudioFormatToPCM() {
File file = new File(srcPath);// 指定要读取的文件
FileInputStream fio = null;
try {
fio = new FileInputStream(file);
byte[] bb = new byte[1024];
while (!codeOver) {
if (fio.read(bb) != -1) {
LogUtil.e("============ putPCMData ============" + bb.length);
dstAudioFormatFromPCM(bb);
} else {
codeOver = true;
}
}
fio.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private byte[] chunkAudio = new byte[0];
/**
* 编码PCM数据 得到AAC格局的音频文件
*/ private void dstAudioFormatFromPCM(byte[] pcmData) {
int inputIndex;
ByteBuffer inputBuffer;
int outputIndex;
ByteBuffer outputBuffer;
int outBitSize;
int outPacketSize;
byte[] PCMAudio;
PCMAudio = pcmData;
encodeInputBuffers = mediaEncode.getInputBuffers();
encodeOutputBuffers = mediaEncode.getOutputBuffers();
encodeBufferInfo = new MediaCodec.BufferInfo();
inputIndex = mediaEncode.dequeueInputBuffer(0);
inputBuffer = encodeInputBuffers[inputIndex];
inputBuffer.clear();
inputBuffer.limit(PCMAudio.length);
inputBuffer.put(PCMAudio);//PCM数据填充给inputBuffer
mediaEncode.queueInputBuffer(inputIndex, 0, PCMAudio.length, 0, 0);//告诉编码器 编码
outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 0);
while (outputIndex > 0) {
outBitSize = encodeBufferInfo.size;
outPacketSize = outBitSize + 7;//7为ADT头部的巨细
outputBuffer = encodeOutputBuffers[outputIndex];//拿到输出Buffer
outputBuffer.position(encodeBufferInfo.offset);
outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
chunkAudio = new byte[outPacketSize];
addADTStoPacket(chunkAudio, outPacketSize);//增加ADTS
outputBuffer.get(chunkAudio, 7, outBitSize);//将编码得到的AAC数据 取出到byte[]中
try {
//录制aac音频文件,保存在手机内存中
bos.write(chunkAudio, 0, chunkAudio.length);
bos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} outputBuffer.position(encodeBufferInfo.offset);
mediaEncode.releaseOutputBuffer(outputIndex, false);
outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 0);
}
}
/**
* 增加ADTS头
*
* @param packet
* @param packetLen
*/
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; // AAC LC
int freqIdx = 8; // 16KHz
int chanCfg = 1; // CPE
// fill in ADTS data
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF1;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}
/**
* 开释资源
*/
public void release() {
...
}
/**
* 解码线程
*/
private class DecodeRunnable implements Runnable {
@Override
public void run() {
srcAudioFormatToPCM();
}
}
}
AudioStack 播映
AudioTrack 类能够完结Android平台上音频数据的输出任务。AudioTrack有两种数据加载形式(MODE_STREAM和MODE_STATIC),对应的是数据加载形式和音频流类型, 对应着两种完全不同的运用场景。
MODE_STREAM: 在这种形式下,经过write一次次把音频数据写到AudioTrack中。这和平常经过write体系调用往文件中写数据相似,但这种工作办法每次都需求把数据从用户供给的Buffer中拷贝到AudioTrack内部的Buffer中,这在必定程度上会使引入延时。为解决这一问题,AudioTrack就引入了第二种形式。
MODE_STATIC: 这种形式下,在play之前只需求把一切数据经过一次write调用传递到AudioTrack中的内部缓冲区,后续就不必再传递数据了。这种形式适用于像铃声这种内存占用量较小,延时要求较高的文件。但它也有一个缺陷,便是一次write的数据不能太多,不然体系无法分配足够的内存来存储悉数数据。
播映声响能够用MediaPlayer和AudioTrack,两者都供给了Java API供运用开发者运用。尽管都能够播映声响,但两者还是有很大的区别的,其中最大的区别是MediaPlayer能够播映多种格局的声响文件,例如MP3,AAC,WAV,OGG,MIDI等。MediaPlayer会在framework层创立对应的音频解码器。而AudioTrack只能播映已经解码的PCM流,如果对比支撑的文件格局的话则是AudioTrack只支撑wav格局的音频文件,因为wav格局的音频文件大部分都是PCM流。AudioTrack不创立解码器,所以只能播映不需求解码的wav文件。
3.1 音频流的类型
在AudioTrack构造函数中,会接触到AudioManager.STREAM_MUSIC这个参数。它的含义与Android体系对音频流的办理和分类有关。
Android将体系的声响分为好几种流类型,下面是几个常见的:
STREAM_ALARM:警告声STREAM_MUSIC:音乐声,例如music等STREAM_RING:铃声STREAM_SYSTEM:体系声响,例如低电提示音,锁屏音等STREAM_VOCIE_CALL:通话声
留意:上面这些类型的区分和音频数据本身并没有关系。例如MUSIC和RING类型都能够是某首MP3歌曲。别的,声响流类型的挑选没有固定的规范,例如,铃声预览中的铃声能够设置为MUSIC类型。音频流类型的区分和Audio体系对音频的办理战略有关。
3.2 Buffer分配和Frame的概念
在核算Buffer分配的巨细的时分,咱们常常用到的一个办法便是:getMinBufferSize。这个函数决议了运用层分配多大的数据Buffer。
AudioTrack.getMinBufferSize(8000,//每秒8K个采样点
AudioFormat.CHANNEL_CONFIGURATION_STEREO,//双声道
AudioFormat.ENCODING_PCM_16BIT);
从AudioTrack.getMinBufferSize开端追溯代码,能够发现在底层的代码中有一个很重要的概念:Frame(帧)。Frame是一个单位,用来描述数据量的多少。1单位的Frame等于1个采样点的字节数声道数(比如PCM16,双声道的1个Frame等于22=4字节)。1个采样点只针对一个声道,而实际上或许会有一或多个声道。因为不能用一个独立的单位来表明悉数声道一次采样的数据量,也就引出了Frame的概念。Frame的巨细,便是一个采样点的字节数声道数。别的,在目前的声卡驱动程序中,其内部缓冲区也是采用Frame作为单位来分配和办理的。
getMinBufSize会综合考虑硬件的情况(比如是否支撑采样率,硬件本身的延迟情况等)后,得出一个最小缓冲区的巨细。一般咱们分配的缓冲巨细会是它的整数倍。
3.3 构建进程
每一个音频流对应着一个AudioTrack类的一个实例,每个AudioTrack会在创立时注册到 AudioFlinger中,由AudioFlinger把一切的AudioTrack进行混合(Mixer),然后输送到AudioHardware中进行播映,目前Android一起最多能够创立32个音频流,也便是说,Mixer最多会一起处理32个AudioTrack的数据流。
3.4 Show Me The Code
public class AudioTrackManager {
...
//音频流类型
private static final int mStreamType = AudioManager.STREAM_MUSIC;
//指定采样率 (MediaRecoder 的采样率一般是8000Hz AAC的一般是44100Hz。 设置采样率为44100,目前为常用的采样率,官方文档表明这个值能够兼容一切的设置)
private static final int mSampleRateInHz = 44100;
//指定捕获音频的声道数目。在AudioFormat类中指定用于此的常量
private static final int mChannelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO; //单声道
//指定音频量化位数 ,在AudioFormaat类中指定了以下各种或许的常量。一般咱们挑选ENCODING_PCM_16BIT和ENCODING_PCM_8BIT PCM代表的是脉冲编码调制,它实际上是原始音频样本。
//因而能够设置每个样本的分辨率为16位或许8位,16位将占用更多的空间和处理才能,表明的音频也愈加接近真实。
private static final int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
//指定缓冲区巨细。调用AudioRecord类的getMinBufferSize办法能够获得。
private int mMinBufferSize;
//STREAM的意思是由用户在运用程序经过write办法把数据一次一次得写到audiotrack中。这个和咱们在socket中发送数据一样,
// 运用层从某个当地获取数据,例如经过编解码得到PCM数据,然后write到audiotrack。
private static int mMode = AudioTrack.MODE_STREAM;
private void initData() {
//根据采样率,采样精度,单双声道来得到frame的巨细。
mMinBufferSize = AudioTrack.getMinBufferSize(mSampleRateInHz, mChannelConfig, mAudioFormat);//核算最小缓冲区
//留意,按照数字音频的常识,这个算出来的是一秒钟buffer的巨细。
//创立AudioTrack
mAudioTrack = new AudioTrack(mStreamType, mSampleRateInHz, mChannelConfig,
mAudioFormat, mMinBufferSize, mMode);
}
/**
* 发动播映线程
*/
private void startThread() {
destroyThread();
isStart = true;
if (mRecordThread == null) {
mRecordThread = new Thread(recordRunnable);
mRecordThread.start();
}
}
/**
* 播映线程
*/
private Runnable recordRunnable = new Runnable() {
@Override
public void run() {
try {
//设置线程的优先级
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
byte[] tempBuffer = new byte[mMinBufferSize];
int readCount = 0;
while (mDis.available() > 0) {
readCount = mDis.read(tempBuffer);
if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
continue;
}
//一边播映一边写入语音数据
if (readCount != 0 && readCount != -1) {
//判别AudioTrack未初始化,中止播映的时分开释了,状况就为STATE_UNINITIALIZED
if (mAudioTrack.getState() == mAudioTrack.STATE_UNINITIALIZED) {
initData();
}
mAudioTrack.play();
mAudioTrack.write(tempBuffer, 0, readCount); }
}
//播映完就中止播映
stopPlay();
} catch (Exception e) {
e.printStackTrace();
}
}
};
/**
* 发动播映
*
* @param path
*/
public void startPlay(String path) {
try {
setPath(path);
startThread();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 中止播映
*/
public void stopPlay() {
try {
destroyThread();//销毁线程
if (mAudioTrack != null) {
if (mAudioTrack.getState() == AudioRecord.STATE_INITIALIZED) {//初始化成功
mAudioTrack.stop();//中止播映
}
if (mAudioTrack != null) {
mAudioTrack.release();//开释audioTrack资源
}
}
if (mDis != null) {
mDis.close();//关闭数据输入流
}
} catch (Exception e) {
e.printStackTrace();
}
}
}