/*
* Copyright 2012 The Stanford MobiSocial Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mobisocial.musubi.objects;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import mobisocial.musubi.R;
import mobisocial.musubi.feed.iface.Activator;
import mobisocial.musubi.feed.iface.DbEntryHandler;
import mobisocial.musubi.feed.iface.FeedRenderer;
import mobisocial.musubi.ui.fragments.FeedListFragment.FeedSummary;
import mobisocial.musubi.ui.widget.DbObjCursorAdapter.DbObjCursor;
import mobisocial.socialkit.musubi.DbObj;
import mobisocial.socialkit.obj.MemObj;
import org.json.JSONException;
import org.json.JSONObject;
import org.mobisocial.corral.CorralDownloadClient;
import android.app.Activity;
import android.content.Context;
import android.graphics.Typeface;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaPlayer;
import android.os.AsyncTask;
import android.os.Environment;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.tuenti.androidilbc.Codec;
/**
* A short audio clip. "Version 0" uses a sample rate of 8000, mono channel, and
* 16bit pcm recording.
*/
public class VoiceObj extends DbEntryHandler implements FeedRenderer, Activator {
public static final String TAG = "VoiceObj";
public static final String TYPE = "voice";
public static final String TYPE_CAF = "audio/x-caf";
private static final int RECORDER_BPP = 16;
private static final int RECORDER_SAMPLERATE = 8000;
private static final int RECORDER_CHANNELS = AudioFormat.CHANNEL_CONFIGURATION_MONO;
private static final int RECORDER_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
@Override
public String getType() {
return TYPE;
}
public static MemObj from(byte[] data) {
return new MemObj(TYPE, new JSONObject(), data);
}
public static JSONObject json(byte[] data){
String encoded = Base64.encodeToString(data, Base64.DEFAULT);
JSONObject obj = new JSONObject();
try{
obj.put("data", encoded);
}catch(JSONException e){}
return obj;
}
@Override
public View createView(Context context, ViewGroup frame) {
ImageView imageView = new ImageView(context);
imageView.setImageResource(R.drawable.play);
imageView.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
return imageView;
}
@Override
public void render(Context context, View view, DbObjCursor obj, boolean allowInteractions) {
// all done!
}
@Override
public void activate(final Context context, final DbObj obj) {
if (obj.getJson() != null && TYPE_CAF.equalsIgnoreCase(
obj.getJson().optString(CorralDownloadClient.OBJ_MIME_TYPE))) {
new PlayCafFileTask(context, obj).execute();
} else {
Runnable r = runnableForWav(context, obj);
if (context instanceof Activity) {
((Activity)context).runOnUiThread(r);
} else {
r.run();
}
}
}
static class CafAudioFormat {
public static final int FORMAT_ILBC = 0x696C6263;
public static final int FORMAT_PCM = 0x6C70636D;
byte[] rawData;
double sampleRate;
int formatId;
int formatFlags;
int bytesPerPacket;
int framesPerPacket;
int channelsPerFrame;
int bitsPerChannel;
int dataOffset;
long dataLength;
public String toString() {
return String.format("[caff: %f %d %d %d %d %d %d, head=%d,size=%d]", sampleRate,
formatId, formatFlags, bytesPerPacket, framesPerPacket,
channelsPerFrame, bitsPerChannel, dataOffset, dataLength);
}
}
private CafAudioFormat parseAsCaff(byte[] audioData) {
final int CAFF = ((int)'c' << 24) | ((int)'a' << 16) | ((int)'f' << 8) | (int)'f';
final int DESC = ((int)'d' << 24) | ((int)'e' << 16) | ((int)'s' << 8) | (int)'c';
final int DATA = ((int)'d' << 24) | ((int)'a' << 16) | ((int)'t' << 8) | (int)'a';
final int INFO = ((int)'i' << 24) | ((int)'n' << 16) | ((int)'f' << 8) | (int)'o';
final int PAKT = ((int)'p' << 24) | ((int)'a' << 16) | ((int)'k' << 8) | (int)'t';
final int CHAN = ((int)'c' << 24) | ((int)'h' << 16) | ((int)'a' << 8) | (int)'n';
final int KUKI = ((int)'k' << 24) | ((int)'u' << 16) | ((int)'k' << 8) | (int)'i';
final int STRG = ((int)'s' << 24) | ((int)'t' << 16) | ((int)'r' << 8) | (int)'g';
final int MARK = ((int)'m' << 24) | ((int)'a' << 16) | ((int)'r' << 8) | (int)'k';
final int REGN = ((int)'r' << 24) | ((int)'e' << 16) | ((int)'g' << 8) | (int)'n';
final int UMID = ((int)'u' << 24) | ((int)'m' << 16) | ((int)'i' << 8) | (int)'d';
final int OVVW = ((int)'o' << 24) | ((int)'v' << 16) | ((int)'v' << 8) | (int)'w';
final int PEAK = ((int)'p' << 24) | ((int)'e' << 16) | ((int)'a' << 8) | (int)'k';
final int INST = ((int)'i' << 24) | ((int)'n' << 16) | ((int)'s' << 8) | (int)'t';
final int MIDI = ((int)'m' << 24) | ((int)'i' << 16) | ((int)'d' << 8) | (int)'i';
final int EDCT = ((int)'e' << 24) | ((int)'d' << 16) | ((int)'c' << 8) | (int)'t';
final int UUID = ((int)'u' << 24) | ((int)'u' << 16) | ((int)'i' << 8) | (int)'d';
final int FREE = ((int)'f' << 24) | ((int)'r' << 16) | ((int)'e' << 8) | (int)'e';
ByteBuffer data = ByteBuffer.wrap(audioData);
data.order(ByteOrder.BIG_ENDIAN);
final int magicNumber = data.getInt();
final int fileVersion = data.getShort();
final int fileFlags = data.getShort();
if (magicNumber != CAFF) {
Log.w(TAG, "Not a CAFF file");
return null;
}
CafAudioFormat caff = new CafAudioFormat();
caff.rawData = audioData;
while(true) {
int chunkType = data.getInt();
long chunkSize = data.getLong();
switch (chunkType) {
case DESC:
data.mark();
caff.sampleRate = data.getDouble();
caff.formatId = data.getInt();
caff.formatFlags = data.getInt();
caff.bytesPerPacket = data.getInt();
caff.framesPerPacket = data.getInt();
caff.channelsPerFrame = data.getInt();
caff.bitsPerChannel = data.getInt();
data.reset();
break;
case DATA:
if (caff == null) {
Log.w(TAG, "no caff description");
return null;
}
caff.dataLength = chunkSize - 4; // 32-bit edit count
caff.dataOffset = data.position() + 4; // can suck a dick
Log.d(TAG, "Audio format: " + caff);
return caff;
case INFO:
Log.d(TAG, "INFO Header size " + chunkSize);
break;
case PAKT:
Log.d(TAG, "PAKT Header size " + chunkSize);
break;
case CHAN:
Log.d(TAG, "CHAN Header size " + chunkSize);
break;
case KUKI:
Log.d(TAG, "KUKI Header size " + chunkSize);
break;
case STRG:
Log.d(TAG, "STRG Header size " + chunkSize);
break;
case MARK:
Log.d(TAG, "MARK Header size " + chunkSize);
break;
case REGN:
Log.d(TAG, "REGN Header size " + chunkSize);
break;
case UMID:
Log.d(TAG, "UMID Header size " + chunkSize);
break;
case OVVW:
Log.d(TAG, "OVVW Header size " + chunkSize);
break;
case PEAK:
Log.d(TAG, "PEAK Header size " + chunkSize);
break;
case MIDI:
Log.d(TAG, "MIDI Header size " + chunkSize);
break;
case INST:
Log.d(TAG, "INST Header size " + chunkSize);
break;
case EDCT:
Log.d(TAG, "EDCT Header size " + chunkSize);
break;
case UUID:
Log.d(TAG, "UUID Header size " + chunkSize);
break;
case FREE:
Log.d(TAG, "FREE Header size " + chunkSize);
default:
Log.d(TAG, "Unused header " + chunkType);
}
data.position(data.position() + (int)chunkSize);
}
}
class PlayCafFileTask extends AsyncTask<Void, Void, Void> {
AudioTrack mAudioTrack;
final Context mContext;
final DbObj mObj;
public PlayCafFileTask(Context context, DbObj obj) {
mContext = context;
mObj = obj;
}
@Override
protected void onPreExecute() {
int stream = AudioManager.STREAM_MUSIC;
AudioManager audioManager =
(AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE);
int v = audioManager.getStreamVolume(stream);
int m = audioManager.getStreamMaxVolume(stream);
if (((float)v / m) < 0.15) {
audioManager.setStreamVolume(stream, v, AudioManager.FLAG_SHOW_UI);
}
}
@Override
protected Void doInBackground(Void... params) {
byte[] caffData = mObj.getRaw();
CafAudioFormat caff = parseAsCaff(caffData);
if (caff == null) {
Log.e(TAG, "not a caff file");
return null;
}
switch (caff.formatId) {
case CafAudioFormat.FORMAT_ILBC:
playIlbc(caff);
break;
case CafAudioFormat.FORMAT_PCM:
playPcm(caff);
break;
default:
Log.w(TAG, "Unsupported CAF format " + caff.formatId);
}
return null;
}
void playPcm(CafAudioFormat caff) {
int bufferSize = 4 * (int)caff.sampleRate;
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, (int)caff.sampleRate,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize, AudioTrack.MODE_STREAM);
mAudioTrack.play();
mAudioTrack.write(caff.rawData, caff.dataOffset, (int)caff.dataLength);
mAudioTrack.stop();
}
void playIlbc(CafAudioFormat caff) {
final int bytesInWavPerSecond = 8000 * 2 * 1; // 8khz 16bit mono pcm
final short msPerFrame;
if (caff.bytesPerPacket == 38) {
msPerFrame = 20;
} else if (caff.bytesPerPacket == 50) {
msPerFrame = 30;
} else {
Log.w(TAG, "invalid ilbc");
return;
}
final int maxReadLength = 20 * caff.bytesPerPacket; // (samples/second)/((ms/second)/(ms/frame))
int bufferSize = 2*bytesInWavPerSecond;
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, (int)caff.sampleRate,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize, AudioTrack.MODE_STREAM);
mAudioTrack.play();
Codec codec = Codec.instance();
synchronized (codec) {
codec.resetDecoder();
byte[] rawAudio = new byte[bufferSize];
int offset = caff.dataOffset;
while (offset < caff.dataOffset + caff.dataLength) {
int length = Math.min(maxReadLength, caff.dataOffset + (int)caff.dataLength - offset);
int read = codec.decode(caff.rawData, offset, length, msPerFrame, rawAudio);
if (read <= 0) {
Log.e(TAG, "Error reading data");
break;
}
int frameCount = read / caff.bytesPerPacket;
int msInAudio = msPerFrame * frameCount;
int bytesInWav = bytesInWavPerSecond * msInAudio / 1000;
mAudioTrack.write(rawAudio, 0, bytesInWav);
offset += read;
}
mAudioTrack.stop();
codec.resetDecoder();
}
}
}
Runnable runnableForWav(final Context context, final DbObj obj) {
return new Runnable() {
@Override
public void run() {
byte[] bytes = obj.getRaw();
int stream = AudioManager.STREAM_MUSIC;
AudioManager audioManager =
(AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
int v = audioManager.getStreamVolume(stream);
int m = audioManager.getStreamMaxVolume(stream);
if (((float)v / m) < 0.15) {
audioManager.setStreamVolume(stream, v, AudioManager.FLAG_SHOW_UI);
}
File file = new File(getTempFilename());
OutputStream out = null;
try {
out = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(out);
bos.write(bytes, 0, bytes.length);
bos.flush();
bos.close();
File tempFile = new File(getTempFilename());
copyWaveFile(tempFile,getFilename());
tempFile.delete();
MediaPlayer mp = new MediaPlayer();
mp.setAudioStreamType(stream);
mp.setDataSource(getFilename());
mp.prepare();
mp.start();
} catch (Exception e) {
Log.e(TAG, "error playign audio", e);
} finally {
try {
if(out != null) out.close();
} catch (IOException e) {
Log.e(TAG, "failed to close output stream for voice", e);
}
}
}
};
}
private String getTempFilename(){
return Environment.getExternalStorageDirectory().getAbsolutePath()+"/temp.raw";
}
private String getFilename(){
return Environment.getExternalStorageDirectory().getAbsolutePath()+"/temp.wav";
}
private void copyWaveFile(File inFile,String outFilename){
FileInputStream in = null;
FileOutputStream out = null;
long totalAudioLen = 0;
long totalDataLen = totalAudioLen + 36;
long longSampleRate = RECORDER_SAMPLERATE;
int channels = 1;
long byteRate = RECORDER_BPP * RECORDER_SAMPLERATE * channels/8;
int bufferSize = AudioRecord.getMinBufferSize(RECORDER_SAMPLERATE,RECORDER_CHANNELS,RECORDER_AUDIO_ENCODING);
byte[] data = new byte[bufferSize];
try {
in = new FileInputStream(inFile);
out = new FileOutputStream(outFilename);
totalAudioLen = in.getChannel().size();
totalDataLen = totalAudioLen + 36;
Log.w("PlayAllAudioAction", "File size: " + totalDataLen);
WriteWaveFileHeader(out, totalAudioLen, totalDataLen,
longSampleRate, channels, byteRate);
while(in.read(data) != -1){
out.write(data);
}
in.close();
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(in != null) in.close();
if(out != null) out.close();
} catch (IOException e) {
Log.e(TAG, "failed to close output stream for voice", e);
}
}
}
private void WriteWaveFileHeader(
FileOutputStream out, long totalAudioLen,
long totalDataLen, long longSampleRate, int channels,
long byteRate) throws IOException {
byte[] header = new byte[44];
header[0] = 'R'; // RIFF/WAVE header
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'; // '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) 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) (2 * 16 / 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) (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);
}
@Override
public void getSummaryText(Context context, TextView view, FeedSummary summary) {
view.setTypeface(null, Typeface.ITALIC);
view.setText(summary.getSender() + " sent a voice message.");
}
}