音视频入门之音频采集、编码、播放

news2024/9/21 10:51:19

作者:花海blog

今天我们学习音频的采集、编码、生成文件、转码等操作,我们生成三种格式的文件格式,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 到现在基本的录音的流程就介绍完了,但是这时候问题来了:

1.我按照流程,把音频数据都输出到文件里面了,停止录音后,打开此文件,发现不能播放,到底是为什么呢?

答:按照流程走完了,数据是进去了,但是现在的文件里面的内容仅仅是最原始的音频数据,术语称为raw(中文解释是“原材料”或“未经处理的东西”),这时候,你让播放器去打开,它既不知道保存的格式是什么,又不知道如何进行解码操作。当然播放不了。

2.那如何才能在播放器中播放我录制的内容呢?

答: 在文件的数据开头加入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等于2×2=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();
        }
    }
}

目前市场上对于音视频人才的需求,市场招聘音视频岗位薪资亦是水涨船高,但音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的。只能通过一点点的学习和积累把这块的知识串联积累起来。

这里给大家推荐一套学习路线,并附有相关《音视频开发核心知识点笔记》及《音视频源码解析》和配套视频,相信可以给大家提供一些帮助,都已经进行了整理好了:https://qr18.cn/Ei3VPD

音视频初级入门:https://qr18.cn/Ei3VPD

音视频初级入门主要是接触Android多媒体展示相关的API,通过单独的列举和使用这些API,对Android音视频处理有一个基本的轮廓,虽然知识点相对来说是比较散的,但是点成线、线成面,基本的基础掌握了,通过学习Android音视频核心的API将音视频的流程串联起来,这样对于音视频的了解和控制就不仅仅局限于最外层的API了,而是能够通过相对底层的方式来加深对Android 音视频开发的认知。

音视频中级进阶:OpenSL ES 学习

学习 Android 平台 OpenSL ES API,了解 OpenSL 开发的基本流程,使用OpenSL播放PCM数据,并了解相关API的简单使用

音视频高级探究:https://qr18.cn/Ei3VPD

音视频编解码技术

流媒体协议

多媒体文件格式

FFmpeg 学习:https://qr18.cn/Ei3VPD

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/797543.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Chat GPT】用 ChatGPT 运行 Python

前言 ChatGPT 是一个基于 GPT-2 模型的人工智能聊天机器人&#xff0c;它可以进行智能对话&#xff0c;同时还支持 Python 编程语言的运行&#xff0c;可以通过 API 接口进行调用。本文将介绍如何使用 ChatGPT 运行 Python 代码&#xff0c;并提供一个实际代码案例。 ChatGPT …

苹果mac电脑好用的pdf编辑工具PDF Expert 最新中文 v3.2.2

PDF Expert提供了丰富的PDF编辑功能&#xff0c;包括添加、删除、移动、旋转、缩放、裁剪等操作&#xff0c;以及文本、图像、链接、表格、注释等元素的添加和修改。 行云如水&#xff01;阅读PDF文档非常流畅&#xff0c;不管你的文件有多大。编辑PDF文档 以简单快速度编辑PD…

兴达易控 ETHERCAT转PROFIBUS从站网关配置方式

ETHERCAT转PROFIBUS&#xff08;XD-ECPBS20&#xff09;是一款PROFIBUS从站功能的通讯网关。ETHERCAT转PROFIBUS从站网关主要功能是将ETHERCAT设备接入到PROFIBUS网络中。 本网关连接到PROFIBUS总线中做为从站使用&#xff0c;连接到ETHERCAT总线中做为从站使用。 1、配置网关…

基于RWEQ模型的土壤风蚀模数估算及其变化归因分析教程

详情点击公众号链接&#xff1a;基于RWEQ模型的土壤风蚀模数估算及其变化归因分析 前沿 土壤风蚀是一个全球性的环境问题。中国是世界上受土壤风蚀危害最严重的国家之一&#xff0c;土壤风蚀是中国干旱、半干旱及部分湿润地区土地荒漠化的首要过程。中国风蚀荒漠化面积达160…

学习笔记20 Java Collections Framework概览

一、Lists, Sets, and Maps list按位置对元素排序&#xff0c;元素x在列表中的位置&#xff0c;也称为其索引。一个列表允许重复的元素。list通过其索引来区分相同的对象。 set是无序无重复集合。与列表不同&#xff0c;set没有其元素的位置概念。集合的实现通常针对搜索进行…

C++-----stack和queue

本期我们来学习stack和queue 目录 stack介绍 栈的使用 栈的模拟实现 queue介绍 队列的使用 队列的模拟实现 deque 优先级队列 模拟实现 仿函数 全部代码 stack介绍 1. stack 是一种容器适配器&#xff0c;专门用在具有后进先出操作的上下文环境中&#xff0c;其删除…

亚马逊测评自养号:如何正确选择学习对象与获取可靠技术知识?

亚马逊是一家知名的跨境电商平台&#xff0c;吸引了越来越多的人涉足这个领域。随着商家数量的增加&#xff0c;亚马逊的竞争力也在不断提高。在亚马逊平台上&#xff0c;产品评价对于卖家账号的评估以及产品曝光量、销量等方面具有直接影响。因此&#xff0c;对于任何一个希望…

单路 PWM 控制的高调光比 LED 降压恒流控制器

概述 OC5401 是一款单路 PWM 控制的高调光比降压恒流驱动控制器&#xff0c;PWM 调光比最高可达 10000&#xff1a;1。 OC5401 支持 16-60V 输入电压范围。 OC5401 采用电流滞环控制方式&#xff0c;无需环路补偿。 OC5401 可通过外接电阻设置 LED 输出电流&#xff0c;最…

数据库中的事务处理

MySQL的事务处理&#xff1a;只支持 lnnoDB 和BDB数据表类型 1.事务就是将一组SQL语句放在同一批次内去执行 2.如果一个SQL语句出错&#xff0c;则该批次内的所有SQL都将被取消执行 MySQL的事务实现方法一&#xff1a; select autocommit 查询当前事务提交模式 set a…

【第二套】Java面试题

第二套&#xff1a; 一、JavaScript前端开发 1、下列的代码输出什么&#xff1f; var y 1; if(function f(){}){y typeof f; } console.log(y);正确的答案应该是 1undefined。 JavaScript中if语句求值其实使用eval函数&#xff0c;eval(function f(){}) 返回 function f()…

Web前端 常用布局

Flex布局&#xff08;弹性布局&#xff09; 参考&#xff1a;滑动验证页面 space-between与space-around的区别

Python编程很简单,四步菜鸟到高手(文末送书5本)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

别在找git报错的解决方案啦,多达20条git错误解决方案助你学习工作

1. 找不到Git命令 $ sudo apt-get update $ sudo apt-get install git2. 无法克隆远程仓库 $ git clone https://github.com/username/repo.git3. 无法拉取或推送到远程仓库 $ git pull origin master $ git add . $ git commit -m "Resolve conflicts" $ git pus…

java.lang.ClassNotFoundException: sun.misc.BASE64Decoder

有一个新的应用服务&#xff0c;idea启动应用应用服务时&#xff0c;突然报错java.lang.ClassNotFoundException: sun.misc.BASE64Decoder &#xff0c;然后在网上搜索&#xff0c;说是建议使用apache包&#xff0c;该类新的JRE已经废弃&#xff0c;并从rt.jar包中移除。但是该…

Visual Components数字化工厂仿真软件 衡祖仿真

Visual Components 3D 制造仿真软件可以方便快捷地设计和验证新的生产解决方案&#xff0c;可以向用户提供快速、便捷、真实的智能制造仿真解决方案。 一、数字化工厂产线仿真 VisualComponents仿真软件将离散物流事件仿真、人机协作、机器人工艺仿真和虚拟调试集于一个平台&am…

【Matter】基于Ubuntu 22.04 编译chip-tool工具

前言 编译过程有点曲折&#xff0c;做下记录&#xff0c;过程中&#xff0c;有参考别人写的博客&#xff0c;也看github 官方介绍&#xff0c;终于跑通了~ 环境说明&#xff1a; 首先需要稳定的梯子&#xff0c;可以访问“外网”ubuntu 环境&#xff0c;最终成功实验在Ubunt…

#P1005. [NOIP2010普及组] 导弹拦截

题目描述 经过 1111年的韬光养晦&#xff0c;某国研发出了一种新的导弹拦截系统&#xff0c;凡是与它的距离不超过其工作半径的导弹都能够被它成功拦截。当工作半径为 00时&#xff0c;则能够拦截与它位置恰好相同的导弹。但该导弹拦截系统也存在这样的缺陷&#xff1a;每套系…

L2Cache 核心原理解析

源码地址&#xff1a;L2Cache L2Cache是什么 L2Cache 是一个基于内存、 Redis 、 Spring Cache 实现的满足高并发场景下的分布式二级缓存框架。 L2Cache如何使用 如何使用 L2cache L2Cache架构图 核心逻辑 1、发起 get(key) 请求2、从localCache中get缓存&#xff0c;若存…

C语言获取13位时间戳以及格式化时间戳

定义time_util.h头文件 #ifndef TIME_UTIL_H #define TIME_UTIL_H #include <stdio.h> #include <time.h> #ifdef _WIN32 #include <windows.h> #define DELTA_EPOCH_IN_MICROSECS 11644473600000000Ui64 #else #include <sys/time.h> #endifvoid ge…

DoIP学习笔记系列:(一)DoIP协议概述

文章目录 1. 为什么会有DoIP协议的需求产生?2. DoIP协议入门2.1 传输层协议和网络层服务2.2 物理层和数据链路层2.3 协议介绍2.3.1 报文封装结构2.3.2 端口号2.3.3 DoIP报文格式2.3.3.1 DoIP首部,协议版本号2.3.3.2 DoIP首部,协议版本号取反2.3.3.3 DoIP首部,负载类型2.3.3…