Androidで周波数変調

サイン波の周波数をサイン波でコントロールします。
今日はテルミンにするつもりでしたが、SeekBarを使いたくてこの題材を選びました。
だいたいのデザインは

SinOsc modosc = new SinOsc(2,20);
Line line = new Line(0, 1, 0.02);
SinOsc sinosc = new SinOsc(440, 0, modosc, line)

ちなみにLineは緩やかに音量変化させるものでしたね。2つのSinOscオブジェクトがありますが、実際に音出すほうがキャリア、コントロールするほうがモジュラーと言います。みたままSinOscがコンストラクタ2種類あります。すでにサイン波のオブジェクトは作りましたので改良し、これに対応させます。まず第1引数の周波数と第2引数の振幅はdoubleにして第3引数と第4引数はオブジェクトにします。

class SinOsc{
    public double sampleRate = 44100;
    public double freq;
    public double amp;
    //周波数freqに2つめの周波数addfreqを
    public SinOsc addfreq;
    public Line addamp;
    public double phase = 0;
    //第3引数にSinOscが入力できるようにする。
    public SinOsc(double f, double a, SinOsc, addf, Line adda){
    	freq = f;
    	amp = a;
        addfreq = addf;
        addamp = adda;
    }
    public double updata(){
        //addfreqの値によってfreqが増減し結果phaseが変化
    	phase += (freq + addfreq.updata())/sampleRate;
    	phase = (phase > 1) ? 0 : phase;
    	return Math.sin(2*Math.PI*phase)*(amp+addamp.updata());
    }
}

ソースのコメントのように今までの周波数freqにSinOscの出力が加わることでダイナミックなサウンドが期待されます。音量もLineじゃなくSinOscに書き換えれば、さらに過激な音が期待されますね。
ここで2種類のコンストラクタを用意しなければなりません。空のクラスをつくって、すべてのサウンドオブジェクトが継承するようにしてみます。入力の時にSinOscだとかLineだとかわずらわしいのでSndで統一します。

class Snd{
  public double sampleRate = 44100;
    public Snd(){}
    public double updata(){
    	return 0;
    }
}
class SinOsc extends Snd{
    public double freq;
    public double amp;
    //周波数freqに2つめの周波数addfreqを
    public Snd addfreq = new Snd();
    public Snd addamp = new Snd();
    public double phase = 0;
    public SinOsc(double f, double a){
    	freq = f;
    	amp = a;
    }
    //第3引数にSinOscが入力できるようにする。
    public SinOsc(double f, double a, Snd, addf, Snd adda){
    	freq = f;
    	amp = a;
        addfreq = addf;
        addamp = adda;
    }
    public double updata(){
        //addfreqの値によってfreqが増減し結果phaseが変化
    	phase += (freq + addfreq.updata())/sampleRate;
    	phase = (phase > 1) ? 0 : phase;
    	return Math.sin(2*Math.PI*phase)*(amp+addamp.updata());
    }
}

いろいろ言われそうです。これで行きますね。当然LineもSndを継承します。最後のソースで確認ください。

次はGUIのSeekBarです。ActivityにView.OnSeekBarChangeListenerをインプリメントして

@Override
public void onCreate(Bundle savedInstanceState) {
//中略
    fm_bar = (SeekBar)this.findViewById(R.id.SeekBar01);
    //最大値、最小値の設定はないみたい。
    fm_bar.setMax(1000);
    //現在の値  
    fm_bar.setProgress(440);
    fm_bar.setOnSeekBarChangeListener(this);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
    //値が動けば周波数が変わる
    if(seekBar == fm_bar){sinosc.freq = progress;}
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}

特にメモ程度です。

ソースの前にひとつ

bf = AudioTrack.getMinBufferSize();
track = new AudioTrack()
buf = new short[1024];
int ii = (int)bf/1024;
track.setPositionNotificationPeriod(1024);
track.setPlaybackPositionUpdateListener(
new AudioTrack.OnPlaybackPositionUpdateListener() {
    public void onMarkerReached(AudioTrack track) {}
        public void onPeriodicNotification(AudioTrack track) {
            sndOut(buf,sinosc);
            track.write(buf,0,buf.length);
        }
    }
);
track.play();
for(int i=0; i<ii; i++){
    sndOut(buf,sinosc);
    track.write(buf,0,buf.length); 
}

何度か実験した結果setPositionNotificationPeriodの値はなるべく小さいほうが音が安定します。ただ以前に話したAudioTrack.play後はgetMinBufferSize分を書き込まないと通知がこないということはお忘れなく。

それではまとめです。

package com.example;

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;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;

public class Imusic extends Activity implements OnClickListener, OnSeekBarChangeListener{

    private Button play_b;
    private Button stop_b;
    private Button quit_b;
    private SeekBar sb;
    Line line;
    SinOsc modosc;
    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);

        sb = (SeekBar)this.findViewById(R.id.SeekBar01);
        sb.setMax(1000);
        sb.setProgress(440);
        sb.setOnSeekBarChangeListener(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[1024];
        int ii = (int)bf/1024;
        track.setPositionNotificationPeriod(1024);
        track.setPlaybackPositionUpdateListener(
            new AudioTrack.OnPlaybackPositionUpdateListener() {
        	    public void onMarkerReached(AudioTrack track) {}
        		public void onPeriodicNotification(AudioTrack track) {
        		    sndOut(buf,sinosc);
        		    track.write(buf,0,buf.length);
        		}
        	}
        );

        modosc = new SinOsc(2, 20);
        line = new Line(0, 0, 0.02);
        sinosc = new SinOsc(440, 0, modosc, line);
        track.play();
        for(int i=0; i<ii; i++){
    	    sndOut(buf,sinosc);
    	    track.write(buf,0,buf.length); 
        }
    }

    public void onClick(View view) {
    	if (view == play_b){
            line.reset(0, 1);
        }
    	else if (view == stop_b){
            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 Snd{
    	public double sampleRate = 44100;
    	public Snd(){}
    	public double updata(){
    		return 0;
    	}
    }
    class SinOsc extends Snd{
        public double freq;
        public double amp;
        //周波数freqに2つめの周波数addfreqを
        public Snd addfreq = new Snd();
        public Snd addamp = new Snd();
        public double phase = 0;
        public SinOsc(double f, double a){
    	    freq = f;
    	    amp = a;
         }
        //第3引数にSinOscが入力できるようにする。
        public SinOsc(double f, double a, Snd addf, Snd adda){
    	    freq = f;
    	    amp = a;
            addfreq = addf;
            addamp = adda;
        }
        public double updata(){
            //addfreqの値によってfreqが増減し結果phaseが変化
    	    phase += (freq + addfreq.updata())/sampleRate;
    	    phase = (phase > 1) ? 0 : phase;
    	    return Math.sin(2*Math.PI*phase)*(amp+addamp.updata());
        }
    }
    //LineもSndを継承します。
    class Line extends Snd{
    	double start_value;
    	double end_value;
    	double current_value;
    	double dur_time;
    	int sample_length;
    	int count;
    	public Line(double s, double e, double t){
    		super();
    		start_value = s;
    		end_value = e;
    		dur_time = t;
    		count = 0;
    		sample_length = (int)(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;
    		}
    		current_value = start_value + (end_value - start_value)*(count / sample_length);
    		return current_value;
    	}
    }

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

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {}
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
        //サインはの周波数を変えます。
    	if(seekBar == sb){
            sinosc.freq = progress;
    	}
    }
    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {}
}

以上です。キャリアーの周波数だけを変えましたがSeekBarを増やしてモジュラーの周波数などを変えてみてください。
ビデオとかで録画したほうがいいかな、うん、どうしよう。