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 套件。支持 Android、Flutter、iOS、Linux、macOS、React Native 和 tvOS。取代 MobileFFmpeg、flutter_ffmpeg 和 react-native-ffmpeg。
ffmpeg-kit
Test Demo
mavenCentral
存储库并将FFmpegKit
依赖项添加到您的模式build.gradle
中 ffmpeg-kit-
。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'
}
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()));
}
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();
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
}
});
执行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
}
});
MediaInformationSession mediaInformation = FFprobeKit.getMediaInformation("" );
mediaInformation.getMediaInformation();
停止正在进行的FFmpeg
操作。
FFmpegKit.cancel();
FFmpegKit.cancel(sessionId);
将存储访问框架 (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);
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()));
}
启用全局回调。
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) {
...
}
});
Mono
使用 和 的框架需要Mono
,例如Unity
和Xamarin
。FFmpegKitConfig.ignoreSignal(Signal.SIGXCPU);
FFmpegKitConfig.setFontDirectoryList(context, Arrays.asList("/system/fonts", "" ), Collections.EMPTY_MAP);
根据选择的音频编码器进行编码的命令
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);
}
}