OSDN Git Service

ca69d853c61fd935601ccdcd54645dd60fbeabbf
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / music / Note.java
1 package camidion.chordhelper.music;
2
3 import java.util.Arrays;
4 import java.util.List;
5 import java.util.Objects;
6
7 /**
8  * 音名(オクターブ抜き)を表すクラスです。値は不変です。
9  *
10  * <p>この音名は、メジャーキーの調号にした場合に
11  * 「♭、#が何個つくか」という数値
12  * 「五度圏インデックス値」で保持することを基本としています。
13  * こうすれば異名同音を明確に区別でき、
14  * しかも音楽理論的な計算を極めて単純な数式で行えるようになります。
15  * この方式はMIDIメタメッセージで調号を指定するときにも使われていて、
16  * 非常に高い親和性を持ちます。
17  * </p>
18  */
19 public class Note {
20         /**
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>
24          */
25         public static enum Language {
26                 /**
27                  * 記号表記(Bb, F#)
28                  */
29                 SYMBOL(Arrays.asList("bb","b","","#","x"),"","m"),
30                 /**
31                  * 英名表記(B flat, F sharp)
32                  */
33                 NAME(Arrays.asList(" double flat"," flat",""," sharp"," double sharp")," major"," minor"),
34                 /**
35                  * 日本名表記(変ロ, 嬰ヘ)
36                  */
37                 IN_JAPANESE(Arrays.asList("重変","変","","嬰","重嬰"),"長調","短調");
38                 //
39                 private Language(List<String> sharpFlatList, String major, String minor) {
40                         if( (this.sharpFlatList = sharpFlatList).contains("変") ) {
41                                 this.notes = "ヘハトニイホロ";
42                                 this.majorMinorDelimiter = "/";
43                         } else {
44                                 this.notes = "FCGDAEB";
45                                 this.majorMinorDelimiter = " / ";
46                         }
47                         this.major = major;
48                         this.minor = minor;
49                 }
50                 /**
51                  * ♭や♯の表記を、半音下がる数が多いほうから順に並べたリスト
52                  */
53                 private List<String> sharpFlatList;
54                 /**
55                  * 引数の先頭にある、♭や♯などの変化記号のインデックス(0~4)を返します。
56                  * <p>変化記号がない場合、2 を返します。それ以外は次の値を返します。</p>
57                  * <ul>
58                  * <li>ダブルシャープ:4</li>
59                  * <li>シャープ:3</li>
60                  * <li>フラット:1</li>
61                  * <li>ダブルフラット:0</li>
62                  * </ul>
63                  * @param s 変化記号で始まる文字列
64                  * @return インデックス
65                  */
66                 private int sharpFlatIndexOf(String s) {
67                         int index = 0;
68                         for( String sharpFlat : sharpFlatList ) {
69                                 if( ! sharpFlat.isEmpty() && s.startsWith(sharpFlat) ) return index;
70                                 index++;
71                         }
72                         return 2;
73                 }
74                 /**
75                  * 音名を五度圏順で並べた7文字
76                  */
77                 private String notes;
78                 /**
79                  * インデックス値に該当する音名を返します。
80                  * @param index インデックス値(定義は{@link Language}参照)
81                  * @return 音名(例:Bb、B flat、変ロ)
82                  * @throws IndexOutOfBoundsException インデックス値が範囲を外れている場合
83                  */
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;
89                 }
90                 /**
91                  * 音名に対するインデックス値を返します。
92                  * 音名は通常、英大文字(ABCDEFG)ですが、英小文字(abcdefg)も認識します。
93                  * 日本語名(イロハニホヘト)はサポートしていません。
94                  *
95                  * @param noteSymbol 音名で始まる文字列
96                  * @return インデックス値(定義は{@link Language}参照)
97                  * @throws UnsupportedOperationException このオブジェクトが {@link #IN_JAPANESE} の場合
98                  * @throws NullPointerException 引数がnullの場合
99                  * @throws IllegalArgumentException 引数が空文字列の場合、または音名で始まっていない場合
100                  */
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;
110                 }
111                 /**
112                  * メジャーを表す文字列
113                  */
114                 private String major;
115                 /**
116                  * マイナーを表す文字列
117                  */
118                 private String minor;
119                 /**
120                  * メジャーとマイナーを併記する場合の区切り文字
121                  */
122                 private String majorMinorDelimiter;
123                 /**
124                  * 調の文字列表現を返します。メジャー/マイナーの区別がない場合、両方の表現を返します(例: "A / F#m")。
125                  * @param key 対象の調
126                  * @return 調の文字列表現
127                  */
128                 public String stringOf(Key key) {
129                         String s = "";
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;
136                         }
137                         return s + stringOf(co5 + INDEX_OF_A) + minor;
138                 }
139         }
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;
144         /** ノート番号(0~11) */
145         private int noteNumber;
146         /**
147          * 1オクターブの半音数
148          */
149         public static final int SEMITONES_PER_OCTAVE = 12;
150         /**
151          * 五度圏インデックス値(メジャーキー基準)から音名を構築します。
152          * @param majorCo5 五度圏インデックス値
153          */
154         public Note(int majorCo5) {
155                 noteNumber = mod12(toggleCo5(this.majorCo5 = majorCo5));
156         }
157         /**
158          * 音名を文字列から構築します。
159          * @param noteSymbol 音名の文字列
160          * @throws NullPointerException 引数がnullの場合
161          * @throws IllegalArgumentException 引数が空文字列の場合、または音名で始まっていない場合
162          */
163         public Note(String noteSymbol) { this(toCo5Of(noteSymbol)); }
164         /**
165          * この音階が指定されたオブジェクトと等しいか調べます。
166          *
167          * <p>双方の五度圏インデックス値が等しい場合のみtrueを返します。
168          * すなわち、異名同音は等しくないものとして判定されます。
169          * </p>
170          *
171          * @return この音階が指定されたオブジェクトと等しい場合true
172          */
173         @Override
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;
178         }
179         /**
180          * この音階のハッシュコード値として、
181          * 五度圏インデックス値をそのまま返します。
182          *
183          * @return この音階のハッシュコード値
184          */
185         @Override
186         public int hashCode() { return majorCo5; }
187         /**
188          * 音階が等しいかどうかを、異名同音を無視して判定します。
189          * @param another 比較対象の音階
190          * @return 等しければtrue
191          */
192         public boolean equalsEnharmonically(Note another) {
193                 return this == another || this.noteNumber == another.noteNumber;
194         }
195         /**
196          * 五度圏インデックス値(メジャーキー基準)を返します。
197          * @return 五度圏インデックス値
198          */
199         public int toCo5() { return majorCo5; }
200         /**
201          * 引数で指定された音名のメジャーキー基準の五度圏インデックスを返します。
202          * @param noteSymbol 音名の文字列
203          * @return メジャーキー基準の五度圏インデックス
204          * @throws NullPointerException 引数がnullの場合
205          * @throws IllegalArgumentException 引数が空文字列の場合、または音名で始まっていない場合
206          */
207         public static int toCo5Of(String noteSymbol) {
208                 return Language.SYMBOL.indexOf(noteSymbol) - INDEX_OF_C;
209         }
210         /**
211          * この音階に対応するMIDIノート番号(0オリジン表記)の最小値(オクターブの最も低い値)を返します。
212          * @return MIDIノート番号の最小値(0~11)
213          */
214         public int toNoteNumber() { return noteNumber; }
215         /**
216          * この音階の文字列表現として音名を返します。
217          * @return この音階の文字列表現
218          */
219         @Override
220         public String toString() { return toStringIn(Language.SYMBOL); }
221         /**
222          * この音階の文字列表現を、引数で指定された言語モードで返します。
223          * @param language 言語モード
224          * @return 文字列表現
225          * @throws NullPointerException 言語モードがnullの場合
226          */
227         public String toStringIn(Language language) {
228                 return language.stringOf(majorCo5 + INDEX_OF_C);
229         }
230         /**
231          * MIDIノート番号と、メジャーキー基準の五度圏インデックス値との間の変換を行います。
232          *
233          * <p>双方向の変換に対応しています。
234          * 内部的には、元の値が奇数のときに6(半オクターブ)を足し、偶数のときにそのまま返しているだけです。
235          * 値は0~11であるとは限りません。その範囲に補正したい場合は {@link #mod12(int)} を併用します。
236          * </p>
237          *
238          * @param n 元の値
239          * @return 変換結果
240          */
241         public static int toggleCo5(int n) { return (n & 1) == 0 ? n : n+6 ; }
242         /**
243          * ノート番号からオクターブ成分を抜きます。
244          * <p>n % 12 と似ていますが、Java の % 演算子では、左辺に負数を与えると答えも負数になってしまうため、
245          * n % 12 で計算しても 0~11 の範囲を外れてしまうことがあります。
246          * そこで、負数の場合に12を足すことにより 0~11 の範囲に入るよう補正します。
247          * </p>
248          * @param n 元のノート番号
249          * @return オクターブ成分を抜いたノート番号(0~11)
250          */
251         public static int mod12(int n) {
252                 int qn = n % SEMITONES_PER_OCTAVE;
253                 return qn < 0 ? qn + 12 : qn ;
254         }
255         /**
256          * 五度圏インデックス値で表された音階を、
257          * 指定された半音数だけ移調した結果を返します。
258          *
259          * <p>移調する半音数が0の場合、指定の五度圏インデックス値をそのまま返します。
260          * それ以外の場合、移調結果を -5 ~ 6 の範囲で返します。
261          * </p>
262          *
263          * @param co5 五度圏インデックス値
264          * @param chromaticOffset 移調する半音数
265          * @return 移調結果
266          */
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;
272         }
273         /**
274          * 指定の五度圏インデックス値の真裏にあたる値を返します。
275          * @param co5 五度圏インデックス値
276          * @return 真裏の五度圏インデックス値
277          */
278         public static int oppositeCo5(int co5) { return co5 > 0 ? co5 - 6 : co5 + 6; }
279         /**
280          * 指定の最大文字数の範囲で、MIDIノート番号が示す音名を返します。
281          * <p>白鍵の場合は A ~ G までの文字、黒鍵の場合は#と♭の両方の表現を返します。
282          * ただし、制限文字数の指定により#と♭の両方を返せないことがわかった場合、
283          * 五度圏のC/Amに近いキーでよく使われるほうの表記(C# Eb F# Ab Bb)だけを返します。
284          * </p>
285          * <p>ノート番号だけでは物理的な音階情報しか得られないため、
286          * 白鍵で#♭のついた音階表現(B#、Cb など)、ダブルシャープ、ダブルフラットを使った表現は返しません。
287          * </p>
288          * @param noteNumber MIDIノート番号
289          * @param maxChars 最大文字数(最大がない場合は負数を指定)
290          * @return MIDIノート番号が示す音名
291          */
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
296
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);
300
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
303         }
304         /**
305          * MIDIノート番号が示す音名を返します。
306          * 内部的には{@link #noteNumberToSymbol(int,int)}を呼び出しているだけです。
307          * </p>
308          * @param noteNumber MIDIノート番号
309          * @return MIDIノート番号が示す音名
310          */
311         public static String noteNumberToSymbol(int noteNumber) {
312                 return noteNumberToSymbol(noteNumber, -1);
313         }
314 }