このページの本文へ

JavaScriptでギターも上達!? 音楽アプリ開発に使えるコードのアイデア

2016年09月13日 12時22分更新

文●Myles English

  • この記事をはてなブックマークに追加
本文印刷
曲の進行にあわせてギター譜が切り替わるWebアプリを作っちゃったEnglishさんが、その作り方を解説。同じ原理で簡易カラオケっぽいものとか、音に合わせたアニメーションとかも作れそう。

HTML5オーディオプレーヤーの登場によって、とりわけWebアプリケーション関連の音楽分野で、新しく刺激的な可能性が生まれています。私が自作したJam Stationの作成方法を紹介しながら、こうした可能性のいくつかをみなさんに伝えたいと考えています。このプロジェクトはもともとは実験として始まりました。しかし、時間が経つにつれて無限の可能性を秘めた、ギター演奏者向けの練習および教育ツールへと成長したのです。

sample

この記事を理解するには、JavaScriptの原理について理解を深めることが不可欠です。

物事をシンプルにするという信念のもと、少しでもシンプルなバージョンをビルドしていきます。

CodePenの、SitePoint(@SitePoint)にあるJam Stationは、今回まさに作ろうとしているものですので参照してください。


この記事を正確に再現することに興味がなくても、問題ありません。記事で紹介する多くは、ほかのタイプの音楽アプリケーションにも応用可能です。

音楽理論の基本を理解していると、この記事を理解しやすくなります。また、拍子記号、小節、拍について理解している必要があります。

必要なもの

選択したオーディオトラックは、クリックトラックやメトロノームに合わせて録音される必要があります。言い換えると、それぞれの拍の長さはトラック全体にわたって均一であることが必要です。

現在の拍および小節を見つけるには、オーディオトラックに関して知っておくべき点がいくつかあります。

  • 1分あたりの拍数(BPMまたはテンポ)
  • 拍子記号
  • トラックの開始時間
  • カウントに何小節が使われているか(常に適用されるわけではありません)

私がCodePenのデモ(右クリックで保存できます)で使用したオーディオトラックも使えます。

以下がCodePenのデモで使ったオーディオトラックのデータです。

  • BPM = 117
  • 拍子記号 = 4/4
  • 1拍目の開始時間 = 0.2s
  • カウントに使用される小節数 = 1

自分のトラックを使用して、これらの情報で不明なものがある場合は、Audacityなどのフリーのオーディオエディターが役立ちます。すべてではありませんが、1拍目の正しい開始時間が分かるようにはなります。これから自分のトラックを録音しようとしているなら、最初からこの情報をメモしておくようおすすめします。

マークアップとCSS

お楽しみの前に、これから使用するHTMLとCSSを示します。

 <div class="wrapper">
   <div id="chord-diagram"></div>
   <audio id="jam-track"  src="https://myguitarpal.com/wp-content/uploads/2014/09/12-Bar-Blues-in-A-Version-1.mp3" controls></audio>
  <br>

  <label>Beat: </label>
  <div class="data" id="beat"></div>

  <label>Measure: </label>
  <div class="data" id="measure"></div>

  <label>Section: </label>
  <div class="data" id="section"></div>

  <label>Chord: </label>
  <div class="data" id="chord"></div>

  <div id="chord-progression"></div>
</div>
.wrapper {
  max-width: 400px;
}

#chord-progression {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid lightgray;
}

.section {
  margin-bottom: 20px;
  display: none;
}

.measure {
  width: 25%;
  display: inline-block;
}

.m-line {
  float: right;
  width: 10px;
}

audio {
  width: 100%;
}

.data {
  display: inline;
  margin-right: 10px;
}

label {
  font-weight: bold;
}

セットアップと変数

最初に、これまでに見てきたすべてのトラックデータを取得し、いくつかの変数をセットアップします。

// beats per minute
var BPM = 117;

// beats per second
var BPS = 60 / BPM;

// measures used for count in
var measuresCount = 1;

// time the track starts
var offsetSeconds = 0.2;

// time signature
var timeSigTop = 4;
var timeSigBottom = 4;

コード進行

この記事向けにコード進行データモデルをシンプルにし、多次元配列を使用するようにしました。

上位レベルの配列は、コード進行セクションです。これらが、バース、ブリッジ、コーラスなどになります(日本語版編注:Aメロ、Bメロ、サビの意)。

多重化された配列は、各セクション内の各小節のコードを保持しています。

var sectionOne = ['A7', 'A7', 'A7', 'A7', 'D7', 'D7', 'A7', 'A7', 'E7', 'D7', 'A7', 'E7'];
var sectionTwo = ['A7', 'A7', 'A7', 'A7', 'D7', 'D7', 'A7', 'A7', 'E7', 'D7', 'A7', 'A7', 'A7', 'A7', 'A7', 'A7', 'A7', 'A7'];

var chordProgression = [
  sectionOne,
  sectionOne,
  sectionOne,
  sectionTwo
];

音楽ではコーラスなどいくつかのセクションで繰り返しが発生することがあるので、シンプルにするために、上に示したようなセクションをそれぞれ変数に保存し、chordProgression配列にそのセクションを組み込みます。

Jam Stationの実行

Jam Stationはtimeupdateイベントを使用しますが、これはゲームループを使用するのと似ています。timeupdateイベントが発生するたびに(トラックが演奏されている間の1秒ごとに数回)、現在の拍、小節、コードなどのいくつかのデータを更新するために関数を実行します。データが更新されないのはトラックが休止しているときのみです。

timeupdateイベントが発生するときに、jamStation関数を実行します。この関数はオーディオが再生されている間、1秒ごとに数回呼び出されます。

var audio = document.getElementById('jam-track');

audio.ontimeupdate = function() {
  jamStation();
};

お楽しみのパート

この関数ではプレゼンテーションではなく、データのみとやりとりするようにします。プレゼンテーションとのやりとりにはあとで別の関数(renderJamStation)を使います。

現在の拍を見つけるには、式(audio.currentTime - offsetSeconds + BPS)/ BPSを使用し、変数beatにこの値を保存します。

現在の小節を見つけるには、式(beat -1)/ timeSigTopを使用し、 変数measureにこの値を保存します。

function jamStation() {
  var beat = (audio.currentTime - offsetSeconds + BPS) / BPS; 
  var measure = (beat - 1) / timeSigTop;
}

beat変数とmeasure変数はここで端数を切り捨てる必要があります。切り捨てることで、比較する際により簡単な数字を使用できます。もっと複雑なアプリケーションを使用して、拍の端数を使う必要がある場合は切り捨てる必要はありません。

これらのすべての数値をcleanBeatcleanMeasureに保存します。

ある小節内の現在の拍を表示させたい場合は次の式((cleanBeat -1)%timeSigTop)+1を使用します。 今13拍目にいるとすると、この式の値は((13-1)%4)+1のようになります。0拍目は存在しないので、1を足します。

ここで、拍は無限に増加していくのではなく、拍子記号の上の部分の数字と同じ数までしか増加しません。measureBeatには小節内の拍の値が入ります。つまり、cleanMeasureが1、2、3、4、5、6、7、8、とカウントしていくのに対して、 measureBeatは1、2、3、4、1、2、3、4、とカウントしていくわけです。

function jamStation() {
  var beat = (audio.currentTime - offsetSeconds + BPS) / BPS; 
  var measure = (beat - 1) / timeSigTop;

  // round down for beat and measure
  var cleanBeat = Math.floor(beat);
  cleanMeasure = Math.floor(measure);

  // find the current beat within the measure
  measureBeat = ((cleanBeat - 1) % timeSigTop) + 1;
}

すでに関数の外で変数cleanMeasureを宣言しているので(CodePenを参照してください)、jamStation()の外部からこの値にアクセスできるように、この変数の前にvarを置かないようにしてください。

実はこの時点で、今後必要になるもっとも重要なデータを持っています。しかしやっかいなのは、コード進行のやりとりをすることです。

jamStation関数の次の部分では、currentSectioncurrentChordの2つを定義します。

まず、1小節目を過ぎたところで、if~else文を使う必要があります。そうすると、現在のセクションと現在のコードの値が分かります。そうしないと、currentSectioncurrentChordにnullがセットされてしまいます。

現在のセクションと現在のコードを見つけるために、セクション間でループをしてから各セクション内の小節間でループする、多重ループを実行します。

変数countがセットされ、各セクションの小節内でループするたびに1ずつ増加していきます。ここで、cleanMeasurecountと等しくなれば、 いまいるセクションと小節が分かったということになります。現在オーディオトラックがいる正しいセクションと小節が分かったので、次回のループ実行時に上書きされないように、それらの値を保存すると同時に両方のループをブレイクする必要があります。

再びcurrentSectioncurrentChordjamStation()の外部で宣言されないように、そして関数間でそれらを共有できるように、これらの変数の前でvarを使用しないようにしてください。

function jamStation() {
  var beat = (audio.currentTime - offsetSeconds + BPS) / BPS; 
  var measure = (beat - 1) / timeSigTop;

  // round down for beat and measure
  var cleanBeat = Math.floor(beat);
  cleanMeasure = Math.floor(measure);

  // find the current beat within the measure
  measureBeat = ((cleanBeat - 1) % timeSigTop) + 1;

  if (cleanMeasure > 0) {
    // find the currentSection and currentChord
    var count = 0;
    var br = false;
      for (var s = 0; s < chordProgression.length; s++) {
        for (var m = 0; m < chordProgression[s].length; m++) {
          count++;
          if (cleanMeasure == count) {
            currentSection = s + 1;
            currentChord = chordProgression[s][m];
            br = true;
            break;
          }
        }
        if (br === true) {
          break;
        }
      }
  } else {
    currentSection = null;
    currentChord = null;
  }

  // display the jam station and its data
  renderJamStation();
}

現時点ですべての必要なデータを保持しており、また、そのデータはグローバルにアクセス可能です。

jamStation関数の最後では、renderJamStation()が実行されていることが分かります。この関数はプレゼンテーションの目的のためだけに使用されますが、それについては後ほど少し取り上げます。

コード進行のレンダリング

コード進行を表示する必要があります。きれいに整理しておくために、renderChordProgressionと呼ばれる関数内でラップします。この関数はコード進行データが絶対に更新されないよう、1度実行されるのみです。

はじめに、コード進行内のセクション間でループを実行します。そのループが実行されるたびに、「section」クラスのdivと「section-[数値]」のidを生成します。各セクションは独自のidを持っているため、特定のセクションが再生されているときにそのセクションを表示できるのです。

// take the chordProgression array and render the HTML
function renderChordProgression() {
  var progression = document.getElementById('chord-progression');

  // make the sections
  for (var s = 0; s < chordProgression.length; s++) {
    progression.innerHTML += '<div class="section" id="section-' + (s + 1) + '"></div>';
  }
}

次に、あとで多重ループ内ですべての小節間でループを実行できるように、再びセクション間でループを実行します。そして、各小節はそれぞれのセクションに含まれます。

おそらく、カウント変数がセットされてインクリメントされたことに気づいたはずです。これは、各セクションで小節ごとにインクリメントするためです。

完成した関数は以下のようになります。

// take the chordProgression array and render the HTML
function renderChordProgression(){
  var progression = document.getElementById('chord-progression');

  // make the sections
  for (var s = 0; s < chordProgression.length; s++){
    progression.innerHTML += '<div class="section" id="section-' + (s + 1) + '"></div>';
  }

  var count = 0;

  for (var s = 0; s < chordProgression.length; s++) {
    for (var m = 0; m < chordProgression[s].length; m++) {
      count++;
      var section = document.getElementById('section-' + (s + 1));
      section.innerHTML += '<div class="measure" id="measure-' + count + '">' 
                        + chordProgression[s][m] 
                        + '<div class="m-line">|</div></div>';
    }
  }
}

みなさんはおそらくコード進行をスタイリングしたいと考えるでしょう。これに関しては、記事の最初に示したCodePen内のCSSを参照してください。

プレゼンテーション

renderJamStation関数はjamStation関数の内部から呼び出されます。

function renderJamStation() {

  // show the beat within the measure, not overall
  document.getElementById('beat').innerHTML = measureBeat;

  // show the current measure, but only if the jam track is past the count in measures
  var measureElem = document.getElementById('measure');

  // only show the current measure if it's > 0
  if (cleanMeasure > 0) {
    measureElem.innerHTML = cleanMeasure;
  } else {
    measureElem.innerHTML = '';
  }

  // show the section number
  document.getElementById('section').innerHTML = currentSection;
  // show the current chord name
  document.getElementById('chord').innerHTML = currentChord;

  // hide all sections before displaying only the section we want to see
  var allSections = document.getElementsByClassName('section');

  for (var i = 0; i < allSections.length; i++) {
    allSections[i].style.display = 'none';
  }

  // show the currently playing section
  if (currentSection != null) {
    document.getElementById('section-' + currentSection).style.display = 'block';
  } else {
    allSections[0].style.display = 'block';
  }

  // style the current chord in the chord progression
  if (cleanMeasure > 0) {
    // style all measures black
    var measures = document.getElementsByClassName('measure');
    for (var i = 0; i < measures.length; i++) {
       measures[i].style.color = 'black';
    }
    // style current measure red
    document.getElementById('measure-' + cleanMeasure).style.color = 'red';
  }
}

JavaScriptに精通していれば、この関数についての大部分はすぐに理解できるはずなので、すべての部分は説明しません。たいていの場合、この関数は現在のコードや現在の小節などいくつかのデータを取得して表示します。

この関数で詳しく見るべき重要なことは、どのように正しいセクションを表示するのかということです。

// hide all sections before displaying only the section we want to see
var allSections = document.getElementsByClassName('section');

for (var i = 0; i < allSections.length; i++) {
  allSections[i].style.display = 'none';
}

// show the currently playing section
if (currentSection != null) {
  document.getElementById('section-' + currentSection).style.display = 'block';
} else {
  allSections[0].style.display = 'block';
}

各セクションが最初は非表示であることが分かります。そうしておかないと、準備ができ次第各セクションが表示されますが、再生されていないセクションが表示されたままになってしまいます。

再生中の現在のセクションを表示させるために、IDでセクションを選択し、表示プロパティをblockへ設定し直します。

currentSectionがnullの場合は、最初のセクションを表示します。こうしないと、トラックの再生前や、そのセクションが表示されるべき時間に到達する前に、最初のセクションが表示されません。

初期表示のレンダリング

ようやく、2つの関数を実行しました。コード進行をレンダリングするために、renderChordProgression()を1度実行する必要があります。また同様に、jamStation()も1度実行する必要があります。もちろん、jamStation()関数はtimeupdateが発生するたびに実行されますが、自動では1度しか実行されるべきではありません。言い換えると、コード進行は最初からはレンダリングされません。

renderChordProgression();
jamStation();

さらなる見解とアイデア

楽器のコード図の表示をしたいと考えているなら、もっとも重要なコードの名称やタイプなど、必要とするデータをたくさん保持している必要があります。

コード進行が適切なコードにあるときに、コード図のイメージを表示したいとします。currentChord変数内に保存された現在のコードはもうすでに保持されています。

コードは保持されていますから、コードオブジェクトの配列を生成できるのです。

var chords = [
  {
    name: 'A',
    type: '7',
    src: '/images/a7.jpg',
  },
  {
    name: 'D',
    type: '7',
    src: '/images/d7.jpg',
  },
  {
    name: 'E',
    type: '7',
    src: '/images/e7.jpg',
  }
];

このあと、jamStation関数内部のちょっとしたロジックを実行すると、適切なタイミングで適切なコードを表示します。

for (var i = 0; i < chords.length; i++) {
  if (chords[i].name + chords[i].type == currentChord) {
    document.getElementById('chord-diagram').innerHTML = '<img src="' + chords[i].src + '"/>';
  }
}

最後に

もしかすると、みなさんは今回紹介したことを正確に再現したくないと考えているかもしれませんが、それで良いのです。この記事で紹介してきたことは、あらゆるタイプのプロジェクトに適用できます。以下にいくつかのアイデアを示します。

  • ギャラリーやスライダーでの新しいテイク
  • 特定の拍や小節における遷移イメージ
  • ビートに合わせて画像をアニメーション
  • タップする足? だれかのダンスのポーズ?
  • オーディオの視覚化
  • 音楽に同期した、教育目的のビジュアル
  • すばらしいオープンソースJavaScript music notation APIを使ったあらゆること

※本記事はChris Perry、Michaela Lehr、Matt Burnettが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Create a Music Jam Station with Vanilla JavaScript

[翻訳:市川千枝/編集:Livit

Web Professionalトップへ

この記事の編集者は以下の記事をオススメしています