プログラマの間で流行っている「ズンドコキヨシ」実装大会を見て、前からやりたいと思ってた Web Audio API の勉強も兼ねて挑戦してみた、というはなしです。
デモを見たい人は以下が最終的なコンテンツです。
- Web Audio API 版 ズンドコキヨシ
- キ・ヨ・シ!マシーン - リズムマシーン的に、ボタンでズンドコできます
- キ・ヨ・シ!マシーン Version 2 - PCのキーボードでもズンドコできるように改良
キ・ヨ・シ!マシーンは習作ですが、これも結構おもしろい。特に Version 2 は(連打できるし)。
0. はじめに
会社で「ズンドコキヨシ」がすごい、という話を聞いた。
Qiita のまとめのページ「ズンドコキヨシまとめ」があった。
そこにあった学生さん(この人が、ズンドコキヨシの創始者だね)のツイートが、すてきだった。
Javaの講義、試験が「自作関数を作り記述しなさい」って問題だったから
— てくも (@kumiromilk) 2016年3月9日
「ズン」「ドコ」のいずれかをランダムで出力し続けて「ズン」「ズン」「ズン」「ズン」「ドコ」の配列が出たら「キ・ヨ・シ!」って出力した後終了って関数作ったら満点で単位貰ってた
コンセプトは分かった。「ズン」と「ドコ」というデジタルな組み合わせと、完成時の「キ・ヨ・シ!」の歓喜のコール、このコンビネーションが絶妙だなと、元ネタの曲を知らないオレでも思った。
この段階で、しかし、素朴な疑問が残った。なんでパターンが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 で、ブラウザ上で再生してみよう。
何も知らないので、ググって情報収集。
- Qiita: WebAudioAPIで遊べるようになった
- phiary: Web Audio API で音を再生しよう
- HTML5Rocks: Web Audio API の基礎 By Boris Smus
一通り目を通した後、結局、「phiary: Web Audio API で音を再生しよう」のコピペで、フランケンシュタインなコードができた。
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 に持っておかないといけなくて、それどうすれば良いんだろうとか、素人らしく悩む。
現状、とりあえず動けば良い状態で、組んだ。
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 のみ。
<!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>
<!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>
さすがにこれはダメだろう、ということで、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;
}
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 は、とりあえず音出しはできたレベル。でも何も分かってない。
- 素材がよかったのか、初手のシンプルな実装の音で、十分、たのしめた。
- こだわりたい人は、是非、ぼくが手を抜いた点を極めたり、歌と一緒にきよしを踊らせたりしてみてください。