Anfänger braucht Hilfe beim einlesen/verstehen von MIDI Datei (MIDI File Format)
-
Hallo,
ich habe jetz schon insgesamt gut 30 Stunden damit verbracht im Internet zu recherchieren wie das MIDI File Format aussieht und wie man das ganze am besten einliest.
Es gibt zwar eine Menge Seiten darüber, die erklären wie das ganze aussieht und was die Bytes genau tun, aber nirgends wird klipp und klar erklärt wie man denn tatsächlich eine MIDI Datei einliest und die Daten/Bytes verarbeitet/zuordnet.Bin ein relativer Neuling was C++ angeht und hab bisher noch nie mit Dateien gearbeitet, möchte also jetzt ein kleines Programm schreiben, dass eine MIDI Datei einliest und sie über die Beep() Funktion ausgibt
Hab es sogar hinbekommen dass ich mit nem MIDI-Keyboard den Biep-PC-Lautsprecher direkt anspielen kann, sogar polyphon, obwohl er ja eigentlich nur 1 Ton gleichzeitig spielen kann (durch kurze 20ms~ lange aufeinander folgend gespielte Töne/Arpeggios).
Das funktioniert also schonmal perfekt, war auch ziemlich überrascht wie einfach das ging.
Das spielen von MIDI Dateien hab ich sogar auch geschafft, bisher allerdings nur über einen Umweg. Mein Programm empfängt MIDI-Daten mit midiInOpen und einer MidiProc Funktion über den Input "In from MIDI Yoke 1" und in ein Sequencer Programm lade ich die Midi Datei und lasse das ganze über "Out to MIDI Yoke 1" senden. Die MIDI Daten werden also über MIDI Yoke umgeleitet zu meinem Programm(MIDI Yoke ist ein Programm/Treiber oder sowas, das virtuelle MIDI Ins/Outs bereitstellt um MIDI Daten intern irgendwie zu routen, so genau weiss ichs auch nicht)
Jetzt möchte ich das Ganze aber ohne Umwege direkt in meinem Programm alles machen, also das heisst: Ich muss dazu also eine MIDI Datei einlesen.
Und genau damit hab ich nun eben meine Probleme...Es macht mich langsam WAHNSINNIG
Es will einfach nicht funktionieren.
Wenn ich eine eigene ganz simple MIDI-Datei erstelle mit nur einem Track lässt sich das ganze auch einlesen. Aber das ist ja langweilig und deswegen möchte ich jede x-beliebige Datei die ich mir so aus dem Netz lade einlesen können.
Nur leider beinhalten diese Dateien eben oft viele komische Events, Titel, Lyrics, Program Changes etc etc... Und ich weiss nicht wie ich die dazugehörigen Bytes überspringen/ignorieren kann, bzw wie erkenne ich wann diese Daten vorbei sind und wieder normale NoteOn/NoteOff Events kommen?Was meine Programmierfähigkeiten angeht, würd ich mich als fortgeschrittenen Anfänger bezeichnen
Mit Dateiformaten hab ich mich bis jetzt noch nie beschäftigt und habe auch kein für Anfänger verständlichen Tutorials dazu gefunden.
Aber na gut, das MIDI Dateiformat scheint mir "einfach genug" zu sein um es zu verstehen.Hier meine bisherige Logik:
Zuerst werden die ersten 14 Bytes eingelesen, die beinhalten irgendwelche Daten über die MIDI Datei (MIDI Type, Number of Tracks etc)
Dann folgt ein Loop:for(int i=0; i<numTracks; i++) ...
In jedem Durchgang werden zuerst die ersten 8 Bytes eingelesen, 4 Byte "MTrk" und 4 Byte die Tracklänge.
Ausserdem deklariere ich die Variable runningState und initialisiere sie mit 0.
Jetzt kommt wieder eine Schleife:while(!endOfTrack) { MidiEvent midiEvent; // Eine Struktur in der ich die einzelnen Noten speichere und später weitergebe. BYTE nextByte; fileRead(ifs, nextByte); // ifs ist das ifstream Objekt mit dem ich die Datei einlese, fileRead eine Funktion mit der ich mehrere Bytes aneinandergereiht "richtig herum" einlesen kann. // Mit ifs.read() bekomm ich statt AABBCCDD -> DDCCBBAA // fileRead liest also zB 4 Bytes und dreht die Bytereihenfolge um. // Da hier aber nur 1 Byte gelesen wird, ist es das gleiche wie ifs.read() ...
So, als nächstes müsste soweit ich das verstanden hab vor jedem Event ein time-offset kommen mit variabler Länge, das ganze lese ich so aus:
while(nextByte > 0x80) { midiEvent.delay += (nextByte & 127); midiEvent.delay <<= 8; // 1 Byte nach links schieben um Platz für das nächste Byte zu machen das folgen wird, da das Byte größer als 0x80 war. fileRead(ifs, nextByte); } midiEvent.delay += (nextByte & 127); // Letztes Byte anhängen // Nächstes Byte einlesen fileRead(ifs, nextByte);
So, jetzt müsste nextByte irgendwelche statusEvents/Meta Events beinhalten, richtig?
Das prüfe ich mit folgendem Code:if(nextByte == 0xFF) // MetaEvent folgt { fileRead(ifs, nextByte); // Nächstes Byte einlesen, enthält MetaDaten if(nextByte == 0x03) // Track Name folgt { fileRead(ifs, nextByte); // Enthält die Länge des Track Namens int cbTrackName = nextByte; char *trackName = new char[cbTrackName+1]; trackName[cbTrackName] = '\0'; for(int j=0; j < cbTrackName; j++) { fileRead(ifs, nextByte); trackName[j] = nextByte; } strcpy_s(midiTrack.trackName, trackName); cout << "Track #" << i << " name: " << midiTrack.trackName << endl; delete[] trackName; } else if(nextByte == 0x2F) // end of track { endOfTrack = true; ifs.ignore(1); // Nächstes Byte ignorieren, da glaub ich nur 0x00 folgt } else // Irgendwas anderes dass ich nicht brauche { fileRead(ifs, nextByte); // Enthält die Länge folgender Bytes, die wir natürlich einfach überspringen :) ifs.ignore(nextByte); } continue; } if(nextByte == 0xF0) //SysEx Message { fileRead(ifs, nextByte); while(nextByte != 0xF7) // 0xF7 markiert das Ende der SysEx Message, solange einlesen bis das Ende erreicht ist. { fileRead(ifs, nextByte); } continue; } // Jetzt sollten irgendwie nurnoch Events kommen wie NoteOff, NoteOn, etc, alles mit einer fixen Länge von jeweils 3 Bytes, // Byte 1 == eventType // Byte 2 == data1 // Byte 3 == data2 if(runningState == 0 || (nextByte & 0x80)) // Wenn (nextByte > 0x80) bzw (nextByte & 0x80) (sollte das Selbe sein), // dann handelt es sich um irgendein neues EventType, das nextByte enthält. { runningState = nextByte; } // Ansonsten gilt weiterhin das alte Event midiEvent.status = runningState; fileRead(ifs, nextByte); // Byte 2, bei NoteOn/NoteOff enhält das die Notennummer/-höhe. midiEvent.pitch = nextByte; fileRead(ifs, nextByte); // Byte 3, bei NoteOn/NoteOff enhält das die Anschlag- bzw Lautstärke. midiEvent.volume = nextByte; midiTrack.events.push_back(midiEvent);
Die continue statements gehören zur while(!endOfTrack) Schleife.
Tja und das wars auch schon, danach wird der Filestream geschlossen und ich sollte am Ende einen Vector haben aus MidiTracks, jeder Track besteht wiederum aus einem Vector mit MidiEvents.
Nur irgendwie liest es nur die ersten 5 Tracks von 13 ein und bleibt irgendwo in einer Endlosschleife stecken, im Debugger hat nextByte dann immer nur den Wert 0x00, was glaube ich bedeuten würde, ich bin am Ende der Datei angelangt, aber wie kann das dann sein dass dann nur 5 Tracks eingelesen wurden?
Irgendwie scheint das ganze auch irgendwann "Out of Sync" zu gehen weil zwischendrin zusätzliche Bytes auftauchen die dann falsch eingelesen werden...
Hat vielleicht irgendjemand von Euch schonmal mit MIDI Datein gearbeitet und kann mir sagen was ich falsch mache?Bzw wie man das normalerweise angeht.
Wie reagiere ich auf unbekannte Events, auf die vielleicht sogar weitere Daten mit variabler Länge folgen.
Woran erkenne ich wann ich wieder ein NoteOn/Off Event vor mir habe? Und ist vor JEDEM Event ein delay/timeOffset? Wenn nicht wie soll ich dann wissen ob da eins ist, wenn das Byte mit dem Event-Typ erst DANACH kommt?
Naja gut ich hoffe ihr könnt mein Problem einigermaßen verstehen und ich hab es klar genug beschrieben
Bin wie gesagt noch relativ unerfahren.Die folgenden Seiten hab ich dazu studiert:
http://www.skytopia.com/project/articles/midi.html
http://www.sonicspot.com/guide/midifiles.html
http://www.4front-tech.com/pguide/midi/midi7.html (an einigen Stellen sind komische Lücken?)Hier mein kompletter Quellcode:
http://pastebin.com/BHQeSeQyHab auch ne Menge Debug couts eingefügt, hoffe das hilft zu verstehen was abgeht xD
Ich verstehs jedenfalls selbst nicht 100%igHiiiiilfe! Gibts denn nirgends ein gut verständliches Tutorial über das MIDI File Format, verständlich erklärt auch für Nicht-profis?
Könnt das ganze ja mal mit ein paar einfachen und komplexeren MIDI Dateien testen, gibt ja genug davon im Netz.
Gespielt wird allerdings nichts, erstmal nur eingelesenHoffe das ist das richtige Forum für meine Frage, konnte sie keinem anderen zuordnen ausser vielleicht DOS/Win32 Konsolen Dingsbums...
Vielen Dank schonmal!
-
Zwei Sachen:
Ich habe gerade keine Zeit mir alles durchzulesen, aber ich habe mal einen Midiparser in C++ geschrieben, der Code ist hier: https://github.com/filmor/star/tree/master/include/utility/midi
Der Hauptcode ist in parser.ipp.Meta Events sind ein String variabler Länge, also in etwa
0x0f <len> <data>
Dabei besteht <len> aus Bytes, deren untere 7 Bit entscheidend sind, das erste Bit gibt nur an, ob noch was kommt, also quasi
10000001 10001000 00001111 = 0000001000100000001111 = 34831
Ist ein bisschen so wie UTF-8
Die entsprechende Funktion in der obigen Datei ist read_var_len.
-
hast Du die Datei auch mit ios_base::binary geöffnet? Falls nicht, kann es nicht funktionieren!
-
Ok den ersten Fehler hab ich schonmal: Ich hab die Länge der Meta Events nicht als variable Länge ausgelesen, sondern nur als 1 Byte.
Funktioniert trotzdem noch nicht richtig...
Jetzt liest es zwar bis zum Ende der Datei alles korrekt ein, aber das ist schon nach 10 Tracks erreicht, obwohl im Header steht es wären 13 Tracks. Oder versteh ich da was falsch?Hier mal der MIDI Header:
4D 54 68 64 = "MThd"
00 00 00 06 = 6 Bytes Headerlänge
00 01 = Type 1 MIDI File
00 0D = 13 Tracks (was nicht stimmt?!)
01 E0 = ppq (irgendwas per quarter, geschwindigkeit)Und wird der running status von einem Track zum nächsten übertragen? Das zB ein Track gleich ohne MIDI Eventcode anfängt?
-
ppq ist die Anzahl der Ticks pro Viertelnote, quasi eine globale Geschwindigkeitsangabe für die Datei.
Zähl doch mal, wieviele "MTrk"s in der Datei vorkommen. Wenn das weniger als 13 sind ist deine Datei einfach kaputt (meine Lib würde da eine Exception schmeißen).
Events ohne Event code am Anfang des Tracks ignoriere ich immer, die Tracks sind ja normalerweise nicht hintereinander zu spielen, deshalb macht das überhaupt keinen Sinn (erst recht beim Erstellen der Datei).
/EDIT: Die Länge eines Tracks ist übrigens genauso kodiert wie die eines Meta-Events, vielleicht geht's da bei dir kaputt
-
Ok sind tatsächlich 13 "MTrk"s in der Datei, aber irgendwie liest mein Programm nur 10 davon aus, ich bin total verwirrt...
Die Länge eines Tracks benutze ich eigentlich für nichts, ich lese stattdessen immer nur von MTrk bis 00 FF 2F 00 (end of track).
Naja gut, wenigstens ließt es schonmal die Datei komplett ein und ohne Fehler, nur dass ein paar Tracks einfach im Nirvana verschwinden
Aber fürs Erste reicht mir das.
Danke übrigens für deinen Code, werd ihn mir noch ein paar mal durchlesen und dann warscheinlich nochmal komplett neu anfangen wenn ich meine es verstanden zu haben.
-
Hallo Leute,
ich hab mal so ein wenig mit dem SMF (Standard Midi File Format) herum experimentiert. Ich kann auch alle Chunks einwandfrei lesen, aber bei der Interpretation des Inhalts - genauer der Midi Events - komme ich nicht weiter.
Ich nehme mal die Datei town.mid (kommt auf C:/Windows/Media vor) als Beispiel. Der Format Type ist 1.
Mit dem Hex-Editor sehe ich dort:00000080 00 FF 03 09 53 74 65 65 6C 20 47 74 72 00 C0 19 ....Steel Gtr... 00000090 00 B0 07 68 00 0A 20 00 79 00 00 5B 20 00 5D 08 ...h.. .y..[ .].
bis 'Steel Gtr' kann ich einwandfrei lesen (Meta Event 0x03 Sequence/Track Name Länge 9).
danach folgen noch zwei Midi Events:00 C0 19
das ist auch klar. C:"Program Change"; program number=0x19.
00 B0 07 68
d.h.: B:Controller; controller number=0x07(Main Volume); controller value=0x68
.. alles klar und jetzt
00 0A 20 00 79 00 00 5B 20 00 5D 08
der allgemeinen Interpretation folgend wäre 00=time stamp, aber 0A das Midi Event vom Typ 0, mit dem Channel 0xA.
Aber Typ 0 ist in keiner Quelle über das SMF, welche ich durchgesehen habe, erwähnt. Die Typen beginne erst mit 0x8 Note off
Das Programm von .filmor steigt hier auch aus (parser.ipp - switch ((status.opcode)){ ... default: // unbekannter opcode )Das tritt praktisch bei allen Midi-Dateien auf, die ich unter Windows gefunden habe.
Weiß jemand Rat?
Gruß
Werner
-
Ich bin nicht mehr voll drin (der Code ist über 4 Jahre alt ;)), aber kann es sein, dass du den Running Status ignorierst? Wenn der Opcode 0 ist müsstest du einfach das status byte von vorher übernehmen und weiterlesen (im Prinzip müsste ich da nur bei default: ein goto an den Anfang des switch setzen, keine Ahnung warum ich das nicht gemacht hab ;)).
Was du hier also interpretieren musst ist
00 B0 07 68 00 [b]B0[/b] 0A 20 … (nächster Event)
-
Hallo .filmor,
das isses. Ich hatte das mit dem 'Running Status' zwar gelesen, aber total miss interpretiert.
Übrigens: Dickes Lob an Deinen Code- lies sich sofort problemlos mit Visual Studio übersetzen. Normal ist das nicht, wenn was aus einer anderen Umgebung kommt.
Danke für die Antwort und Gruß
Werner