Android视频编辑器(五)音频编解码、从视频中分离音频、音频混音、音频音量调节等

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qqchenjian318/article/details/78586371

前言

      这篇博客,主要讲解的是android端的音频处理,在开发Android视频编辑器的时候,有一个非常重要的点就是音频的相关处理。比如如何从视频中分离音频(保存为mp3文件),然后分离出来的音频如何单声道和双声道互转,还有就是如果把两个音频文件合并为一个音频文件(音频混音),以及如何调节音频的原始大小。那这些功能的运用场景做哪里呢?比如如果我们想给视频文件增加要给bgm,那如果保留原声的情况下,就需要用到音频的混音,将原声和bgm合并为一个音频文件,然后两种声音的相对大小我们肯定也是需要可以调节的,比如是bgm声音大一点还是原声音量大一点。这里还会存在另外一个问题,就是 双声道 和单声道的音频不能直接混音,所以我们也需要学习 双声道 和单声道互转。其实类似的功能软件做电脑端已经有很多,而我们这里就是探究他的实现原理,从而开发出android端的音频处理功能。下面我们就一一来实现这些功能。
    这篇博客的重点内容包括android平台的音频编解码、音频的一些基础知识、归一化混音算法等。
    本系列的文章包括如下:
       5、android视频编辑器之音频编解码、从视频中分离音频、音频混音、音频音量调节等
       6、android视频编辑器之通过OpenGL做不同视频的拼接
       7、android视频编辑器之音视频裁剪、增加背景音乐等

从视频文件中分离出音频文件

    首先,我们来实现我们的第一个功能,从视频文件中把音频文件分离出来,我们的目标是从一个完整的视频中,分离出他的音频并且保存为aac文件或者mp3文件。首先在android音视频的处理相关类中,我们需要有两个很重要的类,一个是音视频的分离器MediaExtractor 另一个就是音视频的混合器MediaMuxer。
    
     MediaExtractor:可以从当前的视频文件中读取到音视频相关的信息(音视频的编码格式等),并且逐帧读取文件中的音视频数据。
     MediaMuxer:可以将编码后的音视频数据保存为一个独立的文件,可以只写入音频数据保存为音频文件,也可以同时写入音频和视频数据,保存为一个有画面有声音的视频文件。

    要实现我们的目标,那我们已经很清楚的知道一个大致的过程了,那就是从MediaExtractor从视频中分离出音频数据(编码好的),通过MediaMuxer,将分离出来的音频数据保存为一个音频文件。

    首先需要初始化一个分离器MediaExtractor,并且给他设置文件,拿到音频的format和信道
MediaExtractor extractor = new MediaExtractor();
int audioTrack = -1;
boolean hasAudio = false;
try {
     extractor.setDataSource(videoPath);
     for (int i = 0; i < extractor.getTrackCount(); i++) {
          MediaFormat trackFormat = extractor.getTrackFormat(i);
          String mime = trackFormat.getString(MediaFormat.KEY_MIME);
          if (mime.startsWith("audio/")) {
                audioTrack = i;
                hasAudio = true;
                break;
          }
      }
     上段代码的意思,就是初始化了一个音视频分离器,然后通过setDataSource给他设置了数据源,然后遍历该数据源的所有信道,如果他KEY_MIME是“audio/”开头的,说明他是音频的信道,也就是我们所需要的音频数据所在的信道。然后我们记录下audioTrack。然后我们需要选中音频信道
  if (hasAudio) {
         extractor.selectTrack(audioTrack);
         ...
  }
     接下来,我们就需要初始化一个混合器MediaMuxer了  
     MediaMuxer mediaMuxer = new MediaMuxer(audioSavePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
     MediaFormat trackFormat = extractor.getTrackFormat(audioTrack);
     int writeAudioIndex = mediaMuxer.addTrack(trackFormat);
     mediaMuxer.start();
     ByteBuffer byteBuffer = ByteBuffer.allocate(trackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE));
     MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    上面的代码中,我们就初始化了一个MediaMuxer,并且设置了输出文件的位置和输出的格式,并且初始化了一个buffer缓冲区和一个用来保存视频信息的BufferInfo, 然后我们可以开始读取和写入数据了
 extractor.readSampleData(byteBuffer, 0);
 if (extractor.getSampleFlags() == MediaExtractor.SAMPLE_FLAG_SYNC) {
          extractor.advance();
 }
 while (true) {
          int readSampleSize = extractor.readSampleData(byteBuffer, 0);
          Log.e("hero","---读取音频数据,当前读取到的大小-----:::"+readSampleSize);
          if (readSampleSize < 0) {
                    break;
           }

           bufferInfo.size = readSampleSize;
           bufferInfo.flags = extractor.getSampleFlags();
           bufferInfo.offset = 0;
           bufferInfo.presentationTimeUs = extractor.getSampleTime();
           Log.e("hero","----写入音频数据---当前的时间戳:::"+extractor.getSampleTime());

           mediaMuxer.writeSampleData(writeAudioIndex, byteBuffer, bufferInfo);
           extractor.advance();//移动到下一帧
  }
  mediaMuxer.release();
  extractor.release();

     上面的代码,简单的说就是通过readSampleData从分离器中读取数据,如果没有读到说明分离完成了,就break掉,如果有数据,就设置当前帧的bufferInfo,比如偏移量,标志,时间戳,当前帧大小等,然后把当前帧数据和帧信息写入到混合器MediaMuxer中,然后开始读取下一帧。
    这样通过一个死循环,就可以读取到媒体文件中我们指定信道的所有数据,并且通过混合器保存为一个单独的文件了,这样就可以在android平台将视频中的音视频进行分离而且不需要编解码。

音频文件转PCM数据

    在上面我们已经从视频中分离出了音频文件,接下来,我们要将音频文件进行解码,还原成原始的PCM数据。将音频进行解码的方式有很多,但是大多都是c/c++的方式,比如ffmpeg等,不过,在android中,4.0之后的版本已经支持音视频的硬编码了,所以我们这里用MediaCodec对音频进行解码,保存成PCM文件。MediaCodec是android平台对音视频进行编解码的非常重要的一个类。我们的基本流程是从MediaExtacror里面里面读取音频信道的数据,给到音频解码器进行解码,然后将解码出来的数据写到文件里面。
    所谓的PCM文件,其实就是音频在系统中保存的原始音频数据,没有经过编码的,而我们常见的mp3,aac等是经过编码的音频数据。
    我们要解码音频,首先需要一个分离器,MediaExtactor,哈哈是不是很眼熟,是的,我们刚才才使用了这个类,他不仅仅能读取视频文件,也可以读取单个音频文件,而且用法和上面分离音频差别不大,那么首先我们初始化一个,并且给他设置数据源。
     MediaExtractor extractor = new MediaExtractor();
     int audioTrack = -1;
     boolean hasAudio = false;
     try {
         extractor.setDataSource(audioPath);
         for (int i = 0; i < extractor.getTrackCount(); i++) {
             MediaFormat trackFormat = extractor.getTrackFormat(i);
             String mime = trackFormat.getString(MediaFormat.KEY_MIME);
             if (mime.startsWith("audio/")) {
                    audioTrack = i;
                    hasAudio = true;
                    break;
              }
          }
          if (hasAudio) {
              extractor.selectTrack(audioTrack);
               ...
          }
     同样,因为我们是要解码音频数据,所以就拿到音频的信道。

    然后,接下来就是音频解码中非常重要的一些知识了,首先初始化音频的解码器
   MediaFormat trackFormat = extractor.getTrackFormat(audioTrack);
                //初始化音频的解码器
   MediaCodec audioCodec = MediaCodec.createDecoderByType(trackFormat.getString(MediaFormat.KEY_MIME));
   audioCodec.configure(trackFormat, null, null, 0);

   audioCodec.start();

   ByteBuffer[] inputBuffers = audioCodec.getInputBuffers();
   ByteBuffer[] outputBuffers = audioCodec.getOutputBuffers();
   MediaCodec.BufferInfo decodeBufferInfo = new MediaCodec.BufferInfo();
   MediaCodec.BufferInfo inputInfo = new MediaCodec.BufferInfo();
     上面的代码,就是从分离器中拿到音频信道的MediaFormat,然后通过MediaCodec.createDecoderbyType,初始化一个相应音频格式的解码器,MediaFormat.KEY_MIME的值,其实在MediaFormat中已经列举出来了。
<Image_1>

     然后把要解码音频数据的mediaFormat通过audioCodec.configure方法进行设置, 再通过start方法开启解码器,我们提前拿到了输入的inputBuffer和输出的outputBuffer。
     然后,我们整体的解码过程就是,首先遍历解码器的所有inputBuffers,如果是可以使用的,就从分离器中读取数据,并且给到inputBuffers。
    for (int i = 0; i < inputBuffers.length; i++) {
             //遍历所以的编码器 然后将数据传入之后 再去输出端取数据
             int inputIndex = audioCodec.dequeueInputBuffer(TIMEOUT_USEC);
             if (inputIndex >= 0) {
                      /**从分离器中拿到数据 写入解码器 */
                      ByteBuffer inputBuffer = inputBuffers[inputIndex];//拿到inputBuffer
                      inputBuffer.clear();//清空之前传入inputBuffer内的数据
                      int sampleSize = extractor.readSampleData(inputBuffer, 0);//MediaExtractor读取数据到inputBuffer中

                      if (sampleSize < 0) {
                              audioCodec.queueInputBuffer(inputIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                              inputDone = true;
                       } else {

                              inputInfo.offset = 0;
                              inputInfo.size = sampleSize;
                              inputInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
                              inputInfo.presentationTimeUs = extractor.getSampleTime();

                               audioCodec.queueInputBuffer(inputIndex, inputInfo.offset, sampleSize, inputInfo.presentationTimeUs, 0);//通知MediaDecode解码刚刚传入的数据
                                extractor.advance();//MediaExtractor移动到下一取样处
                        }
                 }
     }
     如果dequeueInputbuffer拿到的inputIndex = -1,说明这个输入流不可用,如果可以用,我们就从输入流的数组中取到相应位置的输入流,然后清空之前遗留的数据,通过分离器的readSampleData读取音频数据,如果读到的数据大小小于0,说明数据已经读取完毕了,就将对应的输入流插入一个流已经结束的标志位
     MediaCodec.BUFFER_FLAG_END_OF_STREAM。大于0的话,就初始化输入流的InputInfo,包括当前数据的时间戳,偏移量等等,然后通知解码器对数据进行解码audioCodec.queueInputBuffer。并且通过extactor.advance()将分离器移动到下一个取数据的地方。
       现在,我们已经往编码器中写入了很多数据,需要从解码器的输出流中读取解码后的数据了。
    while (!decodeOutputDone) {
          int outputIndex = audioCodec.dequeueOutputBuffer(decodeBufferInfo, TIMEOUT_USEC);
          if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                    /**没有可用的解码器output*/
                    decodeOutputDone = true;
           } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                    outputBuffers = audioCodec.getOutputBuffers();
           } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    MediaFormat newFormat = audioCodec.getOutputFormat();
           } else if (outputIndex < 0) {
           } else {
                    ByteBuffer outputBuffer;
                    if (Build.VERSION.SDK_INT >= 21) {
                            outputBuffer = audioCodec.getOutputBuffer(outputIndex);
                    } else {
                            outputBuffer = outputBuffers[outputIndex];
                    }

                    chunkPCM = new byte[decodeBufferInfo.size];
                    outputBuffer.get(chunkPCM);
                    outputBuffer.clear();

                    fos.write(chunkPCM);//数据写入文件中
                    fos.flush();
             }
             audioCodec.releaseOutputBuffer(outputIndex, false);
             if ((decodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                     /**
                      * 解码结束,释放分离器和解码器
                      * */
                      extractor.release();

                      audioCodec.stop();
                      audioCodec.release();
                      codeOver = true;
                      decodeOutputDone = true;
               }
        }
        下面,我们来大致解释一下上面的这些代码,通过dequeueOutputBuffer遍历,如果当前有可以使用的输出流,就通过outputIndex拿到对应位置的OutputBuffer,并且从该输出流缓冲区,读取到byte数据,这个byte数据就是音频解码出来的最原始的音频数据,其实就是一个byte数组。现实世界的声音转化成电脑或者手机系统里面的数据的时候,其实大致过程就是通过取样以及离散化,对脉冲信号进行编码调制,最终转化成PCM格式的数据,也就是一个byte的数组,这就是所有mp3或者aac等音频格式解码出来的在系统中的最原始数据,所谓的PCM格式的音频数据其实可以简单的理解为一个byte数组,而为什么又会有mp3或者是aac的区别呢,因为原始的PCM数据的数据量是非常大的,为了便于保存和传输,就制定了许多不同的编码格式,对一些杂音或者是不重要的数据进行分离,常见的就有mp3、AAC、wav等等,而这样的编码或多或少都是有损的编码,而音质越好,他的数据量就越大,比如wav这种无损的编码格式,同样一首歌,wav格式的大小就远远大于了mp3或者是AAC。

        言归正传,在我们读取到界面后的byte数据之后,我们就可以通过io,写入到一个本地文件中,从输出流中读取到数据后,一定记得释放到当前的输出缓冲区 audioCodec.releaseOutputBuffer(outputIndex, false);

        当然如果我们已经读取到了数据的末尾,就说明整个音频已经解码完毕了,我们就释放掉分离器MediaExtactor和解码器MediaCodec。
       
       通过上面两个 两个步骤,我们就完成了读取一个音频文件里面的数据,并且将数据进行解码保存成原始的PCM文件了,是不是非常简单呢?当然我们可以将上面两部分进行结合,就可以直接从一个视频文件中读取音频数据并且进行解码了。

       既然解码如此简单,那么我们怎么样才能将解码出来的PCM数据还原为一个可以播放的音频文件呢?下面这个我们就将介绍如何才能达成我们的目标。


PCM数据转音频文件

      在上面的两部分内容中我们分别实现了从一个视频文件中将音频给分离出来,以及将一个音频文件解码成最原始的PCM数据。那么现在我们来将PCM文件进行编码成一个可以正常播放的音频文件。基本思路是通过io流从PCM文件中读取数据,然后将数据送到编码器中,进行编码,然后将编码完成的数据,通过io流写到一个文件中即可
      首先,我们需要初始化一个文件读取流,从PCM文件中读取数据
    FileInputStream fis = new FileInputStream(pcmPath);
      然后,初始化编码器相关的东西
   int inputIndex;
   ByteBuffer inputBuffer;
   int outputIndex;
   ByteBuffer outputBuffer;
   byte[] chunkAudio;
   int outBitSize;
   int outPacketSize;
   //初始化编码器
   MediaFormat encodeFormat = MediaFormat.createAudioFormat("audio/mp4a-latm", 44100, 2);//mime type 采样率 声道数
   encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000);//比特率
   encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
   encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 500 * 1024);

   MediaCodec mediaEncode = MediaCodec.createEncoderByType("audio/mp4a-latm");
   mediaEncode.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
   mediaEncode.start();

   ByteBuffer[] encodeInputBuffers = mediaEncode.getInputBuffers();
   ByteBuffer[] encodeOutputBuffers = mediaEncode.getOutputBuffers();
   MediaCodec.BufferInfo encodeBufferInfo = new MediaCodec.BufferInfo();
      上面的代码可以看到,我们这里的输出音频格式和采样率,比特率等等都是写死的,但是原始的音频并不一定就是同样的配置,所以可能会存在问题,比如你解码的音频是mp3格式,而我们这里保存的是aac格式的文件,很明显的一个差别就是aac文件的大小会比原始的mp3格式的音频更小。
      接下来,我们初始化一个文件写入流,可以让我们把编码出来的音频数据写入到一个aac文件中。
  FileOutputStream fos = new FileOutputStream(new File(audioPath));
  BufferedOutputStream bos = new BufferedOutputStream(fos, 500 * 1024);
     肯定,下面就是最核心的读取数据—>给到编码器—>编码器的输出流拿数据—>通过io写入到文件中,这样一个循环中
       boolean isReadEnd = false;
       while (!isReadEnd) {
            for (int i = 0; i < encodeInputBuffers.length - 1; i++) {
                  if (fis.read(buffer) != -1) {
                        allAudioBytes = Arrays.copyOf(buffer, buffer.length);
                   } else {
                        Log.e("hero", "---文件读取完成---");
                        isReadEnd = true;
                        break;
                    }
                    Log.e("hero", "---io---读取文件-写入编码器--" + allAudioBytes.length);
                    inputIndex = mediaEncode.dequeueInputBuffer(-1);
                    inputBuffer = encodeInputBuffers[inputIndex];
                    inputBuffer.clear();//同解码器
                    inputBuffer.limit(allAudioBytes.length);
                    inputBuffer.put(allAudioBytes);//PCM数据填充给inputBuffer
                    mediaEncode.queueInputBuffer(inputIndex, 0, allAudioBytes.length, 0, 0);//通知编码器 编码
                }
                outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);//同解码器
                while (outputIndex >= 0) {
                    //从编码器中取出数据
                    outBitSize = encodeBufferInfo.size;
                    outPacketSize = outBitSize + 7;//7为ADTS头部的大小
                    outputBuffer = encodeOutputBuffers[outputIndex];//拿到输出Buffer
                    outputBuffer.position(encodeBufferInfo.offset);
                    outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
                    chunkAudio = new byte[outPacketSize];
                    AudioCodec.addADTStoPacket(chunkAudio, outPacketSize);//添加ADTS 代码后面会贴上
                    outputBuffer.get(chunkAudio, 7, outBitSize);//将编码得到的AAC数据 取出到byte[]中 偏移量offset=7 你懂得
                    outputBuffer.position(encodeBufferInfo.offset);
                    Log.e("hero", "--编码成功-写入文件----" + chunkAudio.length);
                    bos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 将文件保存到内存卡中 *.aac
                    bos.flush();

                    mediaEncode.releaseOutputBuffer(outputIndex, false);
                    outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);
                }
            }
            mediaEncode.stop();
            mediaEncode.release();
            fos.close();

      上面的代码还是比较容易懂的,但是有个值得注意的点就是,首先我们每次从文件中读取的byte的大小是有限制的,你不能一下子读太多数据,如果一下子给太多数据编码器会卡死,我们这里限制的是8 * 1024 ,然后有一个就是addADTStopacket方法。
   /**
     * 写入ADTS头部数据
     * */
    public static void addADTStoPacket(byte[] packet, int packetLen) {
        int profile = 2; // AAC LC
        int freqIdx = 4; // 44.1KHz
        int chanCfg = 2; // CPE

        packet[0] = (byte) 0xFF;
        packet[1] = (byte) 0xF9;
        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;
    }
      这是音频编码中非常重要的一个写入ADTS头部数据的操作,具体的原因,请查阅相关资料,我们这里就不多说了,其实大概就是用几个字符来表示我们音频的格式,采样率等等信息。
      通过上面几个步骤,我们就完成了将一个很大的音频的原始数据PCM进行编码,然后写入到一个音频文件里面的操作。
      可能有童鞋就会问了,那你这样把音频解码—>PCM—>编码成音频,有什么意义呢?其实这些非常有意义的事情,比如基于上面的这些功能,我们就可以完成很多的音频操作的功能了,比如我们可以把两个音频分别进行解码,然后依次读取PCM文件中的数据进行编码,从而实现两个甚至多个音频的连接,或者是音频的变声功能,将正常的声音进行变调,从而实现变声器的功能,再或者是将两个音频进行混音,也就是基于同一时间戳进行播放。
      看到音频混音,可能有童鞋就不太明白要怎么实现了,那么下面的这部分内容,我们就来讲解音频混音的核心原理。

音频的混音(单声道和双声道的区别)

      这部分,我们来讲解一些音频混音的核心原理,所谓音频混音就是将两个甚至多个音频基于同一个时间戳,同时进行播放,在android中可以通过实例化多个音频播放器来实现,但是如何能编码成一个文件呢?
      在上面讲解PCM部分知识的时候,我们已经了解到了其实声音在计算机系统中的存在形式其实就是一系列的byte数据,至于这些byte数据是如何通过离散数学转换来的,我们这里就不进行深入研究了。但是我们可以明白的是,我们都拿到最原始的byte数据了,那么自然可以对他进行处理了,而所谓的音频混音,其实就是一种处理。这样的音频处理算法其实在c/c++上面已经有非常多的很好的实现了,这不过我们这里用java来实现一遍。
       其实音频混音的核心原理就是将两个音频的原始byte数据进行叠加,非常简单的 + 起来,比如某个位置的数据是1 而另一个音频同样位置是2 加起来就是3,这样就完成了音频的混音,当然这是最基础也是最垃圾的混音算法,我们这里会介绍其中的一种混音算法,基本上可以达到商业使用的。那就是归一化混音算法。
       他的基本公式是
   C = A + B - A * B / (数据类型的最大值);
   byte数据就是
   C = A + B - A * B / 127;
   short数据就是
   C = A + B - A * B / 32767;
       为什么要进行 后面的减去操作呢?如果你使用byte进行数据操作的话,非常容易就出现数据大于最大值,而这样的公式就是为了避免这样的情况出现,另外无法避免时,需要进行归一化,也就是如果超出了范围,就取最大值,如果小于了范围就取最小值
       具体的实现,如下
    /**
     * 归一化混音
     * */
    public static byte[] normalizationMix(byte[][] allAudioBytes){
        if (allAudioBytes == null || allAudioBytes.length == 0)
            return null;

        byte[] realMixAudio = allAudioBytes[0];

        //如果只有一个音频的话,就返回这个音频数据
        if(allAudioBytes.length == 1)
            return realMixAudio;

        //row 有几个音频要混音
        int row = realMixAudio.length /2;
        //
        short[][] sourecs = new short[allAudioBytes.length][row];
        for (int r = 0; r < 2; ++r) {
            for (int c = 0; c < row; ++c) {
                sourecs[r][c] = (short) ((allAudioBytes[r][c * 2] & 0xff) | (allAudioBytes[r][c * 2 + 1] & 0xff) << 8);
            }
        }

        //coloum第一个音频长度 / 2
        short[] result = new short[row];
        //转成short再计算的原因是,提供精确度,高端的混音软件据说都是这样做的,可以测试一下不转short直接计算的混音结果
        for (int i = 0; i < row; i++) {
            int a = sourecs[0][i] ;
            int b = sourecs[1][i] ;
            if (a <0 && b<0){
                int i1 = a  + b  - a  * b / (-32768);
                if (i1 > 32767){
                    result[i] = 32767;
                }else if (i1 < - 32768){
                    result[i] = -32768;
                }else {
                    result[i] = (short) i1;
                }
            }else if (a > 0 && b> 0){
                int i1 = a + b - a  * b  / 32767;
                if (i1 > 32767){
                    result[i] = 32767;
                }else if (i1 < - 32768){
                    result[i] = -32768;
                }else {
                    result[i] = (short) i1;
                }
            }else {
                int i1 = a + b ;
                if (i1 > 32767){
                    result[i] = 32767;
                }else if (i1 < - 32768){
                    result[i] = -32768;
                }else {
                    result[i] = (short) i1;
                }
            }
        }
        return toByteArray(result);
    }
    public static byte[] toByteArray(short[] src) {
        int count = src.length;
        byte[] dest = new byte[count << 1];
        for (int i = 0; i < count; i++) {
            dest[i * 2 +1] = (byte) ((src[i] & 0xFF00) >> 8);
            dest[i * 2] = (byte) ((src[i] & 0x00FF));
        }
        return dest;
    }
    上面代码,就是一个归一化混音算法的java实现,而我们在过程中将原始是byte数据转换成了short数据的原因就是为了提高精度,从而让混音效果更好。核心原理就是上面的公式。
     当然因为音频的原始数据其实是非常多的,为了提升效率,最好使用jni实现混音相关算法,这样就可以实现一个效果较好的混音算法了。项目里面已添加相关实现,可以进行测试查阅。
     但是混音时,有一个问题需要注意一下,就是音频是存在单声道和双声道,立体声的区别的。我们读取音频的信息的时候,可以看到他们是哪种声道的,单声道(mono),双声道(stereo),其实stereo应该叫立体声,但是我查阅资料得到的信息是,大部分的android手机其实是不支持立体声录音的,android平台的很多立体声其实只是单纯的双声道,因为这涉及到非常底层的知识了,我也不太了解这一点,有知道的朋友,还望不惜赐教。这里在进行混音的时候,不同的声道会出现问题,因为不同的声道数据同样的时间戳,播放的数据量是不同的,但是我们这里混音是按照数据量来混的,所以一个单声道和一个多声道的音频直接混音的话,就会出现混音失败。那么如何解决这个问题呢?
     其实我们可以通过将mono转成stereo的方法来解决这个问题,居然的实现很简单,如下代码
  for (int i = 0; i < monoBytes.length; i += 2) {
            stereoBytes[i*2+0] = monoBytes[i];
            stereoBytes[i*2+1] = monoBytes[i+1];
            stereoBytes[i*2+2] = monoBytes[i];
            stereoBytes[i*2+3] = monoBytes[i+1];
  }
    

音频的音量调节

     在上面,我们实现了音频的混音,但是如果我们想要调节音频原始音量的大小(不是通过手机音量键调节),我们应该怎么做呢,比如如果要让你实现给视频增加bgm的时候,你bgm的音量不能太大以至于原视频声音听不见了。其实所谓音量调节,还是很简单,就是原始byte数据 乘上一定范围内的数值,即可实现该功能。
     公式
    C = A * vol;//vol的取值范围 通常是小于10,大于0的,如果是0的话,就没有声音了,如果太大了就会出现杂音
     vol的具体取值范围需要自己多去测试,当然 这是最简单的一种实现方式。

总结

    到这里的话,本篇文章就基本上结束了,我们来回顾一下主要内容,首先,我们实现了从视频里面分离出音频文件,然后将音频文件解码成最原始的PCM数据,再通过android平台的硬编码,将PCM数据的文件重新编码成一个可以播放的音频文件,接下来主要是讲解了一些音频混音的一些知识,实现了一个归一化混音算法,然后说了一下不同声道音频的需要注意的一些问题,最后实现了一个改变原始音频音量大小的功能。
    那么,下一篇,按照计划,我们将要通过OpenGL实现android平台的视频拼接功能,将不同的视频完美的拼接在一起,当然并不是像音频这样的混音,而是在第一个视频播放完毕之后,接着播放第二个视频这种,而不是一个画面中播放两个视频。
    因为个人水平有限,难免有错误和不足之处,还望大家能包涵和提醒。谢谢啦!!!

其他
     相关代码都已经更新到github上面了,项目的github地址,麻烦顺手给个star,谢谢啦~




猜你喜欢

转载自blog.csdn.net/qqchenjian318/article/details/78586371