ブラウザで電子ドラムを鳴らす 【JavaScript】

Python

たまたま興味が沸いたので、ブラウザで電子ドラムを鳴らすスクリプトを作り、遊んでみることにします。
JavaScript の基本機能のみを使って作ることにし、ライブラリなどをインストールすることなくブラウザだけがあれば動くようにします。実質的にフリーの電子ドラムです。
ドラムを触ったことのない方、興味のある方は、遊んでみてください。

以下の環境で動作確認をしています。
環境: Windows パソコン、Microsoft Edge ブラウザ

背景 ~ もっと気軽に作曲などで遊べる環境があるとよいのだが …

以前に Python を使って音階を鳴らすスクリプトについてまとめています。
メロディやコードを自由に鳴らすことができます。
こういった環境でかんたんな曲でも作ろうかなと思ったとき、ドラム音を鳴らしておくことができるとイメージがわきます。
ということで、ドラムを鳴らすスクリプトを作ってみることにします。

8ビート、16ビートなど、基本的なドラムパターンが演奏できると面白そうです。
作ったドラムパターンはテキストで残しておけるようにし、起動時に読み込むことができるとよいです。
パソコン環境にはできるだけ依存せず、余計なプログラムのインストールなども不要としたいです。
ということで、JavaScript の基本機能程度を使うことにし、ブラウザがあればドラム音のパターンが再生できるようにしてみます。

うまく動いたら、Michael Jackson の Smooth Criminal のドラムパターンなども演奏してみてください。
これまでドラムセットに触ったことがなくても、何らの演奏技術がなくても、かなり本格的なドラムパターンが自由に扱えるようになると思います。

設定手順

① パソコンに電子ドラムを再生するためのフォルダを作成します。
例:
c:\user\desktop_music1
② ①のフォルダの中に electronic_drums1.html、drum_pattern1.js という名前でテキストファイルを作成し、下記のスクリプトを貼りつけて保存してください。
例:
c:\user\desktop_music1\electronic_drums1.html
c:\user\desktop_music1\drum_pattern1.js
③ ②の electronic_drums1.html をダブルクリックしてください。

→ 電子ドラムのスクリプトが起動したら設定完了です。

使い方

・ [hihat]、[snare]、[kick] ボタンをクリックすると、各楽器を鳴らすことができます。
それぞれ、ドラムセットのハイハット、スネア、キックドラムに対応しています。ざっくりで、現物のドラムセットの上から順に対応しています。

・ キーボード操作でも各楽器が演奏できるようにしてあります。
・ キーボードの h または g キーを押すと、ハイハットを鳴らすことができます。
(hihat の h とその隣のキー)
・ キーボードの s または d キーを押すと、スネアを鳴らすことができます。
(snear の s とその隣のキー)
・ キーボードの k または l キーを押すと、キックドラムを鳴らすことができます。
(kick の k とその隣のキー)
それぞれ2つのキーで鳴るようにしてあるのは、両手でドラムを叩けるようにするためです。

・ [play] ボタンをクリックすると、[beat pattern:] の欄に書かれたドラムパターンを演奏することができます。[stop] ボタンで演奏を停止します。各行は、ハイハット、スネア、キックドラムの順で記載します。
・ [BPM] (beats per minute) 欄で再生速度を設定できます。
値を大きくすると演奏速度が上がり、値を小さくすると演奏速度が下がります。半角数字で設定します。
・ [hihat]、[snare]、[kick] ボタンの隣の欄で、それぞれの楽器の音量を設定します。半角数字で設定します。
・ 上で値を設定したら、[set] ボタンをクリックすることで設定を反映させることができます。
[play] ボタンでの演奏中でも設定を変更できます。

ビートパターン(beat pattern 欄)の書き方

・ 設定欄の上から各行ごとに各ドラム音を再生していきます。
楽譜は横方向に書きますが、テキストファイルで設定している都合上、縦方向に上から下方向に記載していきます。
・ 各行では3つの半角数字を記載します。
左から順に、ハイハット、スネア、キックドラムの3つの楽器に対応しています。
・ 3つの楽器で、音を鳴らす場合は 1、音を鳴らさない場合は 0 を記載します。
・ 各3つの数字は、半角スペースで区切ります。
・ たとえば、4ビートであれば4行で記載します。8ビートであれば8行で記載します。16ビートであれば16行で記載します。
・ [play] ボタンをクリックすると、欄に記載されたドラムパターンを順に再生し、末尾までいくと冒頭から繰り返して再生します。
・ 途中や末尾などに空行を入れておくと、空行以降の記載はスキップして冒頭からの再生を繰り返します。メモ書きや他のドラムパターンの記載を残しておけるようにしています。

うまく動いたら

・ うまく動いたら、テンポ(BPM、beats per minute)や各楽器の音量を調整してみてください。
・ また、ドラムパターンを変えて演奏してみてください。
・ 8ビート、16ビートなどのパターンも演奏してみてください。

・ 気に入ったドラムパターンができたら、drum_pattern1.js ファイルに記載しておくことで、次回の起動時に読み込んで初期値として設定することが可能です。
具体的には、メモ帳などで drum_pattern1.js を開き、作成したドラムパターンを差し替えて保存します。起動時、または、ページの再読み込み時に、ドラムパターンの初期値として設定することができます。
なお、サンプルスクリプト drum_pattern1.js において、冒頭および末尾の計2行は記載を変えずに残してください。JavaScript のプログラムから呼び出すために必要です。
・ JavaScript、プログラミングがわかる方、音楽に詳しい方は、自由にアレンジをしてみてください。

ドラムパターンの例

事例として、BPM = 60 として、末尾のドラムパターンの例1を beat pattern: 欄にペーストして演奏してみてください。

Michael Jacson の Smooth Criminal を参考に、ドラムパターンを作ってあります。
原曲では、BPM 120 の 16ビートで、2小節のパターンを繰り返すのが基本となっているようです。
2つの小節で少しパターンが変わるため、ここではオリジナルの2小節分を1小節とみなし、BPM 60 の 32ビート(32行)で記載しています。
プログラムはシンプルですが、ドラムパターンに変化があっても、細かな刻みがあっても、演奏することが可能です。

他にも、かっこいいドラムパターン、ノリのよい曲/好きな曲のドラムパターンなどを入力し、演奏してみてください。楽器を扱う技術がなくても、かなり本格的な演奏ができると思います。

参考:ドラムのリズム関連の定義まとめ

ドラムを扱うにあたり、8ビート、16ビートなどの用語が出てきます。
そこで、用語や定義を整理しておくことにします。

・ DTM (Desktop Music)や音楽のプログラミングの分野では、テンポを定義するのによく BPM (beats per minute)が使われます。すなわち、1分間に何拍とするかを BPM の値として定義します。60 BPM なら、1分間に60拍となるテンポです。
・ 1分間は60秒ですので、上記のテンポが数値 BPM1 で与えられたとき、1拍の長さはつぎの式で表されます。
1拍の長さ(秒) = 60 / BPM1
・ 4/4 拍子では、4分音符が4つ分(4拍分)で、1小節の長さに対応します。
多くの音楽、楽譜では 4/4 拍子が使われています。そこで、今回のプログラムも 4/4 拍子の定義に基づいて作成しています。
・ すると 4/4 拍子では、1小節の長さはつぎの式で表されます。
1小節の長さ(秒、time_cycle1) = 60 / BPM1 × 4(拍)
この1小節の長さ time_cycle1 が、繰り返しの基本の長さとなります。
4/4 拍子とするのか他の拍子とするのかで、上記の4の係数部分が変わりうることになります。
・ 4 ビートは、1小節を4分音符4個で分割し、リズムを刻む際の基本の長さ(≒最小単位)としています。
8 ビートは、1小節を8分音符8個で分割します。
16 ビートは、1小節を16分音符16個で分割します。
そこで、1小節の分割数を beats1(ビートの数、何ビートか)で定義しておくことにします。
4 ビートなら、beats1 = 4、8ビートなら beats1 = 8、… となります。
・ ビートの数 beats1 が与えられたとき、そのビートにおける1拍分の長さ(noteTime1)は、以下の式で求められます。
1拍の長さ noteTime1 = 1小節の長さ time_cycle1 / ビートの数 beats1
4/4 拍子のとき、上記の式から、つぎの関係が得られます。
1拍の長さ noteTime1 =  60/BPM1 * 4 / ビートの数 beats1
8ビート、16ビートといったときの1拍の長さと、BPM で定義している1拍の長さが、同じであるとは限らない。同じ「ビート」という単語が2か所に出てくるものの、1拍の長さが一般には異なってくる、ということになります。)

・ 上記の定義を明確化しておくと、リズムに関連する各パラメータの関係が決まり、プログラムを作ることができます。
プログラムを作る上では、BPM は初期値を与えておけばよいです。また、ビートの数 beats1 はビートパターンの欄の行数から取得することができます。
なお、BPM を使って、和音、コードなどを演奏する Python のプログラムに関しては、下記の関連リンクなどでまとめています。

まとめ

少し興味がわいたのでドラムの音を再生するプログラムを作って遊んでみました。
ブラウザの標準機能を使うだけで、かなり本格的なドラムパターンを演奏することが可能です。
ブラウザだけがあれば動くので、何か単純な作業をテキパキとさばきたいときの BGM としても活用ができそうです。

たまたま、YouTube でドラムの演奏について見ていたのですが、定義の説明があいまい(?)なまま、8ビートと16ビートを聞き分けましょうといった動画がかなり多くありました。
BPM の設定を2倍などに変えればどのようにでもなるではないかと思い、ドラム関連の定義をネットで探したのですが、なぜか定義を簡潔にまとめたサイトが見つかりませんでした。
ということで、ドラムに関する定義についてもまとめ、公開しておくことにします。プログラムを自力で作り演奏してみると、あいまいなままわかったつもりでいた部分についても明確に理解することができます。

他にも音楽関連のプログラムをまとめ、公開しています。音楽やプログラミングに興味のある方は下記のリンクなども参照してみてください。

関連リンク
・ Python で Just the Two of Us 進行 【コード進行】
・ ドレミファソラシドを鳴らすサンプル【Python】
・ キーボードピアノ 【本格49鍵!】

外部リンク
・ Michael Jackson – Smooth Criminal – Live Munich 1997- HD (YouTube)

サンプルスクリプト

電子ドラムのスクリプト electronic_drum1.html


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>electronic drums v0.1</title>
</head>
<body>

    <script src="./drum_pattern1.js" onerror="err1()"></script>
    <script>

        document.addEventListener('keydown', function(event) { 
            if ( event.key === 'h' || event.key === 'g' ) { 
                playHihat1(0); 
            } 
            if ( event.key === 's' || event.key === 'd' ) { 
                playSnare1(0); 
            } 
            if ( event.key === 'k' || event.key === 'l' ) { 
                playKick1(0); 
            } 
        }); 

        window.onload = load1; 
        function load1() { 
            str2 = str1_pattern.trim(); 
            document.getElementById( "text1" ).value = str2; 
            set1(); 
        } 
        let busy1 = -1; 
        let audioContext1 = {}; 
        let obj1 = -1; 
        let hihatPattern =  [1, 1, 1, 1, 1, 1, 1, 1]; 
        let snarePattern =  [0, 0, 0, 1, 0, 1, 0, 1]; 
        let kickPattern =   [1, 0, 1, 0, 0, 0, 1, 0]; 
        let bpm1 = 120;   // beats per minute 
        let beats1 = 8; 
        let noteTime1 = (60 / bpm1) * 4 /beats1 ;
        let time_cycle1 = 60 / bpm1 * 4; 
        let vol1 = 1.0; 
        let vol2 = 1.0; 
        let vol3 = 2.0; 
        function set1() { 
            bpm1 = parseInt(document.getElementById( "textbox1" ).value); 
            vol1 = parseFloat(document.getElementById( "textbox2" ).value); 
            vol2 = parseFloat(document.getElementById( "textbox3" ).value); 
            vol3 = parseFloat(document.getElementById( "textbox4" ).value); 
            set1_pattern(); 
            noteTime1 = (60 / bpm1) * 4 / beats1; 
            time_cycle1 = 60 / bpm1 * 4; 
//          document.getElementById( "div1" ).textContent = beats1 + " " + noteTime1 + " " + time_cycle1; 
        } 

        function set1_pattern() { 
            hihatPattern = []; 
            snarePattern = []; 
            kickPattern = []; 
            let str1 = document.getElementById( "text1" ).value.split("\n\n")[0]; 
            let a1 = str1.split("\n"); 
            beats1 = a1.length; 
            for (let i1 = 0; i1 < beats1; i1++ ) { 
                a1[i1] = a1[i1].trim().split( " " ); 
            } 
            for (let i1 = 0; i1 < beats1; i1++ ) { 
                hihatPattern[i1] = parseInt(a1[i1][0]); 
                snarePattern[i1] = parseInt(a1[i1][1]); 
                kickPattern[i1]  = parseInt(a1[i1][2]); 
            } 
        } 

        function ini1() { 
            if (obj1 === -1) { 
                audioContext1 = new (window.AudioContext || window.webkitAudioContext)(); 
                obj1 = 1; 
            } 
        } 

        function playHihat1(time0) {
            ini1(); 
            let time = time0 + audioContext1.currentTime;
            const noiseBuffer = audioContext1.createBuffer(1, audioContext1.sampleRate * 0.05, audioContext1.sampleRate);
            const output = noiseBuffer.getChannelData(0);
            for (let i = 0; i < audioContext1.sampleRate * 0.05; i++) {
                output[i] = Math.random() * 2 - 1;
            }
            const noise = audioContext1.createBufferSource();
            noise.buffer = noiseBuffer;
            const noiseFilter = audioContext1.createBiquadFilter();
            noiseFilter.type = 'highpass';
            noiseFilter.frequency.setValueAtTime(5000, time);
            const noiseGain = audioContext1.createGain();
            noiseGain.gain.setValueAtTime(vol1, time);
            noiseGain.gain.exponentialRampToValueAtTime(0.001, time + 0.05);
            noise.connect(noiseFilter);
            noiseFilter.connect(noiseGain);
            noiseGain.connect(audioContext1.destination);
            noise.start(time);
            noise.stop(time + 0.05);
        }

        function playSnare1(time0) {
            ini1(); 
            let time = time0 + audioContext1.currentTime;
            const noiseBuffer = audioContext1.createBuffer(1, audioContext1.sampleRate * 0.2, audioContext1.sampleRate);
            const output = noiseBuffer.getChannelData(0);
            for (let i = 0; i < audioContext1.sampleRate * 0.2; i++) {
                output[i] = Math.random() * 2 - 1;
            }
            const noise = audioContext1.createBufferSource();
            noise.buffer = noiseBuffer;
            const noiseFilter = audioContext1.createBiquadFilter();
            noiseFilter.type = 'highpass';
            noiseFilter.frequency.setValueAtTime(1000, time);
            const noiseGain = audioContext1.createGain();
            noiseGain.gain.setValueAtTime(vol2, time);
            noiseGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2);
            noise.connect(noiseFilter);
            noiseFilter.connect(noiseGain);
            noiseGain.connect(audioContext1.destination);
            noise.start(time);
            noise.stop(time + 0.2);
            const osc = audioContext1.createOscillator();
            const oscGain = audioContext1.createGain();
            osc.type = 'sine';
            osc.frequency.setValueAtTime(200, time);
            oscGain.gain.setValueAtTime(vol2*0.7, time);
            oscGain.gain.exponentialRampToValueAtTime(0.001, time + 0.1);
            osc.connect(oscGain);
            oscGain.connect(audioContext1.destination);
            osc.start(time);
            osc.stop(time + 0.2);
        }

        function playKick1(time0) {
            ini1(); 
            let time = time0 + audioContext1.currentTime;
            const oscillator = audioContext1.createOscillator();
            const gainNode = audioContext1.createGain();
            oscillator.connect(gainNode);
            gainNode.connect(audioContext1.destination);
            oscillator.type = 'sine';
            oscillator.frequency.setValueAtTime(150, time);
            oscillator.frequency.exponentialRampToValueAtTime(0.001, time + 0.5);
            gainNode.gain.setValueAtTime(vol3, time);  
            gainNode.gain.exponentialRampToValueAtTime(0.001, time + 0.5);
            oscillator.start(time);
            oscillator.stop(time + 0.5);
        }

        function play2() { 
            if ( busy1 < 0 ) { 
                ini1(); 
                set1(); 
                busy1 = 1; 
                play1(); 
            } else { 
                stop1(); 
            } 
        } 

        function play1() { 
            if (busy1 > 0) { 
                for (let i = 0; i < beats1; i++) {
                    const time1 = i * noteTime1;
                    if (hihatPattern[i] === 1) {
                        playHihat1(time1);
                    }
                    if (snarePattern[i] === 1) {
                        playSnare1(time1);
                    }
                    if (kickPattern[i] === 1) {
                        playKick1(time1);
                    }
                } 
                setTimeout(play1, time_cycle1 * 1000);             } 
        }
        
        function stop1() { 
            busy1 = -1; 
        } 

    </script>

    <h1>electronic drums</h1>
    <button onclick="play2()">play</button> <button onclick="stop1()">stop</button> <button onclick="set1()">set</button>
    <input type="text" id="textbox1" style="width:60px; height:14px;" value="120" onchange="set1()">:BPM 
    <br><br>
    <button onclick="playHihat1(0)">hihat (h, g)</button>
    <input type="text" id="textbox2" style="width:60px; height:14px;" value="1.0" onchange="set1()">
    <button onclick="playSnare1(0)">snare (s, d)</button>
    <input type="text" id="textbox3" style="width:60px; height:14px;" value="1.0" onchange="set1()">
    <button onclick="playKick1(0)">kick (k, l)</button>
    <input type="text" id="textbox4" style="width:60px; height:14px;" value="2.0" onchange="set1()">
    <br>
    beat pattern: <br>
    <textarea id="text1" name="text1" value="" rows="10" cols="30" style="background-color:#ffffff " ></textarea><br>
    <div id="div1"></div>

</body>
</html>


設定ファイル drum_pattern1.js


var str1_pattern = `
1 0 1 
1 0 0 
1 0 1 
1 1 0 
1 0 0 
1 1 0 
1 0 1 
1 1 0 
`;

ドラムパターンの例1

BPM = 60 として、下記のパターンを beat pattern: の欄にペーストして [play] ボタンをクリックしてみてください。

1 0 1 
0 0 0 
0 0 1 
0 0 0 
1 1 0 
0 0 0 
0 0 1 
0 0 1 
1 0 1 
0 0 0 
0 0 1 
0 0 0 
1 1 0 
0 0 0 
0 0 1 
0 0 1 
1 0 1 
0 0 0 
0 0 1 
0 0 0 
1 1 0 
0 0 0 
0 0 1 
0 0 1 
1 0 1 
0 0 0 
0 0 1 
0 0 0 
1 1 0 
0 0 0 
0 0 1 
0 0 0 
タイトルとURLをコピーしました