OSDN Git Service

Chordクラスのイミュータブル化
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / music / ChordProgression.java
1 package camidion.chordhelper.music;
2
3 import java.util.ArrayList;
4 import java.util.List;
5 import java.util.Vector;
6 import java.util.regex.Pattern;
7
8 import javax.sound.midi.InvalidMidiDataException;
9 import javax.sound.midi.Sequence;
10
11 /**
12  * Chord Progression - コード進行のクラス
13  */
14 public class ChordProgression {
15
16         public class TickRange implements Cloneable {
17                 long startTickPos = 0, end_tick_pos = 0;
18                 public TickRange( long tick_pos ) {
19                         end_tick_pos = startTickPos = tick_pos;
20                 }
21                 public TickRange( long start_tick_pos, long end_tick_pos ) {
22                         this.startTickPos = start_tick_pos;
23                         this.end_tick_pos = end_tick_pos;
24                 }
25                 protected TickRange clone() {
26                         return new TickRange( startTickPos, end_tick_pos );
27                 }
28                 public void moveForward() {
29                         startTickPos = end_tick_pos;
30                 }
31                 public void moveForward( long duration ) {
32                         startTickPos = end_tick_pos;
33                         end_tick_pos += duration;
34                 }
35                 public long duration() {
36                         return end_tick_pos - startTickPos;
37                 }
38                 public boolean contains( long tick ) {
39                         return ( tick >= startTickPos && tick < end_tick_pos );
40                 }
41         }
42
43         class ChordStroke {
44                 Chord chord; int beatLength; TickRange tickRange = null;
45                 public ChordStroke(Chord chord) { this( chord, 1 ); }
46                 public ChordStroke(Chord chord, int beat_length) {
47                         this.chord = chord;
48                         this.beatLength = beat_length;
49                 }
50                 public String toString() {
51                         String str = chord.toString();
52                         for( int i=2; i <= beatLength; i++ ) str += " %";
53                         return str;
54                 }
55         }
56
57         // 時間位置付き歌詞
58         public class Lyrics {
59                 String text = null;
60                 Long startTickPos = null;
61                 public Lyrics(String text) { this.text = text; }
62                 public Lyrics(String text, long tick_pos) {
63                         this.text = text; startTickPos = tick_pos;
64                 }
65                 public String toString() { return text; }
66         }
67
68         class Measure extends Vector<Object> {
69                 Long ticks_per_beat = null;
70                 public int numberOfBeats() {
71                         int n = 0;
72                         for( Object obj : this ) {
73                                 if( obj instanceof ChordStroke ) {
74                                         n += ((ChordStroke)obj).beatLength;
75                                 }
76                         }
77                         return n;
78                 }
79                 // 小節内のコードストロークが時間的に等間隔かどうか調べる。
80                 // もし等間隔の場合、テキスト出力時に % をつける必要がなくなる。
81                 public boolean isEquallyDivided() {
82                         int l, l_prev = 0;
83                         for( Object obj : this ) {
84                                 if( obj instanceof ChordStroke ) {
85                                         l = ((ChordStroke)obj).beatLength;
86                                         if( l_prev > 0 && l_prev != l ) {
87                                                 return false;
88                                         }
89                                         l_prev = l;
90                                 }
91                         }
92                         return true;
93                 }
94                 public int addBeat() { return addBeat(1); }
95                 public int addBeat(int num_beats) {
96                         ChordStroke last_chord_stroke = null;
97                         for( Object obj : this ) {
98                                 if( obj instanceof ChordStroke ) {
99                                         last_chord_stroke = (ChordStroke)obj;
100                                 }
101                         }
102                         if( last_chord_stroke == null ) {
103                                 return 0;
104                         }
105                         return last_chord_stroke.beatLength += num_beats;
106                 }
107                 public String toString() {
108                         String str = "";
109                         boolean is_eq_dev = isEquallyDivided();
110                         for( Object element : this ) {
111                                 str += " ";
112                                 if( element instanceof ChordStroke ) {
113                                         ChordStroke cs = (ChordStroke)element;
114                                         str += is_eq_dev ? cs.chord : cs;
115                                 }
116                                 else if( element instanceof Lyrics ) {
117                                         str += element.toString();
118                                 }
119                         }
120                         return str;
121                 }
122                 public TickRange getRange() {
123                         long start_tick_pos = -1;
124                         long end_tick_pos = -1;
125                         for( Object element : this ) {
126                                 if( ! (element instanceof ChordProgression.ChordStroke) )
127                                         continue;
128                                 ChordProgression.ChordStroke chord_stroke
129                                 = (ChordProgression.ChordStroke)element;
130                                 // 小節の先頭と末尾の tick を求める
131                                 if( start_tick_pos < 0 ) {
132                                         start_tick_pos = chord_stroke.tickRange.startTickPos;
133                                 }
134                                 end_tick_pos = chord_stroke.tickRange.end_tick_pos;
135                         }
136                         if( start_tick_pos < 0 || end_tick_pos < 0 ) {
137                                 return null;
138                         }
139                         return new TickRange( start_tick_pos, end_tick_pos );
140                 }
141                 public ChordStroke chordStrokeAt( long tick ) {
142                         for( Object element : this ) {
143                                 if( ! (element instanceof ChordProgression.ChordStroke) )
144                                         continue;
145                                 ChordProgression.ChordStroke chord_stroke
146                                 = (ChordProgression.ChordStroke)element;
147                                 if( chord_stroke.tickRange.contains(tick) ) {
148                                         return chord_stroke;
149                                 }
150                         }
151                         return null;
152                 }
153         }
154         class Line extends Vector<Measure> {
155                 public String toString() {
156                         String str = "";
157                         for( Measure measure : this ) str += measure + "|";
158                         return str;
159                 }
160         }
161
162         // 内部変数
163         Vector<Line> lines = null;
164         Key key = null;
165         private Long ticks_per_measure = null;
166
167         public Key getKey() { return key; }
168         public void setKey(Key key) { this.key = key; }
169
170         public String toString() {
171                 String str = "";
172                 if( key != null ) str += "Key: " + key + "\n";
173                 for( Line line : lines ) str += line + "\n";
174                 return str;
175         }
176
177         /**
178          * デフォルトの設定でコード進行を構築します。
179          */
180         public ChordProgression() { }
181         /**
182          * 指定された小節数、キー、拍子に合わせたコード進行を構築します。
183          * コード進行の内容は、ランダムに自動生成されます。
184          * @param measureLength 小節の長さ
185          * @param timeSignatureUpper 拍子の分子
186          */
187         public ChordProgression( int measureLength, int timeSignatureUpper ) {
188                 int keyCo5 = (int)(Math.random() * 12) - 5;
189                 key = new Key(keyCo5, Key.MajorMinor.MAJOR);
190                 lines = new Vector<Line>();
191                 Line line = new Line();
192                 boolean isEnd;
193                 Chord chord, prevChord = new Chord(new NoteSymbol(keyCo5));
194                 int co5Offset, prevCo5Offset;
195                 double r;
196                 for( int mp=0; mp<measureLength; mp++ ) {
197                         isEnd = (mp == 0 || mp == measureLength - 1); // 最初または最後の小節かを覚えておく
198                         Measure measure = new Measure();
199                         ChordStroke lastChordStroke = null;
200                         for( int i=0; i<timeSignatureUpper; i++ ) {
201                                 if(
202                                         i % 4 == 2 && Math.random() < 0.8
203                                         ||
204                                         i % 2 != 0 && Math.random() < 0.9
205                                 ){
206                                         // もう一拍延長
207                                         lastChordStroke.beatLength++;
208                                         continue;
209                                 }
210                                 co5Offset = 0;
211                                 prevCo5Offset = prevChord.rootNoteSymbol().toCo5() - keyCo5;
212                                 if( ! isEnd ) {
213                                         //
214                                         // 最初または最後の小節は常にトニックにする。
215                                         // 完全五度ずつ下がる進行を基本としつつ、時々そうでない進行も出現するようにする。
216                                         // サブドミナントを超えるとスケールを外れるので、超えそうになったらランダムに決め直す。
217                                         //
218                                         r = Math.random();
219                                         co5Offset = prevCo5Offset - 1;
220                                         if( co5Offset < -1 || (prevCo5Offset < 5 && r < 0.5) ) {
221                                                 //
222                                                 // 長7度がルートとなるコードの出現確率を半減させながらコードを決める
223                                                 // (余りが6のときだけが長7度)
224                                                 // なお、前回と同じコードは使わないようにする。
225                                                 do {
226                                                         co5Offset = (int)(Math.random() * 13) % 7 - 1;
227                                                 } while( co5Offset == prevCo5Offset );
228                                         }
229                                 }
230                                 NoteSymbol rootNote = new NoteSymbol(keyCo5 + co5Offset);
231                                 List<Chord.Interval> intervals = new ArrayList<>();
232                                 switch(co5Offset) {
233                                 // ルート音ごとに、7th などの付加や、メジャーマイナー反転を行う確率を決める
234                                 case 5: // VII
235                                         if( Math.random() < 0.5 ) {
236                                                 // m7-5
237                                                 intervals.add(Chord.Interval.MINOR);
238                                                 intervals.add(Chord.Interval.FLAT5);
239                                         }
240                                         if( Math.random() < 0.8 )
241                                                 intervals.add(Chord.Interval.SEVENTH);
242                                         break;
243                                 case 4: // Secondary dominant (III)
244                                         if( prevCo5Offset == 5 ) {
245                                                 // ルートが長7度→長3度の進行のとき、反転確率を上げる。
246                                                 // (ハ長調でいう Bm7-5 の次に E7 を出現しやすくする)
247                                                 if( Math.random() < 0.2 ) intervals.add(Chord.Interval.MINOR);
248                                         }
249                                         else {
250                                                 if( Math.random() < 0.8 ) intervals.add(Chord.Interval.MINOR);
251                                         }
252                                         if( Math.random() < 0.7 ) intervals.add(Chord.Interval.SEVENTH);
253                                         break;
254                                 case 3: // VI
255                                         if( Math.random() < 0.8 ) intervals.add(Chord.Interval.MINOR);
256                                         if( Math.random() < 0.7 ) intervals.add(Chord.Interval.SEVENTH);
257                                         break;
258                                 case 2: // II
259                                         if( Math.random() < 0.8 ) intervals.add(Chord.Interval.MINOR);
260                                         if( Math.random() < 0.7 ) intervals.add(Chord.Interval.SEVENTH);
261                                         break;
262                                 case 1: // Dominant (V)
263                                         if( Math.random() < 0.1 ) intervals.add(Chord.Interval.MINOR);
264                                         if( Math.random() < 0.3 ) intervals.add(Chord.Interval.SEVENTH);
265                                         if( Math.random() < 0.2 ) intervals.add(Chord.Interval.NINTH);
266                                         break;
267                                 case 0: // Tonic(ここでマイナーで終わるとさみしいので setMinorThird() はしない)
268                                         if( Math.random() < 0.2 ) intervals.add(Chord.Interval.MAJOR_SEVENTH);
269                                         if( Math.random() < 0.2 ) intervals.add(Chord.Interval.NINTH);
270                                         break;
271                                 case -1: // Sub-dominant (IV)
272                                         if( Math.random() < 0.1 ) {
273                                                 intervals.add(Chord.Interval.MINOR);
274                                                 if( Math.random() < 0.3 ) intervals.add(Chord.Interval.SEVENTH);
275                                         }
276                                         else
277                                                 if( Math.random() < 0.2 ) intervals.add(Chord.Interval.MAJOR_SEVENTH);
278                                         if( Math.random() < 0.2 ) intervals.add(Chord.Interval.NINTH);
279                                         break;
280                                 }
281                                 chord = new Chord(rootNote, rootNote, intervals);
282                                 measure.add( lastChordStroke = new ChordStroke(chord) );
283                                 prevChord = chord;
284                         }
285                         line.add(measure);
286                         if( (mp+1) % 8 == 0 ) { // 8小節おきに改行
287                                 lines.add(line);
288                                 line = new Line();
289                         }
290                 }
291                 if( line.size() > 0 ) lines.add(line);
292         }
293         // テキストからコード進行を生成
294         public ChordProgression( String source_text ) {
295                 if( source_text == null ) return;
296                 Measure measure;
297                 Line line;
298                 String[] linesSrc, measuresSrc, elementsSrc;
299                 Chord lastChord = null;
300                 String keyHeaderRegex = "^Key(\\s*):(\\s*)";
301                 String keyValueRegex = "[A-G]+.*$";
302                 //
303                 // キーであるかどうか見分けるためのパターン
304                 Pattern keyMatchPattern = Pattern.compile(
305                         keyHeaderRegex + keyValueRegex,
306                         Pattern.CASE_INSENSITIVE
307                 );
308                 // キーのヘッダーを取り除くためのパターン
309                 Pattern keyReplPattern = Pattern.compile(
310                         keyHeaderRegex, Pattern.CASE_INSENSITIVE
311                 );
312                 //
313                 linesSrc = source_text.split("[\r\n]+");
314                 lines = new Vector<Line>();
315                 for( String line_src : linesSrc ) {
316                         measuresSrc = line_src.split("\\|");
317                         if( measuresSrc.length > 0 ) {
318                                 String keyString = measuresSrc[0].trim();
319                                 if( keyMatchPattern.matcher(keyString).matches() ) {
320                                         try {
321                                                 key = new Key(keyReplPattern.matcher(keyString).replaceFirst(""));
322                                                 continue;
323                                         } catch( Exception e ) {
324                                                 e.printStackTrace();
325                                         }
326                                 }
327                         }
328                         line = new Line();
329                         for( String measureSrc : measuresSrc ) {
330                                 elementsSrc = measureSrc.split("[ \t]+");
331                                 measure = new Measure();
332                                 for( String elementSrc : elementsSrc ) {
333                                         if( elementSrc.isEmpty() ) continue;
334                                         if( elementSrc.equals("%") ) {
335                                                 if( measure.addBeat() == 0 ) {
336                                                         measure.add( new ChordStroke(lastChord) );
337                                                 }
338                                                 continue;
339                                         }
340                                         try {
341                                                 measure.add(new ChordStroke(lastChord = new Chord(elementSrc)));
342                                         } catch( IllegalArgumentException ex ) {
343                                                 measure.add( new Lyrics(elementSrc) );
344                                         }
345                                 }
346                                 line.add(measure);
347                         }
348                         lines.add(line);
349                 }
350         }
351
352         // Major/minor 切り替え
353         public void toggleKeyMajorMinor() { key = key.relativeKey(); }
354
355         // コード進行の移調
356         public void transpose(int chromaticOffset) {
357                 for( Line line : lines ) {
358                         for( Measure measure : line ) {
359                                 for( int i=0; i<measure.size(); i++ ) {
360                                         Object element = measure.get(i);
361                                         if( element instanceof ChordStroke ) {
362                                                 ChordStroke cs = (ChordStroke)element;
363                                                 //
364                                                 // キーが未設定のときは、最初のコードから推測して設定
365                                                 if( key == null ) key = new Key(cs.chord);
366                                                 //
367                                                 Chord newChord = cs.chord.transposedNewChord(chromaticOffset, key);
368                                                 measure.set(i, new ChordStroke(newChord, cs.beatLength));
369                                         }
370                                 }
371                         }
372                 }
373                 key = key.transposedKey(chromaticOffset);
374         }
375         // 異名同音の♭と♯を切り替える
376         public void toggleEnharmonically() {
377                 if( key == null ) return;
378                 int original_key_co5 = key.toCo5();
379                 int co5Offset = 0;
380                 if( original_key_co5 > 4 ) {
381                         co5Offset = -Music.SEMITONES_PER_OCTAVE;
382                 }
383                 else if( original_key_co5 < -4 ) {
384                         co5Offset = Music.SEMITONES_PER_OCTAVE;
385                 }
386                 else {
387                         return;
388                 }
389                 key = key.enharmonicKey();
390                 for( Line line : lines ) {
391                         for( Measure measure : line ) {
392                                 for( int i=0; i<measure.size(); i++ ) {
393                                         Object element = measure.get(i);
394                                         if( element instanceof ChordStroke ) {
395                                                 ChordStroke cs = (ChordStroke)element;
396                                                 NoteSymbol root = cs.chord.rootNoteSymbol();
397                                                 NoteSymbol bass = cs.chord.bassNoteSymbol();
398                                                 if( root.equals(bass) ) {
399                                                         bass = root = new NoteSymbol(root.toCo5() + co5Offset);
400                                                 } else {
401                                                         root = new NoteSymbol(root.toCo5() + co5Offset);
402                                                         bass = new NoteSymbol(bass.toCo5() + co5Offset);
403                                                 }
404                                                 Chord newChord = new Chord(root, bass, cs.chord.intervals());
405                                                 measure.set(i, new ChordStroke(newChord, cs.beatLength));
406                                         }
407                                 }
408                         }
409                 }
410         }
411         // コード進行の中に時間軸(MIDI tick)を書き込む
412         //
413         public void setTickPositions( FirstTrackSpec first_track ) {
414                 ticks_per_measure = first_track.ticksPerMeasure;
415                 TickRange tick_range = new TickRange(
416                                 first_track.preMeasures * ticks_per_measure
417                                 );
418                 for( Line line : lines ) { // 行単位の処理
419                         for( Measure measure : line ) { // 小節単位の処理
420                                 int n_beats = measure.numberOfBeats();
421                                 if( n_beats == 0 ) continue;
422                                 long tpb = measure.ticks_per_beat = ticks_per_measure / n_beats ;
423                                 for( Object element : measure ) {
424                                         if( element instanceof Lyrics ) {
425                                                 ((Lyrics)element).startTickPos = tick_range.startTickPos;
426                                                 continue;
427                                         }
428                                         else if( element instanceof ChordStroke ) {
429                                                 ChordStroke chord_stroke = (ChordStroke)element;
430                                                 tick_range.moveForward( tpb * chord_stroke.beatLength );
431                                                 chord_stroke.tickRange = tick_range.clone();
432                                         }
433                                 }
434                         }
435                 }
436         }
437         // コード文字列の書き込み
438         public void setChordSymbolTextTo( AbstractTrackSpec ts ) {
439                 for( Line line : lines ) {
440                         for( Measure measure : line ) {
441                                 if( measure.ticks_per_beat == null ) continue;
442                                 for( Object element : measure ) {
443                                         if( element instanceof ChordStroke ) {
444                                                 ts.addStringTo( 0x01, (ChordStroke)element );
445                                         }
446                                 }
447                         }
448                 }
449         }
450         // 歌詞の書き込み
451         public void setLyricsTo( AbstractTrackSpec ts ) {
452                 for( Line line : lines ) {
453                         for( Measure measure : line ) {
454                                 if( measure.ticks_per_beat == null ) continue;
455                                 for( Object element : measure ) {
456                                         if( element instanceof Lyrics ) {
457                                                 ts.addStringTo( 0x05, (Lyrics)element );
458                                         }
459                                 }
460                         }
461                 }
462         }
463         /**
464          * コード進行をもとに MIDI シーケンスを生成します。
465          * @return MIDIシーケンス
466          */
467         public Sequence toMidiSequence() {
468                 return toMidiSequence(48);
469         }
470         /**
471          * 指定のタイミング解像度で、
472          * コード進行をもとに MIDI シーケンスを生成します。
473          * @return MIDIシーケンス
474          */
475         public Sequence toMidiSequence(int ppq) {
476                 //
477                 // PPQ = Pulse Per Quarter (TPQN = Tick Per Quearter Note)
478                 //
479                 return toMidiSequence( ppq, 0, 0, null, null );
480         }
481         /**
482          * 小節数、トラック仕様、コード進行をもとに MIDI シーケンスを生成します。
483          * @param ppq 分解能(pulse per quarter)
484          * @param startMeasure 開始小節位置
485          * @param endMeasure 終了小節位置
486          * @param firstTrack 最初のトラックの仕様
487          * @param trackSpecs 残りのトラックの仕様
488          * @return MIDIシーケンス
489          */
490         public Sequence toMidiSequence(
491                 int ppq, int startMeasure, int endMeasure,
492                 FirstTrackSpec firstTrack,
493                 Vector<AbstractNoteTrackSpec> trackSpecs
494         ) {
495                 Sequence seq;
496                 try {
497                         seq = new Sequence(Sequence.PPQ, ppq);
498                 } catch ( InvalidMidiDataException e ) {
499                         e.printStackTrace();
500                         return null;
501                 }
502                 // マスタートラックの生成
503                 if( firstTrack == null ) {
504                         firstTrack = new FirstTrackSpec();
505                 }
506                 firstTrack.key = this.key;
507                 firstTrack.createTrack( seq, startMeasure, endMeasure );
508                 //
509                 // 中身がなければここで終了
510                 if( lines == null || trackSpecs == null ) return seq;
511                 //
512                 // コード進行の中に時間軸(MIDI tick)を書き込む
513                 setTickPositions(firstTrack);
514                 //
515                 // コードのテキストと歌詞を書き込む
516                 setChordSymbolTextTo(firstTrack);
517                 setLyricsTo(firstTrack);
518                 //
519                 // 残りのトラックを生成
520                 for( AbstractNoteTrackSpec ts : trackSpecs ) {
521                         ts.createTrack(seq, firstTrack);
522                         if( ts instanceof DrumTrackSpec ) {
523                                 ((DrumTrackSpec)ts).addDrums(this);
524                         }
525                         else {
526                                 ((MelodyTrackSpec)ts).addChords(this);
527                         }
528                 }
529                 return seq;
530         }
531 }