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

three.jsでシェーダーを使う基本

three.jsを使って、板ポリに画像を表示させる手順の覚書。

まず、以下はthree.jsでブラウザにまっさらの画面を表示させるために必要な最小限の構成。

画面を表示する

three.js

//ページの読み込みを待つ
window.addEventListener('load', init);

function init() {

  //画面サイズを指定
  const width = 800;
  const height = 400;

  //レンダラーを作成
  const renderer = new THREE.WebGLRenderer({
        canvas: document.querySelector('#myCanvas')
      });
  renderer.setClearColor(new THREE.Color('grey'));//背景色の設定
  document.body.appendChild( renderer.domElement );
  renderer.setSize(width, height);

  //シーンを作成
  const scene = new THREE.Scene();

  //カメラを作成
  const camera = new THREE.PerspectiveCamera(45, width / height);
  camera.position.set(0, 0, 100);
  camera.lookAt(new THREE.Vector3(0, 0, 0));

  //ライトを設置
  const envlight = new THREE.AmbientLight(0xffffff, 1);
  scene.add(envlight);

 //レンダリング開始
  render();

  function render(){
      requestAnimationFrame(render);
      renderer.render(scene, camera);
  }
}

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <!-- 必要ソース読み込み -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.js"></script>
    <script src="three.js"></script>
  </head>
  <body>
    <canvas id="myCanvas"></canvas>
  </body>
</html>

シェーダーを作成する

さらに、<BODY>内にシェーダーの記述を追加する。以下は、メッシュの全ピクセルを赤一色で塗るだけのシェーダー。

<div id="webgl"></div>
<!-- 頂点シェーダー -->
<script id="vert" type="x-shader/x-vertex">
    void main() {
        //positionはShaderMaterialで補完されるジオメトリの頂点情報
        //カメラ座標に変換したものを最終的にgl_Positionに代入
        vec4 pos = modelViewMatrix * vec4(position, 1.0);
        gl_Position = projectionMatrix * pos;
    }
</script>
<!-- フラグメントシェーダー -->
<script id="frag" type="x-shader/x-fragment">
    void main() {
        //全ピクセルを赤にする
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
</script>

Thee.js側でプレーン(板)を追加し、マテリアルに↑で作ったシェーダーを指定すると、赤い板が出てくるはず。。

//(1)Planeジオメトリ(座標)を作成
const geometry = new THREE.PlaneGeometry( 15, 20, 1 );
//(2)マテリアル(材質)にShaderMaterialを指定する
//htmlからvertとfragのソースを読み込んで指定
const vert = document.getElementById('vert').textContent;
const frag = document.getElementById('frag').textContent;
const material = new THREE.ShaderMaterial({
    vertexShader: vert,
    fragmentShader: frag,
});
//(3)ジオメトリとマテリアルからメッシュを作成
const plane = new THREE.Mesh( geometry, material );
//(4)メッシュをシーンに追加
scene.add( plane );

シェーダーを使ってテクスチャを表示させる

表示させたいpng画像を配置(ここではimgフォルダ配下にfire.pngを配置)。テクスチャとして適用する。pngのアルファ値を反映させたい場合はtransparantをtrueにする。

//テクスチャローダーを作成し画像を読み込む
const loader = new THREE.TextureLoader();
const tex = loader.load('img/fire.png');

//uniform変数uTexとしてテクスチャをシェーダーに渡す
const material = new THREE.ShaderMaterial({
    uniforms: { uTex: { value: tex } }, //uTexとしてテクスチャ情報をセット
    transparent: true, //画像の透明度を有効に
    vertexShader: vert,
    fragmentShader: frag,
 });
<script id="vert" type="x-shader/x-vertex"> 
    varying vec2 vUv; //フラグメントシェーダーに頂点情報を渡す用の変数
    void main() {
        vUv = uv; //頂点情報を格納する
        vec4 pos = modelViewMatrix * vec4(position, 1.0);
        gl_Position = projectionMatrix * pos;
    }
</script>

<script id="frag" type="x-shader/x-fragment">
    varying vec2 vUv; //頂点シェーダから渡された変数
    uniform sampler2D uTex; //three.jsから渡されたテクスチャ情報
    void main() {
        vec4 color = texture2D( uTex, vUv ).rgba; //頂点ごとのテクスチャの色取得
        gl_FragColor = color;
    }
</script>

ここでブラウザのクロスプラットフォームを無効化しないと描画されないので注意。

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;