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 = new HashMap<OffsetIndex, Interval>() {
194 private void set(Interval itv) { put(itv.getChromaticOffsetIndex(), itv); }
195 { set(Interval.MAJOR); set(Interval.PARFECT5); }
200 private NoteSymbol rootNoteSymbol;
202 * このコードのベース音(ルート音と異なる場合は分数コードの分母)
204 private NoteSymbol bassNoteSymbol;
207 * 指定されたルート音と構成音を持つメジャーコードを構築します。
208 * @param root ルート音(ベース音としても使う)
209 * @param itvs その他の構成音の音程
211 public Chord(NoteSymbol root, Interval... itvs) { this(root, root, itvs); }
213 * 指定されたルート音、ベース音、構成音を持つメジャーコードを構築します。
216 * @param itvs その他の構成音の音程
218 public Chord(NoteSymbol root, NoteSymbol bass, Interval... itvs) {
219 this(root, bass, Arrays.asList(itvs));
222 * 指定されたルート音、ベース音、構成音を持つメジャーコードを構築します。
225 * @param itvs その他の構成音の音程
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);
233 * 指定された調と同名のコードを構築します。
236 public Chord(Key key) {
237 int keyCo5 = key.toCo5();
238 if( key.majorMinor() == Key.MajorMinor.MINOR ) {
239 keyCo5 += 3; set(Interval.MINOR);
241 bassNoteSymbol = rootNoteSymbol = new NoteSymbol(keyCo5);
244 * コード名の文字列からコードを構築します。
245 * @param chordSymbol コード名の文字列
247 public Chord(String chordSymbol) {
250 String parts[] = chordSymbol.trim().split("(/|on)");
251 if( parts.length == 0 ) return;
254 rootNoteSymbol = new NoteSymbol(parts[0]);
255 if( parts.length > 1 && ! parts[0].equals(parts[1]) ) {
256 bassNoteSymbol = new NoteSymbol(parts[1]);
258 bassNoteSymbol = rootNoteSymbol;
261 String suffix = parts[0].replaceFirst("^[A-G][#bx]*","");
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];
272 if( suffix.matches(".*(\\+5|aug|#5).*") ) set(Interval.FLAT5);
273 else if( suffix.matches(".*(-5|dim|b5).*") ) set(Interval.SHARP5);
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);
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);
285 if( suffix.matches(".*9.*") ) {
287 if( ! suffix.matches(".*(add9|6|M9|maj9|dim9).*") ) set(Interval.SEVENTH);
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);
296 if( p.matches("(\\+11|#11)") ) set(Interval.SHARP11);
297 else if( p.matches("11") ) set(Interval.ELEVENTH);
299 if( p.matches("(-13|b13)") ) set(Interval.FLAT13);
300 else if( p.matches("13") ) set(Interval.THIRTEENTH);
302 // -5 や +5 が () の中にあっても解釈できるようにする
303 if( p.matches("(-5|b5)") ) set(Interval.FLAT5);
304 else if( p.matches("(\\+5|#5)") ) set(Interval.SHARP5);
312 public NoteSymbol rootNoteSymbol() { return rootNoteSymbol; }
314 * ベース音を返します。分数コードの場合はルート音と異なります。
317 public NoteSymbol bassNoteSymbol() { return bassNoteSymbol; }
319 * このコードの構成音を、ルート音からの音程の配列として返します。
320 * ルート音自身やベース音は含まれません。
323 public Interval[] intervals() {
324 return offsets.values().toArray(new Interval[offsets.size()]);
327 * 指定した音程が設定されているか調べます。
329 * @return 指定した音程が設定されていたらtrue
331 public boolean isSet(Interval itv) {
332 return itv.equals(offsets.get(itv.getChromaticOffsetIndex()));
335 * コードの同一性を判定します。ルート音、ベース音の異名同音は異なるものとみなされます。
336 * @param anObject 比較対象
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);
351 public int hashCode() { return toString().hashCode(); }
353 * ルート音、ベース音の異名同音を同じとみなしたうえで、コードの同一性を判定します。
354 * @param another 比較対象のコード
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);
365 * コード構成音の数を返します。ルート音は含まれますが、ベース音は含まれません。
368 public int numberOfNotes() { return offsets.size() + 1; }
370 * 指定された位置にある構成音のノート番号を返します。
371 * @param index 位置(0をルート音とした構成音の順序)
372 * @return ノート番号(該当する音がない場合は -1)
374 public int noteAt(int index) {
375 int rootnote = rootNoteSymbol.toNoteNumber();
376 if( index == 0 ) return rootnote;
379 for( OffsetIndex offsetIndex : OffsetIndex.values() )
380 if( (itv = offsets.get(offsetIndex)) != null && ++i == index )
381 return rootnote + itv.getChromaticOffset();
385 * コード構成音を格納したノート番号の配列を返します(ベース音は含まれません)。
386 * 音域が指定された場合、その音域の範囲内に収まるように転回されます。
387 * @param range 音域(null可)
388 * @param key キー(null可)
391 public int[] toNoteArray(Range range, Key key) {
392 int rootnote = rootNoteSymbol.toNoteNumber();
393 int ia[] = new int[numberOfNotes()];
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);
404 * MIDIノート番号が、コードの構成音の何番目(0=ルート音)にあるかを表すインデックス値を返します。
405 * 構成音に該当しない場合は -1 を返します。ベース音は検索されません。
406 * @param noteNumber MIDIノート番号
407 * @return 構成音のインデックス値
409 public int indexOf(int noteNumber) {
410 int relativeNote = noteNumber - rootNoteSymbol.toNoteNumber();
411 if( Music.mod12(relativeNote) == 0 ) return 0;
414 for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
415 if( (itv = offsets.get(offsetIndex)) != null ) {
417 if( Music.mod12(relativeNote - itv.getChromaticOffset()) == 0 )
424 * 指定したキーのスケールを外れた構成音がないか調べます。
426 * @return スケールを外れている構成音がなければtrue
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;
433 for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
434 if( (itv = offsets.get(offsetIndex)) == null ) continue;
435 if( ! Music.isOnScale(rootnote + itv.getChromaticOffset(), keyCo5) ) return false;
440 * C/Amの調に近いほうの♯、♭の表記で、移調したコードを返します。
441 * @param chromaticOffset 移調幅(半音単位)
442 * @return 移調した新しいコード(移調幅が0の場合は自分自身)
444 public Chord transposedChord(int chromaticOffset) {
445 return transposedChord(chromaticOffset, 0);
448 * 指定された調に近いほうの♯、♭の表記で、移調したコードを返します。
449 * @param chromaticOffset 移調幅(半音単位)
450 * @param originalKey 基準とする調
451 * @return 移調した新しいコード(移調幅が0の場合は自分自身)
453 public Chord transposedChord(int chromaticOffset, Key originalKey) {
454 return transposedChord(chromaticOffset, originalKey.toCo5());
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;
462 int newRootCo5 = rootNoteSymbol.toCo5() + offsetCo5;
463 int newBassCo5 = bassNoteSymbol.toCo5() + offsetCo5;
468 else if( keyCo5 < -5 ) {
472 return new Chord(new NoteSymbol(newRootCo5), new NoteSymbol(newBassCo5), intervals());
476 * この和音の文字列表現としてコード名を返します。
480 public String toString() {
481 String chordSymbol = rootNoteSymbol + symbolSuffix();
482 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) chordSymbol += "/" + bassNoteSymbol;
488 * Swing の {@link JLabel#setText(String)} は HTML で指定できるので、
489 * 文字の大きさに変化をつけることができます。
491 * @param colorName 色のHTML表現(色名または #RRGGBB 形式)
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;
511 String suffix = symbolSuffix().
512 replace("-5","<sup>-5</sup>").
513 replace("+5","<sup>+5</sup>");
516 "<span style=\"color: " + colorName + "; font-size: 170% ; white-space: nowrap ;\">" +
517 formattedRoot + suffix + endSpan + formattedBass +
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);
537 public Chord clone() {
538 Chord newChord = new Chord(rootNoteSymbol, bassNoteSymbol);
539 newChord.offsets = new HashMap<>(offsets);
546 public void set(Interval itv) {
547 offsets.put(itv.getChromaticOffsetIndex(), itv);
553 public void clear(Interval itv) {
554 offsets.remove(itv.getChromaticOffsetIndex());