Android 原生不支持 MP3 格式解决方案

相关背景

Android 多媒体框架之音频录制 MediaRecorder 和 AudioRecorder,前者用于录制普通音频,后者用于录制原始音频。然而无论是普通音频的amr和aac格式,还是原始音频的pcm格式,都不能在电脑上直接播放,也不能在苹果手机上播放,因为它们属于安卓手机的定制格式,并非通用的音频格式。
若想让录音文件能播放,就得事先将其转为通用的MP3格式,虽然Android官方的开发包不支持MP3转换,不过借助第三方库进行MP3音频编解码,能够将原始音频转存为MP3文件。

Android 原生的 MediaRecorder 不支持直接录制为 MP3 格式音频,而只支持一些特定的音频编码格式。但是,Android 原生的多媒体框架 MediaPlayer 是支持播放 MP3 格式音频的。这是因为 MediaPlayer 类内部集成了各种解码器,包括对 MP3 格式的支持。当您使用 MediaPlayer 播放 MP3 文件时,它会自动识别并使用合适的解码器将 MP3 音频解码为原始音频数据,然后通过音频输出进行播放。

问题描述

使用MediaRecorder录制的MP3音频文件,非标准的通用的MP3格式,需要重新编码MP3格式

mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
outputFile = Environment.getExternalStorageDirectory().getAbsolutePath() + "/recording.mp3";
mediaRecorder.setOutputFile(outputFile);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

解决方案

使用ffmpeg-kit库,将 MediaRecorder 录制完成的的音频文件重新编码MP3音频,使用的是libmp3lame编码器

import com.arthenica.ffmpegkit.FFmpegKit;
import com.arthenica.ffmpegkit.FFmpegSession;
import com.arthenica.ffmpegkit.ReturnCode;

public static void reEncode(String audioSampleFile, String audioOutputFile) {
    
    
    String ffmpegCommand = String.format("-hide_banner -y -i %s -c:a libmp3lame -qscale:a 2 %s", audioSampleFile, audioOutputFile);
    FFmpegSession session = FFmpegKit.execute(ffmpegCommand);
    if (ReturnCode.isSuccess(session.getReturnCode())) {
    
    
        Log.d(TAG, "Command SUCCESS");
    } else if (ReturnCode.isCancel(session.getReturnCode())) {
    
    
        Log.d(TAG, "Command CANCEL");
    } else {
    
    
        Log.d(TAG, String.format("Command failed with state %s and rc %s.%s", session.getState(), session.getReturnCode(), 
session.getFailStackTrace()));
    }
}

这是一个 FFmpeg 的命令行格式的字符串:

"ffmpeg -hide_banner -y -i %s -c:a libmp3lame -qscale:a 2 %s"

下面解释每个参数的含义:

  • -hide_banner: 隐藏 FFmpeg 的版本和配置信息的横幅。
  • -y: 自动覆盖输出文件,如果输出文件已存在。
  • -i %s: 输入文件路径的占位符。%s 是一个格式化字符串占位符,后面会被实际的输入文件路径替代。
  • -c:a libmp3lame: 指定音频编码器为 libmp3lame,使用 MP3 格式进行音频编码。
  • -qscale:a 2: 设置音频质量。-qscale:a 参数用于指定音频质量的参数,取值范围一般是 0-9 或 0-10,其中 0 表示最高质量,9 或 10 表示最低质量。在这个示例中,设置为 2 表示较高的音频质量。

该命令行的目的是将输入文件(audioSampleFile)使用 libmp3lame 编码器编码为 MP3 格式的音频文件,并指定输出文件路径为 audioOutputFile。具体的输入文件路径和输出文件路径将通过替换 %s 占位符来提供。

关于 ffmpeg-kit

适用于应用程序的 FFmpeg 套件。支持 Android、Flutter、iOS、Linux、macOS、React Native 和 tvOS。取代 MobileFFmpeg、flutter_ffmpeg 和 react-native-ffmpeg。

安卓API

  1. 声明mavenCentral存储库并将FFmpegKit依赖项添加到您的模式build.gradleffmpeg-kit-<package name>FFmpegKit使用项目README中给出的包名称之一。
   repositories {
    
    
       mavenCentral()
   }
   
   dependencies {
    
    
       // full version
       implementation 'com.arthenica:ffmpeg-kit-full:5.1'
       // audio version
       implementation 'com.arthenica:ffmpeg-kit-audio:5.1.LTS'
       // video version
       implementation 'com.arthenica:ffmpeg-kit-video:5.1.LTS'
   }
  1. 执行同步FFmpeg命令。
   import com.arthenica.ffmpegkit.FFmpegKit;
   
   FFmpegSession session = FFmpegKit.execute("-i file1.mp4 -c:v mpeg4 file2.mp4");
   if (ReturnCode.isSuccess(session.getReturnCode())) {
    
    
   
       // SUCCESS
   
   } else if (ReturnCode.isCancel(session.getReturnCode())) {
    
    
   
       // CANCEL
   
   } else {
    
    
   
       // FAILURE
       Log.d(TAG, String.format("Command failed with state %s and rc %s.%s", session.getState(), session.getReturnCode(), session.getFailStackTrace()));
   
   }
  1. 每个execute调用(同步或异步)都会创建一个新会话。从创建的会话中访问有关执行的每个详细信息。
   FFmpegSession session = FFmpegKit.execute("-i file1.mp4 -c:v mpeg4 file2.mp4");
   
   // Unique session id created for this execution
   long sessionId = session.getSessionId();
   
   // Command arguments as a single string
   String command = session.getCommand();
   
   // Command arguments
   String[] arguments = session.getArguments();
   
   // State of the execution. Shows whether it is still running or completed
   SessionState state = session.getState();
   
   // Return code for completed sessions. Will be null if session is still running or ends with a failure
   ReturnCode returnCode = session.getReturnCode();
   
   Date startTime = session.getStartTime();
   Date endTime = session.getEndTime();
   long duration = session.getDuration();
   
   // Console output generated for this execution
   String output = session.getOutput();
   
   // The stack trace if FFmpegKit fails to run a command
   String failStackTrace = session.getFailStackTrace();
   
   // The list of logs generated for this execution
   List<com.arthenica.ffmpegkit.Log> logs = session.getLogs();
   
   // The list of statistics generated for this execution
   List<Statistics> statistics = session.getStatistics();
  1. 通过提供会话特定的回调来执行异步FFmpeg命令。execute``log``session
   FFmpegKit.executeAsync("-i file1.mp4 -c:v mpeg4 file2.mp4", new FFmpegSessionCompleteCallback() {
    
    
   
       @Override
       public void apply(FFmpegSession session) {
    
    
           SessionState state = session.getState();
           ReturnCode returnCode = session.getReturnCode();
   
           // CALLED WHEN SESSION IS EXECUTED
   
           Log.d(TAG, String.format("FFmpeg process exited with state %s and rc %s.%s", state, returnCode, session.getFailStackTrace()));
       }
   }, new LogCallback() {
    
    
   
       @Override
       public void apply(com.arthenica.ffmpegkit.Log log) {
    
    
   
           // CALLED WHEN SESSION PRINTS LOGS
   
       }
   }, new StatisticsCallback() {
    
    
   
       @Override
       public void apply(Statistics statistics) {
    
    
   
           // CALLED WHEN SESSION GENERATES STATISTICS
   
       }
   });
  1. 执行FFprobe命令。

    • 同步
   FFprobeSession session = FFprobeKit.execute(ffprobeCommand);
   
   if (!ReturnCode.isSuccess(session.getReturnCode())) {
    
    
       Log.d(TAG, "Command failed. Please check output for the details.");
   }
  • 异步
   FFprobeKit.executeAsync(ffprobeCommand, new FFprobeSessionCompleteCallback() {
    
    
   
       @Override
       public void apply(FFprobeSession session) {
    
    
   
           CALLED WHEN SESSION IS EXECUTED
   
       }
   });
  1. 获取文件的媒体信息。
   MediaInformationSession mediaInformation = FFprobeKit.getMediaInformation("<file path or uri>");
   mediaInformation.getMediaInformation();
  1. 停止正在进行的FFmpeg操作。

    • 停止所有执行
FFmpegKit.cancel();
  • 停止特定会话
FFmpegKit.cancel(sessionId);
  1. 将存储访问框架 (SAF) Uris 转换为可由FFmpegKit.

    • 读取文件:
     Uri safUri = intent.getData();
     String inputVideoPath = FFmpegKitConfig.getSafParameterForRead(requireContext(), safUri);
     FFmpegKit.execute("-i " + inputVideoPath + " -c:v mpeg4 file2.mp4");
  • 写入文件:
     Uri safUri = intent.getData();
     String outputVideoPath = FFmpegKitConfig.getSafParameterForWrite(requireContext(), safUri);
     FFmpegKit.execute("-i file1.mp4 -c:v mpeg4 " + outputVideoPath);
  • 以自定义模式写入文件。
     Uri safUri = intent.getData();
     String path = FFmpegKitConfig.getSafParameter(requireContext(), safUri, "rw");
     FFmpegKit.execute("-i file1.mp4 -c:v mpeg4 " + path);
  1. 从会话历史记录中获取上一个会话FFmpeg和会话。FFprobe
   List<Session> sessions = FFmpegKitConfig.getSessions();
   for (int i = 0; i < sessions.size(); i++) {
    
    
       Session session = sessions.get(i);
       Log.d(TAG, String.format("Session %d = id:%d, startTime:%s, duration:%s, state:%s, returnCode:%s.",
             i,
             session.getSessionId(),
             session.getStartTime(),
             session.getDuration(),
             session.getState(),
             session.getReturnCode()));
   }
  1. 启用全局回调。

    • 会话类型特定的完整回调,在异步会话完成时调用
      FFmpegKitConfig.enableFFmpegSessionCompleteCallback(new FFmpegSessionCompleteCallback() {
    
    
      
          @Override
          public void apply(FFmpegSession session) {
    
    
      
          }
      });
      
      FFmpegKitConfig.enableFFprobeSessionCompleteCallback(new FFprobeSessionCompleteCallback() {
    
    
      
          @Override
          public void apply(FFprobeSession session) {
    
    
      
          }
      });
      
      FFmpegKitConfig.enableMediaInformationSessionCompleteCallback(new MediaInformationSessionCompleteCallback() {
    
    
      
          @Override
          public void apply(MediaInformationSession session) {
    
    
      
          }
      });
- 日志回调,会话产生日志时调用
      FFmpegKitConfig.enableLogCallback(new LogCallback() {
    
    
      
          @Override
          public void apply(final com.arthenica.ffmpegkit.Log log) {
    
    
              ...
          }
      });
- 统计回调,当会话生成统计数据时调用
      FFmpegKitConfig.enableStatisticsCallback(new StatisticsCallback() {
    
    
      
          @Override
          public void apply(final Statistics newStatistics) {
    
    
              ...
          }
      });
  1. 忽略信号的处理。Mono使用 和 的框架需要Mono,例如UnityXamarin
FFmpegKitConfig.ignoreSignal(Signal.SIGXCPU);
  1. 注册系统字体和自定义字体目录。
FFmpegKitConfig.setFontDirectoryList(context, Arrays.asList("/system/fonts", "<folder with fonts>"), Collections.EMPTY_MAP);

Audio API

根据选择的音频编码器进行编码的命令

public String generateAudioEncodeScript() {
    
    
    String audioCodec = selectedCodec;
    String audioSampleFile = getAudioSampleFile().getAbsolutePath();
    String audioOutputFile = getAudioOutputFile().getAbsolutePath();

    switch (audioCodec) {
    
    
        case "mp2 (twolame)":
            return String.format("-hide_banner -y -i %s -c:a mp2 -b:a 192k %s", audioSampleFile, audioOutputFile);
        case "mp3 (liblame)":
            return String.format("-hide_banner -y -i %s -c:a libmp3lame -qscale:a 2 %s", audioSampleFile, audioOutputFile);
        case "mp3 (libshine)":
            return String.format("-hide_banner -y -i %s -c:a libshine -qscale:a 2 %s", audioSampleFile, audioOutputFile);
        case "vorbis":
            return String.format("-hide_banner -y -i %s -c:a libvorbis -b:a 64k %s", audioSampleFile, audioOutputFile);
        case "opus":
            return String.format("-hide_banner -y -i %s -c:a libopus -b:a 64k -vbr on -compression_level 10 %s", audioSampleFile, audioOutputFile);
        case "amr-nb":
            return String.format("-hide_banner -y -i %s -ar 8000 -ab 12.2k -c:a libopencore_amrnb %s", audioSampleFile, audioOutputFile);
        case "amr-wb":
            return String.format("-hide_banner -y -i %s -ar 8000 -ab 12.2k -c:a libvo_amrwbenc -strict experimental %s", audioSampleFile, audioOutputFile);
        case "ilbc":
            return String.format("-hide_banner -y -i %s -c:a ilbc -ar 8000 -b:a 15200 %s", audioSampleFile, audioOutputFile);
        case "speex":
            return String.format("-hide_banner -y -i %s -c:a libspeex -ar 16000 %s", audioSampleFile, audioOutputFile);
        case "wavpack":
            return String.format("-hide_banner -y -i %s -c:a wavpack -b:a 64k %s", audioSampleFile, audioOutputFile);
        default:

            // soxr
            return String.format("-hide_banner -y -i %s -af aresample=resampler=soxr -ar 44100 %s", audioSampleFile, audioOutputFile);
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_44008788/article/details/131809525
今日推荐