如何解码 android 上的 m4a 音频

how to decode an m4a audio on android

我正在尝试解码 android 上的音频并获取原始数据以应用过滤器。

我正在使用 MediaExtractor 从文件中提取编码数据,这似乎有效。 然后我尝试混合来自 MediaExtractor docs plus MediaCodec on Synchronous Processing using Buffers 的代码来提取数据并以块的形式解码它们。

所以我首先使用取自 extractor.getTrackFormat(0);

的格式配置解码器
MediaExtractor extractor = new MediaExtractor();

String path = "...";
extractor.setDataSource(path);

MediaFormat format = extractor.getTrackFormat(0);
mAudioKeyMine = format.getString(MediaFormat.KEY_MIME);

extractor.selectTrack(0);

MediaCodec decoder;
decoder = MediaCodec.createDecoderByType(mAudioKeyMine);
decoder.configure(format, null, null, 0);

然后尝试获取数据:

public void getData(MediaExtractor extractor)
{
    int offset = 0;

    ByteBuffer inputBuffer = ByteBuffer.allocate(2048);

    MediaFormat outputFormat = decoder.getOutputFormat();
    Log.v(TAG, "outputFormat: " + outputFormat.toString());

    decoder.start();
    int index = decoder.dequeueInputBuffer(1000);

    boolean sawInputEOS = false;

    int sample = 0;
    while (sample >= 0)
    {

        int inputBufferId = decoder.dequeueInputBuffer(1000);
        if (inputBufferId >= 0)
        {
            inputBuffer = decoder.getInputBuffer(index);

            sample = extractor.readSampleData(inputBuffer, 0);

            long presentationTimeUs = 0;

            if (sample < 0)
            {
                sawInputEOS = true;
                sample = 0;
            }
            else
            {
                int trackIndex = extractor.getSampleTrackIndex();
                presentationTimeUs = extractor.getSampleTime();

                Log.v(TAG, "trackIndex: " + trackIndex + ", presentationTimeUs: " + presentationTimeUs);
                Log.v(TAG, "sample: " + sample + ", offset: " + offset);
                Log.v(TAG, "inputBuffer: " + inputBuffer.toString());
            }

            decoder.queueInputBuffer(inputBufferId, 0, sample, presentationTimeUs, sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);

            if (!sawInputEOS)
            {
                extractor.advance();
            }

        }
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();

        int outputBufferId = decoder.dequeueOutputBuffer(info, 1000);
        Log.v(TAG, "info: " + info.toString());

        if (outputBufferId >= 0)
        {
            ByteBuffer outputBuffer = decoder.getOutputBuffer(outputBufferId);
            MediaFormat bufferFormat = decoder.getOutputFormat(outputBufferId);

            Log.v(TAG, "option A");
            Log.v(TAG, "outputBufferId: " + outputBufferId);
            if (outputBuffer != null)
            {
                Log.v(TAG, "outputBuffer: " + outputBuffer.toString());
            }
            else
            {
                Log.v(TAG, "outputBuffer: null");
            }
            Log.v(TAG, "bufferFormat: " + bufferFormat.toString());

            if (outputBuffer != null)
            {
                int cont = 0;
                while (outputBuffer.hasRemaining())
                {
                    int pos = outputBuffer.position();
                    byte data = outputBuffer.get();

                    // do something with the data
                    if (cont < 10)
                    {
                        Log.v(TAG, "outputBuffer: " + pos + " -> " + data);
                    }
                    cont++;
                }
            }
            else
            {
                Log.v(TAG, "outputBuffer: null");
            }
            decoder.releaseOutputBuffer(outputBufferId, 0);
        }
        else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED)
        {
            Log.v(TAG, "option B");
            outputFormat = decoder.getOutputFormat(); 
            Log.v(TAG, "outputFormat: " + outputFormat.toString());
        }
        Log.v(TAG, "extractor.advance()");
        offset += sample;
    }
    Log.v(TAG, "end of track");
    extractor.release();
    extractor = null;
    decoder.stop();
    decoder.release();
}

但是我在第 int outputBufferId = decoder.dequeueOutputBuffer(info, 1000); 行收到错误 IllegalStateException

我搜索了错误以及如何正确解码 m4a,但大多数解决方案在 api 21 上已弃用,现在我陷入了这个错误。

所以有api 26/28 的音频解码示例,或者请谁能解释一下如何正确解码?

整个项目托管在 GitHub

我使用回调在异步模式下解决了这个问题。

基本工作流程是:

  • 使用 MediaExtractor 从文件中提取编码数据
  • 将其传递给 MediaCodec 进行解码
  • 将解码后的数据传递给 AudioTrack 以重现它(或对数据做任何您想做的事)

首先我们需要一些初始化,我把它放在 class 我用来解码和复制文件的构造函数中:

// inizialize the mediaExtractor and set the source file
mediaExtractor = new MediaExtractor();
mediaExtractor.setDataSource(fileName);

// select the first audio track in the file and return it's format
mediaFormat = null;
int i;
int numTracks = mediaExtractor.getTrackCount();
for (i = 0; i < numTracks; i++)
{
    mediaFormat = mediaExtractor.getTrackFormat(i);
    if (mediaFormat.getString(MediaFormat.KEY_MIME).startsWith("audio/"))
    {
        mediaExtractor.selectTrack(i);
        break;
    }
}
// we get the parameter from the mediaFormat
channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
duration = mediaFormat.getLong(MediaFormat.KEY_DURATION);
mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);

// we can get the minimum buffer size from audioTrack passing the parameter of the audio
// to keep it safe it's good practice to create a buffer that is 8 times bigger
int minBuffSize = AudioTrack.getMinBufferSize(sampleRate,
                                              AudioFormat.CHANNEL_OUT_STEREO,
                                              AudioFormat.ENCODING_PCM_16BIT);

// to reproduce the data we need to initialize the audioTrack, by passing the audio parameter
// we use the MODE_STREAM so we can put more data dynamically with audioTrack.write()
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                            sampleRate,
                            AudioFormat.CHANNEL_OUT_STEREO,
                            AudioFormat.ENCODING_PCM_16BIT,
                            minBuffSize * 8,
                            AudioTrack.MODE_STREAM);

对于开发人员指南,我用来初始化 audioTrack 的方法已被弃用,但它对我有用,而新方法则不行,因此出于本示例的目的,我保留了这种类型的初始化。

在初始化阶段之后,我们需要创建解码器,为其设置回调,并启动解码器和 audioTrack。 mediaCodec 的回调是:

  • onInputBufferAvailable:当输入缓冲区可用时调用。
  • onOutputBufferAvailable:当输出缓冲区可用时调用。
  • onError:当 MediaCodec 遇到错误时调用
  • onOutputFormatChanged:当输出格式改变时调用

所以我们需要:

  • 使用提取器从文件中提取编码数据,并用它来填充我们从编解码器获得的输入缓冲区。
  • 编解码器完成解码后,我们可以从 outputBuffer 获取解码数据并将其传递给 audioTrack。

我的代码是:

// we get the mediaCodec by creating it using the mime_type extracted form the track
MediaCodec decoder = MediaCodec.createDecoderByType(mimeType);

// to decode the file in asynchronous mode we set the callbacks
decoder.setCallback(new MediaCodec.Callback()
{
    private boolean mOutputEOS = false;
    private boolean mInputEOS = false;

    @Override
    public void onInputBufferAvailable (@NonNull MediaCodec codec,
                                        int index)
    {
        // if i reached the EOS i either the input or the output file i just skip
        if (mOutputEOS | mInputEOS) return;

        // i must use the index to get the right ByteBuffer from the codec
        ByteBuffer inputBuffer = codec.getInputBuffer(index);

        // if the codec is null i just skip and wait for another buffer
        if (inputBuffer == null) return;

        long sampleTime = 0;
        int result;

        // with this method i fill the inputBuffer with the data read from the mediaExtractor
        result = mediaExtractor.readSampleData(inputBuffer, 0);
        // the return parameter of readSampleData is the number of byte read from the file
        // and if it's -1 it means that i reached EOS
        if (result >= 0)
        {
            // if i read some bytes i can pass the index of the buffer, the number of bytes
            // that are in the buffer and the sampleTime to the codec, so that it can decode
            // that data
            sampleTime = mediaExtractor.getSampleTime();
            codec.queueInputBuffer(index, 0, result, sampleTime, 0);
            mediaExtractor.advance();
        }
        else
        {
            // if i reached EOS i need to tell the codec
            codec.queueInputBuffer(index, 0, 0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            mInputEOS = true;
        }
    }

    @Override
    public void onOutputBufferAvailable (@NonNull MediaCodec codec,
                                         int index,
                                         @NonNull MediaCodec.BufferInfo info)
    {
        // i can get the outputBuffer from the codec using the relative index
        ByteBuffer outputBuffer = codec.getOutputBuffer(index);

        // if i got a non null buffer
        if (outputBuffer != null)
        {
            outputBuffer.rewind();
            outputBuffer.order(ByteOrder.LITTLE_ENDIAN);

            // i just need to write the outputBuffer into the audioTrack passing the number of
            // bytes it contain and using the WRITE_BLOCKING so that this call will block
            // until it doesn't finish to write the data
            int ret = audioTrack.write(outputBuffer,
                                       outputBuffer.remaining(),
                                       AudioTrack.WRITE_BLOCKING);
        }

        // if the flags in the MediaCodec.BufferInfo contains the BUFFER_FLAG_END_OF_STREAM
        // it mean that i reached EOS so i set mOutputEOS to true, and to assure
        // that it remain true even if this callback is called again i use the logical or
        mOutputEOS |= ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0);

        // i always need to release the buffer i use so the system can recycle them and use
        // it again
        codec.releaseOutputBuffer(index, false);

        // if i reached the end of the output stream i need to stop and release the codec
        // and the extractor
        if (mOutputEOS)
        {
            codec.stop();
            codec.release();
            mediaExtractor.release();
            audioTrack.release();
        }
    }

    @Override
    public void onError (@NonNull MediaCodec codec,
                         @NonNull MediaCodec.CodecException e)
    {
        Timber.e(e, "mediacodec collback onError: %s", e.getMessage());
    }

    @Override
    public void onOutputFormatChanged (@NonNull MediaCodec codec,
                                       @NonNull MediaFormat format)
    {
        Timber.d("onOutputFormatChanged: %s", format.toString());
    }

});
// now we can configure the codec by passing the mediaFormat and start it
decoder.configure(mediaFormat, null, null, 0);
decoder.start();
// also we need to start the audioTrack.
audioTrack.play();