1 package camidion.chordhelper.music;
4 import java.util.Arrays;
5 import java.util.Collection;
6 import java.util.HashMap;
8 import java.util.Vector;
10 import javax.swing.JLabel;
13 * 和音(コード - musical chord)のクラス
15 public class Chord implements Cloneable {
16 /** コード構成音の順序に対応する色 */
17 public static final Color NOTE_INDEX_COLORS[] = {
19 new Color(0x40,0x40,0xFF),
20 Color.orange.darker(),
21 new Color(0x20,0x99,0x00),
26 /** 音程差の半音オフセットのインデックス */
27 private static enum OffsetIndex {
28 THIRD, FIFTH, SEVENTH, NINTH, ELEVENTH, THIRTEENTH
31 public static enum Interval {
33 /** 長2度(major 2nd / sus2) */
34 SUS2(2, OffsetIndex.THIRD, "sus2", "suspended 2nd"),
36 MINOR(3, OffsetIndex.THIRD, "m", "minor"),
38 MAJOR(4, OffsetIndex.THIRD, "", "major"),
39 /** 完全4度(parfect 4th / sus4) */
40 SUS4(5, OffsetIndex.THIRD, "sus4", "suspended 4th"),
42 /** 減5度または増4度(トライトーン = 三全音 = 半オクターブ) */
43 FLAT5(6, OffsetIndex.FIFTH, "-5", "flatted 5th"),
45 PARFECT5(7, OffsetIndex.FIFTH, "", "parfect 5th"),
47 SHARP5(8, OffsetIndex.FIFTH, "+5", "sharped 5th"),
50 SIXTH(9, OffsetIndex.SEVENTH, "6", "6th"),
52 SEVENTH(10, OffsetIndex.SEVENTH, "7", "7th"),
54 MAJOR_SEVENTH(11, OffsetIndex.SEVENTH, "M7", "major 7th"),
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"),
63 /** 完全11度(完全4度の1オクターブ上) */
64 ELEVENTH(17, OffsetIndex.ELEVENTH, "11", "11th"),
65 /** 増11度(増4度の1オクターブ上) */
66 SHARP11(18, OffsetIndex.ELEVENTH, "+11", "sharped 11th"),
68 /** 短13度(短6度の1オクターブ上) */
69 FLAT13(20, OffsetIndex.THIRTEENTH, "-13", "flatted 13th"),
70 /** 長13度(長6度の1オクターブ上) */
71 THIRTEENTH(21, OffsetIndex.THIRTEENTH, "13", "13th");
73 private Interval(int chromaticOffset, OffsetIndex offsetIndex, String symbol, String description) {
74 this.chromaticOffset = chromaticOffset;
75 this.offsetIndex = offsetIndex;
77 this.description = description;
83 public int getChromaticOffset() { return chromaticOffset; }
84 private int chromaticOffset;
89 public OffsetIndex getChromaticOffsetIndex() { return offsetIndex; }
90 private OffsetIndex offsetIndex;
95 public String getSymbol() { return symbol; }
96 private String symbol;
101 public String getDescription() { return description; }
102 private String description;
105 * コードネームの音名を除いた部分(サフィックス)を組み立てて返します。
106 * @return コードネームの音名を除いた部分
108 public String symbolSuffix() {
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();
116 if( itv7th != null ) {
117 suffix += itv7th.getSymbol();
119 if( Arrays.asList(Interval.SUS2, Interval.SUS4).contains(itv3rd) ) {
120 suffix += itv3rd.getSymbol();
122 if( itv5th != Interval.PARFECT5 ) {
123 suffix += itv5th.getSymbol();
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());
130 if( ! inParen.isEmpty() ) suffix += "("+String.join(",",inParen)+")";
131 String alias = symbolSuffixAliases.get(suffix);
132 return alias == null ? suffix : alias;
134 private static final Map<String, String> symbolSuffixAliases = new HashMap<String, String>() {
143 put("m6-5(9)", "dim9");
147 * コードの説明のうち、音名を除いた部分を組み立てて返します。
148 * @return コード説明の音名を除いた部分
150 public String nameSuffix() {
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();
158 if( itv7th != null ) {
159 suffix += " " + itv7th.getDescription();
161 if( Arrays.asList(Interval.SUS2, Interval.SUS4).contains(itv3rd) ) {
162 suffix += " " + itv3rd.getDescription();
164 if( itv5th != Interval.PARFECT5 ) {
165 suffix += " " + itv5th.getDescription();
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());
172 if( ! inParen.isEmpty() ) suffix += "("+String.join(",",inParen)+")";
173 String alias = nameSuffixAliases.get(suffix);
174 return alias == null ? suffix : alias;
176 private static final Map<String, String> nameSuffixAliases = new HashMap<String, String>() {
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");
191 * 現在有効な構成音(ルート、ベースは除く)の音程
193 private Map<OffsetIndex, Interval> offsets;
197 private NoteSymbol rootNoteSymbol;
199 * このコードのベース音(ルート音と異なる場合は分数コードの分母)
201 private NoteSymbol bassNoteSymbol;
204 * 指定されたルート音と構成音を持つコードを構築します。
205 * @param root ルート音(ベース音としても使う)
206 * @param intervals その他の構成音の音程(長三度、完全五度は指定しなくてもデフォルトで設定されます)
208 public Chord(NoteSymbol root, Interval... intervals) {
209 this(root, root, intervals);
212 * 指定されたルート音、ベース音、構成音を持つコードを構築します。
215 * @param intervals その他の構成音の音程(長三度、完全五度は指定しなくてもデフォルトで設定されます)
217 public Chord(NoteSymbol root, NoteSymbol bass, Interval... intervals) {
218 this(root, bass, Arrays.asList(intervals));
221 * 指定されたルート音、ベース音、構成音を持つコードを構築します。
224 * @param intervals その他の構成音の音程(長三度、完全五度は指定しなくてもデフォルトで設定されます)
226 public Chord(NoteSymbol root, NoteSymbol bass, Collection<Interval> intervals) {
227 rootNoteSymbol = root;
228 bassNoteSymbol = bass;
229 offsets = new HashMap<>();
231 set(Interval.PARFECT5);
235 * 元のコードのルート音、ベース音以外の構成音の一部を変更した新しいコードを構築します。
237 * @param intervals 変更したい構成音の音程(ルート音、ベース音を除く)
238 * @throws NullPointerException 元のコードにnullが指定された場合
240 public Chord(Chord original, Interval... intervals) {
241 this(original, Arrays.asList(intervals));
244 * 元のコードのルート音、ベース音以外の構成音の一部を変更した新しいコードを構築します。
246 * @param intervals 変更したい構成音の音程(ルート音、ベース音を除く)
247 * @throws NullPointerException 元のコードにnullが指定された場合
249 public Chord(Chord original, Collection<Interval> intervals) {
250 rootNoteSymbol = original.rootNoteSymbol;
251 bassNoteSymbol = original.bassNoteSymbol;
252 offsets = new HashMap<>(original.offsets);
256 * 指定された調と同名のコードを構築します。
258 * @throws NullPointerException 調にnullが指定された場合
260 public Chord(Key key) {
261 offsets = new HashMap<>(); set(Interval.PARFECT5);
262 int keyCo5 = key.toCo5(); if( key.majorMinor() == Key.MajorMinor.MINOR ) {
263 keyCo5 += 3; set(Interval.MINOR);
264 } else set(Interval.MAJOR);
265 bassNoteSymbol = rootNoteSymbol = new NoteSymbol(keyCo5);
269 * @param chordSymbol コード名
270 * @throws NullPointerException コード名にnullが指定された場合
272 public Chord(String chordSymbol) {
273 offsets = new HashMap<>();
275 set(Interval.PARFECT5);
277 // 分数コードの分子と分母に分け、先頭の音名を取り込む
278 String rootOnBass[] = chordSymbol.trim().split("(/|on)");
279 if( rootOnBass.length == 0 ) {
280 bassNoteSymbol = rootNoteSymbol = new NoteSymbol();
283 rootNoteSymbol = new NoteSymbol(rootOnBass[0]);
284 if( rootOnBass.length > 1 && ! rootOnBass[0].equals(rootOnBass[1]) ) {
285 // 分子(ルート音)と異なる分母(ベース音)が指定されていた場合
286 bassNoteSymbol = new NoteSymbol(rootOnBass[1]);
288 bassNoteSymbol = rootNoteSymbol;
290 // 先頭の音名はもういらないので削除し、サフィックスだけ残す
291 String suffix = rootOnBass[0].replaceFirst("^[A-G][#bx]*","");
294 String suffixWithParen[] = suffix.split("[\\(\\)]");
295 if( suffixWithParen.length == 0 ) return;
296 String suffixParen = "";
297 if( suffixWithParen.length > 1 ) {
298 suffixParen = suffixWithParen[1];
299 suffix = suffixWithParen[0];
302 if( suffix.matches(".*(\\+5|aug|#5).*") ) set(Interval.FLAT5);
303 else if( suffix.matches(".*(-5|dim|b5).*") ) set(Interval.SHARP5);
306 if( suffix.matches(".*(M7|maj7|M9|maj9).*") ) set(Interval.MAJOR_SEVENTH);
307 else if( suffix.matches(".*(6|dim[79]).*") ) set(Interval.SIXTH);
308 else if( suffix.matches(".*7.*") ) set(Interval.SEVENTH);
310 // minor sus4 (m と maj7 を間違えないよう比較しつつ、mmaj7 も認識させる)
311 if( suffix.matches(".*m.*") && ! suffix.matches(".*ma.*") || suffix.matches(".*mma.*") ) {
314 else if( suffix.matches(".*sus4.*") ) set(Interval.SUS4);
317 if( suffix.matches(".*9.*") ) {
319 if( ! suffix.matches(".*(add9|6|M9|maj9|dim9).*") ) set(Interval.SEVENTH);
323 for( String p : suffixParen.split(",") ) {
324 if( p.matches("(\\+9|#9)") ) set(Interval.SHARP9);
325 else if( p.matches("(-9|b9)") ) set(Interval.FLAT9);
326 else if( p.matches("9") ) set(Interval.NINTH);
328 if( p.matches("(\\+11|#11)") ) set(Interval.SHARP11);
329 else if( p.matches("11") ) set(Interval.ELEVENTH);
331 if( p.matches("(-13|b13)") ) set(Interval.FLAT13);
332 else if( p.matches("13") ) set(Interval.THIRTEENTH);
334 // -5 や +5 が () の中にあっても解釈できるようにする
335 if( p.matches("(-5|b5)") ) set(Interval.FLAT5);
336 else if( p.matches("(\\+5|#5)") ) set(Interval.SHARP5);
344 public NoteSymbol rootNoteSymbol() { return rootNoteSymbol; }
346 * ベース音を返します。分数コードの場合はルート音と異なります。
349 public NoteSymbol bassNoteSymbol() { return bassNoteSymbol; }
351 * このコードの構成音を、ルート音からの音程の配列として返します。
352 * ルート音自身やベース音は含まれません。
355 public Interval[] intervals() {
356 return offsets.values().toArray(new Interval[offsets.size()]);
359 * 指定した音程が設定されているか調べます。
361 * @return 指定した音程が設定されていたらtrue
363 public boolean isSet(Interval itv) {
364 return itv.equals(offsets.get(itv.getChromaticOffsetIndex()));
367 * コードの同一性を判定します。ルート音、ベース音の異名同音は異なるものとみなされます。
368 * @param anObject 比較対象
370 * @see #equalsEnharmonically(Chord)
373 public boolean equals(Object anObject) {
374 if( anObject == this ) return true;
375 if( anObject instanceof Chord ) {
376 Chord another = (Chord) anObject;
377 if( ! rootNoteSymbol.equals(another.rootNoteSymbol) ) return false;
378 if( ! bassNoteSymbol.equals(another.bassNoteSymbol) ) return false;
379 return offsets.equals(another.offsets);
384 public int hashCode() { return toString().hashCode(); }
386 * ルート音、ベース音の異名同音を同じとみなしたうえで、コードの同一性を判定します。
387 * @param another 比較対象のコード
389 * @see #equals(Object)
391 public boolean equalsEnharmonically(Chord another) {
392 if( another == this ) return true;
393 if( another == null ) return false;
394 if( ! rootNoteSymbol.equalsEnharmonically(another.rootNoteSymbol) ) return false;
395 if( ! bassNoteSymbol.equalsEnharmonically(another.bassNoteSymbol) ) return false;
396 return offsets.equals(another.offsets);
399 * コード構成音の数を返します。ルート音は含まれますが、ベース音は含まれません。
402 public int numberOfNotes() { return offsets.size() + 1; }
404 * 指定された位置にある構成音のノート番号を返します。
405 * @param index 位置(0をルート音とした構成音の順序)
406 * @return ノート番号(該当する音がない場合は -1)
408 public int noteAt(int index) {
409 int rootnote = rootNoteSymbol.toNoteNumber();
410 if( index == 0 ) return rootnote;
413 for( OffsetIndex offsetIndex : OffsetIndex.values() )
414 if( (itv = offsets.get(offsetIndex)) != null && ++i == index )
415 return rootnote + itv.getChromaticOffset();
419 * コード構成音を格納したノート番号の配列を返します(ベース音は含まれません)。
420 * 音域が指定された場合、その音域の範囲内に収まるように転回されます。
421 * @param range 音域(null可)
422 * @param key キー(null可)
425 public int[] toNoteArray(Range range, Key key) {
426 int rootnote = rootNoteSymbol.toNoteNumber();
427 int ia[] = new int[numberOfNotes()];
431 for( OffsetIndex offsetIndex : OffsetIndex.values() )
432 if( (itv = offsets.get(offsetIndex)) != null )
433 ia[++i] = rootnote + itv.getChromaticOffset();
434 if( range != null ) range.invertNotesOf(ia, key);
438 * MIDIノート番号が、コードの構成音の何番目(0=ルート音)にあるかを表すインデックス値を返します。
439 * 構成音に該当しない場合は -1 を返します。ベース音は検索されません。
440 * @param noteNumber MIDIノート番号
441 * @return 構成音のインデックス値
443 public int indexOf(int noteNumber) {
444 int relativeNote = noteNumber - rootNoteSymbol.toNoteNumber();
445 if( Music.mod12(relativeNote) == 0 ) return 0;
448 for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
449 if( (itv = offsets.get(offsetIndex)) != null ) {
451 if( Music.mod12(relativeNote - itv.getChromaticOffset()) == 0 )
458 * 指定したキーのスケールを外れた構成音がないか調べます。
460 * @return スケールを外れている構成音がなければtrue
462 public boolean isOnScaleIn(Key key) { return isOnScaleInKey(key.toCo5()); }
463 private boolean isOnScaleInKey(int keyCo5) {
464 int rootnote = rootNoteSymbol.toNoteNumber();
465 if( ! Music.isOnScale(rootnote, keyCo5) ) return false;
467 for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
468 if( (itv = offsets.get(offsetIndex)) == null ) continue;
469 if( ! Music.isOnScale(rootnote + itv.getChromaticOffset(), keyCo5) ) return false;
474 * C/Amの調に近いほうの♯、♭の表記で、移調したコードを返します。
475 * @param chromaticOffset 移調幅(半音単位)
476 * @return 移調した新しいコード(移調幅が0の場合は自分自身)
478 public Chord transposedChord(int chromaticOffset) {
479 return transposedChord(chromaticOffset, 0);
482 * 指定された調に近いほうの♯、♭の表記で、移調したコードを返します。
483 * @param chromaticOffset 移調幅(半音単位)
484 * @param originalKey 基準とする調
485 * @return 移調した新しいコード(移調幅が0の場合は自分自身)
487 public Chord transposedChord(int chromaticOffset, Key originalKey) {
488 return transposedChord(chromaticOffset, originalKey.toCo5());
490 private Chord transposedChord(int chromaticOffset, int originalKeyCo5) {
491 if( chromaticOffset == 0 ) return this;
492 int offsetCo5 = Music.mod12(Music.reverseCo5(chromaticOffset));
493 if( offsetCo5 > 6 ) offsetCo5 -= 12;
494 int keyCo5 = originalKeyCo5 + offsetCo5;
496 int newRootCo5 = rootNoteSymbol.toCo5() + offsetCo5;
497 int newBassCo5 = bassNoteSymbol.toCo5() + offsetCo5;
502 else if( keyCo5 < -5 ) {
506 return new Chord(new NoteSymbol(newRootCo5), new NoteSymbol(newBassCo5), intervals());
510 * この和音の文字列表現としてコード名を返します。
514 public String toString() {
515 String chordSymbol = rootNoteSymbol + symbolSuffix();
516 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) chordSymbol += "/" + bassNoteSymbol;
522 * Swing の {@link JLabel#setText(String)} は HTML で指定できるので、
523 * 文字の大きさに変化をつけることができます。
525 * @param colorName 色のHTML表現(色名または #RRGGBB 形式)
528 public String toHtmlString(String colorName) {
529 String span = "<span style=\"font-size: 120%\">";
530 String endSpan = "</span>";
531 String root = rootNoteSymbol.toString();
532 String formattedRoot = (root.length() == 1) ? root + span :
533 root.replace("#",span+"<sup>#</sup>").
534 replace("b",span+"<sup>b</sup>").
535 replace("x",span+"<sup>x</sup>");
536 String formattedBass = "";
537 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
538 String bass = bassNoteSymbol.toString();
539 formattedBass = (bass.length() == 1) ? bass + span :
540 bass.replace("#",span+"<sup>#</sup>").
541 replace("b",span+"<sup>b</sup>").
542 replace("x",span+"<sup>x</sup>");
543 formattedBass = "/" + formattedBass + endSpan;
545 String suffix = symbolSuffix().
546 replace("-5","<sup>-5</sup>").
547 replace("+5","<sup>+5</sup>");
550 "<span style=\"color: " + colorName + "; font-size: 170% ; white-space: nowrap ;\">" +
551 formattedRoot + suffix + endSpan + formattedBass +
559 public String toName() {
560 String name = rootNoteSymbol.toStringIn(NoteSymbol.Language.NAME) + nameSuffix() ;
561 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
562 name += " on " + bassNoteSymbol.toStringIn(NoteSymbol.Language.NAME);
571 public Chord clone() {
572 Chord newChord = new Chord(rootNoteSymbol, bassNoteSymbol);
573 newChord.offsets = new HashMap<>(offsets);
578 * @param interval 設定する音程
580 public void set(Interval interval) {
581 offsets.put(interval.getChromaticOffsetIndex(), interval);
584 * 指定した複数の音程の構成音を設定します。
585 * @param intervals 設定する音程
587 protected void set(Collection<Interval> intervals) {
588 for(Interval itv : intervals) if(itv != null) set(itv);
594 public void clear(Interval itv) {
595 offsets.remove(itv.getChromaticOffsetIndex());