Brainstorm Ich möchte meinen eigenen MIDI-Sequencer programmieren

DanReed

||||
Muss das sein? Haben das nicht schon Tausende vor mir getan? Nimm doch einfach einen Open-Source-MIDI-Sequencer...

Diese Art von Gedanken bitte nicht. Eher so was wie:
"Hab ich auch schon gemacht. Das Hauptproblem für mich war am Schluss..."
"Wenn ich damit heute beginnen wollte, dann würde ich mir folgende Gedanken zur Speicherverwaltung und Datenstruktur machen, damit man gut editieren kann und der Abspielalg nicht zuviel zu tun hat"
"Gleichzeitig zeitstabil abspielen und aufnehmen ist nicht trivial. Und wenn zeitgleich auch noch mehrere MIDI-Spuren editiert werden sollen, muss man vor allem folgendes beachten ..."

Das Ganze ist in C/C++ geplant z.B. für einen Arduino (auf dem C64 mit 985kHz Taktfrequenz lief der Scoretrack/Supertrack ganz ausgezeichnet), nur um die Rahmenbedingungen mal abzustecken. Entwickeln werde ich auf einem Linux-Rechner.

Ja, das ist nerdig. Aber beruflich bin ich u.a. ein sehr guter Programmierer und werde das hinbekommen. Ich möchte nur fundamentale Fehlentscheidungen jetzt, bevor das Coden beginnt, vermeiden.
 
Ich habe vor gut 30 Jahren angefangen meinen MIDI-Sequenzer zu programmieren, damals noch auf dem Amiga. War fast mein erstes C-Programm, entsprechend fürchterlich ist der Code. Mittlerweile läuft er als VST-Plugin, fühlt sich aber nach Hardware-Sequenzer an, da er von zwei Icon Controllern (Platform M+/X+) incl. Display bedient wird. Um einen vernünftigen Output im VST-Fenster kümmere ich mich garnicht mehr, das öffne ich nämlich nicht mehr, selbst die Spurnamen etc. editiere ich via Controller.

Insgesamt finde ich es eher trivial einen MIDI-Sequenzer zu programmieren (ein anderes Projekt von mir ist ein Audio-Looper, der bereitet mir deutlich mehr Kopfzerbrechen), wichtig ist vor allem zu beachten, dass im Echtzeit-Thread kein Memory allokiert wird. Aber ansonsten muss man sich da z.B. keinerlei Kopf bzgl Performance zerbrechen, wie gesagt, meine erste Version lief auf einem Amiga.

Willst Du eigenen Hardware bauen, oder auch Controller kaufen, und über diesen den Sequenzer editieren?
 
Willst Du eigenen Hardware bauen, oder auch Controller kaufen, und über diesen den Sequenzer editieren?
Wenn er auf den Arduino kommt, dann bekommt er auch ein eigenes Hardware-UI (Endlos-Coder, Tasten, Display, LEDs)

Insgesamt finde ich es eher trivial einen MIDI-Sequenzer zu programmieren
Man kann ja z.B. eingehende Events schon beim Recorden in eine Datenstruktur (z.B. Baum) eintragen, so dass sie beim Abspielen schnell gefunden werden.
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?
 
Zuletzt bearbeitet:
Man kann ja z.B. eingehende Events schon beim Recorden in eine Datenstruktur (z.B. Baum) eintragen, so dass sie beim Abspielen schnell gefunden wird.
Okay, mein Sequenzer ist ein Step-Sequenzer und daher sehr simple von der Datenstruktur, ich habe halt 16 Parts die parallel abspielen können, mit jeweils 16 (im Moment noch monophone) Spuren, wobei jeder Spur maximal 32 Steps hat, und der Zustand der 16 Spuren (incl. aller Settings) können in 32 Pattern abgespeichert werden (also jeder Part hat 32 unterschiedliche Pattern). Um das dynamischer zu gestalten, würde ich wohl pro Spur mit einer Linked-List arbeiten, die nach den Zeitpunkt des Events sortiert ist. Da können dann neue Events schnell eingehängt werden. Wobei (zumindest für Plugins) halt wichtig ist, dass der Speicher für die Elemente der Liste schon allokiert ist, und dann einfach wiederverwendet wird (wie dramatisch das allokieren heutzutage wirklich noch ist, kann ich allerdings garnicht genau sagen, ich habe das halt so gelernt).
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?
Echtzeitmodifikation von Events sind für heutige Prozessoren echt Pillepalle, da musst Du Dir keine Sorgen machen.
 
Denk an MIDI 2.0!

Wenn ich einen Sequenzer programmieren wollte, würde ich definitiv gleich den neuen "Quasi-Standard" bedienen wollen.
 
Schneller geht das mit Reaktor.
AAber kein VSTExport

SynthEdith kann VST exportieren
Syntmacker auch
 
Top, ich wünschte, ich könnte das auch.
Dann würde ich endlich mal diese waagrechte MIDI-Rolle ins Aus befördern,
welche ja 99,9% der DAWs haben.
In HW, super!
 
Gut, das lässt sich in der DAW bestimmt auf Senkrecht umstricken (oder auch nicht).
Ich bin einer der vielen oder wenigen, die das klassische Zählwerk bevorzugen.
Zu jeder Spur gibt es optische Info-Kästchen, welche alle (!) Daten präsentieren.
Einziger Nachteil, ab 32 Spuren nebeneinander wird jeder Bildschirm knapp.
Vorteile sind wie in meinem Yamaha-Sequenzer z.B. supereinfache MIDI-Noten-Bearbeitung,
Übersicht trotz rel. wenig Display, schnelles Arbeiten und sogar Spaß
 
Nix gegen DAWs, Audio aufnehmen und Bearbeiten finde ich damit absolut Klasse.
Aber MIDI-Recording, Leute ganz ehrlich, es macht mir 0 (NULL) Böcke.
Vielleicht habe ich zu lange mit HW-Sequenzern gearbeitet, aber für MIDI gibt es subjektiv nichts Besseres.
Und wenn SW, dann ehr Sweet Sixteen o.ä.
 
Ich würde dringend einen ESP32 empfehlen, günstig, wesentlich leistungsfähiger und mehr RAM als ein Arduino... Außerdem Dual Core und Bluetooth / WIFI bei Bedarf...
ich verweise mal auf mein eigenes Projekt. Der Fokus liegt ganz klar auf intuitiven Recording Möglichkeiten von Mididaten. Problemstellung war das Durchbrechen der oft üblichen 4 bzw. 8 Takte in Verbindung mit Polyphonie, besonders im Hinblick auf polyphoner Stepeingabe. Pro Pattern stehen 1024 Midi Events zur Verfügung, die Maximal auf einer Länge von 255 Takten verteilt werden können. Dabei können theoretisch 128 Noten gleichzeitig aktiv (polyphon) sein.
In einer nächsten Hardware Revision sollen noch CV Clock Eingang, weitere Midi Schnittstellen und USB Midi folgen. Des Weiteren ein UI mit wesentlich mehr Bedienelementen, um die Performance Möglichkeiten zu erweitern.
 
Diese Art von Gedanken bitte nicht. Eher so was wie:
"Hab ich auch schon gemacht. Das Hauptproblem für mich war am Schluss..."

ja, am schluss endet es immer mit problemen. :P


keine ahnung ob du damit was anfangen kannst.

am anfang stand bei mir die mission. die ergab sich zunächst aus meinem generellen ansatz, dass ich immer krampfhaft versuche etwas neues zu schaffen, was es so vorher nicht gab. niemand braucht das 17. multitap echo. wenn es etwas schon gibt, ist es sinnlos es noch einmal zu bauen, außer natürlich um dabei etwas zu lernen.

bei meinen sequencer projekten sah das ungefähr so aus:

- alles ist modular und hoch abstrakt, damit es möglichst einfach zu benutzen ist
- vielseitig verwendbar (also nicht nur für midi output)
- im prinzip nur generation (aufnehmen oder laden muss also im zweifel hinten anstehen)
- das generieren muss in echtzeit und offline funktionieren

dann habe ich ohne viel nachzudenken 3-5 solcher systeme gebaut.

davon lief eines komplett nur in audio signalen, was mir schnell über wurde, weil damit vieles einfach nicht geht, und das immer dann, wenn man z.b. midi output haben wollte, ja auch gar keinen vorteil bot, das gute timing besteht dort nur im zusammenhang mit max, rewire, und CV/gate output, für die midi ausgabe ist es schlechter wie gleich nur zahlen zu verschicken.

ein anderes war ein midi sequencer, in dem alles mit "echtem" midi passierte. wollte man hinterher etwas anderes ausgeben, musst man es umständlich umwandeln. der vorteil von midi ist, dass man für den anwendugnszweck "midi ausgeben" alles so vorliegen hat, wie man es braucht. dadurch kann man z.b. an jeder stelle "reinhorchen".
der nachteil von midi ist, dass es seriell ist. will man analysen oder funktionen in sachen akkorde/harmonien damit durchführen, muss man die einzelnen noten erst zusammensammeln um sie dann gemeinsam zu beabeiten. das ist extrem umständlich - und verursacht latenz.

gewonnen hatte dann nach der testsphase das projekt, was eine art genereller zahlen- und datensequencer war, bei dem zwar ein gewisser schwerpunkt auf midi und chromatik zu erkennen war, aber auch jeder andere beliebige quatsch abgearbeitet und erzeugt werden kann, den man für serial, DMX, CV, kyma, oder zur steuerung von video und audio im programm selbst brauchen könnte.

wer aufgespasst hat, hat bemerkt, dass nur eine mission existiert, und kein machine model und auch kein scrum management. programmieren ist hier einfach nur ein kreativer prozess, der spass machen muss, sonst ist nämlich ganz schnell schluss damit, und die software ist auch kein produkt sondern ein kunstwerk.
 
Zuletzt bearbeitet:
ich immer krampfhaft versuche etwas neues zu schaffen
"krampfhaft" nein danke. Es muss Spaß machen, sonst sind die Fortschritte zu klein. "neu" muss nicht sein, muss nicht einmal besser sein, darf aber auch nicht "schlechter" sein. Typischerweise ergibt sich dann irgendwann doch etwas neues, was ich besser finde.

will man analysen oder funktionen in sachen akkorde/harmonien damit durchführen, muss man die einzelnen noten erst zusammensammeln
Genau auf diese Stelle zielte meine Eingangsfrage: Wie organisiert man die Daten sinnvollerweise so, dass der Wiedergabe-Alg sich nicht totsuchen muss, dass man gleichzeitig neue Spuren aufnehmen kann und dass Editieren in Echtzeit während Wiedergabe möglich ist.

Ach was, direkt los coden spart viel kostbare Planungszeit. 🤯
Ja und mit meinem gegenwärtigen Ergebnis bin ich nicht zufrieden:

Ich kann SMF laden und wiedergeben. Dabei verwende ich einfache getrennte Arrays, für jede Spur eins. Beim gleichzeitigen Aufnehmen kommt für jede Spur ein Array hinzu. Wenn ich aber z.B. zwei oder mehr Spuren gleichzeitig aufnehme, müssen die Daten nach der Aufnahme entsprechend der Spuren in die zugehörigen neuen Arrays einsortiert werden.

Fürs Editieren müssen NoteOn und zugehörige NoteOff zusammengebracht und gleichzeitig bearbeitet werden.

Bei der Wiedergabe wäre das Wissen über NoteOn zusammen mit NoteOff sinnvoll, damit "Pause" nicht zu Notenhängern führt. Die totale Überwachung z.B. hinsichtlich doppelter NoteOn ist eigentlich nötig.

Ich suche immer noch nach einer Datenstruktur, die das Ganze sinnvoll für Aufnahme, Wiedergabe und Editiieren organisiert, so dass Suchen möglicht einfach ist und Sortieren/Organisieren möglichst offline stattfinden kann (z.B. nach "Stop").
 
Zuletzt bearbeitet:
Genau auf diese Stelle zielte meine Eingangsfrage: Wie organisiert man die Daten sinnvollerweise so, dass der Wiedergabe-Alg sich nicht totsuchen muss, dass man gleichzeitig neue Spuren aufnehmen kann und dass Editieren in Echtzeit während Wiedergabe möglich ist.

ob das nicht sogar zu einem größeren teil eine frage von architektur und sprache ist, musst du selbst entscheiden, davon versteh ich nix.

ich mache ja nur software für desktop PCs und habe daher eine quasi beliebig große graphische oberfläche und bei ein bischen daten-sequencing auch quasi endlos speicher und CPU zur verfügung.
die prioriritäten sind da also komplett andere und für verschiedene formen der speicherverwaltung greife ich auf fertige bausteine zurück, von dene ich gerade mal weiß, was die so ungefähr machen... es gibt da welche "mit namen" und welche ohne, oder irgendwas hat eine begrenzte bufferlänge oder sowas.

für einen total normalen konventionellen sequencer sehe ich, was den logischen teil angeht, keinen grund für einzelne spuren. die spuren kannst du genausogut als index irgendwo davor schreiben, nicht anders wie man das mit midi kanälen oder unterscheidlichen arten von midi events ja auch macht. alles andere ist ja nur eine frage des auslesens und recordens. dann hast du das ganze geraffel immer in einem einzigen großen speicherblock.
ein solches system würde praktisch immer auf allen spuren aufnehmen und wiedergeben und den rest erledigen dann filter.
 
eine frage von architektur und sprache
Für nahezu jede Sprache gibt es Libraries mit fertigen höheren Datenstrukturen als Feldern (z.B. verkettete Listen, Bäume, Graphen), auch mit garbage collection.

Aber ich frage mich, wie Lengeling seinen Supertrack 1985 auf dem C-64 realisiert hat, so dass er in dieser unglaublichen Geschwindigkeit große Datenmengen auf 16 Kanälen wiedergeben und gleichzeitig aufnehmen konnte und das Editieren von Notenposition und -länge so effektiv vonstatten ging.

Das war keine Frage der Sprache, sondern des Datentyps und der Algorithmen auf diesem Datentyp.

Wenn man nämlich z.B. Noten löscht und dadurch freier Speicher mitten in einem Array entsteht, dann muss man irgendwann entweder selber aktiv garbage collection durchführen (wobei ein C-64 fühlbar lang braucht, um z.B. 16kByte im Speicher zusammenzuschieben, und so war das eben nicht), oder eben die "Löcher" geschickt verwalten.

Hier lohnt es sich, Gehirnschmalz zu investieren...
 
du hast recht, das habe ich übersehen. fragmentierung findet natürlich auch innerhalb des blocks statt wenn der dumme user stundenlang herumeditiert.

wieviel speicher hast du denn? kannst du das problem nicht einfach ignorieren? davon gehen die manchmal auch weg.

oder wie wäre es mit einer routine, die immer alle 1,000 löschvorgänge einmal die daten umkopiert. man könnte das auch mit dem "save" knopf kombieren.
 
"Die totale Überwachung z.B. hinsichtlich doppelter NoteOn ist eigentlich nötig"

etwas aehnliches hab ich bei meinem 'event looper' so geloest
(stumpf ist trumpf), dass ich fuer jede gedrueckte
note (in einem array) eine 1 gespeichert habe (und beim loslassen 0), und dann beim stopp
das array einmal durgegangen bin und fuer jede 1 vorsichtshalber ein note off gesendet
habe. sind ja nur 128 bit.

"dass der Wiedergabe-Alg sich nicht totsuchen muss"

eine loesung ist da, statt des arrays eine (verkettete) liste zu benutzen:
so lange genug freier speicher da ist, schreibt man die daten einfach dahin,
wo platz ist. ("am ende") und das einfuegen beschraenkt sich darauf,
die neuen daten an die passende stelle der liste 'einzuhaengen'.
alles supi, nur: kein random access! im gegensatz zum array.
wenn man an eine andere stelle in seinem stueck springen will, muss
man erst von irgendwo bis dort alles 'durchrattern', um endlich
loslegegen zu koennen. mit irgendeiner art von baum laesst sich das zumindest entschaerfen.

bei music-x damals auf dem amiga war es auch so, dass man bei viel ctl und pitchbend
in der sequenz beim starten mittendrin erstmal bis 10 zaehlen konnte, ehe es losging.

erst fuer 'garbage collection' muss man dann alles durchrattern, das kann man
dann aber auf einen zeitpunkt legen, wo es nicht stoert,
oder wenns echtzeit sein muss, eben in kleinen portionen 'nebenbei' erledigen...


nixdestotrotz, ueber die 'verheiratung' von array und liste, stepsequenzer und eventrekorder
denke ich auch noch nach. das beste was mir bislang eingefallen ist, ist sowas
wie das, was inzwischen auch bei manchen stepsequenzern gemacht wird: microsteps.
also einfach mehr daten als nur eine note im step speichern. problem: wie geht man
mit events auf einem step um, die eigentlich zum vorherigen gehoeren?
(dh. ein kleines stueck "zu frueh" gespielte noten? also "unscharfen step-grenzen"...
 
Für nahezu jede Sprache gibt es Libraries mit fertigen höheren Datenstrukturen als Feldern (z.B. verkettete Listen, Bäume, Graphen), auch mit garbage collection.

Aber ich frage mich, wie Lengeling seinen Supertrack 1985 auf dem C-64 realisiert hat, so dass er in dieser unglaublichen Geschwindigkeit große Datenmengen auf 16 Kanälen wiedergeben und gleichzeitig aufnehmen konnte und das Editieren von Notenposition und -länge so effektiv vonstatten ging.

Das war keine Frage der Sprache, sondern des Datentyps und der Algorithmen auf diesem Datentyp.

Wenn man nämlich z.B. Noten löscht und dadurch freier Speicher mitten in einem Array entsteht, dann muss man irgendwann entweder selber aktiv garbage collection durchführen (wobei ein C-64 fühlbar lang braucht, um z.B. 16kByte im Speicher zusammenzuschieben, und so war das eben nicht), oder eben die "Löcher" geschickt verwalten.

Hier lohnt es sich, Gehirnschmalz zu investieren...
Spricht aus Deiner Seite etwas gegen die Linked list, die ich ja schon vorgeschlagen habe? Für Number crunching sind Arrays zwar besser, da es dann weniger Cache misses gibt. Bei einer (doubly) linked list elemente einzufügen und zu entfernen ist trivial und kann ich problemlos direkt bei der Aufnahme gemacht werden. Für Note-On Events kannst Du dann ja auch einfach noch einen Pointer zu dem Note-Off Event setzen. Dafür würde ich pro Spur ein NoteOnEvent* ActiveNoteOn[16][128] erzeugen, und bei einem NoteOff Event durch nachschauen, ob gerade ein NoteOn aktiv ist, und dann in dessen NoteOff Feld den Pointer zu dem neuen NoteOff-Event aktualisieren (solange die Note gespielt wird, ist es einfach Null).
 
die neuen daten an die passende stelle der liste 'einzuhaengen'.
alles supi, nur: kein random access! im gegensatz zum array.
wenn man an eine andere stelle in seinem stueck springen will, muss
man erst von irgendwo bis dort alles 'durchrattern', um endlich
loslegegen zu koennen. mit irgendeiner art von baum laesst sich das zumindest entschaerfen.
Das hier bei den aktuellen Prozessoren eine spürbare Latenz auftritt, kann ich mir eigentlich nicht vorstellen, aber vielleicht gehe ich zu sehr von Desktop-Prozessoren aus. Falls das Problem aber tatsächlich auftritt, würde ich ein Array erstellen, bei dem für jeden Taktanfang das nächste Event der Liste steht. Wenn ich neue Events einfüge oder lösche, muss ich dann halt mitracken, ob dies nun das erste Event in dem Takt ist, bzw. war. Falls in dem Takt kein Event (mehr) ist, trage ich ein Nullpointer ein. Wenn ich das erste Event suche, schaue ich erst beim aktuellen Takt nach, und falls da das Array auf einen Nullpointer verweißt, gehe ich das Array durch bis ich ein Event finde. Ist alles ein wenig Overhead, und ich persönlich würde das erst einbauen, wenn das Problem auch wirklich auftritt, aber das gute daran ist, das ich an den bisherigen Datenstrukturen und Funktionen nicht viel ändern muss.
 
Der Gedanke senkrecht vs. waagerecht ist in der Tat nicht uninteressant.
Bei Trackern ist das ja senkrecht und ich fand das auch immer irgendwie gut.
Habe das aber bisher nur mit den Notennamen in Listenform so gesehen, nicht mit den typischen Blöcken und Balken.
Der Vorteil wäre, dass die die Pianorolle dann wirklich wie ein Piano angeordnet wäre und nicht 90 Grad gedreht.
 
Spricht aus Deiner Seite etwas gegen die Linked list, die ich ja schon vorgeschlagen habe?
Gar nichts. In diese Richtung denke ich auch. Eine zweite LinkedList verwaltet die freien Speicherbereiche: "Record" geht dann durch die Liste, verwendet die Einträge und trägt sie in die Datenliste ein. Löschen, Einfügen, Editieren, alles dann sehr einfach und Fragmentierung ist irrelevant. Einzig Suchen dauert, wenn es zig-Tausende Events gibt, aber da könnte das von Dir vorgeschlagene Index-Array abhelfen.

Die Frage ist nur, ob es noch geschickter geht. Ich kann mir nicht vorstellen, dass Lengeling pro Event im Speicher 2 Bytes für die LinkedList verschwendet hätte bei nur 8500 maximal möglichen Events (da wären ja schon 17kByte nur dafür weg, und der Rechenaufwand sie zu verwalten erst).
 
Zuletzt bearbeitet von einem Moderator:
Spricht aus Deiner Seite etwas gegen die Linked list, die ich ja schon vorgeschlagen habe? Für Number crunching sind Arrays zwar besser, da es dann weniger Cache misses gibt. Bei einer (doubly) linked list elemente einzufügen und zu entfernen ist trivial und kann ich problemlos direkt bei der Aufnahme gemacht werden. Für Note-On Events kannst Du dann ja auch einfach noch einen Pointer zu dem Note-Off Event setzen. Dafür würde ich pro Spur ein NoteOnEvent* ActiveNoteOn[16][128] erzeugen, und bei einem NoteOff Event durch nachschauen, ob gerade ein NoteOn aktiv ist, und dann in dessen NoteOff Feld den Pointer zu dem neuen NoteOff-Event aktualisieren (solange die Note gespielt wird, ist es einfach Null).

Mir kommen noch ein paar andere (grundsätzliche) Ideen in den Sinn, die vielleicht einen Gedanke wert sind:
- Wo möglich Reduktion der Datenmengen durch Definition von Standard Werten, die nicht gespeichert und ggf. auch nicht prozessiert werden müssen (weil sie vorbelegt sind).
- Wo möglich ggf. bei Werten nur Multiples/Fractions (also nur den Faktor) oder Deltas (klassische Datenkomprimation) speichern. Auch relative Bezüge statt absolute Werte können je nach Nutzdaten zur Datenreduktion führen. Und Datenreduktion bedeutet oft (nicht immer) weniger Rechenzeit.
- Binäre Datenstrukturen weil diese dann Hardware-nahe prozessiert werden können
 
Super Idee!

Thema senkrecht vs. waagerecht: transposition (im mathematischen Sinne) ermöglichen, also eine Drehung um neunzig Grad, wobei die erste Spur ganz oben bzw. ganz links erscheint. Ich fände das großartig, da ich mit klassischen vertikalen Trackern, trackerähnlichen Programmen mit anderen Spurformaten und handelsüblichen MIDI-Sequenzern gearbeitet habe und nicht sicher bin, welche arbeitsweise ich auf einer hybriden Hardware-Software-Lösung wählen würde.
 


Neueste Beiträge

News

Zurück
Oben