LoginSignup
87
80

音楽に関連する計算式まとめ

Last updated at Posted at 2022-01-15

はじめに

音楽では、音階、音の長さ、音高など、様々なものが計算可能である.
この記事は、それら計算可能なものについての計算式を考え、まとめるものである.
なお、記事中に例示するコードは C# による.
また、記事中ではシャープとして #、フラットとして b、ダブルシャープとして x などを用いる.
全体的に、私の勘違いやタイプミスなどによる誤りがあり得る点をご留意の上お読みください.

記事中に例示するコードにおいて登場する自作関数などの説明

FloorMod(int x, int y)

x を y で割った剰余を (-abs(y) .. abs(y)) の範囲で返す関数.
ただし、剰余は 0、または y と同じ符号であるものとする. (e.g. FloorMod(-20, 7) = 1)
cf. C# において、x % 7 は、0、または x と同じ符号で剰余を返す. (e.g. -20 % 7 = -6)

定義通りの関数
public static int FloorMod(int x, int y) {
  int q = (int) System.Math.Floor((double) x / y);
  return x - q * y;
}

FloorDiv(int x, int y)

System.Math.Floor((double) x / y) と同等の計算を行う関数.

RoundMod(int x, int y)

x を y で割った剰余を (-abs(y/2) .. abs(y/2)]、または [-abs(y/2) .. abs(y/2)) の範囲で、つまり絶対最小剰余を返す関数. (e.g. RoundMod(-20, 7) = 1)
cf. C# において、x % 7 は、0、または x と同じ符号で剰余を返す. (e.g. -20 % 7 = -6)

定義通りの関数
public static int RoundMod(int x, int y) {
  // System.Math.Round(double, MidpointRounding.AwayFromZero) はいわゆる四捨五入
  int q = (int) System.Math.Round((double) x / y, MidpointRounding.AwayFromZero);
  // x > 0 のとき、返り値の範囲は [-abs(y/2) .. abs(y/2))
  // さもなくば、(-abs(y/2) .. abs(y/2)]
  return x - q * y;
}

Power(int a, int b)

System.Math.Pow(double, double) と同等の計算(=aのb乗)を整数で行う関数. Pow を利用してから int にキャストしてもよいが、
私の好みでないため int 型用の関数を用意している.

繰り返し2乗法による実装
public static int Power(int a, int b) {
  if(b < 0) throw new ArgumentException("Cannot calculate with negative exponents", nameof(b));
  int result = 1;
  while(b != 0) {
    if((b & 1) == 1) result *= a;
    a *= a;
    b >>= 1;
  }
  return result;
}

Rational

分数を表す構造体. 約分された形で保持する.
四則演算子、比較演算子などは一通り実装されているものとする.
コンストラクタは new Rational(int num, int den = 1) であり、
num は分子、den は分母を意味する. (numerator, denominator)

音階(音名)

音高や調号の記述にはよく音名が必要とされるため、音階(音名)と整数とのマッピングを考える.

幹音名

# や b のついていない音を幹音と呼ぶ. この記事では、C を 0、D を 1、... B を 6 として定義する.

幹音名(英語) 幹音名(ドレミ)
C 0
D 1
E 2
F ファ 3
G 4
A 5
B 6
コードによる定義の例
enum NaturalTone {
  C = 0
  , D
  , E
  , F
  , G
  , A
  , B
}

変化記号

# や b など、音高を変化させる記号を指して変化記号と呼ぶ.
ここでは「triple flat」や「quadruple sharp」など、基本的に使いどころはないが定義可能な記号などもあるため、
変化記号が「半音いくつ分変化させるか」によって数値にマッピングする.

記号 英名
bbb triple flat -3
bb double flat -2
b flat -1
natural 0
# sharp 1
x double sharp 2
#x triple sharp 3
コードによる定義の例
int alter;

五度圏

すべての音名は、幹音名と変化記号の組によって表現可能であるが、
ひとつの数字によって任意の音名を表現するために、五度圏を用いることとする.
C = 0 とし、完全五度上は +1、完全五度下は -1 であるものとする.

例:

音名 * 音名 * 音名
Bbb -9 Bb -2 B 5
Fb -8 F -1 F# 6
Cb -7 C 0 C# 7
Gb -6 G 1 G# 8
Db -5 D 2 D# 9
Ab -4 A 3
Eb -3 E 4

(幹音名、変化記号) <=> 五度圏 の変換

コードによる定義の例
public class Tone {
  public NaturalTone NaturalTone { get; }
  public int Alter { get; }
  public int Fifths { get; }

  public Tone(NaturalTone naturalTone, int alter) {
    this.NaturalTone = naturalTone;
    this.Alter = alter;
    this.Fifths = ToFifths(naturalTone, alter);
  }

  public Tone(int fifths) {
    var pair = ToPair(fifths);
    this.NaturalTone = pair.naturalTone;
    this.Alter = pair.alter;
    this.Fifths = fifths;
  }

  // (幹音名、変化記号) => 五度圏
  private static int ToFifths(NaturalTone naturalTone, int alter) {
    // naturalTone を [-1 .. 6) の範囲にマッピングする. (幹音は五度圏で F=-1, ..., B=5 の範囲であるため)
    int x = FloorMod(((int) naturalTone) * 2 + 1, 7) - 1;
    int y = alter * 7;
    return x + y;
  }

  // 五度圏 => (幹音名、変化記号)
  private static (NaturalTone naturalTone, int alter) ToPair(int fifths) {
    int naturalTone = FloorMod(fifths * 4, 7);
    int alter = FloorDiv(fifths + 1, 7);
    return ((NaturalTone) naturalTone, alter);
  }
}

調号

一般的な西洋音楽では12平均律で利用可能な12音の内、特定の7音を選び、それらをメインとして作曲する.
このような特定の音の集合を調(Scale)と呼び、その調ではどの音にどの変化記号が付くかを示すものが調号(Key signature)である.
調は、どの音が主役であるか(主音、Tonic)と、それ以外の音が主音とどのような関係で選ばれるか(調性、Tonality)の組によって表される.
一般に、調性は長調(7音すべてが白鍵のとき、ドを主音とする全音階)、(自然-)短調(7音すべてが白鍵のとき、ラを主音とする全音階)の2種類のみが使われる.
しかしここでは、あえてドとラだけを特別扱いする理由もないためモード(日本語では旋法?)によって調性を表現する.

モード(旋法)

ここで扱うモードは以下の7種類:

名前 7音すべてが白鍵のときの主音 補足
Ionian いわゆる長調
Dorian
Phrygian
Lydian ファ
Mixolydian
Aeolian いわゆる短調
Locrian

コード例

コードによる定義の例
// 後の計算のため、
// (int) Mode.X == (int) NaturalTone.<7音すべてが白鍵のときの主音>
// であるように定義する.
public enum Mode {
  Ionian = 0
  , Dorian
  , Phrygian
  , Lydian
  , Mixolydian
  , Aeolian
  , Locrian
  // 必要があれば、次を定義する:
  // , Major = 0// 長調
  // , Minor = 5// 短調
}

public class KeySignature {
  public Tone Tonic { get; }
  public Mode Mode { get; }

  // 特定の音についている変化記号を辞書によって管理する.
  private readonly Dictionary<NaturalTone, int> _alters = new();
  public int GetAlter(NaturalTone naturalTone) => _alters[naturalTone];

  public KeySignature(Tone tonic, Mode mode) {
    // ダブルシャープ/ダブルフラットなどが現れる場合は例外を送出するものとする.
    // 嬰ホ長調(ミ# ファx ソx ラ# シ# ドx レx) や三重変ト短調のような、
    // 変な調号も許す場合(あるのか?)このチェック処理は不要
    // 次は
    // int modeFifths = Tone.ToFifths((NaturalTone) mode, 0);
    // に等しい. 上で ToFiths を private にしているためこうなっている.
    // 五度圏は使い勝手がよいため、どこかにユーティリティとして定義すべきか.
    int modeFifths = FloorMod(((int) mode) * 2 + 1, 7) - 1;
    if(tonic.Fifths < modeFifths - 7 || modeFifths + 7 < tonic.Fifths) {
      throw new ArgumentException();
    }

    this.Tonic = tonic;
    this.Mode = mode;

    int s = tonic.Fifths - modeFifths - 1;
    for(int i = 0; i < 7; ++i) {
      int fifths = i + s;
      Tone newTone = new Tone(fifths);
      _alters[newTone.NaturalTone] = newTone.Alter;
    }
  }

  // 同主調(英語では parallel key と言う. 後述の平行調は英語で relative key のため、混同しないように)
  // 名前の通り、主音が同じで、調性が異なる調のこと.
  public KeySignature ParallelKey(Mode newMode) => new KeySignature(this.Tonic, newMode);

  // 平行調(前述の parallel key と混同しないように)
  // 構成音が同じで、主音/調性が異なる調のこと. ks._alters と ks.RelativeKey(Mode.*)._alters は等しくなる.
  public KeySignature RelativeKey(Mode newMode) {
      // 煩雑になるので、Tone.ToFifths が public であるものとして書いている.
      int oldModeFifths = Tone.ToFifths((NaturalTone) this.Mode, 0);
      int newModeFifths = Tone.ToFifths((NaturalTone) newMode, 0);

      int newFifths = this.Tonic.Fifths - oldModeFifths + newModeFifths;

       return new KeySignature(new Tone(newFifths), newMode);
  }

  // 属調. 属音を主音とする調. 属音=完全五度上の音.
  // 調号としてはシャープがひとつ増える(=フラットがひとつ減る).
  public KeySignature DominantKey => new KeySignature(new Tone(this.Tonic.Fifths + 1), this.Mode);
  // 下属調. 下属音を主音とする調. 下属音=完全五度下の音.
  // 調号としてはフラットがひとつ増える(=シャープがひとつ減る).
  public KeySignature SubdominantKey => new KeySignature(new Tone(this.Tonic.Fifths - 1), this.Mode);
}

音高

一般に西洋音楽では、音の高さは音名とオクターブの組で表現される.
MIDIでは異名同音の別を無視したノートナンバーが用いられる.
また、単純に「音の高さ」の指標としては周波数(Hz) が使われる.
この節ではこれらの(相互)変換を目指す.
なお、オクターブの表現には国際式、ヤマハ式があるが、この記事中では特に注釈がない限り、国際式に従うものとする.
(<国際式オクターブ> = <ヤマハ式オクターブ> + 1)

(音名, オクターブ)

章題の通り、ただの組である.
国際式の表現に従う場合、ピアノの「真ん中のド」は (C, 4) となる.

ノートナンバー

主に MIDI で使用される表記法であり、ピアノの「真ん中のド」は 60 として定義されている.

周波数(12平均律)

一秒あたりの振動数を表す単位を Hz(ヘルツ) と言う.
12平均律は、現代日本でおそらく最も広く使われている音律である.
これは丁度オクターブ上の周波数を <元の周波数> * 2 として定義し、1オクターブを対数表記で12等分するものである.

各音高表現間の変換

コードによる定義の例
public class Pitch {
  public Tone Tone { get; }
  public int Octave { get; }
  public int NoteNumber { get; }
  public double Frequency { get; }

  public Pitch(Tone tone, int octave) {
    this.Tone = tone;
    this.Octave = octave;

    // diffFromC は、幹音が同じオクターブ内の Cナチュラル から半音いくつ分離れているかを示す.
    int diffFromC = tone.NaturalTone <= NaturalTone.E ? ((int) tone.NaturalTone) * 2 : ((int) tone.NaturalTone) * 2 + 1;
    this.NoteNumber = diffFromC + (octave + 1) * 12;
    // 条件分けが嫌いな場合、次でも計算できる. (変化記号を無視した幹音の五度圏を7倍して12で割った余り = 半音数)
    // int diffFromC = FloorMod((FloorMod(((int) tone.NaturalTone) * 2 + 1, 7) - 1) * 7, 12);
    // あるいは、計算を行わずに switch を使用してもよい
    // int diffFromC = tone.NaturalTone switch {
    //                 NaturalTone.C => 0,
    //                 NaturalTone.D => 2,
    //                 NaturalTone.E => 4,
    //                 NaturalTone.F => 5,
    //                 NaturalTone.G => 7,
    //                 NaturalTone.A => 9,
    //                 NaturalTone.B => 11,
    //                 _ => throw new ArgumentException("tone.NaturalTone")
    // }

    this.Frequency = ToFrequency(NoteNumber);
  }

  public Pitch(int noteNumber) {
    // noteNumber が負の場合も計算できるようにするため FloorMod/FloorDiv を使っているが、
    // 範囲が 0 以上であることを前提とする場合は標準の演算子 (/, %) を使ってよい.
    // diffFromC は、同じオクターブ内の Cナチュラル から半音いくつ分離れているかを示す.
    int diffFromC = FloorMod(noteNumber, 12);
    // 五度圏としての音名を [-6 .. 6) の範囲で得る.
    int fifths = RoundMod(diffFromC * 7, 12);
    this.Tone = new Tone(fifths);
    this.Octave = FloorDiv(noteNumber, 12) - 1;

    this.NoteNumber = noteNumber;
    this.Frequency = ToFrequency(noteNumber);

    // Tone については、単純に switch 文で書くこともできる. バグを嫌うならばこちらが確実.
    // noteNumber では異名同音が区別されないため、Db に代えて C# が良い、とか Gb より F# が使いたい、
    // などの場合も変更が簡明.
    // this.Tone = diffFromC switch {
    //              0 => new Tone(NaturalTone.C,  0),// C
    //              1 => new Tone(NaturalTone.D, -1),// Db
    //              2 => new Tone(NaturalTone.D,  0),// D
    //              3 => new Tone(NaturalTone.E, -1),// Eb
    //              4 => new Tone(NaturalTone.E,  0),// E
    //              5 => new Tone(NaturalTone.F,  0),// F
    //              6 => new Tone(NaturalTone.G, -1),// Gb
    //              7 => new Tone(NaturalTone.G,  0),// G
    //              8 => new Tone(NaturalTone.A, -1),// Ab
    //              9 => new Tone(NaturalTone.A,  0),// A
    //             10 => new Tone(NaturalTone.B, -1),// Bb
    //             11 => new Tone(NaturalTone.B,  0),// B
    // }
  }

  private static double ToFrequency(int noteNumber, int standardPitch = 69 /* =A4 */, double standardFrequency = 440.0) {
    // ノートナンバーが standardPitch の音 を standardFrequency[Hz] とした場合の周波数
    // この例ではこの関数をコンストラクタでしか使わないためデフォルト引数である必要がないが、
    // より一般的な関数として定義する場合のためこうしている. A4=442Hz とするピッチで考えたいときとか.
    // 底を 2 としているのは、丁度オクターブ上の音の周波数が「2」倍であると定めているから.
    // 指数を <ノートナンバーの差> / 12 としているのは、「12」平均律で考えているから.
    return standardFrequency * System.Math.Pow(2.0, (double) (noteNumber - standardPitch) / 12.0);
  }

  public static (int noteNumber, double cents) FromFrequency(double frequency, int standardPitch = 69 /* =A4 */, double standardFrequency = 440.0) {
    // 与えられた周波数がぴったりノートナンバーにマッピングできることはまずない.
    // そのため、ノートナンバーとセントの組に変換する.
    // 1 セント := 半音の100分の1 

    // semitones := 目的の音が、standardPitch から半音いくつ分離れているか
    double semitones = 12 * System.Math.Log2(frequency / standardFrequency);
    // semitones を近いほうの整数に丸める. 常に正のセントが欲しい場合(あるのか?)はここで Floor を使えばよい.
    // ここで使用している Round(double) は銀行家丸めであるため、
    // もし四捨五入した値が欲しい場合は Round(double, MidpointRounding.AwayFromZero) を使うこと.
    int intSemitones = (int) System.Math.Round(semitones);
    double cents = (semitones - intSemitones) * 100;
    return (standardPitch + intSemitones, cents);
  }
}

音律

周波数は実数で表されるように、ピアノの C と Db との間には無限種類の音高が存在する.
その無限種類の音高から、例えばピアノで鳴らせる88種類(オクターブの違いを無視すれば12種類)の音高を選び出すためのルールが音律である.
現代においては普通、上記コード例中の Pitch.ToFrequency のように12平均律さえ計算できれば十分だろうが、過去には様々な音律が考案され使われていた.

自然倍音

音の高さは周波数によって表現されるが、楽器や声などでは基本となる周波数のほかにその自然数倍の周波数成分を多く含むことが知られており、この「自然数倍の周波数」を倍音と呼ぶ.
音律は「美しい」音のために開発、改良が続けられたのだが、基準となる音とよく調和する(=うなりを伴わない)音こそが「美しい」音と見なされていた.
ある音とその自然倍音を鳴らせば、自然倍音の周波数は「ある音」自身が含む周波数成分に一致するためよく調和する.
この点で音律の基礎は自然倍音にあると言え、実際これから書く音律の中には第3倍音、第5倍音がそのまま登場する(ここで扱うつもりはないが、7倍音以上も使う音律は存在するらしい).

参考として第n倍音の表を付す(偶数倍音はオクターブの違いでしかないため2を除いて省略する):

n 平均律上で近い音 平均律における音との差(セント)
1 C2 0
2 C3 0
3 G3 1.9550008653874684
5 E4 -13.686286135165204
7 Bb4 -31.174093530874813
9 D5 3.9100017307745816
11 F#5 -48.682057635243225
13 Ab5 40.52766176931044
15 B5 -11.731285269777914

ピタゴラス音律

ピタゴラス音律では第2、第3倍音を駆使して音の集合を得る.
つまり、第3倍音をオクターブ以内に縮めた音程(周波数にして 3/2 倍)を繰り返し適用することで、
同じオクターブに存在する別の音を得る.

音階(音名)の節で定めた五度圏を用いれば簡単に求められる:

コードによる定義の例
// ピタゴラス音律に基づいて pitch の周波数を得る.
// standardPitch: 基準となる音高
// standardFrequency: 基準となる周波数
// e.g. A4 = 440Hz として、C5 の周波数を求めたい場合は次のように呼び出す:
// ToFrequencyByPythagorean(new Pitch(new Tone(NaturalTone.C, 0), 5), new Pitch(new Tone(NaturalTone.A, 0), 4), 440.0)
public static double ToFrequencyByPythagorean(Pitch pitch, Pitch standardPitch, double standardFrequency) {
  // 基準となる周波数に周波数比をかけることによって目的の値を得る
  return standardFrequency * ToFrequencyRatioByPythagorean(pitch, standardPitch);
}

// ピタゴラス音律に基づいて基準となる周波数との比を得る.
public static Rational ToFrequencyRatioByPythagorean(Pitch pitch, Pitch standardPitch) {
  // 基準から見て、五度圏をいくつ上がるか
  int fifths = pitch.Tone.Fifths - standardPitch.Tone.Fifths;
  // 基準から見て、オクターブをいくつ上がるか
  int octaves = pitch.Octave - standardPitch.Octave;

  // (3/2)^fifths を行う間に、いくつのオクターブを跨ぐかを考慮する必要もある
  octaves -= FloorDiv(fifths * 4, 7);

  // 目的の値は (3/2)^fifths * 2^octaves であるから、2, 3 の指数は以下の通りとなる:
  int twos = -fifths + octaves;
  int threes = fifths;

  // Max := System.Math.Max
  int num = Power(2, Max( twos, 0)) * Power(3, Max( threes, 0));
  int den = Power(2, Max(-twos, 0)) * Power(3, Max(-threes, 0));

  return new Rational(num, den);
}

ピタゴラス音律における問題点(ピタゴラスコンマとウルフの五度)

以下は ToFrequencyRatioByPythagorean(new Pitch(new Tone(fifths), 4), (new Tone(0), 4)) の結果についての表である:

fifths 音名
-12 Dbb 524288/531441
-11 Abb 262144/177147
-10 Ebb 65536/ 59049
-9 Bbb 32768/ 19683
-8 Fb 8192/ 6561
-7 Cb 2048/ 2187
-6 Gb 1024/ 729
-5 Db 256/ 243
-4 Ab 128/ 81
-3 Eb 32/ 27
-2 Bb 16/ 9
-1 F 4/ 3
0 C 1/ 1
1 G 3/ 2
2 D 9/ 8
3 A 27/ 16
4 E 81/ 64
5 B 243/ 128
6 F# 729/ 512
7 C# 2187/ 2048
8 G# 6561/ 4096
9 D# 19683/ 16384
10 A# 59049/ 32768
11 E# 177147/131072
12 B# 531441/262144

現代において広く用いられている平均律とは異なり、C、Dbb、B# はすべて異なる音となる.
(上の表では B#4=531441/262144 となっているが、C4 と異名同音になるのは B#3 であり、これは <B#4 の周波数> / 2 である)
この異名同音(「同音」ではないが)との差である $ 3^{12} / 2^{19} = 531441 / 524288 $ はピタゴラスコンマと呼ばれている.
音程にすると半音の 1/5 から 1/4 の間になり、決して小さくない差である(らしい).

無限に純正な完全五度(3/2)を積み重ねられる場合はともかくとして、ピアノのように異名同音は区別なくひとつの鍵盤としなければならないような楽器においてはピタゴラスコンマが問題となる.

C を基準として完全五度で下に5つ分(Db)から上に6つ分(F#)の範囲で調律した場合、
F# から Db の重減六度(平均律の場合は完全五度に等しい)の音程(音高の比)は純正な完全五度よりもピタゴラスコンマひとつ分だけ狭い.
(Db から完全五度下がった Gb は F# とピタゴラスコンマひとつ分だけ離れているため)
この純正な完全五度よりもずっと狭い音程はかなりの不協和を生むため、狼のうなり声に例えてウルフの五度、あるいは単にウルフと呼ばれている. (人名などではなく動物の狼なので日本語では狼の五度となりそうなものの、「ウルフ」と呼ぶ慣習らしい)

ウルフの五度はピタゴラス音律に限らず、このような調和しない五度を指して使われる.

ピタゴラス音律における問題点(シントニックコンマ)

ピタゴラス音律における長三度は 81/64 (上の表における E の行)であるが、このすぐ近くには第5倍音を2オクターブ下げた音 (比にして 5/4)というよく調和する音が存在する.
そのためピタゴラス音律における長三度は不協和に聞こえ、ハーモニーの美しさを損なうものであった.
この 81/64 と 5/4 との比、$ (81/64) \div (5/4) = 81/80 $ のことをシントニックコンマ(S.C.)と呼ぶ.
そこで登場するのが次に紹介する純正律である.

Five-limit temperament(含:純正律)

ピタゴラス音律は第3倍音(純正な完全五度)を積み重ねることで成り立っていたが、三度の音は調子が完全ではなかった.
そこで、完全五度、「純正な」長三度(比にして 5/4)ふたつ(完全八度も数えればみっつ)の組み合わせによってきれいな三度を手に入れようとしたことは蓋し当然のことと言える.
こうして作られたのが純正律である.

純正律は下に示すような平面空間によって記述できる:
tonnetz_open.png

この図はひとマス上に移動することは完全五度上昇することを意味し、左斜め上にひとマス移動することは長三度上昇することを意味している.
(同じような目的でオイラー格子と呼ばれる格子/表が用いられるが、私は馴染めなかったためヘクスグリッドで表示する)

図でマスの色を変えて強調しているように、この平面上には同名の音が複数(無限に)存在する.
つまり、完全十五度上の音(完全五度で4つ上がり、長三度ひとつ分下がった音)が登場するのだ.

しかも、この完全十五度上は第4倍音とは異なるため、この図からどの音を採用するかによってまたバリエーションが生ずる.
普通純正律と言った場合は、完全五度(図中上下の動き)は -1 から +2、長三度(図中左上と右下を結ぶ斜めの動き) は -1 から +1 の範囲で
音を取るものと思われる(あまり確証はないためご指摘いただければ幸いに存じます).

しかし、当記事では五度圏を(実用性はともかく)無限に伸ばせるという形で定義しているため、図中から音名の重複も漏れもなく無限に採用できる形で定義する.

五度圏の値からへクスグリッドのマスにマッピングするが、以下に示す3種類の関数について記述する;

関数0 (どの音からも純正長三度上下の音が使用可能な音律):
tonnetz_0_major_thirds.png

関数1 (どの音からも純正律における短三度上下の音が使用可能な音律):
tonnetz_1_minor_thirds.png

関数2 (オクターブの調整をなるべく減らした音律):
tonnetz_2_horizontal.png

コードによる定義の例
// 周波数を求めるための関数における共通部分.
// X は 0, 1, 2 のいずれかに書き換えて使われたし.
public static Rational ToFrequencyX(Pitch pitch, Pitch standardPitch, double standardFrequency) {
  // 基準から見て、五度圏をいくつ上がるか
  int fifths = pitch.Tone.Fifths - standardPitch.Tone.Fifths;
  // 基準から見て、オクターブをいくつ上がるか
  int octaves = pitch.Octave - standardPitch.Octave;

  return standardFrequency * ToRatioX(fifths, octaves);
}

// 周波数の比を求めるための関数における共通部分.
// X は 0, 1, 2 のいずれかに書き換えて使われたし.
public static Rational ToRatioX(int fifths, int additionalOctaves) {
  // 五度圏の値から完全五度、長三度の組に変換する
  (int _fifths, int thirds) = ToIntervalPairX(_fifths);
  
  // 求める値は、オクターブの違いを無視して次のようになる(f := _fifths, t := thirds)
  //   (3/2)^f * (5/4)^t
  // = 3^f * 2^(-f) * 5^t * 2^(-2t)
  // = 2^(-f-2t) * 3^f * 5^t
  int twos   = -_fifths- thirds * 2;
  int threes = _fifths;
  int fives  = thirds;
  
  // 引き渡されたオクターブの分だけ補正する
  twos += additionalOctaves;

  // 完全五度、長三度の繰り返しによってオクターブを跨いだ場合はそれを補正する
  int degrees = _fifths * 4 + thirds * 2;
  int octaves = FloorDiv(degrees, 7);
  twos -= octaves;
  
  int num = Power(2, Max( twos, 0)) * Power(3, Max( threes, 0)) * Power(5, Max( fives, 0));
  int den = Power(2, Max(-twos, 0)) * Power(3, Max(-threes, 0)) * Power(5, Max(-fives, 0));
  
  return new Rational(num, den);
}

// 関数 0
public static (int fifths, int thirds) ToIntervalPair0(int fifths) {
  int perfectFifths = FloorMod(fifths + 1, 4) - 1;
  int majorThirds = FloorDiv(fifths + 1, 4);
  return (perfectFifths, majorThirds);
}

// 関数 1
public static (int fifths, int thirds) ToIntervalPair1(int fifths) {
  int perfectFifths = FloorMod(fifths + 1, 3) - 1;
  int minorThirds = -FloorDiv(fifths + 1, 3);
  return (perfectFifths + minorThirds, -minorThirds);
}

// 関数 2
public static (int fifths, int thirds) ToIntervalPair2(int fifths) {
  int a = FloorDiv(fifths + 1, 7);
  int b = -2 * a;
  int c = fifths - a * 7;
  int d = b - (c <= 1? 0 : 1);
  
  int e = (2 * fifths) + d * 7;
  int f = -d;
  
  int newFifths = (e - f) / 2;
  int newThirds = f;
  return (newFifths, newThirds);
}

それぞれの関数で比を求めると以下のようになる:

P5 Name Method 0 [(P5s, M3s) -> Rational] Method 1 Method 2
-12 Dbb ( 0, -3) -> 128/125 ( 4, -4) -> 648/625 ( 0, -3) -> 128/125
-11 Abb ( 1, -3) -> 192/125 ( 5, -4) -> 972/625 ( 1, -3) -> 192/125
-10 Ebb ( 2, -3) -> 144/125 ( 2, -3) -> 144/125 ( 2, -3) -> 144/125
-9 Bbb ( -1, -2) -> 128/ 75 ( 3, -3) -> 216/125 ( 3, -3) -> 216/125
-8 Fb ( 0, -2) -> 32/ 25 ( 4, -3) -> 162/125 ( 0, -2) -> 32/ 25
-7 Cb ( 1, -2) -> 24/ 25 ( 1, -2) -> 24/ 25 ( 1, -2) -> 24/ 25
-6 Gb ( 2, -2) -> 36/ 25 ( 2, -2) -> 36/ 25 ( 2, -2) -> 36/ 25
-5 Db ( -1, -1) -> 16/ 15 ( 3, -2) -> 27/ 25 ( -1, -1) -> 16/ 15
-4 Ab ( 0, -1) -> 8/ 5 ( 0, -1) -> 8/ 5 ( 0, -1) -> 8/ 5
-3 Eb ( 1, -1) -> 6/ 5 ( 1, -1) -> 6/ 5 ( 1, -1) -> 6/ 5
-2 Bb ( 2, -1) -> 9/ 5 ( 2, -1) -> 9/ 5 ( 2, -1) -> 9/ 5
-1 F ( -1, 0) -> 4/ 3 ( -1, 0) -> 4/ 3 ( -1, 0) -> 4/ 3
0 C ( 0, 0) -> 1/ 1 ( 0, 0) -> 1/ 1 ( 0, 0) -> 1/ 1
1 G ( 1, 0) -> 3/ 2 ( 1, 0) -> 3/ 2 ( 1, 0) -> 3/ 2
2 D ( 2, 0) -> 9/ 8 ( -2, 1) -> 10/ 9 ( -2, 1) -> 10/ 9
3 A ( -1, 1) -> 5/ 3 ( -1, 1) -> 5/ 3 ( -1, 1) -> 5/ 3
4 E ( 0, 1) -> 5/ 4 ( 0, 1) -> 5/ 4 ( 0, 1) -> 5/ 4
5 B ( 1, 1) -> 15/ 8 ( -3, 2) -> 50/ 27 ( 1, 1) -> 15/ 8
6 F# ( 2, 1) -> 45/ 32 ( -2, 2) -> 25/ 18 ( -2, 2) -> 25/ 18
7 C# ( -1, 2) -> 25/ 24 ( -1, 2) -> 25/ 24 ( -1, 2) -> 25/ 24
8 G# ( 0, 2) -> 25/ 16 ( -4, 3) -> 125/ 81 ( 0, 2) -> 25/ 16
9 D# ( 1, 2) -> 75/ 64 ( -3, 3) -> 125/108 ( -3, 3) -> 125/108
10 A# ( 2, 2) -> 225/128 ( -2, 3) -> 125/ 72 ( -2, 3) -> 125/ 72
11 E# ( -1, 3) -> 125/ 96 ( -5, 4) -> 625/486 ( -1, 3) -> 125/ 96
12 B# ( 0, 3) -> 125/ 64 ( -4, 4) -> 625/324 ( 0, 3) -> 125/ 64

純正律における問題点(ふたつの全音)

純正律においては完全十五度(P5 * 4 - M3)上下した同名の音からいずれかを選択することになるのだが、この完全十五度は周波数比にして 81/20 であり、 2オクターブ と比べて 81/80 = シントニックコンマひとつ分だけ大きい.

この差があるため、全音(=長二度)を
長9度上(P5 * 2)のオクターブ下として考えると $ (3/2)^2 * 2^{-1} = 9/8 $ であり、
単7度下(M3 - P5 * 2)のオクターブ上として考えると $ (5/4) \div (3/2)^2 * 2 = 10/9 $ となる.
このうち大きいほうの全音(9/8)を大全音、小さいほう(10/9)を小全音と呼ぶ.
こうして2種類の全音が現れてしまうため、転調をしたり異なる調の曲を続けて演奏したりする場合には大きな制約となってしまう.

中全音律

ピタゴラス音律では長三度こそ汚いが、ウルフの五度にさえ気を付ければ多少の転調が可能である.
純正律では美しい長三度を使用可能だが、全音が2種類あっては転調に支障をきたす.
そこで、美しい長三度をあきらめず、全音を大全音と小全音の間に1種類に限る試みの中で中全音律が開発されたそう.

1/4 コンマ中全音律

単に中全音律と呼んだ場合、基本的にはこれを意味する.
ピタゴラス音律では純正な完全五度を4回重ねて長17度を作り、そこから2オクターブ下げて長三度を得る.
この「純正な完全五度を4回重ねて」という過程でシントニックコンマ(S.C.)の分だけずれてしまっていることが問題であるから、
S.C.を4分割して、1/4 S.C. だけ完全五度を短くすれば理想的な長三度を得られる、 というのがこの音律の目指すところである.
完全五度は純正でなくなるものの、純正な長三度の響きには代えられないというのだ.
この新しい完全五度を求めると、

$ (S.C.)^{1/4} = (81 / 80)^{1 / 4} = (3 / 2) \times 5^{-1/4} $
$ { 新しい完全五度 } = (3/2) \div ((3 / 2) \times 5^{-1/4}) = 5^{1/4} $

となり、5の四乗根であることが分かる.
完全五度としてこの値を使うほかは、ピタゴラス音律と全く同様の方法で構成される.

1/3 コンマ中全音律

1/4 コンマ中全音律では長三度について純正律のそれを使いたい、というモチベーションであった.
それに対して 1/3 コンマ中全音律 は、短三度を純正律のそれにしようとして生まれたものである.
名前の通り、新しい完全五度は S.C. の 1/3 だけ狭いものが使われる.

$ (S.C.)^{1/3} = (81 / 80)^{1 / 3} $
$ { 新しい完全五度 } = (3/2) \div (81 / 80)^{1 / 3} = (10 / 3) ^ {1/3} $

2/7 コンマ中全音律

長三度、短三度それぞれについて、純正律とまったく同じものを使いたいという動機から上記の中全音律が作られたのだが、
両方の三度を使いたいという需要は当然存在した.
そこで両方とも純正律とまったく同じというわけにはいかないものの、等しく 1/7 S.C. だけずれている音律が開発された.
それが 2/7 コンマ中全音律 である.

$ (S.C.)^{2/7} = (81 / 80)^{2 / 7} $
$ { 新しい完全五度 } = (3/2) \div (81 / 80)^{2 / 7} = (50 / 3) ^ {1/7} $

コード例

コードによる定義の例
// ここにおいて、Pow := System.Math.Pow(double, double)
// ratioByPerfectFifths の値を除いて違いがないため、1/4 コンマ中全音律だけを示す.

// 1/4 コンマ中全音律に基づいて pitch の周波数を得る.
public static double ToFrequencyByQuaterCommaMeantone(Pitch pitch, Pitch standardPitch, double standardFrequency) {
  // 新しい完全五度は上の方で述べた通り 5^(1/4):
  double ratioByPerfectFifths = Pow(5.0, 1.0 / 4.0);
  return ToFrequencyByMeantone(pitch, standardPitch, standardFrequency, ratioByPerfectFifths);
}

// 中全音律に基づいて pitch の周波数を得る.
// standardPitch: 基準となる音高
// standardFrequency: 基準となる周波数
// ratioByPerfectFifths: 新しい完全五度
// e.g. A4 = 440Hz として、C5 の周波数を求めたい場合は次のように呼び出す:
// ToFrequencyByMeantone(new Pitch(new Tone(NaturalTone.C, 0), 5), new Pitch(new Tone(NaturalTone.A, 0), 4), 440.0, ratioByPerfectFifths)
public static double ToFrequencyByMeantone(Pitch pitch, Pitch standardPitch, double standardFrequency, double ratioByPerfectFifths) {
  // 基準となる周波数に周波数比をかけることによって目的の値を得る
  return standardFrequency * ToFrequencyRatioByMeantone(pitch, standardPitch, ratioByPerfectFifths);
}

// 中全音律に基づいて基準となる周波数との比を得る.
public static double ToFrequencyRatioByMeantone(Pitch pitch, Pitch standardPitch, double ratioByPerfectFifths) {
  // 基準から見て、五度圏をいくつ上がるか
  int fifths = pitch.Tone.Fifths - standardPitch.Tone.Fifths;
  // 基準から見て、オクターブをいくつ上がるか
  int octaves = pitch.Octave - standardPitch.Octave;

  // (3/2)^fifths を行う間に、いくつのオクターブを跨ぐかを考慮する必要もある
  octaves -= FloorDiv(fifths * 4, 7);

  // 目的の値は (新しい完全五度)^fifths * 2^octaves である:
  return Pow(ratioByPerfectFifths, fifths) * Pow(2.0, octaves);
}

音程

音と音との高さの差を音程という.
音程は基本的に距離であるから、負の音程というのはおかしいように感じるが、
減1度は定義不能で減8度は定義可能、のような場合分けは煩雑である(特に単音程と複音程を行き来するような場合)から、
<音高> + <音程> = <より低い音高>
となるような音程の存在を認める形で考える.

音程の定義

音度表現

音楽において、同じオクターブ上のドとドは1度、ドとレは2度、...、のように度数を数えていく.
同じ音高同士が 0 ではなく 1 なので、植木算的な計算が求められる.

また、度数より細かい差は補助的な接頭辞によって表現される.
同じオクターブ上のドとファbbは重減4度(doubly diminished fourth)
同じオクターブ上のドとファbは減4度(diminished fourth)
同じオクターブ上のドとファは完全4度(perfect fourth)
同じオクターブ上のドとファ#は増4度(augmented fourth)
同じオクターブ上のドとファxは重増4度(doubly augmented fourth)
同じオクターブ上のドとファ#x(ファのトリプルシャープ)は3重増4度(triply augmented fourth、重々増4度とも)
などと表現される.
重~でさえまれなので、余程変な計算をしない限り3重~などを見かけることはないだろうけれど、一応存在する体で書く.

ややこしいことに、完全n度と呼ばれる音程を持つがある n と、完全とは決してつながらずに長n度/短n度を持つ n とがある.

コード例

コードによる定義の例
// 「補助的な接頭辞」. 日本語でこれを何と呼ぶのが標準的なのかは不明.
public enum Quality {
  Diminished // 減
  , Minor    // 長
  , Perfect  // 完全
  , Major    // 短
  , Augmented// 増
}

public class Interval {
  // 増/減 の数. augmented fourth なら 1、triply diminished fourth なら 3、minor sixth なら 0
  public int Multiplicity { get; }
  // 「補助的な接頭辞」
  public Quality Quality { get; }
  // 度数. 1以上の整数であるから、uint にしてもよいかもしれない.
  public int Degrees { get; }
  // 半音数. この記事においては、重減1度なら -1 など、負の数も許容する.
  public int Semitones { get; }

  public Interval(Multiplicity multiplicity, Quality quality, int degrees) {
    // 渡された音程を単音程(オクターブ、オクターブ未満の度数)に変換する
    int octave = FloorDiv(degrees - 1, 7);
    int simpleDegrees = FloorMod(degrees - 1, 7) + 1;

    // 完全n度を持つ n であるか?
    bool canBePerfect = simpleDegrees == 1 || simpleDegrees == 4 || simpleDegrees == 5;
    // 接頭辞によって、完全/長n度から半音いくつ分動くか
    int alter = quality switch {
                Quality.Diminished => -multiplicity - (canBePerfect? 0 : 1),
                Quality.Minor      => -1,
                Quality.Perfect    => 0,
                Quality.Major      => 0,
                Quality.Augmented  => multiplicity,
                _ => throw new ArgumentException(nameof(quality)),
    };
    // 完全/長n度が半音いくつ分か
    int semitones = simpleDegrees switch {
                1 => 0,
                2 => 2,
                3 => 4,
                4 => 5,
                5 => 7,
                6 => 9,
                7 => 11,
                _ => throw new NotImplementedException(),
    };
    this.Multiplicity = multiplicity;
    this.Quality = quality;
    this.Degrees = degrees;
    this.Semitones = semitones + alter + octave * 12;

    // semitones は次でも求められる:
    // int semitones = FloorMod((FloorMod((simpleDegrees - 1) * 2 + 1, 7) - 1) * 7, 12);
  }

  public Interval(int degrees, int semitones) {
    // 渡された音程を単音程(オクターブ、オクターブ未満の度数)に変換する
    int octave = FloorDiv(degrees - 1, 7);
    int simpleDegrees = FloorMod(degrees - 1, 7) + 1;

    // 完全n度を持つ n であるか?
    bool canBePerfect = simpleDegrees == 1 || simpleDegrees == 4 || simpleDegrees == 5;

    // 完全/長n度が半音いくつ分か
    int semitonesPerfectOrMajor = simpleDegrees switch {
                1 => 0,
                2 => 2,
                3 => 4,
                4 => 5,
                5 => 7,
                6 => 9,
                7 => 11,
                _ => throw new NotImplementedException(),
    };
    int alter = semitones - (semitonesPerfectOrMajor + octave * 12);

    this.Degrees = degrees;
    this.Semitones = semitones;

    if(alter == 0)        { this.Multiplicity = 0;        this.Quality = canBePerfect? Quality.Perfect : Quality.Major; }
    else if(alter > 0)    { this.Multiplicity = alter;    this.Quality = Quality.Augmented; }
    else if(canBePerfect) { this.Multiplicity = -alter;   this.Quality = Quality.Diminished; }
    else if(alter == -1)  { this.Multiplicity = 0;        this.Quality = Quality.Minor; }
    else                  { this.Multiplicity = -alter-1; this.Quality = Quality.Diminished; }

    // semitonesPerfectOrMajor は次でも求められる:
    // int semitonesPerfectOrMajor = FloorMod((FloorMod((simpleDegrees - 1) * 2 + 1, 7) - 1) * 7, 12);
  }

  // 音程の和
  public static Interval operator+(Interval lhs, Interval rhs) {
    int degrees = lhs.Degrees + rhs.Degrees - 1;
    int semitones = lhs.Semitones + rhs.Semitones;
    return new Interval(degrees, semitones);
  }

  // 音高と音程の和
  public static Pitch operator+(Pitch lhs, Interval rhs) {
    int newNoteNumber = lhs.NoteNumber + rhs.Semitones;
    int naturalTone = ((int) lhs.Tone.NaturalTone) + rhs.Degrees - 1;

    NaturalTone newNaturalTone = (NaturalTone) (naturalTone % 7);
    int newOctave = lhs.Octave + (naturalTone / 7);

    int diffFromC = newNaturalTone <= NaturalTone.E ? ((int) newNaturalTone) * 2 : ((int) newNaturalTone) * 2 + 1;
    int noteNumberAsNatural = diffFromC + newOctave * 12;

    int alter = newNoteNumber - noteNumberAsNatural;

    return new Pitch(new Tone(newNaturalTone, alter), newOctave);

    // diffFromC は次でも求められる:
    // int naturalToneFifths = floorMod(((int) newNaturalTone) * 2 + 1, 7) - 1;
    // int diffFromC = FloorMod(naturalToneFifths * 7, 12);
  }
}

音の長さ(音価)

音楽において、音価は音符(休符)の種類、付点の数、連符の組によって表現され、
BPMによって秒などの時間単位と変換される.

楽譜的な音価表現

音符(休符)の種類

全音符の長さを 1 とし、二分音符は 1/2、四分音符は 1/4、... として分数で表現できる.
この記事では、音符の種類の表現に整数 n を使い、分数としての長さは 2^(-n) であるものとして定義する.

コードによる定義の例
public enum NoteValueType {
  Maxima = -3,// 日本語名称も用例も不詳だが、英語版 Wikipedia の Note value のページに記述があるためここから始める
  Longa,
  Breve,
  Semibreve,  // 全音符
  Minim,      // 二分音符
  Crotchet,   // 四分音符
  Quaver,     // 八分音符
  Semiquaver,
  Demisemiquaver,
  Hemidemisemiquaver,
}

付点

音符(休符)につけられる点は、直前の音価の 1/2 を持つものとされる.
そのため、
<四分音符> <付点>
とある場合、全体としての音価は
<四分音符=1/4> <付点=直前の音価の 1/2>
=> <四分音符=1/4> <付点=直前の音価の 1/2=1/8>
=> 1/4 + 1/8
=> 3/8
となる.

注意が必要なのは点が複数になった場合で、
<四分音符> <付点> <付点>
とある場合、全体としての音価は
<四分音符=1/4> <ひとつ目の付点=直前の音価の 1/2> <ふたつ目の付点=直前の音価の 1/2>
だが、ふたつ目の付点にとって「直前の音価」とは「ひとつ目の付点」のみを指す.
したがって、この場合全体としての音価は 3/8 + (3/8 * 1/2) = 9/16 とはならず、
<四分音符=1/4> <ひとつ目の付点=直前の音価の 1/2=1/8> <ふたつ目の付点=直前の音価の 1/2=1/16>
=> 1/4 + 1/8 + 1/16
=> (4 + 2 + 1) / 16
=> 7/16
となる.

連符

通常、単に3連符や5連符などひとつの数字のみで呼ばれる.
しかし、まれに八分音符の2連符で付点八分音符ふたつ分の音価とするような記法があるらしく、数字ひとつで連符表現とするのは考察が面倒なため、
3連符を 3:2 連符、5連符を 5:4 連符などとする記法にならい、比率で表現することとする.
つまり、<全体を何分割するか>:<基準となる音いくつ分の音価か> の形で保持する.

コードによる定義の例
public class Tuplet {
  // 全体を何分割するか
  public int Actual { get; }
  // 基準となる音いくつ分の音価か
  public int Normal { get; }

  public static Tuplet CreateStandardTuplet(int n) {
    // 一般的な n連符を比の形にする
    // Actual が n のとき、Normal としては Pow(2, Floor(Log2(n))) が使われる.
    int actual = n;
    int normal = 1 << System.Numerics.BitOperations.Log2(unchecked((uint) n));
    return new Tuplet(actual, normal);

    // 例:
    //  3連符 ->  3: 2
    //  5連符 ->  5: 4
    //  7連符 ->  7: 4
    //  9連符 ->  9: 8
    // 11連符 -> 11: 8
    // 13連符 -> 13: 8
    // 15連符 -> 15: 8
    // 17連符 -> 17:16

    // ここに書いた定義では 2連符 -> 2:2 となるが、
    // 2:3 になるようにすべきかは不明.
    // もしそうしたい場合は以下のような形か?
    // if(actual == normal) return new Tuplet(actual, normal + (normal >> 1) /* または (normal / 2 * 3) */);
    // else                 return new Tuplet(actual, normal);
  }

  // コンストラクタ略
  // 連符を使わない場合は new Tuplet(1, 1) を使うものとする.
}

コード例

コードによる定義の例
public class NoteValue {
  public NoteValueType Type { get; }
  public int Dots { get; }
  public Tuplet Tuplet { get; }
  // 分数としての音の長さ
  public Rational Length { get; }

  public NoteValue(NoteValueType type, int dots, Tuplet tuplet) {
    int typeAsInt = (int) type;
    Rational baseLength =  typeAsInt >= 0? new Rational(1, 1 << typeAsInt) : new Rational(1 << -typeAsInt);

    // 付点の数から、元の音価の何倍の長さにすればよいかを求める.
    int den = 1 << dots;
    int num = (den << 1) - 1;
    Rational ratioByDots = new Rational(num, den);

    // 連符の比から、元の音価の何倍の長さにすればよいかを求める.
    Rational ratioByTuplet = new Rational(tuplet.Actual, tuplet.Normal);

    this.Type = type;
    this.Dots = dots;
    this.Tuplet = tuplet;
    this.Length = baseLength * ratioByDots * ratioByTuplet;

    // ratioByDots の求め方について:
    // 求めるべきは Σ[k in [0 .. dots]] 1/2^k であるから、等比数列の和として
    // (2^(k+1)-1) / 2^k で求められる(上記のコード例)
    // 愚直に計算する場合、定義そのまま以下のようにできる
    // Rational ratioByDots = new Rational(0);
    // for(int k = 0; k <= dots; ++k) {
    //   ratioByDots += new Rational(1, 1 << k);
    // }
  }
}

BPM(M.M.)

一分間当たりの拍数を BPM と呼ぶ. 通常この「拍」は四分音符ひとつ分の長さを指すが、
楽譜上 <基準となる音価>=<一分間当たりの拍数> と記述されることもあるため、拍が四分音符でないことも考慮して扱う.

コードによる定義の例
public class MM /* Maelzel's Metronome */ {
  public NoteValue Beat { get; }
  // 基準が四分音符でないときも BPM と呼ぶかどうかは不明だが、とりあえず BPM を採用する
  public double BPM { get; }
  // Microseconds Per Quater note, 四分音符ひとつあたりの時間[us]
  public double MPQ { get; }

  public MM(NoteValue beat, double bpm) {
    this.Beat = beat;
    this.Bpm = bpm;

    // 指定された拍、bpm から、拍=四分音符としたときの bpm を求める.
    double quaterNotesPerMinute = bpm * (beat.Length * 4);
    // 四分音符ひとつあたりの時間[us] を求める.
    // 1分 = 60秒 = 60,000ミリ秒 = 60,000,000マイクロ秒
    this.MPQ = 60_000_000 / quaterNotesPerMinute;
  }

  public MM(double mpq) {
    // 「1拍」=四分音符で固定して計算する
    this.Beat = new NoteValue(NoteValueType.Crotchet, 0, new Tuplet(1, 1));
    // 1分 = 60秒 = 60,000ミリ秒 = 60,000,000マイクロ秒
    this.Bpm = 60_000_000 / mpq;

    this.MPQ = mpq;
  }
}

(音価、BPM) => 秒数(マイクロ秒)

コードによる定義の例
public static double ToMicroSeconds(NoteValue noteValue, MM mm) {
  // 拍当たりの音価(mm.Beat) で noteValue.Length を割り、MPQ をかける
  return ((double) (noteValue.Length / mm.Beat)) * mm.MPQ;
}

Base-40 system

[2022-12-17 追記]

音高、音程の表現のため上で色々書いていたが、シャープとフラットふたつまでならば こんなもの もあるらしい.

87
80
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
87
80