1 package camidion.chordhelper.music;
4 import java.util.HashMap;
6 import java.util.Vector;
8 import javax.swing.JLabel;
11 * 和音(コード - musical chord)のクラス
13 public class Chord implements Cloneable {
17 public static final Color NOTE_INDEX_COLORS[] = {
19 new Color(0x40,0x40,0xFF),
20 Color.orange.darker(),
21 new Color(0x20,0x99,0x00),
29 public static enum OffsetIndex {
40 public static enum Interval {
42 /** 長2度(major 2nd / sus2) */
43 SUS2(2, OffsetIndex.THIRD),
45 MINOR(3, OffsetIndex.THIRD),
47 MAJOR(4, OffsetIndex.THIRD),
48 /** 完全4度(parfect 4th / sus4) */
49 SUS4(5, OffsetIndex.THIRD),
51 /** 減5度または増4度(トライトーン = 三全音 = 半オクターブ) */
52 FLAT5(6, OffsetIndex.FIFTH),
54 PARFECT5(7, OffsetIndex.FIFTH),
56 SHARP5(8, OffsetIndex.FIFTH),
59 SIXTH(9, OffsetIndex.SEVENTH),
61 SEVENTH(10, OffsetIndex.SEVENTH),
63 MAJOR_SEVENTH(11, OffsetIndex.SEVENTH),
65 /** 短9度(短2度の1オクターブ上) */
66 FLAT9(13, OffsetIndex.NINTH),
67 /** 長9度(長2度の1オクターブ上) */
68 NINTH(14, OffsetIndex.NINTH),
69 /** 増9度(増2度の1オクターブ上) */
70 SHARP9(15, OffsetIndex.NINTH),
72 /** 完全11度(完全4度の1オクターブ上) */
73 ELEVENTH(17, OffsetIndex.ELEVENTH),
74 /** 増11度(増4度の1オクターブ上) */
75 SHARP11(18, OffsetIndex.ELEVENTH),
77 /** 短13度(短6度の1オクターブ上) */
78 FLAT13(20, OffsetIndex.THIRTEENTH),
79 /** 長13度(長6度の1オクターブ上) */
80 THIRTEENTH(21, OffsetIndex.THIRTEENTH);
82 private Interval(int chromaticOffset, OffsetIndex offsetIndex) {
83 this.chromaticOffset = chromaticOffset;
84 this.offsetIndex = offsetIndex;
86 private OffsetIndex offsetIndex;
87 private int chromaticOffset;
92 public int getChromaticOffset() { return chromaticOffset; }
97 public OffsetIndex getChromaticOffsetIndex() {
102 * デフォルトの半音値(メジャーコード固定)
104 public static Map<OffsetIndex, Interval>
105 DEFAULT_OFFSETS = new HashMap<OffsetIndex, Interval>() {
108 itv = Interval.MAJOR; put(itv.getChromaticOffsetIndex(), itv);
109 itv = Interval.PARFECT5; put(itv.getChromaticOffsetIndex(), itv);
113 * 現在有効な構成音の音程(ルート音を除く)
115 public Map<OffsetIndex, Interval> offsets = new HashMap<>(DEFAULT_OFFSETS);
119 private NoteSymbol rootNoteSymbol;
121 * このコードのベース音(ルート音と異なる場合は分数コードの分母)
123 private NoteSymbol bassNoteSymbol;
126 * コード C major を構築します。
129 this(new NoteSymbol());
132 * 指定した音名のメジャーコードを構築します。
133 * @param noteSymbol 音名
135 public Chord(NoteSymbol noteSymbol) {
140 * 指定された調と同名のコードを構築します。
141 * <p>元の調がマイナーキーの場合はマイナーコード、
142 * それ以外の場合はメジャーコードになります。
146 public Chord(Key key) {
147 int keyCo5 = key.toCo5();
148 if( key.majorMinor() == Key.MajorMinor.MINOR ) {
152 setRoot(new NoteSymbol(keyCo5));
153 setBass(new NoteSymbol(keyCo5));
156 * コード名の文字列からコードを構築します。
157 * @param chordSymbol コード名の文字列
159 public Chord(String chordSymbol) {
160 setChordSymbol(chordSymbol);
166 public Chord clone() {
167 Chord newChord = new Chord(rootNoteSymbol);
168 newChord.offsets = new HashMap<>(offsets);
169 newChord.setBass(bassNoteSymbol);
173 * コードのルート音を指定された音階に置換します。
174 * @param rootNoteSymbol 音階
175 * @return このコード自身(置換後)
177 public Chord setRoot(NoteSymbol rootNoteSymbol) {
178 this.rootNoteSymbol = rootNoteSymbol;
182 * コードのベース音を指定された音階に置換します。
183 * @param rootNoteSymbol 音階
184 * @return このコード自身(置換後)
186 public Chord setBass(NoteSymbol rootNoteSymbol) {
187 this.bassNoteSymbol = rootNoteSymbol;
194 public void set(Interval itv) {
195 offsets.put(itv.getChromaticOffsetIndex(), itv);
199 * @param index 半音差インデックス
201 public void clear(OffsetIndex index) {
202 offsets.remove(index);
205 // コードネームの文字列が示すコードに置き換えます。
206 public Chord setChordSymbol(String chordSymbol) {
209 String parts[] = chordSymbol.trim().split("(/|on)");
210 if( parts.length == 0 ) {
214 setRoot(new NoteSymbol(parts[0]));
215 setBass(new NoteSymbol(parts[ parts.length > 1 ? 1 : 0 ]));
216 String suffix = parts[0].replaceFirst("^[A-G][#bx]*","");
219 String suffixParts[] = suffix.split("[\\(\\)]");
220 if( suffixParts.length == 0 ) {
223 String suffixParen = "";
224 if( suffixParts.length > 1 ) {
225 suffixParen = suffixParts[1];
226 suffix = suffixParts[0];
232 suffix.matches(".*(\\+5|aug|#5).*") ? Interval.SHARP5 :
233 suffix.matches(".*(-5|dim|b5).*") ? Interval.FLAT5 :
238 itv = suffix.matches(".*(M7|maj7|M9|maj9).*") ? Interval.MAJOR_SEVENTH :
239 suffix.matches(".*(6|dim[79]).*") ? Interval.SIXTH :
240 suffix.matches(".*7.*") ? Interval.SEVENTH :
243 clear(OffsetIndex.SEVENTH);
247 // マイナーの判定。maj7 と間違えないように比較
249 (suffix.matches(".*m.*") && ! suffix.matches(".*ma.*") ) ? Interval.MINOR :
250 suffix.matches(".*sus4.*") ? Interval.SUS4 :
255 if( suffix.matches(".*9.*") ) {
257 if( ! suffix.matches( ".*(add9|6|M9|maj9|dim9).*") ) {
258 set(Interval.SEVENTH);
262 offsets.remove(OffsetIndex.NINTH);
263 offsets.remove(OffsetIndex.ELEVENTH);
264 offsets.remove(OffsetIndex.THIRTEENTH);
267 String parts_in_paren[] = suffixParen.split(",");
268 for( String p : parts_in_paren ) {
269 if( p.matches("(\\+9|#9)") )
270 offsets.put(OffsetIndex.NINTH, Interval.SHARP9);
271 else if( p.matches("(-9|b9)") )
272 offsets.put(OffsetIndex.NINTH, Interval.FLAT9);
273 else if( p.matches("9") )
274 offsets.put(OffsetIndex.NINTH, Interval.NINTH);
276 if( p.matches("(\\+11|#11)") )
277 offsets.put(OffsetIndex.ELEVENTH, Interval.SHARP11);
278 else if( p.matches("11") )
279 offsets.put(OffsetIndex.ELEVENTH, Interval.ELEVENTH);
281 if( p.matches("(-13|b13)") )
282 offsets.put(OffsetIndex.THIRTEENTH, Interval.FLAT13);
283 else if( p.matches("13") )
284 offsets.put(OffsetIndex.THIRTEENTH, Interval.THIRTEENTH);
286 // -5 や +5 が () の中にあっても解釈できるようにする
287 if( p.matches("(-5|b5)") )
288 offsets.put(OffsetIndex.FIFTH, Interval.FLAT5);
289 else if( p.matches("(\\+5|#5)") )
290 offsets.put(OffsetIndex.FIFTH, Interval.SHARP5);
299 public NoteSymbol rootNoteSymbol() { return rootNoteSymbol; }
301 * ベース音を返します。分数コードの場合はルート音と異なります。
304 public NoteSymbol bassNoteSymbol() { return bassNoteSymbol; }
306 * 指定した音程が設定されているか調べます。
308 * @return 指定した音程が設定されていたらtrue
310 public boolean isSet(Interval itv) {
311 return offsets.get(itv.getChromaticOffsetIndex()) == itv;
314 * 指定したインデックスに音程が設定されているか調べます。
315 * @param index インデックス
316 * @return 指定したインデックスに音程が設定されていたらtrue
318 public boolean isSet(OffsetIndex index) {
319 return offsets.containsKey(index);
326 public boolean equals(Object anObject) {
327 if( this == anObject )
329 if( anObject instanceof Chord ) {
330 Chord another = (Chord) anObject;
331 if( ! rootNoteSymbol.equals(another.rootNoteSymbol) )
333 if( ! bassNoteSymbol.equals(another.bassNoteSymbol) )
335 return offsets.equals(another.offsets);
340 public int hashCode() {
341 return toString().hashCode();
344 * コードが等しいかどうかを、異名同音を無視して判定します。
345 * @param another 比較対象のコード
348 public boolean equalsEnharmonically(Chord another) {
349 if( this == another )
351 if( another == null )
353 if( ! rootNoteSymbol.equalsEnharmonically(another.rootNoteSymbol) )
355 if( ! bassNoteSymbol.equalsEnharmonically(another.bassNoteSymbol) )
357 return offsets.equals(another.offsets);
361 * (ルート音は含まれますが、ベース音は含まれません)。
365 public int numberOfNotes() { return offsets.size() + 1; }
367 * 指定された位置にあるノート番号を返します。
368 * @param index 位置(0をルート音とした構成音の順序)
369 * @return ノート番号(該当する音がない場合は -1)
371 public int noteAt(int index) {
372 int rootnote = rootNoteSymbol.toNoteNumber();
377 for( OffsetIndex offsetIndex : OffsetIndex.values() )
378 if( (itv = offsets.get(offsetIndex)) != null && ++i == index )
379 return rootnote + itv.getChromaticOffset();
383 * コード構成音を格納したノート番号の配列を返します。
385 * 音域が指定された場合、その音域に合わせたノート番号を返します。
386 * @param range 音域(null可)
387 * @param key キー(null可)
390 public int[] toNoteArray(Range range, Key key) {
391 int rootnote = rootNoteSymbol.toNoteNumber();
392 int ia[] = new int[numberOfNotes()];
396 for( OffsetIndex offsetIndex : OffsetIndex.values() )
397 if( (itv = offsets.get(offsetIndex)) != null )
398 ia[++i] = rootnote + itv.getChromaticOffset();
400 range.invertNotesOf(ia, key);
404 * MIDI ノート番号が、コードの構成音の何番目(0=ルート音)に
405 * あるかを表すインデックス値を返します。
406 * 構成音に該当しない場合は -1 を返します。
408 * @param noteNumber MIDIノート番号
409 * @return 構成音のインデックス値
411 public int indexOf(int noteNumber) {
412 int relativeNote = noteNumber - rootNoteSymbol.toNoteNumber();
413 if( Music.mod12(relativeNote) == 0 ) return 0;
416 for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
417 if( (itv = offsets.get(offsetIndex)) != null ) {
419 if( Music.mod12(relativeNote - itv.getChromaticOffset()) == 0 )
426 * 指定したキーのスケールを外れた構成音がないか調べます。
428 * @return スケールを外れている構成音がなければtrue
430 public boolean isOnScaleInKey(Key key) {
431 return isOnScaleInKey(key.toCo5());
433 private boolean isOnScaleInKey(int keyCo5) {
434 int rootnote = rootNoteSymbol.toNoteNumber();
435 if( ! Music.isOnScale(rootnote, keyCo5) )
438 for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
439 if( (itv = offsets.get(offsetIndex)) == null )
441 if( ! Music.isOnScale(rootnote + itv.getChromaticOffset(), keyCo5) )
448 * @param chromatic_offset 移調幅(半音単位)
449 * @return 移調した新しいコード(移調幅が0の場合は自分自身)
451 public Chord transpose(int chromatic_offset) {
452 return transpose(chromatic_offset, 0);
454 public Chord transpose(int chromatic_offset, Key original_key) {
455 return transpose(chromatic_offset, original_key.toCo5());
457 public Chord transpose(int chromatic_offset, int original_key_co5) {
458 if( chromatic_offset == 0 ) return this;
459 int offsetCo5 = Music.mod12(Music.reverseCo5(chromatic_offset));
460 if( offsetCo5 > 6 ) offsetCo5 -= 12;
461 int key_co5 = original_key_co5 + offsetCo5;
463 int newRootCo5 = rootNoteSymbol.toCo5() + offsetCo5;
464 int newBassCo5 = bassNoteSymbol.toCo5() + offsetCo5;
469 else if( key_co5 < -5 ) {
473 setRoot(new NoteSymbol(newRootCo5));
474 return setBass(new NoteSymbol(newBassCo5));
477 * この和音の文字列表現としてコード名を返します。
481 public String toString() {
482 String chordSymbol = rootNoteSymbol + symbolSuffix();
483 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
484 chordSymbol += "/" + bassNoteSymbol;
491 * Swing の {@link JLabel#setText(String)} は HTML で指定できるので、
492 * 文字の大きさに変化をつけることができます。
494 * @param colorName 色のHTML表現(色名または #RRGGBB 形式)
497 public String toHtmlString(String colorName) {
498 String span = "<span style=\"font-size: 120%\">";
499 String endSpan = "</span>";
500 String root = rootNoteSymbol.toString();
501 String formattedRoot = (root.length() == 1) ? root + span :
502 root.replace("#",span+"<sup>#</sup>").
503 replace("b",span+"<sup>b</sup>").
504 replace("x",span+"<sup>x</sup>");
505 String formattedBass = "";
506 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
507 String bass = bassNoteSymbol.toString();
508 formattedBass = (bass.length() == 1) ? bass + span :
509 bass.replace("#",span+"<sup>#</sup>").
510 replace("b",span+"<sup>b</sup>").
511 replace("x",span+"<sup>x</sup>");
512 formattedBass = "/" + formattedBass + endSpan;
514 String suffix = symbolSuffix().
515 replace("-5","<sup>-5</sup>").
516 replace("+5","<sup>+5</sup>");
519 "<span style=\"color: " + colorName + "; font-size: 170% ; white-space: nowrap ;\">" +
520 formattedRoot + suffix + endSpan + formattedBass +
528 public String toName() {
529 String name = rootNoteSymbol.toStringIn(NoteSymbol.Language.NAME) + nameSuffix() ;
530 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
531 name += " on " + bassNoteSymbol.toStringIn(NoteSymbol.Language.NAME);
536 * コードネームの音名を除いた部分(サフィックス)を組み立てて返します。
537 * @return コードネームの音名を除いた部分
539 public String symbolSuffix() {
541 offsets.get(OffsetIndex.THIRD) == Interval.MINOR ? "m" : ""
544 if( (itv = offsets.get(OffsetIndex.SEVENTH)) != null ) {
546 case SIXTH: suffix += "6"; break;
547 case SEVENTH: suffix += "7"; break;
548 case MAJOR_SEVENTH: suffix += "M7"; break;
552 switch( offsets.get(OffsetIndex.THIRD) ) {
553 case SUS4: suffix += "sus4"; break;
554 case SUS2: suffix += "sus2"; break;
557 switch( offsets.get(OffsetIndex.FIFTH) ) {
558 case FLAT5: suffix += "-5"; break;
559 case SHARP5: suffix += "+5"; break;
562 Vector<String> paren = new Vector<String>();
563 if( (itv = offsets.get(OffsetIndex.NINTH)) != null ) {
565 case NINTH: paren.add("9"); break;
566 case FLAT9: paren.add("-9"); break;
567 case SHARP9: paren.add("+9"); break;
571 if( (itv = offsets.get(OffsetIndex.ELEVENTH)) != null ) {
573 case ELEVENTH: paren.add("11"); break;
574 case SHARP11: paren.add("+11"); break;
578 if( (itv = offsets.get(OffsetIndex.THIRTEENTH)) != null ) {
580 case THIRTEENTH: paren.add("13"); break;
581 case FLAT13: paren.add("-13"); break;
585 if( ! paren.isEmpty() ) {
586 boolean is_first = true;
588 for( String p : paren ) {
597 if( suffix.equals("m-5") ) return "dim";
598 else if( suffix.equals("+5") ) return "aug";
599 else if( suffix.equals("m6-5") ) return "dim7";
600 else if( suffix.equals("(9)") ) return "add9";
601 else if( suffix.equals("7(9)") ) return "9";
602 else if( suffix.equals("M7(9)") ) return "M9";
603 else if( suffix.equals("7+5") ) return "aug7";
604 else if( suffix.equals("m6-5(9)") ) return "dim9";
608 * コードの説明のうち、音名を除いた部分を組み立てて返します。
609 * @return コード説明の音名を除いた部分
611 public String nameSuffix() {
613 if( offsets.get(OffsetIndex.THIRD) == Interval.MINOR )
616 if( (itv = offsets.get(OffsetIndex.SEVENTH)) != null ) {
618 case SIXTH: suffix += " 6th"; break;
619 case SEVENTH: suffix += " 7th"; break;
620 case MAJOR_SEVENTH: suffix += " major 7th"; break;
624 switch( offsets.get(OffsetIndex.THIRD) ) {
625 case SUS4: suffix += " suspended 4th"; break;
626 case SUS2: suffix += " suspended 2nd"; break;
629 switch( offsets.get(OffsetIndex.FIFTH) ) {
630 case FLAT5 : suffix += " flatted 5th"; break;
631 case SHARP5: suffix += " sharped 5th"; break;
634 Vector<String> paren = new Vector<String>();
635 if( (itv = offsets.get(OffsetIndex.NINTH)) != null ) {
637 case NINTH: paren.add("9th"); break;
638 case FLAT9: paren.add("flatted 9th"); break;
639 case SHARP9: paren.add("sharped 9th"); break;
643 if( (itv = offsets.get(OffsetIndex.ELEVENTH)) != null ) {
645 case ELEVENTH: paren.add("11th"); break;
646 case SHARP11: paren.add("sharped 11th"); break;
650 if( (itv = offsets.get(OffsetIndex.THIRTEENTH)) != null ) {
652 case THIRTEENTH: paren.add("13th"); break;
653 case FLAT13: paren.add("flatted 13th"); break;
657 if( ! paren.isEmpty() ) {
658 boolean is_first = true;
659 suffix += "(additional ";
660 for( String p : paren ) {
669 if( suffix.equals(" minor flatted 5th") ) return " diminished (triad)";
670 else if( suffix.equals(" sharped 5th") ) return " augumented";
671 else if( suffix.equals(" minor 6th flatted 5th") ) return " diminished 7th";
672 else if( suffix.equals(" 7th(additional 9th)") ) return " 9th";
673 else if( suffix.equals(" major 7th(additional 9th)") ) return " major 9th";
674 else if( suffix.equals(" 7th sharped 5th") ) return " augumented 7th";
675 else if( suffix.equals(" minor 6th flatted 5th(additional 9th)") ) return " diminished 9th";
676 else if( suffix.isEmpty() ) return " major";