Android 上的 MP3 解码

Posted

技术标签:

【中文标题】Android 上的 MP3 解码【英文标题】:MP3 Decoding on Android 【发布时间】:2011-01-30 11:19:13 【问题描述】:

我们正在为 android 手机实施一个程序,该程序可以播放来自互联网的音频流。以下是我们的大致做法:

    下载自定义加密格式。 解密以获得常规 MP3 数据块。 将 MP3 数据解码为内存缓冲区中的原始 PCM 数据。 将原始 PCM 数据通过管道传输到 AudioTrack

到目前为止,我们的目标设备是 Droid 和 Nexus One。在 Nexus One 上一切正常,但在 Droid 上 MP3 解码太慢了。如果我们将 Droid 置于负载下,音频播放就会开始跳过。我们不允许将 MP3 数据解码到 SD 卡,但我知道这不是我们的问题。

我们没有编写自己的 MP3 解码器,而是使用了 MPADEC (http://sourceforge.net/projects/mpadec/)。它是免费的,并且很容易与我们的程序集成。我们用 NDK 编译它。

在使用各种分析工具进行详尽分析后,我们确信是这个解码器落后了。

以下是我们正在考虑的选项:

    找到另一个我们可以使用 Android NDK 编译的 MP3 解码器。此 MP3 解码器必须进行优化以在移动 ARM 设备上运行,或者可能使用仅整数数学或其他一些优化来提高性能。

    由于内置的​​ Android MediaPlayer 服务将获取 URL,我们可以在我们的程序中实现一个小型 HTTP 服务器,并为 MediaPlayer 提供解密的 MP3。这样我们就可以利用内置的 MP3 解码器。

    通过 NDK 访问内置 MP3 解码器。我不知道这是否可能。

有人对我们可以做些什么来加快 MP3 解码有任何建议吗?

-- 罗伯斯

【问题讨论】:

在您的选项 #2 中,我希望 HTTP 开销会淹没您使用内置 MediaPlayer 流支持所获得的收益。 如果我没记错的话,内置的MediaPlayer 也可以从任何content:// URI 流式传输,这实际上为您提供了一个可以写入的管道。 @jleedev:你能详细说明一下吗?听起来很有趣! @Rob 对您最终选择的方法非常感兴趣。现在正在研究同样的情况。 【参考方案1】:

执行此操作的正确方法是构建您自己的固件并作为自定义 OpenCORE 编解码器的一部分进行解密。当然,这会将您限制在可以安装该固件的设备上。

请记住,剩下的部分是推测性的。我实际上需要做一些类似于你所描述的事情,但我没有时间留出几个月来解决这个问题。所以,我将以我如何解决问题的方式来描述这一点。

一种解决方案是 twk 的答案中描述的解决方案。您不必使用 SD 卡,但您可能必须在应用程序本地文件存储 (getFilesDir()) 中有一个全球可读的临时文件。下载第一个块,解密它,将它写成一个完整的世界可读的 MP3 文件(但具有适当的模糊目录/路径),然后通过setDataSource() 将其交给MediaPlayer。在播放时,您下载/解密并设置第二个MediaPlayer 实例,它会在第一个实例结束后立即开始播放,以实现尽可能无缝的过渡。然后您重置第一个 MediaPlayer 并将其与您的第三个块一起重用,在两者之间进行乒乓操作。

jleedev 的评论中有一个相关的解决方案。这几乎是一回事,只是您通过ContentProvider 提供FileDescriptor。这有一个选项让您使用套接字,可能让您避免临时文件。但是,ContentProvider 本身必须是可公开访问的,因此具有不明确目录的临时文件实际上可能更私密。

如果您担心这些东西可以被其他进程读取,请理解MediaPlayer 本身(或者更确切地说,OpenCORE 子系统)在另一个进程中。此外,您建议的 HTTP 服务器在设备上也是全球可读的。因此,如果您打算让MediaPlayer 进行解码,那么通过默默无闻的安全性是您唯一可行的选择。

AFAIK,NDK 不授予对 OpenCORE 的访问权限,尽管我承认 NDK 经验有限,所以我可能错了。当然还有其他可用的 MP3 解码器(ffmpeg/mplayer 等),不过这些转换成 NDK 库的难易程度尚不清楚。

因此,这真的归结为您要防御的对象。如果你想保护用户,你可能不得不自己解码,不知何故。

【讨论】:

MediaPlayer.create(context, uri) ??【参考方案2】:

MAD 是一个仅限整数运算的解码器库,似乎备受推崇。

(将数据解码到 SD 卡上不会减慢您的速度吗?)

【讨论】:

请注意,MAD 在 GPL 下。【参考方案3】:

这个我没试过,但是我相信你可以给媒体播放器一个文件描述符:

public void setDataSource (FileDescriptor fd, long offset, long length)

也许您可以将解密后的 mp3 写入文件并使用文件描述符播放。假设您提前知道文件长度,这甚至可能适用于流式传输。如果它有效,它会比做一个本地主机网络服务器简单得多。

【讨论】:

也许你没有注意到他的评论:“我们不允许将 MP3 数据解码到 SD 卡”。出于版权原因和数字版权管理(我认为)。 也许你没有注意到 twk 没有提到 SD 卡。 啊,对了——我确实错过了。不过,也许您可​​以使用 pipe() 来获取一些链接的文件描述符。【参考方案4】:

在播放之前解密加密的 mp3 文件。有3种方式:

1、使用MediaPlayer api:

Android M 版之后:

you can play mp3 data in byte array directly by:

//Read encrypted mp3:
byte[] bytesAudioData =  Utils.readFile(path); // or from a url.

final byte[] bytesAudioDataDecrypted = YourUtils.decryptFileToteArray(bytesAudioData);

                ByteArrayMediaDataSource bds = new ByteArrayMediaDataSource(bytesAudioDataDecrypted) ;
                mediaPlayer.setDataSource(bds);

在安卓版本M之前:

//Read encrypted mp3:
byte[] bytesAudioData =  Utils.readFile(path); // or from a url.

final byte[] bytesAudioDataDecrypted = YourUtils.decryptFileToteArray(bytesAudioData);

 File file =new File(dirofyouraudio.mp3");

                FileOutputStream fos = new FileOutputStream(file);
                fos.write(bytesAudioDataDecrypted);
                Log.d(TAG, "playSound :: file write to temp :: System. nanoTime() ="+System. nanoTime());
                fos.close();

                FileInputStream fis = new FileInputStream(file);
                mediaPlayer.setDataSource(fis.getFD());

2,如果你使用AudioTrack api:

  int minBufferSize = AudioTrack.getMinBufferSize(sampleRate,
            AudioFormat.CHANNEL_OUT_MONO,
            AudioFormat.ENCODING_PCM_16BIT);

   AudioTrack track = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
            AudioFormat.CHANNEL_OUT_MONO,
            AudioFormat.ENCODING_PCM_16BIT, minBufferSize,
            AudioTrack.MODE_STREAM);

    int i = 0;
    byte[] tempMinBufferToRead = new byte[minBufferSize];

    try 

        byte[] bytesAudioData = Utils.readFile( lastRecordedAudiofilePath);

        bytesAudioData = yourUtils.decryptYourFileToByteArray(bytesAudioData);

        try 
            fileInputStremForPlay = new FileInputStream( lastRecordedAudiofilePath);

            dataInputStreamForPlay = new DataInputStream(new ByteArrayInputStream(bytesAudioData));

            track.play();
            while ((i = dataInputStreamForPlay.read(tempMinBufferToRead, 0, minBufferSize)) > -1) 
                Log.d(LOG_TAG, "mThreadExitFlag= " + mThreadExitFlag);

                if ((mThreadExitFlag == true) || (!audioFilePathInThisThread.equals(lastRecordedAudiofilePath))) 

                    m_handler.post(new Runnable() 
                        public void run() 
                            Log.d(LOG_TAG, "startPlaying: break and reset play button ");

                            startPlayBtnInToolbar.setEnabled(true);
                            stopPlayBtnInToolbar.setEnabled(false);
                        
                    );

                    break;
                

                track.write(tempMinBufferToRead, 0, i);
            

            Log.d(LOG_TAG, "===== Playing Audio Completed ===== ");
            track.stop();

            track.release();

            dataInputStreamForPlay.close();

            fileInputStremForPlay.close();


         catch (FileNotFoundException e) 
            e.printStackTrace();
         catch (IOException e) 
            e.printStackTrace();
        

3,使用 MediaExtractor , MediaCodec 和 AudioTrack ,您可以使用提取器设置数据源:

            extractor.setDataSource(this.url);

或者

            extractor.setDataSource(this.Mediadatasource);

ps: ByteArrayMediaDataSource 类:

import android.annotation.TargetApi;
import android.media.MediaDataSource;
import android.os.Build;
import java.io.IOException;



import android.content.res.AssetFileDescriptor;
 import android.media.MediaDataSource;
import android.util.Log;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;

@TargetApi(Build.VERSION_CODES.M)
public class ByteArrayMediaDataSource extends MediaDataSource 

    private static final String TAG = ByteArrayMediaDataSource.class.getSimpleName();


    private   byte[] data;

    public ByteArrayMediaDataSource(byte []data) 
//        assert data != null;
        this.data = data;
    
    @Override
    public int readAt(long position, byte[] buffer, int offset, int size) throws IOException 


        if (position >= data.length) 
            return -1; // -1 indicates EOF
        
        if (position + size > data.length) 
            size -= (position + size) - data.length;
        

        Log.d(TAG, "MediaDataSource size = "+size);


        System.arraycopy(data, (int)position, buffer, offset, size);
        return size;

    



    @Override
    public long getSize() throws IOException 

        Log.d(TAG, "MediaDataSource data.length = "+data.length);

        return data.length;
    

    @Override
    public void close() throws IOException 
        // Nothing to do here
        data =null;
    

【讨论】:

以上是关于Android 上的 MP3 解码的主要内容,如果未能解决你的问题,请参考以下文章

android音视频音频mp3剪切

Android 端音频变声方案

Android 上的 HTML5 <audio> 标签

[gitbook] Android框架分析系列之Android stagefright框架

[gitbook] Android框架分析系列之Android stagefright框架

如何在 Android 上的 Termux 中将麦克风录制到 mp3?