/* * Copyright (C) 2013-2017 たんらる */ package fourthline.mmlTools; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import fourthline.mmlTools.core.MMLTicks; import fourthline.mmlTools.core.UndefinedTickException; import fourthline.mmlTools.parser.MMLEventParser; /** * 1行のMMLデータを扱います. */ public final class MMLEventList implements Serializable, Cloneable { private static final long serialVersionUID = -1430758411579285535L; private List<MMLNoteEvent> noteList = new ArrayList<>(); private List<MMLTempoEvent> tempoList; /** * * @param mml */ public MMLEventList(String mml) { this(mml, null); } public MMLEventList(String mml, List<MMLTempoEvent> globalTempoList) { if (globalTempoList != null) { tempoList = globalTempoList; } else { tempoList = new ArrayList<MMLTempoEvent>(); } parseMML(mml); } private void parseMML(String mml) { MMLEventParser parser = new MMLEventParser(mml); while (parser.hasNext()) { MMLEvent event = parser.next(); if (event instanceof MMLTempoEvent) { ((MMLTempoEvent) event).appendToListElement(tempoList); } else if (event instanceof MMLNoteEvent) { noteList.add((MMLNoteEvent) event); } } } public void setGlobalTempoList(List<MMLTempoEvent> globalTempoList) { tempoList = globalTempoList; } public List<MMLTempoEvent> getGlobalTempoList() { return tempoList; } public long getTickLength() { if (noteList.size() > 0) { int lastIndex = noteList.size() - 1; MMLNoteEvent lastNote = noteList.get( lastIndex ); return lastNote.getEndTick(); } else { return 0; } } public List<MMLNoteEvent> getMMLNoteEventList() { return noteList; } /** * 指定したtickOffset位置にあるNoteEventを検索します. * @param tickOffset * @return 見つからなかった場合は、nullを返します. */ public MMLNoteEvent searchOnTickOffset(long tickOffset) { for (MMLNoteEvent noteEvent : noteList) { if (noteEvent.getTickOffset() <= tickOffset) { if (tickOffset < noteEvent.getEndTick()) { return noteEvent; } } else { break; } } return null; } /** * 指定したtickOffset位置の手前のNoteを検索します. * @param tickOffset * @return */ public MMLNoteEvent searchPrevNoteOnTickOffset(long tickOffset) { MMLNoteEvent prevNote = null; for (MMLNoteEvent noteEvent : noteList) { if (noteEvent.getTickOffset() >= tickOffset) { break; } prevNote = noteEvent; } return prevNote; } /** * 指定したtickOffset位置のparsed-MML文字列に対するIndexを取得します. * @param tickOffset * @return */ public int[] indexOfMMLString(long tickOffset) { int start = 0; for (MMLNoteEvent noteEvent : noteList) { int index[] = noteEvent.getIndexOfMMLString(); if (noteEvent.getTickOffset() <= tickOffset) { if (tickOffset < noteEvent.getEndTick()) { return index; } } else { return new int[] { start, index[0] }; } start = index[1]; } return new int[] { start, start }; } /** * ノートイベントを追加します. * TODO: MMLNoteEvent のメソッドのほうがいいかな?Listを引数として渡す. * @param addNoteEvent */ public void addMMLNoteEvent(MMLNoteEvent addNoteEvent) { int i; if ((addNoteEvent.getNote() < -1) || (addNoteEvent.getTick() <= 0) || (addNoteEvent.getEndTick() <= 0)) { return; } int offset = addNoteEvent.getTickOffset(); if (offset < 0) { addNoteEvent.setTick( (addNoteEvent.getTick() + offset) ); addNoteEvent.setTickOffset(0); } // 追加したノートイベントに重なる前のノートを調節します. for (i = 0; i < noteList.size(); i++) { MMLNoteEvent noteEvent = noteList.get(i); int tickOverlap = noteEvent.getEndTick() - addNoteEvent.getTickOffset(); if (addNoteEvent.getTickOffset() < noteEvent.getTickOffset()) { break; } if (tickOverlap >= 0) { // 追加するノートに音が重なっている. int tick = noteEvent.getTick() - tickOverlap; if (tick == 0) { noteList.remove(i); break; } else { noteEvent.setTick(tick); i++; break; } } } // ノートイベントを追加します. noteList.add(i++, addNoteEvent); // 追加したノートイベントに重なっている後続のノートを削除します. for ( ; i < noteList.size(); ) { MMLNoteEvent noteEvent = noteList.get(i); int tickOverlap = addNoteEvent.getEndTick() - noteEvent.getTickOffset(); if (tickOverlap > 0) { noteList.remove(i); } else { break; } } } /** * リスト中のノートイベントに重複しているかを判定します. * @param noteEvent 判定するノートイベント. * @return 重複している場合は trueを返します. */ public boolean isOverlapNote(MMLNoteEvent noteEvent) { int i; for (i = 0; i < noteList.size(); i++) { MMLNoteEvent e = noteList.get(i); if (noteEvent.getTickOffset() < e.getEndTick()) { if (noteEvent.getTickOffset() >= e.getTickOffset()) { return true; } break; } } for (i = 0; i < noteList.size(); i++) { MMLNoteEvent e = noteList.get(i); if (noteEvent.getEndTick() <= e.getEndTick()) { if (noteEvent.getEndTick()-1 >= e.getTickOffset()) { return true; } break; } } return false; } /** * 指定のMMLeventを削除する. * 最後尾はtrim. * @param deleteItem */ public void deleteMMLEvent(MMLEvent deleteItem) { noteList.remove(deleteItem); } /** * 指定されたノートに音量コマンドを設定する. * 後続の同音量のノートも更新する. * @param targetNote * @param velocity */ public void setVelocityCommand(MMLNoteEvent targetNote, int velocity) { setUnsetVelocityCommand(targetNote, velocity, true); } /** * 指定されたノートに音量コマンドを解除する. * 後続の同音量のノートも更新する. * @param targetNote */ public void unsetVelocityCommand(MMLNoteEvent targetNote) { setUnsetVelocityCommand(targetNote, 0, false); } private void setUnsetVelocityCommand(MMLNoteEvent targetNote, int velocity, boolean isON) { int beforeVelocity = targetNote.getVelocity(); int prevVelocity = MMLNoteEvent.INIT_VOL; for (MMLNoteEvent note : noteList) { if (note.getTickOffset() >= targetNote.getTickOffset()) { if (beforeVelocity == note.getVelocity()) { note.setVelocity(isON ? velocity : prevVelocity); } else { break; } } else { prevVelocity = note.getVelocity(); } } } public String toMMLString() throws UndefinedTickException { return toMMLString(false, 0, true); } public String toMMLString(boolean withTempo, boolean mabiTempo) throws UndefinedTickException { return toMMLString(withTempo, 0, mabiTempo); } private MMLNoteEvent insertTempoMML(StringBuilder sb, MMLNoteEvent prevNoteEvent, MMLTempoEvent tempoEvent, boolean mabiTempo) throws UndefinedTickException { if (prevNoteEvent.getEndTick() != tempoEvent.getTickOffset()) { int tickLength = tempoEvent.getTickOffset() - prevNoteEvent.getEndTick(); int tickOffset = prevNoteEvent.getEndTick(); int note = prevNoteEvent.getNote(); // FIXME: 最後の1つのrだけに細工すればよいね. if (mabiTempo) { // マビノギ補正(rrrT***N の処理 MMLTicks ticks = new MMLTicks("c", tickLength, false); if (prevNoteEvent.getVelocity() != 0) { sb.append("v0"); } sb.append(ticks.toMMLText()); prevNoteEvent = new MMLNoteEvent(note, tickLength, tickOffset, 0); } else { MMLTicks ticks = new MMLTicks("r", tickLength, false); prevNoteEvent = new MMLNoteEvent(prevNoteEvent.getNote(), tickLength, tickOffset, prevNoteEvent.getVelocity()); sb.append(ticks.toMMLText()); } } sb.append(tempoEvent.toMMLString()); return prevNoteEvent; } private void insertNoteWithTempo(StringBuilder sb, LinkedList<MMLTempoEvent> localTempoList, MMLNoteEvent prevNoteEvent, MMLNoteEvent noteEvent, boolean withTempo, boolean mabiTempo) throws UndefinedTickException { MMLNoteEvent divNoteEvent = noteEvent.clone(); // endTickOffsetがTempoを跨いでいたら、'&'でつなげる. (withTempoのみ) while ( withTempo && (!localTempoList.isEmpty()) && (divNoteEvent.getTickOffset() < localTempoList.getFirst().getTickOffset()) && (localTempoList.getFirst().getTickOffset() < divNoteEvent.getEndTick()) ) { int tick = localTempoList.getFirst().getTickOffset() - divNoteEvent.getTickOffset(); MMLNoteEvent partNoteEvent = new MMLNoteEvent(divNoteEvent.getNote(), tick, divNoteEvent.getTickOffset(), divNoteEvent.getVelocity()); sb.append( partNoteEvent.toMMLString(prevNoteEvent) ); if (withTempo) { sb.append( localTempoList.getFirst().toMMLString() ); } localTempoList.removeFirst(); divNoteEvent.setTick(divNoteEvent.getTick() - tick); divNoteEvent.setTickOffset(divNoteEvent.getTickOffset() + tick); prevNoteEvent = partNoteEvent; if (withTempo && mabiTempo) { divNoteEvent.setVelocity(0); } else if (divNoteEvent.getTick() > 0) { sb.append('&'); } } if (divNoteEvent.getTick() > 0){ sb.append( divNoteEvent.toMMLString(prevNoteEvent) ); } if (noteEvent.getVelocity() != divNoteEvent.getVelocity()) { sb.append("v"+noteEvent.getVelocity()); } } /** * テンポ出力を行うかどうかを指定してMML文字列を作成する. * TODO: 長いなぁ。 * @param withTempo trueを指定すると、tempo指定を含むMMLを返します. * @param totalTick 最大tick長. これに満たない場合は、末尾を休符分で埋めます. * @param mabiTempo MML for mabi * @return * @throws UndefinedTickException */ public String toMMLString(boolean withTempo, int totalTick, boolean mabiTempo) throws UndefinedTickException { // テンポ LinkedList<MMLTempoEvent> localTempoList = new LinkedList<>(tempoList); StringBuilder sb = new StringBuilder(); // initial note: octave 4, tick 0, offset 0, velocity 8 MMLNoteEvent prevNoteEvent = new MMLNoteEvent(12*4, 0, 0, MMLNoteEvent.INIT_VOL); for (MMLNoteEvent noteEvent : noteList) { // テンポのMML挿入判定 while ( (!localTempoList.isEmpty()) && (localTempoList.getFirst().getTickOffset() <= noteEvent.getTickOffset()) ) { if (withTempo) { // tempo挿入 (rrrT***N の処理) prevNoteEvent = insertTempoMML(sb, prevNoteEvent, localTempoList.getFirst(), mabiTempo); } localTempoList.removeFirst(); } insertNoteWithTempo(sb, localTempoList, prevNoteEvent, noteEvent, withTempo, mabiTempo); prevNoteEvent = noteEvent; } // テンポがまだ残っていれば、その分をつなげる. while (!localTempoList.isEmpty()) { MMLTempoEvent tempo = localTempoList.getFirst(); if (mabiTempo && (tempo.getTickOffset() >= totalTick)) { // mabi-MMLであれば, 不要な終端テンポは付けない. break; } if (withTempo) { // tempo挿入 (rrrT***N の処理) prevNoteEvent = insertTempoMML(sb, prevNoteEvent, tempo, mabiTempo); } localTempoList.removeFirst(); } return sb.toString(); } @Override public String toString() { return tempoList.toString() + noteList.toString(); } @Override public MMLEventList clone() { try { MMLEventList obj = (MMLEventList) super.clone(); obj.noteList = new ArrayList<>(); for (MMLNoteEvent note : noteList) { obj.noteList.add(note.clone()); } obj.tempoList = new ArrayList<>(); for (MMLTempoEvent tempo : tempoList) { obj.tempoList.add(tempo.clone()); } return obj; } catch (CloneNotSupportedException e) { throw new AssertionError(e.getMessage()); } } public int getAlignmentStartTick(MMLEventList list2, int tickOffset) { MMLNoteEvent target = list2.searchOnTickOffset(tickOffset); if ( (target == null) || (target.getTickOffset() == tickOffset) ) { return tickOffset; } else { return list2.getAlignmentStartTick(this, target.getTickOffset()); } } public int getAlignmentEndTick(MMLEventList list2, int endTick) { MMLNoteEvent target = list2.searchOnTickOffset(endTick-1); if ( (target == null) || (target.getEndTick() == endTick) ) { return endTick; } else { return list2.getAlignmentEndTick(this, target.getEndTick()); } } public void swap(MMLEventList list2, int startTick, int endTick) { List<MMLNoteEvent> tmp1 = new ArrayList<>(); List<MMLNoteEvent> tmp2 = new ArrayList<>(); for (MMLNoteEvent noteEvent : noteList) { if ( (noteEvent.getTickOffset() >= startTick) && (noteEvent.getEndTick() <= endTick) ) { tmp1.add(noteEvent); } } for (MMLNoteEvent noteEvent : tmp1) { noteList.remove(noteEvent); } for (MMLNoteEvent noteEvent : list2.getMMLNoteEventList()) { if ( (noteEvent.getTickOffset() >= startTick) && (noteEvent.getEndTick() <= endTick) ) { tmp2.add(noteEvent); } } for (MMLNoteEvent noteEvent : tmp2) { list2.getMMLNoteEventList().remove(noteEvent); addMMLNoteEvent(noteEvent); } for (MMLNoteEvent noteEvent : tmp1) { list2.addMMLNoteEvent(noteEvent); } } public void move(MMLEventList list2, int startTick, int endTick) { List<MMLNoteEvent> tmp1 = new ArrayList<>(); for (MMLNoteEvent noteEvent : noteList) { if ( (noteEvent.getTickOffset() >= startTick) && (noteEvent.getEndTick() <= endTick) ) { tmp1.add(noteEvent); } } for (MMLNoteEvent noteEvent : tmp1) { deleteMMLEvent(noteEvent); list2.addMMLNoteEvent(noteEvent); } } public void copy(MMLEventList list2, int startTick, int endTick) { for (MMLNoteEvent noteEvent : noteList) { if ( (noteEvent.getTickOffset() >= startTick) && (noteEvent.getEndTick() <= endTick) ) { list2.addMMLNoteEvent( noteEvent.clone() ); } } } @Override public boolean equals(Object obj) { if (!(obj instanceof MMLEventList)) { return false; } MMLEventList eventList = (MMLEventList) obj; if ( Arrays.equals(this.noteList.toArray(), eventList.noteList.toArray()) && Arrays.equals(this.tempoList.toArray(), eventList.tempoList.toArray()) ) { return true; } return false; } }