2018/02/17

WaveCymbal

デモを見る (github.io)

WaveCymbal は Banded Waveguides を利用したシンセサイザです。シンバルというよりは空き缶のような音がでます。

仕組み


下図はWaveCymbal の大まかな信号の流れです。



まずインパルスをコムフィルタに通して衝突音を作ります。これが Input になります。

Input は 1D Wave へと入力されます。1D Wave では1次元の波のシミュレーションを行っています。

1D Wave の複数の地点からの出力を、それぞれバンドパスフィルタに通して並列に並べた Karplus-Strong へと送ります。Karplus-Strong からの出力はそれぞれ対応する 1D Wave の地点へとフィードバックされます。

並列に並べた Karplus-Strong からの出力を加算したものが Output になります。

問題点


1D Wave のシミュレーションが適当なので、システムのサンプリング周波数によってピッチが変わる問題があります。

2018/02/02

KSCymbal


デモを見る (github.io)

Karplus-Strongアルゴリズムを応用したシンバルの音を合成するシンセサイザです。

閉じたハイハットを叩くと上下のシンバルが複雑にぶつかりあいます。KSCymbalではこのぶつかる部分をシミュレーションに組み込みました。ただ、あくまでもKarplus-Strongなのでシンバルというよりギターの弦をたくさん並べてぶつけあっているというイメージの方が近いです。

デモのDistanceがシンバル間の距離を表しています。この値を小さくするとぶつかる回数が多くなります。

発振する場合はDistanceの値を上げるかSeedを変更してみてください。

Pluck


デモを見る (github.io)

Karplus-Strongアルゴリズムを用いたシンセサイザです。

Karplus-Strongアルゴリズムはギターなどの撥弦楽器のような音が出せます。デモページのCutoffの値を1にすると素朴なKarplus-Strongの音になります。

フィルタのかけ方はmrahtzさんの実装を参考にしました。

2017/11/26

PADcymbal


デモを見る (github.io)

PADcymbalはPADsynthを利用したシンバルのような音を合成するシンセサイザです。

仕組みとしてはランダムに作った周波数と音量の組をPADsynthに入力しているだけです。その他、製作の過程で調べたことをライドシンバル合成の試みにまとめています。

2017/11/23

PADsynthでシンバルの合成

ZynAddSubFXのDrums -> Natural Drum KitのハイハットがSUBsynthで合成されているのを見てシンバルを加算合成できる気がしたので、いろいろ試したらPADsynthで以下のリンクのような音が簡単に作れることがわかりました。

PADsynthによるシンバル (Freesound)

始めは周波数成分を手作業でPADsynthに入力するつもりでしたが、無理そうだったのでKernel Density Estimationという手法を応用してある程度ランダムに合成することにしました。

作ったプログラムにFreesoundにあったシンバルのPackをいくつか投げてみたところ思ったよりもいい結果が出たのですが、シンバル以外のPackも試したところ何を入れても似たような音が出てくることが分かりました。

何を入れても同じならランダムに作ったデータでもいいんじゃないかと試したところ冒頭のような音が合成できました。ただし音量エンベロープはAudacityで後付けしています。

以下のPython3のプログラムで音量エンベロープをかけていない音が合成できます。

padsynth.py (GitHub)

合成についての細かい話を以下にまとめています。

ライドシンバル合成の試み (GitHub)

2017/09/10

Pulseverb


デモを見る (github.io)

PulseverbはBLITを利用したリバーブのインパルス応答をレンダリングします。

手軽なわりにそれなりの音がします。

2017/09/08

Freeverb


デモを見る (github.io)

Freeverbのインパルス応答を書き出すレンダラを作りました。

Freeverbはリバーブの実装の一つで金属的な音が出ます(1, 2)。デモページのAllpassの値を大きくすると金属的な部分を強調して遊ぶことができます。パラメータによってはレンダリングに時間がかかるので注意してください。

2017/08/20

Ardourクイックスタート

Ardourクイックスタート

2017/08/17

FaustからLV2プラグインにコンパイル

初めてのFaustで作ったsaw.dspをLV2プラグインにコンパイルしてCarlaで呼び出します。

MIDIノート対応とメタデータの追加

MIDIノートへの対応とメタデータの追加を行うためsaw.dspを修正します。
メタデータの追加は declare で行います。 declare name の指定がなければコンパイル後のプラグイン名はファイル名と同じsawとなります。
declare nvoice で最大同時発音数を指定することでMIDIノートを受け取れるようになります。nvoiceの指定がなければエフェクトとしてコンパイルされます。
freq、gain、gateという名前のコントロールを定義することでMIDIノートの音程、ベロシティ、ノートオン/ノートオフをFaust側で扱えるようになります。以下の例の freqとgainの値はGenerating a MIDI Synthesizer for PDの例の値に合わせています。
// メタデータ。
declare name "Saw Synth";
declare author "Uhhyou";
declare version "1.0.0";
declare license "MIT";
declare description "A simple sawtooth synthesizer.";

// declare nvoicesがあればinstrument、なければエフェクトとしてコンパイルされる。
declare nvoices "16";

import("stdfaust.lib");

// freq, gain, gateを定義してMIDIノートを扱う。
amp = hslider("amp", 0.5, 0.0, 1.0, 0.01);
pan = hslider("pan", 0.5, 0.0, 1.0, 0.01);
freq = hslider("freq", 440, 20, 20000, 1);
gain = hslider("gain", 0.1, 0, 1, 0.01);
gate = button("gate");

ampAttack = hslider("amp attack", 0.7, 0.0, 1.0, 0.001);
ampRelease = hslider("amp release", 0.8, 0.0, 1.0, 0.001);

cutoff = hslider("cutoff", 0.5, 0.0, 1.0, 0.01) : ba.lin2LogGain * 980 + 20;
resonance = hslider("resonance", 0.2, 0.01, 1.0, 0.01) : ba.lin2LogGain * 100;
filterAttack = hslider("filter attack", 0.25, 0.0, 1.0, 0.001);
filterRelease = hslider("filter release", 0.9, 0.0, 1.0, 0.001);
filterEnvAmount = hslider("filter envelope amount", 0.7, 0.0, 1.0, 0.01)
  : ba.lin2LogGain * 980 + 20;

filterEnv = en.ar(filterAttack, filterRelease, gate);
filter = fi.resonlp(
  cutoff + filterEnvAmount * filterEnv,
  resonance,
  1);

ampEnv = en.ar(ampAttack, ampRelease, gate);
osc(f) = os.sawtooth(f)
  + os.sawtooth(f * (1.0 + 0.1 * no.pink_noise));
chord(numHarmo) = sum(i, numHarmo, osc((i + no.noise) * freq / numHarmo))
  / numHarmo;
process = chord(7) * amp * gain : filter : sp.panner(pan);

LV2プラグインへのコンパイルとインストール

LV2プラグインのインストールディレクトリは$LV2_PATHで指定されています。
$ echo $LV2_PATH
/home/<user_name>/.lv2:/usr/lib/lv2:/usr/local/lib/lv2
faust2lv2でLV2プラグインにコンパイルして~/.lv2にインストールします。
$ faust2lv2 saw.dsp
$ cp -r saw.lv2/ ~/.lv2/
lv2lsでインストールが成功しているか確認します。
$ lv2ls
...
https://faustlv2.bitbucket.io/saw
...

動作確認

以前インストールしたCarlaで動作確認を行います。
Carlaを起動してCtrl+Aでプラグイン追加ウィンドウが開きます。今回作ったプラグインはSaw Synthという名前になっているので探して追加します。次にPatchbayを開いてSaw Synthのoutをsystemのplaybackに接続します。接続後にSaw Synthのevents-inを選択した状態で、Patchbayの画面下にある鍵盤を左クリックして音がなれば成功です。Saw Synthのパラメータの変更はRackから行うことができます。

2017/07/27

SuperColliderでZynAddSubFXのPADsynth

SuperColliderでZynAddSubFXPADsynth algorithmを実装します。細かい部分についてはPADsynth algorithmのページにあるC/C++でのリファレンス実装が参考になりました。

オリジナルもそうですが、レンダリングに時間がかかります。

(
// 正規分布のharmonic profile。
~profile = { |fi, bwi|
  var x = fi / bwi;
  exp(x.neg * x) / bwi
};

// デフォルト値はリファレンス実装のc_basicと同じ。
~padTable = {
  | server(s), size(2**18), f0(261.0), bw(40.0), number_harmonics(64)
  , ampFunc({ |i| (if ((i % 2) == 0) {2.0} {1.0}) / i }) |

  var sampleRate = server.sampleRate;
  var freq_amp = Signal.newClear(size / 2);
  var amps = Array.fill(number_harmonics + 1, ampFunc);
  var complex, smp;

  for (1, number_harmonics, { |nh|
    var bw_Hz = (pow(2, bw / 1200) - 1.0) * f0 * nh;
    var bwi = bw_Hz / (2.0 * sampleRate);
    var fi = f0 * nh / sampleRate;

    freq_amp.do{ |f_amp, index|
      var hprofile = ~profile.value((index / size) - fi, bwi);
      freq_amp[index] = f_amp + (hprofile * amps[nh]);
    }
  });

  // 直流を除去。
  freq_amp[0] = 0;
  freq_amp.plot;

  // ナイキスト周波数以上の成分を付け加える。
  freq_amp = freq_amp ++ Signal.fill(freq_amp.size, 0);

  // 位相のランダマイズ。
  complex = Polar(
    freq_amp,
    freq_amp.class.fill(freq_amp.size, {2pi.rand})
  );

  smp = complex.real.ifft(complex.imag, Signal.fftCosTable(freq_amp.size));
  smp.real.normalize
};
);

(
~sig = ~padTable.value(s, 2**18); // 乗数が 10 以下のときにピッチがおかしくなる。
~sig.plot;
~waveTable = Buffer.loadCollection(s, ~sig);

{ Pan2.ar(PlayBuf.ar(1, ~waveTable, 1, 1, 0, 1)) }.play;
);

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");
);