Project with midi files

Hey!

Im trying to make a program that reads a midi file, extracts the signature and tempo data to make a programable metronome. But I dont find the way, becase the Midibus library doesn’t reads midi files, and also I don’t know how to understand a midi file, because when I open a midi file with a text editor seems like is writen on hex or binary or kind of. As you can see, im not an expert, but I have some idea.

Also, I should say that i want to export the project to android, that’s why im doing that project on processing. How I can do it? I dont find any library that helps me.

Thanks!

Hi! Java can play Midi files. See an example I wrote here:

Maybe you can search for some keywords found in that program to figure out how to extract the tempo or other elements you need?

Update: here’s Oracle’s documentation for the Sequencer object which I used to play Midi files in the example above: https://docs.oracle.com/javase/tutorial/sound/MIDI-seq-intro.html On the left menu on that page there’s a bunch of links related to Sequencer. I used the method getTempoInBPM to get the beats per minute.

4 Likes

@arufer720
Hi … I’m part way through writing a routine to parse and extract midi file data. It is as you say lots of hex to sort through! I hope to have something in next couple of weeks if you can wait until then?

Hey! Thanks, finally I’ve used your code to make mine. That’s what I’ve done, if anyone needs it or helps anyone.

Btw, the getTempoInBPM I think only returns the first tempo, if it changes it doesnt tell you.

import java.util.Collection;
import javax.sound.midi.*;
import java.util.concurrent.ConcurrentHashMap;

public class MidiReader implements Receiver {

  ConcurrentHashMap<Integer, Note> midiData = new ConcurrentHashMap<Integer, Note>();
  Sequencer sequencer;
  FloatList[] time_events;

  public void load(String path) {

    File midiFile = new File(path);
    try {
      sequencer = MidiSystem.getSequencer();
      if (sequencer == null) {
        println("No midi sequencer");
        exit();
      } else {
        sequencer.open();
        Transmitter transmitter = sequencer.getTransmitter();
        transmitter.setReceiver(this);
        Sequence seq = MidiSystem.getSequence(midiFile);
        sequencer.setSequence(seq);
      }
    } 
    catch(Exception e) {
      e.printStackTrace();
      exit();
    }
  }

  // When I say "send" I mean "receive" :)
  @Override public void send(MidiMessage message, long t) {
    if (message instanceof ShortMessage) {
      ShortMessage sm = (ShortMessage) message;
      int cmd = sm.getCommand(); 
      if (cmd == ShortMessage.NOTE_ON || cmd == ShortMessage.NOTE_OFF) {
        int channel = sm.getChannel() - 1;      
        int note = sm.getData1();
        int velocity = sm.getData2();
        int id = channel * 1000 + note;
        if (cmd == ShortMessage.NOTE_ON && velocity > 0) {
          midiData.put(id, new Note(channel, note, velocity));
        } else {
          midiData.get(id).dying++;
        }
      }
    }
  }

  @Override public void close() {
  }

  //Returns FloatList with tempo and signature changes (events)
  public FloatList[] getTempSignEvents() {
    
    if (sequencer == null) {
      println("You first must call load() method");
      return null;      
    } else {
      time_events = new FloatList[5];
      time_events[0] = new FloatList();//Ticks
      time_events[1] = new FloatList();//tempo
      time_events[2] = new FloatList();//signature nominator
      time_events[3] = new FloatList();//signature denominator
      time_events[4] = new FloatList();//Time(s)
      Sequence sequence;
      Track[] track;
      MidiEvent event;
      MidiMessage message;
        
      sequence = sequencer.getSequence();    //Sequencer to Sequence
      track = sequence.getTracks();          //Squence to track[]
        
      for(int i = 0; i<track[0].size(); i++){
        float ticks = 0;
        float tempo = 0;
        float nominator = 0;
        float denominator = 0;
        event = track[0].get(i);        //Track[] to event. Track[0] have tempo and sign data. Track[1] have the notes, "the music"
        message = event.getMessage();  //Event to MidiMessage
        ticks = event.getTick();
          
        if(message.getMessage()[1] == 81){ //TEMPO
          //Message have size 6 for type 81 (tempo). [3], [4] and [5] together are 3bytes of an hex number.
          float msPQN = unhex(this.bytes2hex(message.getMessage()[3],message.getMessage()[4],message.getMessage()[5]));
          tempo = msPQN2bpm(msPQN);
          time_events[0].append(ticks);
          time_events[1].append(tempo);
          time_events[2].append(nominator);
          time_events[3].append(denominator);
        }
        if(message.getMessage()[1] == 88){ // Signature
          //Message of type 81: [3] is nominator. [4] is denominator.
          nominator = message.getMessage()[3];
          denominator = pow(message.getMessage()[4], 2); //midi gives the exponent
          time_events[0].append(ticks);
          time_events[1].append(tempo);
          time_events[2].append(nominator);
          time_events[3].append(denominator);
        }  
      }//for
      
      this.fillEvents(time_events); //We fill the gapes with the previous tempo/signature, and delete the duplicates
      return time_events;
    }//elseif
  }//end method
  
  private void fillEvents(FloatList[] events){
    //First of all, we search the tempo/sign for the instance 0.
    //Then, for the other events we save the current tempo/signature
    //for the next events since that tempo/signature changes.
    float currentTempo = events[1].get(0);
    float currentNom = events[2].get(1);
    float currentDen = events[3].get(1);
    events[2].set(0, currentNom);
    events[3].set(0, currentDen);
    events[1].set(1, currentTempo);
    events[4].append(0);
    
    for(int i = 1; i<events[0].size(); i++){
      if(events[1].get(i) == 0){
        events[1].set(i, currentTempo);
        currentDen = events[3].get(i);
        currentNom = events[2].get(i);
      } else{
        events[2].set(i, currentNom);
        events[3].set(i, currentDen);
        currentTempo = events[1].get(i);
      }
      events[4].append( events[0].get(i)/( events[1].get(i-1)*8 ));
    }//for
    
    //DELETE THE DUPLICATES. It is when two events have the same time/frames. events[0]
    for(int i = 1; i < events[0].size(); i++){
      if(events[0].get(i) == events[0].get(i-1)){
        for(int j = 0; j<5; j++)
          events[j].remove(i);
      }
    }
  }
  
  public void printEvents(){
    println("------------------------------------");
    for(int i= 0; i<time_events[0].size(); i++){
      println("Tick: " + time_events[0].get(i) + "\t Tempo: " + time_events[1].get(i) + "\t Compas: " + time_events[2].get(i)+ "/" + time_events[3].get(i) + "\t time: " + time_events[4].get(i) );
    }
    println("------------------------------------\n");
  }
  public String bytes2hex(byte num1, byte num2, byte num3) {
    return hex(num1)+hex(num2)+hex(num3);
  }
  
  //milliseconds per quarter note to tempo (bpm)
  public float msPQN2bpm(float msPQN) {
    return 60000000/msPQN;
  }
}
1 Like

Now, Im trying to improve the accurate of the metronome. I’ve made it with millis(), but it has 1-30ms delay sometimes, and it is noticeable. I’ll report any improvement!
If anyone has any idea, it’s welcome haha

Very nice :slight_smile: How do you measure that delay? I don’t see any use of millis(). The only thing that comes to mind is, it’s important to measure times in an absolute time, and not relative, so tiny errors don’t add up to become big errors. Not sure if that applies in your program…

To mesure the delay, I’ve made something like that:
It gives me 17ms now constantly except sometimes gives me 1ms.

long last_click = 0;
long theorical_click = 0;

void draw(){
  
  if(millis()-last_click > 500){
    println("click audio");
    println("Delay :" + (millis() - theorical_click) );
    last_click = millis();
    theorical_click = last_click + 500;
  }
}

I’ve found that post, but it doesn’t work very well too.

That’s the last code I made after reading the post. Maybe problem is audio?

import ddf.minim.*;
import ddf.minim.analysis.*;
import ddf.minim.effects.*;
import ddf.minim.signals.*;
import ddf.minim.spi.*;
import ddf.minim.ugens.*;

Minim min;
AudioPlayer down;
AudioPlayer up;
int cont = 0;
long last = 0;
long next = System.nanoTime() + 1000000000;

void setup(){
  min = new Minim(this);
  up = min.loadFile("C:/Users/Arnau/Desktop/sketch_180907a/MetronomeAM_v02/data/up2.wav");
  down = min.loadFile("C:/Users/Arnau/Desktop/sketch_180907a/MetronomeAM_v02/data/down2.wav");
  
}

void draw(){
  if(System.nanoTime() > next){
    next = next + 500*1000000;
    up.play(10);
    
    cont++;
  }
}

You on Windows by any chance?! The resolution of the clock used for this on Windows is about 16ms so that’s kind of expected. If you want an accurate time, you need to use System.nanoTime() - you can’t measure the accuracy of the accurate clock (nanoTime) using an inaccurate clock (millis). Also, you should cache the value of the clock (long ns = System.nanoTime()), not call it 3 times, because it might change.

btw - the post you cited is for a visual metronome. Attempting to control audio through a timer in the animation loop of draw() is never going to work very well!

Out of curiosity, since 60 fps = 16.666 ms, can it be related to that? May there be any improvement by running the program at 120 fps?

In StackOverflow there’s many posts about timer precision in java.

That as well, but I was more meaning the 17ms with occasional 1ms and nothing in between, which suggest it’s reflecting the precision of the clock.

I actually changed the semantics of millis() in PraxisLIVE to reflect the high precision clock, and to always be the frame time (doesn’t change during draw()), which is far more useful for most things.

Yes, should be, and probably by running at even higher fps. Unless vsync kicks in of course. All of which are good reasons that this should be driven by logic in an audio specific thread rather than by the animation timer. Unfortunately, Processing is really lacking in this regard! Maybe Minim’s AudioListener can help?

That reminds me - neither JavaSound MIDI or Minim are supported on Android as far as I know!