詳解如何使用代碼進(jìn)行音頻合成

作者:鄭童宇
GitHub:https://github.com/CrazyZty

1.前言

音頻合成在現(xiàn)實(shí)生活中應(yīng)用廣泛,在網(wǎng)上可以搜索到不少相關(guān)的講解和代碼實(shí)現(xiàn),但個(gè)人感覺(jué)在網(wǎng)上搜索到的音頻合成相關(guān)文章的講解都并非十分透徹,故而寫(xiě)下本篇博文,計(jì)劃通過(guò)講解如何使用代碼實(shí)現(xiàn)音頻合成功能從而將本人對(duì)音頻合成的理解闡述給各位,力圖讀完的各位可以對(duì)音頻合成整體過(guò)程有一個(gè)清晰的了解。
  本篇博文以Java為示例語(yǔ)言,以Android為示例平臺(tái)。
  本篇博文著力于講解音頻合成實(shí)現(xiàn)原理與過(guò)程中的細(xì)節(jié)和潛在問(wèn)題,目的是讓各位不被編碼語(yǔ)言所限制,在本質(zhì)上理解如何實(shí)現(xiàn)音頻合成的功能。

2.音頻合成

2.1.功能簡(jiǎn)介

本次實(shí)現(xiàn)的音頻合成功能參考"唱吧"的音頻合成,功能流程是:錄音生成PCM文件,接著根據(jù)錄音時(shí)長(zhǎng)對(duì)背景音樂(lè)文件進(jìn)行解碼加裁剪,同時(shí)將解碼后的音頻調(diào)制到與錄音文件相同的采樣率,采樣點(diǎn)字節(jié)數(shù),聲道數(shù),接著根據(jù)指定系數(shù)對(duì)兩個(gè)音頻文件進(jìn)行音量調(diào)節(jié)并合成為PCM文件,最后進(jìn)行壓縮編碼生成MP3文件。

2.2.功能實(shí)現(xiàn)

2.2.1.錄音

錄音功能生成的目標(biāo)音頻格式是PCM格式,對(duì)于PCM的定義,維基百科上是這么寫(xiě)到的:"Pulse-code modulation (PCM) is a method used to digitally represent sampled analog signals. It is the standard form of digital audio in computers, Compact Discs, digital telephony and other digital audio applications. In a PCM stream, the amplitude of the analog signal is sampled regularly at uniform intervals, and each sample is quantized to the nearest value within a range of digital steps.",大致意思是PCM是用來(lái)采樣模擬信號(hào)的一種方法,是現(xiàn)在數(shù)字音頻應(yīng)用中數(shù)字音頻的標(biāo)準(zhǔn)格式,而PCM采樣的原理,是均勻間隔的將模擬信號(hào)的振幅量化成指定數(shù)據(jù)范圍內(nèi)最貼近的數(shù)值。
  PCM文件存儲(chǔ)的數(shù)據(jù)是不經(jīng)壓縮的純音頻數(shù)據(jù),當(dāng)然只是這么說(shuō)可能有些抽象,我們拉上大家熟知的MP3文件進(jìn)行對(duì)比,MP3文件存儲(chǔ)的是壓縮后的音頻,PCM與MP3兩者之間的關(guān)系簡(jiǎn)單說(shuō)就是:PCM文件經(jīng)過(guò)MP3壓縮算法處理后生成的文件就是MP3文件。我們簡(jiǎn)單比較一下雙方存儲(chǔ)所消耗的空間,1分鐘的每采樣點(diǎn)16位的雙聲道的44.1kHz采樣率PCM文件大小為:1*60*16/8*2*44100/1024=10335.9375KB,約為10MB,而對(duì)應(yīng)的128kps的MP3文件大小僅為1MB左右,既然PCM文件占用存儲(chǔ)空間這么大,我們是不是應(yīng)該放棄使用PCM格式存儲(chǔ)錄音,恰恰相反,注意第一句話:"PCM文件存儲(chǔ)的數(shù)據(jù)是不經(jīng)壓縮的純音頻數(shù)據(jù)",這意味只有PCM格式的音頻數(shù)據(jù)是可以用來(lái)直接進(jìn)行聲音處理,例如進(jìn)行音量調(diào)節(jié),聲音濾鏡等操作,相對(duì)的其他的音頻編碼格式都是必須解碼后才能進(jìn)行處理(PCM編碼的WAV文件也得先讀取文件頭),當(dāng)然這不代表PCM文件就好用,因?yàn)闆](méi)有文件頭,所以進(jìn)行處理或者播放之前我們必須事先知道PCM文件的聲道數(shù),采樣點(diǎn)字節(jié)數(shù),采樣率,編碼大小端,這在大多數(shù)情況下都是不可能的,事實(shí)上就我所知沒(méi)有播放器是直接支持PCM文件的播放。不過(guò)現(xiàn)在錄音的各項(xiàng)系數(shù)都是我們定義的,所以我們就不用擔(dān)心這個(gè)問(wèn)題。
  背景知識(shí)了解這些就足夠了,下面我給出實(shí)現(xiàn)代碼,綜合代碼講解實(shí)現(xiàn)過(guò)程。


    if (recordVoice) {
        audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                Constant.RecordSampleRate, AudioFormat.CHANNEL_IN_MONO,
                pcmFormat.getAudioFormat(), audioRecordBufferSize);

        try {
            audioRecord.startRecording();
        } catch (Exception e) {
            NoRecordPermission();
            continue;
        }

        BufferedOutputStream bufferedOutputStream = FileFunction
                .GetBufferedOutputStreamFromFile(recordFileUrl);

        while (recordVoice) {
            int audioRecordReadDataSize =
                    audioRecord.read(audioRecordBuffer, 0, audioRecordBufferSize);

            if (audioRecordReadDataSize > 0) {
                calculateRealVolume(audioRecordBuffer, audioRecordReadDataSize);
                if (bufferedOutputStream != null) {
                    try {
                        byte[] outputByteArray = CommonFunction
                                .GetByteBuffer(audioRecordBuffer,
                                        audioRecordReadDataSize, Variable.isBigEnding);
                        bufferedOutputStream.write(outputByteArray);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            } else {
                NoRecordPermission();
                continue;
            }
        }

        if (bufferedOutputStream != null) {
            try {
                bufferedOutputStream.close();
            } catch (Exception e) {
                LogFunction.error("關(guān)閉錄音輸出數(shù)據(jù)流異常", e);
            }
        }

        audioRecord.stop();
        audioRecord.release();
        audioRecord = null;
    }

錄音的實(shí)際實(shí)現(xiàn)和控制代碼較多,在此僅抽出核心的錄音代碼進(jìn)行講解。在此為獲取錄音的原始數(shù)據(jù),我使用了Android原生的AudioRecord,其他的平臺(tái)基本也會(huì)提供類(lèi)似的工具類(lèi)。這段代碼實(shí)現(xiàn)的功能是當(dāng)錄音開(kāi)始后,應(yīng)用會(huì)根據(jù)設(shè)定的采樣率和聲道數(shù)以及采樣字節(jié)數(shù)來(lái)不斷從MIC中獲取原始的音頻數(shù)據(jù),然后將獲取的音頻數(shù)據(jù)寫(xiě)入到指定文件中,直至錄音結(jié)束。這段代碼邏輯比較清晰的,我就不過(guò)多講解了。
  潛在問(wèn)題的話,手機(jī)平臺(tái)上是需要申請(qǐng)錄音權(quán)限的,如果沒(méi)有錄音權(quán)限就無(wú)法生成正確的錄音文件。

2.2.2.解碼與裁剪背景音樂(lè)

如前文所說(shuō),除了PCM格式以外的所有音頻編碼格式的音頻都必須解碼后才可以處理,因此要讓背景音樂(lè)參與合成必須事先對(duì)背景音樂(lè)進(jìn)行解碼,同時(shí)為減少合成的MP3文件的大小,需要根據(jù)錄音時(shí)長(zhǎng)對(duì)解碼的音頻文件進(jìn)行裁剪。本節(jié)不會(huì)詳細(xì)解釋解碼算法,因?yàn)槊總€(gè)平臺(tái)都會(huì)有對(duì)應(yīng)封裝的工具類(lèi),直接使用即可。
  背景知識(shí)先講這些,本次功能實(shí)現(xiàn)過(guò)程中的潛在問(wèn)題較多,下面我給出實(shí)現(xiàn)代碼,綜合代碼講解實(shí)現(xiàn)過(guò)程。


    private boolean decodeMusicFile(String musicFileUrl, String decodeFileUrl, int startSecond,
                                    int endSecond,
                                    Handler handler,
                                    DecodeOperateInterface decodeOperateInterface) {
        int sampleRate = 0;
        int channelCount = 0;

        long duration = 0;

        String mime = null;

        MediaExtractor mediaExtractor = new MediaExtractor();
        MediaFormat mediaFormat = null;
        MediaCodec mediaCodec = null;

        try {
            mediaExtractor.setDataSource(musicFileUrl);
        } catch (Exception e) {
            LogFunction.error("設(shè)置解碼音頻文件路徑錯(cuò)誤", e);
            return false;
        }

        mediaFormat = mediaExtractor.getTrackFormat(0);
        sampleRate = mediaFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE) ?
                mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE) : 44100;
        channelCount = mediaFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT) ?
                mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) : 1;
        duration = mediaFormat.containsKey(MediaFormat.KEY_DURATION) ? mediaFormat.getLong
                (MediaFormat.KEY_DURATION)
                : 0;
        mime = mediaFormat.containsKey(MediaFormat.KEY_MIME) ? mediaFormat.getString(MediaFormat
                .KEY_MIME) : "";

        LogFunction.log("歌曲信息",
                "Track info: mime:" + mime + " 采樣率sampleRate:" + sampleRate + " channels:" +
                        channelCount + " duration:" + duration);

        if (CommonFunction.isEmpty(mime) || !mime.startsWith("audio/")) {
            LogFunction.error("解碼文件不是音頻文件", "mime:" + mime);
            return false;
        }

        if (mime.equals("audio/ffmpeg")) {
            mime = "audio/mpeg";
            mediaFormat.setString(MediaFormat.KEY_MIME, mime);
        }

        try {
            mediaCodec = MediaCodec.createDecoderByType(mime);

            mediaCodec.configure(mediaFormat, null, null, 0);
        } catch (Exception e) {
            LogFunction.error("解碼器configure出錯(cuò)", e);
            return false;
        }

        getDecodeData(mediaExtractor, mediaCodec, decodeFileUrl, sampleRate, channelCount,
                startSecond,
                endSecond, handler, decodeOperateInterface);
        return true;
    }

decodeMusicFile方法的代碼主要功能是獲取背景音樂(lè)信息,初始化解碼器,最后調(diào)用getDecodeData方法正式開(kāi)始對(duì)背景音樂(lè)進(jìn)行處理。
  代碼中使用了Android原生工具類(lèi)作為解碼器,事實(shí)上作為原生的解碼器,我也遇到過(guò)兼容性問(wèn)題不得不做了一些相應(yīng)的處理,不得不抱怨一句不同的Android定制系統(tǒng)實(shí)在是導(dǎo)致了太多的兼容性問(wèn)題。


    private void getDecodeData(MediaExtractor mediaExtractor, MediaCodec mediaCodec,
                               String decodeFileUrl, int sampleRate,
                               int channelCount, int startSecond, int endSecond,
                               Handler handler,
                               final DecodeOperateInterface decodeOperateInterface) {
        boolean decodeInputEnd = false;
        boolean decodeOutputEnd = false;

        int sampleDataSize;
        int inputBufferIndex;
        int outputBufferIndex;
        int byteNumber;

        long decodeNoticeTime = System.currentTimeMillis();
        long decodeTime;
        long presentationTimeUs = 0;

        final long timeOutUs = 100;
        final long startMicroseconds = startSecond * 1000 * 1000;
        final long endMicroseconds = endSecond * 1000 * 1000;

        ByteBuffer[] inputBuffers;
        ByteBuffer[] outputBuffers;

        ByteBuffer sourceBuffer;
        ByteBuffer targetBuffer;

        MediaFormat outputFormat = mediaCodec.getOutputFormat();

        MediaCodec.BufferInfo bufferInfo;

        byteNumber =
                (outputFormat.containsKey("bit-width") ? outputFormat.getInteger("bit-width") :
                        0) / 8;

        mediaCodec.start();

        inputBuffers = mediaCodec.getInputBuffers();
        outputBuffers = mediaCodec.getOutputBuffers();

        mediaExtractor.selectTrack(0);

        bufferInfo = new MediaCodec.BufferInfo();

        BufferedOutputStream bufferedOutputStream = FileFunction
                .GetBufferedOutputStreamFromFile(decodeFileUrl);

        while (!decodeOutputEnd) {
            if (decodeInputEnd) {
                return;
            }

            decodeTime = System.currentTimeMillis();

            if (decodeTime - decodeNoticeTime > Constant.OneSecond) {
                final int decodeProgress =
                        (int) ((presentationTimeUs - startMicroseconds) * Constant
                                .NormalMaxProgress /
                                endMicroseconds);

                if (decodeProgress > 0) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            decodeOperateInterface.updateDecodeProgress(decodeProgress);
                        }
                    });
                }

                decodeNoticeTime = decodeTime;
            }

            try {
                inputBufferIndex = mediaCodec.dequeueInputBuffer(timeOutUs);

                if (inputBufferIndex >= 0) {
                    sourceBuffer = inputBuffers[inputBufferIndex];

                    sampleDataSize = mediaExtractor.readSampleData(sourceBuffer, 0);

                    if (sampleDataSize < 0) {
                        decodeInputEnd = true;
                        sampleDataSize = 0;
                    } else {
                        presentationTimeUs = mediaExtractor.getSampleTime();
                    }

                    mediaCodec.queueInputBuffer(inputBufferIndex, 0, sampleDataSize,
                            presentationTimeUs,
                            decodeInputEnd ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);

                    if (!decodeInputEnd) {
                        mediaExtractor.advance();
                    }
                } else {
                    LogFunction.error("inputBufferIndex", "" + inputBufferIndex);
                }

                // decode to PCM and push it to the AudioTrack player
                outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, timeOutUs);

                if (outputBufferIndex < 0) {
                    switch (outputBufferIndex) {
                        case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
                            outputBuffers = mediaCodec.getOutputBuffers();
                            LogFunction.error("MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED",
                                    "[AudioDecoder]output buffers have changed.");
                            break;
                        case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                            outputFormat = mediaCodec.getOutputFormat();

                            sampleRate = outputFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE) ?
                                    outputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE) :
                                    sampleRate;
                            channelCount = outputFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT) ?
                                    outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) :
                                    channelCount;
                            byteNumber = (outputFormat.containsKey("bit-width") ? outputFormat
                                    .getInteger
                                            ("bit-width") : 0) / 8;

                            LogFunction.error("MediaCodec.INFO_OUTPUT_FORMAT_CHANGED",
                                    "[AudioDecoder]output format has changed to " +
                                            mediaCodec.getOutputFormat());
                            break;
                        default:
                            LogFunction.error("error",
                                    "[AudioDecoder] dequeueOutputBuffer returned " +
                                            outputBufferIndex);
                            break;
                    }
                    continue;
                }

                targetBuffer = outputBuffers[outputBufferIndex];

                byte[] sourceByteArray = new byte[bufferInfo.size];

                targetBuffer.get(sourceByteArray);
                targetBuffer.clear();

                mediaCodec.releaseOutputBuffer(outputBufferIndex, false);

                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    decodeOutputEnd = true;
                }

                if (sourceByteArray.length > 0 && bufferedOutputStream != null) {
                    if (presentationTimeUs < startMicroseconds) {
                        continue;
                    }

                    byte[] convertByteNumberByteArray = ConvertByteNumber(byteNumber, Constant
                                    .RecordByteNumber,
                            sourceByteArray);

                    byte[] resultByteArray =
                            ConvertChannelNumber(channelCount, Constant.RecordChannelNumber,
                                    Constant.RecordByteNumber,
                                    convertByteNumberByteArray);

                    try {
                        bufferedOutputStream.write(resultByteArray);
                    } catch (Exception e) {
                        LogFunction.error("輸出解壓音頻數(shù)據(jù)異常", e);
                    }
                }

                if (presentationTimeUs > endMicroseconds) {
                    break;
                }
            } catch (Exception e) {
                LogFunction.error("getDecodeData異常", e);
            }
        }

        if (bufferedOutputStream != null) {
            try {
                bufferedOutputStream.close();
            } catch (IOException e) {
                LogFunction.error("關(guān)閉bufferedOutputStream異常", e);
            }
        }

        if (sampleRate != Constant.RecordSampleRate) {
            Resample(sampleRate, decodeFileUrl);
        }

        if (mediaCodec != null) {
            mediaCodec.stop();
            mediaCodec.release();
        }

        if (mediaExtractor != null) {
            mediaExtractor.release();
        }
    }

getDecodeData方法是此次的進(jìn)行解碼和裁剪的核心,方法的傳入?yún)?shù)中mediaExtractor,mediaCodec用以實(shí)際控制處理背景音樂(lè)的音頻數(shù)據(jù),decodeFileUrl用以指明解碼和裁剪后的PCM文件的存儲(chǔ)地址,sampleRate,channelCount分別用以指明背景音樂(lè)的采樣率,聲道數(shù),startSecond用以指明裁剪背景音樂(lè)的開(kāi)始時(shí)間,目前功能中默認(rèn)為0,endSecond用以指明裁剪背景音樂(lè)的結(jié)束時(shí)間,數(shù)值大小由錄音時(shí)長(zhǎng)直接決定。
  getDecodeData方法中通過(guò)不斷通過(guò)mediaCodec讀入背景音樂(lè)原始數(shù)據(jù)進(jìn)行處理,然后解碼輸出到buffer從而獲取解碼后的數(shù)據(jù),因?yàn)閙ediaCodec的讀取解碼方法和平臺(tái)相關(guān)就不過(guò)多描述,在解碼過(guò)程中通過(guò)startSecond與endSecond來(lái)控制解碼后音頻數(shù)據(jù)輸出的開(kāi)始與結(jié)束。
  解碼和裁剪根據(jù)上文的描述是比較簡(jiǎn)單的,通過(guò)平臺(tái)提供的工具類(lèi)解碼背景音樂(lè)數(shù)據(jù),然后通過(guò)變量裁剪出指定長(zhǎng)度的解碼后音頻數(shù)據(jù)輸出到外文件,這一個(gè)流程結(jié)束功能就實(shí)現(xiàn)了,但在過(guò)程中存在幾個(gè)潛在問(wèn)題點(diǎn)。
  首先,要進(jìn)行合成處理的話,我們必須要保證錄音文件和解碼后文件的采樣率,采樣點(diǎn)字節(jié)數(shù),以及聲道數(shù)相同,因?yàn)殇浺粑募倪@三項(xiàng)系數(shù)已經(jīng)固定,所以我們必須對(duì)解碼的音頻數(shù)據(jù)進(jìn)行處理以保證最終生成的解碼文件三項(xiàng)系數(shù)和錄音文件一致。在http://blog.csdn.net/ownwell/article/details/8114121/,我們可以了解PCM文件常見(jiàn)的四種存儲(chǔ)格式。

格式 字節(jié)1 字節(jié)2 字節(jié)1 字節(jié)2
8位單聲道 0聲道 0聲道 0聲道 0聲道
8位雙聲道 0聲道(左) 1聲道(右) 0聲道(左) 1聲道(右)
16位單聲道 0聲道(低字節(jié)) 0聲道(高字節(jié)) 0聲道(低字節(jié)) 0聲道(高字節(jié))
16位雙聲道 0聲道(左,低字節(jié)) 0聲道(左,高字節(jié)) 0聲道(右,低字節(jié)) 0聲道(右,高字節(jié))

了解這些知識(shí)后,我們就可以知道如何編碼以將已知格式的音頻數(shù)據(jù)轉(zhuǎn)化到另一采樣點(diǎn)字節(jié)數(shù)和聲道數(shù)。
  getDecodeData方法中146行調(diào)用的ConvertByteNumber方法是通過(guò)處理音頻數(shù)據(jù)以保證解碼后音頻文件和錄音文件采樣點(diǎn)字節(jié)數(shù)相同。


    private static byte[] ConvertByteNumber(int sourceByteNumber, int outputByteNumber, byte[]
            sourceByteArray) {
        if (sourceByteNumber == outputByteNumber) {
            return sourceByteArray;
        }

        int sourceByteArrayLength = sourceByteArray.length;

        byte[] byteArray;

        switch (sourceByteNumber) {
            case 1:
                switch (outputByteNumber) {
                    case 2:
                        byteArray = new byte[sourceByteArrayLength * 2];

                        byte resultByte[];

                        for (int index = 0; index < sourceByteArrayLength; index += 1) {
                            resultByte = CommonFunction.GetBytes((short) (sourceByteArray[index]
                                    * 256), Variable
                                    .isBigEnding);

                            byteArray[2 * index] = resultByte[0];
                            byteArray[2 * index + 1] = resultByte[1];
                        }

                        return byteArray;
                }
                break;
            case 2:
                switch (outputByteNumber) {
                    case 1:
                        int outputByteArrayLength = sourceByteArrayLength / 2;

                        byteArray = new byte[outputByteArrayLength];

                        for (int index = 0; index < outputByteArrayLength; index += 1) {
                            byteArray[index] = (byte) (CommonFunction.GetShort(sourceByteArray[2
                                            * index],
                                    sourceByteArray[2 * index + 1], Variable.isBigEnding) / 256);
                        }

                        return byteArray;
                }
                break;
        }

        return sourceByteArray;
    }

ConvertByteNumber方法的參數(shù)中sourceByteNumber代表背景音樂(lè)文件采樣點(diǎn)字節(jié)數(shù),outputByteNumber代表錄音文件采樣點(diǎn)字節(jié)數(shù),兩者如果相同就不處理,不相同則根據(jù)背景音樂(lè)文件采樣點(diǎn)字節(jié)數(shù)進(jìn)行不同的處理,本方法只對(duì)單字節(jié)存儲(chǔ)和雙字節(jié)存儲(chǔ)進(jìn)行了處理,歡迎在各位Github上填充其他采樣點(diǎn)字節(jié)數(shù)的處理方法,
  getDecodeData方法中149行調(diào)用的ConvertChannelNumber方法是通過(guò)處理音頻數(shù)據(jù)以保證解碼后音頻文件和錄音文件聲道數(shù)相同。


    private static byte[] ConvertChannelNumber(int sourceChannelCount, int outputChannelCount,
                                               int byteNumber,
                                               byte[] sourceByteArray) {
        if (sourceChannelCount == outputChannelCount) {
            return sourceByteArray;
        }

        switch (byteNumber) {
            case 1:
            case 2:
                break;
            default:
                return sourceByteArray;
        }

        int sourceByteArrayLength = sourceByteArray.length;

        byte[] byteArray;

        switch (sourceChannelCount) {
            case 1:
                switch (outputChannelCount) {
                    case 2:
                        byteArray = new byte[sourceByteArrayLength * 2];

                        byte firstByte;
                        byte secondByte;

                        switch (byteNumber) {
                            case 1:
                                for (int index = 0; index < sourceByteArrayLength; index += 1) {
                                    firstByte = sourceByteArray[index];

                                    byteArray[2 * index] = firstByte;
                                    byteArray[2 * index + 1] = firstByte;
                                }
                                break;
                            case 2:
                                for (int index = 0; index < sourceByteArrayLength; index += 2) {
                                    firstByte = sourceByteArray[index];
                                    secondByte = sourceByteArray[index + 1];

                                    byteArray[2 * index] = firstByte;
                                    byteArray[2 * index + 1] = secondByte;
                                    byteArray[2 * index + 2] = firstByte;
                                    byteArray[2 * index + 3] = secondByte;
                                }
                                break;
                        }

                        return byteArray;
                }
                break;
            case 2:
                switch (outputChannelCount) {
                    case 1:
                        int outputByteArrayLength = sourceByteArrayLength / 2;

                        byteArray = new byte[outputByteArrayLength];

                        switch (byteNumber) {
                            case 1:
                                for (int index = 0; index < outputByteArrayLength; index += 2) {
                                    short averageNumber =
                                            (short) ((short) sourceByteArray[2 * index] + (short)
                                                    sourceByteArray[2 *
                                                            index + 1]);
                                    byteArray[index] = (byte) (averageNumber >> 1);
                                }
                                break;
                            case 2:
                                for (int index = 0; index < outputByteArrayLength; index += 2) {
                                    byte resultByte[] = CommonFunction.AverageShortByteArray
                                            (sourceByteArray[2 * index],
                                                    sourceByteArray[2 * index + 1],
                                                    sourceByteArray[2 *
                                                            index + 2],
                                                    sourceByteArray[2 * index + 3], Variable
                                                            .isBigEnding);

                                    byteArray[index] = resultByte[0];
                                    byteArray[index + 1] = resultByte[1];
                                }
                                break;
                        }

                        return byteArray;
                }
                break;
        }

        return sourceByteArray;
    }

ConvertChannelNumber方法的參數(shù)中sourceChannelCount代表背景音樂(lè)文件聲道數(shù),outputByteNumber代表錄音文件聲道數(shù),兩者如果相同就不處理,不相同則根據(jù)聲道數(shù)和采樣點(diǎn)字節(jié)數(shù)進(jìn)行不同的處理,本方法只對(duì)單雙通道進(jìn)行了處理,歡迎在Github上填充立體聲等聲道的處理方法。
  getDecodeData方法中176行調(diào)用的Resample方法是用以處理音頻數(shù)據(jù)以保證解碼后音頻文件和錄音文件采樣率相同。


    private static void Resample(int sampleRate, String decodeFileUrl) {
        String newDecodeFileUrl = decodeFileUrl + "new";

        try {
            FileInputStream fileInputStream =
                    new FileInputStream(new File(decodeFileUrl));
            FileOutputStream fileOutputStream =
                    new FileOutputStream(new File(newDecodeFileUrl));

            new SSRC(fileInputStream, fileOutputStream, sampleRate, Constant.RecordSampleRate,
                    Constant.RecordByteNumber, Constant.RecordByteNumber, 1, Integer.MAX_VALUE,
                    0, 0, true);

            fileInputStream.close();
            fileOutputStream.close();

            FileFunction.RenameFile(newDecodeFileUrl, decodeFileUrl);
        } catch (IOException e) {
            LogFunction.error("關(guān)閉bufferedOutputStream異常", e);
        }
    }

為了修改采樣率,在此使用了SSRC在Java端的實(shí)現(xiàn),在網(wǎng)上可以搜到一份關(guān)于SSRC的介紹:"SSRC = Synchronous Sample Rate Converter,同步采樣率轉(zhuǎn)換,直白地說(shuō)就是只能做整數(shù)倍頻,不支持任意頻率之間的轉(zhuǎn)換,比如44.1KHz<->48KHz。",但不同的SSRC實(shí)現(xiàn)原理有所不同,我是用的是來(lái)自https://github.com/shibatch/SSRC在Java端的實(shí)現(xiàn),簡(jiǎn)單讀了此SSRC在Java端實(shí)現(xiàn)的源碼,其代碼實(shí)現(xiàn)中通過(guò)判別重采樣前后采樣率的最大公約數(shù)是否滿足設(shè)定條件作為是否可重采樣的依據(jù),可以支持常見(jiàn)的非整數(shù)倍頻率的采樣率轉(zhuǎn)化,如44.1khz<->48khz,但如果目標(biāo)采樣率是比較特殊的采樣率如某一較大的質(zhì)數(shù),那就無(wú)法支持重采樣。
  至此,Resample,ConvertByteNumber,ConvertChannelNumber三個(gè)方法的處理保證了解碼后文件和錄音文件的采樣率,采樣點(diǎn)字節(jié)數(shù),以及聲道數(shù)相同。
  接著,此處潛在的第二個(gè)問(wèn)題就是大小端存儲(chǔ)。 對(duì)計(jì)算機(jī)體系結(jié)構(gòu)有所了解的同學(xué)肯定了解"大小端"這個(gè)概念,大小端分別代表了多字節(jié)數(shù)據(jù)在內(nèi)存中組織的兩種不同順序,如果對(duì)于"大小端"不是太了解,可以瀏覽http://blog.jobbole.com/102432/的闡述,在處理音頻數(shù)據(jù)的方法中,我們可以看到"Variable.isBigEnding"這個(gè)參數(shù),這個(gè)參數(shù)的含義就是當(dāng)前平臺(tái)是否使用大端編碼,這里大家肯定會(huì)有疑問(wèn),內(nèi)存中多字節(jié)數(shù)據(jù)的組織順序?yàn)槭裁磿?huì)影響我們對(duì)音頻數(shù)據(jù)的處理,舉個(gè)例子,如果我們?cè)趯⒉蓸狱c(diǎn)8位的音頻數(shù)據(jù)轉(zhuǎn)化為采樣點(diǎn)16位,目前的做法是將原始數(shù)據(jù)乘以256,相當(dāng)于每一個(gè)byte轉(zhuǎn)化為short,同時(shí)short的高字節(jié)為原byte的內(nèi)容,低字節(jié)為0,那現(xiàn)在問(wèn)題來(lái)了,那就是高字節(jié)放到高地址還是低地址,這就和平臺(tái)采用的大小端存儲(chǔ)格式息息相關(guān)了,當(dāng)然如果我們輸出的數(shù)據(jù)類(lèi)型是short那就不用關(guān)心,Java會(huì)幫我們處理掉,但我們輸出的是byte數(shù)組,這就需要我們自己對(duì)數(shù)據(jù)進(jìn)行處理了。
  這是一個(gè)很容易忽視的問(wèn)題,因?yàn)檎G闆r下的軟件開(kāi)發(fā)過(guò)程中我們基本是不用關(guān)心大小端的問(wèn)題的,但在這里必須對(duì)大小端的情況進(jìn)行處理,不然會(huì)出現(xiàn)在某些平臺(tái)合成的音頻無(wú)法播放的情況。

2.2.3.合成與輸出

錄音和對(duì)背景音樂(lè)的處理結(jié)束了,接下來(lái)就是最后的合成了,對(duì)于合成我們腦海中浮現(xiàn)最多的會(huì)是什么?相加,對(duì)沒(méi)錯(cuò),音頻合成并不神秘,音頻合成的本質(zhì)就是相同系數(shù)的音頻文件之間數(shù)據(jù)的加和,當(dāng)然現(xiàn)實(shí)中的合成往往并非如此簡(jiǎn)單,在網(wǎng)上搜索"混音算法",我們可以看到大量高深的音頻合成算法,但就目前而言,我們沒(méi)必要實(shí)現(xiàn)復(fù)雜的混音算法,只要讓兩個(gè)音頻文件的原始音頻數(shù)據(jù)相加即可,不過(guò)為了讓我們的合成看上去稍微有一些技術(shù)含量,此次提供的音頻合成方法中允許任意音頻文件相對(duì)于另一音頻文件進(jìn)行時(shí)間上的偏移,并可以通過(guò)兩個(gè)權(quán)重?cái)?shù)據(jù)進(jìn)行音量調(diào)節(jié)。下面我就給出具體代碼吧,講解如何實(shí)現(xiàn)。


    public static void ComposeAudio(String firstAudioFilePath, String secondAudioFilePath,
                                    String composeAudioFilePath, boolean deleteSource,
                                    float firstAudioWeight, float secondAudioWeight,
                                    int audioOffset,
                                    final ComposeAudioInterface composeAudioInterface) {
        boolean firstAudioFinish = false;
        boolean secondAudioFinish = false;

        byte[] firstAudioByteBuffer;
        byte[] secondAudioByteBuffer;
        byte[] mp3Buffer;

        short resultShort;
        short[] outputShortArray;

        int index;
        int firstAudioReadNumber;
        int secondAudioReadNumber;
        int outputShortArrayLength;
        final int byteBufferSize = 1024;

        firstAudioByteBuffer = new byte[byteBufferSize];
        secondAudioByteBuffer = new byte[byteBufferSize];
        mp3Buffer = new byte[(int) (7200 + (byteBufferSize * 1.25))];

        outputShortArray = new short[byteBufferSize / 2];

        Handler handler = new Handler(Looper.getMainLooper());

        FileInputStream firstAudioInputStream = FileFunction.GetFileInputStreamFromFile
                (firstAudioFilePath);
        FileInputStream secondAudioInputStream = FileFunction.GetFileInputStreamFromFile
                (secondAudioFilePath);
        FileOutputStream composeAudioOutputStream = FileFunction.GetFileOutputStreamFromFile
                (composeAudioFilePath);

        LameUtil.init(Constant.RecordSampleRate, Constant.LameBehaviorChannelNumber,
                Constant.BehaviorSampleRate, Constant.LameBehaviorBitRate, Constant.LameMp3Quality);

        try {
            while (!firstAudioFinish && !secondAudioFinish) {
                index = 0;

                if (audioOffset < 0) {
                    secondAudioReadNumber = secondAudioInputStream.read(secondAudioByteBuffer);

                    outputShortArrayLength = secondAudioReadNumber / 2;

                    for (; index < outputShortArrayLength; index++) {
                        resultShort = CommonFunction.GetShort(secondAudioByteBuffer[index * 2],
                                secondAudioByteBuffer[index * 2 + 1], Variable.isBigEnding);

                        outputShortArray[index] = (short) (resultShort * secondAudioWeight);
                    }

                    audioOffset += secondAudioReadNumber;

                    if (secondAudioReadNumber < 0) {
                        secondAudioFinish = true;
                        break;
                    }

                    if (audioOffset >= 0) {
                        break;
                    }
                } else {
                    firstAudioReadNumber = firstAudioInputStream.read(firstAudioByteBuffer);

                    outputShortArrayLength = firstAudioReadNumber / 2;

                    for (; index < outputShortArrayLength; index++) {
                        resultShort = CommonFunction.GetShort(firstAudioByteBuffer[index * 2],
                                firstAudioByteBuffer[index * 2 + 1], Variable.isBigEnding);

                        outputShortArray[index] = (short) (resultShort * firstAudioWeight);
                    }

                    audioOffset -= firstAudioReadNumber;

                    if (firstAudioReadNumber < 0) {
                        firstAudioFinish = true;
                        break;
                    }

                    if (audioOffset <= 0) {
                        break;
                    }
                }

                if (outputShortArrayLength > 0) {
                    int encodedSize = LameUtil.encode(outputShortArray, outputShortArray,
                            outputShortArrayLength, mp3Buffer);

                    if (encodedSize > 0) {
                        composeAudioOutputStream.write(mp3Buffer, 0, encodedSize);
                    }
                }
            }

            handler.post(new Runnable() {
                @Override
                public void run() {
                    if (composeAudioInterface != null) {
                        composeAudioInterface.updateComposeProgress(20);
                    }
                }
            });

            while (!firstAudioFinish || !secondAudioFinish) {
                index = 0;

                firstAudioReadNumber = firstAudioInputStream.read(firstAudioByteBuffer);
                secondAudioReadNumber = secondAudioInputStream.read(secondAudioByteBuffer);

                int minAudioReadNumber = Math.min(firstAudioReadNumber, secondAudioReadNumber);
                int maxAudioReadNumber = Math.max(firstAudioReadNumber, secondAudioReadNumber);

                if (firstAudioReadNumber < 0) {
                    firstAudioFinish = true;
                }

                if (secondAudioReadNumber < 0) {
                    secondAudioFinish = true;
                }

                int halfMinAudioReadNumber = minAudioReadNumber / 2;

                outputShortArrayLength = maxAudioReadNumber / 2;

                for (; index < halfMinAudioReadNumber; index++) {
                    resultShort = CommonFunction.WeightShort(firstAudioByteBuffer[index * 2],
                            firstAudioByteBuffer[index * 2 + 1], secondAudioByteBuffer[index * 2],
                            secondAudioByteBuffer[index * 2 + 1], firstAudioWeight,
                            secondAudioWeight, Variable.isBigEnding);

                    outputShortArray[index] = resultShort;
                }

                if (firstAudioReadNumber != secondAudioReadNumber) {
                    if (firstAudioReadNumber > secondAudioReadNumber) {
                        for (; index < outputShortArrayLength; index++) {
                            resultShort = CommonFunction.GetShort(firstAudioByteBuffer[index * 2],
                                    firstAudioByteBuffer[index * 2 + 1], Variable.isBigEnding);

                            outputShortArray[index] = (short) (resultShort * firstAudioWeight);
                        }
                    } else {
                        for (; index < outputShortArrayLength; index++) {
                            resultShort = CommonFunction.GetShort(secondAudioByteBuffer[index * 2],
                                    secondAudioByteBuffer[index * 2 + 1], Variable.isBigEnding);

                            outputShortArray[index] = (short) (resultShort * secondAudioWeight);
                        }
                    }
                }

                if (outputShortArrayLength > 0) {
                    int encodedSize = LameUtil.encode(outputShortArray, outputShortArray,
                            outputShortArrayLength, mp3Buffer);

                    if (encodedSize > 0) {
                        composeAudioOutputStream.write(mp3Buffer, 0, encodedSize);
                    }
                }
            }
        } catch (Exception e) {
            LogFunction.error("ComposeAudio異常", e);

            handler.post(new Runnable() {
                @Override
                public void run() {
                    if (composeAudioInterface != null) {
                        composeAudioInterface.composeFail();
                    }
                }
            });

            return;
        }

        handler.post(new Runnable() {
            @Override
            public void run() {
                if (composeAudioInterface != null) {
                    composeAudioInterface.updateComposeProgress(50);
                }
            }
        });

        try {
            final int flushResult = LameUtil.flush(mp3Buffer);

            if (flushResult > 0) {
                composeAudioOutputStream.write(mp3Buffer, 0, flushResult);
            }
        } catch (Exception e) {
            LogFunction.error("釋放ComposeAudio LameUtil異常", e);
        } finally {
            try {
                composeAudioOutputStream.close();
            } catch (Exception e) {
                LogFunction.error("關(guān)閉合成輸出音頻流異常", e);
            }

            LameUtil.close();
        }

        if (deleteSource) {
            FileFunction.DeleteFile(firstAudioFilePath);
            FileFunction.DeleteFile(secondAudioFilePath);
        }

        try {
            firstAudioInputStream.close();
            secondAudioInputStream.close();
        } catch (IOException e) {
            LogFunction.error("關(guān)閉合成輸入音頻流異常", e);
        }

        handler.post(new Runnable() {
            @Override
            public void run() {
                if (composeAudioInterface != null) {
                    composeAudioInterface.composeSuccess();
                }
            }
        });
    }

ComposeAudio方法是此次的進(jìn)行合成的具體代碼實(shí)現(xiàn),方法的傳入?yún)?shù)中firstAudioFilePath, secondAudioFilePath是用以合成的音頻文件地址,composeAudioFilePath用以指明合成后輸出的MP3文件的存儲(chǔ)地址,firstAudioWeight,secondAudioWeight分別用以指明合成的兩個(gè)音頻文件在合成過(guò)程中的音量權(quán)重,audioOffset用以指明第一個(gè)音頻文件相對(duì)于第二個(gè)音頻文件合成過(guò)程中的數(shù)據(jù)偏移,如為負(fù)數(shù),則合成過(guò)程中先輸出audioOffset個(gè)字節(jié)長(zhǎng)度的第二個(gè)音頻文件數(shù)據(jù),如為正數(shù),則合成過(guò)程中先輸出audioOffset個(gè)字節(jié)長(zhǎng)度的第一個(gè)音頻文件數(shù)據(jù),audioOffset在另一程度上也代表著時(shí)間的偏移,目前我們合成的兩個(gè)音頻文件參數(shù)為16位單通道44.1khz采樣率,那么audioOffset如果為1*16/8*1*44100=88200字節(jié),那么最終合成出的MP3文件中會(huì)先播放1s的第一個(gè)音頻文件的音頻接著再播放兩個(gè)音頻文件加和的音頻。
  整體合成代碼是很清晰的,因?yàn)榧尤肓藭r(shí)間偏移,所以合成過(guò)程中是有可能有一個(gè)文件先輸出完的,在代碼中針對(duì)性的進(jìn)行處理即可,當(dāng)然即使沒(méi)有時(shí)間偏移也是可能出現(xiàn)類(lèi)似情況的,比如音樂(lè)時(shí)長(zhǎng)2分鐘,錄音3分鐘,音樂(lè)輸出結(jié)束后那就只應(yīng)該輸出錄音音頻了,另外在代碼中將PCM數(shù)據(jù)編碼為MP3文件使用了LAME的MP3編碼庫(kù),除此以外代碼中就沒(méi)有比較復(fù)雜的模塊了。

3.總結(jié)

至此,音頻合成的流程我們算是走完了,希望讀到此處的各位對(duì)音頻合成的實(shí)現(xiàn)有清晰的了解。
  這篇博文就到這里結(jié)束了,本文所有代碼已經(jīng)托管到 https://github.com/CrazyZty/ComposeAudio,大家可以自由下載。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 摘要 該配置文件定義了支持高質(zhì)量音頻分發(fā)所需的Bluetooth?設(shè)備的要求。這些要求以終端用戶服務(wù)的方式表達(dá),并...
    公子小水閱讀 10,404評(píng)論 0 4
  • 前言 說(shuō)到視頻,大家自己腦子里基本都會(huì)想起電影、電視劇、在線視頻等等,也會(huì)想起一些視頻格式 AVI、MP4、RMV...
    ForestSen閱讀 23,985評(píng)論 10 203
  • 要在計(jì)算機(jī)內(nèi)播放或是處理音頻文件,也就是要對(duì)聲音文件進(jìn)行數(shù)、模轉(zhuǎn)換,這個(gè)過(guò)程同樣由采樣和量化構(gòu)成,人耳所能聽(tīng)到的聲...
    Viking_Den閱讀 10,571評(píng)論 1 10
  • H264中的sps pps iOS仿微信小視頻功能開(kāi)發(fā)優(yōu)化記錄【如何快速的開(kāi)發(fā)一個(gè)完整的iOS直播app】(原理篇...
    CharlyZheng閱讀 1,524評(píng)論 0 2
  • “青青子衿,悠悠我心” 你的衣襟,你的玉佩,我深深的思念與情愁。 問(wèn):女子幽怨的等待在對(duì)方看來(lái)究竟算是什么?不嗣音...
    生畏503閱讀 232評(píng)論 0 0

友情鏈接更多精彩內(nèi)容