前回の続きです。
バッファの切れ目でノイズが発生するのはなぜでしょう?
バッファに波形データを書き込んだ際に、バッファの終端が「波形の途中」で終わってしまうことがあります。バッファ長が波長の整数倍であることは普通期待できませんから、「波形の途中」で終わってしまうことは回避できません。その状態から、次回のバッファで「最初から」波形を書き出してしまうと、波形が不連続になってしまいます。これがノイズの原因です。
さて、これを解決するには2通りのアプローチがあります。
- 前回のバッファ終端での「位相」を記憶し、次のバッファではその位相から波形を書き始める
- 前回のバッファ書き込みの「はみ出し分」を次のバッファ書き込みの先頭にくっつける
オシレータ系の信号処理であれば、どちらの方法を採用しても結果は同じになりますが、後者のほうが、位相を意識しないオーディオ信号のバッファリングにも適用できるので、こちらを実装しました。バッファ長を超えた地点から、信号が「マイナスから0を超える」地点(ゼロ・クロッシング)までを検索して切り出し、「はみ出し分」とします。
これのロジックをaudioStreamWriteメソッドに実装すると煩雑になるので、BufferSupportというクラスとしてくくり出し、再利用できるようにました。使い方は
- グローバル変数としてBufferSupportを持つ
- setup時に new BufferSupport(BUFFSIZE, BufferSupport.ZERO_CROSSING) する
- audioStreamWriteでは直接streamに書き込まず、BufferSupportのworkBufferに書き込む
- 最後にprepareBufferメソッドを呼ぶ
です。
このBufferSupportクラスを使えばその他のプログラムにも適用できます。自由にコピペしてくださいませ。
おまけで「クロスフェード」モードも用意しています。こちらは「はみ出し分」と「次のバッファ」を重ねて混ぜ合わせます。new BufferSupport(BUFFSIZE, BufferSupport.ZERO_CROSSING) の部分を new BufferSupport(BUFFSIZE, BufferSupport.CROSSFADE) に書き換えればOKです。ZERO_CROSSINGの音に納得いかない場合はこちらも試してみてください。
http://toshiyakobayashi.googlepages.com/LFOBufferSupport.html
import krister.Ess.*; float SAMPLE_RATE = 44100; long currentFrame = 0; BufferSupport support; AudioStream myStream; // Audio stream to write into LFO lfo; int offset = 0; int BUFFSIZE = 10000; void setup() { size(640, 480); Ess.start(this); // Start Ess myStream = new AudioStream(BUFFSIZE); // Create a new AudioStream myStream.smoothPan = true; myStream.pan = -1.0f; lfo = new LFO(1f, 200); support = new BufferSupport(BUFFSIZE, BufferSupport.ZERO_CROSSING); myStream.start(); // Start audio noLoop(); } void draw() { } void mouseMoved() { } void audioStreamWrite(AudioStream s) { background(255); // バッファの切れ目の波形を描画 int lx = 0, ly = height / 2; for (int i = 0; i < width / 2; i++) { int x, y; x = i; y = (int)((s.buffer[s.buffer.length - (width / 2) + i] * -1 + 1.0f) / 2 * height); line(lx, ly, x, y); lx = x; ly = y; } float last2 = s.buffer[s.buffer.length-2]; float last1 = s.buffer[s.buffer.length-1]; // process !! process(s); for (int i = 0; i < (width / 2); i++) { int x, y; x = i + (width / 2); y = (int)((s.buffer[i] * -1 + 1.0f) / 2 * height); line(lx, ly, x, y); lx = x; ly = y; } redraw(); } void process(AudioStream s) { // mouseXで周波数、mouseYで振幅を操作する lfo.fq = (float)mouseX / width * 10; lfo.amp = (float)(height - mouseY) / height * 200; // 1波形ずつ生成 // s.bufferではなく、support.workBufferに対して波形データを書き込む for (int offset = 0; offset < support.workBuffer.length; ) { float fq = lfo.getValue(currentFrame + offset) + 440; int genFrames = (int)((1 / fq) * SAMPLE_RATE); SineWave wave = new SineWave(fq, 1f); wave.generate(support.workBuffer, SAMPLE_RATE, offset, genFrames); offset += genFrames; } support.prepareBuffer(s.buffer); currentFrame += s.buffer.length; } class LFO { float fq; float amp; LFO(float fq, float amp) { this.fq = fq; this.amp = amp; } float getValue(long frame) { float phaseRad = getRadianByFrame(frame, fq); return (float)Math.sin(phaseRad) * amp; } float getRadianByFrame(long frame, float fq) { int priodFrames = (int)((1F / fq) * SAMPLE_RATE); // 1周期のframe数 int phaseFrame = (int)(frame % priodFrames); // 位相をframe数で return (float)((phaseFrame / (float)priodFrames) * 2 * Math.PI); // 位相をラジアンで } } class BufferSupport { static final int ZERO_CROSSING = 0; static final int CROSSFADE = 1; int mode; static final int DEFAULT_EXTRA_LENGTH = 2000; float[] extraBuffer; // 「はみだし分」を保持する float[] workBuffer; // このバッファに波形データを作成する int extraLength = DEFAULT_EXTRA_LENGTH; BufferSupport(int length, int mode) { this.mode = mode; extraBuffer = new float[extraLength]; workBuffer = new float[length + extraLength]; } BufferSupport(int length, int mode, int crossfadeLength) { this.mode = mode; this.extraLength = crossfadeLength; extraBuffer = new float[crossfadeLength]; workBuffer = new float[length + crossfadeLength]; } // 最後にこのメソッドを呼べば、streamBufferを適切に整えてくれます void prepareBuffer(float[] streamBuffer) { if (mode == ZERO_CROSSING) { int crossPoint = 0; for (int i = 1; i < extraLength; i++) { if (extraBuffer[i-1] < 0 && extraBuffer[i] >= 0) { crossPoint = i; } } System.arraycopy(extraBuffer, 0, streamBuffer, 0, crossPoint); System.arraycopy(workBuffer, 0, streamBuffer, crossPoint, streamBuffer.length - crossPoint); System.arraycopy(workBuffer, streamBuffer.length - crossPoint, extraBuffer, 0, extraLength); } else if (mode == CROSSFADE) { FadeOut fadeOut = new FadeOut(Ess.SLOW); fadeOut.filter(extraBuffer, SAMPLE_RATE); FadeIn fadeIn = new FadeIn(Ess.SLOW); fadeIn.filter(workBuffer, SAMPLE_RATE, 0, extraLength); for (int i = 0; i < extraLength; i++) { workBuffer[i] += extraBuffer[i]; } System.arraycopy(workBuffer, 0, streamBuffer, 0, streamBuffer.length); System.arraycopy(workBuffer, streamBuffer.length, extraBuffer, 0, extraLength); } } }
(※8/8 ちょっとcrossPointのところ変だったので修正)