概要
音声感情解析セミナーに行ってきたので、早速使ってみる。
なんと300コール/月まで無料という太っ腹だ。使わざるをえない。
使い道を2分位考えた結果、怒りも数値で取れる事だし、怒りで目覚める事にした。
結果
こんな感じ。
コード
詳細
怒りで目覚めるとどうなるかというと、金髪になる。
なので怒りを検知したら金髪になるようなプログラムを作ることになる。
他にも
- 眉毛がなくなる
- 髪が長くなる
- 青くなる
- すごいゴリラになる
などなどあるが、似せれば似せるほど訴えられかねないのでやめる。
下準備
まずはアカウント作成
ココから新規アカウントを発行する。
ログインしたら、APIキーを発行する。Addボタンを押すだけ。
作成
録音してEmpathAPIを呼び出す
EmpathAPI自体はシンプルなWebAPIなので基本的になんにでも対応している。
ドキュメント上は下記言語のサンプルがある。
- java
- php
- nodejs
なんでもよいのだが、今回はandroid javaで利用する。折角作るなら持ち運んで人に見せたいからだ。
音声を保存する部分はココが超絶参考になる。
まずは適当に画面を作る。シンプルにボタンとラベルだけ。
後はボタンを押したら録音開始。一定時間たったら、AsyncTask経由でファイルに保存した音声を乗せたAPI呼び出すだけ。簡単。
private final String EMPATH_ENDPOINT = "https://api.webempath.net/v2/analyzeWav";
private final String EMPATH_APIKEY = "";
private final String filePath = Environment.getExternalStorageDirectory() + "/empath.wav";
private Button btnDetectEmotion = null;
private TextView labelResult = null;
private Handler handler = null;
private final int MSG_STOP_RECORDING = 1000;
AudioRecord audioRecord;
private int bufSize;
private short[] shortData;
private WaveFile wav = new WaveFile();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnDetectEmotion = this.findViewById(R.id.btnDetectEmotion);
btnDetectEmotion.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startRecording();
}
});
labelResult = this.findViewById(R.id.textViewResult);
handler = new Handler(new Handler.Callback(){
@Override
public boolean handleMessage(Message message) {
switch (message.what){
case MSG_STOP_RECORDING:
stopRecoding();
break;
}
return false;
}
});
wav.createFile(filePath);
bufSize = android.media.AudioRecord.getMinBufferSize(SoundDefine.SAMPLING_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
SoundDefine.SAMPLING_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufSize);
shortData = new short[bufSize / 2];
audioRecord.setRecordPositionUpdateListener(new AudioRecord.OnRecordPositionUpdateListener() {
@Override
public void onPeriodicNotification(AudioRecord recorder) {
audioRecord.read(shortData, 0, bufSize / 2); // 読み込む
wav.addBigEndianData(shortData); // ファイルに書き出す
}
@Override
public void onMarkerReached(AudioRecord recorder) { }
});
audioRecord.setPositionNotificationPeriod(bufSize / 2);
}
private void startRecording() {
labelResult.setText("now recording");
audioRecord.startRecording();
audioRecord.read(shortData, 0, bufSize/2);
Message msg = Message.obtain();
msg.what = MSG_STOP_RECORDING;
handler.sendMessageDelayed(msg, 3000);
btnDetectEmotion.setEnabled(false);
}
private void stopRecoding() {
btnDetectEmotion.setEnabled(true);
labelResult.setText("now detecting");
try {
audioRecord.stop();
new GetEmotion().execute();
} catch (Exception e) {
e.printStackTrace();
}
}
private class GetEmotion extends AsyncTask<String,Void,Long>{
private String result = null;
@Override
protected Long doInBackground(String... strings) {
try{
OkHttpClient client = new OkHttpClient();
File file = new File(filePath);
RequestBody requestBody = new MultipartBody.Builder()
.setType(FORM)
.addFormDataPart("apikey",EMPATH_APIKEY)
.addFormDataPart("wav", "empath.wav", RequestBody.create(MediaType.parse("audio/wav"), file))
.build();
Request request = new Request.Builder().url(EMPATH_ENDPOINT).post(requestBody).build();
Response response = client.newCall(request).execute();
this.result = response.body().string();
}catch(Exception e){
result = "error";
}
return 0L;
}
@Override
protected void onPostExecute(Long aLong) {
labelResult.setText(result);
super.onPostExecute(aLong);
}
}
実行結果はこんな感じ
呼べたらちゃんと記録される。
怒った時金髪になる
動作としては
- ボタンを押すとレコーディング開始
- レコーディングで怒りが規定値を超えたら撮影
- 顔を検知する
- 金髪を合成
- 数秒間は表示
金髪の表示位置はこんな感じ
とりあえず怒りの判定と金髪の仕込みはこんな感じ。
private void createSuperTikyuJin(JSONObject emotion) {
try{
int anger = emotion.getInt("anger");
if(anger >= 30){
boolean takePictureResult;
if (mCamera.takePicture(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader imageReader) {
Log.d(TAG, "onImageAvailable");
Matrix matrix = new Matrix();
matrix.preScale(0.3f, -0.3f);
final Image image = imageReader.acquireLatestImage();
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] buff = new byte[buffer.remaining()];
buffer.get(buff);
Bitmap bitmap = BitmapFactory.decodeByteArray(buff, 0, buff.length);
image.close();
mCorrectRotateBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
Frame frame = new Frame.Builder()
.setBitmap(mCorrectRotateBitmap)
.build();
SparseArray detectedFaces = mFaceDetector.detect(frame);
if (detectedFaces != null && detectedFaces.size() > 0) {
float maxSize = 0;
int idx = 0;
for (int i = 0; i < detectedFaces.size(); i++) {
Face face = (Face) detectedFaces.valueAt(i);
if (maxSize < (face.getWidth() * face.getHeight())) {
maxSize = (face.getWidth() * face.getHeight());
idx = i;
}
}
Face face = (Face) detectedFaces.valueAt(idx);
Bitmap kinpatsu = BitmapFactory.decodeResource(
getResources(),
R.drawable.super_hair);
float scaleWidth = face.getWidth() / (float) kinpatsu.getWidth();
float scaleHeight = face.getWidth() / (float) kinpatsu.getWidth();
Matrix kinpatsuScale = new Matrix();
kinpatsuScale.postScale(scaleWidth, scaleHeight);
Bitmap resizedKinpatsu = Bitmap.createBitmap(
kinpatsu,
0,
0,
kinpatsu.getWidth(),
kinpatsu.getHeight(),
kinpatsuScale,
false);
Canvas canvas = new Canvas(mCorrectRotateBitmap);
canvas.drawBitmap(
resizedKinpatsu,
face.getPosition().x,
face.getPosition().y - ((face.getHeight() / 2.0f)),
null);
imgSuperTikykujin.setImageBitmap(mCorrectRotateBitmap);
resizedKinpatsu.recycle();
kinpatsu.recycle();
Message msgShowImage = Message.obtain();
msgShowImage.what = MSG_SHOW_SUPER_TIKYUJIN;
handler.sendMessage(msgShowImage);
Message msgHideImage = Message.obtain();
msgShowImage.what = MSG_HIDE_SUPER_TIKYUJIN;
handler.sendMessageDelayed(msgHideImage, 5000);
} else {
Toast.makeText(getApplicationContext(), "face not exists", Toast.LENGTH_SHORT).show();
}
bitmap.recycle();
bitmap = null;
}
})) takePictureResult = true;
else takePictureResult = false;
} else {
// not angry(like a hotoke)
Toast.makeText(this, "怒りが足りない", Toast.LENGTH_LONG).show();
}
}catch(JSONException ex){
Toast.makeText(this, "怒りが感じられない(おかしい)", Toast.LENGTH_LONG).show();
}
}
振り返り
音声APIを使って何か作るのが目的だったけど、
ビデオの画像加工をやろうとして時間かかってしまった。
時間かかりそうだったから結局やめた。
泥沼にはまりそうだったから、結局諦めてよかった。
諦め大事。
あと、何回かやってると「穏やかさ」が増えてきた