Web Audio API でズンドコキヨシ

プログラマの間で流行っている「ズンドコキヨシ」実装大会を見て、前からやりたいと思ってた Web Audio API の勉強も兼ねて挑戦してみた、というはなしです。
デモを見たい人は以下が最終的なコンテンツです。

* [Web Audio API 版 ズンドコキヨシ](http://kichiki.github.io/misc/kiyoshi/zdk.html)
* [キ・ヨ・シ！マシーン](http://kichiki.github.io/misc/kiyoshi/km.html) - リズムマシーン的に、ボタンでズンドコできます
* [キ・ヨ・シ！マシーン Version 2](http://kichiki.github.io/misc/kiyoshi/km2.html) - PCのキーボードでもズンドコできるように改良

スクリーンショット
![zdk-50.png](https://qiita-image-store.s3.amazonaws.com/0/105522/c0f95673-f3db-debf-c859-77e624798580.png) ![km-50.png](https://qiita-image-store.s3.amazonaws.com/0/105522/4680cd6a-c12f-117e-0228-19b5e757ef9b.png)

キ・ヨ・シ！マシーンは習作ですが、これも結構おもしろい。特に Version 2 は（連打できるし）。

# 0. はじめに
会社で「ズンドコキヨシ」がすごい、という話を聞いた。
Qiita のまとめのページ「[ズンドコキヨシまとめ](http://qiita.com/shunsugai@github/items/971a15461de29563bf90)」があった。
そこにあった学生さん（この人が、ズンドコキヨシの創始者だね）の[ツイート](https://twitter.com/kumiromilk/status/707437861881180160)が、すてきだった。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Javaの講義、試験が「自作関数を作り記述しなさい」って問題だったから<br>「ズン」「ドコ」のいずれかをランダムで出力し続けて「ズン」「ズン」「ズン」「ズン」「ドコ」の配列が出たら「キ・ヨ・シ！」って出力した後終了って関数作ったら満点で単位貰ってた</p>&mdash; てくも (@kumiromilk) <a href="https://twitter.com/kumiromilk/status/707437861881180160">2016年3月9日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

コンセプトは分かった。「ズン」と「ドコ」というデジタルな組み合わせと、完成時の「キ・ヨ・シ！」の歓喜のコール、このコンビネーションが絶妙だなと、元ネタの曲を知らないオレでも思った。

この段階で、しかし、素朴な疑問が残った。なんでパターンが５つ組なのか？ということ。音楽で、音頭で、４じゃなくて、５？と。

Qiita のまとめのページにあるいくつかの実装を試してみた。
ruby のコードだったかな、実行したら、ダッと試行過程がコンソールに出力されて、最後が（当然）「キ・ヨ・シ！」となっていた。

うん、コンセプトは分かった。

# 1. きよしのズンドコ節とは
氷川きよしは知っていた（名前は、ということだけど）。

ググったら Youtube のビデオが出てきた。黄色いスーツで踊ってるビデオ。
[youtube: きよしのズンドコ節 / 氷川きよし](https://www.youtube.com/watch?v=c0H_qGSJKzE)

これを見て、５つ組（ズン、ズン、ズン、ズン、ドコ）の謎が解けた。つまり「キ・ヨ・シ！」まで含めて８拍、最初のズンと２つ目のズンの間に一拍ある。だからこれ
は、
>ズン、ズン、ズン、ズン、ドコ

じゃなくて、
>ズン、**ウッ**、ズン、ズン、ズン、ドコ、キ・ヨ・シ！

じゃないか、と思った。

Qiita の実装例を見てたら、コンソールへの出力だけど、リズム感を保ったバージョンがあった！
[Qiita: リズムに乗ってズンドコキヨシ♪ with Ruby](http://qiita.com/Kta-M/items/c5ea66ddd0e4123a298c)
これだよね、これ。

でもなにか物足りない。

# 2. 音付き「ズンドコキヨシ」が欲しい
要するにぼくが求めていたのは、「ズン」と「ドコ」のランダムなパターンを、実際に歌わせたい、ということみたい。

今の時代なら、ボーカロイドに歌わせるとか、音声合成させるとかあるんだろうけど、自分のスキルから遠いのでパス。

サンプルファイルを作っておいて、C とか C++ とかで native に audio device を叩くという泥臭いアプローチは、やればできるだろうけど、面白く無いのでパス。

サンプルファイルを使って、前から興味があったけど実際に触る機会がなかった Web Audio API で作れば、ブラウザがあれば誰でも動かせるし、これで行くことに決定！

# 3. ズンドコ・サンプルの作成
先に見つけた、黄色いスーツで踊るきよしの Youtube ビデオを素材にする。

まず [youtube-dl](https://rg3.github.io/youtube-dl/) で動画ファイルをゲット
`% youtube-dl c0H_qGSJKzE`

次に mplayer でオーディオストリームをゲット
`% mplayer -vo null -ao pcm:fast:file=kiyoshi.wav きよしのズンドコ節\ _\ 氷川きよし-c0H_qGSJKzE.mp4`

次に sox でパーツに分割
まず、1st コーラス全体を抜き出した。
`% sox kiyoshi.wav kiyoshi_zun1.wav trim 17.8 9`
それを、ズン、ウッ、ズン、ズン、ズン、ドコ、キ・ヨ・シ！に分割
`% sox kiyoshi_zun1.wav kiyoshi_zun1_1.wav trim 0 0.525`
`% sox kiyoshi_zun1.wav kiyoshi_zun1_2.wav trim 0.525 0.525`
`% sox kiyoshi_zun1.wav kiyoshi_zun1_3.wav trim 1.05 0.525`
`% sox kiyoshi_zun1.wav kiyoshi_zun1_4.wav trim 1.575 0.525`
`% sox kiyoshi_zun1.wav kiyoshi_zun1_5.wav trim 2.1 0.525`
`% sox kiyoshi_zun1.wav kiyoshi_zun1_6.wav trim 2.625 0.525`
`% sox kiyoshi_zun1.wav kiyoshi_zun1_78.wav trim 3.16 1.05`

# 4. Web Audio API で wav ファイルの再生
素材はできたので、まず、それらを使って、Web Audio API で、ブラウザ上で再生してみよう。

何も知らないので、ググって情報収集。

- [Qiita: WebAudioAPIで遊べるようになった](http://qiita.com/fnobi/items/2f08a67800dec1d61f21)
- [phiary: Web Audio API で音を再生しよう](http://phiary.me/webaudio-api-getting-started/)
- [HTML5Rocks: Web Audio API の基礎 By Boris Smus](http://www.html5rocks.com/ja/tutorials/webaudio/intro/)

一通り目を通した後、結局、「[phiary: Web Audio API で音を再生しよう](http://phiary.me/webaudio-api-getting-started/)」のコピペで、フランケンシュタインなコードができた。

```js:km.js
window.AudioContext = window.AudioContext || window.webkitAudioContext;  
var context = new AudioContext();


var getAudioBuffer = function(url, fn) {  
  var request = new XMLHttpRequest();
  request.responseType = 'arraybuffer';

  request.onreadystatechange = function() {
    if (request.readyState === 4) {
      if (request.status === 0 || request.status === 200) {
        context.decodeAudioData(request.response, function(buffer) {
          fn(buffer);
        });
      }
    }
  };

  request.open('GET', url, true);
  request.send('');
};


var playSound = function(buffer) {  
  var source = context.createBufferSource();
  source.buffer = buffer;
  source.connect(context.destination);
  source.start(0);
};


// main
window.onload = function() {

  getAudioBuffer('kiyoshi_zun1_1.wav', function(buffer1) {
    sample1 = buffer1;
    var btn1 = document.getElementById('btn1');
    btn1.onclick = function() {
      playSound(buffer1);
    };
  });
  getAudioBuffer('kiyoshi_zun1_2.wav', function(buffer2) {
    sample2 = buffer2;
    var btn2 = document.getElementById('btn2');
    btn2.onclick = function() {
      playSound(buffer2);
    };
  });
  getAudioBuffer('kiyoshi_zun1_3.wav', function(buffer3) {
    sample3 = buffer3;
    var btn3 = document.getElementById('btn3');
    btn3.onclick = function() {
      playSound(buffer3);
    };
  });
  getAudioBuffer('kiyoshi_zun1_4.wav', function(buffer4) {
    sample4 = buffer4;
    var btn4 = document.getElementById('btn4');
    btn4.onclick = function() {
      playSound(buffer4);
    };
  });
  getAudioBuffer('kiyoshi_zun1_5.wav', function(buffer5) {
    sample5 = buffer5;
    var btn5 = document.getElementById('btn5');
    btn5.onclick = function() {
      playSound(buffer5);
    };
  });
  getAudioBuffer('kiyoshi_zun1_6.wav', function(buffer6) {
    sample6 = buffer6;
    var btn6 = document.getElementById('btn6');
    btn6.onclick = function() {
      playSound(buffer6);
    };
  });
  getAudioBuffer('kiyoshi_zun1_78.wav', function(buffer78) {
    sample78 = buffer78;
    var btn78 = document.getElementById('btn78');
    btn78.onclick = function() {
      playSound(buffer78);
    };
  });
};
```

（１つ１つバッファーに読み込み、イベント作ってるのは、みっともないが。）

名付けて「[キ・ヨ・シ！マシーン](http://kichiki.github.io/misc/kiyoshi/km.html)」。
イメージはドラム・マシーンというかドラム・パッド。
「ズン」「ウッ」「ズン」「ズン」「ズン」「ドコ」「キ・ヨ・シ！」と７つのボタンからなる。２拍目のブラスも入れて、１、３、４、５拍それぞれの「ズン」も独立に（実際、３拍、４拍は音程も違うし、３拍目の裏声っぽいのはポイントだろう）。

# 5. Web Audio API でズンドコキヨシ
やっと本題の「ズンドコキヨシ」。
７つのサンプルを Web Audio API で鳴らすことはできたので、次は、時間制御と、乱数での処理。

JS での乱数は、芸はないけど Math.random() でいいかな。 cf. [w3schools.com: JavaScript random() Method](http://www.w3schools.com/jsref/jsref_random.asp)

時間制御は先の[HTML5Rocks](http://www.html5rocks.com/ja/tutorials/webaudio/intro/)の「時間の管理: リズムに同期して音を再生する」セクションのコピペで行く。

すると、さっき作ったボタンで再生するコードとの整合性がまずくて、つまり、サンプルの入った buffer を global に持っておかないといけなくて、それどうすれば良いんだろうとか、素人らしく悩む。

現状、とりあえず動けば良い状態で、組んだ。

```js:zdk.js
window.AudioContext = window.AudioContext || window.webkitAudioContext;  
var context = new AudioContext();

var getAudioBuffer = function(url, fn) {  
  var request = new XMLHttpRequest();
  request.responseType = 'arraybuffer';

  request.onreadystatechange = function() {
    if (request.readyState === 4) {
      if (request.status === 0 || request.status === 200) {
        context.decodeAudioData(request.response, function(buffer) {
          fn(buffer);
        });
      }
    }
  };

  request.open('GET', url, true);
  request.send('');
};


var playSound = function(buffer, time) {
  var source = context.createBufferSource();
  source.buffer = buffer;
  source.connect(context.destination);
  source.start(time);
};


var sample1 = null;
var sample2 = null;
var sample3 = null;
var sample4 = null;
var sample5 = null;
var sample6 = null;
var sample78 = null;


// main
window.onload = function() {
  getAudioBuffer('kiyoshi_zun1_1.wav', function(buffer1) {
    sample1 = buffer1;
  });
  getAudioBuffer('kiyoshi_zun1_2.wav', function(buffer2) {
    sample2 = buffer2;
  });
  getAudioBuffer('kiyoshi_zun1_3.wav', function(buffer3) {
    sample3 = buffer3;
  });
  getAudioBuffer('kiyoshi_zun1_4.wav', function(buffer4) {
    sample4 = buffer4;
  });
  getAudioBuffer('kiyoshi_zun1_5.wav', function(buffer5) {
    sample5 = buffer5;
  });
  getAudioBuffer('kiyoshi_zun1_6.wav', function(buffer6) {
    sample6 = buffer6;
  });
  getAudioBuffer('kiyoshi_zun1_78.wav', function(buffer78) {
    sample78 = buffer78;
  });
};


var start = function() {
  // We'll start playing the rhythm 100 milliseconds from "now"
  var startTime = context.currentTime + 0.100;

  var tempo = 60; // BPM (beats per minute)
  var eighthNoteTime = (60 / tempo) / 2;

  var time = startTime;
  while (1) {
    var status = 0;
    r = Math.random();
    if (r < 0.5) {
	playSound(sample1, time);
    } else {
	// Doko
	playSound(sample6, time);
	status = 1;
    }

    playSound(sample2, time + 1 * eighthNoteTime);

    r = Math.random();
    if (r < 0.5) {
	playSound(sample3, time + 2 * eighthNoteTime);
    } else {
	// Doko
	playSound(sample6, time + 2 * eighthNoteTime);
	status = 1;
    }
    r = Math.random();
    if (r < 0.5) {
	playSound(sample4, time + 3 * eighthNoteTime);
    } else {
	// Doko
	playSound(sample6, time + 3 * eighthNoteTime);
	status = 1;
    }
    r = Math.random();
    if (r < 0.5) {
	playSound(sample5, time + 4 * eighthNoteTime);
    } else {
	// Doko
	playSound(sample6, time + 4 * eighthNoteTime);
	status = 1;
    }
    r = Math.random();
    if (r < 0.5) {
        // Zun
	playSound(sample5, time + 5 * eighthNoteTime);
	status = 1;
    } else {
	// Doko
	playSound(sample6, time + 5 * eighthNoteTime);
    }

    if (status == 0) {
	// KIYOSHI!
	playSound(sample78, time + 6 * eighthNoteTime);
	break;
    } else {
    }

    time += 8 * eighthNoteTime;
  }
};
```

[Web Audio API 版 ズンドコキヨシ](http://kichiki.github.io/misc/kiyoshi/zdk.html)（リンク先に飛んでも、突然、音がなったりはしませんので、安心してください。）

で、鳴らしてみた。結構いい。

ここまでの過程でも、いろいろ小細工を考えてた。例えば

- 「ドコ」も場所に応じで音程を付けるとか、
- ダメだった時の合いの手をどっかから持ってくるとか、
- イントロをつけるとか、

でも結果として、今のままの何もしないシンプルなものが結構いい。
これも氷川きよしのうまさなんだろうなぁ（よくわかってないけど）。

なかなか終わらないなーと飽きかけてきた時に決まって「キ・ヨ・シ！」ときた時の満足感は、なかなか。

# 6. 仕上げて Qiita デビュー
ここまでやって、Qiita デビューだ！と思って、あれこれ仕上げをする。

- Markdown で書いて、
- デモサイトにアップしないといけないなと思って、github のページにアップして、
- 生のボタン並べただけだとさすがになぁ、と思って CSS を（ググって調べて勉強して）書いて、
- 最初の[キ・ヨ・シ！マシーン](http://kichiki.github.io/misc/kiyoshi/km.html)をマウスでクリックするのもかったるいな、と思って、キーボードで叩ける[キ・ヨ・シ！マシーン Version 2](http://kichiki.github.io/misc/kiyoshi/km2.html)も書いた。

以下、CSS の追加と、キーボード対応について、説明しておこう。

## CSS で見た目を整える

基本、音出し優先だったので、ここまで HTML は button のみ。

```html:km-before.html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>kiyoshi-machine</title>
    <script src="km.js"></script>
  </head>

<body>

<button id="btn1">ズン</button>
<button id="btn2">ウン</button>
<button id="btn3">ズン</button>
<button id="btn4">ズン</button>
<button id="btn5">ズン</button>
<button id="btn6">ドコ</button>
<button id="btn78">キ・ヨ・シ！</button>

</body>
</html>
```

```html:zdk-before.html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>zun-doko-kiyoshi</title>
    <script src="zdk.js"></script>
  </head>

<body>

<button id="btn_start" onclick="start()">スタート</button>

</body>
</html>
```

見た目はご覧の通りだった。
![km-before-50.png](https://qiita-image-store.s3.amazonaws.com/0/105522/53a373bd-f0d1-dba7-5b7c-6217dcb57351.png) ![zdk-before-50.png](https://qiita-image-store.s3.amazonaws.com/0/105522/8b531c57-2115-011d-5836-b55234ea54eb.png)

さすがにこれはダメだろう、ということで、CSS を書く。
参考サイト：

- [CSSでフルスクリーン(全画面) ＋ 縦横中央配置レイアウトを作る【3ステップ + おまけ】](http://mae.chab.in/archives/2749)
- [position: absolute; の指定で要素が上下左右中央配置になる理由](https://hyper-text.org/archives/2014/08/position_absolute_center_layout.shtml)

結果、以下のような感じにまとめた。
![km-50.png](https://qiita-image-store.s3.amazonaws.com/0/105522/4680cd6a-c12f-117e-0228-19b5e757ef9b.png) ![zdk-50.png](https://qiita-image-store.s3.amazonaws.com/0/105522/c0f95673-f3db-debf-c859-77e624798580.png)

```css:km.css
body {
    width: 100vw;
    height: 100vh;
    color: #ccc;
    background-color: #000;
}

#btn1 {
    position: absolute;
    width: 32vw;
    height: 30vh;
    top: 1vh;
    left: 1vw;
    right: 33vw;
    bottom: 31vh;
    font-size: 7vh;
    color: #000;
    background-color: #ff0;
}
#btn2 {
    position: absolute;
    width: 32vw;
    height: 30vh;
    top: 1vh;
    left: 34vw;
    right: 66vw;
    bottom: 31vh;
    font-size: 7vh;
    color: #000;
    background-color: #ff0;
    font-size: 7vh;
    color: #000;
    background-color: #6f0;
}
#btn3 {
    position: absolute;
    width: 32vw;
    height: 30vh;
    top: 1vh;
    left: 67vw;
    right: 99vw;
    bottom: 31vh;
    font-size: 7vh;
    color: #000;
    background-color: #06f;
}
#btn4 {
    position: absolute;
    width: 32vw;
    height: 30vh;
    top: 32vh;
    left: 1vw;
    right: 33vw;
    bottom: 62vh;
    font-size: 7vh;
    color: #000;
    background-color: #aaf;
}
#btn5 {
    position: absolute;
    width: 32vw;
    height: 30vh;
    top: 32vh;
    left: 34vw;
    right: 66vw;
    bottom: 62vh;
    font-size: 7vh;
    color: #000;
    background-color: #0ff;
}
#btn6 {
    position: absolute;
    width: 32vw;
    height: 30vh;
    top: 32vh;
    left: 67vw;
    right: 99vw;
    bottom: 62vh;
    font-size: 7vh;
    color: #000;
    background-color: #aff;
}
#btn78 {
    position: absolute;
    width: 98vw;
    height: 30vh;
    top: 63vh;
    left: 1vw;
    right: 99vw;
    bottom: 93vh;
    font-size: 7vh;
    color: #000;
    background-color: #f06;
}

#credit {
    position: absolute;
    width: 99vw;
    height: 5vh;
    top: 94vh;
    left: 1vw;
    right: 99vw;
    bottom: 99vh;
    font-size: 3vh;
    color: #fff;
    background-color: #000;
}
```

```css:zdk.css
body {
    width: 100vw;
    height: 100vh;
    color: #ccc;
    background-color: #000;
}

#btn_start {
    position: absolute;
    width: 98vw;
    height: 92vh;
    top: 1vh;
    left: 1vw;
    right: 99vw;
    bottom: 93vh;
    font-size: 15vh;
    color: #000;
    background-color: #ff0;
}

#credit {
    position: absolute;
    width: 99vw;
    height: 5vh;
    top: 94vh;
    left: 1vw;
    right: 99vw;
    bottom: 99vh;
    font-size: 3vh;
    color: #fff;
    background-color: #000;
}
```

## キーボード対応

JavaScript でのキーボードの取得は、以下のサイトを参考にした（コピーした）。

- [JavaScript逆引き > 押したキーのキーコードを取得](http://shanabrian.com/web/javascript/keycode.php)

以下、km2.js からの抜粋

```js
    document.onkeydown = function(e) {
        var keyCode = false;
        
        if (e) {
            event = e;
        }
        
        if (event) {
            if (event.keyCode) {
                keyCode = event.keyCode;
            } else if (event.which) {
                keyCode = event.which;
            }
        }
        
        if (keyCode == 81 // Q
            || keyCode == 0x37 // 7
           ) {
            playSound(sample1, 0);
        } else if (keyCode == 87 // W
                   || keyCode == 0x38 // 8
                  ) {
            playSound(sample2, 0);
        } else if (keyCode == 69 // E
                   || keyCode == 0x39 // 9
                  ) {
            playSound(sample3, 0);
        } else if (keyCode == 65 // A
                   || keyCode == 0x34 // 4
                  ) {
            playSound(sample4, 0);
        } else if (keyCode == 83 // S
                   || keyCode == 0x35 // 5
                  ) {
            playSound(sample5, 0);
        } else if (keyCode == 68 // D
                   || keyCode == 0x36 // 6
                  ) {
            playSound(sample6, 0);
        } else if (keyCode == 90 // Z
                   || keyCode == 88 // X
                   || keyCode == 67 // C
                   || keyCode == 0x31 // 1
                   || keyCode == 0x32 // 2
                   || keyCode == 0x33 // 3
                   || keyCode == 0x20 // SPC
                  ) {
            playSound(sample78, 0);
        }
    };
```

で、実際にキーボードでパタパタやってみると、おもしろい。
Web Audio API の正しい使い方の１つの方向がこれなんだろうなと、納得した。

# 7. まとめ

- とりあえず、流行に乗ってみた。
- Web Audio API は、とりあえず音出しはできたレベル。でも何も分かってない。
- 素材がよかったのか、初手のシンプルな実装の音で、十分、たのしめた。
- こだわりたい人は、是非、ぼくが手を抜いた点を極めたり、歌と一緒にきよしを踊らせたりしてみてください。