/* -*- c-basic-offset: 2; indent-tabs-mode: nil; -*- */ /* * FreeDots -- MusicXML to braille music transcription * * Copyright 2008-2010 Mario Lang All Rights Reserved. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3, as * published by the Free Software Foundation. * * This code 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 General Public License * for more details (a copy is included in the LICENSE.txt file that * accompanied this code). * * You should have received a copy of the GNU General Public License * along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * This file is maintained by Mario Lang <mlang@delysid.org>. */ package freedots.musicxml; import java.util.HashMap; import java.util.Map; import java.util.Set; import javax.sound.midi.InvalidMidiDataException; import javax.sound.midi.MetaMessage; import javax.sound.midi.MidiEvent; import javax.sound.midi.ShortMessage; import javax.sound.midi.Track; import freedots.math.AbstractFraction; import freedots.math.Fraction; import freedots.music.AccidentalContext; import freedots.music.Articulation; import freedots.music.EndBar; import freedots.music.Event; import freedots.music.GlobalKeyChange; import freedots.music.KeyChange; import freedots.music.KeySignature; import freedots.music.MusicList; import freedots.music.Ornament; import freedots.music.StartBar; import freedots.music.Syllabic; import freedots.playback.MetaEventRelay; /** Converts MusicXML objects ({@link Score}, {@link Note}) to a representation * suitable for consumption by the Java Sound API. * <p> * As a special extension, {@link MetaEventRelay} can be used to embed * references to the underlying objects in the MIDI sequence. * @see freedots.playback.PlaybackObserver */ public final class MIDISequence extends javax.sound.midi.Sequence { private final Fraction pulseDuration; private boolean unroll = true; /** Some MIDI reading applications do prefer if all tempo changes are bundled * in the first track of the file. */ private Track tempoTrack; private MetaEventRelay metaEventRelay; private int velocity = 64; private Map<Integer, AccidentalContext> accidentalContexts; /** Converts a score to its unrolled MIDI representation. * @param score is the score to convert to MIDI messages * @throws InvalidMidiDataException if the conversion process generated * invalid data. */ public MIDISequence(final Score score) throws InvalidMidiDataException { this(score, true, null); } /** Converts a score to MIDI. * @param score is the score to convert to MIDI representation * @param unroll indicates if repeats and alternative endings should * be honoured * @param metaEventRelay factory for object reference meta message creation * @throws InvalidMidiDataException upon an unexpected conversion error */ public MIDISequence(final Score score, final boolean unroll, final MetaEventRelay metaEventRelay) throws InvalidMidiDataException { super(PPQ, calculatePPQ(score.getDivisions())); this.pulseDuration = QUARTER.divide(resolution); this.unroll = unroll; this.metaEventRelay = metaEventRelay; tempoTrack = createTrack(); for (Part part: score.getParts()) createTrack(part); } /** Converts a single note to MIDI. * @param note is the note object to convert * @throws InvalidMidiDataException if the conversion unexpectedly failed */ public MIDISequence(final Note note) throws InvalidMidiDataException { super(PPQ, calculatePPQ(note.getPart().getScore().getDivisions())); this.pulseDuration = QUARTER.divide(resolution); Track track = createTrack(); initializeMidiPrograms(track, note.getPart()); addToTrack(track, note, note.getMoment().negate()); } private Track createTrack(Part part) throws InvalidMidiDataException { final Track track = createTrack(); MetaMessage metaMessage; velocity = 64; if (part.getName() != null) { track.add(TextMessage.TrackName.createEvent(part.getName(), 0)); } initializeMidiPrograms(track, part); MusicList events = part.getMusicList(); { int staffCount = events.getStaffCount(); accidentalContexts = new HashMap<Integer, AccidentalContext>(); for (int i = 0; i < staffCount; i++) { accidentalContexts.put(new Integer(i), new AccidentalContext(part.getKeySignature())); } } int round = 1; int repeatStartIndex = -1; Fraction offset = Fraction.ZERO; for (int i = 0; i < events.size(); i++) { Event event = events.get(i); if (event instanceof Direction) { Direction direction = (Direction)event; if (direction.getSound() != null) event = direction.getSound(); final int tick = toInteger(direction.getMoment().add(offset)); if (direction.isPedalPress()) { track.add(new MidiEvent(createPedalMessage(0, true), tick)); } else if (direction.isPedalRelease()) { track.add(new MidiEvent(createPedalMessage(0, false), tick)); } } if (event instanceof GlobalKeyChange) { GlobalKeyChange globalKeyChange = (GlobalKeyChange)event; KeySignature keySignature = globalKeyChange.getKeySignature(); for (int j = 0; j < accidentalContexts.size(); j++) { accidentalContexts.get(j).setKeySignature(keySignature); } } else if (event instanceof KeyChange) { KeyChange keyChange = (KeyChange)event; accidentalContexts.get(keyChange.getStaffNumber()).setKeySignature(keyChange.getKeySignature()); } else if (event instanceof Note) { addToTrack(track, (Note)event, offset); } else if (event instanceof Chord) { MetaEventRelay temp = null; if (metaEventRelay != null) { int midiTick = toInteger(event.getMoment().add(offset)); metaMessage = metaEventRelay.createMetaMessage((Chord)event); track.add(new MidiEvent(metaMessage, midiTick)); temp = metaEventRelay; metaEventRelay = null; // Only notify once for a chord } for (Note note: (Chord)event) addToTrack(track, note, offset); if (temp != null) metaEventRelay = temp; } else if (event instanceof Sound) { Sound sound = (Sound)event; MetaMessage tempoMessage = sound.getTempoMessage(); if (tempoMessage != null) { int midiTick = toInteger(sound.getMoment().add(offset)); tempoTrack.add(new MidiEvent(tempoMessage, midiTick)); } Integer newVelocity = sound.getMidiVelocity(); if (newVelocity != null) { velocity = newVelocity; } } else if (event instanceof StartBar) { if (repeatStartIndex == -1) repeatStartIndex = i; for (int j = 0; j < accidentalContexts.size(); j++) { accidentalContexts.get(j).resetToKeySignature(); } StartBar startBar = (StartBar)event; if (unroll) { if (startBar.getRepeatForward()) { repeatStartIndex = i; round = 1; } if (startBar.getEndingStart() > 0 && startBar.getEndingStart() != round) { /* skip to EndBar */ for (int j = i + 1; j < events.size(); j++) { if (events.get(j) instanceof EndBar) { EndBar endBar = (EndBar)events.get(j); if (endBar.getEndingStop() == startBar.getEndingStart()) { offset = offset.subtract(endBar.getMoment().subtract(startBar.getMoment())); i = j + 1; break; } } } } } } else if (event instanceof EndBar) { EndBar endbar = (EndBar)event; if (unroll && endbar.getRepeat()) { if (round == 1) { StartBar repeatStart = (StartBar)events.get(repeatStartIndex); offset = offset.add(endbar.getMoment().subtract(repeatStart.getMoment())); i = repeatStartIndex; round += 1; } } } } return track; } private void addToTrack(Track track, Note note, Fraction add) throws InvalidMidiDataException { int offset = toInteger(note.getMoment().add(add)); if (note.getLyric() != null && !note.getLyric().getText().isEmpty()) { String text = note.getLyric().getText(); if (note.getLyric().getSyllabic() == Syllabic.SINGLE || note.getLyric().getSyllabic() == Syllabic.END) { text += " "; } track.add(TextMessage.Lyric.createEvent(text, offset)); } if (!note.isGrace()) { Pitch pitch = note.getPitch(); int duration = toInteger(note.getDuration()); final Set<Articulation> articulations = note.getArticulations(); if (articulations.contains(Articulation.staccatissimo)) { duration /= 4; } else if (articulations.contains(Articulation.staccato)) { duration /= 2; } else if (articulations.contains(Articulation.mezzoStaccato)) { duration -= duration / 4; } final Set<Ornament> ornaments = note.getOrnaments(); if (metaEventRelay != null) { MetaMessage metaMessage = metaEventRelay.createMetaMessage(note); if (metaMessage != null) { track.add(new MidiEvent(metaMessage, offset)); } } if (pitch != null) { if (accidentalContexts != null) { accidentalContexts.get(note.getStaffNumber()) .accept(pitch, note.getAccidental()); } final int midiPitch = pitch.getMIDIPitch(); final int midiChannel = note.getMidiChannel(); if (accidentalContexts != null) { Integer staffNumber = new Integer(note.getStaffNumber()); AccidentalContext accidentalContext = accidentalContexts.get(staffNumber); if (ornaments.contains(Ornament.turn)) { final int upperPitch = pitch.nextStep(accidentalContext).getMIDIPitch(); final int lowerPitch = pitch.previousStep(accidentalContext).getMIDIPitch(); duration /= 4; noteOnOff(track, midiChannel, upperPitch, velocity, offset, duration); offset += duration; noteOnOff(track, midiChannel, midiPitch, velocity, offset, duration); offset += duration; noteOnOff(track, midiChannel, lowerPitch, velocity, offset, duration); offset += duration; noteOnOff(track, midiChannel, midiPitch, velocity, offset, duration); return; } else if (ornaments.contains(Ornament.mordent)) { final int lowerPitch = pitch.previousStep(accidentalContext).getMIDIPitch(); duration /= 8; noteOnOff(track, midiChannel, midiPitch, velocity, offset, duration); offset += duration; noteOnOff(track, midiChannel, lowerPitch, velocity, offset, duration); offset += duration; noteOnOff(track, midiChannel, midiPitch, velocity, offset, duration*6); return; } } noteOnOff(track, note.getMidiChannel(), midiPitch, velocity, offset, duration); } } } private static void noteOnOff(Track track, int channel, int pitch, int velocity, int tick, int duration) throws InvalidMidiDataException { ShortMessage msg = new ShortMessage(); msg.setMessage(ShortMessage.NOTE_ON, channel, pitch, velocity); track.add(new MidiEvent(msg, tick)); msg = new ShortMessage(); msg.setMessage(ShortMessage.NOTE_OFF, channel, pitch, 0); track.add(new MidiEvent(msg, tick+duration)); } private static int calculatePPQ(int ppq) { if (ppq < 5) return ppq * 80; else if (ppq < 10) return ppq * 40; else if (ppq < 20) return ppq * 20; else if (ppq < 40) return ppq * 10; else if (ppq < 100) return ppq * 4; return ppq; } private static void initializeMidiPrograms(Track track, Part part) throws InvalidMidiDataException { MidiInstrument instrument = part.getMidiInstrument(null); if (instrument != null) { ShortMessage msg = new ShortMessage(); msg.setMessage(ShortMessage.PROGRAM_CHANGE, instrument.getMidiChannel(), instrument.getMidiProgram(), 0); track.add(new MidiEvent(msg, 0)); } } private static ShortMessage createPedalMessage(final int channel, final boolean press) throws InvalidMidiDataException { ShortMessage msg = new ShortMessage(); msg.setMessage(ShortMessage.CONTROL_CHANGE, channel, 64, press? 127: 0); return msg; } protected enum TextMessage { General(1), Copyright(2), TrackName(3), Instrument(4), Lyric(5), Marker(6), CuePoint(7), ProgramName(8), DeviceName(9); private final int type; TextMessage(final int type) { this.type = type; } MidiEvent createEvent(final String data, int offset) throws InvalidMidiDataException { final MetaMessage message = new MetaMessage(); message.setMessage(type, data.getBytes(), data.length()); return new MidiEvent(message, offset); } } protected int toInteger(final AbstractFraction duration) { AbstractFraction value = duration.divide(pulseDuration); assert value.denominator() == 1; return value.intValue(); } protected static final Fraction QUARTER = new Fraction(1, 4); }