Hab vor Jahren keinen Sequenzer gebaut, sondern nur ein
proof of concept. Letztlich nur Midi-IO und Abspielen textbasierter Notenlisten, die Zeitpunkt und Note enthielten, etwa so:
Zu den Fallstricken, wenn's komplexer wird, kann ich also nichts sagen. Einige Anmerkungen habe ich schon.
Der einfachste Fall - ist zu einfach
Wenn du nur einen nackten Midi-Event-Recorder und -Player haben willst, wäre es ausreichend, ein Integer-Array mit soviel Elementen wie man Ticks haben will zu definieren. Mit 96 PPQ und einer Stunde maximaler Länge wären das 44 MB. Ein ziemliche Verschwendung von Speicherplatz, dafür spart man sich einiges Nachdenken beim Programmieren.
Welche Midi-Message beim Tick gesendet werden soll, bringst du im Integer unter. Der läßt sich ja als Folge von Bytes interpretieren.
Das Konzept scheitert erstens an deiner Forderung nach 480 ppq. Zweitens daran, daß die Events ohne jeden logischen Zusammenhang gespeichert werden. Note-On und Note-Off hast du ja schon angesprochen. Was auch immer dein Editor erlaubt, Noten in einer Piano-Roll malen oder Schnippsel von Notenfolgen im Arrange hinundherzuschieben, braucht eine Entsprechung als Klasse. Deine Datenstrukturen müssen sich daran orientieren, was dein Editor können soll, nicht an einer schnöden Midi-Eventlist.
Wie ich es machen würde - immer noch zu einfach.
1) Klasse TimelineEvents. Eine Arrayliste mit Referenzen auf einen Timestamp und eine weitere Liste. Alternativ ein Dictionary, dessen Key der Timestamp ist und im Wert die Referenz auf eine weitere Liste enthält.
2) Klasse TickEvents, die eine Referenz auf alle Midiereignisse enthält, die zu einem bestimmten Tick passieren. (Midi gibt nur ein Event pro Tick her. In Editoren kann man Midievents übereinanderlegen.)
3) Klasse MidiEvent. Enthält die Mididaten. Notennummer, Velocity, Kanal. Ob das Eventobjekt einen Timestamp haben sollte oder nicht, überblicke ich gerade nicht. Würde aber definitiv eine eigene Klasse Timestamp haben wollen mit einer Methoden wie Verschieben.
Verdrahtung:
Du hast einen High-Resolution-Timer, der in Abständen der Tick-Zeit ein OnTimerEvent auslöst. Damit weißt du, "ich bin jetzt bei Tick #14326". Du schaust in die TimelineEvents, ob zu diesem Tick Ereignisse angesagt sind. Falls ja, gehst du die Liste der Ereignisse durch, holst dir jedes MidiEvent aus der TickEvent-Liste und feuerst es ab. Natürlich nicht alle auf einmal, das kann Midi ja nicht. Wo hier die Grenzen liegen, weiß ich nicht.
Oder jede Spur hat eine eigene Datenstruktur, die alle beim Abspielen abgesucht werden müssen. Machbar bei 16 Spuren, aber wie ist es mit den Verzögerungen z.B. bei 64 Spuren, die jede noch Echtzeitmodifikation haben wie Delay, Transposition, Dynamik, Quantisierung?
Alles erstmal nebenläufig in einen Abspielbuffer legen, und Abspielen beginnt, sobald er gut genug gefüllt ist?
Das wären auch meine Bedenken.
Ich würde jedem logischen Element eine Methode mitgeben, die mir den tatsächlichen Timestamp und den tatsächlichen Notenwert nach Transponierung etc. gibt. Sollte es zu langsam sein, das in Echtzeit zu berechnen, müßte man das puffern, so daß es zur "logischen Darstellung" = C auf das dritte Viertel in Takt 25 mit Schnippselparameter transponiert um +3, Velocity +10 und Delay -1/8 eine "resultierende Darstellung" gibt.
Irgend sowas muß es geben, nutzt ja nichts, wenn man das Schippsel, das auf dem zweiten Takt liegt fragt: "Und? Hast du was zu spielen?" Und es antwortet: "Ja, hättest du schon vor einem Achtel ausspielen sollen."
Sonstiges
- Vergiß Linked-Lists. Die lernt man vielleicht im Studium, in der Praxis nimmt man die nur für extreme Spezialfälle. Nimm Arraylisten. Die sind unter der Haube Arrays, haben aber Methoden wie Einfügen und Heraustrennen, so daß man sie bequem bedienen kann und das nicht selbst bauen muß. Eine vernünftige Implementation von Arraylisten hat eine Grow-Methode. Wenn du ein neues Element anforderst, wird das Array nicht mühsam um ein Element vergrößert, es ist bereits vorher auf bestimmte Größe gewachsen und muß erst dann wieder wachsen, wenn diese Größe überschritten wird. Oder nimm Dictionaries. Damit hast O(1)-Zugriff auf die Elemente, allerdings ohne Sortierung der Elemente nach Key, falls man das braucht.
Speicherlücken durch Ein- und Austragen von Element-Objekt sind - im Normalfall - nicht relevant. Jeder gute Speichermanager alloziert Speicherblöcke auf Vorrat und weist neuen Speicheranforderungen alten, bereits freigegebenem Speicher zu ohne über teure Speicheranforderungen ans Betriebssystem gehen zu müssen.
- Vergiß Floats für die Timeline. Deine Programmiersprache wird eine Funktion namens GetTickCount haben. Die liefert dir einen Integer, wieviele Ticks seit Start des Computers oder des Counters vergangen sind. Wenn der TickCount keine Floats braucht, brauchst du sie auch nicht. Außer natürlich zur Anzeige, daß TickCount X soundsovielen Sekundenbruchteilen entspricht.