annotate MidiPlayer.java @ 24:7102e646b223

Sort notes into onset order. Fix incorrect tatums caused by negative rest values.
author Jeremy Gow <jeremy.gow@gmail.com>
date Mon, 12 Nov 2012 22:32:44 +0000
parents 9fc8683b8fed
children 796b3e3e053f
rev   line source
m@0 1 /*=============================================================================
m@0 2 * File: MidiPlayer.java
m@0 3 * Author: Marcus Pearce <m.pearce@gold.ac.uk>
m@0 4 * Created: <2007-02-14 12:13:56 marcusp>
marcus@16 5 * Time-stamp: <2011-11-15 16:52:06 marcusp>
m@0 6 *=============================================================================
m@0 7 */
m@0 8
m@0 9 /*
m@0 10 * Based on:
m@0 11 * http://www.jsresources.org/examples/SimpleMidiPlayer.html
m@0 12 * http://www.jsresources.org/examples/DumpSequence.html
m@0 13 */
m@0 14
m@0 15 import java.io.File;
m@0 16 import java.io.IOException;
m@0 17 import java.util.ArrayList;
m@0 18
m@0 19 import javax.sound.midi.MidiSystem;
m@0 20 import javax.sound.midi.MidiDevice;
m@0 21 import javax.sound.midi.InvalidMidiDataException;
m@0 22 import javax.sound.midi.MidiUnavailableException;
m@0 23 import javax.sound.midi.MetaEventListener;
m@0 24 import javax.sound.midi.Sequence;
m@0 25 import javax.sound.midi.Sequencer;
m@0 26 import javax.sound.midi.Synthesizer;
m@0 27 import javax.sound.midi.MidiChannel;
m@0 28 import javax.sound.midi.Receiver;
m@0 29 import javax.sound.midi.Transmitter;
m@0 30 import javax.sound.midi.Track;
m@0 31 import javax.sound.midi.MidiEvent;
m@0 32 import javax.sound.midi.MidiMessage;
m@0 33 import javax.sound.midi.ShortMessage;
m@0 34 import javax.sound.midi.MetaMessage;
m@0 35 import javax.sound.midi.SysexMessage;
m@0 36 import javax.sound.midi.MetaMessage;
JShulver@22 37 import java.util.Arrays;
jeremy@24 38 import java.util.Iterator;
jeremy@24 39
jeremy@24 40
m@0 41 public class MidiPlayer {
m@0 42
m@0 43 /* variables */
m@0 44 private Sequencer sequencer = null;
m@0 45 private Synthesizer synthesizer = null;
m@0 46 private Sequence sequence = null;
jeremy@24 47 private ArrayList<Long> onsets, offsets = null;
jeremy@24 48 private ArrayList<Integer> pitches, velocities = null;
m@0 49 private int midiDevice = 0; /* 4 for usb midi device */
jeremy@24 50 private boolean debug;
m@0 51
m@0 52 /* accessors */
jeremy@24 53 public Sequencer getSequencer() { return sequencer; }
m@0 54
jeremy@24 55 /* Constructors */
jeremy@24 56 public MidiPlayer(String path, int deviceNumber, boolean d) {
m@0 57 File midiFile = new File(path);
m@1 58 midiDevice = deviceNumber;
jeremy@24 59 debug = d;
m@0 60
m@0 61 // Get sequence
m@0 62 try { sequence = MidiSystem.getSequence(midiFile); }
m@0 63 catch (InvalidMidiDataException e) {
m@0 64 e.printStackTrace();
m@0 65 System.exit(1);
m@0 66 }
m@0 67 catch (IOException e) {
m@0 68 e.printStackTrace();
m@0 69 System.exit(1);
m@0 70 }
m@0 71 // Get sequencer
m@0 72 try { sequencer = MidiSystem.getSequencer(); }
m@0 73 catch (MidiUnavailableException e) {
m@0 74 e.printStackTrace();
m@0 75 System.exit(1);
m@0 76 }
m@0 77 //sequencer.setTempoInBPM(bpm);
m@0 78 // Workaround bug in JDK
m@0 79 // sequencer.addMetaEventListener(new MetaEventListener() {
m@0 80 // public void meta(MetaMessage event) {
m@0 81 // if (event.getType() == 47) {
m@0 82 // sequencer.close();
m@0 83 // if (synthesizer != null) {
m@0 84 // synthesizer.close();
m@0 85 // }
m@0 86 // }
m@0 87 // }
m@0 88 // });
m@0 89 // Open sequencer
m@0 90 try { sequencer.open(); }
m@0 91 catch (MidiUnavailableException e) {
m@0 92 e.printStackTrace();
m@0 93 System.exit(1);
m@0 94 }
m@0 95 // Assign sequence to sequencer
m@0 96 try { sequencer.setSequence(sequence); }
m@0 97 catch (InvalidMidiDataException e) {
m@0 98 e.printStackTrace();
m@0 99 System.exit(1);
m@0 100 }
m@0 101 // Set up MIDI output for sequence
m@0 102 try {
m@0 103 MidiDevice.Info msinfo[] = MidiSystem.getMidiDeviceInfo();
m@0 104 for(int i = 0; i < msinfo.length; i++) {
m@0 105 MidiDevice.Info m = msinfo[i];
m@0 106 // System.out.println("Name: " + m.getName() +
m@0 107 // "; Vendor: " + m.getVendor() +
m@0 108 // "; Version: " + m.getVersion());
m@0 109 }
m@0 110 // synthesizer = MidiSystem.getSynthesizer();
m@0 111 MidiDevice synth = MidiSystem.getMidiDevice(msinfo[midiDevice]);
m@0 112
m@0 113 // Change the patch
m@0 114 // MidiChannel channels[] = synthesizer.getChannels();
m@0 115 // for (int i = 0; i < channels.length; i++)
m@0 116 // channels[i].programChange(65);
m@0 117
m@0 118 synth.open();
m@0 119 Receiver synthReceiver = synth.getReceiver();
m@0 120 Transmitter seqTransmitter = sequencer.getTransmitter();
m@0 121 seqTransmitter.setReceiver(synthReceiver);
m@0 122
m@0 123 }
m@0 124 catch (MidiUnavailableException e) {
m@0 125 e.printStackTrace();
m@0 126 }
m@0 127
m@0 128 // compute data from the MIDI file
jeremy@24 129
jeremy@24 130 /*
m@0 131 onsets = computeOnsets();
m@0 132 offsets = computeOffsets();
m@0 133 pitches = computePitches();
jeremy@24 134 */
jeremy@24 135 registerEvents();
m@0 136
jeremy@24 137 if (debug) {
jeremy@24 138 String divisionType;
jeremy@24 139 if (sequence.getDivisionType() == sequence.PPQ)
jeremy@24 140 divisionType = "ppq";
jeremy@24 141 else
jeremy@24 142 divisionType = "smpte";
jeremy@24 143 System.out.println("division type = " + divisionType +
jeremy@24 144 "; resolution = " + sequence.getResolution());
jeremy@24 145 }
m@0 146 }
m@0 147
m@0 148 /* number of microseconds per MIDI tick */
m@0 149 private double microsecondsPerTick() {
m@0 150 double seqTickLength = (float)sequence.getTickLength();
m@0 151 double seqMicrosecondLength = (float)sequence.getMicrosecondLength();
m@0 152 double microsecondsPerTick = seqMicrosecondLength / seqTickLength;
m@0 153 //System.out.println("seqTickLength = " + seqTickLength);
m@0 154 //System.out.println("seqMicrosecondLength = " + seqMicrosecondLength);
m@0 155 //System.out.println("microsecondsPerTick = " + microsecondsPerTick);
m@0 156 return microsecondsPerTick;
m@0 157 }
m@0 158
m@0 159 private long ticksToMicroseconds(long tick) {
m@0 160 double microsecondsPerTick = microsecondsPerTick();
m@0 161 double seconds = (double)tick * microsecondsPerTick;
m@0 162
m@0 163 return (long)(10 * Math.floor(seconds * 0.1));
m@0 164 }
m@0 165
m@0 166 /* compute a list of note onset times (microseconds) */
m@0 167 private ArrayList computeOnsets() {
m@0 168 ArrayList ons = new ArrayList();
m@0 169 Track[] tracks = sequence.getTracks();
m@0 170
m@0 171 for (int nTrack = 0; nTrack < tracks.length; nTrack++) {
m@0 172 Track track = tracks[nTrack];
m@0 173 for (int nEvent = 0; nEvent < track.size(); nEvent++) {
m@0 174 MidiEvent event = track.get(nEvent);
m@0 175 MidiMessage message = event.getMessage();
m@0 176 if (message instanceof ShortMessage &&
m@0 177 ((ShortMessage)message).getCommand() == ShortMessage.NOTE_ON) {
m@0 178 // System.out.println("onset in ticks = " + event.getTick()+
m@0 179 // "; onset in microseconds = " +
m@0 180 // ticksToMicroseconds(event.getTick()));
JShulver@22 181 //if the event does not have a velocity of zero (i.e. switching a note off)
JShulver@22 182 if(message.getMessage()[2] != 0) {
JShulver@22 183 ons.add(ticksToMicroseconds(event.getTick())); //THEN we can add the note
JShulver@22 184 }
m@0 185 }
m@0 186 }
m@0 187 }
m@0 188 return ons;
m@0 189 }
m@0 190
m@0 191 /* compute a list of note offset times (microseconds) */
m@0 192 private ArrayList computeOffsets() {
m@0 193 ArrayList offs = new ArrayList();
m@0 194 Track[] tracks = sequence.getTracks();
m@0 195
m@0 196 for (int nTrack = 0; nTrack < tracks.length; nTrack++) {
m@0 197 Track track = tracks[nTrack];
m@0 198 for (int nEvent = 0; nEvent < track.size(); nEvent++) {
m@0 199 MidiEvent event = track.get(nEvent);
m@0 200 MidiMessage message = event.getMessage();
m@0 201 if (message instanceof ShortMessage &&
m@0 202 ((ShortMessage)message).getCommand() == ShortMessage.NOTE_OFF) {
m@0 203 //System.out.println("offset in ticks = " + event.getTick()+
m@0 204 // "; microsecondsPerTick = " + microsecondsPerTick +
m@0 205 // "; offset in microseconds = " +
m@0 206 // (float)event.getTick() * microsecondsPerTick);
m@0 207 offs.add(ticksToMicroseconds(event.getTick()));
m@0 208 }
JShulver@22 209 //if we have not found a note off,
JShulver@22 210 else if (message instanceof ShortMessage
JShulver@22 211 && ((ShortMessage)message).getCommand() == ShortMessage.NOTE_ON
JShulver@22 212 && message.getMessage()[2] == 0) { //but it is a note on with a velocity of zero
JShulver@22 213 offs.add(ticksToMicroseconds(event.getTick())); //add it as an off signal
JShulver@22 214 }
JShulver@22 215
m@0 216 }
m@0 217 }
m@0 218 return offs;
m@0 219 }
m@0 220
m@0 221 /* compute a list of note pitches */
m@0 222 private ArrayList computePitches() {
m@0 223 ArrayList pit = new ArrayList();
m@0 224 Track[] tracks = sequence.getTracks();
m@0 225
m@0 226 for (int nTrack = 0; nTrack < tracks.length; nTrack++) {
m@0 227 Track track = tracks[nTrack];
m@0 228 for (int nEvent = 0; nEvent < track.size(); nEvent++) {
m@0 229 MidiEvent event = track.get(nEvent);
m@0 230 MidiMessage message = event.getMessage();
m@0 231 if (message instanceof ShortMessage &&
m@0 232 ((ShortMessage)message).getCommand() == ShortMessage.NOTE_ON)
m@0 233 pit.add(((ShortMessage)message).getData1());
m@0 234 }
m@0 235 }
m@0 236 return pit;
m@0 237 }
m@0 238
jeremy@24 239 /*
jeremy@24 240 * Simultaneously construct lists of event onsets, offsets and pitches
jeremy@24 241 */
jeremy@24 242 private void registerEvents() {
jeremy@24 243 onsets = new ArrayList<Long>();
jeremy@24 244 offsets = new ArrayList<Long>();
jeremy@24 245 pitches = new ArrayList<Integer>();
jeremy@24 246 velocities = new ArrayList<Integer>();
jeremy@24 247
jeremy@24 248 Track[] tracks = sequence.getTracks();
jeremy@24 249
jeremy@24 250 // Iterate over MIDI tracks
jeremy@24 251 for (int i = 0; i < tracks.length; i++) {
jeremy@24 252 Track track = tracks[i];
jeremy@24 253 // Iterate over track events
jeremy@24 254 for (int j = 0; j < track.size(); j++) {
jeremy@24 255 registerEvent(i, j);
jeremy@24 256 }
jeremy@24 257 }
jeremy@24 258
jeremy@24 259 if (debug) {
jeremy@24 260 System.out.println("\nRegistered events...");
jeremy@24 261 Iterator<Long> oi = onsets.iterator();
jeremy@24 262 Iterator<Long> fi = offsets.iterator();
jeremy@24 263 Iterator<Integer> pi = pitches.iterator();
jeremy@24 264 Iterator<Integer> vi = velocities.iterator();
jeremy@24 265
jeremy@24 266 int pos = 1;
jeremy@24 267 while (oi.hasNext() && fi.hasNext() && pi.hasNext() && vi.hasNext()) {
jeremy@24 268 System.out.println("Event " + pos + ": onset " + oi.next() + ", offset " + fi.next()
jeremy@24 269 + ", pitch " + pi.next() + ", velocity " + vi.next());
jeremy@24 270 pos++;
jeremy@24 271 }
jeremy@24 272
jeremy@24 273 if (oi.hasNext() || fi.hasNext() || pi.hasNext() || vi.hasNext()) {
jeremy@24 274 System.out.println("Warning: event lists not equal length.");
jeremy@24 275 }
jeremy@24 276 }
jeremy@24 277 }
jeremy@24 278
jeremy@24 279 /*
jeremy@24 280 * Add the given event to the onset, offset and pitch lists.
jeremy@24 281 */
jeremy@24 282 private void registerEvent(int trackIndex, int eventIndex) {
jeremy@24 283
jeremy@24 284 Track track = sequence.getTracks()[trackIndex];
jeremy@24 285 MidiEvent event = track.get(eventIndex);
jeremy@24 286 MidiMessage message = event.getMessage();
jeremy@24 287
jeremy@24 288 // Register NOTE_ON events
jeremy@24 289 if (message instanceof ShortMessage) {
jeremy@24 290 ShortMessage shortMsg = (ShortMessage) message;
jeremy@24 291 int velocity = shortMsg.getData2();
jeremy@24 292
jeremy@24 293 if (shortMsg.getCommand() == ShortMessage.NOTE_ON && velocity != 0) {
jeremy@24 294
jeremy@24 295 long onset = ticksToMicroseconds(event.getTick());
jeremy@24 296 int pitch = shortMsg.getData1();
jeremy@24 297 long offset = getOffset(trackIndex, eventIndex, onset, pitch);
jeremy@24 298
jeremy@24 299 registerEvent(onset, offset, pitch, velocity);
jeremy@24 300 }
jeremy@24 301 }
jeremy@24 302 }
jeremy@24 303
jeremy@24 304 /*
jeremy@24 305 * Add the event details to the onset, offset and pitch lists,
jeremy@24 306 * use onset to determine the list position.
jeremy@24 307 */
jeremy@24 308 private void registerEvent(long onset, long offset, int pitch, int velocity) {
jeremy@24 309
jeremy@24 310 int index = 0;
jeremy@24 311 boolean inserted = false;
jeremy@24 312
jeremy@24 313 while (index < onsets.size()) {
jeremy@24 314
jeremy@24 315 if (onsets.get(index) > onset)
jeremy@24 316 break;
jeremy@24 317 else
jeremy@24 318 index++;
jeremy@24 319 }
jeremy@24 320
jeremy@24 321 onsets.add(index, onset);
jeremy@24 322 offsets.add(index, offset);
jeremy@24 323 pitches.add(index, pitch);
jeremy@24 324 velocities.add(index, pitch);
jeremy@24 325 }
jeremy@24 326
jeremy@24 327
jeremy@24 328
jeremy@24 329 /*
jeremy@24 330 * Find the matching NOTE_OFF event *after* the given NOTE_ON event
jeremy@24 331 * (the <onsetIndex>th event in track <trackNum>)
jeremy@24 332 */
jeremy@24 333 private long getOffset(int trackNum, int onsetIndex, long onsetTime, int pitch) {
jeremy@24 334
jeremy@24 335 long offset = -1;
jeremy@24 336 Track track = sequence.getTracks()[trackNum];
jeremy@24 337
jeremy@24 338 // Iterate through remaining events looking for NOTE_OFF
jeremy@24 339 for (int k = onsetIndex + 1; k < track.size(); k++) {
jeremy@24 340 MidiEvent event = track.get(k);
jeremy@24 341 MidiMessage message = event.getMessage();
jeremy@24 342
jeremy@24 343 if (message instanceof ShortMessage) {
jeremy@24 344 ShortMessage shortMsg = (ShortMessage) message;
jeremy@24 345 int command = shortMsg.getCommand();
jeremy@24 346
jeremy@24 347 // Check NOTE_OFF message
jeremy@24 348 if (command == ShortMessage.NOTE_OFF
jeremy@24 349 || (command == ShortMessage.NOTE_ON && shortMsg.getData2() == 0)) {
jeremy@24 350 int pitch2 = shortMsg.getData1();
jeremy@24 351 // If pitches are identical then offset found
jeremy@24 352 if (pitch == pitch2) {
jeremy@24 353 offset = ticksToMicroseconds(event.getTick());
jeremy@24 354 break;
jeremy@24 355 }
jeremy@24 356 }
jeremy@24 357 }
jeremy@24 358 }
jeremy@24 359
jeremy@24 360 if (offset < 0) {
jeremy@24 361 System.out.println("No NOTE_OFF found for track " + trackNum
jeremy@24 362 + ", onset " + onsetTime + ", pitch " + pitch);
jeremy@24 363 }
jeremy@24 364
jeremy@24 365 return offset;
jeremy@24 366 }
jeremy@24 367
jeremy@24 368
jeremy@24 369
m@0 370 /* return a list of note onset times (milliseconds) */
m@0 371 public ArrayList getOnsets() { return onsets; }
m@0 372
m@0 373 /* return a list of note offset times (milliseconds) */
m@0 374 public ArrayList getOffsets() { return offsets; }
m@0 375
m@0 376 /* return a list of note pitches */
m@0 377 public ArrayList getPitches() { return pitches; }
m@0 378
m@0 379 /* return a list of note durations (microseconds) */
m@0 380 public ArrayList getDurations() {
m@0 381 Object[] ons = onsets.toArray();
m@0 382 Object[] offs = offsets.toArray();
m@0 383
m@0 384 ArrayList durations = new ArrayList();
m@0 385
m@0 386 for(int i = 0; i < ons.length; i++) {
m@0 387 durations.add(((Long)offs[i]).longValue() -
m@0 388 ((Long)ons[i]).longValue());
m@0 389 }
m@0 390 return durations;
m@0 391 }
m@0 392
m@0 393 /* return a list of inter-onset intervals (microseconds) */
m@0 394 public ArrayList getInterOnsetIntervals() {
m@0 395 Object[] ons = onsets.toArray();
m@0 396
m@0 397 ArrayList iois = new ArrayList();
m@0 398 long firstIOI = 0;
m@0 399 iois.add(firstIOI); // IOI of first note is zero
m@0 400
m@0 401 for(int i = 1; i < ons.length; i++) {
m@0 402 iois.add(((Long)ons[i]).longValue() -
m@0 403 ((Long)ons[i-1]).longValue());
m@0 404 }
m@0 405 return iois;
m@0 406 }
m@0 407
m@0 408 /* return the length of a rest preceding a note */
m@0 409 public ArrayList getDeltaST() {
m@0 410 Object[] durs = getDurations().toArray();
m@0 411 Object[] iois = getInterOnsetIntervals().toArray();
m@0 412
m@0 413 ArrayList deltaST = new ArrayList();
m@0 414 long firstDeltaST = 0;
m@0 415 deltaST.add(firstDeltaST); // deltaST of first note is zero
m@0 416
m@0 417 for(int i = 1; i < durs.length; i++) {
m@0 418 deltaST.add(((Long)durs[i-1]).longValue() -
m@0 419 ((Long)iois[i]).longValue());
m@0 420 }
m@0 421 return deltaST;
m@0 422 }
m@0 423
jeremy@24 424 /*
jeremy@24 425 * Return the tatum of the midi file, i.e. the shortest
jeremy@24 426 * note duration or positive rest.
jeremy@24 427 */
JShulver@23 428 public long getTatum() {
JShulver@23 429
JShulver@23 430
m@0 431 Object[] durs = getDurations().toArray();
m@0 432 Object[] rests = getDeltaST().toArray();
m@0 433 long tatum = -1;
m@0 434
jeremy@24 435
m@0 436 for(int i = 0; i < durs.length; i++) {
m@0 437 long dur = ((Long)durs[i]).longValue();
m@0 438 long rest = ((Long)rests[i]).longValue();
m@0 439 long min = dur;
jeremy@24 440 if (rest > 0)
m@0 441 min = Math.min(dur, rest);
jeremy@24 442 if (debug)
jeremy@24 443 System.out.println("Dur: " + dur + " Rest: " + rest);
m@0 444 if (tatum < 0)
m@0 445 tatum = dur;
m@0 446 else if (min < tatum)
m@0 447 tatum = min;
m@0 448 }
JShulver@23 449 //tatum = sequence.getResolution()*1000;
m@0 450 return tatum;
m@0 451 }
m@0 452
m@0 453 /* return length of sequence in microseconds */
m@0 454 public long getLength() {
m@0 455 return sequence.getMicrosecondLength();
m@0 456 }
m@0 457
m@0 458 /* play the midi file */
m@0 459 public void play() {
marcus@16 460 //System.out.println("MidiPlayer.play: run sequencer.start().");
m@0 461 sequencer.start();
m@0 462 }
m@0 463
m@0 464 public void stop() {
m@4 465 if (synthesizer != null) {
m@4 466 synthesizer.close();
m@4 467 }
m@4 468 if (sequencer != null) {
m@4 469 sequencer.close();
m@4 470 }
m@4 471 sequencer = null;
m@4 472 synthesizer = null;
m@4 473 }
m@0 474 }