/*
* Computoser is a music-composition algorithm and a website to present the results
* Copyright (C) 2012-2014 Bozhidar Bozhanov
*
* Computoser is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Computoser is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Computoser. If not, see <http://www.gnu.org/licenses/>.
*/
package com.music.util.music;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.Vector;
import jm.JMC;
import jm.midi.MidiUtil;
import jm.midi.Track;
import jm.midi.event.EndTrack;
import jm.midi.event.Event;
import jm.midi.event.NoteOn;
import jm.midi.event.PChange;
import jm.midi.event.VoiceEvt;
import jm.music.data.Note;
import jm.music.data.Part;
import jm.music.data.Phrase;
import jm.music.data.Score;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Code copied from SMF and MidiParser, but without System.exit(..)
*
* @author bozho
*
*/
public final class SMFTools implements JMC {
private static final Logger logger = LoggerFactory.getLogger(SMFTools.class);
// --------------------------------------
// attributes
// --------------------------------------
/** The standard MIDI file type to be read or written */
@SuppressWarnings("unused")
private short fileType;
/** The current number of tracks stored by this Class */
private short numOfTracks;
/** Pulses per quarter note value */
private short ppqn;
/** list of Tracks contained within this SMF */
private Vector<Track> trackList;
public SMFTools() {
this((short) 1, (short) 480);
}
public SMFTools(short fileType, short ppqn) {
this.fileType = fileType;
this.ppqn = ppqn;
this.numOfTracks = 0;
this.trackList = new Vector<>();
}
// -----------------------------------------------------------
// Converts a SMF into jMusic Score data
// -----------------------------------------------------------
/**
* Convert a SMF into the jMusic data type
*/
@SuppressWarnings("rawtypes")
public static void SMFToScore(Score score, SMFTools smf) {
Enumeration<Track> en = smf.getTrackList().elements();
// Go through tracks
while (en.hasMoreElements()) {
Part part = new Part();
Track smfTrack = en.nextElement();
Vector evtList = smfTrack.getEvtList();
Vector phrVct = new Vector();
sortEvents(evtList, phrVct, smf, part);
for (int i = 0; i < phrVct.size(); i++) {
part.addPhrase((Phrase) phrVct.elementAt(i));
}
score.addPart(part);
score.clean();
}
}
@SuppressWarnings("rawtypes")
private static void sortEvents(Vector evtList, Vector phrVct, SMFTools smf, Part part) {
double startTime = 0.0;
double[] currentLength = new double[100];
Note[] curNote = new Note[100];
int phrIndex = 0;
// Go through evts
for (int i = 0; i < evtList.size(); i++) {
Event evt = (Event) evtList.elementAt(i);
startTime += (double) evt.getTime() / (double) smf.getPPQN();
if (evt.getID() == 007) {
PChange pchg = (PChange) evt;
part.setInstrument(pchg.getValue());
// if this event is a NoteOn event go on
} else if (evt.getID() == 005) {
NoteOn noteOn = (NoteOn) evt;
part.setChannel(noteOn.getMidiChannel());
short pitch = noteOn.getPitch();
int dynamic = noteOn.getVelocity();
short midiChannel = noteOn.getMidiChannel();
// if you're a true NoteOn
if (dynamic > 0) {
noteOn(phrIndex, curNote, smf, i, currentLength, startTime, phrVct, midiChannel, pitch,
dynamic, evtList);
}
}
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private static void noteOn(int phrIndex, Note[] curNote, SMFTools smf, int i, double[] currentLength,
double startTime, Vector phrVct, short midiChannel, short pitch, int dynamic, Vector evtList) {
phrIndex = -1;
// work out what phrase is ready to accept a note
for (int p = 0; p < phrVct.size(); p++) {
// Warning 0.02 should really be fixed
if (currentLength[p] <= (startTime + 0.08)) {
phrIndex = p;
break;
}
}
// need to create new phrase for a new voice?
if (phrIndex == -1) {
phrIndex = phrVct.size();
phrVct.addElement(new Phrase(startTime));
currentLength[phrIndex] = startTime;
}
// Do we need to add a rest ?
if ((startTime > currentLength[phrIndex]) && (curNote[phrIndex] != null)) {
double newTime = startTime - currentLength[phrIndex];
// perform a level of quantisation first
if (newTime < 0.25) {
double length = curNote[phrIndex].getRhythmValue();
curNote[phrIndex].setRhythmValue(length + newTime);
} else {
Note restNote = new Note(REST, newTime, 0);
restNote.setPan(midiChannel);
restNote.setDuration(newTime);
restNote.setOffset(0.0);
((Phrase) phrVct.elementAt(phrIndex)).addNote(restNote);
}
currentLength[phrIndex] += newTime;
}
// get end time
double time = MidiUtil.getEndEvt(pitch, evtList, i) / (double) smf.getPPQN();
// create the new note
Note tempNote = new Note(pitch, time, dynamic);
tempNote.setDuration(time);
curNote[phrIndex] = tempNote;
((Phrase) phrVct.elementAt(phrIndex)).addNote(curNote[phrIndex]);
currentLength[phrIndex] += curNote[phrIndex].getRhythmValue();
}
public Vector<Track> getTrackList() {
return this.trackList;
}
public short getPPQN() {
return this.ppqn;
}
public void clearTracks() {
if (!this.trackList.isEmpty()) {
// remove any previous tracks
this.trackList.removeAllElements();
}
}
// ------------------------------------------
// Read SMF
// ------------------------------------------
/**
* Read from a standard MIDI file
*
* @params InputStream - the datasource to read from
* @params Score score - the score to place jMusic data translation into
* @exception IOException
* - any IO problems
*/
public void read(InputStream is) throws IOException {
// Given the small size of MIDI files read all
// data into a ByteArrayStream for further processing
byte[] fileData = new byte[is.available()];
is.read(fileData);
ByteArrayInputStream bais = new ByteArrayInputStream(fileData);
DataInputStream dis = new DataInputStream(bais);
// clear any SMF data
if (!this.trackList.isEmpty()) {
this.trackList.removeAllElements(); // remove any previous tracks
}
// check header for MIDIfile validity
if (dis.readInt() != 0x4D546864) {// Check for MIDI track validity
throw new IOException("This is NOT a MIDI file !!!");
} else {// If MIDI file passes skip length bytes
dis.readInt(); // skip over Length info
}
// get SMF Class data
try {
fileType = dis.readShort();
this.numOfTracks = dis.readShort();
this.ppqn = dis.readShort();
} catch (IOException e) {
throw new IllegalStateException(e);
}
// skip the tempo track fro type 1 files
/*
* if(fileType == 1) { skipATrack(dis); numOfTracks--; }
*/
// Read all track chunks
for (int i = 0; i < numOfTracks; i++) {
readTrackChunk(dis);
}
is.close();
dis.close();
}
// ------------------------------------
// Write a SMF
// -------------------------------------
/**
* Write to a standard MIDI file
*
* @param OutputStream
* the datasource to write to
* @param Score
* score the Score to get data from
* @exception IOException
* did the write go ok
*/
public void write(OutputStream os) throws IOException {
// IO Stream stuff
DataOutputStream dos = new DataOutputStream(os);
// find number of tracks
this.numOfTracks = (short) trackList.size();
// write header chunk
try {
dos.writeInt(0x4D546864); // MThd
dos.writeInt(6); // Length
dos.writeShort(1); // Midi File Type
dos.writeShort(numOfTracks); // Number of tracks
dos.writeShort(ppqn); // Pulses Per Quarter Note
} catch (Exception e) {
e.printStackTrace();
}
// write all tracks
Enumeration<Track> en = trackList.elements();
while (en.hasMoreElements()) {
Track smfTrack = en.nextElement();
writeTrackChunk(dos, smfTrack);
}
os.flush();
os.close();
dos.flush();
dos.close();
}
/**
* Print all MIDI tracks and MIDI events
*/
public void print() {
Enumeration<Track> en = trackList.elements();
while (en.hasMoreElements()) {
Track track = en.nextElement();
track.print();
}
}
// ----------------------------------------
// SMF Track Reads and Writes
// ----------------------------------------
/**
* Reads a MIDI track chunk
*
* @param DataInputStream
* dis - the input stream to read from
* @exception IOException
*/
private void readTrackChunk(DataInputStream dis) throws IOException {
// local variables for Track class
Track track = new Track();
// Insert new Track into a list of tracks
this.trackList.addElement(track);
int deltaTime = 0;
// Read track header
if (dis.readInt() != 0x4D54726B) {// If MTrk read is wrong
throw new IOException("Track started in wrong place!!!! ABORTING");
} else {// If MTrk read ok get bytesRemaining
dis.readInt();
}
// loop variables
int status, oldStatus = 0, eventLength = 0;
// Start gathering event data
Event event = null;
while (true) {
try {
// get variable length timestamp
deltaTime = MidiUtil.readVarLength(dis);
// mark stream so we can return if we need running status
dis.mark(2);
status = dis.readUnsignedByte();
// decide on running status
if (status < 0x80) { // set running status
status = oldStatus;
// return stream to before status read
dis.reset();
}
// create default event of correct type
if (status >= 0xFF) { // Meta Event
int type = dis.readUnsignedByte();
eventLength = MidiUtil.readVarLength(dis);
event = jm.midi.MidiUtil.createMetaEvent(type);
} else if (status >= 0xF0) { // System Exclusive --- NOT
// SUPPORTED
eventLength = MidiUtil.readVarLength(dis);
} else if (status >= 0x80) { // MIDI voice event
short selection = (short) (status / 0x10);
short midiChannel = (short) (status - (selection * 0x10));
VoiceEvt evt = (VoiceEvt) MidiUtil.createVoiceEvent(selection);
if (evt == null) {
throw new IOException("MIDI file read error: invalid voice event type!");
}
evt.setMidiChannel(midiChannel);
event = evt;
}
oldStatus = status;
} catch (EOFException ex) {
logger.warn("EOFException (" + ex.getMessage() + ") encountered in SMFTools");
} catch (Exception e) {
throw new IllegalStateException(e);
}
if (event != null) {
// read data into the new event and
// add the new event to the Track object
event.setTime(deltaTime);
event.read(dis);
track.addEvent(event);
// event.print();
if (event instanceof EndTrack)
break;
} else {
// skip the stream ahead to next valid event
dis.skipBytes(eventLength);
}
}
}
/**
* Write the Track Chunk
*
* @param DataOutputStream
* dos
* @param Track
* track - track to write
* @exception IOException
*/
@SuppressWarnings("rawtypes")
private void writeTrackChunk(DataOutputStream odos, Track track) throws IOException {
// Write to temporary stream to buffer disk writes and
// calculate the number of bytes written to the stream
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
int header = 0x4D54726B;
Enumeration en = track.getEvtList().elements();
en = track.getEvtList().elements();
// At this stage Except that all events are NoteOn events
while (en.hasMoreElements()) {
Event evt = (Event) en.nextElement();
evt.write(dos);
if (DEBUG)
evt.print();
}
// Write to the real stream
odos.writeInt(header);
odos.writeInt(baos.size());
odos.write(baos.toByteArray(), 0, baos.size());
}
}