OSDN Git Service

24c44510eacc7db52ba9333cbaaa9ace86ec0eff
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / music / Chord.java
1 package camidion.chordhelper.music;
2
3 import java.awt.Color;
4 import java.util.Arrays;
5 import java.util.Collection;
6 import java.util.HashMap;
7 import java.util.Map;
8 import java.util.Vector;
9
10 import javax.swing.JLabel;
11
12 /**
13  * 和音(コード - musical chord)のクラス
14  */
15 public class Chord implements Cloneable {
16         /** コード構成音の順序に対応する色 */
17         public static final Color NOTE_INDEX_COLORS[] = {
18                 Color.red,
19                 new Color(0x40,0x40,0xFF),
20                 Color.orange.darker(),
21                 new Color(0x20,0x99,0x00),
22                 Color.magenta,
23                 Color.orange,
24                 Color.green
25         };
26         /** 音程差の半音オフセットのインデックス */
27         private static enum OffsetIndex {
28                 THIRD, FIFTH, SEVENTH, NINTH, ELEVENTH, THIRTEENTH
29         }
30         /** 音程差 */
31         public static enum Interval {
32
33                 /** 長2度(major 2nd / sus2) */
34                 SUS2(2, OffsetIndex.THIRD, "sus2", "suspended 2nd"),
35                 /** 短3度または増2度 */
36                 MINOR(3, OffsetIndex.THIRD, "m", "minor"),
37                 /** 長3度 */
38                 MAJOR(4, OffsetIndex.THIRD, "", "major"),
39                 /** 完全4度(parfect 4th / sus4) */
40                 SUS4(5, OffsetIndex.THIRD, "sus4", "suspended 4th"),
41
42                 /** 減5度または増4度(トライトーン = 三全音 = 半オクターブ) */
43                 FLAT5(6, OffsetIndex.FIFTH, "-5", "flatted 5th"),
44                 /** 完全5度 */
45                 PARFECT5(7, OffsetIndex.FIFTH, "", "parfect 5th"),
46                 /** 増5度または短6度 */
47                 SHARP5(8, OffsetIndex.FIFTH, "+5", "sharped 5th"),
48
49                 /** 長6度または減7度 */
50                 SIXTH(9, OffsetIndex.SEVENTH, "6", "6th"),
51                 /** 短7度 */
52                 SEVENTH(10, OffsetIndex.SEVENTH, "7", "7th"),
53                 /** 長7度 */
54                 MAJOR_SEVENTH(11, OffsetIndex.SEVENTH, "M7", "major 7th"),
55
56                 /** 短9度(短2度の1オクターブ上) */
57                 FLAT9(13, OffsetIndex.NINTH, "-9", "flatted 9th"),
58                 /** 長9度(長2度の1オクターブ上) */
59                 NINTH(14, OffsetIndex.NINTH, "9", "9th"),
60                 /** 増9度(増2度の1オクターブ上) */
61                 SHARP9(15, OffsetIndex.NINTH, "+9", "sharped 9th"),
62
63                 /** 完全11度(完全4度の1オクターブ上) */
64                 ELEVENTH(17, OffsetIndex.ELEVENTH, "11", "11th"),
65                 /** 増11度(増4度の1オクターブ上) */
66                 SHARP11(18, OffsetIndex.ELEVENTH, "+11", "sharped 11th"),
67
68                 /** 短13度(短6度の1オクターブ上) */
69                 FLAT13(20, OffsetIndex.THIRTEENTH, "-13", "flatted 13th"),
70                 /** 長13度(長6度の1オクターブ上) */
71                 THIRTEENTH(21, OffsetIndex.THIRTEENTH, "13", "13th");
72
73                 private Interval(int chromaticOffset, OffsetIndex offsetIndex, String symbol, String description) {
74                         this.chromaticOffset = chromaticOffset;
75                         this.offsetIndex = offsetIndex;
76                         this.symbol = symbol;
77                         this.description = description;
78                 }
79                 /**
80                  * 半音差を返します。
81                  * @return 半音差
82                  */
83                 public int getChromaticOffset() { return chromaticOffset; }
84                 private int chromaticOffset;
85                 /**
86                  * 対応するインデックスを返します。
87                  * @return 対応するインデックス
88                  */
89                 public OffsetIndex getChromaticOffsetIndex() { return offsetIndex; }
90                 private OffsetIndex offsetIndex;
91                 /**
92                  * コード名に使う略称を返します。
93                  * @return 略称
94                  */
95                 public String getSymbol() { return symbol; }
96                 private String symbol;
97                 /**
98                  * コード用の説明を返します。
99                  * @return 説明
100                  */
101                 public String getDescription() { return description; }
102                 private String description;
103         }
104         /**
105          * コードネームの音名を除いた部分(サフィックス)を組み立てて返します。
106          * @return コードネームの音名を除いた部分
107          */
108         public String symbolSuffix() {
109                 String suffix = "";
110                 Interval itv3rd = offsets.get(OffsetIndex.THIRD);
111                 Interval itv5th = offsets.get(OffsetIndex.FIFTH);
112                 Interval itv7th = offsets.get(OffsetIndex.SEVENTH);
113                 if( itv3rd == Interval.MINOR ) {
114                         suffix += itv3rd.getSymbol();
115                 }
116                 if( itv7th != null ) {
117                         suffix += itv7th.getSymbol();
118                 }
119                 if( Arrays.asList(Interval.SUS2, Interval.SUS4).contains(itv3rd) ) {
120                         suffix += itv3rd.getSymbol();
121                 }
122                 if( itv5th != Interval.PARFECT5 ) {
123                         suffix += itv5th.getSymbol();
124                 }
125                 Vector<String> inParen = new Vector<String>();
126                 for( OffsetIndex index : Arrays.asList(OffsetIndex.NINTH, OffsetIndex.ELEVENTH, OffsetIndex.THIRTEENTH) ) {
127                         Interval interval = offsets.get(index);
128                         if( interval != null ) inParen.add(interval.getSymbol());
129                 }
130                 if( ! inParen.isEmpty() ) suffix += "("+String.join(",",inParen)+")";
131                 String alias = symbolSuffixAliases.get(suffix);
132                 return alias == null ? suffix : alias;
133         }
134         private static final Map<String, String> symbolSuffixAliases = new HashMap<String, String>() {
135                 {
136                         put("m-5", "dim");
137                         put("+5", "aug");
138                         put("m6-5", "dim7");
139                         put("(9)", "add9");
140                         put("7(9)", "9");
141                         put("M7(9)", "M9");
142                         put("7+5", "aug7");
143                         put("m6-5(9)", "dim9");
144                 }
145         };
146         /**
147          * コードの説明のうち、音名を除いた部分を組み立てて返します。
148          * @return コード説明の音名を除いた部分
149          */
150         public String nameSuffix() {
151                 String suffix = "";
152                 Interval itv3rd = offsets.get(OffsetIndex.THIRD);
153                 Interval itv5th = offsets.get(OffsetIndex.FIFTH);
154                 Interval itv7th = offsets.get(OffsetIndex.SEVENTH);
155                 if( itv3rd == Interval.MINOR ) {
156                         suffix += " " + itv3rd.getDescription();
157                 }
158                 if( itv7th != null ) {
159                         suffix += " " + itv7th.getDescription();
160                 }
161                 if( Arrays.asList(Interval.SUS2, Interval.SUS4).contains(itv3rd) ) {
162                         suffix += " " + itv3rd.getDescription();
163                 }
164                 if( itv5th != Interval.PARFECT5 ) {
165                         suffix += " " + itv5th.getDescription();
166                 }
167                 Vector<String> inParen = new Vector<String>();
168                 for( OffsetIndex index : Arrays.asList(OffsetIndex.NINTH, OffsetIndex.ELEVENTH, OffsetIndex.THIRTEENTH) ) {
169                         Interval interval = offsets.get(index);
170                         if( interval != null ) inParen.add(interval.getDescription());
171                 }
172                 if( ! inParen.isEmpty() ) suffix += "("+String.join(",",inParen)+")";
173                 String alias = nameSuffixAliases.get(suffix);
174                 return alias == null ? suffix : alias;
175         }
176         private static final Map<String, String> nameSuffixAliases = new HashMap<String, String>() {
177                 {
178                         put("", " "+Interval.MAJOR.getDescription());
179                         put(" minor flatted 5th", " diminished (triad)");
180                         put(" sharped 5th", " augumented");
181                         put(" minor 6th flatted 5th", " diminished 7th");
182                         put("(9th)", " additional 9th");
183                         put(" 7th(9th)", " 9th");
184                         put(" major 7th(9th)", " major 9th");
185                         put(" 7th sharped 5th", " augumented 7th");
186                         put(" minor 6th flatted 5th(9th)", " diminished 9th");
187                 }
188         };
189
190         /**
191          * 現在有効な構成音(ルート、ベースは除く)の音程(初期値はメジャーコードの構成音)
192          */
193         private Map<OffsetIndex, Interval> offsets = new HashMap<OffsetIndex, Interval>() {
194                 private void set(Interval itv) { put(itv.getChromaticOffsetIndex(), itv); }
195                 { set(Interval.MAJOR); set(Interval.PARFECT5); }
196         };
197         /**
198          * このコードのルート音
199          */
200         private NoteSymbol rootNoteSymbol;
201         /**
202          * このコードのベース音(ルート音と異なる場合は分数コードの分母)
203          */
204         private NoteSymbol bassNoteSymbol;
205
206         /**
207          * 指定されたルート音と構成音を持つメジャーコードを構築します。
208          * @param root ルート音(ベース音としても使う)
209          * @param itvs その他の構成音の音程
210          */
211         public Chord(NoteSymbol root, Interval... itvs) { this(root, root, itvs); }
212         /**
213          * 指定されたルート音、ベース音、構成音を持つメジャーコードを構築します。
214          * @param root ルート音
215          * @param bass ベース音
216          * @param itvs その他の構成音の音程
217          */
218         public Chord(NoteSymbol root, NoteSymbol bass, Interval... itvs) {
219                 this(root, bass, Arrays.asList(itvs));
220         }
221         /**
222          * 指定されたルート音、ベース音、構成音を持つメジャーコードを構築します。
223          * @param root ルート音
224          * @param bass ベース音
225          * @param itvs その他の構成音の音程
226          */
227         public Chord(NoteSymbol root, NoteSymbol bass, Collection<Interval> itvs) {
228                 bassNoteSymbol = root;
229                 rootNoteSymbol = bass;
230                 for(Interval itv : itvs) if(itv != null) set(itv);
231         }
232         /**
233          * 指定された調と同名のコードを構築します。
234          * @param key 調
235          */
236         public Chord(Key key) {
237                 int keyCo5 = key.toCo5();
238                 if( key.majorMinor() == Key.MajorMinor.MINOR ) {
239                         keyCo5 += 3; set(Interval.MINOR);
240                 }
241                 bassNoteSymbol = rootNoteSymbol = new NoteSymbol(keyCo5);
242         }
243         /**
244          * コード名の文字列からコードを構築します。
245          * @param chordSymbol コード名の文字列
246          */
247         public Chord(String chordSymbol) {
248                 //
249                 // 分数コードの分子と分母に分ける
250                 String parts[] = chordSymbol.trim().split("(/|on)");
251                 if( parts.length == 0 ) return;
252                 //
253                 // ルート音とベース音を設定
254                 rootNoteSymbol = new NoteSymbol(parts[0]);
255                 if( parts.length > 1 && ! parts[0].equals(parts[1]) ) {
256                         bassNoteSymbol = new NoteSymbol(parts[1]);
257                 } else {
258                         bassNoteSymbol = rootNoteSymbol;
259                 }
260                 // 先頭の音名はもういらないので削除
261                 String suffix = parts[0].replaceFirst("^[A-G][#bx]*","");
262                 //
263                 // () があれば、その中身を取り出す
264                 String suffixParts[] = suffix.split("[\\(\\)]");
265                 if( suffixParts.length == 0 ) return;
266                 String suffixParen = "";
267                 if( suffixParts.length > 1 ) {
268                         suffixParen = suffixParts[1];
269                         suffix = suffixParts[0];
270                 }
271                 // +5 -5 aug dim
272                 if( suffix.matches(".*(\\+5|aug|#5).*") ) set(Interval.FLAT5);
273                 else if( suffix.matches(".*(-5|dim|b5).*") ) set(Interval.SHARP5);
274                 //
275                 // 6 7 M7
276                 if( suffix.matches(".*(M7|maj7|M9|maj9).*") ) set(Interval.MAJOR_SEVENTH);
277                 else if( suffix.matches(".*(6|dim[79]).*") ) set(Interval.SIXTH);
278                 else if( suffix.matches(".*7.*") ) set(Interval.SEVENTH);
279                 //
280                 // minor sus4  (maj7 と間違えないように比較しつつ、mmaj7も解釈させる)
281                 if( suffix.matches(".*m.*") && ! suffix.matches(".*ma.*") || suffix.matches(".*mma.*") ) set(Interval.MINOR);
282                 else if( suffix.matches(".*sus4.*") ) set(Interval.SUS4);
283                 //
284                 // 9th の判定
285                 if( suffix.matches(".*9.*") ) {
286                         set(Interval.NINTH);
287                         if( ! suffix.matches(".*(add9|6|M9|maj9|dim9).*") ) set(Interval.SEVENTH);
288                 }
289                 else {
290                         // () の中を , で分ける
291                         for( String p : suffixParen.split(",") ) {
292                                 if( p.matches("(\\+9|#9)") ) set(Interval.SHARP9);
293                                 else if( p.matches("(-9|b9)") ) set(Interval.FLAT9);
294                                 else if( p.matches("9") ) set(Interval.NINTH);
295
296                                 if( p.matches("(\\+11|#11)") ) set(Interval.SHARP11);
297                                 else if( p.matches("11") ) set(Interval.ELEVENTH);
298
299                                 if( p.matches("(-13|b13)") ) set(Interval.FLAT13);
300                                 else if( p.matches("13") ) set(Interval.THIRTEENTH);
301
302                                 // -5 や +5 が () の中にあっても解釈できるようにする
303                                 if( p.matches("(-5|b5)") ) set(Interval.FLAT5);
304                                 else if( p.matches("(\\+5|#5)") ) set(Interval.SHARP5);
305                         }
306                 }
307         }
308         /**
309          * ルート音を返します。
310          * @return ルート音
311          */
312         public NoteSymbol rootNoteSymbol() { return rootNoteSymbol; }
313         /**
314          * ベース音を返します。分数コードの場合はルート音と異なります。
315          * @return ベース音
316          */
317         public NoteSymbol bassNoteSymbol() { return bassNoteSymbol; }
318         /**
319          * このコードの構成音を、ルート音からの音程の配列として返します。
320          * ルート音自身やベース音は含まれません。
321          * @return 音程の配列
322          */
323         public Interval[] intervals() {
324                 return offsets.values().toArray(new Interval[offsets.size()]);
325         }
326         /**
327          * 指定した音程が設定されているか調べます。
328          * @param itv 音程
329          * @return 指定した音程が設定されていたらtrue
330          */
331         public boolean isSet(Interval itv) {
332                 return itv.equals(offsets.get(itv.getChromaticOffsetIndex()));
333         }
334         /**
335          * コードの同一性を判定します。ルート音、ベース音の異名同音は異なるものとみなされます。
336          * @param anObject 比較対象
337          * @return 等しければtrue
338          */
339         @Override
340         public boolean equals(Object anObject) {
341                 if( anObject == this ) return true;
342                 if( anObject instanceof Chord ) {
343                         Chord another = (Chord) anObject;
344                         if( ! rootNoteSymbol.equals(another.rootNoteSymbol) ) return false;
345                         if( ! bassNoteSymbol.equals(another.bassNoteSymbol) ) return false;
346                         return offsets.equals(another.offsets);
347                 }
348                 return false;
349         }
350         @Override
351         public int hashCode() { return toString().hashCode(); }
352         /**
353          * ルート音、ベース音の異名同音を同じとみなしたうえで、コードの同一性を判定します。
354          * @param another 比較対象のコード
355          * @return 等しければtrue
356          */
357         public boolean equalsEnharmonically(Chord another) {
358                 if( another == this ) return true;
359                 if( another == null ) return false;
360                 if( ! rootNoteSymbol.equalsEnharmonically(another.rootNoteSymbol) ) return false;
361                 if( ! bassNoteSymbol.equalsEnharmonically(another.bassNoteSymbol) ) return false;
362                 return offsets.equals(another.offsets);
363         }
364         /**
365          * コード構成音の数を返します。ルート音は含まれますが、ベース音は含まれません。
366          * @return コード構成音の数
367          */
368         public int numberOfNotes() { return offsets.size() + 1; }
369         /**
370          * 指定された位置にある構成音のノート番号を返します。
371          * @param index 位置(0をルート音とした構成音の順序)
372          * @return ノート番号(該当する音がない場合は -1)
373          */
374         public int noteAt(int index) {
375                 int rootnote = rootNoteSymbol.toNoteNumber();
376                 if( index == 0 ) return rootnote;
377                 Interval itv;
378                 int i=0;
379                 for( OffsetIndex offsetIndex : OffsetIndex.values() )
380                         if( (itv = offsets.get(offsetIndex)) != null && ++i == index )
381                                 return rootnote + itv.getChromaticOffset();
382                 return -1;
383         }
384         /**
385          * コード構成音を格納したノート番号の配列を返します(ベース音は含まれません)。
386          * 音域が指定された場合、その音域の範囲内に収まるように転回されます。
387          * @param range 音域(null可)
388          * @param key キー(null可)
389          * @return ノート番号の配列
390          */
391         public int[] toNoteArray(Range range, Key key) {
392                 int rootnote = rootNoteSymbol.toNoteNumber();
393                 int ia[] = new int[numberOfNotes()];
394                 int i;
395                 ia[i=0] = rootnote;
396                 Interval itv;
397                 for( OffsetIndex offsetIndex : OffsetIndex.values() )
398                         if( (itv = offsets.get(offsetIndex)) != null )
399                                 ia[++i] = rootnote + itv.getChromaticOffset();
400                 if( range != null ) range.invertNotesOf(ia, key);
401                 return ia;
402         }
403         /**
404          * MIDIノート番号が、コードの構成音の何番目(0=ルート音)にあるかを表すインデックス値を返します。
405          * 構成音に該当しない場合は -1 を返します。ベース音は検索されません。
406          * @param noteNumber MIDIノート番号
407          * @return 構成音のインデックス値
408          */
409         public int indexOf(int noteNumber) {
410                 int relativeNote = noteNumber - rootNoteSymbol.toNoteNumber();
411                 if( Music.mod12(relativeNote) == 0 ) return 0;
412                 Interval itv;
413                 int i=0;
414                 for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
415                         if( (itv = offsets.get(offsetIndex)) != null ) {
416                                 i++;
417                                 if( Music.mod12(relativeNote - itv.getChromaticOffset()) == 0 )
418                                         return i;
419                         }
420                 }
421                 return -1;
422         }
423         /**
424          * 指定したキーのスケールを外れた構成音がないか調べます。
425          * @param key 調べるキー
426          * @return スケールを外れている構成音がなければtrue
427          */
428         public boolean isOnScaleIn(Key key) { return isOnScaleInKey(key.toCo5()); }
429         private boolean isOnScaleInKey(int keyCo5) {
430                 int rootnote = rootNoteSymbol.toNoteNumber();
431                 if( ! Music.isOnScale(rootnote, keyCo5) ) return false;
432                 Interval itv;
433                 for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
434                         if( (itv = offsets.get(offsetIndex)) == null ) continue;
435                         if( ! Music.isOnScale(rootnote + itv.getChromaticOffset(), keyCo5) ) return false;
436                 }
437                 return true;
438         }
439         /**
440          * C/Amの調に近いほうの♯、♭の表記で、移調したコードを返します。
441          * @param chromaticOffset 移調幅(半音単位)
442          * @return 移調した新しいコード(移調幅が0の場合は自分自身)
443          */
444         public Chord transposedChord(int chromaticOffset) {
445                 return transposedChord(chromaticOffset, 0);
446         }
447         /**
448          * 指定された調に近いほうの♯、♭の表記で、移調したコードを返します。
449          * @param chromaticOffset 移調幅(半音単位)
450          * @param originalKey 基準とする調
451          * @return 移調した新しいコード(移調幅が0の場合は自分自身)
452          */
453         public Chord transposedChord(int chromaticOffset, Key originalKey) {
454                 return transposedChord(chromaticOffset, originalKey.toCo5());
455         }
456         private Chord transposedChord(int chromaticOffset, int originalKeyCo5) {
457                 if( chromaticOffset == 0 ) return this;
458                 int offsetCo5 = Music.mod12(Music.reverseCo5(chromaticOffset));
459                 if( offsetCo5 > 6 ) offsetCo5 -= 12;
460                 int keyCo5 = originalKeyCo5 + offsetCo5;
461                 //
462                 int newRootCo5 = rootNoteSymbol.toCo5() + offsetCo5;
463                 int newBassCo5 = bassNoteSymbol.toCo5() + offsetCo5;
464                 if( keyCo5 > 6 ) {
465                         newRootCo5 -= 12;
466                         newBassCo5 -= 12;
467                 }
468                 else if( keyCo5 < -5 ) {
469                         newRootCo5 += 12;
470                         newBassCo5 += 12;
471                 }
472                 return new Chord(new NoteSymbol(newRootCo5), new NoteSymbol(newBassCo5), intervals());
473         }
474
475         /**
476          * この和音の文字列表現としてコード名を返します。
477          * @return この和音のコード名
478          */
479         @Override
480         public String toString() {
481                 String chordSymbol = rootNoteSymbol + symbolSuffix();
482                 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) chordSymbol += "/" + bassNoteSymbol;
483                 return chordSymbol;
484         }
485         /**
486          * コード名を HTML で返します。
487          *
488          * Swing の {@link JLabel#setText(String)} は HTML で指定できるので、
489          * 文字の大きさに変化をつけることができます。
490          *
491          * @param colorName 色のHTML表現(色名または #RRGGBB 形式)
492          * @return コード名のHTML
493          */
494         public String toHtmlString(String colorName) {
495                 String span = "<span style=\"font-size: 120%\">";
496                 String endSpan = "</span>";
497                 String root = rootNoteSymbol.toString();
498                 String formattedRoot = (root.length() == 1) ? root + span :
499                         root.replace("#",span+"<sup>#</sup>").
500                         replace("b",span+"<sup>b</sup>").
501                         replace("x",span+"<sup>x</sup>");
502                 String formattedBass = "";
503                 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
504                         String bass = bassNoteSymbol.toString();
505                         formattedBass = (bass.length() == 1) ? bass + span :
506                                 bass.replace("#",span+"<sup>#</sup>").
507                                 replace("b",span+"<sup>b</sup>").
508                                 replace("x",span+"<sup>x</sup>");
509                         formattedBass = "/" + formattedBass + endSpan;
510                 }
511                 String suffix = symbolSuffix().
512                         replace("-5","<sup>-5</sup>").
513                         replace("+5","<sup>+5</sup>");
514                 return
515                         "<html>" +
516                         "<span style=\"color: " + colorName + "; font-size: 170% ; white-space: nowrap ;\">" +
517                         formattedRoot + suffix + endSpan + formattedBass +
518                         "</span>" +
519                         "</html>" ;
520         }
521         /**
522          * コードの説明(英語)を返します。
523          * @return コードの説明(英語)
524          */
525         public String toName() {
526                 String name = rootNoteSymbol.toStringIn(NoteSymbol.Language.NAME) + nameSuffix() ;
527                 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
528                         name += " on " + bassNoteSymbol.toStringIn(NoteSymbol.Language.NAME);
529                 }
530                 return name;
531         }
532
533         /**
534          * このコードのクローンを作成します。
535          */
536         @Override
537         public Chord clone() {
538                 Chord newChord = new Chord(rootNoteSymbol, bassNoteSymbol);
539                 newChord.offsets = new HashMap<>(offsets);
540                 return newChord;
541         }
542         /**
543          * 指定した音程の構成音を設定します。
544          * @param itv 設定する音程
545          */
546         public void set(Interval itv) {
547                 offsets.put(itv.getChromaticOffsetIndex(), itv);
548         }
549         /**
550          * 指定した音程の構成音をクリアします。
551          * @param itv クリアする音程
552          */
553         public void clear(Interval itv) {
554                 offsets.remove(itv.getChromaticOffsetIndex());
555         }
556 }