Schwierig ist etwas anderes: wir müssen einerseits dafür sorgen, daß der Interpreter den aktuellen Stand der Bedienelemente erfährt (falls sie denn aktiviert sind, ansonsten den letzten Stand, oder Defaults), und andererseits immer die gerade ausgerechneten MIDI-Signale an die Ausgabeschnittstelle abschicken.
Bei heutigen Rechnern ist es sicher so, daß insgesamt gesehen ein Midistrom mit Leichtigkeit "live" berechnet werden könnte. Für 30 Minuten Midi-Musik braucht man sicher keine 30 Minuten CPU-Zeit, sondern eher etwas bei 30 Sekunden. Die Klangerzeugung leistet ja der externe Synthesizer, wir schicken ihm nur immer mal ein paar Zahlen.
Aber so schön es wäre - wir werden es doch nicht schaffen, in Echtzeit zu rechnen, also das Midi-Signal genau dann zu erzeugen, wenn es ertönen soll. Wir müssen ja nicht nur im Mittel mit der Rechenzeit auskommen, sondern jedesmal (zwischen je zwei Noten).
Da kann aber soviel dazwischenkommen, zum Beispiel eine Garbage Collection des Haskell-Laufzeitsystems (obwohl es für GHC bald einen konkurrenten Kollektor geben soll) (was ist Garbage Collection überhaupt? Siehe Vorlesung über Grundlagen der Funktionalen Programmierung, Prof. Gerber.) oder - und das wollen wir ja - wir laden (d. h. parsen) ein Modul neu - das geht sicher nicht im Zeitraum zwischen zwei Sechszehntelnoten, jedenfalls nicht sicher.
Deshalb müssen wir ein bißchen puffern. Wir erzeugen immer etwas Midistrom auf Vorrat, und schicken den an den Puffer eines parallel laufenden Midi-Treibers, der das dann im exakten Timing abspielt. So weit so gut - wie groß sollte der Puffer (der Vorrat) sein?
Die Implementierung geht so, daß im GUI alle Sekunden ein Zähler tickt, der als Listener die folgende Aktion hat: nimm den derzeitigen IOStream, berechne die Ereignisse für die nächste Sekunde, und lege den Tail wieder weg. Wir beachten hier, daß wir nicht davon abhängen dürfen, daß wir exakt immer zur vollen Sekunde aufgerufen werden. Die Zeitansage ist hoffentlich immer exakt, aber die Abstände zwischen den Ansagen sind nicht genau vorhersehbar. Das brauchen wir auch nicht, da wir immer unsere "Eigenzeit" mitzählen, und zwar in exakten Einerschritten, und dann immer vergleichen, ob ein neuer Schritt erforderlich ist.
q <- liftIO $ newIORef s lasttime <- liftIO $ newIORef (0.0 :: Time) let emit t1 = do t0 <- readIORef lasttime let delta = t1 - t0 if delta > -0.2 then do writeIORef lasttime (t0 + 1.0) s <- readIORef q (pre, post) <- split 100 s emission h pre writeIORef q post else return () liftIO $ do e <- toStream time addListener e $ mkL emit everyTick 0.5Bei der Programmierung benutzen wir ganz heftig IORefs. Das ist einfach ein Zeiger auf einen Wert, den wir auch updaten dürfen. Das gibts ja sonst in einer rein funktionalen Sprache nicht, aber es hat alles seine Richtigkeit, die Operationen liegen in der IO-Monade (hugs-Modul IOExts)
data IORef a -- mutable variables containing values of type a newIORef :: a -> IO (IORef a) readIORef :: IORef a -> IO a writeIORef :: IORef a -> a -> IO ()Damit gibt es also fröhliches imperatives Programmieren. Das ist das, was wir in dieser Vorlesung eigentlich vermeiden wollten? Nein, wir haben nur elegante Hilfsmittel dazu bereitgestellt. Wenn es eben um IO-Aktionen geht, dann schreiben wir diese eben. Wenn es um reine Funktionen geht, dann nehmen wir diese. Der Vorteil von Haskell ist nicht, daß wir die IO-Sachen trotzdem können (das ist kein Vorteil, sondern eine Vorbedingung für die Benutzung der Sprache in der realen Welt), sondern daß wir sie, ihre Verbindung und ihre Argumente sehr sehr komfortabel hinschreiben können. (In unserem Fall kommen die Argumente ja aus dem Interpreter, und dessen Struktur ist eben überhaupt nicht imperativ, sondern folgt der Baumstruktur der Sprache, die wir interpretieren.)
Zurück zur Zeitfrage: Wir haben aber ein weiteres Problem: das mit der Sekunde können wir uns zwar wünschen, aber wo messen wir die: wir können in FranTk zwar nach der Systemzeit fragen, aber das ist nur die eine Seite. Am anderen Puffer-Ende steht ja der Midi-Device-Treiber, und der hat eben auch seine Zeitvorstellung, von der wir aber gar nichts mitbekommen. Wir sind aber auf jeden Fall daran interessiert, daß beide Uhren absolut gleich laufen, und der Puffer immer etwa gleich lang bleibt. Nun hängt ja beides an der gleichen Systemuhr, aber jeder teilt den Takt (FranTk nimmt Tcl/Tk, und dort gibts die Systemzeit in Mikrosekunden, der Miditreiber teilt die (gleiche) Uhr anhand der Midi-Spezifikation. Entweder rechnen wir das sehr genau nach, oder ... Es gibt eigentlich kein Oder. Zur absolut sicheren Lösung brauchten wir einen Treiber, der Bescheid sagt, wie voll sein Puffer ist, und wenn er eine bestimmte Länge hat, dann darf er eben nicht blocken und warten, sondern muß sofort ablehnen. Zur Not müssen wir das eben einbauen (TODO: link auf midid)