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を増やしてモジュラーの周波数などを変えてみてください。
ビデオとかで録画したほうがいいかな、うん、どうしよう。