1 package camidion.chordhelper.music;
4 import java.util.Arrays;
5 import java.util.Collection;
6 import java.util.Collections;
7 import java.util.HashMap;
10 import java.util.Objects;
11 import java.util.StringJoiner;
13 import javax.swing.JLabel;
16 * 和音(コード - musical chord)のクラス(値は不変)
19 /** コード構成音の順序に対応する色 */
20 public static final Color NOTE_INDEX_COLORS[] = {
22 new Color(0x40,0x40,0xFF),
23 Color.orange.darker(),
24 new Color(0x20,0x99,0x00),
30 private static enum IntervalGroup {
31 THIRD, FIFTH, SEVENTH, NINTH, ELEVENTH, THIRTEENTH
34 public static enum Interval {
36 /** 長2度(major 2nd / sus2) */
37 SUS2(2, IntervalGroup.THIRD, "sus2", "suspended 2nd"),
39 MINOR(3, IntervalGroup.THIRD, "m", "minor"),
41 MAJOR(4, IntervalGroup.THIRD, "", "major"),
42 /** 完全4度(parfect 4th / sus4) */
43 SUS4(5, IntervalGroup.THIRD, "sus4", "suspended 4th"),
45 /** 減5度または増4度(トライトーン = 三全音 = 半オクターブ) */
46 FLAT5(6, IntervalGroup.FIFTH, "-5", "flatted 5th"),
48 PARFECT5(7, IntervalGroup.FIFTH, "", "parfect 5th"),
50 SHARP5(8, IntervalGroup.FIFTH, "+5", "sharped 5th"),
53 SIXTH(9, IntervalGroup.SEVENTH, "6", "6th"),
55 SEVENTH(10, IntervalGroup.SEVENTH, "7", "7th"),
57 MAJOR_SEVENTH(11, IntervalGroup.SEVENTH, "M7", "major 7th"),
59 /** 短9度(短2度の1オクターブ上) */
60 FLAT9(13, IntervalGroup.NINTH, "-9", "flatted 9th"),
61 /** 長9度(長2度の1オクターブ上) */
62 NINTH(14, IntervalGroup.NINTH, "9", "9th"),
63 /** 増9度(増2度の1オクターブ上) */
64 SHARP9(15, IntervalGroup.NINTH, "+9", "sharped 9th"),
66 /** 完全11度(完全4度の1オクターブ上) */
67 ELEVENTH(17, IntervalGroup.ELEVENTH, "11", "11th"),
68 /** 増11度(増4度の1オクターブ上) */
69 SHARP11(18, IntervalGroup.ELEVENTH, "+11", "sharped 11th"),
71 /** 短13度(短6度の1オクターブ上) */
72 FLAT13(20, IntervalGroup.THIRTEENTH, "-13", "flatted 13th"),
73 /** 長13度(長6度の1オクターブ上) */
74 THIRTEENTH(21, IntervalGroup.THIRTEENTH, "13", "13th");
76 private Interval(int chromaticOffset, IntervalGroup intervalGroup, String symbol, String description) {
77 this.chromaticOffset = chromaticOffset;
78 this.intervalGroup = intervalGroup;
80 this.description = description;
83 * 半音差を返します。これはルート音からの音程差を半音単位で表した値です。
86 public int getChromaticOffset() { return chromaticOffset; }
87 private int chromaticOffset;
89 * この音程が属しているグループを返します。
90 * @return この音程が属しているグループ
92 public IntervalGroup getIntervalGroup() { return intervalGroup; }
93 private IntervalGroup intervalGroup;
98 public String getSymbol() { return symbol; }
99 private String symbol;
104 public String getDescription() { return description; }
105 private String description;
108 * コードネームの音名を除いた部分(サフィックス)を組み立てて返します。
109 * @return コードネームの音名を除いた部分
111 public String symbolSuffix() {
113 Interval i3 = intervalMap.get(IntervalGroup.THIRD);
114 Interval i5 = intervalMap.get(IntervalGroup.FIFTH);
115 Interval i7 = intervalMap.get(IntervalGroup.SEVENTH);
116 if( Interval.MINOR == i3 ) suffix += i3.getSymbol();
117 if( i7 != null ) suffix += i7.getSymbol();
118 if( SUSPENDED.contains(i3) ) suffix += i3.getSymbol();
119 if( Interval.PARFECT5 != i5 ) suffix += i5.getSymbol();
120 StringJoiner inParen = new StringJoiner(",","(",")").setEmptyValue("");
121 for( IntervalGroup group : EXTENDED ) {
122 Interval i9 = intervalMap.get(group);
123 if( i9 != null ) inParen.add(i9.getSymbol());
125 String alias = symbolSuffixAliases.get(suffix += inParen);
126 return alias == null ? suffix : alias;
129 * コードの説明のうち、音名を除いた部分を組み立てて返します。
130 * @return コード説明の音名を除いた部分
132 public String nameSuffix() {
134 Interval i3 = intervalMap.get(IntervalGroup.THIRD);
135 Interval i5 = intervalMap.get(IntervalGroup.FIFTH);
136 Interval i7 = intervalMap.get(IntervalGroup.SEVENTH);
137 if( Interval.MINOR == i3 ) suffix += " " + i3.getDescription();
138 if( i7 != null ) suffix += " " + i7.getDescription();
139 if( SUSPENDED.contains(i3) ) suffix += " " + i3.getDescription();
140 if( Interval.PARFECT5 != i5 ) suffix += " " + i5.getDescription();
141 StringJoiner inParen = new StringJoiner(",","(",")").setEmptyValue("");
142 for( IntervalGroup index : EXTENDED ) {
143 Interval i9 = intervalMap.get(index);
144 if( i9 != null ) inParen.add(i9.getDescription());
146 String alias = nameSuffixAliases.get(suffix += inParen);
147 return alias == null ? suffix : alias;
149 private static final List<Interval> SUSPENDED = Arrays.asList(
152 private static final List<IntervalGroup> EXTENDED = Arrays.asList(
154 IntervalGroup.ELEVENTH,
155 IntervalGroup.THIRTEENTH);
156 private static final Map<String, String> symbolSuffixAliases = new HashMap<String, String>() {
165 put("m6-5(9)", "dim9");
168 private static final Map<String, String> nameSuffixAliases = new HashMap<String, String>() {
170 put("", " "+Interval.MAJOR.getDescription());
171 put(" minor flatted 5th", " diminished (triad)");
172 put(" sharped 5th", " augumented");
173 put(" minor 6th flatted 5th", " diminished 7th");
174 put("(9th)", " additional 9th");
175 put(" 7th(9th)", " 9th");
176 put(" major 7th(9th)", " major 9th");
177 put(" 7th sharped 5th", " augumented 7th");
178 put(" minor 6th flatted 5th(9th)", " diminished 9th");
181 private void setSymbolSuffix(String suffix) {
183 String outInParen[] = suffix.split("[\\(\\)]");
184 if( outInParen.length == 0 ) return;
185 String outParen = suffix;
187 if( outInParen.length > 1 ) {
188 outParen = outInParen[0];
189 inParen = outInParen[1];
192 if( outParen.matches(".*(\\+5|aug|#5).*") ) set(Interval.SHARP5);
193 else if( outParen.matches(".*(-5|dim|b5).*") ) set(Interval.FLAT5);
196 if( outParen.matches(".*(M7|maj7|M9|maj9).*") ) set(Interval.MAJOR_SEVENTH);
197 else if( outParen.matches(".*(6|dim[79]).*") ) set(Interval.SIXTH);
198 else if( outParen.matches(".*7.*") ) set(Interval.SEVENTH);
200 // minor sus4 (m と maj7 を間違えないよう比較しつつ、mmaj7 も認識させる)
201 if( outParen.matches(".*m.*") && ! outParen.matches(".*ma.*") || outParen.matches(".*mma.*") ) {
204 else if( outParen.matches(".*sus4.*") ) set(Interval.SUS4);
207 if( outParen.matches(".*9.*") ) {
209 if( ! outParen.matches(".*(add9|6|M9|maj9|dim9).*") ) {
210 set(Interval.SEVENTH);
213 else for(String p : inParen.split(",")) {
214 if( p.matches("(\\+9|#9)") ) set(Interval.SHARP9);
215 else if( p.matches("(-9|b9)") ) set(Interval.FLAT9);
216 else if( p.matches("9") ) set(Interval.NINTH);
218 if( p.matches("(\\+11|#11)") ) set(Interval.SHARP11);
219 else if( p.matches("11") ) set(Interval.ELEVENTH);
221 if( p.matches("(-13|b13)") ) set(Interval.FLAT13);
222 else if( p.matches("13") ) set(Interval.THIRTEENTH);
224 // -5 や +5 が () の中にあっても解釈できるようにする
225 if( p.matches("(-5|b5)") ) set(Interval.FLAT5);
226 else if( p.matches("(\\+5|#5)") ) set(Interval.SHARP5);
233 public Note rootNoteSymbol() { return rootNoteSymbol; }
234 private Note rootNoteSymbol;
236 * ベース音を返します。オンコードの場合はルート音と異なります。
239 public Note bassNoteSymbol() { return bassNoteSymbol; }
240 private Note bassNoteSymbol;
242 * 指定した音程が設定されているか調べます。
244 * @return 指定した音程が設定されていたらtrue
246 public boolean isSet(Interval interval) {
247 return interval.equals(intervalMap.get(interval.getIntervalGroup()));
250 * このコードの構成音(ルート音自身やベース音は含まない)を、
251 * ルート音からの音程のコレクション(変更不可能なビュー)として返します。
254 public Collection<Interval> intervals() { return intervals; }
255 private Collection<Interval> intervals;
256 private void fixIntervals() {
257 intervals = Collections.unmodifiableCollection(intervalMap.values());
259 /** 現在有効な構成音(ルート、ベースは除く)の音程 */
260 private Map<IntervalGroup, Interval> intervalMap;
261 private void set(Interval interval) {
262 intervalMap.put(interval.getIntervalGroup(), interval);
264 private void set(Collection<Interval> intervals) {
265 for(Interval interval : intervals) if(interval != null) set(interval);
269 * 指定されたルート音、構成音を持つコードを構築します。
271 * @param intervals その他の構成音の音程
272 * (メジャーコードの構成音である長三度・完全五度は、指定しなくてもデフォルトで設定されます)
273 * @throws NullPointerException ルート音にnullが指定された場合
275 public Chord(Note root, Interval... intervals) {
276 this(root, root, intervals);
279 * 指定されたルート音、ベース音、構成音を持つコードを構築します。
281 * @param bass ベース音(nullの場合はルート音が指定されたとみなされます)
282 * @param intervals その他の構成音の音程
283 * (メジャーコードの構成音である長三度・完全五度は、指定しなくてもデフォルトで設定されます)
284 * @throws NullPointerException ルート音にnullが指定された場合
286 public Chord(Note root, Note bass, Interval... intervals) {
287 this(root, bass, Arrays.asList(intervals));
290 * 指定されたルート音、ベース音、構成音を持つコードを構築します。
292 * @param bass ベース音(nullの場合はルート音が指定されたとみなされます)
293 * @param intervals その他の構成音の音程のコレクション
294 * (メジャーコードの構成音である長三度・完全五度は、指定しなくてもデフォルトで設定されます)
295 * @throws NullPointerException ルート音、または構成音コレクションにnullが指定された場合
297 public Chord(Note root, Note bass, Collection<Interval> intervals) {
298 rootNoteSymbol = Objects.requireNonNull(root);
299 bassNoteSymbol = (bass==null ? root : bass);
300 intervalMap = new HashMap<>();
302 set(Interval.PARFECT5);
307 * 元のコードのルート音、ベース音以外の構成音の一部を変更した新しいコードを構築します。
309 * @param intervals 変更したい構成音の音程(ルート音、ベース音を除く)
310 * @throws NullPointerException 元のコードにnullが指定された場合
312 public Chord(Chord original, Interval... intervals) {
313 this(original, Arrays.asList(intervals));
316 * 元のコードのルート音、ベース音以外の構成音の一部を変更した新しいコードを構築します。
318 * @param intervals 変更したい構成音の音程(ルート音、ベース音を除く)のコレクション
319 * @throws NullPointerException 引数にnullが指定された場合
321 public Chord(Chord original, Collection<Interval> intervals) {
322 rootNoteSymbol = original.rootNoteSymbol;
323 bassNoteSymbol = original.bassNoteSymbol;
324 intervalMap = new HashMap<>(original.intervalMap);
329 * 指定された調と同名のコードを構築します。
331 * @throws NullPointerException 調にnullが指定された場合
333 public Chord(Key key) {
334 intervalMap = new HashMap<>(); set(Interval.PARFECT5);
335 int keyCo5 = key.toCo5(); if( key.majorMinor() == Key.MajorMinor.MINOR ) {
336 keyCo5 += 3; set(Interval.MINOR);
337 } else set(Interval.MAJOR);
338 bassNoteSymbol = rootNoteSymbol = new Note(keyCo5);
343 * @param chordSymbol コード名
344 * @throws NullPointerException コード名にnullが指定された場合
345 * @throws IllegalArgumentException コード名が空文字列の場合、または音名で始まっていない場合
347 public Chord(String chordSymbol) {
348 String rootOnBass[] = Objects.requireNonNull(chordSymbol, "Chord symbol must not be null").trim().split("(/|on)");
349 String root = (rootOnBass.length > 0 ? rootOnBass[0].trim() : "");
350 bassNoteSymbol = rootNoteSymbol = new Note(root);
351 if( rootOnBass.length > 1 ) {
352 String bass = rootOnBass[1].trim();
353 if( ! root.equals(bass) ) bassNoteSymbol = new Note(bass);
355 intervalMap = new HashMap<>();
357 set(Interval.PARFECT5);
358 setSymbolSuffix(root.replaceFirst("^[A-G][#bx]*",""));
362 * コードの同一性を判定します。ルート音、ベース音の異名同音は異なるものとみなされます。
363 * @param anObject 比較対象
365 * @see #equalsEnharmonically(Chord)
368 public boolean equals(Object anObject) {
369 if( anObject == this ) return true;
370 if( anObject instanceof Chord ) {
371 Chord another = (Chord) anObject;
372 if( ! rootNoteSymbol.equals(another.rootNoteSymbol) ) return false;
373 if( ! bassNoteSymbol.equals(another.bassNoteSymbol) ) return false;
374 return intervalMap.equals(another.intervalMap);
379 public int hashCode() { return toString().hashCode(); }
381 * ルート音、ベース音の異名同音を同じとみなしたうえで、コードの同一性を判定します。
382 * @param another 比較対象のコード
384 * @see #equals(Object)
386 public boolean equalsEnharmonically(Chord another) {
387 if( another == this ) return true;
388 if( another != null ) {
389 if( ! rootNoteSymbol.equalsEnharmonically(another.rootNoteSymbol) ) return false;
390 if( ! bassNoteSymbol.equalsEnharmonically(another.bassNoteSymbol) ) return false;
391 return intervalMap.equals(another.intervalMap);
396 * コード構成音の数を返します。ルート音は含まれますが、ベース音は含まれません。
399 public int numberOfNotes() { return intervalMap.size() + 1; }
401 * 指定された位置にある構成音のノート番号を返します。
402 * @param index 位置(0をルート音とした構成音の順序)
403 * @return ノート番号(該当する音がない場合は -1)
405 public int noteAt(int index) {
406 int root = rootNoteSymbol.toNoteNumber();
407 if( index == 0 ) return root;
409 for( IntervalGroup group : IntervalGroup.values() ) {
410 Interval interval = intervalMap.get(group);
411 if( interval == null ) continue;
412 if( ++current == index ) return root + interval.getChromaticOffset();
417 * コード構成音を格納したノート番号の配列を返します(ベース音は含まれません)。
418 * 音域が指定された場合、その音域の範囲内に収まるように転回されます。
419 * @param range 音域(null可)
420 * @param key キー(null可)
423 public int[] toNoteArray(Range range, Key key) {
424 int rootnote = rootNoteSymbol.toNoteNumber();
425 int ia[] = new int[numberOfNotes()];
429 for( IntervalGroup offsetIndex : IntervalGroup.values() )
430 if( (itv = intervalMap.get(offsetIndex)) != null )
431 ia[++i] = rootnote + itv.getChromaticOffset();
432 if( range != null ) range.invertNotesOf(ia, key);
436 * MIDIノート番号が、コードの構成音の何番目(0=ルート音)にあるかを表すインデックス値を返します。
437 * 構成音に該当しない場合は -1 を返します。ベース音は検索されません。
438 * @param noteNumber MIDIノート番号
439 * @return 構成音のインデックス値
441 public int indexOf(int noteNumber) {
442 int relativeNote = noteNumber - rootNoteSymbol.toNoteNumber();
443 if( Note.mod12(relativeNote) == 0 ) return 0;
446 for( IntervalGroup offsetIndex : IntervalGroup.values() ) {
447 if( (itv = intervalMap.get(offsetIndex)) != null ) {
449 if( Note.mod12(relativeNote - itv.getChromaticOffset()) == 0 )
456 * 指定したキーのスケールを外れた構成音がないか調べます。
458 * @return スケールを外れている構成音がなければtrue
459 * @throws NullPointerException キーにnullが指定された場合
461 public boolean isOnScaleIn(Key key) {
462 int root = rootNoteSymbol.toNoteNumber();
463 if( ! key.isOnScale(root) ) return false;
465 for( IntervalGroup offsetIndex : IntervalGroup.values() ) {
466 if( (itv = intervalMap.get(offsetIndex)) == null ) continue;
467 if( ! key.isOnScale(root + itv.getChromaticOffset()) ) return false;
472 * 指定された調に近いほうの♯、♭の表記で、移調したコードを返します。
473 * @param chromaticOffset 移調幅(半音単位)
474 * @param originalKey 基準とする調
475 * @return 移調した新しいコード(移調幅が0の場合は自分自身)
477 public Chord transposedNewChord(int chromaticOffset, Key originalKey) {
478 return transposedNewChord(chromaticOffset, originalKey.toCo5());
481 * C/Amの調に近いほうの♯、♭の表記で、移調したコードを返します。
482 * @param chromaticOffset 移調幅(半音単位)
483 * @return 移調した新しいコード(移調幅が0の場合は自分自身)
485 public Chord transposedNewChord(int chromaticOffset) {
486 return transposedNewChord(chromaticOffset, 0);
488 private Chord transposedNewChord(int chromaticOffset, int originalKeyCo5) {
489 if( chromaticOffset == 0 ) return this;
490 int offsetCo5 = Note.mod12(Note.toggleCo5(chromaticOffset));
491 if( offsetCo5 > 6 ) offsetCo5 -= 12;
492 int keyCo5 = originalKeyCo5 + offsetCo5;
493 int newRootCo5 = rootNoteSymbol.toCo5() + offsetCo5;
494 int newBassCo5 = bassNoteSymbol.toCo5() + offsetCo5;
499 else if( keyCo5 < -5 ) {
503 Note root = new Note(newRootCo5);
504 Note bass = (newBassCo5 == newRootCo5 ? root : new Note(newBassCo5));
505 return new Chord(root, bass, intervals);
508 /** この和音の文字列表現としてコード名(例: C、Am、G7)を返します。 */
510 public String toString() {
511 String chordSymbol = rootNoteSymbol + symbolSuffix();
512 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) chordSymbol += "/" + bassNoteSymbol;
518 * Swing の {@link JLabel#setText(String)} は HTML で指定できるので、
519 * 文字の大きさに変化をつけることができます。
521 * @param colorName 色のHTML表現(色名または #RRGGBB 形式)
524 public String toHtmlString(String colorName) {
525 String smallSpan = "<span style=\"font-size: 120%\">";
526 String endSpan = "</span>";
527 String root = rootNoteSymbol.toString();
528 String formattedRoot = (root.length() == 1) ? root + smallSpan :
529 root.replace("#",smallSpan+"<sup>#</sup>").
530 replace("b",smallSpan+"<sup>b</sup>").
531 replace("x",smallSpan+"<sup>x</sup>");
532 String formattedBass = "";
533 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
534 String bass = bassNoteSymbol.toString();
535 formattedBass = (bass.length() == 1) ? bass + smallSpan :
536 bass.replace("#",smallSpan+"<sup>#</sup>").
537 replace("b",smallSpan+"<sup>b</sup>").
538 replace("x",smallSpan+"<sup>x</sup>");
539 formattedBass = "/" + formattedBass + endSpan;
541 String suffix = symbolSuffix().
542 replace("-5","<sup>-5</sup>").
543 replace("+5","<sup>+5</sup>");
546 "<span style=\"color: " + colorName + "; font-size: 170% ; white-space: nowrap ;\">" +
547 formattedRoot + suffix + endSpan + formattedBass +
555 public String toName() {
556 String name = rootNoteSymbol.toStringIn(Note.Language.NAME) + nameSuffix() ;
557 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
558 name += " on " + bassNoteSymbol.toStringIn(Note.Language.NAME);