Web Audio API でズンドコキヨシ

  • 10
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

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

スクリーンショット
zdk-50.png km-50.png

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

0. はじめに

会社で「ズンドコキヨシ」がすごい、という話を聞いた。
Qiita のまとめのページ「ズンドコキヨシまとめ」があった。
そこにあった学生さん(この人が、ズンドコキヨシの創始者だね)のツイートが、すてきだった。


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

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

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

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

1. きよしのズンドコ節とは

氷川きよしは知っていた(名前は、ということだけど)。

ググったら Youtube のビデオが出てきた。黄色いスーツで踊ってるビデオ。
youtube: きよしのズンドコ節 / 氷川きよし

これを見て、5つ組(ズン、ズン、ズン、ズン、ドコ)の謎が解けた。つまり「キ・ヨ・シ!」まで含めて8拍、最初のズンと2つ目のズンの間に一拍ある。だからこれ
は、

ズン、ズン、ズン、ズン、ドコ

じゃなくて、

ズン、ウッ、ズン、ズン、ズン、ドコ、キ・ヨ・シ!

じゃないか、と思った。

Qiita の実装例を見てたら、コンソールへの出力だけど、リズム感を保ったバージョンがあった!
Qiita: リズムに乗ってズンドコキヨシ♪ with Ruby
これだよね、これ。

でもなにか物足りない。

2. 音付き「ズンドコキヨシ」が欲しい

要するにぼくが求めていたのは、「ズン」と「ドコ」のランダムなパターンを、実際に歌わせたい、ということみたい。

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

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

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

3. ズンドコ・サンプルの作成

先に見つけた、黄色いスーツで踊るきよしの Youtube ビデオを素材にする。

まず 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 で、ブラウザ上で再生してみよう。

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

一通り目を通した後、結局、「phiary: Web Audio API で音を再生しよう」のコピペで、フランケンシュタインなコードができた。

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);
    };
  });
};

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

名付けて「キ・ヨ・シ!マシーン」。
イメージはドラム・マシーンというかドラム・パッド。
「ズン」「ウッ」「ズン」「ズン」「ズン」「ドコ」「キ・ヨ・シ!」と7つのボタンからなる。2拍目のブラスも入れて、1、3、4、5拍それぞれの「ズン」も独立に(実際、3拍、4拍は音程も違うし、3拍目の裏声っぽいのはポイントだろう)。

5. Web Audio API でズンドコキヨシ

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

JS での乱数は、芸はないけど Math.random() でいいかな。 cf. w3schools.com: JavaScript random() Method

時間制御は先のHTML5Rocksの「時間の管理: リズムに同期して音を再生する」セクションのコピペで行く。

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

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

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 版 ズンドコキヨシ(リンク先に飛んでも、突然、音がなったりはしませんので、安心してください。)

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

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

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

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

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

6. 仕上げて Qiita デビュー

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

  • Markdown で書いて、
  • デモサイトにアップしないといけないなと思って、github のページにアップして、
  • 生のボタン並べただけだとさすがになぁ、と思って CSS を(ググって調べて勉強して)書いて、
  • 最初のキ・ヨ・シ!マシーンをマウスでクリックするのもかったるいな、と思って、キーボードで叩けるキ・ヨ・シ!マシーン Version 2も書いた。

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

CSS で見た目を整える

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

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>
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 zdk-before-50.png

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

結果、以下のような感じにまとめた。
km-50.png zdk-50.png

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;
}
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 でのキーボードの取得は、以下のサイトを参考にした(コピーした)。

以下、km2.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 の正しい使い方の1つの方向がこれなんだろうなと、納得した。

7. まとめ

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