メロディー自動生成 in SuperCollider

Ableton and Max Community Japanが今月配信していた、「作曲 vs 生成音楽」をテーマにした番組が面白かった。(配信が2021/1/30までだったので今はもう見れない)

AbelteonとMAXを使ったアルゴリズム作曲の基礎を実践しつつ、自動生成音楽の歴史とか、作るときの考え方についての話もたっぷり聞けて親切な内容だった。

中でも、「どう禁則を作るか」というところに創作性が発生するというのは何か作るときのとっかかりとして心に留めておきたい。

その中でやっていた、MAXでマルコフ連鎖の手法を使ってメロディーを自動生成するというのが面白そうだったので、SuperColliderでも似たようなこと試せないのかな?と調べてみたところ、SuperColliderのチュートリアルに、Strategies for Algorithmic Compositionのページを発見。

マルコフ連鎖を使って3つのノートを鳴らす参考コードがあったので、ここから発展させて、任意のスケール内の音を鳴らすコードを作ってみることにした。

コード全体

//トリガー
(
    var state, key, scale, bpm, matrix;
    state = 7.rand;
    key = "F5".notemidi;
    scale = Scale.spanish;
    bpm = 90;
    
    matrix = [
    [ 0, 3, 1, 1, 1, 1, 3 ],
    [ 3, 0, 3, 1, 1, 1, 1 ],
    [ 1, 3, 0, 3, 1, 1, 1 ],
    [ 1, 1, 3, 0, 3, 1, 1 ],
    [ 1, 1, 1, 3, 0, 3, 1 ],
    [ 1, 1, 1, 1, 3, 0, 3 ],
    [ 3, 1, 1, 1, 1, 3, 0 ],
    ]/10;

    {
        inf.do{
            var midi = ( state.degreeToKey( scale ) + key );
            var octave = [ 0, 12 ].wchoose( [ 0.8, 0.1 ] );
            Synth( \saw, [ \midi, midi+octave ] );
            state = Array.series(7).wchoose( matrix[state] );
            ( 60/bpm/3 ).wait; //三連符
        };
    }.fork;
)
//シンセ
(
    SynthDef( \saw, { |midi=70|
        var freq, env, sn;
        freq = midi.midicps;
        sn = Saw.ar( freq, 0.2 );
        env = EnvGen.kr( Env.perc( 0.1, 1 ), doneAction:2 );
        Out.ar( 0, sn*env );
    }).add;
)

wchooseメソッドは、リスト内からどの値を選択するかに確率をつけることができる。

[1, 2, 3, 4].wchoose([0.1, 0.2, 0.3, 0.4]);

のようにすると1〜4の中で大きい数字ほど選ばれる確率を高くすることができる。

マルコフ連鎖的な仕組みのためには「今の状態からどの状態に推移するかを確率的に決めたい」ので、推移する状態の数ぶんの確率リストのテーブル(matrix)を作成し、現在の値によってどの確率リストを採用するか決めている。

state = Array.series(7).wchoose( matrix[state] );

今の状態(state)が0の場合、0〜6の値からmatrix[0]=[ 0, 3, 1, 1, 1, 1, 3 ]/10(合計1にするために/10している)の確率によって次の値が選ばれるので、0が選ばれる確率は0、2か6が選ばれる確率が高いことになる。

レクチャーの中では既存の楽曲のMIDIデータから確率テーブルを生成する仕組みも自動でやっていたが、そこまでやるのは大変そうだったのでここでは手動で確率を書いていってる。

一応、連続した値が選ばれず、隣り合った値に推移する確率が高い、という設計にしてみたけれど、より”音楽的”にするためにはもうちょっと確率の値を考える必要がありそう。

key = "F5".notemidi;
scale = Scale.spanish;
midi = ( state.degreeToKey( scale ) + key );

上の部分で選ばれたstateの値(0〜6)をmidiノートに変換している。

degreeToKeyメソッドに度数(ただし0はじまりで数える)として値を与えると、任意のスケール内で何番目の値になるか返してくれる。マイナースケールだったら、2を与えると3(減三度は0番目から数えて3番目)を返す。

そこに基準のキーを加算してmidiノートとしてシンセに与えている。

ついでに

さっきのページの最後のほうにロジスティック写像(logistic map)を使った音生成というのも紹介されていたのでちょっと試してみる。

x_{n+1}=ax_{n}(1-x_{n})

のaのところに適当な値を入れるとカオスっぽい結果が得られる、っていうのを利用するアルゴリズムのようだ。

試しにaの値3.94のときn=500までをプロットしたもの↓

aの値3.04のとき↓

(プロットするのに使ったコード)

(
    m = Array.new();
    a = 3.04;
    p = 0.5;
    500.do{
	    p = ( a * p * (1.0-p) );
	    m = m.add( p );
    }
)
m.plot

というわけでaの値で結果が全然違う。3〜3.5あたりまでは規則的に振動していて、3.5から4に近づくとカオス度が上がっていくっぽい。

4を超すと値がinfになっちゃうので注意。

以下はlogistic mapで得た値を音階に変換してメロディーを生成するテスト。

(
var scale = Scale.romanianMinor;
var key = "C6".notemidi;
var state1 = 1.0.rand;
var state2 = 1.0.rand;
var r = 3.88;
var logisticmap, note;

//ロジスティック写像関数
logisticmap = { |previous=60| r*previous*(1.0-previous) };
			
{
    inf.do{
    //リードメロディ
    state1 = logisticmap.(state1);
    note = ( state1 * 14 ).round; 
    note = ( note.degreeToKey( scale ) + key );
    Synth(\asynth,[\midi, note]);
   //ベース
    state2 = logisticmap.(state2);
    note = ( state2 * 7 ).round; 
    note = ( note.degreeToKey( scale ) + key-24 );
    Synth(\asynth,[\midi, note]);
    0.25.wait;	
    };
}.fork;
)

鳴らしてみた

(ちなみにマルコフ連鎖の方の伴奏はニンテンドースイッチのkorgのやつで作っていて、リードにかかっているワウっぽいエフェクトが外部でかけている)

ていうか

ここまで書いてから気付いたけどSuperColliderのUGenにそもそもMarkov SynthLogisticというのがあった。

Logisticのほうは任意の周波数でロジスティックス写像を用いたノイズを信号として出力できるみたいだ。

Markov Synthのほうはinputしたシグナルを解析してマルコフ連鎖を生成してくれるっぽい?のでなんか凝ったことができそう(ただサンプル単位の値の推移を読むっぽいので音階の推移を解析するより使い方が複雑かも?)

このへんは後でまたちゃんと試してみたい。

SuperCollider: Wave TableシンセとGuiの連携(2)

今度は複数のウェーブテーブルを作って、波形を動かしてみる。

こんな感じ。

ひとつのウェーブテーブルを作成して音を鳴らすまでは前回の記事に。

複数スライダーのGuiを作成する

今回は波形を描く用のスライダーを3つ作成する。MulthSliderViewを3つ分作って変数mに格納し、そのあとcollectでm配列のアイテム(各スライダー)ごとに描画の設定をしている。

var slide=3, dot=100, width=350, height=80;
w = Window.new.front;
m = Array.fill(slide,{arg i;
	MultiSliderView(w, Rect(0, i*height, width, height))});
m.collect({arg item;
	item.value_(Array.fill(dot, {0.5}));
	item.elasticMode=1;
	item.thumbSize = 2;
	item.background_(Color(1,1,1,0.2));
});

zeroラインの描画

各スライダーの背景に0.0位置のラインをPenで描画

w.drawFunc = {
	slide.do({ arg i; 
            var px = (i*height)+(height/2);
            Pen.line(Point(0, px), Point(width,px));
            Pen.stroke;
	})
};

Bufferの確保

サイズ2048のBufferを3つぶん作成。allocConsecutiveを使うと連続したbufnumのBufferを作成できて便利。ここではbufnum:0から2までのBufferを作成している。

b = Buffer.allocConsecutive( slide, s, 2048, bufnum:0 );

ボタンが押された時に実行される関数を作成

前回はButtonクラスのアクションファンクションに直接書いていたが、見た目がごちゃっとするので今回は分けてみた。

やっている中身は単ウェーブテーブルの時と一緒だが、今回はbに格納されているBufferそれぞれに処理をしている。(引数のbufにはb内のアイテム(Buffer)が、iにはdoごとにイテレーションされるカウンターが入ってくる)

f = {
	b.do({ arg buf, i;
	  var cs, level, env;
          cs = ControlSpec(-1, 1, \lin, 0.00001, 0);
          level = cs.map(m[i].value); //mはスライダーの配列
          level.add(level.at(0));
          env = Env(level, 1, \sin).asSignal(1024).asWavetable;
          buf.loadCollection(env);
	})
}

関数fを実行するためのボタンを作成する。

p = Button(w, Rect(0, height*slide+20, 80, 30))
    .states_([["update"]])
    .action_({ f.value }); //関数fを実行する

バッファーのポジションを指定するためのスライダーを作る

今回は複数のバッファーに格納された複数ウェーブテーブルを混ぜて使う(使用するウェーブテーブルが入っているbufnumが0と1の場合、0.5を指定すれば0と1のウェーブテーブルがブレンドされたような波形になる⇒スムーズに波形から波形をトランスフォームさせることが可能)

今回はEZSliderクラスを使う。ControlSpecの部分でバッファーのポジションの範囲を設定している(今回はスライダーが3つなので、0~2の範囲)が、上限を2にすると何故か音が止まっちゃうので(原因はよくわからない。。)1.99までにしている。

また、アクションファンクションでSynthを格納するxにスライダーの値をセットしている。

g = EZSlider( w,                    // parent
              Rect(0,height*slide,width,20), // bounds
              " pos ",              // label
              ControlSpec(maxval:1.99),      // controlSpec
              { |ez| x.set(\pos, ez.value) } // action
)

UGenを作る

複数ウェーブテーブルを使用するにはVOscを使用する。引数posでEZSliderからバッファーのポジションを受け取る。

(
SynthDef(\wtSynth, { arg pos=0;
	var env, sn;
	sn = VOsc.ar(pos, mul:0.3);
	Out.ar(0, sn);
}).add;
)
x = Synth(\wtSynth);

全体のコード

https://github.com/rucochanman/SCLibrary/blob/master/Gui/waveTable

SuperCollider: Wave TableシンセとGuiの連携

↓こういうのを作るのの覚書。Guiでお絵かきした波形を鳴らすやつ。

まず、supercolliderでのWaveTableシンセの作成方法。

(1)Wave Tableを格納するBufferを用意

b = Buffer.alloc(s, 2048);

ウェーブテーブルで使う波形データ(Signal)のサイズは1024ぐらいがちょうどいい解像度とのこと。実際このSignalデータをウェーブテーブルとして使用するにはWave Tableフォーマットに変換する必要があり、変換後はデータ量が2倍になるので、Bufferを確保するサイズは2048に指定。

(2)波形データを作成

  • Bufferクラスのsin3メソッドでデータを作成(加算合成が簡単)
  • Envをシグナルに変換(波形から作りたい場合)

多分他にも色々やり方はあるがこの2パターンをよく使う。

//Bufferを直接sinの加算合成で埋める場合
(
var freqs, amps, phases, len=10;
freqs = Array.series(len);
amps = Array.fill(len, {arg i; 1/(i+1)});
phases = {pi.rand}!len;
b.sine3(freqs, amps, phases);
)

//envelopeを使う場合
( 
var env, sig, wt;
env = Env([0,1,-1,0],[3,1,1],\sin); //この時durは実際の時間でなく比率
sig = env.asSignal(1024); //Signalに変換
wt = sig.asWavetable; //wavetableフォーマットに変換
b.loadCollection(wt); //Bufferに格納
)

(3)UGenのOSCで演奏する

波形を決め打ちじゃなくてランダムで生成しているような場合はLeakDCをかませていたほうが安全

{LeakDC.ar(Osc.ar(b, freq:440, mul:0.1))}.play

ここまでがWave Table基本。以下は、Guiと連携させてみたもの。

先ずスライダーとボタンのGuiを作成。

ボタンが押されたタイミングで、スライダーの値でenvelopeを作成⇒ウェーブテーブルで使える形式に変換⇒バッファーのデータを更新している

(
//sliderのguiを作る
var dot=100, width=350, height=100; //波形の点の数、sliderの縦横幅
w = Window.new;
m = MultiSliderView(w, Rect(0, 0, width, height));
m.value_(Array.fill(dot, {0.5})); //中央値で初期化
m.elasticMode=1; //sliderの自動サイズ調整
m.thumbSize = 2; //sliderの点のサイズ
m.background_(Color(1,1,1,0.2)); //背景に透明度を設定

//ゼロのラインを描く
w.drawFunc = {
	Pen.line(Point(0,height/2), Point(width,height/2));
	Pen.stroke;
};

//波形アップデート用ボタンを作る
b = Buffer.alloc(s, 2048);
p = Button(w, Rect(0, 120, 80, 30))
        .states_([["update"]])
        .action_({ //ボタンが押された時のアクション
	    var cs, level, env;
            cs = ControlSpec(-1, 1, \lin, 0.00001, 0);
            level = cs.map(m.value); //sliderから取得した値を-1~1で正規化
            level.add(level.at(0)); //波形の頭とおしりのギャップ埋める
            env = Env(level, 1, \sin).asSignal(1024).asWavetable;
            b.loadCollection(env);
        });
//guiの表示
w.front;
)

//演奏する
{Osc.ar(b, MouseX.kr(100,1000,1), mul:0.2)}.play;