2017年06月02日

音を利用する 2 〜Android デバイスでの音波通信:準備編〜

はてなブックマークに登録

この記事の内容

前回の記事では音を媒体とする機器間の情報通信が近年ふたたび注目されていることに触れ複数の具体的な事例を挙げました。近距離通信手段としての音波の利用には技術的好奇心をそそられますが、それをどのように形にすればよいのかは案外ピンと来ません。そこで、Android デバイスを題材に手元で行った音波通信の実験と試作の内容をこれから何回かに分けて紹介します。今回は準備編として実装に必要な要素の整理を行います。

2017-11-29 追記:
本記事に掲載のコードは開発時に Android 5.x 環境を使用しており、Android 6.0 以降では android.permission.RECORD_AUDIO について実行時パーミッション取得が必要です。
この件への対応例として sonic01 の MainActivity.java に手を加えました。 (diff
他のプロジェクトについても適宜加味して下さい。

準備 1. Andorid での音声入出力処理を覚える

プラットフォームにより開発環境や API に違いはあっても音を扱う処理の本質は変わりません。手元でターゲットとして Android を選んだのは以下の理由によるものです。

  • メジャーである
  • サウンド I/O を標準で利用できる(マイコン系と比べての利点)
  • ポータブルであるため応用が利きやすく IoT 方面との連携性も良い(PC と比べての利点)
  • 開発環境の制約ごとが相対的に少ない(他のスマホ系と比べての利点)

まず、今回使用した音声入出力 API の情報をリファレンスより引用します。特に難しい要素もなく分かりやすい内容でした。

入力:AudioRecord クラス

  • AudioRecord
    The AudioRecord class manages the audio resources for Java applications to record audio from the audio input hardware of the platform.
          :
    Upon creation, an AudioRecord object initializes its associated audio buffer that it will fill with the new audio data.
          :
    Data should be read from the audio hardware in chunks of sizes inferior to the total recording buffer size.
    • AudioRecord (constructor)
      AudioRecord (int audioSource,
            int sampleRateInHz,
            int channelConfig,
            int audioFormat,
            int bufferSizeInBytes)
            :
      Parameters
            :
      bufferSizeInBytes
      int: the total size (in bytes) of the buffer where audio data is written to during the recording. New audio data can be read from this buffer in smaller chunks than this size. See getMinBufferSize(int, int, int) to determine the minimum required buffer size for the successful creation of an AudioRecord instance. Using values smaller than getMinBufferSize() will result in an initialization failure.
    • getMinBufferSize
      int getMinBufferSize (
            int sampleRateInHz,
            int channelConfig,
            int audioFormat)

      Returns the minimum buffer size required for the successful creation of an AudioRecord object, in byte units.
            :
    • startRecording
      void startRecording ()

      Starts recording from the AudioRecord instance.
    • read
      int read (short[] audioData,
            int offsetInShorts,
            int sizeInShorts)

      Reads audio data from the audio hardware for recording into a short array. The format specified in the AudioRecord constructor should be ENCODING_PCM_16BIT to correspond to the data in the array.
            :
    • getRecordingState
      int getRecordingState ()

      Returns the recording state of the AudioRecord instance.

      See also:
            RECORDSTATE_STOPPED
            RECORDSTATE_RECORDING
    • stop
      void stop ()

      Stops recording.

出力:AudioTrack クラス

  • AudioTrack
    The AudioTrack class manages and plays a single audio resource for Java applications. It allows streaming of PCM audio buffers to the audio sink for playback.
          :
    An AudioTrack instance can operate under two modes: static or streaming. In Streaming mode, the application writes a continuous stream of data to the AudioTrack, using one of the write() methods.
          :
    The static mode should be chosen when dealing with short sounds that fit in memory and that need to be played with the smallest latency possible.
          :
    • AudioTrack (constructor)
      AudioTrack (
            int streamType,
            int sampleRateInHz,
            int channelConfig,
            int audioFormat,
            int bufferSizeInBytes,
            int mode)
            :
      Parameters
            :
      int: the total size (in bytes) of the internal buffer where audio data is read from for playback. This should be a nonzero multiple of the frame size in bytes.
            :
      If the track's creation mode is MODE_STREAM, this should be the desired buffer size for the AudioTrack to satisfy the application's latency requirements. If bufferSizeInBytes is less than the minimum buffer size for the output sink, it is increased to the minimum buffer size.
            :
      See getMinBufferSize(int, int, int) to determine the estimated minimum buffer size for an AudioTrack instance in streaming mode.
    • play
      void play ()

      Starts playing an AudioTrack.

      If track's creation mode is MODE_STATIC, you must have called one of the write methods(中略)prior to play().

      If the mode is MODE_STREAM, you can optionally prime the data path prior to calling play(), by writing up to bufferSizeInBytes (from constructor). If you don't call write() first, or if you call write() but with an insufficient amount of data, then the track will be in underrun state at play(). In this case, playback will not actually start playing until the data path is filled to a device-specific minimum level.
            :
    • write
      int write (
            short[] audioData,
            int offsetInShorts,
            int sizeInShorts)

      Writes the audio data to the audio sink for playback (streaming mode), or copies audio data for later playback (static buffer mode). The format specified in the AudioTrack constructor should be ENCODING_PCM_16BIT to correspond to the data in the array.
            :
    • getPlayState
      int getPlayState ()

      Returns the playback state of the AudioTrack instance.

      See also:
            PLAYSTATE_STOPPED
            PLAYSTATE_PAUSED
            PLAYSTATE_PLAYING
    • pause
      void pause ()

      Pauses the playback of the audio data. Data that has not been played back will not be discarded. Subsequent calls to play() will play this data back. See flush() to discard this data.
            :
    • stop
      void stop ()

      Stops playing the audio data.
            :
    • flush
      void flush ()

      Flushes the audio data currently queued for playback. Any data that has been written but not yet presented will be discarded. No-op if not stopped or paused, or if the track's creation mode is not MODE_STREAM.
            :

試作

Android 環境での音声入出力の基本的な作法を覚えるために最初にごくシンプルなアプリを書いてみました。 入力側と出力側は多くの場合機能的に分けられますが、ここではその両方の要素を次の内容で単一のプログラムに組み入れています。

  • UI 上のボタンを押下するとマイクロフォンからの録音を開始する
  • 再度ボタンを押下すると録音を終了しその内容を再生する
  • 音声データはメモリ上で扱いファイルは使用しない

処理のイメージ

   

動作の様子

動画:33秒  ※音量注意!

ソースコード

sonic01     GitHub

  • MainActivity.java
    
    /**
     *
     * sonic01
     *
     * 端末のマイクから音声を録音しスピーカーで再生する
     * Android 標準の AudioRecord, AudioTrack を使用
     *
     * サンプリング周波数 44.1kHz
     * 量子化ビット数 16
     * モノラル
     *
     */
    
    package jp.klab.sonic01;
    
    import android.Manifest;
    import android.content.pm.PackageManager;
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.AudioManager;
    import android.media.AudioTrack;
    import android.media.MediaRecorder;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v4.app.ActivityCompat;
    import android.support.v4.content.ContextCompat;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.ProgressBar;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final int BLOCK_NUMBER = 300;
    
      private static final int MSG_RECORD_START = 100;
      private static final int MSG_RECORD_END   = 110;
      private static final int MSG_PLAY_END   = 130;
    
      private Handler mHandler;
      private AudioRecord mAudioRecord = null;
      private AudioTrack mAudioTrack = null;
    
      private Button mButton01;
      private ProgressBar mProgressBar;
    
      private boolean mInRecording = false;
      private boolean mStop = false;
      private int mBufferSizeInShort;
    
      private short mPlayBuf[];
      private short mRecordBuf[];
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mButton01 = (Button)findViewById(R.id.button01);
        mButton01.setOnClickListener(this);
        mProgressBar = (ProgressBar)findViewById(R.id.progressBar);
        mProgressBar.setMax(BLOCK_NUMBER);
        mProgressBar.setProgress(0);
    
        /***** add for Android 6.0 or later ****/
        // https://developer.android.com/training/permissions/requesting.html
        // https://developer.android.com/topic/libraries/support-library/index.html#backward
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) !=
            PackageManager.PERMISSION_GRANTED) {
          // RECORD_AUDIO の実行時パーミッションを要求
          ActivityCompat.requestPermissions(this,
              new String[]{Manifest.permission.RECORD_AUDIO}, 1);
        }
        initAudio();
        /**** add end ****/
    
    /***** move to initAudio() ****
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
        mBufferSizeInShort = bufferSizeInBytes / 2;
        // 録音用バッファ
        mRecordBuf = new short[mBufferSizeInShort];
        // 再生用バッファ
        mPlayBuf = new short[mBufferSizeInShort * BLOCK_NUMBER];
    
        // 録音用
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes);
        // 再生用
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_OUT_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes,
                        AudioTrack.MODE_STREAM);
    ****/
      }
    
      /**** add for Android 6.0 or later ****/
      // https://developer.android.com/training/permissions/requesting.html
      @Override
      public void onRequestPermissionsResult(int requestCode,
                           String permissions[], int[] grantResults) {
        switch (requestCode) {
          case 1: {
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
              initAudio();
            } else {
              finish();
            }
            return;
          }
        }
      }
    
      private void initAudio() {
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
            AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT);
    
        mBufferSizeInShort = bufferSizeInBytes / 2;
        // 録音用バッファ
        mRecordBuf = new short[mBufferSizeInShort];
        // 再生用バッファ
        mPlayBuf = new short[mBufferSizeInShort * BLOCK_NUMBER];
    
        // 録音用
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
            SAMPLE_RATE,
            AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT,
            bufferSizeInBytes);
        // 再生用
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
            SAMPLE_RATE,
            AudioFormat.CHANNEL_OUT_MONO,
            AudioFormat.ENCODING_PCM_16BIT,
            bufferSizeInBytes,
            AudioTrack.MODE_STREAM);
      }
      /**** add end ****/
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        mStop = true;
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioRecord != null) {
          if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioRecord");
            mAudioRecord.stop();
          }
          mAudioRecord = null;
        }
        if (mAudioTrack != null) {
          if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioTrack");
            mAudioTrack.stop();
            mAudioTrack.flush();
          }
          mAudioTrack = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        if (v == (View)mButton01) {
          // 集音開始 or 終了
          if (!mInRecording) {
            mInRecording = true;
            new Thread(this).start();
          } else {
            mInRecording = false;
          }
        }
        return;
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_RECORD_START:
            Log.d(TAG, "MSG_RECORD_START");
            mButton01.setText("STOP");
            break;
          case MSG_RECORD_END:
            Log.d(TAG, "MSG_RECORD_END");
            mButton01.setEnabled(false);
            break;
          case MSG_PLAY_END:
            Log.d(TAG, "MSG_PLAY_END");
            mButton01.setEnabled(true);
            mButton01.setText("START");
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        mHandler.sendEmptyMessage(MSG_RECORD_START);
        // 集音開始
        mAudioRecord.startRecording();
        int count = 0;
        while (mInRecording && !mStop) {
          mAudioRecord.read(mRecordBuf, 0, mBufferSizeInShort);
          // 再生用バッファはリングバッファとして扱う
          if (count * mBufferSizeInShort >= mPlayBuf.length) {
            count = 0;
            mProgressBar.setProgress(0);
          }
          // 再生用バッファへ集音したデータをアペンド
          System.arraycopy(mRecordBuf, 0, mPlayBuf, count * mBufferSizeInShort, mBufferSizeInShort);
          mProgressBar.setProgress(++count);
        }
        // 集音終了
        mAudioRecord.stop();
        mProgressBar.setProgress(0);
        mHandler.sendEmptyMessage(MSG_RECORD_END);
        if (mStop) {
          return;
        }
        // 再生
        mAudioTrack.setPlaybackRate(SAMPLE_RATE);
        mAudioTrack.play();
        mAudioTrack.write(mPlayBuf, 0, count * mBufferSizeInShort);
        mAudioTrack.stop();
        mAudioTrack.flush();
        mHandler.sendEmptyMessage(MSG_PLAY_END);
      }
    }
    
    
  • AndroidManifest.xml
    
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="jp.klab.sonic01">
      <uses-permission android:name="android.permission.RECORD_AUDIO"/>
      <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
          android:name=".MainActivity"
          android:label="@string/app_name"
          android:configChanges="orientation|screenSize"
          android:screenOrientation="unspecified"
          android:theme="@style/AppTheme.NoActionBar">
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>
        <!-- ATTENTION: This was auto-generated to add Google Play services to your project for
           App Indexing.  See https://g.co/AppIndexing/AndroidStudio for more information. -->
        <meta-data
          android:name="com.google.android.gms.version"
          android:value="@integer/google_play_services_version" />
      </application>
    </manifest>
    
    

準備 2. 任意の周波数の音波を生成・出力する

音による情報伝達(言語音を除く)においては、伝えたい内容の意味を構成する要素として「音の高さ(周波数)」と「音の大きさ(振幅)」が挙げられます。これらを所定の規則に基づいて組み合わせることで送り手と受け手の間でのやりとりが可能となるでしょう。送り手側にはそこで任意の周波数・振幅の音を作る処理が必要となります。これがふたつめの課題です。

サイン波について

音の波形には種類がありますが、ここでは基本波形のひとつであるサイン波を使うことにしました。サイン波をプログラムで生成するためにその性質・特徴と関連する情報を整理してみます。

  • 正弦波 - wikipedia より
    正弦波(せいげんは、sine wave、sinusoidal wave)は、正弦関数として観測可能な周期的変化を示す波動のことである。その波形は正弦曲線(せいげんきょくせん、sine curve)もしくはシヌソイド (Sinusoid) と呼ばれ、数学、信号処理、電気工学およびその他の分野において重要な働きをする。       :
  • 正弦波交流って、実は円運動なんです - manapo.com 様のサイトより
    ある点を中心に回転する運動は「円運動」と言われ、 物理の世界ではよく出てくる言葉でもある。この円運動の軌跡を回転角度を横軸にしてグラフにすると、このように正弦波の形になる。
    角度が90°のところで山になり、180°のところで出発点と同じになり、270°のところで谷になり、360°のところで元に戻る。

    図では回転角度ではなく、少しでも理解しやすいように時間としてある。言い換えると、円運動で1回転するのに必要な時間を周期と呼ぶのである。そして、1秒間の回転数が即ち周波数である。
  •  https://www.youtube.com/embed/ij0K2tUVEGs - surikenclub 様による Youtube 動画
  • 第5章 フーリエ級数 - Masasi.Sanae 様のサイトより
    ・1秒間に描かれる波の回数を「周波数」といい,単位はヘルツ(Hz)を用います。
    ・1秒間に進む角度のことを「角速度」といいω(オメガ)で表します。
    ・横軸からの波の振れ具合を「振幅」といいます。

    sin波,cos波の1つの波が現れる角度(周期といいます)は360°ですから

        角速度=周波数×360°

    が成り立ちます。次の例では周波数が3,角速度1080°,振幅1のsin波を表しています。
    周波数k,振幅aのsin波はy=a sin kωtで表されます。(ω=360°)

    y=sin 1080°t=sin 3ωt

  • ラジアン(弧度法) - wikipedia より
    ラジアン(radian, 単位記号: [rad])は、国際単位系 (SI) における角度(平面角)の単位である。円周上でその円の半径と同じ長さのを切り取る2本の半径が成す角の値と定義される。

    1ラジアンは度数法では (180/π)° で、およそ 57.29578° に相当する。180°は弧度法においては π rad であり、360° は 2π rad である。
          :
    数学で三角関数を扱う時は、角度にラジアンを用いるのが普通である。その理由は、それが「自然」だからであり、多くの公式が簡潔に書けるようになる。

    (※半径 r の円の円周は 2 π r であるため 円(=360°)を弧度法で表すと「2 π r ÷ r」。よって 360° = 2π ラジアン)

以上のことから、振幅が A、角速度 ω のサイン波信号のある時点 t 秒における値 y は次の式によって得られます。

    y = A * sin ( ω * t )

ここで、角速度 = 周波数( f Hz )× 360° であり 360° = 2π ラジアン であるため角速度は「周波数( f Hz ) × 2π」と表せるためこの式は次のようにも書けます。

    y = A * sin ( 2π * f * t )

サンプリングレートについて

所定の周波数の信号を出力するためには相応のサンプリング(標本化)レートが必要となります。

オーディオ CD の規格で採用されているサンプリングレート 44,100Hz であれば理論上 22,000Hz 付近までの再現が可能であるため実用上の不足はないでしょう。また、十分なダイナミックレンジを確保するために振幅情報の量子化にはこれもオーディオ CD と同様に 16ビットを用いることとします。(ちなみに DVD は 24ビットだそうです)

計算してみると、「サンプリングレート 44,100Hz・量子化ビット数 16・モノラル」で 1秒間の信号を構成するには、44,100点 * 2バイト * 1チャネル = 88,200バイトのメモリ空間が必要ということになります。

関連情報

試作

以上の情報をもとにプログラムを試作しました。200Hz 〜 14,000Hz の範囲の 10種類の周波数のサイン波信号を生成・出力するものです。ここでは要素数 44,100 の short 型配列に 1秒分のサイン波データを格納しループ再生を行っています。

動作の様子

動画:2分13秒  ※音量注意!


分析に使用しているアプリ: AudioUtil Spectrum Analyzer - play.google.com

ソースコード

sonic03     GitHub

  • MainActivity.java
    
    /**
     *
     * sonic03
     *
     * サイン波信号を生成して鳴らす
     *
     * サンプリング周波数 44.1kHz
     * 量子化ビット数 16
     * モノラル
     *
     */
    
    package jp.klab.sonic03;
    
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.AudioManager;
    import android.media.AudioTrack;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.ToggleButton;
    
    import java.util.Arrays;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final float SEC_PER_SAMPLEPOINT = 1.0f / SAMPLE_RATE;
      private static final int AMP = 4000;
    
      private static final int [] FREQS =
          new int[] {200, 400, 800, 1600, 2000, 4000, 8000, 10000, 12000, 14000};
      private static final int DO = 262 * 2;
      private static final int RE = 294 * 2;
      private static final int MI = 330 * 2;
      private static final int FA = 349 * 2;
      private static final int SO = 392 * 2;
      private static final int RA = 440 * 2;
      private static final int SI = 494 * 2;
    
      private static final int MSG_PLAY_START = 120;
      private static final int MSG_PLAY_END   = 130;
    
      private Handler mHandler;
      private AudioTrack mAudioTrack = null;
      private ToggleButton mButtons[];
      private int mIdxButtonPushed = -1;
      private int mBufferSizeInShort;
      private short mPlayBuf[];
    
      // 1秒分のサイン波データを生成
      private void createSineWave(int freq, int amplitude, boolean doClear) {
        if (doClear) {
          Arrays.fill(mPlayBuf, (short) 0);
        }
        for (int i = 0; i < SAMPLE_RATE; i++) {
          float currentSec = i * SEC_PER_SAMPLEPOINT; // 現在位置の経過秒数
          // y(t) = A * sin(2π * f * t)
          double val = amplitude * Math.sin(2.0 * Math.PI * freq * currentSec);
          mPlayBuf[i] += (short)val;
        }
      }
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mButtons = new ToggleButton[11];
        (mButtons[0] = (ToggleButton)findViewById(R.id.button01)).setOnClickListener(this);
        (mButtons[1] = (ToggleButton)findViewById(R.id.button02)).setOnClickListener(this);
        (mButtons[2] = (ToggleButton)findViewById(R.id.button03)).setOnClickListener(this);
        (mButtons[3] = (ToggleButton)findViewById(R.id.button04)).setOnClickListener(this);
        (mButtons[4] = (ToggleButton)findViewById(R.id.button05)).setOnClickListener(this);
        (mButtons[5] = (ToggleButton)findViewById(R.id.button06)).setOnClickListener(this);
        (mButtons[6] = (ToggleButton)findViewById(R.id.button07)).setOnClickListener(this);
        (mButtons[7] = (ToggleButton)findViewById(R.id.button08)).setOnClickListener(this);
        (mButtons[8] = (ToggleButton)findViewById(R.id.button09)).setOnClickListener(this);
        (mButtons[9] = (ToggleButton)findViewById(R.id.button10)).setOnClickListener(this);
        (mButtons[10] = (ToggleButton)findViewById(R.id.button11)).setOnClickListener(this);
    
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
        // 再生用バッファ
        mPlayBuf = new short[SAMPLE_RATE]; // 1秒分のバッファを確保
    
        // 再生用
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_OUT_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes,
                        AudioTrack.MODE_STREAM);
      }
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioTrack != null) {
          if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioTrack");
            mAudioTrack.stop();
            mAudioTrack.flush();
          }
          mAudioTrack = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        for (int i = 0; i < mButtons.length; i++) {
          if (mButtons[i] == (ToggleButton)v) {
            mIdxButtonPushed = i;
            break;
          }
        }
        if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
          mAudioTrack.stop();
          mAudioTrack.flush();
        }
        if (mButtons[mIdxButtonPushed].isChecked()) {
          new Thread(this).start();
        }
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_PLAY_START:
            Log.d(TAG, "MSG_PLAY_START");
            break;
          case MSG_PLAY_END:
            Log.d(TAG, "MSG_PLAY_END");
            mButtons[mIdxButtonPushed].setChecked(false);
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        mHandler.sendEmptyMessage(MSG_PLAY_START);
        if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
          mAudioTrack.stop();
          mAudioTrack.flush();
        }
        mAudioTrack.play();
        if (mIdxButtonPushed != 10) {
          createSineWave(FREQS[mIdxButtonPushed], AMP, true);
          for (int i = 0; i < 5; i++) { // 5秒程度鳴らす
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE);
          }
        } else { // SONG
          for (int i = 0; i < 2; i++) {
            mAudioTrack.play();
            createSineWave(DO, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(RE, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(MI, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(FA, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(MI, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(RE, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(DO, AMP, true);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE);
    
            // 和音
            createSineWave(DO, AMP, true);
            createSineWave(MI, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(RE, AMP, true);
            createSineWave(FA, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(MI, AMP, true);
            createSineWave(SO, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(FA, AMP, true);
            createSineWave(RA, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(MI, AMP, true);
            createSineWave(SO, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(RE, AMP, true);
            createSineWave(FA, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE / 2);
            createSineWave(DO, AMP, true);
            createSineWave(MI, AMP, false);
            mAudioTrack.write(mPlayBuf, 0, SAMPLE_RATE);
          }
        }
        mAudioTrack.stop();
        mAudioTrack.flush();
        mHandler.sendEmptyMessage(MSG_PLAY_END);
      }
    }
    
    
  • AndroidManifest.xml
    
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="jp.klab.sonic03">
      <uses-permission android:name="android.permission.RECORD_AUDIO"/>
      <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
          android:name=".MainActivity"
          android:label="@string/app_name"
          android:configChanges="orientation|screenSize"
          android:screenOrientation="unspecified"
          android:theme="@style/AppTheme.NoActionBar">
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>
        <!-- ATTENTION: This was auto-generated to add Google Play services to your project for
           App Indexing.  See https://g.co/AppIndexing/AndroidStudio for more information. -->
        <meta-data
          android:name="com.google.android.gms.version"
          android:value="@integer/google_play_services_version" />
      </application>
    </manifest>
    
    

スプレッドシートを使っての検証

前述のように上のコードの createSineWave 関数は要素数 44,100 の short 型配列に 44,100Hz・16ビット・モノラル 1秒分のサイン波データを生成しています。

この配列の先頭 441 要素分、つまり 1/100 秒分のデータとそれをグラフ化したものを以下に示します。それぞれ期待通りの波形が生成されていることが見てとれます。

  • 周波数 400Hz を指定した場合:400Hz/100 につき 4 周期分の出力
    element,value
    0,0
    1,227
    2,454
    3,680
    4,903
    5,1124
    6,1341
    7,1553
    8,1761
    9,1962
    10,2158
    11,2346
    12,2527
    13,2699
    14,2863
    15,3017
    16,3162
    17,3296
    18,3420
    19,3533
    20,3634
    21,3723
    22,3800
    23,3865
    24,3917
    25,3957
    26,3984
    27,3997
    28,3998
    29,3986
    30,3961
    31,3923
    32,3872
    33,3809
    34,3733
    35,3646
    36,3546
    37,3435
    38,3312
    39,3179
    40,3036
    41,2883
    42,2720
    43,2549
    44,2369
    45,2182
    46,1987
    47,1786
    48,1579
    49,1368
    50,1151
    51,931
    52,708
    53,483
    54,256
    55,28
    56,-199
    57,-426
    58,-652
    59,-876
    60,-1097
    61,-1314
    62,-1527
    63,-1735
    64,-1937
    65,-2134
    66,-2323
    67,-2505
    68,-2678
    69,-2843
    70,-2999
    71,-3145
    72,-3280
    73,-3405
    74,-3519
    75,-3622
    76,-3712
    77,-3791
    78,-3858
    79,-3911
    80,-3953
    81,-3981
    82,-3996
    83,-3999
    84,-3988
    85,-3965
    86,-3928
    87,-3879
    88,-3818
    89,-3743
    90,-3657
    91,-3559
    92,-3449
    93,-3328
    94,-3197
    95,-3055
    96,-2902
    97,-2741
    98,-2571
    99,-2392
    100,-2205
    101,-2012
    102,-1812
    103,-1606
    104,-1394
    105,-1179
    106,-959
    107,-736
    108,-511
    109,-284
    110,-56
    111,170
    112,398
    113,624
    114,848
    115,1069
    116,1287
    117,1501
    118,1709
    119,1913
    120,2110
    121,2300
    122,2482
    123,2657
    124,2823
    125,2980
    126,3127
    127,3264
    128,3390
    129,3506
    130,3610
    131,3702
    132,3782
    133,3850
    134,3905
    135,3948
    136,3978
    137,3995
    138,3999
    139,3990
    140,3968
    141,3934
    142,3886
    143,3826
    144,3753
    145,3669
    146,3572
    147,3464
    148,3344
    149,3214
    150,3073
    151,2922
    152,2762
    153,2592
    154,2415
    155,2229
    156,2036
    157,1837
    158,1632
    159,1421
    160,1206
    161,987
    162,764
    163,539
    164,313
    165,85
    166,-142
    167,-369
    168,-596
    169,-820
    170,-1042
    171,-1260
    172,-1474
    173,-1684
    174,-1887
    175,-2085
    176,-2276
    177,-2460
    178,-2636
    179,-2803
    180,-2961
    181,-3109
    182,-3247
    183,-3375
    184,-3492
    185,-3597
    186,-3691
    187,-3773
    188,-3842
    189,-3899
    190,-3944
    191,-3975
    192,-3994
    193,-3999
    194,-3992
    195,-3972
    196,-3939
    197,-3893
    198,-3834
    199,-3763
    200,-3680
    201,-3585
    202,-3478
    203,-3360
    204,-3231
    205,-3091
    206,-2941
    207,-2782
    208,-2614
    209,-2437
    210,-2253
    211,-2061
    212,-1862
    213,-1658
    214,-1448
    215,-1233
    216,-1014
    217,-792
    218,-567
    219,-341
    220,-113
    221,113
    222,341
    223,567
    224,792
    225,1014
    226,1233
    227,1448
    228,1658
    229,1862
    230,2061
    231,2253
    232,2437
    233,2614
    234,2782
    235,2941
    236,3091
    237,3231
    238,3360
    239,3478
    240,3585
    241,3680
    242,3763
    243,3834
    244,3893
    245,3939
    246,3972
    247,3992
    248,3999
    249,3994
    250,3975
    251,3944
    252,3899
    253,3842
    254,3773
    255,3691
    256,3597
    257,3492
    258,3375
    259,3247
    260,3109
    261,2961
    262,2803
    263,2636
    264,2460
    265,2276
    266,2085
    267,1887
    268,1684
    269,1474
    270,1260
    271,1042
    272,820
    273,596
    274,369
    275,142
    276,-85
    277,-313
    278,-539
    279,-764
    280,-987
    281,-1206
    282,-1421
    283,-1632
    284,-1837
    285,-2036
    286,-2229
    287,-2415
    288,-2592
    289,-2762
    290,-2922
    291,-3073
    292,-3214
    293,-3344
    294,-3464
    295,-3572
    296,-3669
    297,-3753
    298,-3826
    299,-3886
    300,-3934
    301,-3968
    302,-3990
    303,-3999
    304,-3995
    305,-3978
    306,-3948
    307,-3905
    308,-3850
    309,-3782
    310,-3702
    311,-3610
    312,-3506
    313,-3390
    314,-3264
    315,-3127
    316,-2980
    317,-2823
    318,-2657
    319,-2482
    320,-2300
    321,-2110
    322,-1913
    323,-1709
    324,-1501
    325,-1287
    326,-1069
    327,-848
    328,-624
    329,-398
    330,-170
    331,56
    332,284
    333,511
    334,736
    335,959
    336,1179
    337,1394
    338,1606
    339,1812
    340,2012
    341,2205
    342,2392
    343,2571
    344,2741
    345,2902
    346,3055
    347,3197
    348,3328
    349,3449
    350,3559
    351,3657
    352,3743
    353,3818
    354,3879
    355,3928
    356,3965
    357,3988
    358,3999
    359,3996
    360,3981
    361,3953
    362,3911
    363,3858
    364,3791
    365,3712
    366,3622
    367,3519
    368,3405
    369,3280
    370,3145
    371,2999
    372,2843
    373,2678
    374,2505
    375,2323
    376,2134
    377,1937
    378,1735
    379,1527
    380,1314
    381,1097
    382,876
    383,652
    384,426
    385,199
    386,-28
    387,-256
    388,-483
    389,-708
    390,-931
    391,-1151
    392,-1368
    393,-1579
    394,-1786
    395,-1987
    396,-2182
    397,-2369
    398,-2549
    399,-2720
    400,-2883
    401,-3036
    402,-3179
    403,-3312
    404,-3435
    405,-3546
    406,-3646
    407,-3733
    408,-3809
    409,-3872
    410,-3923
    411,-3961
    412,-3986
    413,-3998
    414,-3997
    415,-3984
    416,-3957
    417,-3917
    418,-3865
    419,-3800
    420,-3723
    421,-3634
    422,-3533
    423,-3420
    424,-3296
    425,-3162
    426,-3017
    427,-2863
    428,-2699
    429,-2527
    430,-2346
    431,-2158
    432,-1962
    433,-1761
    434,-1553
    435,-1341
    436,-1124
    437,-903
    438,-680
    439,-454
    440,-227
    
  • 周波数 1,600Hz を指定した場合:1,600Hz/100 につき 16 周期分の出力
    element,value
    0,0
    1,903
    2,1761
    3,2527
    4,3162
    5,3634
    6,3917
    7,3998
    8,3872
    9,3546
    10,3036
    11,2369
    12,1579
    13,708
    14,-199
    15,-1097
    16,-1937
    17,-2678
    18,-3280
    19,-3712
    20,-3953
    21,-3988
    22,-3818
    23,-3449
    24,-2902
    25,-2205
    26,-1394
    27,-511
    28,398
    29,1287
    30,2110
    31,2823
    32,3390
    33,3782
    34,3978
    35,3968
    36,3753
    37,3344
    38,2762
    39,2036
    40,1206
    41,313
    42,-596
    43,-1474
    44,-2276
    45,-2961
    46,-3492
    47,-3842
    48,-3994
    49,-3939
    50,-3680
    51,-3231
    52,-2614
    53,-1862
    54,-1014
    55,-113
    56,792
    57,1658
    58,2437
    59,3091
    60,3585
    61,3893
    62,3999
    63,3899
    64,3597
    65,3109
    66,2460
    67,1684
    68,820
    69,-85
    70,-987
    71,-1837
    72,-2592
    73,-3214
    74,-3669
    75,-3934
    76,-3995
    77,-3850
    78,-3506
    79,-2980
    80,-2300
    81,-1501
    82,-624
    83,284
    84,1179
    85,2012
    86,2741
    87,3328
    88,3743
    89,3965
    90,3981
    91,3791
    92,3405
    93,2843
    94,2134
    95,1314
    96,426
    97,-483
    98,-1368
    99,-2182
    100,-2883
    101,-3435
    102,-3809
    103,-3986
    104,-3957
    105,-3723
    106,-3296
    107,-2699
    108,-1962
    109,-1124
    110,-227
    111,680
    112,1553
    113,2346
    114,3017
    115,3533
    116,3865
    117,3997
    118,3923
    119,3646
    120,3179
    121,2549
    122,1786
    123,931
    124,28
    125,-876
    126,-1735
    127,-2505
    128,-3145
    129,-3622
    130,-3911
    131,-3999
    132,-3879
    133,-3559
    134,-3055
    135,-2392
    136,-1606
    137,-736
    138,170
    139,1069
    140,1913
    141,2657
    142,3264
    143,3702
    144,3948
    145,3990
    146,3826
    147,3464
    148,2922
    149,2229
    150,1421
    151,539
    152,-369
    153,-1260
    154,-2085
    155,-2803
    156,-3375
    157,-3773
    158,-3975
    159,-3972
    160,-3763
    161,-3360
    162,-2782
    163,-2061
    164,-1233
    165,-341
    166,567
    167,1448
    168,2253
    169,2941
    170,3478
    171,3834
    172,3992
    173,3944
    174,3691
    175,3247
    176,2636
    177,1887
    178,1042
    179,142
    180,-764
    181,-1632
    182,-2415
    183,-3073
    184,-3572
    185,-3886
    186,-3999
    187,-3905
    188,-3610
    189,-3127
    190,-2482
    191,-1709
    192,-848
    193,56
    194,959
    195,1812
    196,2571
    197,3197
    198,3657
    199,3928
    200,3996
    201,3858
    202,3519
    203,2999
    204,2323
    205,1527
    206,652
    207,-256
    208,-1151
    209,-1987
    210,-2720
    211,-3312
    212,-3733
    213,-3961
    214,-3984
    215,-3800
    216,-3420
    217,-2863
    218,-2158
    219,-1341
    220,-454
    221,454
    222,1341
    223,2158
    224,2863
    225,3420
    226,3800
    227,3984
    228,3961
    229,3733
    230,3312
    231,2720
    232,1987
    234,256
    235,-652
    236,-1527
    237,-2323
    238,-2999
    239,-3519
    240,-3858
    241,-3996
    242,-3928
    243,-3657
    244,-3197
    245,-2571
    246,-1812
    247,-959
    248,-56
    249,848
    250,1709
    251,2482
    252,3127
    253,3610
    254,3905
    255,3999
    256,3886
    257,3572
    258,3073
    259,2415
    260,1632
    261,764
    262,-142
    263,-1042
    264,-1887
    265,-2636
    266,-3247
    267,-3691
    268,-3944
    269,-3992
    270,-3834
    271,-3478
    272,-2941
    273,-2253
    274,-1448
    275,-567
    276,341
    277,1233
    278,2061
    279,2782
    280,3360
    281,3763
    282,3972
    283,3975
    284,3773
    285,3375
    286,2803
    287,2085
    288,1260
    289,369
    290,-539
    291,-1421
    292,-2229
    293,-2922
    294,-3464
    295,-3826
    296,-3990
    297,-3948
    298,-3702
    299,-3264
    300,-2657
    302,-1069
    303,-170
    304,736
    305,1606
    306,2392
    307,3055
    308,3559
    309,3879
    310,3999
    311,3911
    312,3622
    313,3145
    314,2505
    315,1735
    316,876
    317,-28
    318,-931
    319,-1786
    320,-2549
    321,-3179
    322,-3646
    323,-3923
    324,-3997
    325,-3865
    326,-3533
    327,-3017
    328,-2346
    329,-1553
    330,-680
    331,227
    332,1124
    333,1962
    334,2699
    335,3296
    336,3723
    337,3957
    338,3986
    339,3809
    340,3435
    341,2883
    342,2182
    343,1368
    344,483
    345,-426
    346,-1314
    347,-2134
    348,-2843
    349,-3405
    350,-3791
    351,-3981
    352,-3965
    353,-3743
    354,-3328
    355,-2741
    356,-2012
    357,-1179
    358,-284
    359,624
    360,1501
    361,2300
    362,2980
    363,3506
    364,3850
    365,3995
    366,3934
    367,3669
    368,3214
    369,2592
    370,1837
    371,987
    372,85
    373,-820
    374,-1684
    375,-2460
    376,-3109
    377,-3597
    378,-3899
    379,-3999
    380,-3893
    381,-3585
    382,-3091
    383,-2437
    384,-1658
    385,-792
    386,113
    387,1014
    388,1862
    389,2614
    390,3231
    391,3680
    392,3939
    393,3994
    394,3842
    395,3492
    396,2961
    397,2276
    398,1474
    399,596
    400,-313
    401,-1206
    402,-2036
    403,-2762
    404,-3344
    405,-3753
    406,-3968
    407,-3978
    408,-3782
    409,-3390
    410,-2823
    411,-2110
    412,-1287
    413,-398
    414,511
    415,1394
    416,2205
    417,2902
    418,3449
    419,3818
    420,3988
    421,3953
    422,3712
    423,3280
    424,2678
    425,1937
    426,1097
    427,199
    428,-708
    429,-1579
    430,-2369
    431,-3036
    432,-3546
    433,-3872
    434,-3998
    435,-3917
    436,-3634
    437,-3162
    438,-2527
    439,-1761
    440,-903
    

準備 3. 集音したデータの周波数分析を行う

FFT(Fast Fourier Transform 高速フーリエ変換)について

無響室のように人為的な特殊環境を除き世界は雑多な音で満ちています。人間の脳は自身が無意識のうちに複数の音の中から必要な音を選り分けており(カクテルパーティー効果)、静かに感じられる室内であってもレコーダーでの録音内容を再生すると意外なノイズに気づくといった経験は一般的なものですね。そのため、受け手側の装置が特定の音を検知するためには必要なものだけを明示的にピックアップする必要があります。これがみっつめの課題です。広く知られるフーリエ変換を音声信号に適用すれば音の塊を周波数成分ごとに分析できます。

フーリエ変換とは

  • フーリエ変換の本質 - apphokuson 様のサイトより

    全ての信号は、上図のように波の足しあわせで表現することが出来ます。

    具体的には、周波数が1の波1と周波数が2の波2と周波数が3の波3と・・・周波数が nの波nを足し合わせることで、あらゆる信号を表現することが出来るのです。
          :
    周波数を横軸に、振幅の大きさ(パワー値)を縦軸にとってグラフを書きなおしてみます。
          :
    この結果は、左図の信号に周波数1の波が大きさ4で含まれていること、周波数2の 波が大きさ0.5で含まれていること、周波数3の波が・・・ということを意味しています。
          :
    フーリエ変換を行えば、「信号に含まれる周波数の成分比」を得ることが出来ます。
          :

    (※引用に際し表記に一部手を加えさせて頂きました tanabe)

    • 統計的音声認識の基本原理 - 篠崎隆宏様のサイトより
      音声信号は,一次元の時系列データです.横軸を時間,縦軸を音の大きさとしてグラフを作成すると,以下のような図が得られます.

高速フーリエ変換とは

  • 高速フーリエ変換 - wikipedia より
    高速フーリエ変換(こうそくフーリエへんかん、英: Fast Fourier Transform、FFT) とは、離散フーリエ変換 (Discrete Fourier Transform、DFT) を計算機上で高速に 計算するアルゴリズム。FFTの逆変換をIFFT (Inverse FFT) と呼ぶ。
    • 離散フーリエ変換入門 - EnergyChord 様のサイトより
      コンピュータで行われるフーリエ変換は基本的に離散フーリエ変換である.なぜなら,電子計算機は連続量をそのまま扱うことができないため,コンピュータが処理するデータは必ず離散化される必要があるからである.つまり,コンピュータによるデータ処理が当たり前となった現代技術においては,フーリエ変換と言えば離散フーリエ変換を指すと言っても過言ではない.
      • 離散化とは - IT用語辞典バイナリより
        離散化とは、ある連続した情報を、非連続の値に分割することである。
        連続した値を持った情報を解析することは非常に困難であるが、離散化を行い非連続な数値に置き換えることで、近似的な計算結果を比較的容易に算出することが可能となる。
    • フーリエ変換と離散フーリエ変換 - 岩本真裕子様のサイトより
            :
      フーリエ変換では、波y(t)を構成する三角関数とその周波数・振幅・初期位相などを知ることができるため、波の特徴を明確に捉えることができる。

      離散フーリエ変換

      これまでは、連続関数に対するフーリエ変換およびフーリエ積分(逆フーリエ変換)について考えてきた。実際、音声や音楽などは、連続的な波として出力される。しかし、コンピュータは離散値でしか扱えないため、Analog-Didital (AD) 変換器を使って、一定間隔でサンプリングし、コンピュータが直接扱える離散的な波に変換される。
            :


      連続的な波と離散的な波の関係

      フーリエ変換は、連続的な波を三角関数を重ね合わせることで表そうとするものであった。例えば、下図のように、8個の周期関数を重ね合わせたものが実際の音であったとする。


      次に、AD変換によって、1秒間に4個のサンプリング数(4等分)で連続的な波が離散化されたとする(黒丸)。よく見てみると、cos 3t の離散の4点は cos t の離散の4点と全く同じである。実は sin 3t と sin t についても同様である。
      つまり、離散化された波においては、高周波数の波は低周波数の波に吸収されてしまう。今の場合は、重ね合わせられた複雑な波が、k=0,1,2 の三角関数のみで表せるということになってしまう。
      しかし、容易に予想できるように、サンプリング数を 増やすと、この問題は解消する。
            :
      離散フーリエ変換を用いた音などの解析や加工には、サンプリングは非常に重要な要素であり、扱う波についてある程度特徴や性質を知っている必要がある。例えば、人間が聞くことができる音の周波数は、20 Hzから20 kHzと言われている(一般的には15 kHzくらいまで)ので、人間が聞くことができるすべての周波数をカバーしようとすると、サンプリング数は1秒間あたり、20000(Hz)×2=40000 回必要となる。
            :

FFT ライブラリ「JTransforms」

Android には標準の Visualizer クラスに FFT 用の API getFft() が用意されています。ただし、あくまでも「The Visualizer class enables application to retrieve part of the currently playing audio for visualization purpose.」であり、「Frequency data: 8-bit magnitude FFT by using the getFft(byte[]) method」と機能的にも控えめな内容となっています。 Android で使える FFT ライブラリには複数の選択肢がありますが、Piotr Wendykier 氏による JTransforms が高性能かつメジャーであることを知りこれを利用することにしました。

  • JTransforms - Piotr Wendykier - sites.google.com
    JTransforms is the first, open source, multithreaded FFT library written in pure Java. Currently, four types of transforms are available: Discrete Fourier Transform (DFT), Discrete Cosine Transform (DCT), Discrete Sine Transform (DST) and Discrete Hartley Transform (DHT). The code is derived from General Purpose FFT Package written by Takuya Ooura and from Java FFTPack written by Baoshe Zhang.

    (Google 訳)
    JTransformsは、純粋なJavaで書かれた最初のオープンソースのマルチスレッド FFTライブラリです。現在、離散フーリエ変換(DFT)、離散コサイン変換(DCT)、 離散サイン変換(DST)および離散ハートレー変換(DHT)の4種類の変換が利用可能 である。このコードは、Takuya Ooura が書いた汎用FFTパッケージと、Baoshe Zhangが 書いたJava FFTPackから得られたものです。
          :
    License
    JTransforms is distributed under the terms of the BSD-2-Clause license.
  • wendykierp/JTransforms - GitHub
    • Documentation | Overview (JTransforms 3.1 API) - wendykierp.github.io
      JTransforms 3.1 API
      Packages
      Package                Description
      org.jtransforms.dct    Discrete Cosine Transforms.
      org.jtransforms.dht    Discrete Hartley Transforms.
      org.jtransforms.dst    Discrete Sine Transforms.
      org.jtransforms.fft    Discrete Fourier Transforms.
      org.jtransforms.utils  Utility classes.
      • Class DoubleFFT_1D
        org.jtransforms.fft
        Class DoubleFFT_1D

        Computes 1D Discrete Fourier Transform (DFT) of complex and real,double precision data. The size of the data can be an arbitrary number.

        (Google 訳)
        複素数と実数の倍精度データの一次元離散フーリエ変換(DFT)を計算します。データのサイズは任意の数にすることができます。
前掲のように音声信号は一次元データなので使うのはこの DoubleFFT_1D クラスでよさそうですが Javadoc では利用方法がさっぱりわからず使用例をネットで探しました。
  • JTransforms FFT in Android from PCM data - stackoverflow.com
      System.arraycopy(applyWindow(sampleData), 0, a, 0, sampleData.length);
      fft.realForward(a);

      /* find the peak magnitude and it's index */
      double maxMag = Double.NEGATIVE_INFINITY;
      int maxInd = -1;

      for(int i = 0; i < a.length / 2; ++i) {
        double re  = a[2*i];
        double im  = a[2*i+1];
        double mag = Math.sqrt(re * re + im * im);

        if(mag > maxMag) {
          maxMag = mag;
          maxInd = i;
        }
      }
  • How to get frequency from fft result? - stackoverflow.com
    The complex data is interleaved, with real components at even indices and imaginary components at odd indices, i.e. the real components are at index 2*i, the imaginary components are at index 2*i+1.

    To get the magnitude of the spectrum at index i, you want:

    re = fft[2*i];
    im = fft[2*i+1];
    magnitude[i] = sqrt(re*re+im*im);

    Then you can plot magnitude[i] for i = 0 to N / 2 to get the power spectrum. Depending on the nature of your audio input you should see one or more peaks in the spectrum.

    To get the approximate frequency of any given peak you can convert the index of the peak as follows:

    freq = i * Fs / N;

    where:

    freq = frequency in Hz
    i = index of peak
    Fs = sample rate (e.g. 44100 Hz or whatever you are using)
    N = size of FFT (e.g. 1024 in your case)

なるほど、使い方は難しくないようです。整理してみます。

  • 音声情報(=実数のみで構成される)を FFT にかける場合は信号データの配列を引数として realForward メソッドを呼び出す
  • 実行結果は配列に複素数で上書きされ偶数インデックスには実部、ひとつ後の奇数インデックスには虚部、というペアが続く
  • 各周波数成分の振幅値は当該複素数の絶対値re2+im2 で得られる
    • 複素平面 - KIT数学ナビゲーション様のサイトより
      xy 平面において, x 軸に実数, y 軸に虚数を対応させて,複素数を表したものを複素平面という.または,複素数平面,ガウス平面ともいう.

      複素数 z=a+ⅈb を複素平面上に表したものが,右の図である.

      複素数z の絶対値の定義:

      | z |=| a+b |= a 2 + b 2 =r

      すなわち,複素平面上の原点Oから z までの距離 r となる.
  • 周波数は「配列要素のインデックス * サンプリングレート / 信号データの要素数(=「FFT サイズ」)」の式で得られる
  • つまり、もっとも振幅の大きい周波数(ピーク周波数)を得るには最大振幅値を持つ配列要素のインデックスを上の式に突っ込めばよい

試作

上記の JTransforms ライブラリを利用して集音データ中のピーク周波数を画面に表示するアプリを作成。期待どおりの結果が得られました。

動作の様子

動画:1分7秒  ※音量注意!

ソースコード

sonic04     GitHub

  • MainActivity.java
    
    /**
     *
     * sonic04
     *
     * 端末のマイクから音声を受信しピーク周波数を表示する
     * FFT 処理に JTransforms ライブラリを利用
     *
     * サンプリング周波数 44.1kHz
     * 量子化ビット数 16
     * モノラル
     *
     */
    
    package jp.klab.sonic04;
    
    
    import org.jtransforms.fft.DoubleFFT_1D;
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.MediaRecorder;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.Toolbar;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    
    public class MainActivity extends AppCompatActivity
        implements Runnable, View.OnClickListener, Handler.Callback {
      private static final String TAG = "SNC";
    
      private static final int SAMPLE_RATE = 44100;
      private static final short THRESHOLD_AMP = 0x00ff;
    
      private static final int MSG_RECORD_START = 100;
      private static final int MSG_RECORD_END   = 110;
      private static final int MSG_FREQ_PEAK  = 120;
      private static final int MSG_SILENCE    = 130;
    
      private Handler mHandler;
      private AudioRecord mAudioRecord = null;
    
      private Button mButton01;
      private TextView mTextView02;
    
      private boolean mInRecording = false;
      private boolean mStop = false;
      private int mBufferSizeInShort;
    
      private short mRecordBuf[];
      private DoubleFFT_1D mFFT;
      private double mFFTBuffer[];
      private int mFFTSize;
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        mHandler = new Handler(this);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mButton01 = (Button)findViewById(R.id.button01);
        mButton01.setOnClickListener(this);
        mTextView02 = (TextView)findViewById(R.id.textView02);
    
        int bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
        mBufferSizeInShort = bufferSizeInBytes / 2;
        // 録音用バッファ
        mRecordBuf = new short[mBufferSizeInShort];
    
        // FFT 処理用
        mFFTSize = mBufferSizeInShort;
        mFFT = new DoubleFFT_1D(mFFTSize);
        mFFTBuffer = new double[mFFTSize];
    
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes);
      }
    
      @Override
      public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        mStop = true;
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        if (mAudioRecord != null) {
          if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
            Log.d(TAG, "cleanup mAudioRecord");
            mAudioRecord.stop();
          }
          mAudioRecord = null;
        }
      }
    
      @Override
      public void onClick(View v) {
        if (v == (View)mButton01) {
          // 集音開始 or 終了
          if (!mInRecording) {
            mInRecording = true;
            new Thread(this).start();
          } else {
            mInRecording = false;
          }
        }
        return;
      }
    
      @Override
      public boolean handleMessage(Message msg) {
        switch (msg.what) {
          case MSG_RECORD_START:
            Log.d(TAG, "MSG_RECORD_START");
            mButton01.setText("STOP");
            break;
          case MSG_RECORD_END:
            Log.d(TAG, "MSG_RECORD_END");
            mButton01.setText("START");
            break;
          case MSG_FREQ_PEAK:
            mTextView02.setText(Integer.toString(msg.arg1) + " Hz");
            break;
          case MSG_SILENCE:
            mTextView02.setText("");
            break;
        }
        return true;
      }
    
      @Override
      public void run() {
        boolean bSilence = false;
        mHandler.sendEmptyMessage(MSG_RECORD_START);
        // 集音開始
        mAudioRecord.startRecording();
        while (mInRecording && !mStop) {
          mAudioRecord.read(mRecordBuf, 0, mBufferSizeInShort);
          bSilence = true;
          for (int i = 0; i < mBufferSizeInShort; i++) {
            short s = mRecordBuf[i];
            if (s > THRESHOLD_AMP) {
              bSilence = false;
            }
          }
          if (bSilence) { // 静寂
            mHandler.sendEmptyMessage(MSG_SILENCE);
            continue;
          }
          int freq = doFFT(mRecordBuf);
          Message msg = new Message();
          msg.what = MSG_FREQ_PEAK;
          msg.arg1 = freq;
          mHandler.sendMessage(msg);
        }
        // 集音終了
        mAudioRecord.stop();
        mHandler.sendEmptyMessage(MSG_RECORD_END);
      }
    
      private int doFFT(short[] data) {
        for (int i = 0; i < mFFTSize; i++) {
          mFFTBuffer[i] = (double)data[i];
        }
        // FFT 実行
        mFFT.realForward(mFFTBuffer);
    
        // 処理結果の複素数配列から各周波数成分の振幅値を求めピーク分の要素番号を得る
        double maxAmp = 0;
        int index = 0;
        for (int i = 0; i < mFFTSize/2; i++) {
          double a = mFFTBuffer[i*2]; // 実部
          double b = mFFTBuffer[i*2 + 1]; // 虚部
          // a+ib の絶対値 √ a^2 + b^2 = r が振幅値
          double r = Math.sqrt(a*a + b*b);
          if (r > maxAmp) {
            maxAmp = r;
            index = i;
          }
        }
        // 要素番号・サンプリングレート・FFT サイズからピーク周波数を求める
        return index * SAMPLE_RATE / mFFTSize;
      }
    }
    
    
  • AndroidManifest.xml
    
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="jp.klab.sonic04">
      <uses-permission android:name="android.permission.RECORD_AUDIO"/>
      <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
          android:name=".MainActivity"
          android:label="@string/app_name"
          android:configChanges="orientation|screenSize"
          android:screenOrientation="unspecified"
          android:theme="@style/AppTheme.NoActionBar">
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>
        <!-- ATTENTION: This was auto-generated to add Google Play services to your project for
           App Indexing.  See https://g.co/AppIndexing/AndroidStudio for more information. -->
        <meta-data
          android:name="com.google.android.gms.version"
          android:value="@integer/google_play_services_version" />
      </application>
    </manifest>
    
    

次回の予定

以上の内容により Android デバイスで「任意の周波数・振幅の音を出すこと」と「拾った音に含まれる周波数を検知すること」ができるようになりました。音を使ってデバイス間で通信を行うための素材がこれで揃ったことになります。次回はここまでの到達点を応用してビットデータの送受信を試みます。


(tanabe)
klab_gijutsu2 at 07:21│Comments(4)Android | IoT

この記事へのコメント

2. Posted by あばば   2017年11月29日 00:36
githubのファイルを使ってやってみましたが
code -20 when initializing native AudioRecord object.
このようなエラーがでます。どこを修正しましたか?出なかったのですか?
3. Posted by tanabe   2017年11月29日 10:16
開発当時は Android 5.x 環境を使用しており、Android 6.0 で導入された実行時パーミッション仕様の影響を想像しています。 https://developer.android.com/training/permissions/requesting.html
コード例として先ほど GitHub 上の sonic01 のみ手を加えました。上記ドキュメントを確認の上、参考になれば他のプロジェクトについても適宜試して下さい。
4. Posted by あばば   2017年11月29日 17:34
APIレベルを23から22に下げたら問題なく動作しました。
ありがとうございました。
db表示するプログラムの例も挙げて頂くことは可能ですか?
5. Posted by tanabe   2017年11月29日 18:00
手元には今のところそういうコードも作成予定もありません。ご自身でどうぞ。

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント