1 package camidion.chordhelper.music;
3 import java.util.ArrayList;
5 import java.util.Vector;
6 import java.util.regex.Pattern;
8 import javax.sound.midi.InvalidMidiDataException;
9 import javax.sound.midi.Sequence;
12 * Chord Progression - コード進行のクラス
14 public class ChordProgression {
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;
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;
25 protected TickRange clone() {
26 return new TickRange( startTickPos, end_tick_pos );
28 public void moveForward() {
29 startTickPos = end_tick_pos;
31 public void moveForward( long duration ) {
32 startTickPos = end_tick_pos;
33 end_tick_pos += duration;
35 public long duration() {
36 return end_tick_pos - startTickPos;
38 public boolean contains( long tick ) {
39 return ( tick >= startTickPos && tick < end_tick_pos );
44 Chord chord; int beatLength; TickRange tickRange = null;
45 public ChordStroke(Chord chord) { this( chord, 1 ); }
46 public ChordStroke(Chord chord, int beat_length) {
48 this.beatLength = beat_length;
50 public String toString() {
51 String str = chord.toString();
52 for( int i=2; i <= beatLength; i++ ) str += " %";
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;
65 public String toString() { return text; }
68 class Measure extends Vector<Object> {
69 Long ticks_per_beat = null;
70 public int numberOfBeats() {
72 for( Object obj : this ) {
73 if( obj instanceof ChordStroke ) {
74 n += ((ChordStroke)obj).beatLength;
79 // 小節内のコードストロークが時間的に等間隔かどうか調べる。
80 // もし等間隔の場合、テキスト出力時に % をつける必要がなくなる。
81 public boolean isEquallyDivided() {
83 for( Object obj : this ) {
84 if( obj instanceof ChordStroke ) {
85 l = ((ChordStroke)obj).beatLength;
86 if( l_prev > 0 && l_prev != l ) {
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;
102 if( last_chord_stroke == null ) {
105 return last_chord_stroke.beatLength += num_beats;
107 public String toString() {
109 boolean is_eq_dev = isEquallyDivided();
110 for( Object element : this ) {
112 if( element instanceof ChordStroke ) {
113 ChordStroke cs = (ChordStroke)element;
114 str += is_eq_dev ? cs.chord : cs;
116 else if( element instanceof Lyrics ) {
117 str += element.toString();
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) )
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;
134 end_tick_pos = chord_stroke.tickRange.end_tick_pos;
136 if( start_tick_pos < 0 || end_tick_pos < 0 ) {
139 return new TickRange( start_tick_pos, end_tick_pos );
141 public ChordStroke chordStrokeAt( long tick ) {
142 for( Object element : this ) {
143 if( ! (element instanceof ChordProgression.ChordStroke) )
145 ChordProgression.ChordStroke chord_stroke
146 = (ChordProgression.ChordStroke)element;
147 if( chord_stroke.tickRange.contains(tick) ) {
154 class Line extends Vector<Measure> {
155 public String toString() {
157 for( Measure measure : this ) str += measure + "|";
163 Vector<Line> lines = null;
165 private Long ticks_per_measure = null;
167 public Key getKey() { return key; }
168 public void setKey(Key key) { this.key = key; }
170 public String toString() {
172 if( key != null ) str += "Key: " + key + "\n";
173 for( Line line : lines ) str += line + "\n";
178 * デフォルトの設定でコード進行を構築します。
180 public ChordProgression() { }
182 * 指定された小節数、キー、拍子に合わせたコード進行を構築します。
183 * コード進行の内容は、ランダムに自動生成されます。
184 * @param measureLength 小節の長さ
185 * @param timeSignatureUpper 拍子の分子
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();
193 Chord chord, prevChord = new Chord(new NoteSymbol(keyCo5));
194 int co5Offset, prevCo5Offset;
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++ ) {
202 i % 4 == 2 && Math.random() < 0.8
204 i % 2 != 0 && Math.random() < 0.9
207 lastChordStroke.beatLength++;
211 prevCo5Offset = prevChord.rootNoteSymbol().toCo5() - keyCo5;
214 // 最初または最後の小節は常にトニックにする。
215 // 完全五度ずつ下がる進行を基本としつつ、時々そうでない進行も出現するようにする。
216 // サブドミナントを超えるとスケールを外れるので、超えそうになったらランダムに決め直す。
219 co5Offset = prevCo5Offset - 1;
220 if( co5Offset < -1 || (prevCo5Offset < 5 && r < 0.5) ) {
222 // 長7度がルートとなるコードの出現確率を半減させながらコードを決める
224 // なお、前回と同じコードは使わないようにする。
226 co5Offset = (int)(Math.random() * 13) % 7 - 1;
227 } while( co5Offset == prevCo5Offset );
230 NoteSymbol rootNote = new NoteSymbol(keyCo5 + co5Offset);
231 List<Chord.Interval> intervals = new ArrayList<>();
233 // ルート音ごとに、7th などの付加や、メジャーマイナー反転を行う確率を決める
235 if( Math.random() < 0.5 ) {
237 intervals.add(Chord.Interval.MINOR);
238 intervals.add(Chord.Interval.FLAT5);
240 if( Math.random() < 0.8 )
241 intervals.add(Chord.Interval.SEVENTH);
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);
250 if( Math.random() < 0.8 ) intervals.add(Chord.Interval.MINOR);
252 if( Math.random() < 0.7 ) intervals.add(Chord.Interval.SEVENTH);
255 if( Math.random() < 0.8 ) intervals.add(Chord.Interval.MINOR);
256 if( Math.random() < 0.7 ) intervals.add(Chord.Interval.SEVENTH);
259 if( Math.random() < 0.8 ) intervals.add(Chord.Interval.MINOR);
260 if( Math.random() < 0.7 ) intervals.add(Chord.Interval.SEVENTH);
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);
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);
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);
277 if( Math.random() < 0.2 ) intervals.add(Chord.Interval.MAJOR_SEVENTH);
278 if( Math.random() < 0.2 ) intervals.add(Chord.Interval.NINTH);
281 chord = new Chord(rootNote, rootNote, intervals);
282 measure.add( lastChordStroke = new ChordStroke(chord) );
286 if( (mp+1) % 8 == 0 ) { // 8小節おきに改行
291 if( line.size() > 0 ) lines.add(line);
294 public ChordProgression( String source_text ) {
295 if( source_text == null ) return;
298 String[] linesSrc, measuresSrc, elementsSrc;
299 Chord lastChord = null;
300 String keyHeaderRegex = "^Key(\\s*):(\\s*)";
301 String keyValueRegex = "[A-G]+.*$";
303 // キーであるかどうか見分けるためのパターン
304 Pattern keyMatchPattern = Pattern.compile(
305 keyHeaderRegex + keyValueRegex,
306 Pattern.CASE_INSENSITIVE
308 // キーのヘッダーを取り除くためのパターン
309 Pattern keyReplPattern = Pattern.compile(
310 keyHeaderRegex, Pattern.CASE_INSENSITIVE
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() ) {
321 key = new Key(keyReplPattern.matcher(keyString).replaceFirst(""));
323 } catch( Exception e ) {
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) );
341 measure.add(new ChordStroke(lastChord = new Chord(elementSrc)));
342 } catch( IllegalArgumentException ex ) {
343 measure.add( new Lyrics(elementSrc) );
353 public void toggleKeyMajorMinor() { key = key.relativeKey(); }
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;
364 // キーが未設定のときは、最初のコードから推測して設定
365 if( key == null ) key = new Key(cs.chord);
367 Chord newChord = cs.chord.transposedNewChord(chromaticOffset, key);
368 measure.set(i, new ChordStroke(newChord, cs.beatLength));
373 key = key.transposedKey(chromaticOffset);
376 public void toggleEnharmonically() {
377 if( key == null ) return;
378 int original_key_co5 = key.toCo5();
380 if( original_key_co5 > 4 ) {
381 co5Offset = -Music.SEMITONES_PER_OCTAVE;
383 else if( original_key_co5 < -4 ) {
384 co5Offset = Music.SEMITONES_PER_OCTAVE;
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);
401 root = new NoteSymbol(root.toCo5() + co5Offset);
402 bass = new NoteSymbol(bass.toCo5() + co5Offset);
404 Chord newChord = new Chord(root, bass, cs.chord.intervals());
405 measure.set(i, new ChordStroke(newChord, cs.beatLength));
411 // コード進行の中に時間軸(MIDI tick)を書き込む
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
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;
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();
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 );
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 );
464 * コード進行をもとに MIDI シーケンスを生成します。
467 public Sequence toMidiSequence() {
468 return toMidiSequence(48);
472 * コード進行をもとに MIDI シーケンスを生成します。
475 public Sequence toMidiSequence(int ppq) {
477 // PPQ = Pulse Per Quarter (TPQN = Tick Per Quearter Note)
479 return toMidiSequence( ppq, 0, 0, null, null );
482 * 小節数、トラック仕様、コード進行をもとに MIDI シーケンスを生成します。
483 * @param ppq 分解能(pulse per quarter)
484 * @param startMeasure 開始小節位置
485 * @param endMeasure 終了小節位置
486 * @param firstTrack 最初のトラックの仕様
487 * @param trackSpecs 残りのトラックの仕様
490 public Sequence toMidiSequence(
491 int ppq, int startMeasure, int endMeasure,
492 FirstTrackSpec firstTrack,
493 Vector<AbstractNoteTrackSpec> trackSpecs
497 seq = new Sequence(Sequence.PPQ, ppq);
498 } catch ( InvalidMidiDataException e ) {
503 if( firstTrack == null ) {
504 firstTrack = new FirstTrackSpec();
506 firstTrack.key = this.key;
507 firstTrack.createTrack( seq, startMeasure, endMeasure );
510 if( lines == null || trackSpecs == null ) return seq;
512 // コード進行の中に時間軸(MIDI tick)を書き込む
513 setTickPositions(firstTrack);
516 setChordSymbolTextTo(firstTrack);
517 setLyricsTo(firstTrack);
520 for( AbstractNoteTrackSpec ts : trackSpecs ) {
521 ts.createTrack(seq, firstTrack);
522 if( ts instanceof DrumTrackSpec ) {
523 ((DrumTrackSpec)ts).addDrums(this);
526 ((MelodyTrackSpec)ts).addChords(this);