/* * JFugue, an Application Programming Interface (API) for Music Programming * http://www.jfugue.org * * Copyright (C) 2003-2014 David Koelle * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jfugue.midi; import org.jfugue.theory.Note; import java.util.logging.Logger; import jp.kshoji.javax.sound.midi.InvalidMidiDataException; import jp.kshoji.javax.sound.midi.MetaMessage; import jp.kshoji.javax.sound.midi.MidiEvent; import jp.kshoji.javax.sound.midi.Sequence; import jp.kshoji.javax.sound.midi.ShortMessage; import jp.kshoji.javax.sound.midi.SysexMessage; import jp.kshoji.javax.sound.midi.Track; /** * Places musical data into the MIDI sequence. * Package scope, final class. * *@author David Koelle */ final class MidiEventManager extends TrackTimeManager { private Sequence sequence; private Track[] track; private float divisionType; private int resolutionTicksPerBeat; private int tempoBeatsPerMinute; private float mpqn; private byte metronomePulse; private byte thirtysecondNotesPer24MidiClockSignals; private Logger logger = Logger.getLogger("org.jfugue"); public MidiEventManager() { super(); setDefaults(); } public MidiEventManager(float divisionType, int resolution) { super(); this.divisionType = divisionType; this.resolutionTicksPerBeat = resolution; } private void setDefaults() { sequence = null; track = new Track[MidiDefaults.TRACKS]; divisionType = MidiDefaults.DEFAULT_DIVISION_TYPE; resolutionTicksPerBeat = MidiDefaults.DEFAULT_RESOLUTION_TICKS_PER_BEAT; tempoBeatsPerMinute = MidiDefaults.DEFAULT_TEMPO_BEATS_PER_MINUTE; mpqn = 60000000 / MidiDefaults.DEFAULT_TEMPO_BEATS_PER_MINUTE; // MPQN = Milliseconds per quarter note metronomePulse = MidiDefaults.DEFAULT_METRONOME_PULSE; thirtysecondNotesPer24MidiClockSignals = MidiDefaults.DEFAULT_THIRTYSECOND_NOTES_PER_24_MIDI_CLOCK_SIGNALS; // Default value } public void reset() throws InvalidMidiDataException { setDefaults(); this.sequence = new Sequence(divisionType, resolutionTicksPerBeat); createTrack((byte)0); this.tempoBeatsPerMinute = MidiDefaults.DEFAULT_TEMPO_BEATS_PER_MINUTE; } @Override protected void createTrack(byte track) { super.createTrack(track); this.track[track] = sequence.createTrack(); } public void setDivisionType(float divisionType) { this.divisionType = divisionType; } public float getDivisionType() { return this.divisionType; } public void setResolution(int resolution) { this.resolutionTicksPerBeat = resolution; } public void setTempo(int tempoBPM) { this.tempoBeatsPerMinute = tempoBPM; this.mpqn = 60000000 / tempoBPM; // MPQN = microseconds per minute / BPM // Tempo is set in terms of microseconds per quarter note (MPQN), encoded in three big-endian bytes. byte[] bytes = new byte[3]; bytes[0] = (byte)((int)mpqn >> 16); bytes[1] = (byte)((int)mpqn >> 8); bytes[2] = (byte)((int)mpqn); this.addMetaMessage(0x51, bytes); } public void setTimeSignature(byte beatsPerMeasure, byte durationForBeat) { // Denominator passed to meta message is actually the power by which 2 must be raised to equal the // duration of a beat. Example: Given a time signature of 5/8, we must pass 3, because 2^3 = 8. byte d2 = (byte)(Math.log(durationForBeat) / Math.log(2)); this.addMetaMessage(MidiDefaults.META_TIMESIG, new byte[] { beatsPerMeasure, d2, getMetronomePulse(), get32ndNotesPer24MidiClockSignals() } ); } /* MIDI-Specific Settings */ public void setMetronomePulse(byte metronomePulse) { this.metronomePulse = metronomePulse; } public byte getMetronomePulse() { return this.metronomePulse; } public void set32ndNotesPer24MidiClockSignals(byte t) { this.thirtysecondNotesPer24MidiClockSignals = t; } public byte get32ndNotesPer24MidiClockSignals() { return this.thirtysecondNotesPer24MidiClockSignals; } public void setSequenceResolution(float divisionType, int resolution) { this.setDivisionType(divisionType); this.setResolution(resolution); } public float getSequenceDivisionType() { return this.divisionType; } public int getSequenceResolution() { return this.resolutionTicksPerBeat; } /** * Finishes the sequence by adding an End of Track meta message (0x2F) to each track * that has been used in this sequence. */ public void finishSequence() { MetaMessage message = new MetaMessage(); try { message.setMessage(0x2F, null, 0); for (byte i=0; i < getLastCreatedTrack(); i++) { if (track[i] != null) { track[i].add(new MidiEvent(message, convertBeatsToTicks(getLatestTrackBeatTime(i)))); } } } catch (InvalidMidiDataException e) { // We know what's going into this message. This exception won't happen. logger.warning(e.getMessage()); } } /** * Adds a MetaMessage to the current track. * * @param type the type of the MetaMessage * @param bytes the data of the MetaMessage */ public void addMetaMessage(int type, byte[] bytes) { try { MetaMessage message = new MetaMessage(); message.setMessage(type, bytes, bytes.length); MidiEvent event = new MidiEvent(message, convertBeatsToTicks(getTrackBeatTime())); track[getCurrentTrack()].add(event); } catch (InvalidMidiDataException e) { // We've kept a good eye on the data. This exception won't happen. logger.warning(e.getMessage()); } } /** * Adds a SysexMessage to the current track. * * @param bytes the data of the SysexMessage */ public void addSystemExclusiveEvent(byte[] bytes) { try { SysexMessage message = new SysexMessage(); message.setMessage(bytes, bytes.length); MidiEvent event = new MidiEvent(message, convertBeatsToTicks(getTrackBeatTime())); track[getCurrentTrack()].add(event); } catch (InvalidMidiDataException e) { // We've kept a good eye on the data. This exception won't happen. logger.warning(e.getMessage()); } } /** * Adds a MIDI event to the current track. * * @param command the MIDI command represented by this message * @param data1 the first data byte */ public void addEvent(int command, int data1) { try { ShortMessage message = new ShortMessage(); message.setMessage(command, getCurrentTrack(), data1); track[getCurrentTrack()].add(new MidiEvent(message, convertBeatsToTicks(getTrackBeatTime()))); } catch (InvalidMidiDataException e) { // We've kept a good eye on the data. This exception won't happen. logger.warning(e.getMessage()); } } /** * Adds a MIDI event to the current track. * * @param command the MIDI command represented by this message * @param data1 the first data byte * @param data2 the second data byte */ public void addEvent(int command, int data1, int data2) { try { if (track[getCurrentTrack()] == null) { track[getCurrentTrack()] = sequence.createTrack(); } track[getCurrentTrack()].add(new MidiEvent(createShortMessage(command, data1, data2), convertBeatsToTicks(getTrackBeatTime()))); } catch (InvalidMidiDataException e) { // We've kept a good eye on the data. This exception won't happen. logger.warning(e.getMessage()); } } private ShortMessage createShortMessage(int status,int data1, int data2) throws InvalidMidiDataException { ShortMessage message = new ShortMessage(); message.setMessage(status, getCurrentTrack(), data1, data2); return message; } public void addNote(Note note) { if (note.getDuration() == 0.0) { note.useDefaultDuration(); } // If this is the first note in a sequence of harmonic or melodic notes, remember what time it is. if (note.isFirstNote()) { setInitialNoteBeatTimeForHarmonicNotes(getTrackBeatTime()); } // If we're going to the next sequence in a parallel note situation, roll back the time to the beginning of the first note. // A note will never be a parallel note if a first note has not happened first. if (note.isHarmonicNote()) { setTrackBeatTime(getInitialNoteBeatTimeForHarmonicNotes()); } // If the note is a rest, simply advance the track time and get outta here if (note.isRest()) { advanceTrackBeatTime(note.getDuration()); return; } // Add a NOTE_ON event. // If the note is continuing a tie, it is already sounding, and there is not need to turn the note on if (!note.isEndOfTie()) { addEvent(ShortMessage.NOTE_ON, note.getValue(), note.getOnVelocity()); } // Advance the track timer advanceTrackBeatTime(note.getDuration()); // Add a NOTE_OFF event. // If this note is the start of a tie, the note will continue to sound, so we don't want to turn it off. if (!note.isStartOfTie()) { addEvent(ShortMessage.NOTE_OFF, note.getValue(), note.getOffVelocity()); } } private long convertBeatsToTicks(double beats) { return (long)(resolutionTicksPerBeat * beats * MidiDefaults.DEFAULT_TEMPO_BEATS_PER_WHOLE); } /** * Returns the current sequence, which is a collection of tracks. * If your goal is to add events to the sequence, you don't want to use this method to * get the sequence; instead, use the addEvent methods to add your events. * @return the current sequence */ public Sequence getSequence() { return this.sequence; } }