はじめに

ユービーセキュアでWebエンジニアとしてどかっと開発に携わっている田中です。弊社はWebセキュリティの会社ですが、本記事では前2回の記事『Web Audio APIでシンセサイザーを作って遊ぶ(前編)』『Web Audio APIでシンセサイザーを作って遊ぶ(中編)』の総括として、プログラムに音楽を演奏させてみます。まずは音楽を記述することについて簡単に述べ、音楽表記用の言語を実装します。前の記事まではプログラムの中にデータを書き込んで開発者コンソールの中で音を鳴らしました。しかし持続音を鳴らすだけではちょっぴりさみしいので、メロディを奏でたり複数の音を重ねてより音楽っぽいものを演奏できるようにしたいです。そこでマルチトラックの音楽記述言語を読み込むことでAudioNodeのグラフを自動構築し、生の周波数や時間を触らなくてよい、より音楽記述に向いたテキスト表現を実装します。

この記事の目標 (この記事でつくるもの)

この記事では、簡単なシンセサイザープログラムを製作して遊びます。ブラウザのWeb Audio APIを用いて簡単なシンセサイザーを作って遊びます。遊びます!

が、その前に。まずリズムの登場です。前記事までは音は鳴ったら鳴らしっぱなしの状態で説明しますが、ここでは時間経過に沿って音の高さを変化させたり、あるいは音を切ったりすることについて考察し、中編の内容と合わせてメロディを鳴らすプログラムをつくります。

そして、シンセサイザープログラムの製作に入ります。完成品のリポジトリはこちら (GitHub)です。ここで作るシンセサイザーは3つのパートで構成されます:

    1. 音長や音高を計算する部分
    2. 音楽記述言語をパースする部分
    3. 音の発生タイミングを設定する部分

まず1で、音名情報から周波数を計算したり、音長から秒換算での時間を計算するなどのユーティリティを実装します。ここでは中編で述べた周波数の計算方法が再登場します。

つぎに2で、音楽を記述する言語からその意味するところを抽出する処理を書きます。ここではよく知られた音楽記述言語であるMML (Music Macro Language) ふうのテキスト言語を定義し、それをパースします。

最後に3で、1と2で作ったコードを結合しプログラムを完成させます。ついでにUIをHTMLで実装し、記述した言語で音楽を鳴らして遊べるようにします。

リズムをとる

というわけでまずはリズムの登場です。前節までは音は鳴ったら鳴らしっぱなしの状態で説明しましたが、ここでは時間経過に沿って音の高さを変化させたり、あるいは音を切ったりすることについて考察します。これによってメロディを鳴らすことができるようになるわけです。

音符とテンポと時間について

Black music note button on white keyboard

メロディとはもっとも単純には、時間が刻々と進むになかで音の高さが変わったり、音が鳴ったり止まったりすること、と捉えることができるでしょう。音楽の授業ででてきた楽譜を想像してください。楽譜の上には、さまざまな種類の音符がに並べられています。このうち縦方向の位置が音の高さを表していることはご存知の通りです。では横方向はというと、音符のある位置がその音の開始位置です。終了位置は、開始位置がわかっていることと、音符の種類つまり音の長さが決まっていることから導き出せます。1小節分の長さを全音符で表現し、長さが半分になると2分音符、さらに半分で4分音符、……といった具合です。音符の並びを先頭から読んでいくことで、逐一音の開始と長さが指示されることによって、いつ音を鳴らし、いつ音を止めればよいかがわかるというしくみになっています。

では、4分音符1個分の時間は何秒間でしょうか。楽譜は音楽の速さが違っても同じ記法が使えるよう、「小節の何分の一」といった相対的な長さをもつ音符で記述します。ですが、テンポ (beats per second) というものがあります。テンポは演奏の速さを指定するもので「4分音符を1分間にいくつ打つか」の数で表現されます。テンポが120のとき、4分音符1個分の長さは60秒 / 120回 = 0.5秒となります。このように、テンポを指定することですべての音符の実際の長さを決めることができます。

後の節ではこのことを利用して、楽譜ではないテキスト言語で実際にメロディを記述します。

例: メトロノームを実装する

指定の時間にイベント処理を実行するには本来、OSのイベントタイマ等を用いて定期的にイベントの有無を調べてあったら対応する処理を実行するというプログラミングが必要なのですが、Web Audio APIではイベント処理がわりと簡単に実現できるようになっています。AudioParamのsetValueAtTime()メソッドは、AudioParamの値を変えるメソッドなのですが、なんと値を変える時間 (AudioContextを開始してからの秒数) を指定することができます。

ここではこのAudioParam.setValueAtTime()を用いてメトロノームを作ってみることで、イベント処理の基本を学びましょう。

let ctx = new AudioContext()
// オシレータ
let osc = new OscillatorNode(ctx)
osc.frequency.value = 440
// 音量制御用のノード
let gain = new GainNode(ctx)
gain.gain.value = 0

// 接続してオシレータを開始
osc.connect(gain)
gain.connect(ctx.destination)
osc.start()

// テンポに従って音の鳴り始める(音量が0.3になる)時間と鳴り終わる(音量が0になる)時間を設定する
let bpm = 120
let note_length = 60 / bpm
// 120回分のメトロノームの音を設定する
for (let n = 0; n < 120; n++) {
    // 音の開始・終了時間を計算する
    let start_time = n * note_length;
    let end_time = start_time + 0.05
    // gain (音量)を時間指定で設定することで鳴らしたり止めたりする
    gain.gain.setValueAtTime(0.3, ctx.currentTime + start_time)
    gain.gain.setValueAtTime(0.0, ctx.currentTime + end_time)
    // 小節の最初の音だけ高くする
    if (n % 4 == 0) {
        osc.frequency.setValueAtTime(880, ctx.currentTime + start_time)
    } else {
        osc.frequency.setValueAtTime(440, ctx.currentTime + start_time)
    }
}

これでリズムを刻むことができるようになり、音楽を記述する準備が整いました。

音楽を記述する

さて、いよいよタイトルにあるようにシンセサイザーを作って遊んでいきます。音楽をテキストで記述するためには何かしらの言語をつくる必要がありますが、ここではいわゆるMML (Music Macro Language)ふうの言語を実装しそれを解釈・演奏するという方針をとります。ただしMMLをまじめにパースしようと思うとパーサのコード量が増えて大変なので、本記事ではパーサを超簡単にするためトークンを空白区切りで並べるMMLっぽい言語 (以下、本記事プログラム用の言語をMMLと呼びます) を用意します。

インターフェースを決める

言語の設計に入る前に、これからつくるプログラムの構成を設計します。プログラムはHTMLファイルとJavaScriptファイルで構成され、JavaScriptファイルはMMLの解釈や再生開始に関するAPIを公開する方針とします。そのAPIをHTMLで書いたUIから叩いて使う、という設計にします。

本プログラムは以下4つのAPIを公開します。

    • parse(str): MML文字列をパースし、AST (Abstract Syntax Tree) を返す
    • make_player(ast): ASTに基づいてAudioNodeのグラフを構成し、プレイヤーオブジェクトを返す
    • play(player): プレイヤーオブジェクトを再生開始する
    • stop(player): プレイヤーオブジェクトを停止する

make_player()の返り値はAudioContextそのままでもよいのですが、今後の拡張のことを考えてプレイヤーオブジェクトの中にAudioContextの中に入れるという作りにしようと思います。

構文の定義

ここでは音楽を記述するのに用いる言語のことを考えていきます。まず、盛り込みたい要素を整理してから実際の構文を考えていくことにします。

要素の整理

最初に、必要な要素を整理します。

まず音符 (高さと長さ)の指定をできる必要があります。音高の指定には音名の英語表記 (cdefgabc = ドレミファソラシド)とオクターブの表記を同時に記述し、オクターブ部分は省略できません (本物のMMLではできる)。MIDIノートナンバーの69、つまり440Hzのラの音をオクターブ数4とします。また、ドのシャープなど半音上げ下げを表現するためにシャープとフラットのような記号も用います。休符 (発音しないが時間は進む) も用意しておく必要があるでしょう。音の長さは4で4分音符、2で2分音符というように音符の数字で指定します。

また複数の音を同時に鳴らしたいので「トラック」という概念を導入します。あるトラックでは同時に1音しか鳴りませんが複数のトラックを用意することで同時に鳴らすことができる、というものです。これからつくる言語ではトラックの境界を指定する構文を導入することでトラックを表現します。

パラメータも指定できると表現の幅が増えてうれしいです。システム全体のパラメータとして考えられるのは、とりあえずテンポのみとしておきましょう。各トラック中に仕込めるパラメータとしてパン(ステレオ定位)と音量、そして音高に加算できるピッチを周波数で指定できるようにします。ピッチを指定できると擬似的なビブラートなどの表現が可能になります。

構文

これからつくる言語は記事用のものですので、パースをとても簡単にしたいです。なので、要素は空白文字 (半角スペースと改行文字)で必ず区切ることにします。空白で切ったあとのそれぞれのトークンが各要素のどれかを表現している、というふうにします。

要素 構文 (正規表現) 説明
音符情報 [cdefgab][0-9][+-]?:[0-9]+ 最初のアルファベットは音名。次の数字でオクターブを指定し、オプションで+-をつけて半音上下させる。:の後ろは音の長さを「4分音符」の「4」などの数値で指定する。
休符または同音の音符 [r=]:[0-9]+ 最初の記号で休符か同音の音符かが分かれる。rの場合は休符、=の場合は直前の音符と同じ音高の音になる。:の後ろは音の長さを指定する。
トラック区切り --- トラックの境界を表す。前のトラック区切り (あるいは文字列先頭) からこの記号の直前まであるいは文字列終端までが1つのトラックになる。
パラメータ設定 @[^=@]+=-?[0-9]+(.[0-9]+) @param=123のように書く。=の前はパラメータの名前、=以降は設定値。

次にパラメータの名前と意味を記しておきます。

パラメータ名 説明
sys.bpm テンポを設定する。
pan -1.0 ~ 1.0でトラックのパンを設定する。-1.0で左、0が中心、1.0で右。
volume 0.0 ~ 1.0でトラックの音量を設定する。
pitch 浮動小数点でピッチを指定する。単位は(実装の簡単化のため)周波数であることに注意。

これでパーサを書く準備が整いました。

パーサを書く

ここでのパーサはUIから送られてきた文字列から音楽に関する情報を抜きだすものです。文字列で表現された音楽記述言語で表現できる情報にはあるトラックを構成する時間軸上の情報と、それらをまとめた各トラックの情報があります。JavaScriptの記法でイメージを書くならば、取り出した情報は以下のような感じになるでしょう:

[
  # 1つめのトラック
  [
    # 1つめの情報 (音符)
    {
      "type": "note",
      "note_name": "c",
      "octave": 4,
      "half_note": "+",
      "length": 4,
    },
    # 2つめの情報 (パラメータ設定)
    {
      "type": "param",
      "name": "volume",
      "value": 0.4,
    },
    ...
  ],
  # 2つめのトラック
  [
    ...
  ],
  ...
]

個々の情報にはその種類を表すtypeスロットを付けておきます。

情報を抽出するためには以下の手順を踏みます:

      1. 文字列を各情報の塊に分割する
      2. 塊の中身を吟味する

幸いここで扱う音楽記述言語は「各情報を空白文字(と改行文字)で区切る」のでした。運がよかったですね。また、各情報の文字列記述はなぜか正規表現で構造が与えられています。ということは、対応する正規表現にマッチするならその正規表現のグループには情報の詳細が入っている、ということになります。運がよかったです。つまり、上の情報抽出手順をプログラムの内容に言い換えるとこのようになります。

      1. 文字列を空白文字でString.prototype.split()し、空の文字列は取り除く
      2. 得られた文字列配列の要素についてループし、文字列から正規表現マッチで情報を取り出す

まず1を行うコードですがstrに入力の文字列が入っているとして、これでできます。

let tokens = str.split(/[ \n]+/).filter((s) => s.length !== 0);

tokensに入っている各トークンについてループを回し、以下のコードでトークンの種類を判別します (syntax_xxxには前節で述べた正規表現が入っています)。

const syntax_type = (token) => {
  if (syntax_note.test(token)) {
    return 'note';
  } else if (syntax_rest.test(token)) {
    return 'rest';
  } else if (syntax_track.test(token)) {
    return 'track_separator';
  } else if (syntax_param.test(token)) {
    return 'param';
  } else {
    return undefined;
  }
};

判別した結果によってswitchし、情報を取り出した結果をJSのオブジェクト型に格納して配列にまとめます (コードはindex.jsparse_tokens()を参照)。

そうして得られた配列はトークンのパース結果の配列でありトラックごとに分離されていません。そこで以下のようにtoken.type"track_separator"のところで配列を切って、トラックに分離します。

// Split tokens into arrays by track_separators.
const split_into_tracks = (tokens) => {
  let tracks = [[]];

  for (let token of tokens) {
    if (token.type === 'track_separator') {
      tracks.push(new Array());
    } else {
      tracks[tracks.length - 1].push(token);
    }
  }

  return tracks;
};

これでトラックを含め情報を抽出することができました。

AudioNodeのグラフを構成する

つぎに、トラック情報からAudioNodeのグラフを構成し、言語で記述できるイベントのスケジューリングも行います。

各トラックにはpanやvolumeなどの設定値があるのでこれを制御するためのノードを繋ぐ必要があります。以下のようなイメージです。

              / [ トラックの音量 ] ← [ トラックのパン ] ← [ オシレータの音量 ] ← [ トラックのオシレータ ]
[ スピーカー ] ── [ トラックの音量 ] ← [ トラックのパン ] ← [ オシレータの音量 ] ← [ トラックのオシレータ ]
              \ [ トラックの音量 ] ← [ トラックのパン ] ← [ オシレータの音量 ] ← [ トラックのオシレータ ]

処理としては以下のようにします。

  • トラックについてループ
    • トラックのノードを初期化し結線
    • トラック内の時間等を初期化
    • トラック内のイベントについてループ
      • 現在の時間に基づいて音量やイベントを対象ノードに設定

現在の時間に基づいて音量やイベントを対象ノードに設定の部分はAudioParam.setValueAtTime()を用いて時間つきで設定します。こうすることでイベントの発生時間を指定しておけばWeb Audio APIのほうでススケジューリングしてくれるのでイベントスケジューリングについてなにも考えなくてよいです。イベントを処理するコードの例としてトークンがnoteの場合の処理を見てみましょう。

      switch (token.type) {
        case 'note':
          let note_number = calculate_note_number(token.note_name, token.octave, token.half_note);
          freq = pitch + convert_to_frequency(note_number);
          len = calculate_length(token.length, player.bpm);

          // Set osc sound on.
          osc.frequency.setValueAtTime(freq, ctx.currentTime + time);
          osc_gain.gain.setValueAtTime(1, ctx.currentTime + time);

          // Set osc sound off.
          time += len;
          osc_gain.gain.setValueAtTime(0, ctx.currentTime + time);
          break;

音符 (note) の場合音名などから周波数を計算する必要がありますのでcalculate_note_number()convert_to_frequency()などこれまでに登場したユーティリティを用いて計算します。また音符の場合はその長さが秒換算で必要なのでこれもcalculate_length()。そののちに音を鳴らす瞬間に音高を設定し音量を1にします。音符の時間だけ経過したところで音量を0に設定することで、音符の処理を実現します。詳細についてはコードのmake_player()関数の定義をご確認ください。

ユーザインターフェースをつくる

最後にUIですが、これはテキストエリアと再生、停止ボタンを配置するだけの至極シンプルなものを用意します。

<html>
  <head>
    <title>MML Player</title>
  </head>
  <body>
    <h1>MML Player</h1>
    <textarea id="mmlcode" rows=15 cols=80></textarea><br>
    <button id="mmlplay">play</button>
    <button id="mmlstop">stop</button>
    <p id="mmlconsole">error messages here...</p>
    <script src="./index.js"></script>
  </body>
</html>

index.jsの中でボタンクリック時のイベントを再生と停止用の関数に結び付けておきます。

// Configure event handlers
document.getElementById('mmlplay').addEventListener('click', play_button);
document.getElementById('mmlstop').addEventListener('click', stop_button);

これで完成です!

MML Player
UIのスクリーンショット

利用例

ここまでで実装してきたMMLプレイヤーを実際に利用してみましょう。JSとHTMLのみですのでリポジトリのGitHub Pagesで利用できるようにしておきました。このリンクのページのテキストエリアに以下のMMLコードを貼り付けてplayボタンを押すと曲が鳴ります。!

@sys.bpm=100

@volume=0.3
f5:4 @volume=0.1 =:8 @volume=0.3 g5:8 f5:4 d5:2 d5:8 @volume=0.1 =:16 @volume=0.3 r:16 
f5:4 @volume=0.1 =:8 @volume=0.3 g5:8 f5:4 d5:2 d5:8 @volume=0.1 =:16 @volume=0.3 r:16 

c6:4 =:8 @volume=0.1 =:16 =:32 r:32 @volume=0.3
c6:4 a5:4 =:8 @volume=0.1 =:8 r:4 @volume=0.3
a5+:4 =:8 @volume=0.1 =:16 =:32 r:32 @volume=0.3
a5+:4 f5:4 =:8 @volume=0.1 =:8 r:4 @volume=0.3

g5:4 @volume=0.1 =:8 r:8 @volume=0.3 =:4 a5+:4 @volume=0.1 =:8 @volume=0.3 a5:8 g5:8 @volume=0.1 =:8 @volume=0.3
f5:4 @volume=0.1 =:8 @volume=0.3 g5:8 f5:4 d5:2 d5:8 @volume=0.1 =:16 @volume=0.3 r:16 
g5:4 @volume=0.1 =:8 r:8 @volume=0.3 =:4 a5+:4 @volume=0.1 =:8 @volume=0.3 a5:8 g5:8 @volume=0.1 =:8 @volume=0.3
f5:4 @volume=0.1 =:8 @volume=0.3 g5:8 f5:4 d5:2 d5:8 @volume=0.1 =:16 @volume=0.3 r:16 

c6:4 @volume=0.1 =:8 r:8 @volume=0.3 =:4 d6+:4 @volume=0.1 =:8 @volume=0.3 c6:8 a5:8 @volume=0.1 =:8 @volume=0.3
a5+:4 @volume=0.2 =:4 @volume=0.1 =:4 @volume=0.3 d6:4 @volume=0.2 =:4 @volume=0.1 =:4 @volume=0.3

a5+:8 @volume=0.1 =:8 @volume=0.3 f5:8 @volume=0.1 =:8 @volume=0.3 d5:8 @volume=0.1 =:8 @volume=0.3
f5:4 @volume=0.1 =:8 @volume=0.3 d5+:8 c5:8 @volume=0.1 =:8
@volume=0.3 a4+:2 @volume=0.2 =:4 @volume=0.1 =:4 

---

@volume=0.3
a2+:32 r:32 r:16 r:8 a2+:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
a2+:32 r:32 r:16 r:8 a2+:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
a2+:32 r:32 r:16 r:8 a2+:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
a2+:32 r:32 r:16 r:8 a2+:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
d2+:32 r:32 r:16 r:8 d2+:32 r:32 r:16 r:8 r:8 c2:32 r:32 r:16
f2:32 r:32 r:16 r:8 f2:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
a2+:32 r:32 r:16 r:8 a2+:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
a2-:32 r:32 r:16 r:8 a2-:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16

d2+:32 r:32 r:16 r:8 d2+:32 r:32 r:16 r:8 r:8 a1+:32 r:32 r:16
d2+:32 r:32 r:16 r:8 d2+:32 r:32 r:16 r:8 r:8 a1+:32 r:32 r:16
a2+:32 r:32 r:16 r:8 a2+:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
a2+:32 r:32 r:16 r:8 a2+:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
d2+:32 r:32 r:16 r:8 d2+:32 r:32 r:16 r:8 r:8 a1+:32 r:32 r:16
d2+:32 r:32 r:16 r:8 d2+:32 r:32 r:16 r:8 r:8 a1+:32 r:32 r:16
a2+:32 r:32 r:16 r:8 a2+:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
a2+:32 r:32 r:16 r:8 a2+:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16

d2+:32 r:32 r:16 r:8 d2+:32 r:32 r:16 r:8 r:8 c2:32 r:32 r:16
f2:32 r:32 r:16 r:8 f2:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
a2+:32 r:32 r:16 r:8 a2+:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
a2-:32 r:32 r:16 r:8 a2-:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
a2+:32 r:32 r:16 r:8 a2+:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
f2:32 r:32 r:16 r:8 f2:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
a2+:32 r:32 r:16 r:8 a2+:32 r:32 r:16 r:8 r:8 f2:32 r:32 r:16
a2+:32 r:32 r:16 r:8 a2+:32 

---

@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32

@pan=0.2 @volume=0.2 d6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 g6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d7+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 g6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 c7:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d7+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 c7:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6-:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32

@pan=0.2 @volume=0.2 f5:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 g6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 g6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f5:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 g6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 g6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32

@pan=0.2 @volume=0.2 f5:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 g6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 g6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f5:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 g6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 g6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32

@pan=0.2 @volume=0.2 d6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 c7:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 c7:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d7+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 c7:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32

@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6-:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32

@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 c6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 c6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32

@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a6+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 f6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 d6:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32

@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a4+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32
@pan=0.2 @volume=0.2 a5+:128 r:128 r:64 r:32 @pan=-0.1 @volume=0.05 =:128 r:128 r:64 r:32

メリークリスマス!

拡張

さて、基本的な機能をひととおり実装してみましたが、記事に出てきた内容がすべて実装されているわけではありません。あるいは触っていて何か機能の不足を感じることがあるかもしれません。そのようなときは改造して、あるいは新規に設計して楽しみましょう! 以下にありうる機能拡張を書き記しておきます。

        • ADSRエンベロープを実装・MMLで指定可能にし、音量変化によりやわらかな音等を実現する
        • オシレータの波形選択機能をMMLより指定可能にする
        • AM変調やFM変調、フィルターをパラメータも含めてMMLより指定可能にする
        • 現在演奏されている音を表示する機能を実現する (TiMidity++の鍵盤表示のように)
        • 再生情報等を表示する
        • テキストで譜面を入力したくない! ピアノロールを実装してやる!

これらの実装は読者への課題とする。

おわりに

本記事ではこれまでの総括として音楽を記述し解釈実行することを考え、ブラウザで動く音楽演奏プログラムを作成しました。また前編・中編・後編を通してシンセサイザーのしくみやそれをWeb Audio APIを用いて実現する方法を述べてきました。この記事がシンセサイザーやコンピュータ上で行う音楽への興味をもつきっかけになれたら幸いです。

ここまでお付き合いいただいたみなさまへ、やられたらやりかえす、感謝の1000倍返しをお送りいたします。それがぼくのモットーなんでね。