1 package camidion.chordhelper.music;
3 import java.util.Arrays;
5 import java.util.Objects;
8 * 音名(オクターブ抜き)を表すクラスです。値は不変です。
10 * <p>この音名は、メジャーキーの調号にした場合に
12 * 「五度圏インデックス値」で保持することを基本としています。
14 * しかも音楽理論的な計算を極めて単純な数式で行えるようになります。
15 * この方式はMIDIメタメッセージで調号を指定するときにも使われていて、
21 * 音階や調を表すシンボルの言語モードによる違いを定義します。
22 * <p>音名には、下記のような五度圏順のインデックス値(0~34)が割り当てられます。
23 * <pre>Fbb=0, Cbb=1, .. Bb=13, F=14, C=15, .. B=20, F#=21, C#=22, .. B#=27, Fx=28, .. Bx=34</pre>
25 public static enum Language {
29 SYMBOL(Arrays.asList("bb","b","","#","x"),"","m"),
31 * 英名表記(B flat, F sharp)
33 NAME(Arrays.asList(" double flat"," flat",""," sharp"," double sharp")," major"," minor"),
37 IN_JAPANESE(Arrays.asList("重変","変","","嬰","重嬰"),"長調","短調");
39 private Language(List<String> sharpFlatList, String major, String minor) {
40 if( (this.sharpFlatList = sharpFlatList).contains("変") ) {
41 this.notes = "ヘハトニイホロ";
42 this.majorMinorDelimiter = "/";
44 this.notes = "FCGDAEB";
45 this.majorMinorDelimiter = " / ";
51 * ♭や♯の表記を、半音下がる数が多いほうから順に並べたリスト
53 private List<String> sharpFlatList;
55 * 引数の先頭にある、♭や♯などの変化記号のインデックス(0~4)を返します。
56 * <p>変化記号がない場合、2 を返します。それ以外は次の値を返します。</p>
63 * @param s 変化記号で始まる文字列
66 private int sharpFlatIndexOf(String s) {
68 for( String sharpFlat : sharpFlatList ) {
69 if( ! sharpFlat.isEmpty() && s.startsWith(sharpFlat) ) return index;
79 * インデックス値に該当する音名を返します。
80 * @param index インデックス値(定義は{@link Language}参照)
81 * @return 音名(例:Bb、B flat、変ロ)
82 * @throws IndexOutOfBoundsException インデックス値が範囲を外れている場合
84 private String stringOf(int index) {
85 int sharpFlatIndex = index / 7;
86 char note = notes.charAt(index - sharpFlatIndex * 7);
87 String sharpFlat = sharpFlatList.get(sharpFlatIndex);
88 return this == IN_JAPANESE ? sharpFlat + note : note + sharpFlat;
92 * 音名は通常、英大文字(ABCDEFG)ですが、英小文字(abcdefg)も認識します。
93 * 日本語名(イロハニホヘト)はサポートしていません。
95 * @param noteSymbol 音名で始まる文字列
96 * @return インデックス値(定義は{@link Language}参照)
97 * @throws UnsupportedOperationException このオブジェクトが {@link #IN_JAPANESE} の場合
98 * @throws NullPointerException 引数がnullの場合
99 * @throws IllegalArgumentException 引数が空文字列の場合、または音名で始まっていない場合
101 private int indexOf(String noteSymbol) {
102 if( this == IN_JAPANESE ) throw new UnsupportedOperationException();
103 Objects.requireNonNull(noteSymbol,"Musical note symbol must not be null");
104 String trimmed = noteSymbol.trim();
105 if( trimmed.isEmpty() ) throw new IllegalArgumentException("Empty musical note symbol");
106 int noteIndex = notes.indexOf(Character.toUpperCase(trimmed.charAt(0)));
107 if( noteIndex < 0 ) throw new IllegalArgumentException(
108 "Unknown musical note symbol ["+noteSymbol+"] not in ["+notes+"]");
109 return 7 * sharpFlatIndexOf(trimmed.substring(1)) + noteIndex;
114 private String major;
118 private String minor;
120 * メジャーとマイナーを併記する場合の区切り文字
122 private String majorMinorDelimiter;
124 * 調の文字列表現を返します。メジャー/マイナーの区別がない場合、両方の表現を返します(例: "A / F#m")。
128 public String stringOf(Key key) {
130 int co5 = key.toCo5();
131 Key.MajorMinor majorMinor = key.majorMinor();
132 if( majorMinor.includes(Key.MajorMinor.MAJOR) ) {
133 s = stringOf(co5 + INDEX_OF_C) + major;
134 if( ! majorMinor.includes(Key.MajorMinor.MINOR) ) return s;
135 s += majorMinorDelimiter;
137 return s + stringOf(co5 + INDEX_OF_A) + minor;
140 private static final int INDEX_OF_A = Language.SYMBOL.indexOf("A");
141 private static final int INDEX_OF_C = Language.SYMBOL.indexOf("C");
142 /** メジャーキー基準の五度圏インデックス値 */
143 private int majorCo5;
145 private int noteNumber;
149 public static final int SEMITONES_PER_OCTAVE = 12;
151 * 五度圏インデックス値(メジャーキー基準)から音名を構築します。
152 * @param majorCo5 五度圏インデックス値
154 public Note(int majorCo5) {
155 noteNumber = mod12(toggleCo5(this.majorCo5 = majorCo5));
159 * @param noteSymbol 音名の文字列
160 * @throws NullPointerException 引数がnullの場合
161 * @throws IllegalArgumentException 引数が空文字列の場合、または音名で始まっていない場合
163 public Note(String noteSymbol) { this(toCo5Of(noteSymbol)); }
165 * この音階が指定されたオブジェクトと等しいか調べます。
167 * <p>双方の五度圏インデックス値が等しい場合のみtrueを返します。
168 * すなわち、異名同音は等しくないものとして判定されます。
171 * @return この音階が指定されたオブジェクトと等しい場合true
174 public boolean equals(Object anObject) {
175 if( this == anObject ) return true;
176 if( ! (anObject instanceof Note) ) return false;
177 return majorCo5 == ((Note)anObject).majorCo5;
181 * 五度圏インデックス値をそのまま返します。
183 * @return この音階のハッシュコード値
186 public int hashCode() { return majorCo5; }
188 * 音階が等しいかどうかを、異名同音を無視して判定します。
189 * @param another 比較対象の音階
192 public boolean equalsEnharmonically(Note another) {
193 return this == another || this.noteNumber == another.noteNumber;
196 * 五度圏インデックス値(メジャーキー基準)を返します。
199 public int toCo5() { return majorCo5; }
201 * 引数で指定された音名のメジャーキー基準の五度圏インデックスを返します。
202 * @param noteSymbol 音名の文字列
203 * @return メジャーキー基準の五度圏インデックス
204 * @throws NullPointerException 引数がnullの場合
205 * @throws IllegalArgumentException 引数が空文字列の場合、または音名で始まっていない場合
207 public static int toCo5Of(String noteSymbol) {
208 return Language.SYMBOL.indexOf(noteSymbol) - INDEX_OF_C;
211 * この音階に対応するMIDIノート番号(0オリジン表記)の最小値(オクターブの最も低い値)を返します。
212 * @return MIDIノート番号の最小値(0~11)
214 public int toNoteNumber() { return noteNumber; }
216 * この音階の文字列表現として音名を返します。
220 public String toString() { return toStringIn(Language.SYMBOL); }
222 * この音階の文字列表現を、引数で指定された言語モードで返します。
223 * @param language 言語モード
225 * @throws NullPointerException 言語モードがnullの場合
227 public String toStringIn(Language language) {
228 return language.stringOf(majorCo5 + INDEX_OF_C);
231 * MIDIノート番号と、メジャーキー基準の五度圏インデックス値との間の変換を行います。
234 * 内部的には、元の値が奇数のときに6(半オクターブ)を足し、偶数のときにそのまま返しているだけです。
235 * 値は0~11であるとは限りません。その範囲に補正したい場合は {@link #mod12(int)} を併用します。
241 public static int toggleCo5(int n) { return (n & 1) == 0 ? n : n+6 ; }
243 * ノート番号からオクターブ成分を抜きます。
244 * <p>n % 12 と似ていますが、Java の % 演算子では、左辺に負数を与えると答えも負数になってしまうため、
245 * n % 12 で計算しても 0~11 の範囲を外れてしまうことがあります。
246 * そこで、負数の場合に12を足すことにより 0~11 の範囲に入るよう補正します。
249 * @return オクターブ成分を抜いたノート番号(0~11)
251 public static int mod12(int n) {
252 int qn = n % SEMITONES_PER_OCTAVE;
253 return qn < 0 ? qn + 12 : qn ;
256 * 五度圏インデックス値で表された音階を、
257 * 指定された半音数だけ移調した結果を返します。
259 * <p>移調する半音数が0の場合、指定の五度圏インデックス値をそのまま返します。
260 * それ以外の場合、移調結果を -5 ~ 6 の範囲で返します。
263 * @param co5 五度圏インデックス値
264 * @param chromaticOffset 移調する半音数
267 public static int transposeCo5(int co5, int chromaticOffset) {
268 if( chromaticOffset == 0 ) return co5;
269 int transposedCo5 = mod12( co5 + toggleCo5(chromaticOffset) );
270 if( transposedCo5 > 6 ) transposedCo5 -= Note.SEMITONES_PER_OCTAVE;
271 return transposedCo5;
274 * 指定の五度圏インデックス値の真裏にあたる値を返します。
275 * @param co5 五度圏インデックス値
276 * @return 真裏の五度圏インデックス値
278 public static int oppositeCo5(int co5) { return co5 > 0 ? co5 - 6 : co5 + 6; }
280 * 指定の最大文字数の範囲で、MIDIノート番号が示す音名を返します。
281 * <p>白鍵の場合は A ~ G までの文字、黒鍵の場合は#と♭の両方の表現を返します。
282 * ただし、制限文字数の指定により#と♭の両方を返せないことがわかった場合、
283 * 五度圏のC/Amに近いキーでよく使われるほうの表記(C# Eb F# Ab Bb)だけを返します。
285 * <p>ノート番号だけでは物理的な音階情報しか得られないため、
286 * 白鍵で#♭のついた音階表現(B#、Cb など)、ダブルシャープ、ダブルフラットを使った表現は返しません。
288 * @param noteNumber MIDIノート番号
289 * @param maxChars 最大文字数(最大がない場合は負数を指定)
290 * @return MIDIノート番号が示す音名
292 public static String noteNumberToSymbol(int noteNumber, int maxChars) {
293 int co5 = mod12(toggleCo5(noteNumber));
294 if( co5 == 11 ) co5 -= Note.SEMITONES_PER_OCTAVE; // E# -> F
295 if( co5 < 6 ) return Language.SYMBOL.stringOf(co5 + INDEX_OF_C); // F C G D A E B
297 if( maxChars < 0 || maxChars >= "F# / Gb".length() ) return
298 Language.SYMBOL.stringOf(co5 + INDEX_OF_C) + " / " +
299 Language.SYMBOL.stringOf(co5 + INDEX_OF_C - Note.SEMITONES_PER_OCTAVE);
301 if( co5 >= 8 ) co5 -= Note.SEMITONES_PER_OCTAVE; // G# -> Ab, D# -> Eb, A# -> Bb
302 return Language.SYMBOL.stringOf(co5 + INDEX_OF_C); // C# Eb F# Ab Bb
305 * MIDIノート番号が示す音名を返します。
306 * 内部的には{@link #noteNumberToSymbol(int,int)}を呼び出しているだけです。
308 * @param noteNumber MIDIノート番号
309 * @return MIDIノート番号が示す音名
311 public static String noteNumberToSymbol(int noteNumber) {
312 return noteNumberToSymbol(noteNumber, -1);