JavaScript
HTML5
WebAudioAPI
WebGL2.0
WebGLDay 9

WebGL×WebAudioAPI GLSLから音を作ろう

概要

 2017-12-21 11.39.25.png

あまりの興奮にわざわざ画像を使ってしまった..........
ということで今回はGLSLのvertexShaderで記述した"音"をwebAudioAPIを使って流してやろうといったお話です

流れ

webGLを使ってGLSLで"音"を記述
       ↓
      GPGPU
       ↓
   webAudioAPIで流す!

要はwebGLでのGPGPUの仕方や,webAudioAPIの使い方,その他TypedArrayとかなんとかよくわかんないやつ細かいこと色々知っておかなければならないわけです
もともとプログラミングに通ずる方達にとってはわけないかもしれませんが我々下々の者にとっては知らないことが多すぎてなかなか困る.....
ということでいろいろ最初から書いてサンプルも載せます
サンプルということでGLSLでsin波を書いて流してみましょう

まずwebGLのプログラムをリンクさせるところまで

    const c = document.createElement("canvas");
    let cw = window.innerWidth;
    let ch = window.innerHeight;
    c.width = cw; c.height = ch;
    const gl = c.getContext("webgl2");
    const prg = create_program(create_shader("vs"), create_shader("fs"));        

  function create_shader(id) {
        let shader;
        const scriptElement = document.getElementById(id);
        if (!scriptElement) { return; }
        switch (scriptElement.type) {
            case "vertexShader.glsl":
                shader = gl.createShader(gl.VERTEX_SHADER);
                break;
            case "fragmentShader.glsl":
                shader = gl.createShader(gl.FRAGMENT_SHADER);
                break;
            default:
                return;
        }
        gl.shaderSource(shader, scriptElement.text);
        gl.compileShader(shader);
        if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            return shader;
        } else {
            alert(gl.getShaderInfoLog(shader));
            console.log(gl.getShaderInfoLog(shader));
        }
    }
    function create_program(vs, fs) {
        const program = gl.createProgram();
        gl.attachShader(program, vs);
        gl.attachShader(program, fs);
        gl.linkProgram(program);
        if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
            gl.useProgram(program);
            return program;
        } else {
            return null;
        }
    }

ここらへんはもうwgld.orgさんを読みましょう.大変お世話になりました
sin波を生成するのみなら頂点属性やuniform変数などもいりません
唯一気をつけるのは,GPGPUをするためにTransformFeedBackという関数を使用しますが,その関数はwebGL2.0からのみ使用できます
なのでwebGL2.0を取得してください
また,GLSLも3.0を使用しましょう.それについては後述します

GPGPUしよう!

一応GPGPUについては過去に簡単に触れています
WebGLでGPGPUできる計算機を作ってみた話
なのでそんなに詳しくやりませんがとりあえずソースだけ,

        const vTransformFeedback = gl.createBuffer();
        const transformFeedback = gl.createTransformFeedback();
        gl.bindBuffer(gl.ARRAY_BUFFER, vTransformFeedback);
        gl.bufferData(gl.ARRAY_BUFFER, sampleNum * Float32Array.BYTES_PER_ELEMENT, gl.DYNAMIC_COPY);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);
        gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);
        gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, vTransformFeedback);
        gl.beginTransformFeedback(gl.POINTS);
        gl.drawArrays(gl.POINTS, 0, sampleNum);
        gl.endTransformFeedback();
        let arrBuffer = new Float32Array(sampleNum);
        gl.getBufferSubData(gl.TRANSFORM_FEEDBACK_BUFFER, 0, arrBuffer);

sampleNumというのはそのままですがサンプリング数です
一秒間をどれだけサンプリングするかの数です(そうだよね...?)
sampleNumで指定された数だけ頂点シェーダで計算し,それらを全部まとめた物を最終的にgl.getBufferSubDataで取得してます

それと大事なことなのですが,先ほどのソースコードの中の関数create_programで定義されているgl.linkProgram(program)の前に,
gl.transformFeedbackVaryings(program, ["sound"], gl.SEPARATE_ATTRIBS);
の1行を追加してください
この関数の第二引数はGLSL側で定義します
GLSLのどの変数の値をGPGPUしたいか決める関数みたいなもんです(たぶん)

ここらへんででてくるFloat32ArrayとかArrayBufferがすごく厄介です.........ぶっちゃけよくわかってないです
まあ色々サイトはあるのでわからない場合は調べましょう

GLSL側

fragmentShader
#version 300 es
    void main(void){}
vertexShader
#version 300 es 
    precision highp float;
    out float sound;
    void main(void){
        float id=float(gl_VertexID);
        sound=sin(id);
    }

fragmentShaderは今何も書いてないです
vertexShaderに注目すると,まず1行目,前述の通り,GLSL3.0を使用する記述です
これ抜けるとまずダメです
また,これも前述の通り,out修飾子をつけて,soundを定義しています
要はこの変数に格納されている値がjavascript側に渡されるわけです

また,gl_VertexIDというのもあります
これもGLSL3.0からの登場で,頂点ごとに割り振られてる値らしいです
わざわざ頂点属性でIDを割り振らなくてもいいってことですね
最終的にこれらのIDのsinを返すように記述しています

wenAudioAPI

ということでいままでの流れで取得された結果を音にして流します

    let audioContext = new (window.AudioContext || window.webkitAudioContext)();
    let source = audioContext.createBufferSource();
    let sampleNum = audioContext.sampleRate;
    let audioBuffer = audioContext.createBuffer(2, sampleNum, sampleNum);
    for (let i = 0; i < 2; i++) {
        let bufferring = audioBuffer.getChannelData(i);
        bufferring.set(arrBuffer);
    }
    let source = audioContext.createBufferSource();
    source.buffer = audioBuffer;
    source.connect(audioContext.destination);
    source.loop = true;
    source.start();

sampleNumは前述のサンプリング数ですね
ということでここまでのコードはさっきのソースの上に書きましょう(わかりにくくてすいません)
でやってることとしては,先ほどGPGPUで取得したFloat32Arrayをbufferとして,音流してるって感じです
audioContext.createBuffer(2, sampleNum, sampleNum);の第二引数がサンプリング数,第三引数がサンプリングレートなので,今回は1秒だけ流れます
最後にloopをtrueとしているので1秒間のsin波が延々ながれる感じです

終わり

前々から存在は知っていたのでやってみたいやってみたいとは思っていましたが途中で心が折れてなんとなくできてませんでした
やっとできたのでメモ代わりとしても書いてみました
これから音楽作ってみたりもしてみたいと思います
何かこうしたほうがいいだとかここおかしいだとかご意見ありましたらコメントの方にお願いします

今回のサンプルhttps://github.com/ukeyshima/glslSound