2017/07/26

SuperCollider - ウェーブテーブルの作成

Signal を使って Osc などのUGenで使うウェーブテーブルを作ります。

sineFill

sineFill で加算合成ができます。

以下の例ではランダムに生成したArrayを降順にソートしています。これによって低い倍音ほど音量が大きくなり、音程がわかりやすくなります。

(
var size = 1024;
var harmonics = 128;
var sig = Signal.sineFill(
  size,
  {exprand(0.00001, 1.0)}.dup(harmonics).sort.reverse,
  {100pi.rand}.dup(harmonics).sort);
sig.plot;
sig.play(loop: true, numChannels: 2)
);

Window.closeAll; // ウィンドウをまとめて閉じる。

chebyFill

chebyFillチェビシェフ多項式を使った合成ができます。

得られるウェーブテーブルは Shaper での利用に向いているようです。 Shaper で使うときは引数に zeroOffset: true を渡すことが推奨されています。

(
var size = 1024;
var ampSize = 32;
var sig = Signal.chebyFill(
  size,
  {exprand(0.00001, 1.0)}.dup(ampSize).sort.reverse,
  zeroOffset: true);

sig.plot;

{
  var buffer = Buffer.loadCollection(s, sig);
  var mouseX = 1 - MouseX.kr(1, 0.00001, 1);
  var osc = SinOsc.ar(60, mul: mouseX);
  Pan2.ar(Shaper.ar(buffer, osc, 0.2))
}.play;
);

以下は chebyFill を点対称な波形に変形する例です。

(
var size = 1024;
var ampSize = 32;
var sig = Signal.chebyFill(
  size / 2,
  {exprand(0.00001, 1.0)}.dup(ampSize).sort.reverse);
var zero = sig[0];
var sigPositive = Signal.newFrom(sig) - zero;
var sigNegative = sig.invert.reverse + zero;
sig = (sigNegative ++ sigPositive).normalize;

sig.plot;

{
  var buffer = Buffer.loadCollection(s, sig);
  var mouseX = 1 - MouseX.kr(1, 0.00001, 1);
  var osc = SinOsc.ar(60, mul: mouseX);
  Pan2.ar(Shaper.ar(buffer, osc, 0.2))
}.play;
);

fft, ifft

FFTを使って周波数領域での編集を行います。 Signal.fftSignal.ifft は引数に Signal.fftCosTable が必要となる点に注意してください。

のこぎり波を作ります。

( // FFT saw.
~addSaw = { |array, step|
  var sign = 1000;
  forBy(1, array.size / 2 - 1, 1, { |index|
    array[index] = array[index] + (sign / index);
    sign = sign.neg;
  });
  array
};

~dckill = { |complex|
  complex.real[0] = 0;
  complex.imag[0] = 0;
  complex
};

~saw = { |size(1024), noiseGain(0.001)|
  var signal, cosTable;
  cosTable = Signal.fftCosTable(size);

  signal = Signal.fill(size, {noiseGain.rand}); // ノイズ。
  signal = ~addSaw.value(signal);
  signal[0] = 0; // 直流を除去。
  signal = Signal.newClear(size).ifft(signal, cosTable);

  signal = signal.real.rotate(size / 2); // 位相を進める。
  signal.normalize
};

~wave = ~saw.value(noiseGain: 1.0);
~wave.plot;
~wave.play(true, numChannels: 2);
);

以下の例では、時間領域で作ったのこぎり波の位相を周波数領域でランダマイズして、矩形波を重ねています。

(
~addSquare = { |array|
  var sign = 1000/0.75;
  forBy(1, array.size / 2 - 1, 2, { |index|
    array[index] = array[index] + (sign / index);
    sign = sign.neg;
  });
  array
};

~randPhase = { |complex|
  var polar = complex.asPolar;
  polar.theta = polar.theta.collect{360.0.rand};
  polar.asComplex
};

~dckill = { |complex|
  complex.real[0] = 0;
  complex.imag[0] = 0;
  complex
};

~sawsquare = { |size(1024), noiseGain(0.001)|
  var signal, cosTable;
  signal = Signal.fill(size, { |index|
    2 * index / size -1 + noiseGain.rand}); // のこぎり波とノイズ。
  cosTable = Signal.fftCosTable(size);

  signal = signal.fft(Signal.newClear(size), cosTable);
  signal = ~randPhase.value(signal);
  signal.real = ~addSquare.value(signal.real);
  signal = ~dckill.value(signal);
  signal = signal.real.ifft(signal.imag, cosTable);
  signal.real.normalize
};

~wave = ~sawsquare.value;
~wave.plot;
~wave.play(true, numChannels: 2);
);

ファイルに保存

一度 Buffer にしてから保存します。

(
~sineFill = { |size(1024), harmonics(128)|
  Signal.sineFill(
    size,
    {exprand(0.00001, 1.0)}.dup(harmonics).sort.reverse,
    {100pi.rand}.dup(harmonics).sort);
}:

~dir = Platform.recordingsDir +/+ "wavetable";
~dir.mkdir; // String.mkdir

Buffer
.loadCollection(s, ~sineFill.value)
.write(~dir +/+ "wavetable.wav", "WAV", "float");
);