Java Input/Output Midi-File auf Byte-Ebene schreiben / ohne javax.sound

Elcrian

Commander
Registriert
Feb. 2008
Beiträge
2.493
Hallo,
ich möchte aus bestimmten Gründen ein Midi-File ohne die javax.sound-Library generieren. Dazu habe ich mich gestern erst mit dem Aufbau von Midi-Dateien im Allgemeinen (Header, Footer, Track-Chunks) sowie mit zwei Beispiel-Programmen beschäftigt und es versucht umzusetzen.
Der Sinn ist eigentlich just ein einfaches "Klick" nach einer bestimmten BPM-Geschwindigkeit zu erzeugen.

Ich gebe euch einfach mal den Code rein, habe ihn eigentlich sehr gut auskommentiert.

Mein "Workflow" sollte sein:
  • Header
  • Track-Chunk (Header/Identifier + NoteOn + NoteOff)
  • Footer
  • Output

Leider sind die Dateien nicht abspielbar. Das ist der Hex-Code der mit einem gewünschten Input von 120BPM raus kommt.
Code:
4D 54 68 64 00 00 00 06 00 01 00 01 00 00 4D 54 72 6B 00 00 00 14 09 00 3C 78 09 00 3C 78 01 FF 2F 00

Ich weiß, dass das wohl etwas speziell und komplex ist, das hat aber den Vorteil das mir jemand der sich da auskennt bestimmt einen einfachen Fehler ankreiden können wird. :)

lg

Code:
package rawMidi;

import java.io.DataOutput;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Vector;

/**
 * @author: C.H.
 * @date: 12/10/11
 * 
 * Translates BPM to midi-file with specified speed 
 */

public class RawMidi {

	final static int MSPM = 60000000;
	
	// Midi-Event-Values
	final static int noteOn = 0x08;
	final static int noteOff = 0x09;
	final static int metaEvent = 0xFF;
	
	private Vector<int[]> trackStorage = new Vector<int[]>();
	
	
	// Header Chunk
	final static int header[] = new int[]
	   	     { 
				0x4D, 0x54, 0x68, 0x64, // MThd header
				0x00, 0x00, 0x00, 0x06, // 0x0000006 chunk-size constant
				0x00, 0x01, // format type = 0 -> one track
				0x00, 0x01, // track number (value 0-1 -> up to 65,535 tracks)
				0x00, // time division (01 -> FPS) in 15 bytes:
				0x7F00 // first 7 bits: frames (30), second 8: how many ticks per frame (default TPQ)
				/**
				 * [url=http://www.sonicspot.com/guide/midifiles.html]MIDI File Format - The Sonic Spot[/url]
				 * The top 7 bits (bit mask 0x7F00) define 
				 * a value for the number of SMPTE frames 
				 * and can be 24, 25, 29 (for 29.97 fps) or 30.
				 * The remaining byte (bit mask 0x00FF) defines 
				 * how many clock ticks or track delta positions 
				 * there are per frame. So a time division example 
				 * of 0x9978 could be broken down into it's three parts:
				 * the top bit is one, so it is in SMPTE frames per second format, 
				 * the following 7 bits have a value of 25 (0x19) and the bottom byte has
				 * a value of 120 (0x78). This means the example plays at 24 frames per second
				 * SMPTE time and has 120 ticks per frame.
				 */
	   	     };
	
	// Footer Chunk
	final static int footer[] = new int[]
			 {
			     0x01, 0xFF, 0x2F, 0x00 // constants
			 };
	
	/**
	 * MIDI-Events
	 */
	
	private int[] getNoteOn(int delta, int note, int velocity) {
		 int[] data = new int[4];
			data[0] = (byte) delta; // chanel
			data[1] =  0x08; // indentifer
			data[2] = (byte) note; // note number
			data[3] = (byte) velocity; // velocity
		 return	data;
	}
	
	private int[] getNoteOff(int delta, int note, int velocity) {
		 int[] data = new int[4];
			data[0] = (byte) delta;
			data[1] = 0x09;
			data[2] = (byte) note;
			data[3] = 0;
		 return	data;
	}
	
	private int[] getTempoAsHex(int bpmAsInt) {
		int[] data = new int[4];
		data[0] = metaEvent;
		data[1] = 0x51;
		data[2] = 0x03;
		data[3] = (byte) parseBPM(bpmAsInt);
		
		return data;
	}
	
	/**
	 * END MIDI-Events
	 */	
	
	private int parseBPM(int bpm){
		if (bpm == 0) return 0;
		return MSPM / bpm;
	}	

	
	private int[] createTrackChunk(int bpmAsInt){
		int[] data = new int[16]; // 2 events allowed so far
		
		// track-information
		trackStorage.add(getNoteOn(4,60,bpmAsInt));
		trackStorage.add(getNoteOff(4,60,bpmAsInt));
		
		// header (constant) -> MTrk
		data[0] = 0x4D;
		data[1] = 0x54;
		data[2] = 0x72;
		data[3] = 0x6B;
		
		// Size in Big-Endian as byte (default to reserve array-space)
	    data[4] = 0x00;
	    data[5] = 0x00;
	    data[6] = 0x00;
	    data[7] = 0x00;
	    
	    int size = 0;
	    for (int p = 0; p < trackStorage.size(); p++) {
	    	// Note on
	    	int[] noteOn = (int[]) trackStorage.get(p);
			for (int i = 0; i < noteOn.length; i++) {
				data[i+8] = noteOn[i]; // offset 0x09
				size++;
			}
			
			// Note Off
			int[] noteOff = (int[]) trackStorage.get(p);
			for (int i = 0; i < noteOff.length; i++) {
				data[i+12] = noteOff[i]; // offset 0x13
				size++;
			}
	    }
	    	System.out.println("Chunk size: "+size);
	    // set size / if <= 0xFF
	    data[4] = 0x00;
	    data[5] = 0x00;
	    data[6] = 0x00;
	    data[7] = (byte) size+4;
		
		return data;
	}
	
	/**
	 * WRITE
	 */		
	public void writeMetronomeFile(String filename, String dir, int bpm) {
		try {
			FileOutputStream fos = new FileOutputStream (dir+filename);
		
			fos.write(intArrayToByteArray(header)); // Header-Chunk
			fos.write(intArrayToByteArray(createTrackChunk(bpm))); // Track-Chunk
			fos.write(intArrayToByteArray(footer)); // Footer-Chunk
			
			System.out.println("File written: "+dir+filename);
			
		} catch (FileNotFoundException e) {
			System.err.println(e);
		} catch (IOException e) {
			System.err.println(e);
		}
	}
	
	private static byte[] intArrayToByteArray (int[] ints)
	  {
	    int l = ints.length;
	    byte[] out = new byte[ints.length]; 
	    for (int i = 0; i < l; i++) 
	    {
	      out[i] = (byte) ints[i];
	    }
	    return out;
	  }	
	
}
 
Es wäre hilfreich, wenn man wüßte was (binär) rauskommen soll, dann könnte man auch vergleichen welche Stelle falsch ist und somit auf die Codestelle schließen.

Was du Dir mal anschauen solltest ist die intArrayToByteArray Methode. Du castest die int-Werte einfach zu byte. Byte hat nur einen Wertebereich von -128 bis 127. Du reichst zumindest einmal einen int wert rein, welcher außerhalb dieses Bereichs liegt. Und zwar hast du im Header den Wert "0x7F00". Das passt nicht in ein Byte rein.

Vllt war es das ja schon, aber hilfreich wäre ein "SOLL"-Stand.
 
Zuletzt bearbeitet: (byte geht nur bis 127, nicht 128)
Für das Umwandeln in ein Byte-Array kannst du auch folgendes machen:

Code:
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream( baos );

out.writeInt(integerZahl);
out.flush();
out.close();

byte[] result = baos.toByteArray();

Beachte dabei, dass die Byte-Reihenfolge Big-Endian ist.
 
Zuletzt bearbeitet:
Danke schon mal.

Das mit dem intArrayToByteArray() habe ich mal korrigiert - die war allerdings nicht so ganz von mir. :p Ebenso den falschen Wert im Header, da muss ich zu meiner Verteidigung aber sagen, dass das einer der Werte im Header war die ich nicht so verstanden habe.

Mittlerweile habe ich nach zu langer Zeit am Sonntag das Teil zum Laufen gekriegt; würde euch gerne sagen was genau ich getan habe, leider habe ich das nicht mehr 100%ig auf dem Schirm. Waren aber definitiv Detail-Sachen. u.A. allerdings die Größe des Track-Arrays. Außerdem arbeite ich nun mehr mit der Vector-Collection, das ist flexibler und praktischer.


Jedenfalls, einfache Midi-Ausgaben funktionieren.


Allerdings wird es nun was die Meta-Events angeht interessant - die lassen "der Gerät" nämlich kollabieren. :D

Konkret folgende Methoden:
Code:
	private void setProgramChange(int program) {
		 int[] data = new int[3];
			data[0] = 0x00;
			data[1] = 0xC0; // identifier
			data[2] = (byte) program;
		trackStorage.add(data);
	}

	
	private void setTempo(int bpmAsInt) {
		int[] data = new int[6];
		data[0] = metaEvent;
		data[1] = 0x51;
		data[2] = 0x03;
		
		String[] buffer = parseIntToHexArray(parseBPM(bpmAsInt));

		int i = 0;
		for (String s: buffer) {			
			data[i+3] = Integer.parseInt(buffer[i].trim(), 16 /* radix */ );
			i++;
		}
		
		trackStorage.add(data);
	}

parseIntToHexArray nur das tut was es sagt und liefert mir einen gesplitteten Hex-Wert in Array-Form.

Edit: Die Midi-Chanel-Events blockieren tatsächlich bzw. werden falsch interpretiert. Schreibe ich sie nach den Sound-Ausgaben in den Vektor ist die Datei abspielbar, die Events werden natürlich ignoriert.
 
Zuletzt bearbeitet:
Zurück
Oben