Home記事一覧フォーラム

無限に即興演奏し続けるFMシンセ(JavaScript版)

【更新履歴】
2017/07/31 映像と音声の同期を向上
2017/07/15 ビジュアライザーを追加
2017/05/23 インスタンス解放時の不具合を修正
2017/05/22 曲を追加
2017/04/11 リバーブの仕様を変更
2017/04/03 新規公開

無限に即興演奏し続けるFMシンセのJavaScript版です。

Web Audio API、WebGLに対応したウェブ・ブラウザーでこのページを開き、ボタンをクリックすると演奏します。
クリックすると黒画面になり、数秒後に音と絵が出ます。
クリック、もしくはEscキーで元の画面に戻ります。
「FullScreen」ボタンでフルスクリーンモードになります。

      

ソースコード

これらのソースコードはBSD 2-Clauseライセンスで公開します。

synth.js
/*
  Copyright (c) 2017, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


function Synthesizer()
{
  var STATE_ATTACK = 0;
  var STATE_DECAY = 1;
  var STATE_SUSTAIN = 2;
  var STATE_RELEASE = 3;
  var STATE_SLEEP = 4;
  var WAVE_BUFFER_BITS = 8;
  var INT_BITS = 32;
  var FIXED_BITS = 14;
  var FIXED_BITS_ENV = 8;
  var WAVE_ADDR_SHIFT = (INT_BITS - WAVE_BUFFER_BITS);
  var WAVE_ADDR_SHIFT_M = (WAVE_ADDR_SHIFT - FIXED_BITS);
  var FIXED_SCALE = (1 << FIXED_BITS);
  var FIXED_SCALE_M1 = (FIXED_SCALE - 1);
  var WAVE_BUFFER_SIZE = (1 << WAVE_BUFFER_BITS);
  var WAVE_BUFFER_SIZE_M1 = (WAVE_BUFFER_SIZE - 1);
  var ENV_VALUE_MAX = (1 << FIXED_BITS << FIXED_BITS_ENV);
  var MOD_LEVEL_MAX = (Math.floor(FIXED_SCALE * 0.52));
  var DEFAULT_LEVEL = (Math.floor(FIXED_SCALE / 4));
  var bufferLength = 2048;

  var oscs = 4;
  var reverbLLength = 0.5111;
  var reverbRLength = 0.5;
  var reverbLSize = 0;
  var reverbRSize = 0;
  var reverbVolume = (Math.floor(FIXED_SCALE * 0.3));
  var reverbDecay = 0.8;
  var outVolume = (1.0 / FIXED_SCALE);

  var self = this;
  var callback;
  var callbackRate = 0;
  var callbackCounter = 0;
  var reverbCounter = 0;
  var reverbAddrL = 0;
  var reverbAddrR = 0;
  var outL = 0;
  var outR = 0;
  var waac = new AudioContext();
  var wasp = null;
  var waveData = [];

  var reverbBufferL = [];
  var reverbBufferR = [];
  var params = [];

  function Param()
  {
    this.envelopeLevelA = 0;
    this.envelopeLevelS = 0;
    this.envelopeDiffA = 0;
    this.envelopeDiffD = 0;
    this.envelopeDiffR = 0;
    this.modPatch0 = 0;
    this.modPatch1 = 0;
    this.modLevel0 = 0;
    this.modLevel1 = 0;
    this.levelL = 0;
    this.levelR = 0;
    this.levelRev = 0;

    this.state = 0;
    this.count = 0;
    this.currentLevel = 0;
    this.pitch = 0;
    this.mod0 = 0;
    this.mod1 = 0;
    this.outData = 0;
    this.outWaveL = 0;
    this.outWaveR = 0;
    this.outRevL = 0;
    this.outRevR = 0;
    this.mixOut = false;
    this.noteOn = false;
    this.noteOnSave = false;
  }

  var sp_process = function(ev)
  {
    var i;
    var bufL = ev.outputBuffer.getChannelData(0);
    var bufR = ev.outputBuffer.getChannelData(1);
    for (i = 0; i < bufferLength; i++)
    {
      callbackCounter++;
      if (callbackCounter > callbackRate)
      {
        callbackCounter = 0;
        callback(i);
      }
      render();
      bufL[i] = outL;
      bufR[i] = outR;
    }
  };

  // default callback
  var dummy = function(delaySample)
  {
  };

  this.init = function()
  {
    var i, reverbLBufferSize, reverbRBufferSize;
    callback = dummy;
    reverbLSize = Math.floor(this.getSampleRate() * reverbLLength);
    reverbRSize = Math.floor(this.getSampleRate() * reverbRLength);
    reverbLBufferSize = (reverbLSize + 0x800) & (~0x7ff);
    reverbRBufferSize = (reverbRSize + 0x800) & (~0x7ff);
    reverbBufferL = new Array(reverbLBufferSize);
    reverbBufferR = new Array(reverbRBufferSize);
    for (i = 0; i < reverbLBufferSize; i++)
    {
      reverbBufferL[i] = 0;
    }
    for (i = 0; i < reverbRBufferSize; i++)
    {
      reverbBufferR[i] = 0;
    }

    for (i = 0; i < WAVE_BUFFER_SIZE; i++)
    {
      waveData[i] = Math.floor(Math.sin(Math.PI * 2.0 / WAVE_BUFFER_SIZE * i) * FIXED_SCALE);
    }

    for (i = 0; i < oscs; i++)
    {
      params[i] = new Param();

      params[i].state = STATE_SLEEP;
      params[i].envelopeLevelA = ENV_VALUE_MAX;
      params[i].envelopeLevelS = 0;
      params[i].envelopeDiffA = ENV_VALUE_MAX >> 3;
      params[i].envelopeDiffD = (- ENV_VALUE_MAX) >> 15;
      params[i].envelopeDiffR = (- ENV_VALUE_MAX) >> 13;
      params[i].levelL = DEFAULT_LEVEL;
      params[i].levelR = DEFAULT_LEVEL;
      params[i].levelRev = reverbVolume;
      params[i].mixOut = true;
      params[i].modPatch0 = i;
      params[i].modPatch1 = i;
      params[i].modLevel0 = MOD_LEVEL_MAX * randI(6);
      params[i].modLevel1 = 0;
    }

    reverbAddrL = 0;
    reverbAddrR = 0;
    reverbCounter = 0;
  };

  // from callback
  var render = function()
  {
    var i, waveAddrF, waveAddrR, oscOutF, oscOutR, waveAddrM, oscOut, limitValue, valueDiff, limitGt, waveAddr, mixL, mixR, mixRevL, mixRevR, reverbL, reverbR;
    // patch
    for (i = 0; i < oscs; i++)
    {
      params[i].mod0 = params[params[i].modPatch0].outData;
      params[i].mod1 = params[params[i].modPatch1].outData;
    }

    // synth
    for (i = 0; i < oscs; i++)
    {
      // envelope generator
      if ((params[i].noteOn === true) && (params[i].noteOnSave !== params[i].noteOn))
      {
        params[i].state = STATE_ATTACK;
      }
      if ((params[i].noteOn === false) && (params[i].noteOnSave !== params[i].noteOn))
      {
        params[i].state = STATE_RELEASE;
      }
      params[i].noteOnSave = params[i].noteOn;

      if (params[i].state === STATE_SLEEP)
      {
        params[i].count = 0;
      }

      limitValue = 0;
      valueDiff = 0;
      limitGt = false;

      if (params[i].state === STATE_ATTACK)
      {
        limitValue = params[i].envelopeLevelA;
        valueDiff = params[i].envelopeDiffA;
        limitGt = true;
      }
      else if (params[i].state === STATE_DECAY)
      {
        limitValue = params[i].envelopeLevelS;
        valueDiff = params[i].envelopeDiffD;
        limitGt = false;
      }
      else if (params[i].state === STATE_RELEASE)
      {
        limitValue = 0;
        valueDiff = params[i].envelopeDiffR;
        limitGt = false;
      }

      params[i].currentLevel += valueDiff;

      if (((limitGt === true) && (params[i].currentLevel > limitValue)) ||
          ((limitGt === false) && (params[i].currentLevel < limitValue)))
      {
        params[i].currentLevel = limitValue;
        params[i].state++;
      }

      waveAddr = (params[i].count +
                  (params[i].mod0 * params[i].modLevel0) +
                  (params[i].mod1 * params[i].modLevel1)) >>> WAVE_ADDR_SHIFT_M;

      // fetch wave data
      waveAddrF = waveAddr >> FIXED_BITS;
      waveAddrR = (waveAddrF + 1) & WAVE_BUFFER_SIZE_M1;
      oscOutF = waveData[waveAddrF];
      oscOutR = waveData[waveAddrR];
      waveAddrM = waveAddr & FIXED_SCALE_M1;
      oscOut = ((oscOutF * (FIXED_SCALE - waveAddrM)) >> FIXED_BITS) +
        ((oscOutR * waveAddrM) >> FIXED_BITS);
      params[i].outData = (oscOut * (params[i].currentLevel >> FIXED_BITS_ENV)) >> FIXED_BITS;
      params[i].count += params[i].pitch;

      // mix
      if (params[i].mixOut === 0)
      {
        params[i].outWaveL = 0;
        params[i].outWaveR = 0;
        params[i].outRevL = 0;
        params[i].outRevR = 0;
      }
      else
      {
        params[i].outWaveL = (params[i].outData * params[i].levelL) >> FIXED_BITS;
        params[i].outWaveR = (params[i].outData * params[i].levelR) >> FIXED_BITS;
        params[i].outRevL = (params[i].outWaveL * params[i].levelRev) >> FIXED_BITS;
        params[i].outRevR = (params[i].outWaveR * params[i].levelRev) >> FIXED_BITS;
      }
    }

    // mixing
    mixL = 0;
    mixR = 0;
    mixRevL = 0;
    mixRevR = 0;
    for (i = 0; i < oscs; i++)
    {
      mixL += params[i].outWaveL;
      mixR += params[i].outWaveR;
      mixRevL += params[i].outRevL;
      mixRevR += params[i].outRevR;
    }

    // reverb
    reverbL = reverbBufferR[reverbAddrR] * reverbDecay;
    reverbR = reverbBufferL[reverbAddrL] * reverbDecay;
    reverbL += mixRevR;
    reverbR += mixRevL;
    reverbBufferL[reverbAddrL] = reverbL;
    reverbBufferR[reverbAddrR] = reverbR;
    reverbAddrL++;
    if (reverbAddrL > reverbLSize)
    {
      reverbAddrL = 0;
    }
    reverbAddrR++;
    if (reverbAddrR > reverbRSize)
    {
      reverbAddrR = 0;
    }
    outL = (mixL + reverbBufferL[reverbAddrL]) * outVolume;
    outR = (mixR + reverbBufferR[reverbAddrR]) * outVolume;
  };

  this.start = function()
  {
    var i;
    wasp = waac.createScriptProcessor(0, 2, 2);
    bufferLength = wasp.bufferSize;
    wasp.onaudioprocess = sp_process;
    wasp.connect(waac.destination);
  };

  this.stop = function()
  {
    if (wasp)
    {
      wasp.disconnect();
    }
  };

  this.getSampleRate = function()
  {
    return waac.sampleRate;
  };

  this.getBufferLength = function()
  {
    return bufferLength;
  };

  this.getWaveBufferSize = function()
  {
    return WAVE_BUFFER_SIZE;
  };

  this.getEnvValueMax = function()
  {
    return ENV_VALUE_MAX;
  };

  this.getOscs = function()
  {
    return oscs;
  };

  this.getModLevelMax = function()
  {
    return MOD_LEVEL_MAX;
  };

  this.getParams = function(osc)
  {
    return params[osc];
  };

  this.setCallback = function(callback_arg)
  {
    callback = callback_arg;
  };

  this.setCallbackRate = function(callbackRate_arg)
  {
    callbackRate = callbackRate_arg;
    callbackCounter = 0;
  };

  this.setSynthParam = function(oscs_arg, reverbLLength_arg, reverbRLength_arg, reverbVolume_arg, reverbDecay_arg, outVolume_arg)
  {
    oscs = oscs_arg;
    reverbLLength = reverbLLength_arg;
    reverbRLength = reverbRLength_arg;
    reverbVolume = reverbVolume_arg;
    reverbDecay = reverbDecay_arg;
    outVolume = outVolume_arg;
  };
}
sequencer.js
/*
  Copyright (c) 2017, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


function DataSeq(satz, ch, chord, note, oct, delay)
{
  this.satz = satz;
  this.ch = ch;
  this.chord = chord;
  this.note = note;
  this.oct = oct;
  this.delay = delay;
}

function Sequencer()
{
  var self = this;
  var tempo = 5.0;
  var seqLength = 8;
  var bassFrequency = 2;
  var scaleBufferSize = 16;
  var seqCounter = 0;
  var barCounter = 0;
  var satzCounter = 0;
  var deleteCounter = 0;
  var chord = 0;
  var note = 0;
  var octMin = 5;
  var octRange = 3;
  var deleteFrequency = 16;
  var repeat = 4;
  var toneChange = true;
  var appendBass = true;
  var tempoCount = 0;
  var chordLength = 3;
  var browserType = '';
  var scaleTable = [];
  var chordData = [
    [0,4,7,0,0,0,0,0],
    [4,7,11,0,0,0,0,0],
    [0,4,9,0,0,0,0,0],
    [0,5,9,0,0,0,0,0],
    [2,5,9,0,0,0,0,0],
    [2,7,11,0,0,0,0,0]
  ];
  var progressionData = [
    [0, 6],
    [0, 6],
    [0, 6],
    [0, 6],
    [0, 6],
    [0, 3]
  ];
  var bassData = [0,0,2,1,0,1];
  var seqData = [];

  this.synth = new Synthesizer();

  // default callback
  var dummy = function()
  {
  };
  var callbackNoteOn = dummy;

  function NoteData()
  {
    this.note = 0;
    this.oct = 0;
  }

  this.stop = function()
  {
    if (self.synth)
    {
      self.synth.stop();
    }
  };

  this.init = function()
  {
    var i, j;
    browserType = queryBrowserType();
    self.synth.init();
    tempoCount = Math.floor(self.synth.getSampleRate() / tempo);

    for (i = 0; i < scaleBufferSize; i++)
    {
      scaleTable[i] = Math.floor(Math.pow(2.0, i / 12.0) * 440.0 * (0x100000000 / self.synth.getSampleRate() / self.synth.getWaveBufferSize()));
    }

    for (i = 0; i < self.synth.getOscs(); i++)
    {
      seqData[i] = [];
      for (j = 0; j < seqLength; j++)
      {
        seqData[i][j] = new NoteData();
      }
    }
  };

  this.start = function()
  {
    seqCounter = 0;
    barCounter = 0;
    deleteCounter = 0;
    chord = 0;
    note = 0;

    self.synth.setCallback(loop);
    self.synth.setCallbackRate(tempoCount);
    self.synth.start();
  };

  // callback
  var loop = function(delaySample)
  {
    // sequencer
    var i, ch, beat, n, mixL, mixR, mixRevL, mixRevR, reverbL, reverbR;
    var oscs = self.synth.getOscs();
    var timeNow = Date.now();
    var buflen = self.synth.getBufferLength();
    if (browserType === "firefox")
    {
      buflen = 0;
    }
    var soundDelay = 1000.0 * (delaySample + buflen) / self.synth.getSampleRate();

    if (seqCounter >= seqLength)
    {
      seqCounter = 0;
      if (barCounter >= repeat)
      {
        barCounter = 0;
        if (toneChange === true)
        {
          for (i = 0; i < oscs; i++)
          {
            self.synth.getParams(i).envelopeDiffA = self.synth.getEnvValueMax() >> (randI(11) + 2);
            self.synth.getParams(i).modLevel0 = self.synth.getModLevelMax() * randI(6);
          }
        }
        chord = randI(progressionData[chord][1]) + progressionData[chord][0];
        deleteCounter = randI(deleteFrequency);
        satzCounter++;
      }
      for (i = 0; i < deleteCounter; i++)
      {
        seqData[randI(oscs)][randI(seqLength)].oct = 0;
      }
      for (i = 0; i < 4; i++)
      {
        ch = randI(oscs);
        beat = randI(seqLength);
        beat_prev = beat - 1;
        if (beat_prev < 0)
        {
          beat_prev += seqLength;
        }
        seqData[ch][beat].note = randI(chordLength);
        seqData[ch][beat].oct = randI(octRange) + octMin;
        seqData[ch][beat_prev].oct = 0;
      }
      if (appendBass === true)
      {
        for (i = 0; i < seqLength; i+=bassFrequency)
        {
          seqData[0][i].note = bassData[chord];
          seqData[0][i].oct = octMin;
        }
      }
      barCounter++;
    }
    for (i = 0; i < oscs; i++)
    {
      if (seqData[i][seqCounter].oct !== 0)
      {
        n = chordData[chord][seqData[i][seqCounter].note];
        self.synth.getParams(i).pitch = scaleTable[n] << seqData[i][seqCounter].oct;
        self.synth.getParams(i).noteOn = true;
        callbackNoteOn(new DataSeq(satzCounter, i, chord, n, seqData[i][seqCounter].oct, timeNow + soundDelay));
      }
      else
      {
        self.synth.getParams(i).noteOn = false;
      }
    }
    seqCounter++;
  };

  this.setSeqParam = function(tempo_arg, seqLength_arg, bassFrequency_arg, octMin_arg, octRange_arg, deleteFrequency_arg, repeat_arg, toneChange_arg, appendBass_arg)
  {
    tempo = tempo_arg;
    seqLength = seqLength_arg;
    bassFrequency = bassFrequency_arg;
    octMin = octMin_arg;
    octRange = octRange_arg;
    deleteFrequency = deleteFrequency_arg;
    repeat = repeat_arg;
    toneChange = toneChange_arg;
    appendBass = appendBass_arg;
  };

  this.setChordData = function(chordLength_arg, chordData_arg, progressionData_arg, bassData_arg)
  {
    chordLength = chordLength_arg;
    chordData = chordData_arg;
    progressionData = progressionData_arg;
    bassData = bassData_arg;
  };

  this.setCallback = function(callbackNoteOn_arg)
  {
    callbackNoteOn = callbackNoteOn_arg;
  };
}
visualizer.js
/*
  Copyright (c) 2017, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


function Visualizer()
{
  var vs_s =
      "uniform float time;\n" +
      "uniform float dir;\n" +
      "attribute vec2 pos_s;\n" +
      "attribute vec2 pos_e;\n" +
      "attribute vec3 col;\n" +
      "attribute float otime;\n" +
      "varying vec4 fcol;\n" +
      "void main(void)\n" +
      "{\n" +
      " vec2 d2 = vec2(dir, 1.0);\n" +
      " float t1 = clamp(otime - time, 0.0, 1.0);\n" +
      " vec2 p1 = mix(pos_s * d2, pos_e * d2, vec2(t1, t1));\n" +
      " gl_Position = vec4(p1, 0.0, 1.0);\n" +
      " vec3 c1 = mix(vec3(0.0, 0.0, 0.0), col, vec3(t1, t1, t1));\n" +
      " fcol = vec4(c1, 1.0);\n" +
      "}\n";

  var fs_s =
      "precision mediump float;\n" +
      "varying vec4 fcol;\n" +
      "void main(void)\n" +
      "{\n" +
      " gl_FragColor = fcol;\n" +
      "}\n";

  var self = this;
  var webgl = new WebGLLib();
  var sprog;
  var vboPosS;
  var vboPosE;
  var vboCol;
  var vboOtime;
  var objects = 0;
  var time = 0.0;
  var vdataPosS = [];
  var vdataPosE = [];
  var vdataCol = [];
  var vdataOtime = [];
  var objPtr = 0;
  var speed = 0.01;
  var masterSeed = randI(0xffffffff);
  var playing = false;

  this.fifo = new Fifo();
  this.randf = new RandF();

  // callback sub
  var addObj = function(data)
  {
    var i, j, k;
    var pos_s = [];
    var pos_e = [];
    var col = [];
    var otime = [];
    self.randf.seed((data.ch << 24) ^ (data.satz << 20) ^ (data.chord << 16) ^ (data.note << 8) ^ data.oct ^ masterSeed);
    var x = self.randf.get() * 2.0 - 1.0;
    var y = self.randf.get() * 2.0 - 1.0;
    var r = self.randf.get();
    var g = self.randf.get();
    var b = self.randf.get();
    for (i = 0; i < 3; i++)
    {
      pos_s.push(x + self.randf.get() - 0.5);
      pos_s.push(y + self.randf.get() - 0.5);
      pos_e.push(x + self.randf.get() - 0.5);
      pos_e.push(y + self.randf.get() - 0.5);
      col.push(r);
      col.push(g);
      col.push(b);
      otime.push(time + 1.0);
    }
    webgl.updateBuffer(vboPosS, pos_s, objPtr * 6);
    webgl.updateBuffer(vboPosE, pos_e, objPtr * 6);
    webgl.updateBuffer(vboCol, col, objPtr * 9);
    webgl.updateBuffer(vboOtime, otime, objPtr * 3);
    objPtr++;
    if (objPtr >= objects)
    {
      objPtr = 0;
    }
  };

  // callback
  var draw = function()
  {
    var i, peek, data;
    var timeNow = Date.now();
    while (true)
    {
      peek = self.fifo.peek();
      if (peek === undefined)
      {
        break;
      }
      else if (peek.delay > timeNow)
      {
        break;
      }
      data = self.fifo.pop();
      if (data === undefined)
      {
        break;
      }
      addObj(data);
    }
    webgl.clearScreen();
    webgl.uniform1f(sprog, 'time', time);
    webgl.uniform1f(sprog, 'dir', 1.0);
    webgl.drawTriangles(0, objects * 3);
    webgl.uniform1f(sprog, 'dir', -1.0);
    webgl.drawTriangles(0, objects * 3);
    time += speed;
    if (playing === true)
    {
      webgl.nextFrame();
    }
  };

  this.init = function(objects_arg, speed_arg)
  {
    var i, j, k;
    objects = objects_arg;
    speed = speed_arg;
    self.fifo.init(objects * 16);
    webgl.init('canvas1', draw);
    sprog = webgl.createProgram(vs_s, fs_s);
    webgl.useProgram(sprog);

    for (i = 0; i < objects; i++)
    {
      for (k = 0; k < 3; k++)
      {
        for (j = 0; j < 2; j++)
        {
          vdataPosS.push(0.0);
          vdataPosE.push(0.0);
        }
        for (j = 0; j < 3; j++)
        {
          vdataCol.push(0.0);
        }
        vdataOtime.push(0.0);
      }
    }

    vboPosS = webgl.createBuffer(sprog, 'pos_s', vdataPosS, 2, true);
    vboPosE = webgl.createBuffer(sprog, 'pos_e', vdataPosE, 2, true);
    vboCol = webgl.createBuffer(sprog, 'col', vdataCol, 3, true);
    vboOtime = webgl.createBuffer(sprog, 'otime', vdataOtime, 1, true);

    webgl.setBlendMode(1);
  };

  this.start = function()
  {
    playing = true;
    draw();
  };

  this.stop = function()
  {
    playing = false;
    webgl.close();
  };
}
webgl_lib.js
/*
  Copyright (c) 2017, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


function WebGLLib()
{
  var canvas;
  var context;
  var gl;
  var width;
  var height;
  var callback;
  var resized = true;
  var averageTime = 16.666666;
  var fpsCounter = 0;
  var dateTime;

  var clearScreen = function()
  {
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LEQUAL);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  };

  this.clearScreen = function()
  {
    clearScreen();
  };

  this.getWidth = function()
  {
    return width;
  };

  this.getHeight = function()
  {
    return height;
  };

  var delayResize = function()
  {
    if (resized === true)
    {
      resized = false;
      setTimeout(resize, 1000);
    }
  };

  var resize = function()
  {
    width = window.innerWidth;
    height = window.innerHeight;
    canvas.width = width;
    canvas.height = height;
    gl.viewport(0, 0, width, height);
    clearScreen();
    resized = true;
  };

  var createShader = function(source, type)
  {
    var shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);

    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
    {
      console.log("Error: Create Shader");
      shader = null;
    }
    return shader;
  };

  this.nextFrame = function()
  {
    var now = Date.now();
    if (fpsCounter >= 60)
    {
      fpsCounter = 0;
      averageTime = (now - dateTime) * 0.0166666666667;
      dateTime = now;
    }
    fpsCounter++;
    window.requestAnimationFrame(callback);
  };

  this.drawTriangles = function(start, count)
  {
    gl.drawArrays(gl.TRIANGLES, start, count);
  };

  this.uniform1f = function(program, name, value)
  {
    gl.uniform1f(gl.getUniformLocation(program, name), value);
  };

  this.useProgram = function(pg_arg)
  {
    gl.useProgram(pg_arg);
  };

  this.createProgram = function(vs_source, fs_source)
  {
    var vs = createShader(vs_source, gl.VERTEX_SHADER);
    var fs = createShader(fs_source, gl.FRAGMENT_SHADER);
    var sprog = gl.createProgram();
    gl.attachShader(sprog, vs);
    gl.attachShader(sprog, fs);
    gl.linkProgram(sprog);

    if (!gl.getProgramParameter(sprog, gl.LINK_STATUS))
    {
      console.log("Error: Create Program");
      sprog = null;
    }
    return sprog;
  };

  this.setBlendMode = function(mode)
  {
    switch (mode)
    {
      case 0:
      gl.disable(gl.BLEND);
      break;
      case 1:
      gl.enable(gl.BLEND);
      gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ONE, gl.ONE);
      gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
      break;
    }
  };

  this.createBuffer = function(program, name, array, size, isDynamic)
  {
    var usage;
    if (isDynamic)
    {
      usage = gl.DYNAMIC_DRAW;
    }
    else
    {
      usage = gl.STATIC_DRAW;
    }
    var vbo = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(array), usage);
    var location = gl.getAttribLocation(program, name);
    gl.enableVertexAttribArray(location);
    gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
    return vbo;
  };

  this.bindBuffer = function(buffer)
  {
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  };

  this.updateBuffer = function(buffer, array, offset)
  {
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferSubData(gl.ARRAY_BUFFER, offset * 4, new Float32Array(array));
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  };

  this.deleteBuffer = function(buffer)
  {
    gl.deleteBuffer(buffer);
  };

  this.init = function(canvas_arg, callback_arg)
  {
    canvas = document.getElementById(canvas_arg);
    callback = callback_arg;
    fpsCounter = 0;
    dateTime = Date.now();

    if (canvas)
    {
      gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
    }
    else
    {
      console.log("Error: Get Canvas");
    }

    if (gl)
    {
      resize();
      window.addEventListener('resize', delayResize);
    }
    else
    {
      console.log("Error: Get webgl");
    }
  };

  this.close = function()
  {
    window.removeEventListener('resize', delayResize);
  };

  this.getAverageTime = function()
  {
    return averageTime;
  };
}
stlib.js
/*
  Copyright (c) 2017, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


var queryBrowserType = function()
{
  var type = '';
  var ua = window.navigator.userAgent.toLowerCase();
  if ((ua.indexOf('msie') !== -1) || (ua.indexOf('trident') !== -1))
  {
    type = 'msie';
  }
  else if (ua.indexOf('edge') !== -1)
  {
    type = 'edge';
  }
  else if (ua.indexOf('safari') !== -1)
  {
    type = 'safari';
  }
  else if (ua.indexOf('firefox') !== -1)
  {
    type = 'firefox';
  }
  else if (ua.indexOf('opera') !== -1)
  {
    type = 'opera';
  }
  else if (ua.indexOf('chrome') !== -1)
  {
    type = 'chrome';
  }
  return type;
};

var isFullScreen = function()
{
  var r = false;
  if (document.fullscreenElement)
  {
    if (document.fullscreenElement !== null)
    {
      r = true;
    }
  }
  else if (document.msFullscreenElement)
  {
    if (document.msFullscreenElement !== null)
    {
      r = true;
    }
  }
  else if (document.webkitFullscreenElement)
  {
    if (document.webkitFullscreenElement !== null)
    {
      r = true;
    }
  }
  else if (document.mozFullScreenElement)
  {
    if (document.mozFullScreenElement !== null)
    {
      r = true;
    }
  }
  else if (document.mozFullScreen || document.webkitIsFullScreen)
  {
    r = true;
  }
  return r;
};

var requestFullScreen = function()
{
  var elem = document.documentElement;
  if (elem.requestFullscreen)
  {
    elem.requestFullscreen();
  }
  else if (elem.webkitRequestFullscreen)
  {
    elem.webkitRequestFullscreen();
  }
  else if (elem.mozRequestFullScreen)
  {
    elem.mozRequestFullScreen();
  }
  else if (elem.msRequestFullscreen)
  {
    elem.msRequestFullscreen();
  }
};

var exitFullScreen = function()
{
  if (document.exitFullscreen)
  {
    document.exitFullscreen();
  }
  else if (document.webkitExitFullscreen)
  {
    document.webkitExitFullscreen();
  }
  else if (document.webkitCancelFullScreen)
  {
    document.webkitCancelFullScreen();
  }
  else if (document.mozCancelFullScreen)
  {
    document.mozCancelFullScreen();
  }
  else if (document.msExitFullscreen)
  {
    document.msExitFullscreen();
  }
};

var toggleFullScreen = function()
{
  if (isFullScreen() === true)
  {
    exitFullScreen();
  }
  else
  {
    requestFullScreen();
  }
};

var randI = function(max)
{
  return Math.floor(Math.random() * max);
};

function RandF()
{
  var r = 0x92d68ca2;

  this.seed = function(s)
  {
    r = 0x92d68ca2 ^ s;
  };

  this.get = function()
  {
    r = r ^ (r << 13);
    r = r ^ (r >> 17);
    r = r ^ (r << 5);
    return ((r & 0x7fffffff) * 4.656612873e-10);
  };
}

function Fifo()
{
  var self = this;
  var objects = 0;
  this.ptr_in = 0;
  this.ptr_out = 0;
  this.data = [];

  this.init = function(size)
  {
    objects = size;
    var i;
    for (i = 0; i < objects; i++)
    {
      self.data[i] = null;
    }
    self.ptr_in = 0;
    self.ptr_out = 0;
  };

  this.push = function(data_arg)
  {
    var pi = self.ptr_in;
    var po = self.ptr_out;
    pi++;
    if (pi >= objects)
    {
      pi -= objects;
    }
    if (pi === po)
    {
      return;
    }
    self.data[pi] = data_arg;
    self.ptr_in = pi;
    return true;
  };

  this.pop = function()
  {
    var pi = self.ptr_in;
    var po = self.ptr_out;
    if (pi === po)
    {
      return;
    }
    po++;
    if (po >= objects)
    {
      po -= objects;
    }
    var d = self.data[po];
    self.ptr_out = po;
    return d;
  };

  this.peek = function()
  {
    var pi = self.ptr_in;
    var po = self.ptr_out;
    if (pi === po)
    {
      return;
    }
    po++;
    if (po >= objects)
    {
      po -= objects;
    }
    var d = self.data[po];
    return d;
  };
}
main.js
/*
  Copyright (c) 2017, miya
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


function Main()
{
  var self = this;
  var FIXED_BITS = 14;
  var FIXED_BITS_ENV = 8;
  var FIXED_SCALE = (1 << FIXED_BITS);
  var ENV_VALUE_MAX = (1 << FIXED_BITS << FIXED_BITS_ENV);
  var VSPEED = 0.001;
  var OBJECTS = 32;
  var saveDocument = '';

  var seq = new Sequencer();
  var visualizer = new Visualizer();

  this.start = function()
  {
    visualizer.start();
    seq.start();
  };

  this.stop = function()
  {
    window.removeEventListener('keyup', evKeyUp);
    window.removeEventListener('dblclick', evDblClick);
    seq.stop();
    visualizer.stop();
    document.body.innerHTML = saveDocument;
  };

  // callback
  var noteOnCallback = function(data)
  {
    visualizer.fifo.push(data);
  };

  var init = function()
  {
    saveDocument = document.body.innerHTML;
    document.body.innerHTML = '<canvas id="canvas1"></canvas>';
    window.addEventListener('keyup', evKeyUp);
    window.addEventListener('dblclick', evDblClick);
  };

  this.playMonologue = function()
  {
    var i;
    var OSCS = 4;
    var TEMPO = 6.0;
    var PART_VOLUME = (Math.floor(FIXED_SCALE / OSCS));
    var MOD_LEVEL_MAX = (Math.floor(FIXED_SCALE * 0.52));
    var REVERB_VOLUME = (Math.floor(FIXED_SCALE * 0.3));
    var OUT_VOLUME = (1.0 / FIXED_SCALE);
    var chordData = [
      [0,2,4,7,11,0,0,0],
      [2,4,5,7,11,0,0,0],
      [0,4,7,9,11,0,0,0],
      [0,4,5,7,9,0,0,0],
      [0,2,4,5,9,0,0,0],
      [2,5,7,9,11,0,0,0]
    ];
    var progressionData = [
      [0, 6],
      [0, 6],
      [0, 6],
      [0, 6],
      [0, 6],
      [0, 3]
    ];
    var bassData = [0,1,3,2,1,2];
    init();
    seq.setSeqParam(TEMPO, 16, 4, 5, 3, 16, 4, true, false);
    seq.setChordData(5, chordData, progressionData, bassData);
    seq.synth.setSynthParam(OSCS, 0.278639455782, 0.136533333333, REVERB_VOLUME, 0.5, OUT_VOLUME);
    seq.setCallback(noteOnCallback);
    seq.init();
    visualizer.init(OBJECTS, TEMPO * VSPEED);
    for (i = 0; i < OSCS; i++)
    {
      seq.synth.getParams(i).envelopeDiffA = ENV_VALUE_MAX >> 3;
      seq.synth.getParams(i).envelopeDiffD = (- ENV_VALUE_MAX) >> 15;
      seq.synth.getParams(i).envelopeDiffR = (- ENV_VALUE_MAX) >> 13;
      seq.synth.getParams(i).modLevel0 = MOD_LEVEL_MAX;
    }
    seq.synth.getParams(0).levelL = PART_VOLUME >> 1;
    seq.synth.getParams(0).levelR = PART_VOLUME;
    seq.synth.getParams(1).levelL = PART_VOLUME;
    seq.synth.getParams(1).levelR = PART_VOLUME >> 1;
    self.start();
  };

  this.playLuna = function()
  {
    var i;
    var OSCS = 4;
    var TEMPO = 1.0;
    var PART_VOLUME = (Math.floor(FIXED_SCALE / OSCS));
    var MOD_LEVEL_MAX = (Math.floor(FIXED_SCALE * 0.3));
    var REVERB_VOLUME = (Math.floor(FIXED_SCALE * 0.3));
    var OUT_VOLUME = (1.0 / FIXED_SCALE);
    var chordData = [
      [0,2,4,5,7,0,0,0],
      [0,2,5,7,9,0,0,0],
      [0,2,5,7,11,0,0,0]
    ];
    var progressionData = [
      [0, 3],
      [0, 3],
      [0, 1]
    ];
    var bassData = [0,2,3];
    init();
    seq.setSeqParam(TEMPO, 8, 8, 6, 3, 16, 4, false, true);
    seq.setChordData(5, chordData, progressionData, bassData);
    seq.synth.setSynthParam(OSCS, 0.557278911565, 0.519439673469, REVERB_VOLUME, 0.8, OUT_VOLUME);
    seq.setCallback(noteOnCallback);
    seq.init();
    visualizer.init(OBJECTS, TEMPO * VSPEED);
    for (i = 0; i < OSCS; i++)
    {
      seq.synth.getParams(i).envelopeDiffA = ENV_VALUE_MAX >> 7;
      seq.synth.getParams(i).envelopeDiffD = (- ENV_VALUE_MAX) >> 17;
      seq.synth.getParams(i).envelopeDiffR = (- ENV_VALUE_MAX) >> 15;
      seq.synth.getParams(i).modLevel0 = MOD_LEVEL_MAX;
    }
    seq.synth.getParams(0).levelL = PART_VOLUME >> 1;
    seq.synth.getParams(0).levelR = PART_VOLUME;
    seq.synth.getParams(1).levelL = PART_VOLUME;
    seq.synth.getParams(1).levelR = PART_VOLUME >> 1;
    self.start();
  };

  this.playIntoxication = function()
  {
    var i;
    var OSCS = 4;
    var TEMPO = 5.0;
    var PART_VOLUME = (Math.floor(FIXED_SCALE / OSCS));
    var MOD_LEVEL_MAX = (Math.floor(FIXED_SCALE * 0.52));
    var REVERB_VOLUME = (Math.floor(FIXED_SCALE * 0.3));
    var OUT_VOLUME = (1.0 / FIXED_SCALE);
    var chordData = [
      [0,4,7,0,0,0,0,0],
      [4,7,11,0,0,0,0,0],
      [0,4,9,0,0,0,0,0],
      [0,5,9,0,0,0,0,0],
      [2,5,9,0,0,0,0,0],
      [2,7,11,0,0,0,0,0]
    ];
    var progressionData = [
      [0, 6],
      [0, 6],
      [0, 6],
      [0, 6],
      [0, 6],
      [0, 3]
    ];
    var bassData = [0,0,2,1,0,1];
    init();
    seq.setSeqParam(TEMPO, 6, 3, 5, 3, 16, 8, true, true);
    seq.setChordData(3, chordData, progressionData, bassData);
    seq.synth.setSynthParam(OSCS, 0.4, 0.8123, REVERB_VOLUME, 0.8, OUT_VOLUME);
    seq.setCallback(noteOnCallback);
    seq.init();
    visualizer.init(OBJECTS, TEMPO * VSPEED);
    for (i = 0; i < OSCS; i++)
    {
      seq.synth.getParams(i).envelopeDiffA = ENV_VALUE_MAX >> 3;
      seq.synth.getParams(i).envelopeDiffD = (- ENV_VALUE_MAX) >> 15;
      seq.synth.getParams(i).envelopeDiffR = (- ENV_VALUE_MAX) >> 13;
      seq.synth.getParams(i).modLevel0 = MOD_LEVEL_MAX * randI(6);
    }
    seq.synth.getParams(0).levelL = PART_VOLUME >> 1;
    seq.synth.getParams(0).levelR = PART_VOLUME;
    seq.synth.getParams(1).levelL = PART_VOLUME;
    seq.synth.getParams(1).levelR = PART_VOLUME >> 1;
    self.start();
  };

  var evKeyUp = function(ev)
  {
    if (ev.key === 'Escape')
    {
      self.stop();
    }
  };

  var evDblClick = function(ev)
  {
    self.stop();
  };
}

var main = new Main();
synth.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Generative Music and Graphic Art</title>
<link rel="stylesheet" type="text/css" href="synth.css" />
<script src="stlib.js"></script>
<script src="synth.js"></script>
<script src="sequencer.js"></script>
<script src="webgl_lib.js"></script>
<script src="visualizer.js"></script>
<script src="main.js"></script>
</head>
<body>

<div id="topmenu">
<div>
<span id="toplogo">Generative Music and Graphic Art</span><br />
<span id="toplink"><span id="barcolor">|</span> <span id="button" onclick="main.playMonologue();">Play &quot;Monologue&quot;</span> <span id="barcolor">|</span> <span id="button" onclick="main.playLuna();">Play &quot;Luna&quot;</span> <span id="barcolor">|</span> <span id="button" onclick="main.playIntoxication();">Play &quot;Intoxication&quot;</span> <span id="barcolor">|</span> <span id="button" onclick="toggleFullScreen();">Full Screen</span> <span id="barcolor">|</span></span><br />
<br />
<span id="toplink">Exit: Esc / Double Click &nbsp;&nbsp;&nbsp;&nbsp;&copy; 2017 miya</span>
</div>
</div>

</body>
</html>
synth.css
@charset "utf-8";
/* (C) miya. All rights reserved. */

html {
    margin: 0px;
    padding: 0px;
    height: 100%;
}

body {
    margin: 0px;
    padding: 0px;
    font-size: 12pt;
    background-color: #000000;
    height: 100%;
}

canvas {
    margin: 0px;
    padding: 0px;
    vertical-align: bottom;
}

div#topmenu {
    text-align: center;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #ffffff;
}

span#toplogo {
    font-family: "Times New Roman", Times, serif;
    font-size: 24pt;
}

span#toplink {
    font-family: "Times New Roman", Times, serif;
    font-size: 12pt;
}

span#barcolor {
    font-weight: bold;
    color: #0080ff;
}

span#button {
    cursor: pointer;
}

span#button:hover {
    text-decoration: underline;
}

ダウンロード:synth_js.tar.gz