怒りを爆発させて地球人を少しだけ超える

# 概要

音声感情解析セミナーに行ってきたので、早速使ってみる。
なんと300コール/月まで無料という太っ腹だ。使わざるをえない。

使い道を2分位考えた結果、怒りも数値で取れる事だし、怒りで目覚める事にした。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/96d13264-0189-c1ee-51d2-dacd5b8845f3.png)
<font color="DeepPink">何を思い浮かべるかで年がバレる瞬間</font>

# 結果

こんな感じ。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/d62ff35e-42ba-94e9-d503-673a3fb8cfe5.png)
<font color="DeepPink">何か想像より人造人間に近くなった。</font>

コード

[SuperTikyujin](https://github.com/miyatama/SuperTikyujin)

# 詳細

怒りで目覚めるとどうなるかというと、金髪になる。
なので怒りを検知したら金髪になるようなプログラムを作ることになる。

他にも

 + 眉毛がなくなる
 + 髪が長くなる
 + 青くなる
 + すごいゴリラになる

などなどあるが、似せれば似せるほど訴えられかねないのでやめる。

# 下準備

まずはアカウント作成
[ココ](https://webempath.net/sign_in)から新規アカウントを発行する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/be69427c-e347-4caf-bad7-d7e6d6bd4153.png)
<font color="DeepPink">新規ならSign up</font>

ログインしたら、APIキーを発行する。Addボタンを押すだけ。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/b53adab7-2a2f-3c2f-0765-d2d6c13309d3.png)

# 作成

## 録音してEmpathAPIを呼び出す

EmpathAPI自体はシンプルなWebAPIなので基本的になんにでも対応している。
ドキュメント上は下記言語のサンプルがある。

 + java
 + php
 + nodejs

なんでもよいのだが、今回はandroid javaで利用する。折角作るなら持ち運んで人に見せたいからだ。
音声を保存する部分は[ココ](http://steavevaivai.hatenablog.com/entry/2015/01/03/225505)が超絶参考になる。

まずは適当に画面を作る。シンプルにボタンとラベルだけ。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/b1bdb647-0740-cdfd-daff-be3fa5a44725.png)

後はボタンを押したら録音開始。一定時間たったら、AsyncTask経由でファイルに保存した音声を乗せたAPI呼び出すだけ。簡単。

```java:MainActivity.java
    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);
        }
    }
```

実行結果はこんな感じ

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/83bc60d8-2f74-0ae5-0825-34e8a95349ce.png)
<font color="DeepPink">悲しみが出てしまった</font>

呼べたらちゃんと記録される。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/5a70cba5-ce8d-b88d-f932-ee144bb6ee83.png)
<font color="DeepPink">後298回呼べる</font>

## 怒った時金髪になる

動作としては

 + ボタンを押すとレコーディング開始
 + レコーディングで怒りが規定値を超えたら撮影
 + 顔を検知する
 + 金髪を合成
 + 数秒間は表示

金髪の表示位置はこんな感じ

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/48705503-6ef8-9362-82c5-e3e6e43c1c04.png)
<font color="DeepPink">大きさは大体顔と同じぐらいかな</font>

とりあえず怒りの判定と金髪の仕込みはこんな感じ。

```java
    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を使って何か作るのが目的だったけど、
ビデオの画像加工をやろうとして時間かかってしまった。

時間かかりそうだったから結局やめた。
泥沼にはまりそうだったから、結局諦めてよかった。

諦め大事。

あと、何回かやってると「穏やかさ」が増えてきた

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/30c2ae3c-c097-3206-0cd6-93621775d236.png)
<font color="DeepPink">年取ると丸くなるんだろうな</font>