Перекодирование WAV-файла в AAC LC, AMR WB/NB
Есть ли способ перекодировать WAV-файл PCM в другую кодировку, используя стандартный Android SDK?
Я вижу, что можно напрямую записывать с микрофона в эти форматы, но приложение, которое я пишу, должно сначала записать в PCM. Из-за лицензионных ограничений ffmpeg недоступен.
Теперь у меня есть следующий код для Jelly bean, но вывод не читается никакими медиа-плеерами.
Код испуга сцены из aosp, кажется, предлагает контейнер mpeg4
profile.nSampleRate = sampleRate;
profile.nBitRate = bitRate;
profile.nAudioBandWidth = 0;
profile.nFrameLength = 0;
profile.nAACtools = OMX_AUDIO_AACToolAll;
profile.nAACERtools = OMX_AUDIO_AACERNone;
profile.eAACProfile = (OMX_AUDIO_AACPROFILETYPE) aacProfile;
profile.eAACStreamFormat = OMX_AUDIO_AACStreamFormatMP4FF;
Но вывод из кода андроида не читается.
Входной файл WAV 32 кГц, 16-битный моно со знаком, как требуется.
public void doConvert( View v)
{
new AsyncTask<Void, Void, Void>()
{
@Override
protected Void doInBackground(Void... params)
{
try
{
int codecCount = MediaCodecList.getCodecCount();
for ( int i=0; i < codecCount; i++)
{
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
Logger.getLogger(MainActivity.class.getSimpleName()).log(Level.INFO, info.getName());
for ( String type : info.getSupportedTypes() )
{
Logger.getLogger(MainActivity.class.getSimpleName()).log(Level.INFO, type);
}
}
File inputFile = new File( Environment.getExternalStorageDirectory().getAbsolutePath()+"/Media/Report-test5.wav");
FileInputStream fis = new FileInputStream(inputFile);
fis.skip(44);//remove wav header
File outputFile = new File( Environment.getExternalStorageDirectory().getAbsolutePath()+"/Media/out.mp4");
if ( outputFile.exists()) outputFile.delete();
FileOutputStream fos = new FileOutputStream(outputFile);
MediaCodec codec = MediaCodec.createEncoderByType("audio/mp4a-latm");
MediaFormat outputFormat = MediaFormat.createAudioFormat("audio/mp4a-latm", 32000, 1);
outputFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
//outputFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, AudioFormat.CHANNEL_OUT_MONO);
outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, 48000 );
//outputFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 64000);
double durationInMs = (inputFile.length()/64.0)*1000.0;
outputFormat.setLong(MediaFormat.KEY_DURATION, (long)durationInMs );
//Logger.getLogger(MainActivity.class.getSimpleName()).log(Level.INFO, codec.getOutputFormat().toString());
codec.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE );
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffer = codec.getOutputBuffers();
boolean hasMoreData = true;
MediaCodec.BufferInfo outBuffInfo = new BufferInfo();
byte readBuffer[] = new byte[64000];
byte writeBuffer[] = new byte[64000];
do
{
int nextBuffer = codec.dequeueInputBuffer(1000);
logger.log(Level.INFO,"nextInputBuffer = "+nextBuffer);
if ( nextBuffer >= 0 )
{
ByteBuffer inBuf = inputBuffers[nextBuffer];
inBuf.clear();
int bytesRead = fis.read( readBuffer,0, inBuf.capacity() );
logger.log(Level.INFO,"Read = "+bytesRead);
if ( bytesRead < inBuf.capacity() )
{
hasMoreData = false;
}
inBuf.put(readBuffer, 0, bytesRead );
codec.queueInputBuffer(nextBuffer, 0, bytesRead, 0, hasMoreData?0:MediaCodec.BUFFER_FLAG_END_OF_STREAM);
}
int outputBufferIndex = codec.dequeueOutputBuffer( outBuffInfo, 1000 );
logger.log(Level.INFO,"nextOutputBuffer = "+outputBufferIndex);
logger.log(Level.INFO,"outBuffInfo offset = "+outBuffInfo.offset);
logger.log(Level.INFO,"outBuffInfo size = "+outBuffInfo.size);
logger.log(Level.INFO,"outBuffInfo flags = "+outBuffInfo.flags);
//while ( outputBufferIndex > -1 )
//{
outputBuffer[outputBufferIndex].position(outBuffInfo.offset);
outputBuffer[outputBufferIndex].get(writeBuffer,0,outBuffInfo.size);
fos.write(writeBuffer,0, outBuffInfo.size);
logger.log(Level.INFO,"Writing = "+outBuffInfo.size+" bytes");
outputBuffer[outputBufferIndex].clear();
codec.releaseOutputBuffer(outputBufferIndex, false);
if ( outBuffInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM )
{
codec.flush();
codec.stop();
codec.release();
break;
}
//outputBufferIndex = codec.dequeueOutputBuffer( outBuffInfo, 1000 );
//logger.log(Level.INFO,"nextOutputBuffer = "+outputBufferIndex);
//}
} while ( outBuffInfo.flags != MediaCodec.BUFFER_FLAG_END_OF_STREAM);
fis.close();
fos.flush();
fos.close();
}
catch ( Exception e)
{
Logger.getLogger(MainActivity.class.getSimpleName()).log(Level.INFO, "Codec Error",e);
}
logger.log(Level.INFO,"Done");
return null;
}
}.execute();
}
4 ответа
По сути, вы записываете все сгенерированные закодированные буферы в файл, но в нем отсутствуют всевозможные метаданные, связанные с дорожками / семплами / файлом и т. Д.
Я думаю, что решения, которые я могу придумать, таковы: либо вы можете найти хорошую библиотеку мультиплексора для записи буферов в нужный формат файла, либо вам придется подождать и посмотреть, предоставит ли вам в будущем Android такие API.
Чтобы ответить на мой собственный вопрос,
В Android 4.3 и выше есть класс MediaMuxer, который создает контейнеры MPEG4 для аудио и видео потока.
public boolean encode( File outputFile )
{
if ( Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 )
{
try
{
mInputWav.close();
}
catch (IOException e)
{
logger.log(Level.WARNING,"Unable to close Input Wav File ",e);
}
throw new UnsupportedOperationException("Only Available on Android 4.3 and Above");
}
try
{
int sampleRate = mInputWav.getSampleRate();
int percentComplete = 0;
int fileSize = mInputWav.getDataLength();
int totalBytesRead = 0;
if ( outputFile.exists()) outputFile.delete();
MediaMuxer mux = new MediaMuxer( outputFile.getAbsolutePath(), OutputFormat.MUXER_OUTPUT_MPEG_4);
MediaCodec codec = MediaCodec.createEncoderByType("audio/mp4a-latm");
MediaFormat outputFormat = MediaFormat.createAudioFormat("audio/mp4a-latm",sampleRate,1);
outputFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000 );
codec.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE );
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffer = codec.getOutputBuffers();
boolean hasMoreData = true;
MediaCodec.BufferInfo outBuffInfo = new BufferInfo();
byte readBuffer[] = new byte[64000];
double presentationTimeUs=0;
int audioTrackIdx=0;
do
{
int nextBuffer = 0;
while ( nextBuffer != -1 && hasMoreData )
{
nextBuffer = codec.dequeueInputBuffer(1000);
if ( nextBuffer >= 0 )
{
ByteBuffer inBuf = inputBuffers[nextBuffer];
inBuf.clear();
int bytesRead = mInputWav.read( readBuffer,0, inBuf.limit() );
if ( bytesRead == -1 )
{
hasMoreData = false;
codec.queueInputBuffer(nextBuffer, 0, 0, (long)presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
}
else
{
totalBytesRead += bytesRead;
inBuf.put(readBuffer, 0, bytesRead );
codec.queueInputBuffer(nextBuffer, 0, bytesRead, (long)presentationTimeUs, 0);
presentationTimeUs = 1000000l * (totalBytesRead) / 2 / sampleRate;
}
}
}
//Drain audio
int outputBufferIndex = 0;
while (outputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER )
{
outputBufferIndex = codec.dequeueOutputBuffer( outBuffInfo, 10000 );
if ( outputBufferIndex >= 0 )
{
ByteBuffer encodedData = outputBuffer[outputBufferIndex];
encodedData.position(outBuffInfo.offset);
encodedData.limit(outBuffInfo.offset + outBuffInfo.size );
if ((outBuffInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG)!= 0 && outBuffInfo.size != 0)
{
logger.log(Level.FINE, "video encoder: codec config buffer");
// Simply ignore codec config buffers.
codec.releaseOutputBuffer(outputBufferIndex, false);
}
else
{
mux.writeSampleData(audioTrackIdx, outputBuffer[outputBufferIndex], outBuffInfo);
codec.releaseOutputBuffer(outputBufferIndex, false);
}
}
else if ( outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED )
{
logger.info("Output Format Changed :"+codec.getOutputFormat().toString());
outputFormat = codec.getOutputFormat();
audioTrackIdx = mux.addTrack(outputFormat);
mux.start();
}
else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED )
{
logger.info("Output Buffers Changed: shouldn't happen on an encode ");
}
else if ( outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER)
{
logger.info("Encoder Timed Out");
}
else
{
logger.info("Unkown return code from dequeueOutputBuffer "+outputBufferIndex);
}
}
percentComplete = (int)Math.round(((float)totalBytesRead / (float)fileSize)*100.0);
logger.info("Percent Complete "+percentComplete+"%");
if ( mListener != null )
{
mListener.onProgressUpdate(percentComplete);
}
} while ( outBuffInfo.flags != MediaCodec.BUFFER_FLAG_END_OF_STREAM && !mCanceled);
mInputWav.close();
mux.stop();
mux.release();
logger.info("Finished");
}
catch ( Exception e)
{
logger.log(Level.INFO, "Codec Error",e);
}
logger.log(Level.INFO,"Done");
return true;
}
Выберите контейнер для него. Я тоже предпочитаю adts, но flv/mp4 тоже работает.
Скопируйте данные полезной нагрузки в массив, достаточно большой для вашего контейнера, просто добавьте свои биты. Так что после поиска в интернете своего решения я разработал фрагмент
profile =( configParams[0]>>3 )&0x1f;
frequency_index = (this.configParams[0]&0x7) <<1 | (this.configParams[1]>>7) &0x1;
channel_config = (this.configParams[1]>>3) &0xf;
int finallength = encoded_length + 7;
ENCodedByteArray[0] = (byte) 0xff;
ENCodedByteArray[1] = (byte) 0xf1;
ENCodedByteArray[2] = (byte) ( ((profile - 1) << 6) + (frequency_index << 2) +(channel_config >> 2));
ENCodedByteArray[3] = (byte) (((channel_config & 0x3) << 6) + (finallength >> 11));
ENCodedByteArray[4] = (byte)( (finallength & 0x7ff) >> 3);
ENCodedByteArray[5] = (byte) (((finallength & 7) << 5) + 0x1f) ;
ENCodedByteArray[6] = (byte) 0xfc;
Используя что-то вроде ниже, это вызывает выше
byte chunkADTS[]=new byte[info.size + 7];
fillInADTSHeader(chunkADTS,info.size);
outputBuffers[bR].get(chunkADTS,7,info.size);
buffer.pushData(chunkADTS);
Файл не может быть воспроизведен, потому что он не содержит никакой информации заголовка. Простой (ish) способ сделать это воспроизводимым - добавить заголовки ADTS в необработанные кадры AAC. Вы можете найти описание здесь.
Удачи!