I have been trying to merge two WAVE files on Android for quite some time now but really can't seem to get it working properly.
Everything looks fine, the files are read, and written to the output file which is also readable at a later stage and has the file size I would expect to see.
The problems occur right after the app has finished merging. This message will show in the log: Error occured in updateListener, recording is aborted which is a message from extAudioRecorder
and appears when the OnRecordPositionUpdateListener
reaches the catch clause (the Exception has the following detailMessage: write failed: EBADF (Bad file number)). This does not seem to break anything so I'm not too worried about this.
The real problem arises when I try to create a MediaPlayer
and call the setDataSource(String path)
on the MediaPlayer instance. Whenever I do this with a merged file the following error message will show in the log: Unable to to create media player (the IOException
that is thrown contains the following detailMessage: setDataSourceFD failed.: status=0x80000000). Note that the first time the file will play perfectly fine (this first file was not made by the combineWaveFiles()
method). That Error message appears to indicate that the audio file's format is incorrect and/or can't be read by the MediaPlayer.
My question is if anyone sees any real problems with the code below (I know it's sub-optimal in many ways but I prefer to get it to work first then worry about performance).
public static String MergeRecordings(String cumulativeFile, String recordFile, int sampleRate, int bpp, int bufferSize, int channels) {
if (cumulativeFile == null) {
return recordFile;
} else if (recordFile == null) {
return cumulativeFile;
}
String outputFile = FileUtils.getFilePath(null, MDSettings.shared().getMainActivity());
FileUtils.combineWaveFiles(cumulativeFile, recordFile, outputFile, sampleRate, bpp, bufferSize, channels);
//FileUtils.removeFile(cumulativeFile);
//FileUtils.removeFile(recordFile);
return outputFile;
}
//creates a new file containing file1 + file2 stuck together as such.
private static void combineWaveFiles(String file1, String file2, String outputFile, int sampleRate, int bpp, int bufferSize, int channels) {
FileInputStream in1 = null, in2 = null;
FileOutputStream out = null;
long longSampleRate = sampleRate;
long byteRate = sampleRate * channels * bpp / 8;
byte[] data;
try {
try {
in1 = new FileInputStream(file1);
} catch (Exception e) { }
try {
in2 = new FileInputStream(file2);
} catch (FileNotFoundException e) { }
out = new FileOutputStream(outputFile);
long file1Size = 0;
long file2Size = 0;
if (in1 != null) { file1Size = in1.getChannel().size() - 44; }
if (in2 != null) { file2Size = in2.getChannel().size() - 44; }
long totalAudioLen = file1Size + file2Size;
long totalDataLen = totalAudioLen + 36;
FileUtils.writeWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate, bpp);
if (in1 != null) {
in1.skip(44);
data = new byte[bufferSize];
if (file1Size < bufferSize) {
data = new byte[(int)file1Size];
}
while (in1.read(data) != -1) {
out.write(data);
file1Size -= bufferSize;
if (file1Size <= 0) {
break;
} else if (file1Size < bufferSize) {
data = new byte[(int)file1Size];
}
}
}
if (in2 != null) {
in2.skip(44);
data = new byte[bufferSize];
if (file2Size < bufferSize) {
data = new byte[(int)file2Size];
}
while (in2.read(data) != -1) {
out.write(data);
file2Size -= bufferSize;
if (file2Size <= 0) {
break;
} else if (file2Size < bufferSize) {
data = new byte[(int)file2Size];
}
}
}
out.close();
if (in1 != null) { in1.close(); }
if (in2 != null) { in2.close(); }
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void writeWaveFileHeader(FileOutputStream out, long totalAudioLen, long totalDataLen, long longSampleRate, int channels, long byteRate, int bpp)
throws IOException {
byte[] header = new byte[44];
header[0] = 'R';
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
header[4] = (byte)(totalDataLen & 0xff);
header[5] = (byte)((totalDataLen >> 8) & 0xff);
header[6] = (byte)((totalDataLen >> 16) & 0xff);
header[7] = (byte)((totalDataLen >> 24) & 0xff);
header[8] = 'W';
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
header[12] = 'f';
header[13] = 'm';
header[14] = 't';
header[15] = ' ';
header[16] = 16;
header[17] = 0;
header[18] = 0;
header[19] = 0;
header[20] = 1;
header[21] = 0;
header[22] = (byte) channels;
header[23] = 0;
header[24] = (byte)(longSampleRate & 0xff);
header[25] = (byte)((longSampleRate >> 8) & 0xff);
header[26] = (byte)((longSampleRate >> 16) & 0xff);
header[27] = (byte)((longSampleRate >> 24) & 0xff);
header[28] = (byte)(byteRate & 0xff);
header[29] = (byte)((byteRate >> 8) & 0xff);
header[30] = (byte)((byteRate >> 16) & 0xff);
header[31] = (byte)((byteRate >> 24) & 0xff);
header[32] = (byte)(channels * bpp); //(2 * 16 / 8);
header[33] = 0;
header[34] = (byte)bpp;
header[35] = 0;
header[36] = 'd';
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte)(totalAudioLen & 0xff);
header[41] = (byte)((totalAudioLen >> 8) & 0xff);
header[42] = (byte)((totalAudioLen >> 16) & 0xff);
header[43] = (byte)((totalAudioLen >> 24) & 0xff);
out.write(header, 0, 44);
}
Large parts of this code are taken from this answer.
i do this exactly in my android app. Instead of two, i merge multiple files based on users selection. I use AsyncTask to merge the samples in the background. Take a look at it here. Just filter the section that you need. If you are curious about my app, its called Sound Recorder + Pro, Beside merge i do mix, add echo and amplify samples:
@Override
protected Void doInBackground(Void... params) {
isProcessingOn=true;
try {
DataOutputStream amplifyOutputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(Environment.getExternalStorageDirectory() + "/Soundrecpluspro/" + year +"-"+ month +"-"+ date +"-"+ hour+"-" + min +"-"+ sec+"ME.wav")));
DataInputStream[] mergeFilesStream = new DataInputStream[selection.size()];
long[] sizes=new long[selection.size()];
for(int i=0; i<selection.size(); i++) {
File file = new File(Environment.getExternalStorageDirectory() + "/Soundrecpluspro/" +selection.get(i));
sizes[i] = (file.length()-44)/2;
}
for(int i =0; i<selection.size(); i++) {
mergeFilesStream[i] =new DataInputStream(new BufferedInputStream(new FileInputStream(Environment.getExternalStorageDirectory() + "/Soundrecpluspro/" +selection.get(i))));
if(i == selection.size()-1) {
mergeFilesStream[i].skip(24);
byte[] sampleRt = new byte[4];
mergeFilesStream[i].read(sampleRt);
ByteBuffer bbInt = ByteBuffer.wrap(sampleRt).order(ByteOrder.LITTLE_ENDIAN);
RECORDER_SAMPLERATE = bbInt.getInt();
mergeFilesStream[i].skip(16);
}
else {
mergeFilesStream[i].skip(44);
}
}
for(int b=0; b<selection.size(); b++) {
for(int i=0; i<(int)sizes[b]; i++) {
byte[] dataBytes = new byte[2];
try {
dataBytes[0] = mergeFilesStream[b].readByte();
dataBytes[1] = mergeFilesStream[b].readByte();
}
catch (EOFException e) {
amplifyOutputStream.close();
}
short dataInShort = ByteBuffer.wrap(dataBytes).order(ByteOrder.LITTLE_ENDIAN).getShort();
float dataInFloat= (float) dataInShort/37268.0f;
short outputSample = (short)(dataInFloat * 37268.0f);
byte[] dataFin = new byte[2];
dataFin[0] = (byte) (outputSample & 0xff);
dataFin[1] = (byte)((outputSample >> 8) & 0xff);
amplifyOutputStream.write(dataFin, 0 , 2);
}
}
amplifyOutputStream.close();
for(int i=0; i<selection.size(); i++) {
mergeFilesStream[i].close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
long size =0;
try {
FileInputStream fileSize = new FileInputStream(Environment.getExternalStorageDirectory() + "/Soundrecpluspro/"+year +"-"+ month +"-"+ date +"-"+ hour+"-" + min +"-"+ sec+"ME.wav");
size = fileSize.getChannel().size();
fileSize.close();
} catch (FileNotFoundException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
final int RECORDER_BPP = 16;
long datasize=size+36;
long byteRate = (RECORDER_BPP * RECORDER_SAMPLERATE)/8;
long longSampleRate = RECORDER_SAMPLERATE;
byte[] header = new byte[44];
header[0] = 'R'; // RIFF/WAVE header
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
header[4] = (byte) (datasize & 0xff);
header[5] = (byte) ((datasize >> 8) & 0xff);
header[6] = (byte) ((datasize >> 16) & 0xff);
header[7] = (byte) ((datasize >> 24) & 0xff);
header[8] = 'W';
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
header[12] = 'f'; // 'fmt ' chunk
header[13] = 'm';
header[14] = 't';
header[15] = ' ';
header[16] = 16; // 4 bytes: size of 'fmt ' chunk
header[17] = 0;
header[18] = 0;
header[19] = 0;
header[20] = 1; // format = 1
header[21] = 0;
header[22] = (byte) 1;
header[23] = 0;
header[24] = (byte) (longSampleRate & 0xff);
header[25] = (byte) ((longSampleRate >> 8) & 0xff);
header[26] = (byte) ((longSampleRate >> 16) & 0xff);
header[27] = (byte) ((longSampleRate >> 24) & 0xff);
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
header[32] = (byte) ((RECORDER_BPP) / 8); // block align
header[33] = 0;
header[34] = RECORDER_BPP; // bits per sample
header[35] = 0;
header[36] = 'd';
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (size & 0xff);
header[41] = (byte) ((size >> 8) & 0xff);
header[42] = (byte) ((size >> 16) & 0xff);
header[43] = (byte) ((size >> 24) & 0xff);
// out.write(header, 0, 44);
try {
RandomAccessFile rFile = new RandomAccessFile(Environment.getExternalStorageDirectory() + "/Soundrecpluspro/" +year +"-"+ month +"-"+ date +"-"+ hour+"-" + min +"-"+ sec+ "ME.wav", "rw");
rFile.seek(0);
rFile.write(header);
rFile.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
Try with this code for concatenating the wav files:
public class ConcateSongActivity extends Activity {
Button mbutt;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mbutt = (Button)findViewById(R.id.button_clickme);
mbutt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
try {
FileInputStream fis1 = new FileInputStream("/sdcard/MJdangerous.wav");
FileInputStream fis2 = new FileInputStream("/sdcard/MJBad.wav");
SequenceInputStream sis = new SequenceInputStream(fis1,fis2);
FileOutputStream fos = new FileOutputStream(new File("/sdcard/MJdangerousMJBad.wav"));
int temp;
try {
while ((temp = sis.read())!= -1){
fos.write(temp);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
} }
Note: don't forget to give this permission WRITE_EXTERNAL_STORAGE
other tricks: This is the methods used to mix 3 sounds together:
private void mixFiles(){
try {
InputStream is1 = getResources().openRawResource(R.raw.test1);
List<Short> music1 = createMusicArray(is1);
InputStream is2 = getResources().openRawResource(R.raw.test2);
List<Short> music2 = createMusicArray(is2);
completeStreams(music1,music2);
short[] music1Array = buildShortArray(music1);
short[] music2Array = buildShortArray(music2);
short[] output = new short[music1Array.length];
for(int i=0; i < output.length; i++){
float samplef1 = music1Array[i] / 32768.0f;
float samplef2 = music2Array[i] / 32768.0f;
float mixed = samplef1 + samplef2;
// reduce the volume a bit:
mixed *= 0.8;
// hard clipping
if (mixed > 1.0f) mixed = 1.0f;
if (mixed < -1.0f) mixed = -1.0f;
short outputSample = (short)(mixed * 32768.0f);
output[i] = outputSample;
}
saveToFile(output);
} catch (NotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Once we have a PCM mixed sound, it can be transformed in a .wav file so that every player can read it.
/**
* Dealing with big endian streams
* @param byte0
* @param byte1
* @return a shrt with the two bytes swapped
*/
private static short swapBytes(byte byte0, byte byte1){
return (short)((byte1 & 0xff) << 8 | (byte0 & 0xff));
}
/**
* From file to byte[] array
* @param sample
* @param swap should swap bytes?
* @return
* @throws IOException
*/
public static byte[] sampleToByteArray(File sample, boolean swap) throws IOException{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sample));
int BUFFERSIZE = 4096;
byte[] buffer = new byte[BUFFERSIZE];
while(bis.read(buffer) != - 1){
baos.write(buffer);
}
byte[] outputByteArray = baos.toByteArray();
bis.close();
baos.close();
if(swap){
for(int i=0; i < outputByteArray.length - 1; i=i+2){
byte byte0 = outputByteArray[i];
outputByteArray[i] = outputByteArray[i+1];
outputByteArray[i+1] = byte0;
}
}
return outputByteArray;
}
/**
* Read a file and returns its contents as array of shorts
* @param sample the sample file
* @param swap true if we should swap the bytes of short (reading a little-endian file), false otherwise (reading a big-endian file)
* @return
* @throws IOException
*/
public static short[] sampleToShortArray(File sample, boolean swap) throws IOException{
short[] outputArray = new short[(int)sample.length()/2];
byte[] outputByteArray = sampleToByteArray(sample,false);
for(int i=0, j=0; i < outputByteArray.length; i+= 2, j++){
if(swap){
outputArray[j] = swapBytes(outputByteArray[i], outputByteArray[i + 1]);
}
else{
outputArray[j] = swapBytes(outputByteArray[i + 1], outputByteArray[i]);
}
}
return outputArray;
}
public void completeStreams(List<Short> mListShort_1, List<Short> mListShort_2) {
//TODO: check length
int size_a = mListShort_1.size();
int size_b = mListShort_2.size();
if (size_a > size_b){
// adding series of '0'
for (int i = size_b+1; i <= size_a; i++) {
mListShort_2.set(i, (short) 0);
}
} else if (size_a < size_b) {
for (int i = size_a+1; i <= size_b; i++) {
mListShort_1.set(i, (short) 0);
}
} else {
//do nothing
}
}
Credits: nalitzis
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With