/*
* Copyright (C) 2013-2017 たんらる
*/
package fourthline.mmlTools;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import fourthline.mabiicco.midi.MabiDLS;
import fourthline.mmlTools.core.MMLTicks;
import fourthline.mmlTools.core.UndefinedTickException;
import fourthline.mmlTools.parser.IMMLFileParser;
import fourthline.mmlTools.parser.MMLParseException;
import fourthline.mmlTools.parser.MMSFile;
import fourthline.mmlTools.parser.SectionContents;
import fourthline.mmlTools.parser.TextParser;
/**
* Score
*/
public final class MMLScore implements IMMLFileParser {
private final LinkedList<MMLTrack> trackList = new LinkedList<>();
private final List<MMLTempoEvent> globalTempoList = new ArrayList<>();
private final List<Marker> markerList = new ArrayList<>();
public static final int MAX_TRACK = 16;
private String title = "";
private String author = "";
private int numTime = 4;
private int baseTime = 4;
/**
* 新たにトラックを追加します.
* @param track
* @return トラック数の上限を超えていて、追加できないときは -1. 追加できた場合は、追加したindex値を返します(0以上).
*/
public synchronized int addTrack(MMLTrack track) {
if (trackList.size() >= MAX_TRACK) {
return -1;
}
// トラックリストの末尾に追加
trackList.add(track);
int trackIndex = trackList.size() - 1;
// グローバルテンポリストの統合.
MMLTempoEvent.mergeTempoList(track.getGlobalTempoList(), globalTempoList);
track.setGlobalTempoList(globalTempoList);
return trackIndex;
}
/**
* 指定したindexのトラックを削除します.
* @param index
*/
public synchronized void removeTrack(int index) {
trackList.remove(index);
}
public synchronized void moveTrack(int fromIndex, int toIndex) {
MMLTrack mmlTrack = getTrack(fromIndex);
removeTrack(fromIndex);
trackList.add(toIndex, mmlTrack);
}
/**
* 保持しているトラックの数を返します.
* @return
*/
public int getTrackCount() {
return trackList.size();
}
/**
* 保持しているトラックリストを返します.
* @return MMLTrackの配列
*/
public synchronized List<MMLTrack> getTrackList() {
return trackList;
}
/**
* 指定したindexのトラックを返します.
* @param index
* @return
*/
public MMLTrack getTrack(int index) {
return trackList.get(index);
}
/**
* 指定されたindexにトラックをセットします.
* @param index
* @param track
*/
public void setTrack(int index, MMLTrack track) {
trackList.set(index, track);
// グローバルテンポリストの統合.
MMLTempoEvent.mergeTempoList(track.getGlobalTempoList(), globalTempoList);
track.setGlobalTempoList(globalTempoList);
}
public int getTempoOnTick(long tickOffset) {
return MMLTempoEvent.searchOnTick(globalTempoList, tickOffset);
}
public List<MMLTempoEvent> getTempoEventList() {
return globalTempoList;
}
public List<Marker> getMarkerList() {
return markerList;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitle() {
return this.title;
}
public void setAuthor(String author) {
this.author = author;
}
public String getAuthor() {
return this.author;
}
public void setBaseTime(String baseTime) {
String s[] = baseTime.split("/");
this.numTime = Integer.parseInt(s[0]);
this.baseTime = Integer.parseInt(s[1]);
}
public String getBaseTime() {
return numTime + "/" + baseTime;
}
public String getBaseOnly() {
return String.valueOf(baseTime);
}
public void setBaseOnly(int base) {
baseTime = base;
}
public int getTimeCountOnly() {
return numTime;
}
public void setTimeCountOnly(int value) {
numTime = value;
}
public int getMeasureTick() {
return (getTimeCountOnly() * getBeatTick());
}
public int getBeatTick() {
try {
return MMLTicks.getTick(getBaseOnly());
} catch (UndefinedTickException e) {
throw new AssertionError();
}
}
public void addTicks(int tickPosition, int tick) {
for (MMLTrack track : getTrackList()) {
for (MMLEventList eventList : track.getMMLEventList()) {
MMLEvent.insertTick(eventList.getMMLNoteEventList(), tickPosition, tick);
}
}
// テンポ
MMLEvent.insertTick(globalTempoList, tickPosition, tick);
// マーカー
MMLEvent.insertTick(markerList, tickPosition, tick);
}
public void removeTicks(int tickPosition, int tick) {
for (MMLTrack track : getTrackList()) {
for (MMLEventList eventList : track.getMMLEventList()) {
MMLEvent.removeTick(eventList.getMMLNoteEventList(), tickPosition, tick);
}
}
// テンポ
MMLEvent.removeTick(globalTempoList, tickPosition, tick);
// マーカー
MMLEvent.removeTick(markerList, tickPosition, tick);
}
public int getTotalTickLength() {
long tick = 0;
for (MMLTrack track : trackList) {
long currentTick = track.getMaxTickLength();
if (tick < currentTick) {
tick = currentTick;
}
}
return (int)tick;
}
/**
* @return (ms)
*/
public long getTotalTime() {
int totalTick = getTotalTickLength();
long totalTime = MMLTempoEvent.getTimeOnTickOffset(globalTempoList, totalTick);
return totalTime;
}
private String getTempoObj() {
if (globalTempoList.size() == 0) {
return "";
}
StringBuffer sb = new StringBuffer();
for (MMLTempoEvent t : globalTempoList) {
sb.append(',');
sb.append(t.toString());
}
return sb.substring(1);
}
private void putTempoObj(String s) {
if (s.length() > 0) {
String l[] = s.split(",");
globalTempoList.clear();
for (String str : l) {
MMLTempoEvent e = MMLTempoEvent.fromString(str);
if (e != null) {
e.appendToListElement(globalTempoList);
}
}
}
}
public byte[] getObjectState() {
ByteArrayOutputStream ostream = new ByteArrayOutputStream();
writeToOutputStream(ostream);
return ostream.toByteArray();
}
public void putObjectState(byte objState[]) {
try {
ByteArrayInputStream bis = new ByteArrayInputStream(objState);
parse(bis);
} catch (Exception e) {
e.printStackTrace();
}
}
public void writeToOutputStream(OutputStream outputStream) {
try {
PrintStream stream = new PrintStream(outputStream, false, "UTF-8");
stream.println("[mml-score]");
stream.println("version=1");
stream.println("title="+getTitle());
stream.println("author="+getAuthor());
stream.println("time="+getBaseTime());
stream.println("tempo="+getTempoObj());
for (MMLTrack track : trackList) {
stream.println("mml-track="+track.getOriginalMML());
stream.println("name="+track.getTrackName());
stream.println("program="+track.getProgram());
stream.println("songProgram="+track.getSongProgram());
stream.println("panpot="+track.getPanpot());
}
if (!markerList.isEmpty()) {
stream.println("[marker]");
for (Marker marker : markerList) {
stream.println(marker.toString());
}
}
stream.close();
} catch (UnsupportedEncodingException e) {}
}
public List<MMLNoteEvent[]> getNoteListOnTickOffset(long tick) {
ArrayList<MMLNoteEvent[]> noteListArray = new ArrayList<>();
for (MMLTrack track : this.getTrackList()) {
int partIndex = 0;
MMLNoteEvent noteList[] = new MMLNoteEvent[4];
for (MMLEventList eventList : track.getMMLEventList()) {
if (partIndex == 3) {
continue;
}
noteList[partIndex] = eventList.searchOnTickOffset(tick);
partIndex++;
}
noteListArray.add(noteList);
}
return noteListArray;
}
/**
* generateした結果が同じであれば, generateした状態のMMLScoreにする.
* @return
*/
public MMLScore toGeneratedScore() {
try {
MMLScore score = new MMLScore();
score.putObjectState( this.getObjectState() );
score.generateAll();
if ( Arrays.equals(this.getObjectState(), score.getObjectState()) ) {
return score;
}
} catch (UndefinedTickException e) {}
return this;
}
@Override
public MMLScore parse(InputStream istream) throws MMLParseException {
this.globalTempoList.clear();
this.trackList.clear();
this.markerList.clear();
List<SectionContents> contentsList = SectionContents.makeSectionContentsByInputStream(istream, "UTF-8");
if (contentsList.isEmpty()) {
throw(new MMLParseException());
}
for (SectionContents section : contentsList) {
if (section.getName().equals("[mml-score]")) {
parseMMLScore(section.getContents());
} else if (section.getName().equals("[marker]")) {
parseMarker(section.getContents());
}
}
return this;
}
/**
* parse [mml-score] contents
* @param contents
*/
private void parseMMLScore(String contents) {
TextParser.text(contents)
.pattern("mml-track=", t -> this.addTrack(new MMLTrack().setMML(t)) )
.pattern("name=", t -> this.trackList.getLast().setTrackName(t) )
.pattern("program=", t -> this.trackList.getLast().setProgram(Integer.parseInt(t)) )
.pattern("songProgram=", t -> this.trackList.getLast().setSongProgram(Integer.parseInt(t)) )
.pattern("panpot=", t -> this.trackList.getLast().setPanpot(Integer.parseInt(t)) )
.pattern("title=", this::setTitle )
.pattern("author=", this::setAuthor )
.pattern("time=", this::setBaseTime )
.pattern("tempo=", this::putTempoObj )
.parse();
}
/**
* parse [marker] contents
* @param contents
*/
private void parseMarker(String contents) {
markerList.clear();
for (String s : contents.split("\n")) {
// <tickOffset>=<name>
int index = s.indexOf('=');
if (index > 0) {
String tickString = s.substring(0, index);
String name = s.substring(index+1);
markerList.add( new Marker(name, Integer.parseInt(tickString)) );
}
}
}
public MMLScore generateAll() throws UndefinedTickException {
Stack<UndefinedTickException> exceptionStack = new Stack<>();
trackList.parallelStream().forEach(t -> {
try {
t.generate();
} catch (UndefinedTickException e) {
exceptionStack.push(e);
}
});
if (!exceptionStack.isEmpty()) {
throw exceptionStack.pop();
}
return this;
}
/**
* 移調する.
* @param transpose
*/
public void transpose(int transpose) {
MabiDLS dls = MabiDLS.getInstance();
for (MMLTrack track : trackList) {
// 移調ができる楽器の種類かを確認. 通常の打楽器は不可, シロフォンは可能.
if (dls.getInstByProgram(track.getProgram()).getType().allowTranspose()) {
for (MMLEventList eventList : track.getMMLEventList()) {
for (MMLNoteEvent note : eventList.getMMLNoteEventList()) {
note.setNote( note.getNote() + transpose );
}
}
}
}
}
public static void main(String args[]) {
try {
System.out.println(" --- parse sample.mms ---");
MMSFile mms = new MMSFile();
MMLScore score = mms.parse(new FileInputStream("sample.mms"));
score.writeToOutputStream(System.out);
System.out.println(" --- parse sample-version1.mmi ---");
score = new MMLScore();
score.parse(new FileInputStream("sample-version1.mmi"));
} catch (Exception e) {
e.printStackTrace();
}
}
}