FMsynth.htm

戻る

<!DOCTYPE HTML>
<HTML LANG="ja">

<HEAD>
<META CHARSET="Shift_JIS">
<TITLE>FM 音源みたいなもの</TITLE>
</HEAD>

<BODY STYLE="background-color:#CCFFFF">
<DIV STYLE="text-align:center">
<BR>
<B>FM 音源みたいなもの</B>
<BR><BR>

<FORM>
<DIV STYLE="display:inline-block; text-align:left">
アルゴリズム:
<SELECT ID="alg" onChange="alg_sel()">
<OPTION>1
<OPTION>2
<OPTION>3
<OPTION>4
<OPTION>5
<OPTION>6
<OPTION>7
</SELECT>
<SPAN STYLE="display:inline-block; width:211px; height:81px; overflow:hidden; vertical-align:top; margin-left:2px">
<IMG ID="fig" SRC="images/algo.gif" WIDTH=211 HEIGHT=567 STYLE="position:relative; left:0; top:0">
</SPAN>
<BR><BR>

<TABLE BORDER=1 CELLSPACING=0>
<TR><TD STYLE="background-color:#CCCCCC"></TD>
<TD ALIGN=CENTER STYLE="background-color:#CCCCCC">周波数(Hz)</TD>
<TD ALIGN=CENTER STYLE="background-color:#CCCCCC">出力レベル</TD>
<TD ALIGN=CENTER STYLE="background-color:#CCCCCC">変調レベル</TD>
</TR>
<TR><TD ALIGN=LEFT STYLE="background-color:#CCCCCC">OP1</TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="freq0" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="out0" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="mod0" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
</TR>
<TR><TD ALIGN=LEFT STYLE="background-color:#CCCCCC">OP2</TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="freq1" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="out1" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="mod1" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
</TR>
<TR><TD ALIGN=LEFT STYLE="background-color:#CCCCCC">OP3</TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="freq2" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="out2" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="mod2" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
</TR>
<TR><TD ALIGN=LEFT STYLE="background-color:#CCCCCC">OP4</TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="freq3" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="out3" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="mod3" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
</TR>
</TABLE>
<BR>

<TABLE BORDER=1 CELLSPACING=0>
<TR><TD STYLE="background-color:#CCCCCC"></TD>
<TD ALIGN=CENTER STYLE="background-color:#CCCCCC">AR(/ms)</TD>
<TD ALIGN=CENTER STYLE="background-color:#CCCCCC">DR(/ms)</TD>
<TD ALIGN=CENTER STYLE="background-color:#CCCCCC">SR(/ms)</TD>
<TD ALIGN=CENTER STYLE="background-color:#CCCCCC">RR(/ms)</TD>
<TD ALIGN=CENTER STYLE="background-color:#CCCCCC">SL</TD>
</TR>
<TR><TD ALIGN=LEFT STYLE="background-color:#CCCCCC">OP1</TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="ar0" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="dr0" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="sr0" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="rr0" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="sl0" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
</TR>
<TR><TD ALIGN=LEFT STYLE="background-color:#CCCCCC">OP2</TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="ar1" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="dr1" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="sr1" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="rr1" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="sl1" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
</TR>
<TR><TD ALIGN=LEFT STYLE="background-color:#CCCCCC">OP3</TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="ar2" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="dr2" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="sr2" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="rr2" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="sl2" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
</TR>
<TR><TD ALIGN=LEFT STYLE="background-color:#CCCCCC">OP4</TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="ar3" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="dr3" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="sr3" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="rr3" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
<TD STYLE="background-color:white"><INPUT TYPE=TEXT ID="sl3" SIZE=18 STYLE="border-style:none; background-color:white"></TD>
</TR>
</TABLE>
<BR>

OPn: オペレータ n (n = 1,2,3,4)<BR>
AR: アタック レート,DR: ディケイ レート,SR: サステイン レート,RR: リリース レート,SL: サステイン レベル<BR><BR>

<INPUT TYPE=BUTTON VALUE="パラメータ読込" onClick="load_para()">
<INPUT TYPE=BUTTON VALUE="パラメータ保存" onClick="save_para()" STYLE="margin-left:1em">
<INPUT TYPE=BUTTON VALUE="パラメータ クリア" onClick="clear_para()" STYLE="margin-left:1em">
</DIV>
<BR><BR>

<INPUT TYPE=BUTTON VALUE="発音" onMouseDown="key_on()" onMouseUp="key_off()" onMouseOut="key_off()" STYLE="font-size:large">
</FORM>

</DIV>

<A ID="dl" DOWNLOAD="FMsynth.dat" STYLE="display:none"></A><!--パラメータ保存処理用-->

<SCRIPT TYPE="text/javascript">
<!--

// 小数 キー入力チェック
function check_float(e) {
  return (e.which < 0x20 || e.which >= 0x30 && e.which <= 0x39 || e.which == 0x2e || e.ctrlKey || e.metaKey);
}

// アルゴリズム選択
function alg_sel() {
  for(let i_op = 0; i_op < 4; i_op++) {
    env[i_op].gain.cancelScheduledValues(0);
    env[i_op].gain.value = 0;  // Opera で必要
  }
  et[0] = 0;

  out[0].disconnect();
  out[1].disconnect();
  out[2].disconnect();
  out[0].connect(feedback);

  switch(elem_alg.selectedIndex) {
  case 0:
    out[0].connect(mod[1]);
    out[1].connect(mod[2]);
    out[2].connect(mod[3]);
    break;

  case 1:
    out[0].connect(mod[2]);
    out[1].connect(mod[2]);
    out[2].connect(mod[3]);
    break;

  case 2:
    out[0].connect(mod[3]);
    out[1].connect(mod[2]);
    out[2].connect(mod[3]);
    break;

  case 3:
    out[0].connect(mod[1]);
    out[1].connect(mod[3]);
    out[2].connect(mod[3]);
    break;

  case 4:
    out[0].connect(mod[1]);
    out[1].connect(aud_ctx.destination);
    out[2].connect(mod[3]);
    break;

  case 5:
    out[0].connect(mod[1]);
    out[0].connect(mod[2]);
    out[0].connect(mod[3]);
    out[1].connect(aud_ctx.destination);
    out[2].connect(aud_ctx.destination);
    break;

  case 6:
    out[0].connect(aud_ctx.destination);
    out[1].connect(aud_ctx.destination);
    out[2].connect(aud_ctx.destination);
    break;
  }

  // 接続図
  elem_fig.style.top = String(elem_alg.selectedIndex * -81) + "px";
}

// パラメータ読み込み
function load_para() {
  elem_ul = document.createElement("INPUT");
  elem_ul.type = "FILE";
  elem_ul.onchange = ul_sel;
  elem_ul.click();
}

function ul_sel() {
  clear_para();

  file_reader = new FileReader();
  file_reader.onload = ul_sel2;
  file_reader.readAsText(elem_ul.files.item(0), "UTF-8");
}

function ul_sel2() {
  let vals = file_reader.result.split("\0");
  file_reader = undefined;
  if(vals.length != 33)
    return;

  let i = parseInt(vals[0], 10);
  if(!isNaN(i)) {
    if(i >= 0 && i < 7) {
      elem_alg.selectedIndex = i;
      alg_sel();
    }
  }

  i = 1;
  for(let i_op = 0; i_op < 4; i_op++) {
    elem_freq[i_op].value = vals[i++];
    elem_out[i_op].value = vals[i++];
    elem_mod[i_op].value = vals[i++];

    elem_ar[i_op].value = vals[i++];
    elem_dr[i_op].value = vals[i++];
    elem_sr[i_op].value = vals[i++];
    elem_rr[i_op].value = vals[i++];
    elem_sl[i_op].value = vals[i++];
  }
}

// パラメータ保存
function save_para() {
  let elem = document.getElementById("dl");
  if(elem.download == undefined)
    return;

  let params = String(elem_alg.selectedIndex);

  for(let i_op = 0; i_op < 4; i_op++) {
    params += "\0" + elem_freq[i_op].value;
    params += "\0" + elem_out[i_op].value;
    params += "\0" + elem_mod[i_op].value;

    params += "\0" + elem_ar[i_op].value;
    params += "\0" + elem_dr[i_op].value;
    params += "\0" + elem_sr[i_op].value;
    params += "\0" + elem_rr[i_op].value;
    params += "\0" + elem_sl[i_op].value;
  }

  elem.href = "data:application/octet-stream," + encodeURI(params);
  elem.click();
}

// パラメータ クリア
function clear_para() {
  document.forms[0].reset();

  elem_alg.selectedIndex = 0;
  alg_sel();
}

// 発音開始
function key_on() {
  let i_op;

  for(i_op = 0; i_op < 4; i_op++) {
    if(to_id[i_op] != undefined) {
      clearTimeout(to_id[i_op]);
      to_id[i_op] = undefined;
    }
  }

  // パラメータ設定
  for(i_op = 0; i_op < 4; i_op++) {
    let val;

    // 周波数
    val = str_float(elem_freq[i_op].value);
    osc[i_op].frequency.value = val;
    mod2[i_op].gain.value = val * aud_ctx.sampleRate / 100;

    // 出力レベル
    val = str_float(elem_out[i_op].value);
    if(val > 1) {
      alert("出力レベルは 0 〜 1 の値を入力してください。");
      return;
    }
    out[i_op].gain.value = val;

    // 変調レベル
    val = str_float(elem_mod[i_op].value);
    if(val > 1) {
      alert("変調レベルは 0 〜 1 の値を入力してください。");
      return;
    }
    mod[i_op].gain.value = val;

    // アタック レート
    val = str_float(elem_ar[i_op].value);
    if(val > 1) {
      alert("アタック レートは 0 〜 1 の値を入力してください。");
      return;
    }
    ar[i_op] = val;

    // ディケイ レート
    val = str_float(elem_dr[i_op].value);
    if(val > 1) {
      alert("ディケイ レートは 0 〜 1 の値を入力してください。");
      return;
    }
    dr[i_op] = val;

    // サステイン レート
    val = str_float(elem_sr[i_op].value);
    if(val > 1) {
      alert("サステイン レートは 0 〜 1 の値を入力してください。");
      return;
    }
    sr[i_op] = val;

    // リリース レート
    val = str_float(elem_rr[i_op].value);
    if(val > 1) {
      alert("リリース レートは 0 〜 1 の値を入力してください。");
      return;
    }
    rr[i_op] = val;

    // サステイン レベル
    val = str_float(elem_sl[i_op].value);
    if(val > 1) {
      alert("サステイン レベルは 0 〜 1 の値を入力してください。");
      return;
    }
    sl[i_op] = val;
  }

  let ct = aud_ctx.currentTime;
  for(i_op = 0; i_op < 4; i_op++) {
    env[i_op].gain.cancelScheduledValues(0);
    env[i_op].gain.value = 0;  // Opera で必要

    if(ar[i_op]) {  // アタック レートあり
      env[i_op].gain.setValueAtTime(0, ct);
      env[i_op].gain.linearRampToValueAtTime(1, dt[i_op] = ct + 0.001 / ar[i_op]);
    }
    else {
      env[i_op].gain.setValueAtTime(1, dt[i_op] = ct);
    }
    if(sl[i_op]) {  // サステイン レベルあり
      if(dr[i_op])
        env[i_op].gain.linearRampToValueAtTime(sl[i_op], st[i_op] = dt[i_op] + (1 - sl[i_op]) / (dr[i_op] * 1000));
      else
        env[i_op].gain.setValueAtTime(sl[i_op], st[i_op] = dt[i_op]);
    }
    else {
      sl[i_op] = 1;
      st[i_op] = dt[i_op];
    }
    if(sr[i_op])  // サステイン レートあり
      env[i_op].gain.linearRampToValueAtTime(0, et[i_op] = st[i_op] + sl[i_op] / (sr[i_op] * 1000));
    else
      et[i_op] = Number.MAX_VALUE;
  }

  // Opera 60 以上でも動くようにするため
  if(aud_ctx.resume !== undefined)
    aud_ctx.resume();
  if(osc_ini) {
    for(i_op = 0; i_op < 4; i_op++)
      osc[i_op].start();
    osc_ini = false;
  }
}

function str_float(str) {
  str = str.trim();
  let val = 0;
  if(str.length) {
    if(str.search(/^\s*\d*\.?\d*\s*$/) != -1) {
      val = parseFloat(str);
      if(isNaN(val))
        val = 0;
    }
  }
  return val;
}

// 発音停止
function key_off() {
  if(!et[0])
    return;

  let ct = aud_ctx.currentTime;
  for(let i_op = 0; i_op < 4; i_op++) {
    env[i_op].gain.cancelScheduledValues(0);
    env[i_op].gain.value = 0;  // Opera で必要

    if(rr[i_op]) {  // リリース レートあり
      if(ct < et[i_op]) {  // 出力中
        let val;
        if(ct < st[i_op]) {  // サステイン前
          if(ct < dt[i_op])  // アタック中
            val = 1 - (dt[i_op] - ct) * (ar[i_op] * 1000);
          else  // ディケイ中
            val = 1 - (ct - dt[i_op]) * (dr[i_op] * 1000);
        }
        else {  // サステイン中
          val = sl[i_op] - (ct - st[i_op]) * (sr[i_op] * 1000);
        }
        env[i_op].gain.setValueAtTime(val, ct);
        let time = val / rr[i_op];
        env[i_op].gain.linearRampToValueAtTime(0, ct + time / 1000);
        to_id[i_op] = setTimeout(clear_auto, time, i_op);
      }
    }
    else {
      env[i_op].gain.setValueAtTime(0, 0);
    }
  }
  et[0] = 0;
}

function clear_auto(i_op) {
  env[i_op].gain.cancelScheduledValues(0);
  env[i_op].gain.value = 0;  // Opera で必要
  to_id[i_op] = undefined;
}

  const osc = new Array();
  osc.length = 4;
  const env = new Array();
  env.length = 4;
  const out = new Array();
  out.length = 4;
  const mod = new Array();
  mod.length = 4;
  const dif = new Array();
  dif.length = 4;
  const mod2 = new Array();
  mod2.length = 4;
  let feedback;

  const aud_ctx = new AudioContext();
  with(aud_ctx) {
    // オペレータ作成
    for(let i_op = 0; i_op < 4; i_op++) {
      osc[i_op] = createOscillator();
//    osc[i_op].start();
      env[i_op] = createGain();  // エンベロープ作成用
      env[i_op].gain.value = 0;
      osc[i_op].connect(env[i_op]);
      out[i_op] = createGain();  // 出力レベル用
      env[i_op].connect(out[i_op]);
      mod2[i_op] = createGain();  // 周波数変位調整用
      mod2[i_op].connect(osc[i_op].frequency);
      dif[i_op] = createIIRFilter([1.0, -1.0], [1.0]);
      dif[i_op].connect(mod2[i_op]);
      mod[i_op] = createGain();  // 変調レベル用
      mod[i_op].connect(dif[i_op]);
    }

    // オペレータ 1 フィードバック
    // 循環する接続を作る場合は,間に DelayNode を挟む必要がある
    feedback = createDelay();
    feedback.connect(mod[0]);

    out[3].connect(destination);
  }

  const ar = new Float64Array(4);  // アタック レート
  const dr = new Float64Array(4);  // ディケイ レート
  const sr = new Float64Array(4);  // サステイン レート
  const rr = new Float64Array(4);  // リリース レート
  const sl = new Float64Array(4);  // サステイン レベル

  const dt = new Float64Array(4);  // ディケイ開始時刻
  const st = new Float64Array(4);  // サステイン開始時刻
  const et = new Float64Array(4);  // サステインで出力がゼロになる時刻

  et[0] = 0;
  const to_id = [undefined, undefined, undefined, undefined];

  const elem_alg = document.getElementById("alg");
  const elem_fig = document.getElementById("fig");

  const elem_freq = new Array();
  elem_freq.length = 4;
  const elem_out = new Array();
  elem_out.length = 4;
  const elem_mod = new Array();
  elem_mod.length = 4;

  const elem_ar = new Array();
  elem_ar.length = 4;
  const elem_dr = new Array();
  elem_dr.length = 4;
  const elem_sr = new Array();
  elem_sr.length = 4;
  const elem_rr = new Array();
  elem_rr.length = 4;
  const elem_sl = new Array();
  elem_sl.length = 4;

  for(let i_op = 0; i_op < 4; i_op++) {
    let str_i = String(i_op);

    (elem_freq[i_op] = document.getElementById("freq" + str_i)).onkeypress = check_float;
    (elem_out[i_op] = document.getElementById("out" + str_i)).onkeypress = check_float;
    (elem_mod[i_op] = document.getElementById("mod" + str_i)).onkeypress = check_float;

    (elem_ar[i_op] = document.getElementById("ar" + str_i)).onkeypress = check_float;
    (elem_dr[i_op] = document.getElementById("dr" + str_i)).onkeypress = check_float;
    (elem_sr[i_op] = document.getElementById("sr" + str_i)).onkeypress = check_float;
    (elem_rr[i_op] = document.getElementById("rr" + str_i)).onkeypress = check_float;
    (elem_sl[i_op] = document.getElementById("sl" + str_i)).onkeypress = check_float;
  }

  let osc_ini = true;

  let elem_ul;
  let file_reader;

  // ページを再ロードしたときのため
  document.forms[0].reset();

  elem_alg.selectedIndex = 0;
  alg_sel();

//-->
</SCRIPT>

</BODY>

</HTML>