In diesem Beispiel möchten wir einen Oszilografen (mit Spektrumanalyse) programmieren. Man kann sich darunter folgendes vorstellen: Ein elektrisches Signal wird grafisch dargestellt. Auf der X-Achse befindet sich die Zeit, auf der Y-Achse der Wert der elektrischen Spannung. Ein einfacher Sinuston wird dann z.B. als Wellenlinie dargestellt.
Bei der Spektrumanalyse werden Anteile einzelner Frequenzen im Signal berechnet. Heraus kommt dann ein Diagramm, mit der Frequenz auf der X-Achse und der Amplitude auf der Y-Achse.
Zur realisierung verwenden wir den Jack-Server unter Linux, der die Audio Ein-/Ausgabe standardisiert. Als GUI-Framework verwenden wir QT4. Für die Spektrumanalyse verwenden wir die fftw-Bibliothek.
Eine Herausforderung ist hier zweifellos die Synchronisation der drei Prozesse/Threads sowie deren Datenaustausch:
- Input, Jack-Server
- Berechnung
- Output, QT4-GUI
Was machen wir z.B. wenn neue Eingabedaten zur Verfügung stehen, während das GUI noch am zeichnen ist? Oder wenn das GUI nicht schnell genug zeichnet und nur jedes zweite Frame darstellen kann?
Betrachten wir zunächst die einzelnen Komponenten:
Input Prozess / Jack
Unsere Applikation meldet sich über das Jack-API beim Server an
client = jack_client_new ("testclient");
Danach teilen wir dem Server einen Pointer auf eine Prozedur mit, welche die Eingabe verarbeitet (jack-handler):
jack_set_process_callback (client, process, 0);
Diese Prozedur hat folgende Signatur:
int process (jack_nframes_t nframes, void *arg)
Damit die Prozedur die zu verarbeitenden Daten abrufen kann, wird folgende Funktion aufgerufen:
in = (jack_default_audio_sample_t *) jack_port_get_buffer (input_port, nframes);
Beobachtung: Wir erhalten immer dieselbe Bufferadresse zurück (&in).
Folgerung: Es sind weitere Buffers im Spiel: Zunächst wird ein Primärer Buffer mit Daten gefüllt und danach werden diese in den Sekundären Buffer kopiert, worauf der Client zugriff hat. Das ist nicht besonders effizient.
Synchronisation Input / Output
Der naheliegendste Ansatz, nämlich dass der Input bzw. der Jack-Handler nach jedem Durchlauf die Repaint-Prozedur des GUIs aufruft, funktioniert aus verschiedenen Gründen nicht:
- Unerwarteter Abbruch mit der Meldung: “zombified – calling shutdown handler”
- Die Repaint-Prozedur ist zu langsam.
- wenn diese direkt auf den Jack-Buffer zugreift, führt dies zu einem unerwarteten Programmabbruch ohne genauere Hinweise.
- Wird vorher eine Kopie des Buffers angelegt, gibt es Überschneidungen in der Darstellung. Da das Kopieren im Jack-Handler stattfindet und paralell dazu das GUI gezeichnet wird, kann es sein, dass auf der Ausgabe ein Teil des letzten und ein Teil des neuen Buffers dargestellt wird.
Wir haben zunächst – um überhaupt mal den Ansatz einer Lösung zu haben – einen simplen Ansatz gewählt, der zwar unbefriedigend aber funktioniert:
- Keine Synchronisation, unabhängige Threads/Prozesse für Eingabeverarbeitung und Darstellung, die paralell laufen.
- Es ist Glückssache, dass es keine Überschneidungen in der Darstellung gibt. Wenn die GUI-Prozedur einfach gehalten wird bzw. schnell genug läuft, sind die Überschneidungen nicht wahrnehmbar.
Warnungen
Wenn wir vom Jack-Handler aus die Repaint-prozedur aufrufen: “QPixmap: It is not safe to use pixmaps outside the GUI thread”
Fehler
Wenn wir von GUI-Paint-Event direkt auf den jack_port_buffer zugreifen, terminiert das Programm unerwartet nach ca. 430 Zugriffen.
Wenn wir vom Jack-Handler die Repaint-prozedur aufrufen: “zombified – calling shutdown handler” , nach dem 2. Aufruf