OSDN Git Service

New MIDI Sequence画面のTrack切替をコンボボックスから横長リストに変更して使いやすくした
[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.Collections;
7 import java.util.HashMap;
8 import java.util.List;
9 import java.util.Map;
10 import java.util.Objects;
11 import java.util.StringJoiner;
12
13 import javax.swing.JLabel;
14
15 /**
16  * 和音(コード - musical chord)のクラス(値は不変)
17  */
18 public class Chord {
19         /** コード構成音の順序に対応する色 */
20         public static final Color NOTE_INDEX_COLORS[] = {
21                 Color.red,
22                 new Color(0x40,0x40,0xFF),
23                 Color.orange.darker(),
24                 new Color(0x20,0x99,0x00),
25                 Color.magenta,
26                 Color.orange,
27                 Color.green
28         };
29         /** 音程差のグループ分け */
30         private static enum IntervalGroup {
31                 THIRD, FIFTH, SEVENTH, NINTH, ELEVENTH, THIRTEENTH
32         }
33         /** 音程差 */
34         public static enum Interval {
35
36                 /** 長2度(major 2nd / sus2) */
37                 SUS2(2, IntervalGroup.THIRD, "sus2", "suspended 2nd"),
38                 /** 短3度または増2度 */
39                 MINOR(3, IntervalGroup.THIRD, "m", "minor"),
40                 /** 長3度 */
41                 MAJOR(4, IntervalGroup.THIRD, "", "major"),
42                 /** 完全4度(parfect 4th / sus4) */
43                 SUS4(5, IntervalGroup.THIRD, "sus4", "suspended 4th"),
44
45                 /** 減5度または増4度(トライトーン = 三全音 = 半オクターブ) */
46                 FLAT5(6, IntervalGroup.FIFTH, "-5", "flatted 5th"),
47                 /** 完全5度 */
48                 PARFECT5(7, IntervalGroup.FIFTH, "", "parfect 5th"),
49                 /** 増5度または短6度 */
50                 SHARP5(8, IntervalGroup.FIFTH, "+5", "sharped 5th"),
51
52                 /** 長6度または減7度 */
53                 SIXTH(9, IntervalGroup.SEVENTH, "6", "6th"),
54                 /** 短7度 */
55                 SEVENTH(10, IntervalGroup.SEVENTH, "7", "7th"),
56                 /** 長7度 */
57                 MAJOR_SEVENTH(11, IntervalGroup.SEVENTH, "M7", "major 7th"),
58
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"),
65
66                 /** 完全11度(完全4度の1オクターブ上) */
67                 ELEVENTH(17, IntervalGroup.ELEVENTH, "11", "11th"),
68                 /** 増11度(増4度の1オクターブ上) */
69                 SHARP11(18, IntervalGroup.ELEVENTH, "+11", "sharped 11th"),
70
71                 /** 短13度(短6度の1オクターブ上) */
72                 FLAT13(20, IntervalGroup.THIRTEENTH, "-13", "flatted 13th"),
73                 /** 長13度(長6度の1オクターブ上) */
74                 THIRTEENTH(21, IntervalGroup.THIRTEENTH, "13", "13th");
75
76                 private Interval(int chromaticOffset, IntervalGroup intervalGroup, String symbol, String description) {
77                         this.chromaticOffset = chromaticOffset;
78                         this.intervalGroup = intervalGroup;
79                         this.symbol = symbol;
80                         this.description = description;
81                 }
82                 /**
83                  * 半音差を返します。これはルート音からの音程差を半音単位で表した値です。
84                  * @return 半音差
85                  */
86                 public int getChromaticOffset() { return chromaticOffset; }
87                 private int chromaticOffset;
88                 /**
89                  * この音程が属しているグループを返します。
90                  * @return この音程が属しているグループ
91                  */
92                 public IntervalGroup getIntervalGroup() { return intervalGroup; }
93                 private IntervalGroup intervalGroup;
94                 /**
95                  * コード名に使う略称を返します。
96                  * @return 略称
97                  */
98                 public String getSymbol() { return symbol; }
99                 private String symbol;
100                 /**
101                  * コード用の説明を返します。
102                  * @return 説明
103                  */
104                 public String getDescription() { return description; }
105                 private String description;
106         }
107         /**
108          * コードネームの音名を除いた部分(サフィックス)を組み立てて返します。
109          * @return コードネームの音名を除いた部分
110          */
111         public String symbolSuffix() {
112                 String suffix = "";
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());
124                 }
125                 String alias = symbolSuffixAliases.get(suffix += inParen);
126                 return alias == null ? suffix : alias;
127         }
128         /**
129          * コードの説明のうち、音名を除いた部分を組み立てて返します。
130          * @return コード説明の音名を除いた部分
131          */
132         public String nameSuffix() {
133                 String suffix = "";
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());
145                 }
146                 String alias = nameSuffixAliases.get(suffix += inParen);
147                 return alias == null ? suffix : alias;
148         }
149         private static final List<Interval> SUSPENDED = Arrays.asList(
150                         Interval.SUS2,
151                         Interval.SUS4);
152         private static final List<IntervalGroup> EXTENDED = Arrays.asList(
153                         IntervalGroup.NINTH,
154                         IntervalGroup.ELEVENTH,
155                         IntervalGroup.THIRTEENTH);
156         private static final Map<String, String> symbolSuffixAliases = new HashMap<String, String>() {
157                 {
158                         put("m-5", "dim");
159                         put("+5", "aug");
160                         put("m6-5", "dim7");
161                         put("(9)", "add9");
162                         put("7(9)", "9");
163                         put("M7(9)", "M9");
164                         put("7+5", "aug7");
165                         put("m6-5(9)", "dim9");
166                 }
167         };
168         private static final Map<String, String> nameSuffixAliases = new HashMap<String, String>() {
169                 {
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");
179                 }
180         };
181         private void setSymbolSuffix(String suffix) {
182                 // ()の中と外に分ける
183                 String outInParen[] = suffix.split("[\\(\\)]");
184                 if( outInParen.length == 0 ) return;
185                 String outParen = suffix;
186                 String inParen = "";
187                 if( outInParen.length > 1 ) {
188                         outParen = outInParen[0];
189                         inParen = outInParen[1];
190                 }
191                 // +5 -5 aug dim
192                 if( outParen.matches(".*(\\+5|aug|#5).*") ) set(Interval.SHARP5);
193                 else if( outParen.matches(".*(-5|dim|b5).*") ) set(Interval.FLAT5);
194                 //
195                 // 6 7 M7
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);
199                 //
200                 // minor sus4  (m と maj7 を間違えないよう比較しつつ、mmaj7 も認識させる)
201                 if( outParen.matches(".*m.*") && ! outParen.matches(".*ma.*") || outParen.matches(".*mma.*") ) {
202                         set(Interval.MINOR);
203                 }
204                 else if( outParen.matches(".*sus4.*") ) set(Interval.SUS4);
205                 //
206                 // extended
207                 if( outParen.matches(".*9.*") ) {
208                         set(Interval.NINTH);
209                         if( ! outParen.matches(".*(add9|6|M9|maj9|dim9).*") ) {
210                                 set(Interval.SEVENTH);
211                         }
212                 }
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);
217
218                         if( p.matches("(\\+11|#11)") ) set(Interval.SHARP11);
219                         else if( p.matches("11") ) set(Interval.ELEVENTH);
220
221                         if( p.matches("(-13|b13)") ) set(Interval.FLAT13);
222                         else if( p.matches("13") ) set(Interval.THIRTEENTH);
223
224                         // -5 や +5 が () の中にあっても解釈できるようにする
225                         if( p.matches("(-5|b5)") ) set(Interval.FLAT5);
226                         else if( p.matches("(\\+5|#5)") ) set(Interval.SHARP5);
227                 }
228         }
229         /**
230          * ルート音を返します。
231          * @return ルート音
232          */
233         public Note rootNoteSymbol() { return rootNoteSymbol; }
234         private Note rootNoteSymbol;
235         /**
236          * ベース音を返します。オンコードの場合はルート音と異なります。
237          * @return ベース音
238          */
239         public Note bassNoteSymbol() { return bassNoteSymbol; }
240         private Note bassNoteSymbol;
241         /**
242          * 指定した音程が設定されているか調べます。
243          * @param interval 音程
244          * @return 指定した音程が設定されていたらtrue
245          */
246         public boolean isSet(Interval interval) {
247                 return interval.equals(intervalMap.get(interval.getIntervalGroup()));
248         }
249         /**
250          * このコードの構成音(ルート音自身やベース音は含まない)を、
251          * ルート音からの音程のコレクション(変更不可能なビュー)として返します。
252          * @return 音程のコレクション
253          */
254         public Collection<Interval> intervals() { return intervals; }
255         private Collection<Interval> intervals;
256         private void fixIntervals() {
257                 intervals = Collections.unmodifiableCollection(intervalMap.values());
258         }
259         /** 現在有効な構成音(ルート、ベースは除く)の音程 */
260         private Map<IntervalGroup, Interval> intervalMap;
261         private void set(Interval interval) {
262                 intervalMap.put(interval.getIntervalGroup(), interval);
263         }
264         private void set(Collection<Interval> intervals) {
265                 for(Interval interval : intervals) if(interval != null) set(interval);
266         }
267
268         /**
269          * 指定されたルート音、構成音を持つコードを構築します。
270          * @param root ルート音
271          * @param intervals その他の構成音の音程
272          * (メジャーコードの構成音である長三度・完全五度は、指定しなくてもデフォルトで設定されます)
273          * @throws NullPointerException ルート音にnullが指定された場合
274          */
275         public Chord(Note root, Interval... intervals) {
276                 this(root, root, intervals);
277         }
278         /**
279          * 指定されたルート音、ベース音、構成音を持つコードを構築します。
280          * @param root ルート音
281          * @param bass ベース音(nullの場合はルート音が指定されたとみなされます)
282          * @param intervals その他の構成音の音程
283          * (メジャーコードの構成音である長三度・完全五度は、指定しなくてもデフォルトで設定されます)
284          * @throws NullPointerException ルート音にnullが指定された場合
285          */
286         public Chord(Note root, Note bass, Interval... intervals) {
287                 this(root, bass, Arrays.asList(intervals));
288         }
289         /**
290          * 指定されたルート音、ベース音、構成音を持つコードを構築します。
291          * @param root ルート音
292          * @param bass ベース音(nullの場合はルート音が指定されたとみなされます)
293          * @param intervals その他の構成音の音程のコレクション
294          * (メジャーコードの構成音である長三度・完全五度は、指定しなくてもデフォルトで設定されます)
295          * @throws NullPointerException ルート音、または構成音コレクションにnullが指定された場合
296          */
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<>();
301                 set(Interval.MAJOR);
302                 set(Interval.PARFECT5);
303                 set(intervals);
304                 fixIntervals();
305         }
306         /**
307          * 元のコードのルート音、ベース音以外の構成音の一部を変更した新しいコードを構築します。
308          * @param chord 元のコード
309          * @param intervals 変更したい構成音の音程(ルート音、ベース音を除く)
310          * @throws NullPointerException 元のコードにnullが指定された場合
311          */
312         public Chord(Chord original, Interval... intervals) {
313                 this(original, Arrays.asList(intervals));
314         }
315         /**
316          * 元のコードのルート音、ベース音以外の構成音の一部を変更した新しいコードを構築します。
317          * @param chord 元のコード
318          * @param intervals 変更したい構成音の音程(ルート音、ベース音を除く)のコレクション
319          * @throws NullPointerException 引数にnullが指定された場合
320          */
321         public Chord(Chord original, Collection<Interval> intervals) {
322                 rootNoteSymbol = original.rootNoteSymbol;
323                 bassNoteSymbol = original.bassNoteSymbol;
324                 intervalMap = new HashMap<>(original.intervalMap);
325                 set(intervals);
326                 fixIntervals();
327         }
328         /**
329          * 指定された調と同名のコードを構築します。
330          * @param key 調
331          * @throws NullPointerException 調にnullが指定された場合
332          */
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);
339                 fixIntervals();
340         }
341         /**
342          * コード名からコードを構築します。
343          * @param chordSymbol コード名
344          * @throws NullPointerException コード名にnullが指定された場合
345          * @throws IllegalArgumentException コード名が空文字列の場合、または音名で始まっていない場合
346          */
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);
354                 }
355                 intervalMap = new HashMap<>();
356                 set(Interval.MAJOR);
357                 set(Interval.PARFECT5);
358                 setSymbolSuffix(root.replaceFirst("^[A-G][#bx]*",""));
359                 fixIntervals();
360         }
361         /**
362          * コードの同一性を判定します。ルート音、ベース音の異名同音は異なるものとみなされます。
363          * @param anObject 比較対象
364          * @return 等しければtrue
365          * @see #equalsEnharmonically(Chord)
366          */
367         @Override
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);
375                 }
376                 return false;
377         }
378         @Override
379         public int hashCode() { return toString().hashCode(); }
380         /**
381          * ルート音、ベース音の異名同音を同じとみなしたうえで、コードの同一性を判定します。
382          * @param another 比較対象のコード
383          * @return 等しければtrue
384          * @see #equals(Object)
385          */
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);
392                 }
393                 return false;
394         }
395         /**
396          * コード構成音の数を返します。ルート音は含まれますが、ベース音は含まれません。
397          * @return コード構成音の数
398          */
399         public int numberOfNotes() { return intervalMap.size() + 1; }
400         /**
401          * 指定された位置にある構成音のノート番号を返します。
402          * @param index 位置(0をルート音とした構成音の順序)
403          * @return ノート番号(該当する音がない場合は -1)
404          */
405         public int noteAt(int index) {
406                 int root = rootNoteSymbol.toNoteNumber();
407                 if( index == 0 ) return root;
408                 int current = 0;
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();
413                 }
414                 return -1;
415         }
416         /**
417          * コード構成音を格納したノート番号の配列を返します(ベース音は含まれません)。
418          * 音域が指定された場合、その音域の範囲内に収まるように転回されます。
419          * @param range 音域(null可)
420          * @param key キー(null可)
421          * @return ノート番号の配列
422          */
423         public int[] toNoteArray(Range range, Key key) {
424                 int rootnote = rootNoteSymbol.toNoteNumber();
425                 int ia[] = new int[numberOfNotes()];
426                 int i;
427                 ia[i=0] = rootnote;
428                 Interval itv;
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);
433                 return ia;
434         }
435         /**
436          * MIDIノート番号が、コードの構成音の何番目(0=ルート音)にあるかを表すインデックス値を返します。
437          * 構成音に該当しない場合は -1 を返します。ベース音は検索されません。
438          * @param noteNumber MIDIノート番号
439          * @return 構成音のインデックス値
440          */
441         public int indexOf(int noteNumber) {
442                 int relativeNote = noteNumber - rootNoteSymbol.toNoteNumber();
443                 if( Note.mod12(relativeNote) == 0 ) return 0;
444                 Interval itv;
445                 int i=0;
446                 for( IntervalGroup offsetIndex : IntervalGroup.values() ) {
447                         if( (itv = intervalMap.get(offsetIndex)) != null ) {
448                                 i++;
449                                 if( Note.mod12(relativeNote - itv.getChromaticOffset()) == 0 )
450                                         return i;
451                         }
452                 }
453                 return -1;
454         }
455         /**
456          * 指定したキーのスケールを外れた構成音がないか調べます。
457          * @param key 調べるキー
458          * @return スケールを外れている構成音がなければtrue
459          * @throws NullPointerException キーにnullが指定された場合
460          */
461         public boolean isOnScaleIn(Key key) {
462                 int root = rootNoteSymbol.toNoteNumber();
463                 if( ! key.isOnScale(root) ) return false;
464                 Interval itv;
465                 for( IntervalGroup offsetIndex : IntervalGroup.values() ) {
466                         if( (itv = intervalMap.get(offsetIndex)) == null ) continue;
467                         if( ! key.isOnScale(root + itv.getChromaticOffset()) ) return false;
468                 }
469                 return true;
470         }
471         /**
472          * 指定された調に近いほうの♯、♭の表記で、移調したコードを返します。
473          * @param chromaticOffset 移調幅(半音単位)
474          * @param originalKey 基準とする調
475          * @return 移調した新しいコード(移調幅が0の場合は自分自身)
476          */
477         public Chord transposedNewChord(int chromaticOffset, Key originalKey) {
478                 return transposedNewChord(chromaticOffset, originalKey.toCo5());
479         }
480         /**
481          * C/Amの調に近いほうの♯、♭の表記で、移調したコードを返します。
482          * @param chromaticOffset 移調幅(半音単位)
483          * @return 移調した新しいコード(移調幅が0の場合は自分自身)
484          */
485         public Chord transposedNewChord(int chromaticOffset) {
486                 return transposedNewChord(chromaticOffset, 0);
487         }
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;
495                 if( keyCo5 > 6 ) {
496                         newRootCo5 -= 12;
497                         newBassCo5 -= 12;
498                 }
499                 else if( keyCo5 < -5 ) {
500                         newRootCo5 += 12;
501                         newBassCo5 += 12;
502                 }
503                 Note root = new Note(newRootCo5);
504                 Note bass = (newBassCo5 == newRootCo5 ? root : new Note(newBassCo5));
505                 return new Chord(root, bass, intervals);
506         }
507
508         /** この和音の文字列表現としてコード名(例: C、Am、G7)を返します。 */
509         @Override
510         public String toString() {
511                 String chordSymbol = rootNoteSymbol + symbolSuffix();
512                 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) chordSymbol += "/" + bassNoteSymbol;
513                 return chordSymbol;
514         }
515         /**
516          * コード名を HTML で返します。
517          *
518          * Swing の {@link JLabel#setText(String)} は HTML で指定できるので、
519          * 文字の大きさに変化をつけることができます。
520          *
521          * @param colorName 色のHTML表現(色名または #RRGGBB 形式)
522          * @return コード名のHTML
523          */
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;
540                 }
541                 String suffix = symbolSuffix().
542                         replace("-5","<sup>-5</sup>").
543                         replace("+5","<sup>+5</sup>");
544                 return
545                         "<html>" +
546                         "<span style=\"color: " + colorName + "; font-size: 170% ; white-space: nowrap ;\">" +
547                         formattedRoot + suffix + endSpan + formattedBass +
548                         "</span>" +
549                         "</html>" ;
550         }
551         /**
552          * コードの説明(英語)を返します。
553          * @return コードの説明(英語)
554          */
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);
559                 }
560                 return name;
561         }
562 }