Android AVDemo(3):音頻封裝研侣,采集編碼并封裝為 M4A丨音視頻工程示例

vx 搜索『gjzkeyframe』 關(guān)注『關(guān)鍵幀Keyframe』來及時獲得最新的音視頻技術(shù)文章。

[圖片上傳失敗...(image-1f0a2a-1654072303086)]
這個公眾號會路線圖 式的遍歷分享音視頻技術(shù)音視頻基礎(chǔ)(完成)音視頻工具(完成)音視頻工程示例(進(jìn)行中) → 音視頻工業(yè)實(shí)戰(zhàn)(準(zhǔn)備)。

iOS/Android 客戶端開發(fā)同學(xué)如果想要開始學(xué)習(xí)音視頻開發(fā)潭苞,最絲滑的方式是對音視頻基礎(chǔ)概念知識有一定了解后,再借助 iOS/Android 平臺的音視頻能力上手去實(shí)踐音視頻的采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染過程真朗,并借助音視頻工具來分析和理解對應(yīng)的音視頻數(shù)據(jù)此疹。

音視頻工程示例這個欄目,我們將通過拆解采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染流程并實(shí)現(xiàn) Demo 來向大家介紹如何在 iOS/Android 平臺上手音視頻開發(fā)遮婶。

這里是 Android 第三篇:Android 音頻封裝 Demo蝗碎。這個 Demo 里包含以下內(nèi)容:

  • 1)實(shí)現(xiàn)一個音頻采集模塊;
  • 2)實(shí)現(xiàn)一個音頻編碼模塊蹭睡;
  • 3)實(shí)現(xiàn)一個音頻封裝模塊衍菱;
  • 4)串聯(lián)音頻采集、編碼肩豁、封裝模塊脊串,將采集到的音頻數(shù)據(jù)輸入給 AAC 編碼模塊進(jìn)行編碼辫呻,再將編碼后的數(shù)據(jù)輸入給 M4A 封裝模塊封裝和存儲;
  • 5)詳盡的代碼注釋琼锋,幫你理解代碼邏輯和原理放闺。

如果你想獲得全部源碼和參與音視頻技術(shù)討論,可以知識星球搜索『關(guān)鍵幀的音視頻開發(fā)圈』加入我們缕坎,當(dāng)然也可以跳過直接看后續(xù)的內(nèi)容怖侦。

1、音頻采集模塊

在這個 Demo 中谜叹,音頻采集模塊 KFAudioCapture 的實(shí)現(xiàn)與 《Android 音頻采集 Demo》 中一樣匾寝,這里就不再重復(fù)介紹了,其接口如下:

KFAudioCapture.java


public class KFAudioCapture {
 public KFAudioCapture(KFAudioCaptureConfig config,KFAudioCaptureListener listener);
 public void startRunning(); // 開始采集音頻數(shù)據(jù)荷腊。
 public void stopRunning(); // 停止采集音頻數(shù)據(jù)艳悔。
 public void release(); // 釋放音頻采集。
}

2女仰、音頻編碼模塊

同樣的猜年,音頻編碼模塊 KFAudioByteBufferEncoder 的實(shí)現(xiàn)與《Android 音頻編碼 Demo》中一樣,這里就不再重復(fù)介紹了疾忍,其接口如下:

KFMediaCodecInterface.java

public interface KFMediaCodecInterface {
    public static final int KFMediaCodecInterfaceErrorCreate = -2000;
    public static final int KFMediaCodecInterfaceErrorConfigure = -2001;
    public static final int KFMediaCodecInterfaceErrorStart = -2002;
    public static final int KFMediaCodecInterfaceErrorDequeueOutputBuffer = -2003;
    public static final int KFMediaCodecInterfaceErrorParams = -2004;

    public static int KFMediaCodeProcessParams = -1;
    public static int KFMediaCodeProcessAgainLater = -2;
    public static int KFMediaCodeProcessSuccess = 0;

    ///< 初始化 Codec乔外,第一個參數(shù)需告知使用編碼還是解碼。
    public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext);
    ///< 釋放 Codec一罩。
    public void release();

    ///< 獲取輸出格式描述杨幼。
    public MediaFormat getOutputMediaFormat();
    ///< 獲取輸入格式描述。
    public MediaFormat getInputMediaFormat();
    ///< 處理每一幀數(shù)據(jù)擒抛,編碼前與編碼后都可以推汽,支持編解碼 2 種模式。
    public int processFrame(KFFrame frame);
    ///< 清空 Codec 緩沖區(qū)歧沪。
    public void flush();
}

3歹撒、音頻封裝模塊

接下來,我們來實(shí)現(xiàn)一個音頻封裝模塊诊胞,在這里輸入編碼后的數(shù)據(jù)暖夭,輸出封裝后的文件。

這次我們要封裝的格式是 M4A撵孤,屬于 MPEG-4 標(biāo)準(zhǔn)迈着,通常普通的 MPEG-4 文件擴(kuò)展名是 .mp4,只包含音頻的 MPEG-4 文件擴(kuò)展名用 .m4a邪码。所以裕菠,其實(shí)我們這里實(shí)現(xiàn)的是一個 MP4 封裝模塊,支持將音頻編碼數(shù)據(jù)封裝成 M4A闭专,也支持將音視頻數(shù)據(jù)封裝成 MP4奴潘。關(guān)于 MP4 格式旧烧,可以看一看《MP4 格式》這篇文章了解一下。

由于 MP4 封裝涉及到一些參數(shù)設(shè)置画髓,所以我們先實(shí)現(xiàn)一個 KFMuxerConfig 類用于定義 MP4 封裝的參數(shù)的配置掘剪。這里包括了:封裝文件輸出地址、封裝文件類型這幾個參數(shù)奈虾。

KFMuxerConfig.java

public class KFMuxerConfig {
    ///< 輸出路徑夺谁。
    public String outputPath = null;
    ///< 封裝僅音頻、僅視頻肉微、音視頻匾鸥。
    public KFMediaBase.KFMediaType muxerType = KFMediaBase.KFMediaType.KFMediaAV;

    public KFMuxerConfig(String path) {
        outputPath = path;
    }
}

其中用到的 KFMediaType 是定義在 KFMediaBase 中的一個枚舉:

KFMediaBase.java


public class KFMediaBase {
    public enum KFMediaType {
        KFMediaUnkown(0),
        KFMediaAudio (1 << 0),
        KFMediaVideo  (1 << 1),
        KFMediaAV ((1 << 0) | (1 << 1));
        private int index;
        KFMediaType(int index) {
            this.index = index;
        }

        public int value() {
            return index;
        }
    }
}

接下來,我們來實(shí)現(xiàn) KFMP4Muxer 模塊浪册。

KFMP4Muxer.java

public class KFMP4Muxer {
    public static final int KFMuxerErrorCreate = -2200;
    public static final int KFMuxerErrorAudioAddTrack = -2201;
    public static final int KFMuxerErrorVideoAddTrack = -2202;

    private static final String TAG = "KFMuxer";
    private KFMuxerConfig mConfig = null; ///< 封裝配置
    private KFMuxerListener mListener = null; ///< 回調(diào)
    private MediaMuxer mMediaMuxer = null; ///< 封裝實(shí)例
    private int mVideoTrackIndex = -1; ///< 視頻 track 軌道下標(biāo)
    private MediaFormat mVideoFormat = null; ///< 視頻輸入視頻格式描述
    private List<KFBufferFrame> mVideoList = new ArrayList<>(); ///< 視頻輸入緩存
    private int mAudioTrackIndex = -1; ///< 音頻 track 軌道下標(biāo)
    private MediaFormat mAudioFormat = null; ///< 音頻輸入視頻格式描述
    private List<KFBufferFrame> mAudioList = new ArrayList<>(); ///< 音頻輸入緩存
    private boolean mIsStart = false;
    private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主線程

    public KFMP4Muxer(KFMuxerConfig config, KFMuxerListener listener) {
        mConfig = config;
        mListener = listener;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public void start() {
        _setupMuxer();
    }

    public void stop() {
        _stop();
    }

    public void setVideoMediaFormat(MediaFormat mediaFormat) {
        mVideoFormat = mediaFormat;
    }

    public void setAudioMediaFormat(MediaFormat mediaFormat) {
        mAudioFormat = mediaFormat;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    ///< 寫入音視頻數(shù)據(jù)(編碼后數(shù)據(jù))扫腺。
    public void writeSampleData(boolean isVideo, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo) {
        if ((bufferInfo.flags & BUFFER_FLAG_CODEC_CONFIG) != 0) {
            return;
        }

        if (buffer ==null || bufferInfo == null || mMediaMuxer == null || bufferInfo.size == 0) {
            return;
        }

        ///< 校驗(yàn)視頻數(shù)據(jù)是否進(jìn)入。
        if (!_hasAudioTrack() && !isVideo) {
            return;
        }

        ///< 校驗(yàn)視頻數(shù)據(jù)是否進(jìn)入村象。
        if (!_hasVideoTrack() && isVideo) {
            return;
        }

        ///< 數(shù)據(jù)轉(zhuǎn)換結(jié)構(gòu)體 KFBufferFrame。
        KFBufferFrame packet = new KFBufferFrame();
        ByteBuffer newBuffer = ByteBuffer.allocateDirect(bufferInfo.size);
        newBuffer.put(buffer).position(0);

        MediaCodec.BufferInfo newInfo = new MediaCodec.BufferInfo();
        newInfo.size = bufferInfo.size;
        newInfo.flags = bufferInfo.flags;
        newInfo.presentationTimeUs = bufferInfo.presentationTimeUs;

        packet.buffer = newBuffer;
        packet.bufferInfo = newInfo;
        if (isVideo) {
            ///< 初始化視頻 Track攒至。
            if (mVideoFormat != null && mVideoTrackIndex == -1) {
                _setupVideoTrack();
            }
            mVideoList.add(packet);
        } else {
            ///< 初始化音頻Track
            if (mAudioFormat != null && mAudioTrackIndex == -1) {
                _setupAudioTrack();
            }
            mAudioList.add(packet);
        }

        ///< 校驗(yàn)音視頻 Track 是否都初始化好厚者。
        if ((_hasAudioTrack() && _hasVideoTrack() && mAudioTrackIndex >=0 && mVideoTrackIndex >= 0) ||
                (_hasAudioTrack() && !_hasVideoTrack() && mAudioTrackIndex >= 0) ||
                (!_hasAudioTrack() && _hasVideoTrack() && mVideoTrackIndex >= 0)) {
            if (!mIsStart) {
                _start();
                mIsStart = true;
            }

            ///< 音視頻交錯,目的音視頻時間戳盡量不跳躍迫吐。
            if(mIsStart){
                _avInterleavedBuffers();
            }
        }
    }

    public void release() {
        _stop();
    }

    private void _start() {
        ///< 開啟封裝库菲。
        try {
            if (mMediaMuxer != null) {
                mMediaMuxer.start();
            }
        } catch (Exception e) {
            Log.e(TAG, "start" + e);
        }
    }

    private void _stop() {
        ///< 關(guān)閉封裝
        try {
            if (mMediaMuxer != null) {
                ///< 兜底一路沒進(jìn)來的 case,如果外層配置音視頻一起封裝但最終只進(jìn)來一路也會處理志膀。
                if (!mIsStart && (mVideoTrackIndex != 0 || mAudioTrackIndex != 0) && (mVideoList.size() > 0 || mAudioList.size() > 0)) {
                    mMediaMuxer.start();
                    mIsStart = true;
                }

                ///< 將緩沖中數(shù)據(jù)推入封裝器熙宇。
                if (mIsStart) {
                    _appendAudioBuffers();
                    _appendVideoBuffers();
                    mMediaMuxer.stop();
                }

                ///< 釋放封裝器實(shí)例。
                mMediaMuxer.release();
                mMediaMuxer = null;
            }
        } catch (Exception e) {
            Log.e(TAG, "stop release" + e);
        }
        ///< 清空相關(guān)緩存與標(biāo)記位溉浙。
        mVideoTrackIndex = -1;
        mAudioTrackIndex = -1;
        mIsStart = false;
        mVideoList.clear();
        mAudioList.clear();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private boolean _hasAudioTrack() {
        return (mConfig.muxerType.value() & KFMediaBase.KFMediaType.KFMediaAudio.value()) != 0;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private boolean _hasVideoTrack() {
        return (mConfig.muxerType.value() & KFMediaBase.KFMediaType.KFMediaVideo.value()) != 0;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void _setupMuxer() {
        ///< 初始化封裝器烫止。
        if(mMediaMuxer == null){
            try {
                mMediaMuxer = new MediaMuxer(mConfig.outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            } catch (IOException e) {
                Log.e(TAG, "new MediaMuxer" + e);
                _callBackError(KFMuxerErrorCreate,e.getMessage());
                return;
            }
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void _setupVideoTrack() {
        ///< 根據(jù)外層輸入格式描述初始化視頻 Track。
        if (mVideoFormat != null) {
            ///< 添加視頻 Track戳稽。
            try {
                mVideoTrackIndex = mMediaMuxer.addTrack(mVideoFormat);
            } catch (Exception e) {
                Log.e(TAG, "addTrack" + e);
                _callBackError(KFMuxerErrorVideoAddTrack,e.getMessage());
            }
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void _setupAudioTrack() {
        ///< 根據(jù)外層輸入格式描述初始化音頻 Track馆蠕。
        if(mAudioFormat != null){
            ///< 添加音頻 Track。
            try {
                mAudioTrackIndex = mMediaMuxer.addTrack(mAudioFormat);
            } catch (Exception e) {
                Log.e(TAG, "addTrack" + e);
                _callBackError(KFMuxerErrorAudioAddTrack,e.getMessage());
            }
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void _avInterleavedBuffers() {
        ///< 音視頻交錯惊奇,通過對比時間戳大小交錯進(jìn)入互躬。
        if (_hasVideoTrack() && _hasAudioTrack()) {
            while (mAudioList.size() > 0 && mVideoList.size() > 0) {
                KFBufferFrame audioPacket = mAudioList.get(0);
                KFBufferFrame videoPacket = mVideoList.get(0);

                if (audioPacket.bufferInfo.presentationTimeUs >= videoPacket.bufferInfo.presentationTimeUs) {
                    mMediaMuxer.writeSampleData(mVideoTrackIndex,videoPacket.buffer,videoPacket.bufferInfo);
                    mVideoList.remove(0);
                } else {
                    mMediaMuxer.writeSampleData(mAudioTrackIndex,audioPacket.buffer,audioPacket.bufferInfo);
                    mAudioList.remove(0);
                }
            }
        } else if (_hasVideoTrack()) {
            _appendVideoBuffers();
        } else if (_hasAudioTrack()) {
            _appendAudioBuffers();
        }
    }

    private void _appendAudioBuffers() {
        ///< 音頻隊列緩沖區(qū)推到封裝器。
        while (mAudioList.size() > 0) {
            KFBufferFrame packet = mAudioList.get(0);
            mMediaMuxer.writeSampleData(mAudioTrackIndex,packet.buffer,packet.bufferInfo);
            mAudioList.remove(0);
        }
    }

    private void _appendVideoBuffers() {
        ///< 視頻隊列緩沖區(qū)推到封裝器颂郎。
        while (mVideoList.size() > 0) {
            KFBufferFrame packet = mVideoList.get(0);
            mMediaMuxer.writeSampleData(mVideoTrackIndex,packet.buffer,packet.bufferInfo);
            mVideoList.remove(0);
        }
    }

    private void _callBackError(int error, String errorMsg) {
        ///< 錯誤回調(diào)吼渡。
        if (mListener != null) {
            mMainHandler.post(()->{
                mListener.muxerOnError(error,TAG + errorMsg);
            });
        }
    }
}

上面是 KFMP4Muxer 的實(shí)現(xiàn),從代碼上可以看到主要有這幾個部分:

  • 1)創(chuàng)建封裝器實(shí)例乓序。調(diào)用 start寺酪。
    • _setupMuxer 方法中實(shí)現(xiàn)舟奠,通過輸出路徑與格式 2 個參數(shù)生成。
  • 2)創(chuàng)建音視頻軌道及添加音頻和視頻數(shù)據(jù)房维。調(diào)用 writeSampleData: 檢測音視頻數(shù)據(jù)會創(chuàng)建對應(yīng)的軌道沼瘫。
    • _setupVideoTrack_setupAudioTrack 方法中實(shí)現(xiàn)。音頻和視頻的格式描述分別為mVideoFormat咙俩、mAudioFormat耿戚。
    • 當(dāng)音頻軌道與視頻軌道都創(chuàng)建好后,會觸發(fā)真正的開始 _start阿趁。這樣設(shè)計的原因是外層可能優(yōu)先輸入音頻或視頻膜蛔,但封裝器開始前又需要創(chuàng)建音視頻軌道,所以這里實(shí)現(xiàn)了等待邏輯脖阵。
  • 3)用兩個隊列作為緩沖區(qū)皂股,分別管理音頻和視頻待封裝數(shù)據(jù)。
    • 這兩個隊列分別是 mAudioListmVideoList命黔,存儲數(shù)據(jù)類型為 KFBufferFrame呜呐。
    • 每次當(dāng)外部調(diào)用 writeSampleData: 方法送入待封裝數(shù)據(jù)時,都是把數(shù)據(jù)放入兩個隊列中的一個悍募,以便根據(jù)情況進(jìn)行后續(xù)的音視頻數(shù)據(jù)交織蘑辑。
  • 4)同時封裝音頻和視頻數(shù)據(jù)時,進(jìn)行音視頻數(shù)據(jù)交織坠宴。
    • _avInterleavedBuffers 方法中實(shí)現(xiàn)音視頻數(shù)據(jù)交織洋魂。當(dāng)帶封裝的數(shù)據(jù)既有音頻又有視頻,就需要根據(jù)他們的時間戳信息進(jìn)行交織喜鼓,這樣便于在播放該音視頻時提升體驗(yàn)副砍。
  • 5)音視頻數(shù)據(jù)寫入封裝。
    • 同時封裝音頻和視頻數(shù)據(jù)時庄岖,在做完音視頻交織后豁翎,即分別將交織后的音視頻數(shù)據(jù)寫入封裝器 mMediaMuxer writeSampleData。在 _avInterleavedBuffers 中實(shí)現(xiàn)顿锰。
    • 單獨(dú)封裝音頻或視頻數(shù)據(jù)時谨垃,則直接將數(shù)據(jù)寫入封裝器 mMediaMuxer writeSampleData。分別在 _appendAudioBuffers_appendVideoBuffers 方法中實(shí)現(xiàn)硼控。
  • 6)停止寫入刘陶。
    • stop_stop 方法中實(shí)現(xiàn)。
    • 在停止前牢撼,還需要消費(fèi)掉 mAudioListmVideoList的剩余數(shù)據(jù)匙隔,要調(diào)用 _appendAudioBuffers_appendVideoBuffers
    • 封裝器執(zhí)行停止操作 mMediaMuxer stop熏版。

更具體細(xì)節(jié)見上述代碼及其注釋纷责。

4捍掺、采集音頻數(shù)據(jù)進(jìn)行 AAC 編碼以及 M4A 封裝和存儲

我們還是在一個 MainActivity 中來實(shí)現(xiàn)采集音頻數(shù)據(jù)進(jìn)行 AAC 編碼、M4A 封裝和存儲的邏輯再膳。

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private KFAudioCapture mAudioCapture = null; ///< 音頻采集
    private KFAudioCaptureConfig mAudioCaptureConfig = null; ///< 音頻采集配置
    private KFMediaCodecInterface mEncoder = null; ///< 音頻編碼
    private MediaFormat mAudioEncoderFormat = null; ///< 音頻編碼格式描述
    private KFMP4Muxer mMuxer; ///< 封裝起器
    private KFMuxerConfig mMuxerConfig; ///< 封裝器配置
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ///< 申請存儲挺勿、音頻采集權(quán)限。
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions((Activity) this,
                    new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    1);
        }

        ///< 創(chuàng)建采集實(shí)例喂柒。
        mAudioCaptureConfig = new KFAudioCaptureConfig();
        mAudioCapture = new KFAudioCapture(mAudioCaptureConfig,mAudioCaptureListener);
        mAudioCapture.startRunning();

        mMuxerConfig = new KFMuxerConfig(Environment.getExternalStorageDirectory().getPath() + "/test.m4a");
        mMuxerConfig.muxerType = KFMediaBase.KFMediaType.KFMediaAudio;

        FrameLayout.LayoutParams startParams = new FrameLayout.LayoutParams(200, 120);
        startParams.gravity = Gravity.CENTER_HORIZONTAL;
        Button startButton = new Button(this);
        startButton.setTextColor(Color.BLUE);
        startButton.setText("開始");
        startButton.setVisibility(View.VISIBLE);
        startButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ///< 創(chuàng)建音頻編碼實(shí)例不瓶。
                if (mEncoder == null) {
                    mEncoder = new KFAudioByteBufferEncoder();
                    MediaFormat mediaFormat = KFAVTools.createAudioFormat(mAudioCaptureConfig.sampleRate,mAudioCaptureConfig.channel,96*1000);
                    mEncoder.setup(true,mediaFormat,mAudioEncoderListener,null);
                    ((Button)view).setText("停止");
                    mMuxer = new KFMP4Muxer(mMuxerConfig,mMuxerListener);
                } else {
                    mEncoder.release();
                    mEncoder = null;
                    mMuxer.stop();
                    mMuxer.release();
                    mMuxer = null;
                    ((Button)view).setText("開始");
                }
            }
        });
        addContentView(startButton, startParams);
    }

    private KFAudioCaptureListener mAudioCaptureListener = new KFAudioCaptureListener() {
        @Override
        public void onError(int error, String errorMsg) {
            Log.e("KFAudioCapture","errorCode" + error + "msg"+errorMsg);
        }

        @Override
        public void onFrameAvailable(KFFrame frame) {
            ///< 采集回調(diào)輸入編碼。
            if (mEncoder != null) {
                mEncoder.processFrame(frame);
            }
        }
    };

    private KFMediaCodecListener mAudioEncoderListener = new KFMediaCodecListener() {
        @Override
        public void onError(int error, String errorMsg) {
            Log.i("KFMediaCodecListener","error" + error + "msg" + errorMsg);
        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void dataOnAvailable(KFFrame frame) {
            ///< 編碼回調(diào)寫入封裝器灾杰。
            if (mAudioEncoderFormat == null && mEncoder != null) {
                mAudioEncoderFormat = mEncoder.getOutputMediaFormat();
                mMuxer.setAudioMediaFormat(mEncoder.getOutputMediaFormat());
                mMuxer.start();
            }

            if (mMuxer != null) {
                mMuxer.writeSampleData(false,((KFBufferFrame)frame).buffer,((KFBufferFrame)frame).bufferInfo);
            }
        }
    };

    private KFMuxerListener mMuxerListener = new KFMuxerListener() {
        @Override
        public void muxerOnError(int error, String errorMsg) {
            ///< 音頻封裝錯誤回調(diào)蚊丐。
            Log.i("KFMuxerListener","error" + error + "msg" + errorMsg);
        }
    };
}

上面是 MainActivity 的實(shí)現(xiàn),其中主要包含這幾個部分:

  • 1)在采集音頻前需要設(shè)置 Manifest.permission.RECORD_AUDIO 權(quán)限艳吠。
  • 2)通過啟動和停止音頻采集來驅(qū)動整個采集和編碼流程麦备。
  • 3)在采集模塊 KFAudioCapture 的數(shù)據(jù)回調(diào)中將數(shù)據(jù)交給編碼模塊 KFAudioByteBufferEncoder 進(jìn)行編碼。
    • KFAudioCaptureListeneronFrameAvailable 回調(diào)中實(shí)現(xiàn)昭娩。
  • 4)在編碼模塊 KFAudioByteBufferEncoder的數(shù)據(jù)回調(diào)中獲取編碼后的 AAC 裸流數(shù)據(jù)凛篙,并將數(shù)據(jù)交給封裝器 KFMP4Muxer 進(jìn)行封裝。
    • KFMediaCodecListenerdataOnAvailable 回調(diào)中實(shí)現(xiàn)题禀。
  • 5)在調(diào)用 stop 停止整個流程后鞋诗,如果沒有出現(xiàn)錯誤,封裝的 M4A 文件會被存儲到 mMuxerConfig 設(shè)置的路徑迈嘹。

5、用工具播放 M4A 文件

完成音頻采集和編碼后全庸,可以將 sdcard 文件夾下面的 test.m4a 文件拷貝到電腦上秀仲,使用 ffplay 播放來驗(yàn)證一下音頻采集是效果是否符合預(yù)期:

$ ffplay -I test.m4a

關(guān)于播放 M4A 文件的工具,可以參考《FFmpeg 工具》第 2 節(jié) ffplay 命令行工具《可視化音視頻分析工具》第 1.1 節(jié) Adobe Audition壶笼。

上面我們講過 M4A 格式是屬于 MPEG-4 標(biāo)準(zhǔn)神僵,所以我們這里還可以用《可視化音視頻分析工具》第 3.1 節(jié) MP4Box.js 等工具來查看它的格式:

[圖片上傳失敗...(image-f772b-1654072303086)]

- 完 -

推薦閱讀

《Android AVDemo(2):音頻編碼》

《Android AVDemo(1):音頻采集》

《iOS AVDemo(3):音頻封裝》

《iOS AVDemo(2):音頻編碼》

《iOS AVDemo(1):音頻采集》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市覆劈,隨后出現(xiàn)的幾起案子保礼,更是在濱河造成了極大的恐慌,老刑警劉巖责语,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件炮障,死亡現(xiàn)場離奇詭異,居然都是意外死亡坤候,警方通過查閱死者的電腦和手機(jī)胁赢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來白筹,“玉大人智末,你說我怎么就攤上這事谅摄。” “怎么了系馆?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵送漠,是天一觀的道長。 經(jīng)常有香客問我由蘑,道長闽寡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任纵穿,我火速辦了婚禮下隧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谓媒。我一直安慰自己淆院,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布句惯。 她就那樣靜靜地躺著土辩,像睡著了一般抢野。 火紅的嫁衣襯著肌膚如雪拷淘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天指孤,我揣著相機(jī)與錄音启涯,去河邊找鬼。 笑死恃轩,一個胖子當(dāng)著我的面吹牛结洼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播叉跛,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼松忍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了筷厘?” 一聲冷哼從身側(cè)響起鸣峭,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎酥艳,沒想到半個月后摊溶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡玖雁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年更扁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡浓镜,死狀恐怖溃列,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情膛薛,我是刑警寧澤听隐,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站哄啄,受9級特大地震影響雅任,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜咨跌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一沪么、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧锌半,春花似錦禽车、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至记焊,卻和暖如春逸月,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背遍膜。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工碗硬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瓢颅。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓肛响,卻偏偏與公主長得像,于是被迫代替她去往敵國和親惜索。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容