LoginSignup
14
14

More than 5 years have passed since last update.

Web Audio API でズンドコキヨシ

Last updated at Posted at 2016-04-02

プログラマの間で流行っている「ズンドコキヨシ」実装大会を見て、前からやりたいと思ってた 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 は、とりあえず音出しはできたレベル。でも何も分かってない。
  • 素材がよかったのか、初手のシンプルな実装の音で、十分、たのしめた。
  • こだわりたい人は、是非、ぼくが手を抜いた点を極めたり、歌と一緒にきよしを踊らせたりしてみてください。
14
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
14