OSDN Git Service

リファクタリング
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / music / Chord.java
1 package camidion.chordhelper.music;
2
3 import java.awt.Color;
4 import java.util.HashMap;
5 import java.util.Map;
6 import java.util.Vector;
7
8 import javax.swing.JLabel;
9
10 /**
11  * 和音(コード - musical chord)のクラス
12  */
13 public class Chord implements Cloneable {
14         /**
15          * コード構成音の順序に対応する色
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          * 音程差の半音オフセットのインデックス
28          */
29         public static enum OffsetIndex {
30                 THIRD,
31                 FIFTH,
32                 SEVENTH,
33                 NINTH,
34                 ELEVENTH,
35                 THIRTEENTH
36         }
37         /**
38          * 音程差
39          */
40         public static enum Interval {
41
42                 /** 長2度(major 2nd / sus2) */
43                 SUS2(2, OffsetIndex.THIRD),
44                 /** 短3度または増2度 */
45                 MINOR(3, OffsetIndex.THIRD),
46                 /** 長3度 */
47                 MAJOR(4, OffsetIndex.THIRD),
48                 /** 完全4度(parfect 4th / sus4) */
49                 SUS4(5, OffsetIndex.THIRD),
50
51                 /** 減5度または増4度(トライトーン = 三全音 = 半オクターブ) */
52                 FLAT5(6, OffsetIndex.FIFTH),
53                 /** 完全5度 */
54                 PARFECT5(7, OffsetIndex.FIFTH),
55                 /** 増5度または短6度 */
56                 SHARP5(8, OffsetIndex.FIFTH),
57
58                 /** 長6度または減7度 */
59                 SIXTH(9, OffsetIndex.SEVENTH),
60                 /** 短7度 */
61                 SEVENTH(10, OffsetIndex.SEVENTH),
62                 /** 長7度 */
63                 MAJOR_SEVENTH(11, OffsetIndex.SEVENTH),
64
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),
71
72                 /** 完全11度(完全4度の1オクターブ上) */
73                 ELEVENTH(17, OffsetIndex.ELEVENTH),
74                 /** 増11度(増4度の1オクターブ上) */
75                 SHARP11(18, OffsetIndex.ELEVENTH),
76
77                 /** 短13度(短6度の1オクターブ上) */
78                 FLAT13(20, OffsetIndex.THIRTEENTH),
79                 /** 長13度(長6度の1オクターブ上) */
80                 THIRTEENTH(21, OffsetIndex.THIRTEENTH);
81
82                 private Interval(int chromaticOffset, OffsetIndex offsetIndex) {
83                         this.chromaticOffset = chromaticOffset;
84                         this.offsetIndex = offsetIndex;
85                 }
86                 private OffsetIndex offsetIndex;
87                 private int chromaticOffset;
88                 /**
89                  * 半音差を返します。
90                  * @return 半音差
91                  */
92                 public int getChromaticOffset() { return chromaticOffset; }
93                 /**
94                  * 対応するインデックスを返します。
95                  * @return 対応するインデックス
96                  */
97                 public OffsetIndex getChromaticOffsetIndex() {
98                         return offsetIndex;
99                 }
100         }
101         /**
102          * デフォルトの半音値(メジャーコード固定)
103          */
104         public static Map<OffsetIndex, Interval>
105                 DEFAULT_OFFSETS = new HashMap<OffsetIndex, Interval>() {
106                         {
107                                 Interval itv;
108                                 itv = Interval.MAJOR; put(itv.getChromaticOffsetIndex(), itv);
109                                 itv = Interval.PARFECT5; put(itv.getChromaticOffsetIndex(), itv);
110                         }
111                 };
112         /**
113          * 現在有効な構成音の音程(ルート音を除く)
114          */
115         public Map<OffsetIndex, Interval> offsets = new HashMap<>(DEFAULT_OFFSETS);
116         /**
117          * このコードのルート音
118          */
119         private NoteSymbol rootNoteSymbol;
120         /**
121          * このコードのベース音(ルート音と異なる場合は分数コードの分母)
122          */
123         private NoteSymbol bassNoteSymbol;
124
125         /**
126          * コード C major を構築します。
127          */
128         public Chord() {
129                 this(new NoteSymbol());
130         }
131         /**
132          * 指定した音名のメジャーコードを構築します。
133          * @param noteSymbol 音名
134          */
135         public Chord(NoteSymbol noteSymbol) {
136                 setRoot(noteSymbol);
137                 setBass(noteSymbol);
138         }
139         /**
140          * 指定された調と同名のコードを構築します。
141          * <p>元の調がマイナーキーの場合はマイナーコード、
142          * それ以外の場合はメジャーコードになります。
143          * </p>
144          * @param key 調
145          */
146         public Chord(Key key) {
147                 int keyCo5 = key.toCo5();
148                 if( key.majorMinor() == Key.MajorMinor.MINOR ) {
149                         keyCo5 += 3;
150                         set(Interval.MINOR);
151                 }
152                 setRoot(new NoteSymbol(keyCo5));
153                 setBass(new NoteSymbol(keyCo5));
154         }
155         /**
156          * コード名の文字列からコードを構築します。
157          * @param chordSymbol コード名の文字列
158          */
159         public Chord(String chordSymbol) {
160                 setChordSymbol(chordSymbol);
161         }
162         /**
163          * このコードのクローンを作成します。
164          */
165         @Override
166         public Chord clone() {
167                 Chord newChord = new Chord(rootNoteSymbol);
168                 newChord.offsets = new HashMap<>(offsets);
169                 newChord.setBass(bassNoteSymbol);
170                 return newChord;
171         }
172         /**
173          * コードのルート音を指定された音階に置換します。
174          * @param rootNoteSymbol 音階
175          * @return このコード自身(置換後)
176          */
177         public Chord setRoot(NoteSymbol rootNoteSymbol) {
178                 this.rootNoteSymbol = rootNoteSymbol;
179                 return this;
180         }
181         /**
182          * コードのベース音を指定された音階に置換します。
183          * @param rootNoteSymbol 音階
184          * @return このコード自身(置換後)
185          */
186         public Chord setBass(NoteSymbol rootNoteSymbol) {
187                 this.bassNoteSymbol = rootNoteSymbol;
188                 return this;
189         }
190         /**
191          * コードの種類を設定します。
192          * @param itv 設定する音程
193          */
194         public void set(Interval itv) {
195                 offsets.put(itv.getChromaticOffsetIndex(), itv);
196         }
197         /**
198          * コードに設定した音程をクリアします。
199          * @param index 半音差インデックス
200          */
201         public void clear(OffsetIndex index) {
202                 offsets.remove(index);
203         }
204         //
205         // コードネームの文字列が示すコードに置き換えます。
206         public Chord setChordSymbol(String chordSymbol) {
207                 //
208                 // 分数コードの分子と分母に分ける
209                 String parts[] = chordSymbol.trim().split("(/|on)");
210                 if( parts.length == 0 ) {
211                         return this;
212                 }
213                 // ルート音とベース音を設定
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]*","");
217                 //
218                 // () があれば、その中身を取り出す
219                 String suffixParts[] = suffix.split("[\\(\\)]");
220                 if( suffixParts.length == 0 ) {
221                         return this;
222                 }
223                 String suffixParen = "";
224                 if( suffixParts.length > 1 ) {
225                         suffixParen = suffixParts[1];
226                         suffix = suffixParts[0];
227                 }
228                 Interval itv;
229                 //
230                 // +5 -5 aug dim の判定
231                 set(
232                         suffix.matches(".*(\\+5|aug|#5).*") ? Interval.SHARP5 :
233                         suffix.matches(".*(-5|dim|b5).*") ? Interval.FLAT5 :
234                         Interval.PARFECT5
235                 );
236                 //
237                 // 6 7 M7 の判定
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 :
241                         null;
242                 if(itv==null)
243                         clear(OffsetIndex.SEVENTH);
244                 else
245                         set(itv);
246                 //
247                 // マイナーの判定。maj7 と間違えないように比較
248                 set(
249                         (suffix.matches(".*m.*") && ! suffix.matches(".*ma.*") ) ? Interval.MINOR :
250                         suffix.matches(".*sus4.*") ? Interval.SUS4 :
251                         Interval.MAJOR
252                 );
253                 //
254                 // 9th の判定
255                 if( suffix.matches(".*9.*") ) {
256                         set(Interval.NINTH);
257                         if( ! suffix.matches( ".*(add9|6|M9|maj9|dim9).*") ) {
258                                 set(Interval.SEVENTH);
259                         }
260                 }
261                 else {
262                         offsets.remove(OffsetIndex.NINTH);
263                         offsets.remove(OffsetIndex.ELEVENTH);
264                         offsets.remove(OffsetIndex.THIRTEENTH);
265                         //
266                         // () の中を , で分ける
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);
275
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);
280
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);
285
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);
291                         }
292                 }
293                 return this;
294         }
295         /**
296          * ルート音を返します。
297          * @return ルート音
298          */
299         public NoteSymbol rootNoteSymbol() { return rootNoteSymbol; }
300         /**
301          * ベース音を返します。分数コードの場合はルート音と異なります。
302          * @return ベース音
303          */
304         public NoteSymbol bassNoteSymbol() { return bassNoteSymbol; }
305         /**
306          * 指定した音程が設定されているか調べます。
307          * @param itv 音程
308          * @return 指定した音程が設定されていたらtrue
309          */
310         public boolean isSet(Interval itv) {
311                 return offsets.get(itv.getChromaticOffsetIndex()) == itv;
312         }
313         /**
314          * 指定したインデックスに音程が設定されているか調べます。
315          * @param index インデックス
316          * @return 指定したインデックスに音程が設定されていたらtrue
317          */
318         public boolean isSet(OffsetIndex index) {
319                 return offsets.containsKey(index);
320         }
321         /**
322          * コードが等しいかどうかを判定します。
323          * @return 等しければtrue
324          */
325         @Override
326         public boolean equals(Object anObject) {
327                 if( this == anObject )
328                         return true;
329                 if( anObject instanceof Chord ) {
330                         Chord another = (Chord) anObject;
331                         if( ! rootNoteSymbol.equals(another.rootNoteSymbol) )
332                                 return false;
333                         if( ! bassNoteSymbol.equals(another.bassNoteSymbol) )
334                                 return false;
335                         return offsets.equals(another.offsets);
336                 }
337                 return false;
338         }
339         @Override
340         public int hashCode() {
341                 return toString().hashCode();
342         }
343         /**
344          * コードが等しいかどうかを、異名同音を無視して判定します。
345          * @param another 比較対象のコード
346          * @return 等しければtrue
347          */
348         public boolean equalsEnharmonically(Chord another) {
349                 if( this == another )
350                         return true;
351                 if( another == null )
352                         return false;
353                 if( ! rootNoteSymbol.equalsEnharmonically(another.rootNoteSymbol) )
354                         return false;
355                 if( ! bassNoteSymbol.equalsEnharmonically(another.bassNoteSymbol) )
356                         return false;
357                 return offsets.equals(another.offsets);
358         }
359         /**
360          * コード構成音の数を返します
361          * (ルート音は含まれますが、ベース音は含まれません)。
362          *
363          * @return コード構成音の数
364          */
365         public int numberOfNotes() { return offsets.size() + 1; }
366         /**
367          * 指定された位置にあるノート番号を返します。
368          * @param index 位置(0をルート音とした構成音の順序)
369          * @return ノート番号(該当する音がない場合は -1)
370          */
371         public int noteAt(int index) {
372                 int rootnote = rootNoteSymbol.toNoteNumber();
373                 if( index == 0 )
374                         return rootnote;
375                 Interval itv;
376                 int i=0;
377                 for( OffsetIndex offsetIndex : OffsetIndex.values() )
378                         if( (itv = offsets.get(offsetIndex)) != null && ++i == index )
379                                 return rootnote + itv.getChromaticOffset();
380                 return -1;
381         }
382         /**
383          * コード構成音を格納したノート番号の配列を返します。
384          * (ベース音は含まれません)
385          * 音域が指定された場合、その音域に合わせたノート番号を返します。
386          * @param range 音域(null可)
387          * @param key キー(null可)
388          * @return ノート番号の配列
389          */
390         public int[] toNoteArray(Range range, Key key) {
391                 int rootnote = rootNoteSymbol.toNoteNumber();
392                 int ia[] = new int[numberOfNotes()];
393                 int i;
394                 ia[i=0] = rootnote;
395                 Interval itv;
396                 for( OffsetIndex offsetIndex : OffsetIndex.values() )
397                         if( (itv = offsets.get(offsetIndex)) != null )
398                                 ia[++i] = rootnote + itv.getChromaticOffset();
399                 if( range != null )
400                         range.invertNotesOf(ia, key);
401                 return ia;
402         }
403         /**
404          * MIDI ノート番号が、コードの構成音の何番目(0=ルート音)に
405          * あるかを表すインデックス値を返します。
406          * 構成音に該当しない場合は -1 を返します。
407          * ベース音は検索されません。
408          * @param noteNumber MIDIノート番号
409          * @return 構成音のインデックス値
410          */
411         public int indexOf(int noteNumber) {
412                 int relativeNote = noteNumber - rootNoteSymbol.toNoteNumber();
413                 if( Music.mod12(relativeNote) == 0 ) return 0;
414                 Interval itv;
415                 int i=0;
416                 for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
417                         if( (itv = offsets.get(offsetIndex)) != null ) {
418                                 i++;
419                                 if( Music.mod12(relativeNote - itv.getChromaticOffset()) == 0 )
420                                         return i;
421                         }
422                 }
423                 return -1;
424         }
425         /**
426          * 指定したキーのスケールを外れた構成音がないか調べます。
427          * @param key 調べるキー
428          * @return スケールを外れている構成音がなければtrue
429          */
430         public boolean isOnScaleInKey(Key key) {
431                 return isOnScaleInKey(key.toCo5());
432         }
433         private boolean isOnScaleInKey(int keyCo5) {
434                 int rootnote = rootNoteSymbol.toNoteNumber();
435                 if( ! Music.isOnScale(rootnote, keyCo5) )
436                         return false;
437                 Interval itv;
438                 for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
439                         if( (itv = offsets.get(offsetIndex)) == null )
440                                 continue;
441                         if( ! Music.isOnScale(rootnote + itv.getChromaticOffset(), keyCo5) )
442                                 return false;
443                 }
444                 return true;
445         }
446         /**
447          * コードを移調します。
448          * @param chromatic_offset 移調幅(半音単位)
449          * @return 移調した新しいコード(移調幅が0の場合は自分自身)
450          */
451         public Chord transpose(int chromatic_offset) {
452                 return transpose(chromatic_offset, 0);
453         }
454         public Chord transpose(int chromatic_offset, Key original_key) {
455                 return transpose(chromatic_offset, original_key.toCo5());
456         }
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;
462                 //
463                 int newRootCo5 = rootNoteSymbol.toCo5() + offsetCo5;
464                 int newBassCo5 = bassNoteSymbol.toCo5() + offsetCo5;
465                 if( key_co5 > 6 ) {
466                         newRootCo5 -= 12;
467                         newBassCo5 -= 12;
468                 }
469                 else if( key_co5 < -5 ) {
470                         newRootCo5 += 12;
471                         newBassCo5 += 12;
472                 }
473                 setRoot(new NoteSymbol(newRootCo5));
474                 return setBass(new NoteSymbol(newBassCo5));
475         }
476         /**
477          * この和音の文字列表現としてコード名を返します。
478          * @return この和音のコード名
479          */
480         @Override
481         public String toString() {
482                 String chordSymbol = rootNoteSymbol + symbolSuffix();
483                 if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
484                         chordSymbol += "/" + bassNoteSymbol;
485                 }
486                 return chordSymbol;
487         }
488         /**
489          * コード名を HTML で返します。
490          *
491          * Swing の {@link JLabel#setText(String)} は HTML で指定できるので、
492          * 文字の大きさに変化をつけることができます。
493          *
494          * @param colorName 色のHTML表現(色名または #RRGGBB 形式)
495          * @return コード名のHTML
496          */
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;
513                 }
514                 String suffix = symbolSuffix().
515                         replace("-5","<sup>-5</sup>").
516                         replace("+5","<sup>+5</sup>");
517                 return
518                         "<html>" +
519                         "<span style=\"color: " + colorName + "; font-size: 170% ; white-space: nowrap ;\">" +
520                         formattedRoot + suffix + endSpan + formattedBass +
521                         "</span>" +
522                         "</html>" ;
523         }
524         /**
525          * コードの説明(英語)を返します。
526          * @return コードの説明(英語)
527          */
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);
532                 }
533                 return name;
534         }
535         /**
536          * コードネームの音名を除いた部分(サフィックス)を組み立てて返します。
537          * @return コードネームの音名を除いた部分
538          */
539         public String symbolSuffix() {
540                 String suffix = (
541                         offsets.get(OffsetIndex.THIRD) == Interval.MINOR ? "m" : ""
542                 );
543                 Interval itv;
544                 if( (itv = offsets.get(OffsetIndex.SEVENTH)) != null ) {
545                         switch(itv) {
546                         case SIXTH:         suffix += "6";  break;
547                         case SEVENTH:       suffix += "7";  break;
548                         case MAJOR_SEVENTH: suffix += "M7"; break;
549                         default: break;
550                         }
551                 }
552                 switch( offsets.get(OffsetIndex.THIRD) ) {
553                 case SUS4: suffix += "sus4"; break;
554                 case SUS2: suffix += "sus2"; break;
555                 default: break;
556                 }
557                 switch( offsets.get(OffsetIndex.FIFTH) ) {
558                 case FLAT5:  suffix += "-5"; break;
559                 case SHARP5: suffix += "+5"; break;
560                 default: break;
561                 }
562                 Vector<String> paren = new Vector<String>();
563                 if( (itv = offsets.get(OffsetIndex.NINTH)) != null ) {
564                         switch(itv) {
565                         case NINTH:  paren.add("9"); break;
566                         case FLAT9:  paren.add("-9"); break;
567                         case SHARP9: paren.add("+9"); break;
568                         default: break;
569                         }
570                 }
571                 if( (itv = offsets.get(OffsetIndex.ELEVENTH)) != null ) {
572                         switch(itv) {
573                         case ELEVENTH: paren.add("11"); break;
574                         case SHARP11:  paren.add("+11"); break;
575                         default: break;
576                         }
577                 }
578                 if( (itv = offsets.get(OffsetIndex.THIRTEENTH)) != null ) {
579                         switch(itv) {
580                         case THIRTEENTH: paren.add("13"); break;
581                         case FLAT13:     paren.add("-13"); break;
582                         default: break;
583                         }
584                 }
585                 if( ! paren.isEmpty() ) {
586                         boolean is_first = true;
587                         suffix += "(";
588                         for( String p : paren ) {
589                                 if( is_first )
590                                         is_first = false;
591                                 else
592                                         suffix += ",";
593                                 suffix += p;
594                         }
595                         suffix += ")";
596                 }
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";
605                 else return suffix ;
606         }
607         /**
608          * コードの説明のうち、音名を除いた部分を組み立てて返します。
609          * @return コード説明の音名を除いた部分
610          */
611         public String nameSuffix() {
612                 String suffix = "";
613                 if( offsets.get(OffsetIndex.THIRD) == Interval.MINOR )
614                         suffix += " minor";
615                 Interval itv;
616                 if( (itv = offsets.get(OffsetIndex.SEVENTH)) != null ) {
617                         switch(itv) {
618                         case SIXTH:         suffix += " 6th"; break;
619                         case SEVENTH:       suffix += " 7th"; break;
620                         case MAJOR_SEVENTH: suffix += " major 7th"; break;
621                         default: break;
622                         }
623                 }
624                 switch( offsets.get(OffsetIndex.THIRD) ) {
625                 case SUS4: suffix += " suspended 4th"; break;
626                 case SUS2: suffix += " suspended 2nd"; break;
627                 default: break;
628                 }
629                 switch( offsets.get(OffsetIndex.FIFTH) ) {
630                 case FLAT5 : suffix += " flatted 5th"; break;
631                 case SHARP5: suffix += " sharped 5th"; break;
632                 default: break;
633                 }
634                 Vector<String> paren = new Vector<String>();
635                 if( (itv = offsets.get(OffsetIndex.NINTH)) != null ) {
636                         switch(itv) {
637                         case NINTH:  paren.add("9th"); break;
638                         case FLAT9:  paren.add("flatted 9th"); break;
639                         case SHARP9: paren.add("sharped 9th"); break;
640                         default: break;
641                         }
642                 }
643                 if( (itv = offsets.get(OffsetIndex.ELEVENTH)) != null ) {
644                         switch(itv) {
645                         case ELEVENTH: paren.add("11th"); break;
646                         case SHARP11:  paren.add("sharped 11th"); break;
647                         default: break;
648                         }
649                 }
650                 if( (itv = offsets.get(OffsetIndex.THIRTEENTH)) != null ) {
651                         switch(itv) {
652                         case THIRTEENTH: paren.add("13th"); break;
653                         case FLAT13:     paren.add("flatted 13th"); break;
654                         default: break;
655                         }
656                 }
657                 if( ! paren.isEmpty() ) {
658                         boolean is_first = true;
659                         suffix += "(additional ";
660                         for( String p : paren ) {
661                                 if( is_first )
662                                         is_first = false;
663                                 else
664                                         suffix += ",";
665                                 suffix += p;
666                         }
667                         suffix += ")";
668                 }
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";
677                 else return suffix ;
678         }
679 }