package com.xenoage.zong.io.midi.out; import com.xenoage.zong.core.music.MusicElementType; import com.xenoage.zong.core.music.clef.ClefType; import com.xenoage.zong.core.music.key.Key; import com.xenoage.zong.core.music.key.TraditionalKey; import com.xenoage.zong.core.music.time.TimeType; import com.xenoage.zong.io.midi.out.time.TimeMap; import lombok.val; import static com.xenoage.utils.math.MathUtils.clamp; import static com.xenoage.utils.math.MathUtils.log2; /** * Factory for an {@link MidiSequence} instance. * It has to be subclassed to support platform-specific sequence data. * * @param <T> the platform-specific sequence class * * @author Andreas Wenger */ public abstract class MidiSequenceWriter<T> { //controller numbers protected static final int controllerVolume = 7; protected static final int controllerPan = 10; //short message commands - TIDY: not compatible with AndroidMidiSequenceWriter, since //the order of the bytes is reversed. Can we find a platform independent representation? protected static final int commandProgramChange = 0xC0; //192 protected static final int commandControlChange = 0xB0; //176 protected static final int commandNoteOn = 0x90; //144 protected static final int commandNoteOff = 0x80; //128 //meta message types protected static final int typeTempo = 0x51; //81 protected static final int typeClef = 0x57; //87 TODO: really? no docs found! protected static final int typeTime = 0x58; //88 protected static final int typeKey = 0x59; //89 /** * Initializes a MIDI sequence. * @param tracksCount the number of tracks in the sequence * @param resolutionPpq resolution in ticks ("pulses") per quarter note */ public abstract void init(int tracksCount, int resolutionPpq); /** * Writes a MIDI program change. * @param track the index of the track where to write the event * @param channel the channel which is affected by the program change * @param tick the time of the event * @param program the MIDI program to change to */ public void writeProgramChange(int track, int channel, long tick, int program) { writeShortMessage(track, channel, tick, commandProgramChange, program, 0); } /** * Writes a MIDI volume change. * @param track the index of the track where to write the event * @param channel the channel which is affected by the volume change * @param tick the time of the event * @param volume the volume between 0 (silent) and 1 (full) */ public void writeVolumeChange(int track, int channel, long tick, float volume) { writeShortMessage(track, channel, tick, commandControlChange, controllerVolume, (int) (127 * volume)); } /** * Writes a MIDI pan change. * @param track the index of the track where to write the event * @param channel the channel which is affected by the pan change * @param tick the time of the event * @param pan the panning between -1 (left) and 1 (right) */ public void writePanChange(int track, int channel, long tick, float pan) { writeShortMessage(track, channel, tick, commandControlChange, controllerPan, (int) (64 + (63 * pan))); } /** * Writes a MIDI control change with the given data. * @param track the index of the track where to write the event * @param channel the channel which is affected by the pan change * @param tick the time of the event * @param data1 the first data byte * @param data2 the second data byte */ public void writeControlChange(int track, int channel, long tick, int data1, int data2) { writeShortMessage(track, channel, tick, commandControlChange, data1, data2); } /** * Writes a MIDI note on or off. * @param track the index of the track where to write the event * @param channel the channel where to play the note * @param tick the time of the event * @param note the MIDI note * @param on true for note on, false for note off * @param velocity velocity of the note event */ public void writeNote(int track, int channel, long tick, int note, boolean on, int velocity) { writeShortMessage(track, channel, tick, on ? commandNoteOn : commandNoteOff, note, velocity); } /** * Writes a MIDI tempo change. * @param track the index of the track where to write the event * @param tick the time of the event * @param bpm the new tempo in beats per minute */ public void writeTempoChange(int track, long tick, int bpm) { byte[] data = toByteArray(getMicrosecondsPerBeat(bpm)); writeMetaMessage(track, tick, typeTempo, data); } /** * Writes a MIDI key signature. Only {@link TraditionalKey} is supported. * @param track the index of the track where to write the event * @param tick the time of the event * @param key The {@link Key} to write */ public void writeKey(int track, long tick, Key key) { if (key.getMusicElementType() == MusicElementType.TraditionalKey) { val tradKey = (TraditionalKey) key; byte fifths = (byte) tradKey.getFifths(); byte mode = (byte) (tradKey.getMode() == TraditionalKey.Mode.Minor ? 1 : 0); //minor, or (everything else) major byte[] data = { fifths, mode }; writeMetaMessage(track, tick, typeKey, data); } } /** * Writes a MIDI time signature. * @param track the index of the track where to write the event * @param tick the time of the event * @param time the {@link TimeType} to write * @param resolution the resolution in ticks per quarter note */ public void writeTimeSignature(int track, long tick, TimeType time, int resolution) { if (time.getDenominator() > 0) { byte nom = (byte) time.getNumerator(); byte den = getDenominatorExponent(time.getDenominator()); byte res1 = (byte) (resolution * 4 / time.getDenominator()); //1 beat per quarter note byte res2 = (byte) 8; //32nd notes per beat: 8, since we defined 1 beat = 1 quarter note byte[] data = {nom, den, res1, res2}; writeMetaMessage(track, tick, typeTime, data); } } /** * Writes a MIDI clef, when possible. * TODO: Found only documentation in the "Beyond MIDI" book, but without event type id. * @param track the index of the track where to write the event * @param tick the time of the event * @param clef the {@link ClefType} to write */ public void writeClef(int track, long tick, ClefType clef) { //clef type byte cl = -1; switch (clef.getSymbol()) { case C: cl = 0; break; case G: cl = 1; break; case F: cl = 2; break; case PercTwoRects: case PercEmptyRect: cl = 3; break; } if (cl != -1) { byte li = (byte) clamp(clef.getLp() * 2 + 1, 1, 5); //line number from bottom byte oc = (byte) clef.getSymbol().octaveChange; byte[] data = {cl, li, oc}; writeMetaMessage(track, tick, typeClef, data); } } /** * Writes a MIDI short message with the given data. * @param track the index of the track where to write the event * @param channel the channel which is affected by the pan change * @param tick the time of the event * @param command the MIDI short message command * @param data1 the first data byte * @param data2 the second data byte */ public abstract void writeShortMessage(int track, int channel, long tick, int command, int data1, int data2); /** * Writes a MIDI meta message with the given data. * @param track the index of the track where to write the event * @param tick the time of the event * @param type the type of the event * @param data the data bytes */ public abstract void writeMetaMessage(int track, long tick, int type, byte... data); /** * Gets the current length of the sequence in ticks. */ public abstract long getLength(); /** * Gets the position in microseconds of the given tick. * This method is aware of all tempo changes written so far. */ public abstract long tickToMicrosecond(long tick); /** * Converts the given value in beats per minutes into microseconds per beat. */ public static int getMicrosecondsPerBeat(int bpm) { final int msPerMinute = 60000000; return msPerMinute / bpm; } /** * Returns the last three bytes of the given integer. */ private static byte[] toByteArray(int val) { byte[] res = new byte[3]; res[0] = (byte) (val / 0x10000); res[1] = (byte) ((val - res[0] * 0x10000) / 0x100); res[2] = (byte) (val - res[0] * 0x10000 - res[1] * 0x100); return res; } /** * Gets the platform-specific sequence with the current state. */ protected abstract T getSequence(); /** * Returns a {@link MidiSequence} with the written data. * @param metronomeTrack the number of the metronome track, or null if no metronome track was created * @param timeMap the mapping between MIDI and score time. */ public MidiSequence<T> finish(Integer metronomeTrack, TimeMap timeMap) { return new MidiSequence<>(getSequence(), metronomeTrack, timeMap); } /** * MIDI saves the denominator of a time signature as the exponent of the power of 2. * For example, 8 = 2 ^ 3, thus, the denomiator for a x/8 time would be 3. * 0 means x/1 time, 1 means x/2 time, 2 means x/4 time, and so on. */ private byte getDenominatorExponent(int denominator) { return (byte) log2(denominator); } }