Androidでサイン波をなめらかに音量変化

昨日の楽器にひそむ不安を解消します。
AudioTrack.playとAudioTrack.stopを実行した際におこるバジッとなるグリッチ音です。意図的にAudioTrack.playとAudioTrack.stopを繰り返してグリッチ音楽をやるのも素敵かもしれませんが、今日はそのグリッチ音退治です。

昨日の楽器においてAudioTrack.stopの前にAudioTrack.setStereoVolume(0, 0)を実行することでグリッチ音は出ていません。

if(track.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
    //ボタンが押されたら停止
    track.setStereoVolume(0, 0);
    track.stop();
} 

ただ色々頑張ったのですがAudioTrack.playの方は防げていません。

解決策としてAudioTrack.playとAudioTrack.stopのときは無音で始めることが重要であると考えます。つまりAudioTrack.writeで書きこむshort型の配列は[0,0,0,0,,,]になっている必要があるということです。さらにAudioTrack.playは楽器の初期化で呼ばれ、Audio.stopは楽器終了の際にしか呼ばないようにします。前回の楽器を一部書き換えます。

@Override
public void onCreate(Bundle savedInstanceState) {
    //中略
    buffer = AudioTrack.getMinBufferSize();
    track = new AudioTrack();
    buf = new short[buffer];
    track.setPositionNotificationPeriod(buffer);
    track.setPlaybackPositionUpdateListener();
    //第二引数を0にしました。音量はゼロです。
    sinosc = new SinOsc(400, 0); 
    //楽器の初期化の時だけAudioTrack.playを実行
    track.play();
    sndOut(buf,sinosc);
}
public void onClick(View view) {
    if (view == play_b){
        //ここになめらかに音量変化させる何かを書く
    } else if (view == stop_b){
        //ここになめらかに音量変化させる何かを書く
    } 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();
    }
}

見てもらいたいのはSinOscの音量はゼロになっていることです。

sinosc = new SinOsc(400, 0); 

これならグリッチ音がなるはずありませんが音もなりませんので、PLAYボタンが押されたときにサイン波の音量(振幅)が0になっているので1までなめらか上昇させる何かの処理を書くのが今日のポイントです。

一般的な音楽プログラミング言語にもなめらかに音量を上昇下降させるオブジェクトがあります。max/mapなどのline~オブジェクトなのです。僕はpysndobjユーザーということもありましてそれに習ってデザインしてみます。

//最初の値0から目的の値1まで0.02秒後に到達させる。
Line line = new Line(0, 1, 0.02); 
SinOsc sinosc = new SinOsc(440, line);

STOPボタンが押されたらどのようになるか

public void onClick(View view) {
    //中略
    } else if (view == stop_b){
        //音量1から音量0までリセットする
        line.reset(1,0);
    }
    //中略
}

line.resetで音量が1から0に時間0.02秒かけて移動します。

さて実際にLineオブジェクトを作ってみましょう。

class Line{
    //SndObj Interp.cpp 参考
    double start_value;
    double end_value;
    double dur_time;
    double sample_rate = 44100;
    double sample_length;
    double count;
    public Line(double s, double e, double t){
        start_value = s;
    	end_value = e;
    	dur_time = t;
    	count = 0;
    	sample_length = dur_time*sample_rate;
    }
    public void reset(double s, double e){
        start_value = s;
    	end_value = e;
    	count = 0;
    }
    public double updata(){
        if(sample_length > count){
    	    count = count + 1;
    	}else{
    	    count =  sample_length;
    	}
    	return start_value + (end_value - start_value)*(count / sample_length);
    }
}

サンプリング周波数sample_rateに時間dur_timeを掛けてサンプルの長さsample_lengthを導き出し、countがsample_lengthとおなじになった時に目的の音量に到達するような仕組みです。じっくり読みましょうコードを

ここでSinOscも改良しなければならなくなりました。しかしこう新しいオブジェットを作るたびに改良しなければならないのは大変です。ここらへんの問題は明日以降にします。ではSinOscを改良します。

class SinOsc {
    double sample_rate = 44100;
    public double freq = 0;
    public Line amp;
    public double phase = 0;
    public SinOsc(double f, Line a){
        freq = f;
        amp = a;
    }
    //ここでサイン波の生成をする
    public double updata(){
        phase += freq/sample_rate;
        phase = (phase > 1) ? 0 : phase;
        return Math.sin(2*Math.PI*phase)*amp.updata();
    }
}

第二引数の型がdoubleからLineに変更です。

大変小さいことですが、音量変化が音の個性といった人がいました。
複雑な音量変化に導くための基本になります。
では最後は修正したコードをドバっと貼ります。

package miu.jun.TestSnd;

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

public class TestSnd extends Activity implements OnClickListener{
    private Button play_b;
    private Button stop_b;
    private Button quit_b;
    Line line;
    SinOsc sinosc;
    private AudioTrack track;
    short[] buf;
    int bf;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        play_b = (Button)this.findViewById(R.id.button1);
        play_b.setOnClickListener(this);
        stop_b = (Button)this.findViewById(R.id.button2);
        stop_b.setOnClickListener(this);
        quit_b = (Button)this.findViewById(R.id.button3);
        quit_b.setOnClickListener(this);
        
        bf = AudioTrack.getMinBufferSize(44100, AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT);
        track = new AudioTrack(AudioManager.STREAM_MUSIC, 
                               44100, 
                               AudioFormat.CHANNEL_CONFIGURATION_MONO,
                               AudioFormat.ENCODING_PCM_16BIT,
                               bf,
                               AudioTrack.MODE_STREAM);

        buf = new short[bf];
        track.setPositionNotificationPeriod(bf);
        track.setPlaybackPositionUpdateListener(
            new AudioTrack.OnPlaybackPositionUpdateListener() {
        	    public void onMarkerReached(AudioTrack track) {}
        		public void onPeriodicNotification(AudioTrack track) {
        		    sndOut(buf,sinosc);
        		    track.write(buf,0,buf.length);
        		}
        	}
        );
        //緩やかな音量変化でグリッチを防ぐ。
        line = new Line(0, 0, 0.02);
        sinosc = new SinOsc(440, line);
        track.play();
    	sndOut(buf,sinosc);
    	track.write(buf,0,buf.length);        
    }
    public void onClick(View view) {
    	if (view == play_b){
            //音量を0から1へ
            line.reset(0, 1);
        }
    	else if (view == stop_b){
            //音量を1から0へ
            line.reset(1, 0);
        }
        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 sampleRate = 44100;
        public double freq;
    	public Line amp;
    	public double phase = 0;
        //第2引数に注意
    	public SinOsc(double f, Line a){
    		freq = f;
    		amp = a;
    	}
    	public double updata(){
    		phase += freq/sampleRate;
    		phase = (phase > 1) ? 0 : phase;
    		return Math.sin(2*Math.PI*phase)*amp.updata();
    	}
    }
    class Line{
    	double start_value;
    	double end_value;
    	double dur_time;
        public double sampleRate = 44100;
    	double sample_length;
    	double count;
    	public Line(double s, double e, double t){
    		start_value = s;
    		end_value = e;
    		dur_time = t;
    		count = 0;
    		sample_length = dur_time*sampleRate;
    	}
    	public void reset(double s, double e){
    		start_value = s;
    		end_value = e;
    		count = 0;
    	}
    	public double updata(){
    		if(sample_length > count){
    		    count = count + 1;
    		}else{
    			count =  sample_length;
    		}
    		return start_value + (end_value - start_value)*(count / sample_length);
    	}
    }

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