JavaScript
npm
音楽
楽器

どんな(音楽の)コードでも翻訳できる(javascriptの)コードを書いてみた

uzura です。
ファーストサーバ Advent Calendar 2017 の最終日を担当させて頂きます。

8日目も担当させて頂いたんですが、思いのほか反響があった(なんと 160いいね! ありがとうございます…)ので、今回の記事ではその後日譚を書きます。
「また音楽の話かよ」「ファーストサーバって何する会社でしたっけ」といった声がどこからか聞こえて来そうですがどうかお許しください。

TL;DR

chord-translator というのを作りました。

前回の記事のおさらい

  • 作曲や耳コピのサポートをするサービスを(個人で)作ってみたよ!
  • サーバサイドは Ruby on Rails, フロントは React を中心とした javascript で動いてるよ!
  • 図にするとこんな感じの構成だよ! components.png

記事およびサービス公開後の反響

「コード進行単独でいじって遊ぶ人なんてそんなにたくさん居ないでしょ」と雑なことをなんとなく思っていたのですが、前述のいいね数もさることながら、主に Twitter でなかなかの数の反響を頂きました。本当にありがたい限りです。
ただ、元々アドベントカレンダーに間に合うようにひとまず公開したという感じで、実装予定の機能もまだまだある状態だったので、それについては「こういう機能があったらいいなぁ」という要望もたくさん頂きました。
しかし、その中でもあんまり想定してなかったのが、

こういうコードが鳴らないんだけど??

という声が非常に多かったことでした。
前回の記事で
image.png
こんなことを書いていた自分をぶん殴りたい………

鳴らないコードとは

ここから少し音楽的な話になりますのでご了承ください。
今回声が多かったのは主に2つのパターン。

1. いろんな表記法に対応してない!

例えば CM7 というコードは Cmaj7 とか C△7 とか書くことが出来ますが、基本的に1つの記法にしか対応していませんでした。
C#5 C+5 Caug なんかも表記は違いますが全部同じです。

2. とにかく複雑なコード (C7(b9,#11,b13)みたいなやつ) に対応してない!

これについては「そもそもこんな複雑なコード使う必要あるの!?」と思いましたが、ジャズ界隈とかは使い得るみたいですね。
このコードは誇張しすぎな感じがありますが、実際自分でも普通に使い得るコードが鳴らないといったパターンは多々ありました。

前回の記事で使用した tonal は便利なんですが、「特定のコードに完全一致したら翻訳する」といった実装になっていたため、例え tonal の前に自前の翻訳をかませたところで、上記すべてに対応するのは難しそう…という感じでした。
ので、最終的にどんな表記のコードでも柔軟に翻訳できるライブラリを自前で実装しようということになりました。

chord-translator

今回 chord-translator というライブラリを作成しました。
と言っても rechord 内で書いていたコードを切り出しただけで、テストすら書いてない状態ですが…その辺はご了承ください(しかもなんだかんだで途中 tonal も使ってるし…)

基本的な考え方

音楽には 度数 という、音と音の距離を測る単位があります。
簡単に言うと、ドとミは3度、ドと1オクターブ上のドは8度といった具合に、1音離れるごとに1度あがるという仕組みです(ドとミbだと短3度と呼びます)
C というコードは1度・3度・5度の組み合わせです (以下、[1,3,5] と表記します)
CM7 というコードはそこに長7度の音が付与されるという意味なので [1,3,5,7] となります。(C7は短7度(シb)、CM7は長7度(シ)という話もあるのですがその辺は割愛します)
そして CM13 といったコードが通常もっとも音の数が多く、[1,3,5,7,9,11,13] という度数になります。音で言うと ド・ミ・ソ・シb・レ・ファ・ラ です。
13度より上は通常足されないようです。
前述の C7(b9,#11,b13) も音の数は同じで、[1,3,5,b7,b9,#11,b13] といった度数になります(各度数の音に # や b がつく感じです)

音の足し方の法則性

勘の良い方はお気づきかと思いますが、基本的に奇数の度数の音が足されていきます
もちろん C6 といった偶数の音を足すパターンもありますが、その場合は隣り合う7度の音を足すということはまずやりません(たぶん)
つまり、次のように考えられます。

  1. [1,3,5,7,9,11,13] というそれぞれの度数に合わせて7つの箱(=配列変数)を用意しておく
  2. コードの種類に応じてその箱に音を入れていく(普通は0, #は1, bは-1 といった具合)
  3. 最後にその配列を元に構成音を用意する

C7(b9,#11,b13) の例で具体的に言うと

  1. デフォルトをメジャーコード(C)として [0, 0, 0, null, null, null, null] という配列を用意する
  2. 翻訳し、度数に合わせて配列を [0, 0, 0, -1, -1, 1, -1] とする
  3. 最後にそれぞれの度数からの相対的な音を算出して付与する (ド・ミ・ソ・シb・レb・ファ#・ラb)

翻訳の仕方

ここまで来れば、あとは書かれたコードをパースして順番に処理していくだけです。
ある程度「この表記とこの表記は一緒にしない」みたいなのがあるので、その辺は switch 文で処理していきます。

実際のコードはこちら

テンションとomit

() で書かれている部分はテンションと呼びます。
omit は「この度数の音は鳴らさない」という役割のものです (e.g. Comit3 => [1,5])
この2つは正規表現でよけておいて後で処理します。

  const notes = [0, 0, 0, null, null, null, null]
  let baseType = type
  let tension
  let omit

  // テンションの処理
  const tensionRegex = /\((.*)\)/
  const tensionMatch = type.match(tensionRegex)
  if (tensionMatch) {
    tension = tensionMatch[1].replace(/\s+/g, "").split(",")
    baseType = baseType.replace(tensionRegex, "")
  }

  // omit の処理
  const omitRegex = /omit(\d+)/
  const omitMatch = type.match(omitRegex)
  if (omitMatch) {
    omit = omitMatch[1]
    baseType = baseType.replace(omitRegex, "")
  }

いろんなコードの翻訳

  const parseType = (regex) => {
    if (baseType.match(regex)) {
      baseType = baseType.replace(regex, "")
      return true
    } else {
      return false
    }
  }

  // base
  switch (true) {
    case parseType(/^M(?!(7|9|11|13))/): break
    case parseType(/^m(?!aj)/): notes[1] = -1; break
    case parseType(/aug/):      notes[2] = 1;  break
    case parseType(/Φ|φ/):      notes[1] = -1; notes[2] = -1; notes[3] = 0; break
  }
  // +-
  switch (true) {
    case parseType(/\+5|#5/): notes[2] = 1;  break
    case parseType(/-5|b5/):  notes[2] = -1; break
  }
  switch (true) {
    case parseType(/^6/):  notes[3] = -1; break
    case parseType(/^7/):  notes[3] = 0;  break
    case parseType(/^9/):  notes[3] = 0;  notes[4] = 0; break
    case parseType(/^11/): notes[3] = 0;  notes[4] = 0; notes[5] = 0; break
    case parseType(/^13/): notes[3] = 0;  notes[4] = 0; notes[5] = 0; notes[6] = 0; break
  }
  // sus
  switch (true) {
    case parseType(/sus4/): notes[1] = 1;  break
    case parseType(/sus2/): notes[1] = -2; break
  }
  // add
  switch (true) {
    case parseType(/add2/):  notes[4] = -12; break
    case parseType(/add9/):  notes[4] = 0;   break
    case parseType(/add4/):  notes[5] = -12; break
    case parseType(/add11/): notes[5] = 0;   break
    case parseType(/add6/):  notes[6] = -12; break
    case parseType(/add13/): notes[6] = 0;   break
  }
  // M
  switch (true) {
    case parseType(/(M|maj|△|Δ)7/):  notes[3] = 1; break
    case parseType(/(M|maj|△|Δ)9/):  notes[3] = 1; notes[4] = 0; break
    case parseType(/(M|maj|△|Δ)11/): notes[3] = 1; notes[4] = 0; notes[5] = 0; break
    case parseType(/(M|maj|△|Δ)13/): notes[3] = 1; notes[4] = 0; notes[5] = 0; notes[6] = 0; break
  }
  // dim
  switch (true) {
    case parseType(/^(dim|o)7/): notes[1] -= 1; notes[2] -= 1; notes[3] = -1; break
    case parseType(/^(dim|o)/):  notes[1] -= 1; notes[2] -= 1; break
  }

特に説明しなかったコードもありますが、上から順番に 正規表現に合致したら配列を操作 (parseType) して次の処理に進む という感じです。

前述の M7 なんかは正規表現でどの表記でもいけるようにしています。

case parseType(/(M|maj|△|Δ)7/): notes[3] = 1; break

6add2 といった、奇数の度数から外れているものも処理しています。
6 は7度の音と一緒に鳴ることは無いので、7度の箱に -1 を入れて対応しています。

case parseType(/^6/): notes[3] = -1; break

add2 の2度の音は、9度の音の1オクターブ下なので、9度の箱に -12 を入れて対応しています。

case parseType(/add2/): notes[4] = -12; break

退避しておいたテンションとomitの処理

テンションは複数ある想定で、omit は1つしか無い想定で以下のように実装しています。

  // tension
  if (tension) baseType += tension.join("")
  if (parseType(/(#|\+)9/))  notes[4] = 1
  if (parseType(/(b|-)9/))   notes[4] = -1
  if (parseType(/9/))        notes[4] = 0
  if (parseType(/(#|\+)11/)) notes[5] = 1
  if (parseType(/(b|-)11/))  notes[5] = -1
  if (parseType(/11/))       notes[5] = 0
  if (parseType(/(#|\+)13/)) notes[6] = 1
  if (parseType(/(b|-)13/))  notes[6] = -1
  if (parseType(/13/))       notes[6] = 0

  // omit
  switch (omit) {
    case "1":  notes[0] = null; break
    case "3":  notes[1] = null; break
    case "5":  notes[2] = null; break
    case "7":  notes[3] = null; break
    case "9":  notes[4] = null; break
    case "11": notes[5] = null; break
    case "13": notes[6] = null; break
  }

あとは最後に音を算出して構成音を作る、といった具合です。

最後に

最終的にこれを rechord に組み込んで、実際に稼働しています。
「こんな複雑な音も鳴らせるんだ!」って喜んでくれているユーザを見つけた時はとても嬉しかったです。
他にも実装したい機能がありましたが、ユーザの声に耳を傾けて優先して対応する、というのもやはり大事だなと思いました(根っこのコンセプトがブレないようにするのももちろん大事ですが)

:christmas_tree: Merry Christmas!! :christmas_tree:

すてきなホリデイ(クリスマスが今年もやってくるやつ) / 竹内まりや
https://rechord.cc/3mb20-_5-us
今回の記事にはあんまり関係ないですが、rechord で打ち込んでみたので良ければお聴きください。

それでは皆さま、良いお年を。