OSDN Git Service

リファクタリング(Chord#set()、Chord#clear()を外部から使う箇所を減らした)
[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;
194         /**
195          * このコードのルート音
196          */
197         private NoteSymbol rootNoteSymbol;
198         /**
199          * このコードのベース音(ルート音と異なる場合は分数コードの分母)
200          */
201         private NoteSymbol bassNoteSymbol;
202
203         /**
204          * 指定されたルート音と構成音を持つコードを構築します。
205          * @param root ルート音(ベース音としても使う)
206          * @param intervals その他の構成音の音程(長三度、完全五度は指定しなくてもデフォルトで設定されます)
207          */
208         public Chord(NoteSymbol root, Interval... intervals) {
209                 this(root, root, intervals);
210         }
211         /**
212          * 指定されたルート音、ベース音、構成音を持つコードを構築します。
213          * @param root ルート音
214          * @param bass ベース音
215          * @param intervals その他の構成音の音程(長三度、完全五度は指定しなくてもデフォルトで設定されます)
216          */
217         public Chord(NoteSymbol root, NoteSymbol bass, Interval... intervals) {
218                 this(root, bass, Arrays.asList(intervals));
219         }
220         /**
221          * 指定されたルート音、ベース音、構成音を持つコードを構築します。
222          * @param root ルート音
223          * @param bass ベース音
224          * @param intervals その他の構成音の音程(長三度、完全五度は指定しなくてもデフォルトで設定されます)
225          */
226         public Chord(NoteSymbol root, NoteSymbol bass, Collection<Interval> intervals) {
227                 rootNoteSymbol = root;
228                 bassNoteSymbol = bass;
229                 offsets = new HashMap<>();
230                 set(Interval.MAJOR);
231                 set(Interval.PARFECT5);
232                 set(intervals);
233         }
234         /**
235          * 元のコードのルート音、ベース音以外の構成音の一部を変更した新しいコードを構築します。
236          * @param chord 元のコード
237          * @param intervals 変更したい構成音の音程(ルート音、ベース音を除く)
238          * @throws NullPointerException 元のコードにnullが指定された場合
239          */
240         public Chord(Chord original, Interval... intervals) {
241                 this(original, Arrays.asList(intervals));
242         }
243         /**
244          * 元のコードのルート音、ベース音以外の構成音の一部を変更した新しいコードを構築します。
245          * @param chord 元のコード
246          * @param intervals 変更したい構成音の音程(ルート音、ベース音を除く)
247          * @throws NullPointerException 元のコードにnullが指定された場合
248          */
249         public Chord(Chord original, Collection<Interval> intervals) {
250                 rootNoteSymbol = original.rootNoteSymbol;
251                 bassNoteSymbol = original.bassNoteSymbol;
252                 offsets = new HashMap<>(original.offsets);
253                 set(intervals);
254         }
255         /**
256          * 指定された調と同名のコードを構築します。
257          * @param key 調
258          * @throws NullPointerException 調にnullが指定された場合
259          */
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);
266         }
267         /**
268          * コード名からコードを構築します。
269          * @param chordSymbol コード名
270          * @throws NullPointerException コード名にnullが指定された場合
271          */
272         public Chord(String chordSymbol) {
273                 offsets = new HashMap<>();
274                 set(Interval.MAJOR);
275                 set(Interval.PARFECT5);
276                 //
277                 // 分数コードの分子と分母に分け、先頭の音名を取り込む
278                 String rootOnBass[] = chordSymbol.trim().split("(/|on)");
279                 if( rootOnBass.length == 0 ) {
280                         bassNoteSymbol = rootNoteSymbol = new NoteSymbol();
281                         return;
282                 }
283                 rootNoteSymbol = new NoteSymbol(rootOnBass[0]);
284                 if( rootOnBass.length > 1 && ! rootOnBass[0].equals(rootOnBass[1]) ) {
285                         // 分子(ルート音)と異なる分母(ベース音)が指定されていた場合
286                         bassNoteSymbol = new NoteSymbol(rootOnBass[1]);
287                 } else {
288                         bassNoteSymbol = rootNoteSymbol;
289                 }
290                 // 先頭の音名はもういらないので削除し、サフィックスだけ残す
291                 String suffix = rootOnBass[0].replaceFirst("^[A-G][#bx]*","");
292                 //
293                 // ()の中と外に分ける
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];
300                 }
301                 // +5 -5 aug dim
302                 if( suffix.matches(".*(\\+5|aug|#5).*") ) set(Interval.FLAT5);
303                 else if( suffix.matches(".*(-5|dim|b5).*") ) set(Interval.SHARP5);
304                 //
305                 // 6 7 M7
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);
309                 //
310                 // minor sus4  (m と maj7 を間違えないよう比較しつつ、mmaj7 も認識させる)
311                 if( suffix.matches(".*m.*") && ! suffix.matches(".*ma.*") || suffix.matches(".*mma.*") ) {
312                         set(Interval.MINOR);
313                 }
314                 else if( suffix.matches(".*sus4.*") ) set(Interval.SUS4);
315                 //
316                 // 9th の判定
317                 if( suffix.matches(".*9.*") ) {
318                         set(Interval.NINTH);
319                         if( ! suffix.matches(".*(add9|6|M9|maj9|dim9).*") ) set(Interval.SEVENTH);
320                 }
321                 else {
322                         // () の中を , で分ける
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);
327
328                                 if( p.matches("(\\+11|#11)") ) set(Interval.SHARP11);
329                                 else if( p.matches("11") ) set(Interval.ELEVENTH);
330
331                                 if( p.matches("(-13|b13)") ) set(Interval.FLAT13);
332                                 else if( p.matches("13") ) set(Interval.THIRTEENTH);
333
334                                 // -5 や +5 が () の中にあっても解釈できるようにする
335                                 if( p.matches("(-5|b5)") ) set(Interval.FLAT5);
336                                 else if( p.matches("(\\+5|#5)") ) set(Interval.SHARP5);
337                         }
338                 }
339         }
340         /**
341          * ルート音を返します。
342          * @return ルート音
343          */
344         public NoteSymbol rootNoteSymbol() { return rootNoteSymbol; }
345         /**
346          * ベース音を返します。分数コードの場合はルート音と異なります。
347          * @return ベース音
348          */
349         public NoteSymbol bassNoteSymbol() { return bassNoteSymbol; }
350         /**
351          * このコードの構成音を、ルート音からの音程の配列として返します。
352          * ルート音自身やベース音は含まれません。
353          * @return 音程の配列
354          */
355         public Interval[] intervals() {
356                 return offsets.values().toArray(new Interval[offsets.size()]);
357         }
358         /**
359          * 指定した音程が設定されているか調べます。
360          * @param itv 音程
361          * @return 指定した音程が設定されていたらtrue
362          */
363         public boolean isSet(Interval itv) {
364                 return itv.equals(offsets.get(itv.getChromaticOffsetIndex()));
365         }
366         /**
367          * コードの同一性を判定します。ルート音、ベース音の異名同音は異なるものとみなされます。
368          * @param anObject 比較対象
369          * @return 等しければtrue
370          * @see #equalsEnharmonically(Chord)
371          */
372         @Override
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);
380                 }
381                 return false;
382         }
383         @Override
384         public int hashCode() { return toString().hashCode(); }
385         /**
386          * ルート音、ベース音の異名同音を同じとみなしたうえで、コードの同一性を判定します。
387          * @param another 比較対象のコード
388          * @return 等しければtrue
389          * @see #equals(Object)
390          */
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);
397         }
398         /**
399          * コード構成音の数を返します。ルート音は含まれますが、ベース音は含まれません。
400          * @return コード構成音の数
401          */
402         public int numberOfNotes() { return offsets.size() + 1; }
403         /**
404          * 指定された位置にある構成音のノート番号を返します。
405          * @param index 位置(0をルート音とした構成音の順序)
406          * @return ノート番号(該当する音がない場合は -1)
407          */
408         public int noteAt(int index) {
409                 int rootnote = rootNoteSymbol.toNoteNumber();
410                 if( index == 0 ) return rootnote;
411                 Interval itv;
412                 int i=0;
413                 for( OffsetIndex offsetIndex : OffsetIndex.values() )
414                         if( (itv = offsets.get(offsetIndex)) != null && ++i == index )
415                                 return rootnote + itv.getChromaticOffset();
416                 return -1;
417         }
418         /**
419          * コード構成音を格納したノート番号の配列を返します(ベース音は含まれません)。
420          * 音域が指定された場合、その音域の範囲内に収まるように転回されます。
421          * @param range 音域(null可)
422          * @param key キー(null可)
423          * @return ノート番号の配列
424          */
425         public int[] toNoteArray(Range range, Key key) {
426                 int rootnote = rootNoteSymbol.toNoteNumber();
427                 int ia[] = new int[numberOfNotes()];
428                 int i;
429                 ia[i=0] = rootnote;
430                 Interval itv;
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);
435                 return ia;
436         }
437         /**
438          * MIDIノート番号が、コードの構成音の何番目(0=ルート音)にあるかを表すインデックス値を返します。
439          * 構成音に該当しない場合は -1 を返します。ベース音は検索されません。
440          * @param noteNumber MIDIノート番号
441          * @return 構成音のインデックス値
442          */
443         public int indexOf(int noteNumber) {
444                 int relativeNote = noteNumber - rootNoteSymbol.toNoteNumber();
445                 if( Music.mod12(relativeNote) == 0 ) return 0;
446                 Interval itv;
447                 int i=0;
448                 for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
449                         if( (itv = offsets.get(offsetIndex)) != null ) {
450                                 i++;
451                                 if( Music.mod12(relativeNote - itv.getChromaticOffset()) == 0 )
452                                         return i;
453                         }
454                 }
455                 return -1;
456         }
457         /**
458          * 指定したキーのスケールを外れた構成音がないか調べます。
459          * @param key 調べるキー
460          * @return スケールを外れている構成音がなければtrue
461          */
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;
466                 Interval itv;
467                 for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
468                         if( (itv = offsets.get(offsetIndex)) == null ) continue;
469                         if( ! Music.isOnScale(rootnote + itv.getChromaticOffset(), keyCo5) ) return false;
470                 }
471                 return true;
472         }
473         /**
474          * C/Amの調に近いほうの♯、♭の表記で、移調したコードを返します。
475          * @param chromaticOffset 移調幅(半音単位)
476          * @return 移調した新しいコード(移調幅が0の場合は自分自身)
477          */
478         public Chord transposedChord(int chromaticOffset) {
479                 return transposedChord(chromaticOffset, 0);
480         }
481         /**
482          * 指定された調に近いほうの♯、♭の表記で、移調したコードを返します。
483          * @param chromaticOffset 移調幅(半音単位)
484          * @param originalKey 基準とする調
485          * @return 移調した新しいコード(移調幅が0の場合は自分自身)
486          */
487         public Chord transposedChord(int chromaticOffset, Key originalKey) {
488                 return transposedChord(chromaticOffset, originalKey.toCo5());
489         }
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;
495                 //
496                 int newRootCo5 = rootNoteSymbol.toCo5() + offsetCo5;
497                 int newBassCo5 = bassNoteSymbol.toCo5() + offsetCo5;
498                 if( keyCo5 > 6 ) {
499                         newRootCo5 -= 12;
500                         newBassCo5 -= 12;
501                 }
502                 else if( keyCo5 < -5 ) {
503                         newRootCo5 += 12;
504                         newBassCo5 += 12;
505                 }
506                 return new Chord(new NoteSymbol(newRootCo5), new NoteSymbol(newBassCo5), intervals());
507         }
508
509         /**
510          * この和音の文字列表現としてコード名を返します。
511          * @return この和音のコード名
512          */
513         @Override
514         public String toString() {
515                 String chordSymbol = rootNoteSymbol + symbolSuffix();
516                 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) chordSymbol += "/" + bassNoteSymbol;
517                 return chordSymbol;
518         }
519         /**
520          * コード名を HTML で返します。
521          *
522          * Swing の {@link JLabel#setText(String)} は HTML で指定できるので、
523          * 文字の大きさに変化をつけることができます。
524          *
525          * @param colorName 色のHTML表現(色名または #RRGGBB 形式)
526          * @return コード名のHTML
527          */
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;
544                 }
545                 String suffix = symbolSuffix().
546                         replace("-5","<sup>-5</sup>").
547                         replace("+5","<sup>+5</sup>");
548                 return
549                         "<html>" +
550                         "<span style=\"color: " + colorName + "; font-size: 170% ; white-space: nowrap ;\">" +
551                         formattedRoot + suffix + endSpan + formattedBass +
552                         "</span>" +
553                         "</html>" ;
554         }
555         /**
556          * コードの説明(英語)を返します。
557          * @return コードの説明(英語)
558          */
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);
563                 }
564                 return name;
565         }
566
567         /**
568          * このコードのクローンを作成します。
569          */
570         @Override
571         public Chord clone() {
572                 Chord newChord = new Chord(rootNoteSymbol, bassNoteSymbol);
573                 newChord.offsets = new HashMap<>(offsets);
574                 return newChord;
575         }
576         /**
577          * 指定した音程の構成音を設定します。
578          * @param interval 設定する音程
579          */
580         public void set(Interval interval) {
581                 offsets.put(interval.getChromaticOffsetIndex(), interval);
582         }
583         /**
584          * 指定した複数の音程の構成音を設定します。
585          * @param intervals 設定する音程
586          */
587         protected void set(Collection<Interval> intervals) {
588                 for(Interval itv : intervals) if(itv != null) set(itv);
589         }
590         /**
591          * 指定した音程の構成音をクリアします。
592          * @param itv クリアする音程
593          */
594         public void clear(Interval itv) {
595                 offsets.remove(itv.getChromaticOffsetIndex());
596         }
597 }