Androidでサイン波をならす

初日だけ前置き。諸事情で「HTC EVO WIMAX」を買いました。本当は2.3を買ってOpenSL ESをやってみたかったのですが、いかんせん諸事情です。ただ昔作ったAndroid音楽アプリやサイズ合わせたFLASH音楽アプリも思いのほか動くので結構楽しんでいます。さて当ブログがよく扱うPython(pysndobj)とかChuckとかnyquistに比べ若干タイプ量が多くなるので説明が上手にはいかないこともあるでしょう。僕が作ってつまずいたところとかポイントを絞って解説したいと思っています。バーンってコード貼るだけになるかもしれません。長くなりましたが僕はjavaに慣れていないためにコードにお見苦しいところが醸されるでしょうがご勘弁ください。動け重視でやっとります。何かあればコメントや@miura_offにつぶやきかけてください。

まず楽器づくりの前に画面を縦横決めて作ったほうが良いです。縦横変化したときに呼ばれる関数で楽器が重複したりします?。音が2重になったりする。ここらをうまくやる方法もあるとは思いますが(僕は知らないので)レイアウトもたいへんだし一方に決めてしまいます。AndroidManifest.xmlのactivityというタグ?(下の例だと9行目)にandroid:screenOrientation="portrait"と追記。これは縦方向に拘束します。あくまで今日は縦で。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="miu.jun.sinwave"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".TestSnd"
                  android:label="@string/app_name"
                  android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest> 

それでは楽器を作っていきましょう。サイン波440hzを鳴らすだけというアプリです。テルミンとかはそのうちやりましょう。GUIはボタン3つで再生(PLAY)停止(STOP)終了(QUIT)とします。早速バーンとコードを貼ります。

package miu.jun.TestSnd;

import android.app.Activity;
import android.os.Bundle;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class TestSnd extends Activity implements OnClickListener{

    private Button play_b;
    private Button stop_b;
    private Button quit_b;
    int samplerate = 44100;
    SinOsc sinosc;
    private AudioTrack track;
    short[] buf;
    int buffer;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        play_b = (Button)this.findViewById(R.id.button01);
        play_b.setOnClickListener(this);
        stop_b = (Button)this.findViewById(R.id.button02);
        stop_b.setOnClickListener(this);
        quit_b = (Button)this.findViewById(R.id.button03);
        quit_b.setOnClickListener(this);
        //バッファーサイズの取得
        buffer = AudioTrack.getMinBufferSize(
                     samplerate, 
                     AudioFormat.CHANNEL_CONFIGURATION_MONO, 
                     AudioFormat.ENCODING_PCM_16BIT);
        //AudioTrackの初期化
        track = new AudioTrack(AudioManager.STREAM_MUSIC,
                               //サンプリング定数
                               samplerate,
                               //モノラル
                               AudioFormat.CHANNEL_CONFIGURATION_MONO,
                               //16bit
                               AudioFormat.ENCODING_PCM_16BIT,
                               //バッファーサイズ
                               buffer,
                               //ストリームモード
                               AudioTrack.MODE_STREAM);

        buf = new short[buffer];
        //所得したバッファーサイズごとに通知させる。
        track.setPositionNotificationPeriod(buffer);
        track.setPlaybackPositionUpdateListener(
            new AudioTrack.OnPlaybackPositionUpdateListener() {
                public void onMarkerReached(AudioTrack track) {}
                //通知があるごとに実行される。
                public void onPeriodicNotification(AudioTrack track) {
        	    sndOut(buf,sinosc);
        	}
            }
        );
        sinosc = new SinOsc(400, 1);
    }
    
    public void onClick(View view) {
        if (view == play_b & track != null){
            if(track.getPlayState() == AudioTrack.PLAYSTATE_STOPPED) {
                //ボタンが押されたら再生する。
        	track.setStereoVolume(0, 0);
                //AudioTrack.playの後はgetMinBufferSizeで取得した
                //サイズより小さいとonPeriodicNotificationが実行されない。
                track.play();
        	sndOut(buf,sinosc);
        	track.setStereoVolume(1, 1);
            }
        } else if (view == stop_b& track != null){
            if(track.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
                //ボタンが押されたら停止
        	track.setStereoVolume(0, 0);
                track.stop();
            }
        } else if (view == quit_b){
        	if( track != null ){
                    //ボタンが押されたら終了
        	    track.setStereoVolume(0, 0);
        	    if(track.getPlayState()==AudioTrack.PLAYSTATE_PLAYING){
        	    	track.stop();
        	    }
        	    track.flush();
        	    track.release();
        	}
        	finish();
        }
    }
    //後々モジュレーション方式に移行するためスタイル
    class SinOsc {
        public double freq = 0;
    	public double amp = 0;
    	public double phase = 0;
    	public SinOsc(double f, double a){
    		freq = f;
                amp = a;
    	}
    	public double updata(){
    		phase += freq/samplerate;
    		phase = (phase > 1) ? 0 : phase;
    		return Math.sin(2*Math.PI*phase)*amp;
    	}
    }

    void sndOut(short data[],SinOsc input) {
        for (int i = 0; i < data.length; i++) {
            data[i] = (short)(Short.MAX_VALUE * input.updata());
        }
	track.write(buf,0,buf.length);
    }
}

以上で終わりなのですが個別に見ていきましょう。

以前音楽アプリを作ったときはAudioTrack.writeをスレッドをつくりループさせていたのですが、OnPlaybackPositionUpdateListenerをインプリメントし通知(setPositionNotificationPeriodで設定できる)がある毎にonPeriodicNotificationを実行することができるようです。以前からあったのかもしれませんが当時は知りませんでした。

また書き方としては別に以下のようにしてもいいわけです。このほうがわかりやすいかな。

public class TestSnd extends Activity implements OnPlaybackPositionUpdateListener{
    public void onCreate(Bundle savedInstanceState) {
        buffer = AudioTrack.getMinBufferSize();
        track = new AudioTrack();
        //通知の間隔
        track.setPositionNotificationPeriod(buffer); 
    }
     //今のところ必要ないが書かないといけない。
    public void onMarkerReached(AudioTrack track) {}
    public void onPeriodicNotification(AudioTrack track) {
        //通知がある毎に音声の処理をする。
    }
}

実際はこういう書き方をしています

track = new AudioTrack();
//通知の間隔
track.setPositionNotificationPeriod(buffer);
track.setPlaybackPositionUpdateListener(
    new AudioTrack.OnPlaybackPositionUpdateListener() {
        public void onMarkerReached(AudioTrack track) {}
        public void onPeriodicNotification(AudioTrack track) {
            //通知がある毎に音声の処理をする。
        }
    }
);

ここらへんは覚えましょう。それとAudioTrackの引数をざっとみるか。

track = new AudioTrack(AudioManager.STREAM_MUSIC,
                       //サンプリング定数
                       samplerate, 
                       //モノラル
                       AudioFormat.CHANNEL_CONFIGURATION_MONO,
                       // 16bit 
                       AudioFormat.ENCODING_PCM_16BIT,
                       //バッファーサイズ
                       buffer,
                       //ストリームモード
                       AudioTrack.MODE_STREAM); 

MODE_STREAMはテルミンのようにいつ再生が終わるかわからないときに設定するものだと思われる。ゲームの効果音とかは長さ決まっていますでしょう。違うモードでいいと思われます。

次が1番言いたかったことです。

buffer = AudioTrack.getMinBufferSize()
//ここで指定した間隔で以下のonPeriodicNotificationが実行
track.setPositionNotificationPeriod(buffer);
public void onPeriodicNotification(AudioTrack track) {
    sndOut(buf,sinosc);
}
public void onClick(View view) {
    //PLAY直後はbufferサイズを出力する
    track.play(); 
    sndOut(buf,sinosc);
}
void sndOut(short data[],SinOsc input) {
    track.write();
}

setPositionNotificationPeriodで設定できる数値を小さくしたいと考えるかもしれません。数値が小さいと遅延が小さいと考えるわけですから。ただ小さくしてもいいのですが最初にAudioTrack.playした後のAudio.writeはgetMinBufferSize()で取得したサイズに達しないとonPeriodicNotificationを実行する通知が起きないようです。Audio.writeで書きだす量を小さくしてしまって音がならないと書いてある方がありましたので参考にしてください。また最初に空のデータを大量に書き出している無駄なことをしているソースも見かけました。僕は親切じゃないのでここを見て直していただければと思います。ただ別の意図があるのかもしれませんが。

実際の楽器のプログラミング。

class SinOsc {
    //音程の変化
    public double freq = 0;
    //音量の変化
    public double amp = 0;
     //カウンタみたいなもの
    public double phase = 0;
    public SinOsc(double f, double a){
        freq = f;
        amp = a;
    }
    //ここでサイン波の生成をする
    public double updata(){
        phase += freq/samplerate;
        phase = (phase > 1) ? 0 : phase;
        return Math.sin(2*Math.PI*phase)*amp;
    }
}

まずSinOscというクラスがあります。周波数freq音量amp位相phaseというメンバ変数を持っています。位相は内部にあるカウンタのようなもので0から1まで数えて0に戻ります。updataの中を見ます。

phase += freq/samplerate;
phase = (phase > 1) ? 0 : phase;

例えばに周波数freqの値を高くすれば0から1をカウントするのが早まります。急いでる感じがしますよね。いい加減ではありますが雰囲気はつかめるでしょう。

return Math.sin(2*Math.PI*phase)*amp

ですから位相phaseに2πをかけてMath.sin関数の中に叩き込むわけです。Math.sinはfreqによるphaseで波の間隔を、振幅(音量)ampの値で波の高さを変えます。

実際に音を出力する部分は

//音を吐き出す。
void sndOut(short data[],SinOsc input) {
    for (int i = 0; i < data.length; i++) {
        //AudioTrack.writeはshortかbyteです。
        data[i] = (short)(Short.MAX_VALUE * input.updata());
    }
    track.write(buf,0,buf.length);
}

SinOscが第2引数にあります。これがupdataを呼ばれてサイン波の値を返します。ですからinputを矩形波にする場合とかはSqrOsc inputとかにして引数に入力します。ここらへんは次回でもっとうまくやります。

最後にGUIです。ひな形として

public class TestSnd extends Activity implements OnClickListener{
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        //ボタン。レイアウトはres/layout/main.xml
        play_b = (Button)this.findViewById(R.id.button01); 
        play_b.setOnClickListener(this);
    }
    public void onClick(View view) {
        //ボタンが押された処理を書く
    }

レイアウトは直接書きこむこともできますが、res/layout/main.xmlに書きましょう。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="@string/hello"/>
<Button android:id="@+id/button01"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="play" />
<Button android:id="@+id/button02"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="stop" />
<Button android:id="@+id/button03"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="quit" />
</LinearLayout>

さて長くなりました次回以降はスピーディに行きます。