Android
Android Supported Media Formats : http://developer.android.com/guide/appendix/media-formats.html
iOS
The Basics: Audio Codecs, Supported Audio Formats, and Audio Sessions : http://developer.apple.com/library/ios/#documentation/AudioVideo/Conceptual/MultimediaPG/UsingAudio/UsingAudio.html
总结
对比 Android 与 iOS 所支持的音频格式,如果需要跨平台进行音频数据交换,只有 AAC 和 Linear PCM 可以选择
AAC 对音频进行压缩,音频数据较小
Linear PCM未对音频进行压缩,实时性更好,但音频数据较大
近期在
做一个有关于语音播放的项目,其中用到了android录音部分,查了好多资料只能录制amr和3gp格式,不能录制mp3格式;IOS端遇到同样问题,
只能录制caf格式,不能录制mp3,所以通用性就得到了考验。在痛苦中挣扎,在烦恼中度过,终于在苦思冥想中,解决了这个问题,总结核心部分如下:
无论是android还是IOS都是同一个思路,android中先想办法录制wav格式,然后通过lame进行转换。IOS是先录制caf文件,然后通过lame转换成mp3格式。
lame是一个mp3的免费格式库,baidu或者google都可以查到源代码,是用c写的。
在开发过程中,由于IOS可以直接录制成caf文件,但是android录制wav遇到了
困难。大家肯定会问为什么不用3gp或者amr直接转换成mp3呢?我最开始也是这样想的,但是经过无数次3gp ||
amr进行lame转换,发现都不成功,最终确认3gp || amr通过lame转换MP3格式行不通。
================================ IOS
part =============================================
相对来说,IOS的转换比较简单,下载编译好的lame库文件,libmp3lame.a放在Frameworks下面,把lame.h这个头文件引入项目中,在项目中转换函数如下,其中需要指定被转换和转换后文件路径,视项目需要而定:
//转换Mp3格式方法
- (IBAction)toMp3 {
NSString *mp3AudioPath = [[NSString stringWithFormat:@"%@/%@.mp3", DOCUMENTS_FOLDER, @"temp"] retain]; //新转换mp3文件路径
//进入转换
int read, write;
FILE *pcm = fopen([recorderFilePath cStringUsingEncoding:1], "rb");//被转换的文件
FILE *mp3 = fopen([mp3AudioPath cStringUsingEncoding:1], "wb");//转换后文件的存放位置
const int PCM_SIZE = 8192;
const int MP3_SIZE = 8192;
short int pcm_buffer[PCM_SIZE*2];
unsigned char mp3_buffer[MP3_SIZE];
lame_t lame = lame_init();
lame_set_in_samplerate(lame, 44100);
lame_set_VBR(lame, vbr_default);
lame_init_params(lame);
do {
read = fread(pcm_buffer, 2*sizeof(short int), PCM_SIZE, pcm);
if (read == 0)
write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
else
write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
fwrite(mp3_buffer, write, 1, mp3);
} while (read != 0);
lame_close(lame);
fclose(mp3);
fclose(pcm);
}
- (IBAction)toMp3 {
NSString *mp3AudioPath = [[NSString stringWithFormat:@"%@/%@.mp3", DOCUMENTS_FOLDER, @"temp"] retain]; //新转换mp3文件路径
//进入转换
int read, write;
FILE *pcm = fopen([recorderFilePath cStringUsingEncoding:1], "rb");//被转换的文件
FILE *mp3 = fopen([mp3AudioPath cStringUsingEncoding:1], "wb");//转换后文件的存放位置
const int PCM_SIZE = 8192;
const int MP3_SIZE = 8192;
short int pcm_buffer[PCM_SIZE*2];
unsigned char mp3_buffer[MP3_SIZE];
lame_t lame = lame_init();
lame_set_in_samplerate(lame, 44100);
lame_set_VBR(lame, vbr_default);
lame_init_params(lame);
do {
read = fread(pcm_buffer, 2*sizeof(short int), PCM_SIZE, pcm);
if (read == 0)
write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
else
write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
fwrite(mp3_buffer, write, 1, mp3);
} while (read != 0);
lame_close(lame);
fclose(mp3);
fclose(pcm);
}
至此,新的mp3文件已经生成。
================================
android part ===============================================
android录制wav用到了一个文件ExtAudioRecorder.java,代码如下:
package com.example.util;
import java.io.File;
import java.io.IOException;
import
java.io.RandomAccessFile;
import
android.media.AudioFormat;
import
android.media.AudioRecord;
import
android.media.MediaRecorder;
import
android.media.MediaRecorder.AudioSource;
import android.util.Log;
public class ExtAudioRecorder
{
private final
static int[] sampleRates = {44100, 22050, 11025, 8000};
public static
ExtAudioRecorder getInstanse(Boolean recordingCompressed)
{
ExtAudioRecorder result = null;
if(recordingCompressed)
{
result = new ExtAudioRecorder( false,
AudioSource.MIC,
sampleRates[3],
AudioFormat.CHANNEL_IN_STEREO,
//AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT);
}
else
{
int i=0;
do
{
result = new ExtAudioRecorder( true,
AudioSource.MIC,
sampleRates[i],
AudioFormat.CHANNEL_CONFIGURATION_STEREO,
AudioFormat.ENCODING_PCM_16BIT);
} while((++i<sampleRates.length) & !(result.getState() ==
ExtAudioRecorder.State.INITIALIZING));
}
return result;
}
/**
* INITIALIZING :
recorder is initializing;
* READY :
recorder has been initialized, recorder not yet started
* RECORDING :
recording
* ERROR :
reconstruction needed
* STOPPED: reset
needed
*/
public enum State
{INITIALIZING, READY, RECORDING, ERROR, STOPPED};
public static
final boolean RECORDING_UNCOMPRESSED = true;
public static
final boolean RECORDING_COMPRESSED = false;
// The interval
in which the recorded samples are output to the file
// Used only in
uncompressed mode
private static
final int TIMER_INTERVAL = 120;
// Toggles
uncompressed recording on/off; RECORDING_UNCOMPRESSED /
RECORDING_COMPRESSED
private
boolean
rUncompressed;
// Recorder used
for uncompressed recording
private
AudioRecord audioRecorder = null;
// Recorder used
for compressed recording
private
MediaRecorder mediaRecorder = null;
// Stores current
amplitude (only in uncompressed mode)
private
int
cAmplitude= 0;
// Output file
path
private
String
filePath = null;
// Recorder
state; see State
private
State
state;
// File writer
(only in uncompressed mode)
private
RandomAccessFile randomAccessWriter;
// Number of
channels, sample rate, sample size(size in bits), buffer size, audio
source, sample size(see AudioFormat)
private
short
nChannels;
private
int
sRate;
private
short
bSamples;
private
int
bufferSize;
private
int
aSource;
private
int
aFormat;
// Number of
frames written to file on each output(only in uncompressed
mode)
private
int
framePeriod;
// Buffer for
output(only in uncompressed mode)
private
byte[]
buffer;
// Number of
bytes written to file after header(only in uncompressed mode)
// after stop()
is called, this size is written to the header/data chunk in the wave
file
private
int
payloadSize;
/**
*
* Returns the
state of the recorder in a RehearsalAudioRecord.State typed
object.
* Useful, as no
exceptions are thrown.
*
* @return
recorder state
*/
public State
getState()
{
return state;
}
/*
*
* Method used for
recording.
*
*/
private
AudioRecord.OnRecordPositionUpdateListener updateListener = new
AudioRecord.OnRecordPositionUpdateListener()
{
public void onPeriodicNotification(AudioRecord recorder)
{
audioRecorder.read(buffer, 0, buffer.length); // Fill buffer
try
{
randomAccessWriter.write(buffer); // Write buffer to file
payloadSize += buffer.length;
if (bSamples == 16)
{
for (int i=0; i<buffer.length/2; i++)
{ // 16bit sample size
short curSample = getShort(buffer[i*2], buffer[i*2+1]);
if (curSample > cAmplitude)
{ // Check amplitude
cAmplitude = curSample;
}
}
}
else
{ // 8bit sample size
for (int i=0; i<buffer.length; i++)
{
if (buffer[i] > cAmplitude)
{ // Check amplitude
cAmplitude = buffer[i];
}
}
}
}
catch (IOException e)
{
e.printStackTrace();
Log.e(ExtAudioRecorder.class.getName(), "Error occured in
updateListener, recording is aborted");
//stop();
}
}
public void onMarkerReached(AudioRecord recorder)
{
// NOT USED
}
};
/**
*
*
* Default
constructor
*
*
Instantiates a new recorder, in case of compressed recording the
parameters can be left as 0.
* In case
of errors, no exception is thrown, but the state is set to
ERROR
*
*/
public
ExtAudioRecorder(boolean uncompressed, int audioSource, int
sampleRate, int channelConfig, int audioFormat)
{
try
{
rUncompressed = uncompressed;
if (rUncompressed)
{ // RECORDING_UNCOMPRESSED
if (audioFormat == AudioFormat.ENCODING_PCM_16BIT)
{
bSamples = 16;
}
else
{
bSamples = 8;
}
if (channelConfig == AudioFormat.CHANNEL_CONFIGURATION_MONO)
{
nChannels = 1;
}
else
{
nChannels = 2;
}
aSource = audioSource;
sRate = sampleRate;
aFormat = audioFormat;
framePeriod = sampleRate * TIMER_INTERVAL / 1000;
bufferSize = framePeriod * 2 * bSamples * nChannels / 8;
if (bufferSize < AudioRecord.getMinBufferSize(sampleRate,
channelConfig, audioFormat))
{ // Check to make sure buffer size is not smaller than the smallest
allowed one
bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig,
audioFormat);
// Set frame period and timer interval accordingly
framePeriod = bufferSize / ( 2 * bSamples * nChannels / 8 );
Log.w(ExtAudioRecorder.class.getName(), "Increasing buffer size to "
+ Integer.toString(bufferSize));
}
audioRecorder = new AudioRecord(audioSource, sampleRate,
channelConfig, audioFormat, bufferSize);
if (audioRecorder.getState() != AudioRecord.STATE_INITIALIZED)
throw new Exception("AudioRecord initialization failed");
audioRecorder.setRecordPositionUpdateListener(updateListener);
audioRecorder.setPositionNotificationPeriod(framePeriod);
} else
{ // RECORDING_COMPRESSED
mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
}
cAmplitude = 0;
filePath = null;
state = State.INITIALIZING;
} catch (Exception e)
{
e.printStackTrace();
if (e.getMessage() != null)
{
Log.e(ExtAudioRecorder.class.getName(), e.getMessage());
}
else
{
Log.e(ExtAudioRecorder.class.getName(), "Unknown error occured while
initializing recording");
}
state = State.ERROR;
}
}
/**
* Sets
output file path, call directly after construction/reset.
*
* @param
output file path
*
*/
public void
setOutputFile(String argPath)
{
try
{
if (state == State.INITIALIZING)
{
filePath = argPath;
if (!rUncompressed)
{
mediaRecorder.setOutputFile(filePath);
}
}
}
catch (Exception e)
{
e.printStackTrace();
if (e.getMessage() != null)
{
Log.e(ExtAudioRecorder.class.getName(), e.getMessage());
}
else
{
Log.e(ExtAudioRecorder.class.getName(), "Unknown error occured while
setting output path");
}
state = State.ERROR;
}
}
/**
*
* Returns
the largest amplitude sampled since the last call to this
method.
*
* @return
returns the largest amplitude since the last call, or 0 when not in
recording state.
*
*/
public int
getMaxAmplitude()
{
if (state == State.RECORDING)
{
if (rUncompressed)
{
int result = cAmplitude;
cAmplitude = 0;
return result;
}
else
{
try
{
return mediaRecorder.getMaxAmplitude();
}
catch (IllegalStateException e)
{
e.printStackTrace();
return 0;
}
}
}
else
{
return 0;
}
}
/**
*
* Prepares the
recorder for recording, in case the recorder is not in the
INITIALIZING state and the file path was not set
* the recorder is
set to the ERROR state, which makes a reconstruction
necessary.
* In case
uncompressed recording is toggled, the header of the wave file is
written.
* In case of an
exception, the state is changed to ERROR
*
*/
public void
prepare()
{
try
{
if (state == State.INITIALIZING)
{
if (rUncompressed)
{
if ((audioRecorder.getState() == AudioRecord.STATE_INITIALIZED)
& (filePath != null))
{
// write file header
randomAccessWriter = new RandomAccessFile(filePath, "rw");
randomAccessWriter.setLength(0); // Set file length to 0, to
prevent unexpected behavior in case the file already existed
randomAccessWriter.writeBytes("RIFF");
randomAccessWriter.writeInt(0); // Final file size not known yet,
write 0
randomAccessWriter.writeBytes("WAVE");
randomAccessWriter.writeBytes("fmt ");
randomAccessWriter.writeInt(Integer.reverseBytes(16)); // Sub-chunk
size, 16 for PCM
randomAccessWriter.writeShort(Short.reverseBytes((short) 1)); //
AudioFormat, 1 for PCM
randomAccessWriter.writeShort(Short.reverseBytes(nChannels));//
Number of channels, 1 for mono, 2 for stereo
randomAccessWriter.writeInt(Integer.reverseBytes(sRate)); // Sample
rate
randomAccessWriter.writeInt(Integer.reverseBytes(sRate*bSamples*nChannels/8));
// Byte rate, SampleRate*NumberOfChannels*BitsPerSample/8
randomAccessWriter.writeShort(Short.reverseBytes((short)(nChannels*bSamples/8)));
// Block align, NumberOfChannels*BitsPerSample/8
randomAccessWriter.writeShort(Short.reverseBytes(bSamples)); // Bits
per sample
randomAccessWriter.writeBytes("data");
randomAccessWriter.writeInt(0); // Data chunk size not known yet,
write 0
buffer = new byte[framePeriod*bSamples/8*nChannels];
state = State.READY;
}
else
{
Log.e(ExtAudioRecorder.class.getName(), "prepare() method called on
uninitialized recorder");
state = State.ERROR;
}
}
else
{
mediaRecorder.prepare();
state = State.READY;
}
}
else
{
Log.e(ExtAudioRecorder.class.getName(), "prepare() method called on
illegal state");
release();
state = State.ERROR;
}
}
catch(Exception e)
{
e.printStackTrace();
if (e.getMessage() != null)
{
Log.e(ExtAudioRecorder.class.getName(), e.getMessage());
}
else
{
Log.e(ExtAudioRecorder.class.getName(), "Unknown error occured in
prepare()");
}
state = State.ERROR;
}
}
/**
*
*
*
Releases the resources associated with this class, and removes the
unnecessary files, when necessary
*
*/
public void
release()
{
if (state == State.RECORDING)
{
stop();
}
else
{
if ((state == State.READY) & (rUncompressed))
{
try
{
randomAccessWriter.close(); // Remove prepared file
}
catch (IOException e)
{
e.printStackTrace();
Log.e(ExtAudioRecorder.class.getName(), "I/O exception occured while
closing output file");
}
(new File(filePath)).delete();
}
}
if (rUncompressed)
{
if (audioRecorder != null)
{
audioRecorder.release();
}
}
else
{
if (mediaRecorder != null)
{
mediaRecorder.release();
}
}
}
/**
*
*
* Resets
the recorder to the INITIALIZING state, as if it was just
created.
* In case
the class was in RECORDING state, the recording is stopped.
* In case
of exceptions the class is set to the ERROR state.
*
*/
public void
reset()
{
try
{
if (state != State.ERROR)
{
release();
filePath = null; // Reset file path
cAmplitude = 0; // Reset amplitude
if (rUncompressed)
{
audioRecorder = new AudioRecord(aSource, sRate, nChannels+1,
aFormat, bufferSize);
}
else
{
mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
}
state = State.INITIALIZING;
}
}
catch (Exception e)
{
e.printStackTrace();
Log.e(ExtAudioRecorder.class.getName(), e.getMessage());
state = State.ERROR;
}
}
/**
*
*
* Starts
the recording, and sets the state to RECORDING.
* Call
after prepare().
*
*/
public void
start()
{
if (state == State.READY)
{
if (rUncompressed)
{
payloadSize = 0;
audioRecorder.startRecording();
audioRecorder.read(buffer, 0, buffer.length);
}
else
{
mediaRecorder.start();
}
state = State.RECORDING;
}
else
{
Log.e(ExtAudioRecorder.class.getName(), "start() called on illegal
state");
state = State.ERROR;
}
}
/**
*
*
*
Stops the recording, and sets the state to STOPPED.
* In case
of further usage, a reset is needed.
* Also
finalizes the wave file in case of uncompressed recording.
*
*/
public void
stop()
{
if (state == State.RECORDING)
{
if (rUncompressed)
{
audioRecorder.stop();
try
{
randomAccessWriter.seek(4); // Write size to RIFF header
randomAccessWriter.writeInt(Integer.reverseBytes(36+payloadSize));
randomAccessWriter.seek(40); // Write size to Subchunk2Size
field
randomAccessWriter.writeInt(Integer.reverseBytes(payloadSize));
randomAccessWriter.close();
}
catch(IOException e)
{
e.printStackTrace();
Log.e(ExtAudioRecorder.class.getName(), "I/O exception occured while
closing output file");
state = State.ERROR;
}
}
else
{
mediaRecorder.stop();
}
state = State.STOPPED;
}
else
{
Log.e(ExtAudioRecorder.class.getName(), "stop() called on illegal
state");
state = State.ERROR;
}
}
/*
*
* Converts
a byte[2] to a short, in LITTLE_ENDIAN format
*
*/
private short
getShort(byte argB1, byte argB2)
{
return (short)(argB1 | (argB2 << 8));
}
}
在开始录音的地方代码如下:
extRecorder = ExtAudioRecorder.getInstanse(false); //设置为false,录制wav
extRecorder.setOutputFile(tempPath);
//输出SD卡路径
extRecorder.prepare();
extRecorder.start();
在停止录音的地方代码如下:
extRecorder.stop();
extRecorder.release(); 得到wav文件后,就可以开始lame转换mp3了,如下:
首先,导入相关lame的包,baidu和google都可以搜到lame的库文件,截图如下:
添加LameActivity.java文件,进行mp3的合成操作,LameActivity.java代码如下:
package cn.itcast.lame;
import java.io.File;
import
com.example.util.FileUtil;
import android.app.Activity;
import
android.app.ProgressDialog;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.widget.Toast;
public class LameActivity extends
Activity {
private
ProgressDialog pd;
private String
tempPath;
private String
realPath;
static{
System.loadLibrary("mp3lame"); //加载mp3lame库文件
}
public native
String getVersion();
public native
void Convert(String wav,String mp3);
@Override
public void
onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
pd = new ProgressDialog(this);
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
Intent intent = getIntent();
tempPath = intent.getStringExtra("tempPath");
realPath = intent.getStringExtra("realPath");
//显示具体进度的进度条对话框
convert(null); //合成MP3语音
}
public void
getlameversion(View view){
String version = getVersion();
Toast.makeText(this, version, Toast.LENGTH_SHORT).show();
}
public void
convert(View view){
final String wav = tempPath;
final String mp3 = realPath;
File wavfile = new File(wav);
if(wavfile.exists()){
int length = (int) wavfile.length();
pd.setMax(length);
pd.show();
new Thread(){
public void run() {
Convert(wav, mp3);
FileUtil.deleteTempFile(tempPath);
pd.dismiss();
setResult(100);
finish();
};
}.start();
}else{
Log.i("Debug", "合成MP3文件不存在");
finish();
return;
}
}
public void
setPDProgress(int progress){
pd.setProgress(progress);
}
}
利用此文件就可以进行合成mp3,由于项目中涉及业务逻辑的问题比较敏感,只把lame的使用部分进行记录,给遇到同样问题的童鞋们一个参考。