FMS - Developer’s Documentation

1) Allgemeines

a) Was ist FMS?

FMS ist ein freies Programm zur Synthese frequenter Klänge wie z.B. stimmhafte Laute oder Sinuswellen. Dabei wird die Wellenstruktur eines Lautes in einer Datei abgespeichert und kann dann mit nahezu beliebiger Frequenz und Dauer wiedergegeben werden. Da die Struktur der Schwingung mathematisch abgespeichert werden kann, ist der Speicherbedarf der “Sounddateien” äußerst gering. 36 Bytes reichen für eine Sinuswelle inklusive dem Namen aus. Als Betriebssystem wurde dabei bisher nur Linux getestet. Das Programm nutzt die Datei /dev/dsp zur Ausgabe der Sounds. Es ist wahrscheinlich, dass FMS auf jedem Betriebssystem, das eine solche Datei nach dem Schema eines aktuellen Linux-Kernels besitzt, läuft. Es sollte ein relativ leistungsfähiger PC sein, wenn die Sprachsynthesefähigkeiten ohne zu große Pausen zwischen den einzelnen Lauten verwendet werden soll. Auf meinem PC (AMD Athlon 1 GHz, 384 MB RAM) läuft dieses Feature ohne Probleme, auf einem Rechner mit 266 MHz, 32 MB RAM gab es starke Beeinträchtigungen (was auch an dem Sound-Blaster 1.0 gelegen haben kann). Eine aktuelle Version von FMS sollte unter

http://sourceforge.net/projects/fmsynth erhältlich sein. FMS heißt übri-

gens Frequency Module Synthesizer, hauptsächlich weil mir kein besserer englischer Name eingefallen ist, als ich das Projekt bei Sourceforge anmeldete.

b) Lizenzierung und Version

FMS wird unter der Gnu General Public License (GPL) veröffentlicht, die

unter http://www.gnu.org/licenses/gpl.html einzusehen ist. Dieses Doku-

ment darf unter den Bedingungen der Gnu Free Documentation License (FDL,

http://www.gnu.org/licenses/fdl.html) weitergegeben und verändert wer-

den.

Eine wichtige Grundlage für den Erfolg von Open Source besteht darin, dass Programmierer auf der ganzen Welt ein Programm verstehen und verbessern können. Durch dieses Dokument soll gewährleistet werden, dass dies nicht allzu schwierig ist.

Diese Dokumentation bezieht sich auf die Version 0.52-1 vom 28.06.2002,

die unter http://sourceforge.net/projects/fmsynth verfügbar sein sollte.

Teile können sich auch auf spätere Versionen beziehen.

2) Code Walkthrough

a) checkint.cpp / checkint.h

Diese Headerdatei enthält eine Funktion zur Benutzereingabe und Überprüfung eines Integers. Als Parameter übernimmt sie die Variablen count, min und max, die eingelesene Zahl, die niedrigste und die höchste erlaubte Ganzzahl. Durch

   return checkint(-1, min, max);

wird die Funktion im Fehlerfall so oft wiederholt, bis der Benutzer endlich eine passende Zahl eingegeben hat. Zur Konvertierung des eingelesenen Strings in einen Integer wird die stdlib-Funktion atoi verwendet. Diese liefert im Fehlerfall 0 zurück, weshalb die Verwendung von checkint bei Zahlen, die 0 sein dürfen, nicht zu empfehlen ist. Auch negative Zahlen sollten nicht überprüft werden, da bei der Zahl -1 als erlaubte Zahl auch bei einer Falscheingabe -1 zurückgegeben wird (siehe oben). Eine fast gleiche Funktion (checkfloat) überprüft float-Dezimalzahlen.

Die Funktion chooseint ist etwas komplexer, da sie zum auswählen einer Zahl aus einem Array dient. Als Argumente übernimmt sie außer dem aktuellen Wert value einen Pointer auf ein Array von Integern und die Anzahl der Integer in dem Array. Die Funktion enthält den gleichen Mechanismus wie checkint und checkfloat, wiederholt sich also immer wieder bis eine richtige Zahl eingegeben wird.

b) fmdisplay.cpp

Das Programm fmdisplay ist praktisch baugleich zu xfmdisplay, nur stellt es den Wellenverlauf nicht grafisch in einem X-Fenster sondern auf Konsole dar. Dazu wird ein zweidimensionales Array data[x][y] verwendet, das mit zwei ineinander verschachtelten for-Schleifen Zeichen für Zeichen, Zeile für Zeile ausgegeben wird. Außerdem hat fmdisplay nicht die Fähigkeit, gemischte Töne, also mehrere Töne gleichzeitig, darzustellen.

c) fmfile.cpp

Da die Funktionen zum Schreiben der eingelesenen Daten in eine Datei in dem Header fmofstream stecken, ist der Inhalt von fmfile.cpp ziemlich trivial.

Das Programm liest nacheinander die Werte für den Speichermodus, die Bezeichnung des Frequenzmusters und ggf. der Anzahl der Werte ein. Dann kommt der u.U. interessanteste Teil, das Einlesen der eingentlichen Werte.

Da beim Modus 1 (in der Kommunikation mit dem User Modus 2 genannt) die endgültige Anzahl der Werte nicht bekannt ist, wird eine Pointerlist zur Speicherung der Werte verwendet. Die struct VTLP (value-time-linetype-pointer, siehe fmofstream.h) enthält außer den eingelesenen Werten einen Pointer auf den jeweils nächsten Werteverbund. Aus diesem Pointer (VTLP *) kann wenn nötig mit new ein neues VTLP im Speicher erzeugt werden. Es gibt im Programm einen VTLP * first, der auf die erste Wertestruktur verweist, einen VTLP * recent, der auf die aktuelle Wertestruktur verweist und einen VTLP * before, der auf die vorhergehende Wertestruktur verweist. Vom Benutzer wird zuerst die Eingabe des Zeitindexes verlangt, der mit checkint auf seine Gültigkeit überprüft wird. Dann wird der Wert eingelesen, der auf seine Gültigkeit im Bezug auf den vorherigen Wert und Linienstil geprüft wird. War der vorherige Wert z.B. 100 und der Linienstil eine aufsteigende Sinuskurve, so darf der aktuelle Wert nicht kleiner oder gleich 100 sein. Als nächstes wird der auf den aktuellen Wert folgender Linienstil eingelesen und überprüft, ob er in Verbindung mit dem aktuellen Wert möglich ist. Ist der aktuelle Wert z.B. 255 (der Maximalwert, in der Userkommunikation 256), so darf der darauffolgende Linienstil nicht eine aufsteigende Sinuskurve sein. Die Schleife wird wiederholt, bis der aktuelle Linientyp 0, also “letzter Wert” ist. Eine maximale Anzahl von 16777216 Werten ist gegeben, daher muss auch der Linienstil mit dem Zeitindex 16777215 0 sein. Zusätzlich wird die Anzahl der Bytes pro Wert, die zum heutigen Entwicklungsstandpunkt nur 1 sein darf, eingelesen. Als letztes wird alles in die Datei geschrieben. Dies erledigt die polymorphe Funktion values (fmofstream). Für den älteren und in vielen Fällen festplattenplatzintensiveren Speichermodus 0 ist im Prinzip die gleiche Vorgehensweise nötig. Nur brauchen wir hier keine sich an den Schwänzen festhaltenden Elefanten, da die Anzahl der Werte von Vorne herein bekannt ist. Es werden immer der Zeitindex und der dazugehörige Wert eingelesen und überprüft. Dann werden mit Hilfe des mit \frac{ΔW ert}{ΔZeit} errechneten Steigungsfaktors faktor die benötigten Werte nacheinander berechnet und in das Array valarray geschrieben. Wenn der eingegebene Zeitindex dem maximalen Zeitindex (also valuecount) entspricht, beendet die Funktion ihre Arbeit und übergibt die Daten an die polymorphe Funktion values (siehe fmofstream).

Die Funktion line_help gibt Hilfe für im Bezug auf die Auswahl des richtigen Linienstils unsichere, ist aber vom Code her uninteressant (ich habe schwer mit mir gekämpft, ob ich sie überhaupt erwähnen soll).

d) fmifstream.cpp / fmifstream.h

Dieser Header hat die Fähigkeit, die Daten aus einer von fmfile gespeicherten Datei zu lesen und einem Programm zur Verfügung zu stellen. Nach der Definition einer Fmdata-Klasse wird die Funktion Fmdata::init mit einem Argument, dem Dateinamen aufgerufen. Als erstes versucht die Funktion, die Datei zu öffnen und setzt bei einem Fehler den Fehleranzeiger error auf 1:

   fd = open(filename, O_RDONLY); error = 0;
   if(fd < 1) { error = 1; }

Dabei werden nicht die C-Funktionen zur Dateibehandlung verwendet, sondern die UNIX- (in diesem Fall Linux-)eigenen. Deshalb müssen auch die dazu benötigten Header eingebunden werden:

   #include <fcntl.h>
   #include <unistd.h>
   #include <sys/stat.h>
   #include <sys/types.h>

Danach werden die Angaben über den Speicherungsmodus, die Länge der Bezeichnung, die Bezeichnung selbst, die Anzahl der (eingegebenen) Werte, die Anzahl der Bytes pro Wert (wobei bisher nur ein Byte unterstützt wird und deshalb bei einer anderen Anzahl als 1 ein Fehler zurückgegeben wird) der Reihe nach eingelesen.

Ist der einfache Modus 0 der Speichermodus, so können die Werte in einen String eingelesen werden. Beim Modus 1 ist das schon schwieriger. Wie weiter unten im Kapitel 3 über die Speichermodi beschrieben besteht ein Wert im Modus 1 aus 5 Bytes. Dabei stehen die ersten drei für den Zeitindex in einem 256er-System, der vierte für den Wert und der fünfte für den darauffolgenden Linienstil. Dabei werden die Werte in ein struct namens VTL (value-time-linetype) aus einem Integer für den Zeitindex und jeweils einem unsigned char für Wert und Linienstil eingelesen. Der Datentyp unsigned char ist dabei von großer Wichtigkeit, da die Werte sonst auch negativ sein könnten.

Sollte bei einem der eingelesenen Werte etwas Unerwartetes auftreten, so wird der Fehlerindikator error auf 1 gesetzt. Dies kann vom den Konstruktor aufrufenden Programm behandelt oder einfach ignoriert werden. Auch wenn die Datei nicht existiert werden die Funktionen weiter durchlaufen, weshalb die Variablen bei einem Fehlerindikator von 1 mehr oder weniger zufällige Werte enthalten. Deshalb ist es nicht es zu empfehlen, diesen Datenmüll auch noch auszugeben.

Der Destruktor räumt auf, indem er die noch geöffnete Datei schließt und je nach Speicherungsmodus entweder das Array von chars oder VTLs, das vorher mit new im Speicher erzeugt worden ist, mit destroy zunichte macht.

Der Header fmifstream wird von den Prorgammen fmplay, fmdisplay und xfmdisplay verwendet und kann ideal in andere GPL-Projekte, die fertige FMS-Dateien nutzen wollen, eingebunden werden.

e) fmlanguage.cpp / fmlanguage.h

Um FMS mehrsprachig zu machen, werden die Strings zur Kommunikation mit dem User in der Headerdatei fmlanguage initialisiert. Dabei wird in jedem der Konstruktoren der verschiedenen Klassen die folgende Programmstruktur verwendet, um den char * einen String zuzuweisen:

   char tmp[40];
   strcpy(tmp, "Programmtitel");
   title = new char[strlen(tmp)+1];
   strcpy(title, tmp);

Der eigentliche String wird hier in die Variable tmp kopiert, aus dem char * (hier title) wird ein neuer String mit der Länge von tmp plus der nötigen binären Null als Kennzeichen für das Stringende und schließlich bekommt title auch den Inhalt von char. Für die Stringoperationen wird der Standartheader cstring gebraucht, tmp kann dabei natürlich wiederverwendet werden.

In jedem Programm werden einige dieser Strings benötigt. Die Klassenvariable heißt dabei immer t. So ist in fmfile zum Beispiel t vom Typ text_fmfile und t.title enthält die Bezeichnung des Programms.

Eine nicht allzu schwere, aber doch sehr nützliche Arbeit ist das Übersetzen der Strings in eine andere Sprache. Dabei ist lediglich darauf zu achten, dass tmp mindestens so lang wie der längste String der jeweiligen Klasse sein muss. Bitte senden Sie übersetzte fmlanguage.cpp-Dateien an mich <daniel_gruen@web.de>. Achten Sie aber unbedingt auf eine genügend große tmp-Variable, da sonst unweigerlich Segementation Faults dem schönen Programmdurchlauf ein jähes Ende setzen.

f) fmofstream.cpp / fmofstream.h

Der Header fmofstream enthält Funktionen zum Speichern von fms-Daten. Ich erkläre die einzelnen Funktionen hier von unten nach oben:

fmopen:

Diese Funktion übernimmt einen Dateinamen als Parameter und öffnet diesen, wobei sie bei Nichtvorhandensein die Datei mit den Attributen rw-r–r– erzeugt. Sie gibt einen Integerwert als UNIX-Filedescriptor zurück, der im Fehlerfall -1 ist.

values:

Bei den beiden Funktionen values handelt es sich um s.g. polymorphe Funktionen. Da die Namen gleich sind, wird die richtige Funktion beim Aufruf an den Argumenten erkannt. Während beim einfacheren Speichermodus 0 nach Umrechnung der Werteanzahl in ein 256er-System einfach nur das Wertearray in die Datei geschrieben werden muss, ist values für den Speichermodus 1 (in der Userkommunikation Modus 2 genannt) nicht ganz so trivial. Der Funktion wird ja nur ein Pointer auf den ersten VTLP-Komplex übergeben, aus dem die Werte erst ausgelesen werden müssen. Dabei nutzt die Funktion die Eigenschaft der Pointerlist, durch die die Wertestrukturen bildlich gesprochen wie Elefanten, die sich an den Schwänzen halten, aneinander hängen. Man geht vom letzten Elefanten bis zum ersten durch und schreibt immer die Werte in die Datei. Weitere Informationen über die Dateiformate enthält Kapitel 3.

name:

Diese Funktion schreibt schlicht und ergreifend die Länge der Bezeichnung und dann die Bezeichnung selbst in die Datei.

mode:

Diese bei näherem Hinsehen ziemlich dreckig geschriebene Funktion überprüft ein letztes Mal den eingegebenen Speichermodus und schriebt ihn ggf. in die Datei. Sollte der Modus falsch sein, liest sie ihn nochmals ein.

cleanup:

cleanup schließt die Datei.

g) fmplay.cpp

Für den “Normaluser” ist das Programm fmplay das Herzstück von fms.

Es spielt von fmfile erstellte Dateien mit einer beliebigen Frequenz, Lautstärke und Länge ab. Zusätzlich kann es mehrere Töne mixen, also gleichzeitig abspielen. Informationen über die Soundausgabe mit Hilfe von /dev/dsp erhält man beispielsweise in der /dev/dsp-Programmierung-Dokumentation von

FMS unter http://fmsynth.sourceforge.net/dsp.dvi. Nur die zusätzlich

benötigten Informationen bietet diese Dokumentation. Zuerst werden die Kommandozeilen-Argumente geparst. Dies erweist sich als etwas schwieriger, da die Abspieloptionen von fmplay doch ziemlich komplex sind. Dabei gibt es Optionen, die nur für die einzelne Sounddatei gelten und solche, die für ein ganzes so genanntes “sound package” Bedeutung haben. Ein “sound package” ist ein abspielfertiger, gemixter und lautstärkegeregelter Arrayabschnitt von Soundausgabewerten.

Zuerst wird die Anzahl der Soundpakete festgestellt und in pcount gespeichert, wobei das Trennargument -n ein neues Paket einleitet. Die danach eingeführten structs package_attributes dienen zur Speicherung der jeweiligen Optionen. Dann werden die für die jeweiligen Soundpakete wichtigen Werte zuerst auf 0 bzw. bei der Lautstärke auf den maximalen Wert gesetzt und - sofern vorhanden - aus den Kommandozeilenargumenten ausgelesen. Die Structinternen-Variablen sounds enthalten dabei die Anzahl der in einem Soundpaket gleichzeitig abgespielten Sounds, dabei gelten alle Argumente, die nicht mit einer Zahl oder einem Bindestrich beginnen als Dateien. time enthält den Wert für die zeitliche Länge des jeweiligen Soundpaketes, dem Zahlenwert nach dem -t - Argument. Der Parameter zu -V schließlich enthält die Gesamtlautstärke in Prozent für das Soundpaket, die in der jeweiligen vol-Variablen gespeichert wird.

Die nächste Schleife findet die jeweils erste zu einem Soundpaket gehörende Datei und legt zudem die Gesamtanzahl der Dateien (allfiles) fest.

Für jede dieser Dateien wird zunächst eine struct namens file_attributes (siehe fmvalue.cpp / fmvalue.h) eingeführt und der Dateiname in die jeweilige name-Variable eingelesen, place auf den Platz im Kommandozeilenparameterarray und andere Werte auf 0 bzw. einen Standartwert gesetzt.

Nach Einführung der Fmdata-Klassen für jede der Dateien werden diese durch die jeweilige init-Funktion eingelesen und Fehler behandelt (siehe fmifstream.cpp / fmifstream.h).

Die place-Variablen in den file_attributes-structs finden ihre Anwendung beim Parsen der dateispezifischen Argumente als da wären die auf -f folgende Frequenz, und die auf -v folgende relative Lautstärke. Die relative Lautstärke gibt die Lautstärkendominanz eines Tones innerhalb des Soundpaketes an. Der Standartwert ist 100. Setzt man alle relativen Lautstärken innerhalb eines Soundpaketes auf 1, so wird das Soundpaket dadurch in der Gesamtheit nicht leiser und auch die Lautstärkegewichtungen ändern sich nicht. Nur mit dem -V - Argument für die Gesamtlautstärke in % und dem -h-Argument (Hüllkurve) kann man auf die Endlautstärke eines Soundpaketes Einfluss nehmen.

Wenn die Werte in einer ansprechenden Art und Weise ausgegeben sind und gleichzeitig noch einmal überprüft wurden, werden die relative Gesamtlautstärke (die Summe der relativen Lautstärken) jedes Soundpakets und die Gesamtdauer aller Soundpakete (time) bestimmt. Jetzt ist das Kommandozeilenparsen beendet.

Die folgenden IOCTLs sind in o.g. /dev/dsp - HowTo genauer beschrieben. Standartmäßig wird eine Samplingrate von 44100 und das standartmäßig von jeder Soundkarte unterstützte Ausgabeformat AFMT_U8 verwendet. Um auch bessere Soundkarten auszureizen aber trotzdem noch alte SoundBlaster 1.0 zu unterstützen werden diese Werte in den kommenden Versionen wählbar sein.

Das Wertearray mit dem Pointer data bekommt die richtige Größe, nämlich Samplingrate ⋆ \frac{Dauer}{s}.

Nun kommt der eigentliche Kern des Programms, die drei inenander verschachtelten Schleifen, die die Töne generieren. Die Schleife k (benannt nach der Zählvariablen) wird für jedes Soundpaket einmal, also pcount mal durchlaufen. Es wird die Variable oldptr auf die Verschiebung des ersten Werte des aktuellen Soundpaketes pn[k] vom ersten Wert im data-Array aus gesetzt und die Gewichtung der willkürlichen, jetzt noch im Array vorhandenen Daten auf 0.0 festgelegt. Die in k enthaltene Schleife i wird für jede der Dateien des jeweiligen Soundpakets einmal durchlaufen, also pn[k].sounds mal, wobei darauf zu achten ist, dass i am Anfang bereits dem Stellenwert der ersten Datei in den file_attributes-Variablen entspricht. Darin wird der Pointer ptr auf den gewünschten ersten Wert im Sounddatenarray gesetzt und je nach Modus die jeweilige if-Schleife gestartet.

Für den Modus 0 muss der von fmval (siehe fmvalue.cpp / fmvalue.h) berechnete Wert in avalue nur noch mit der gewünschten Lautstärke multipliziert werden. Sollte für die jeweilige Datei eine Lautstärkekurve verwendet werden, so wird der aktuelle Lautstärkekurvenwert hvol auch mit Hilfe von fmval berechnet. Ist dies nicht der Fall, steht hvol auf dem Maximalwert 255.

Beim Modus 1 ist ein weiterer Schritt nötig. Die Funktion fmval benötigt zur Berechnung des aktuellen Wertes die Variable v, den Index des in der Datei angegebenen Wertekomplexes (VTL: value-time-linetype) vor dem aktuellen Wert mit dem Zeitindex atime. Dieser Wert wird durch die while-Schleife berechnet. Sollte fmval -2 zurückgeben, so ist ein unbekannter Linienstil aufgetreten und das Programm bricht ab.

Die errechneten Werte werden über den Pointer *ptr in das Array data geschrieben. Am Schluss jedes Schleifendurchlaufs von j muss der Wert atime für den aktuellen Zeitindex auf den im nächsten Durchlauf gültigen Wert gebracht werden, wobei das Produkt von maximalem Zeitindex und Frequenz durch die Samplingrate dividiert und zum alten aktuellen Zeitindex addiert wird. Mit der Floting-Point-Modulo-Funktion fmod wird Sorge getragen, dass der aktuelle Zeitindex nicht den maximalen Zeitindex maxtime, den Zeitindex des letzten VTL-Elementes, überschreitet.

Nach einem i-Durchlauf wird die Gewichtung der bereits im Array befindlichen Werte auf 1 gesetzt, um die alten Werte nicht komplett zu überschreiben sondern eben zu mixen. Der Rest des Programms steht im /dev/dsp - HowTo beschrieben.

h) fmvalue.cpp / fmvalue.h

Dieser Header enthält die zum Abspielen - egal ob durch Grafik oder Sound - benötigte Funktionen und Klassen.

Die Struktur file_attributes enthält die Eigenschaften einer einzelnen Sounddatei, die Bedeutung der einzelnen Variablen ist wie bei package_attributes direkt in fmvalue.h beschrieben.

Der wichtigste Teil von fmvalue ist die polymorphe Funktion fmval zur Berechnung des aktuellen Ausgabewertes.

Für den relativ einfach wiederzugebenden Modus 0 reicht die Rückgabe des am ehesten passenden Wertes aus dem Wertearray. Dies gibt zwar keinen sonderlich sauberen Klang, der Modus 0 ist allerdings seit Einführung des Modus 1 nur für Spezialsounds wie z.B. Pausen gedacht, wo die Dateigröße noch etwas kleiner ist und es sowieso nicht auf den Klang ankommt.

Wie leicht an der größeren Zeilenzahl zu erkennen ist, ist das Abspielen von Dateien im Speichermodus 1 nicht ganz so trivial. Es muss mit Hilfe einer sich hier anbietenden switch-Anweisung zwischen den verschiedenen Linienstilen unterschieden werden. Recht einfach gestaltet sich das beim Linienstil 1, einer geraden Linie. Durch eine ähnliche Vorgehensweise wie bei der Erstellung der Werte für Modus 0 in fmofstream wird mit Hilfe des Steigungsfaktors \frac{ΔW ert}{ΔZeit} und dem Verhältnis von dem zeitlichen Abstand zwischen vorherigem Wert und aktueller Zeit zu dem zeitlichen Abstand zwischen aktueller Zeit und darauffolgendem Wert ausgerechnet.

Bei den vier verschiedenen Sinuskurven (aufwärts, linearer Anfang; abwärts, gekrümmter Anfang; abwärts, linearer Anfang; aufwärts, gekrümmter Anfang) wird der Wert aus dem mit 90 malgenommenen Verhältnis von dem zeitlichen Abstand zwischen vorherigem Wert und aktueller Zeit zu dem zeitlichen Abstand zwischen vorherigem Wert und darauffolgendem Wert ausgerechnet. Da der aktuelle Wert nicht zeitlich hinter dem darauffolgenden Wert liegen kann, ist das Ergebnis höchstens 90. Je nach Abschnitt der Sinuskurve wird 0, 90, 180 oder 270 hinzugerechnet und der Sinus davon genommen. Dabei muss der Wert, von dem der Sinus genommen werden soll mit \frac{π}{180} multipliziert werden, um den bekannten Sinuswert, der bei 0 0, bei 90 1, bei 180 0, bei 270 -1 und bei 360 wieder 0 ist zu bekommen. Je nachdem ob der Wert negativ oder positiv, mit steigendem Zeitindex abnehmend oder zunehmend ist, muss er zum vorherigen Wert dazugerechnet, zum darauffolgenden Wert dazugerechnet, vom vorherigen Wert abgezogen oder vom darauffolgenden Wert abgezogen werden.

Sollte der Modus keinem der bekannten Modi entsprechen, so gibt die Funktion -2 zurück. In seltenen Fällen kann der Modus 0 vorkommen, hier wird einfach -1 zurückgegeben, ein Zeichen dafür, dass atime dem Zeitindex des letzten VTL-Elements eines Tons entspricht oder ihn übersteigt.

i) xfmdisplay.cpp

Will man eine von fmfile erstellte Datei grafisch darstellen, so ist xfmdisplay das Mittel der Wahl. Es öffnet ein X-Windows-Fenster und zeichnet das Oszillogramm der gewählten Datei darin mit SDL und SDL_gfx (erhältlich unter

http://libsdl.org und http://ferzkopp.net/Software.html.

Die Darstellung von Dateien im Modus 0 ist im FMS-SDL-Wave-HowTo

unter http://fmsynth.sourceforge.net/sdlwave.dvi beschrieben. Ich gehe

hier nur auf dir zusätzlich nötigen Maßnahmen für den Modus 1 und andere, im HowTo nicht beschriebene, Arbeitsschritte ein.

Zuerst werden die Kommandozeilenargumente geparst und ggf. abgebrochen bzw. fehlende Argumente eingelesen.

Als Zwischenspeicher für die Werte dient das Integer-Array value. Es wird nicht mit der üblichen new-Anweisung eingeführt, da dies seltsamerweise einen SDL-Fehler verursacht.

Ein Raster hilft beim Ablesen der Werte. Dies ist zum Verständnis der Mixeigenschaften besonders wichtig.

Bei der Berechnung der Werte wird im Prinzip gleich vorgegangen wie in fmplay, nur dass hier z.B. die Berechnung des aktuellen Zeitindexes einfacher ist. Er wird einfach in jedem Durchlauf der Quotient von maximalem Zeitindex und Anzahl der Pixel der X-Achse erhöht. Es gilt ja schließlich: größerer maximaler Zeitindex -> größere Zeitschritte pro Pixel, größere Anzahl der X-Achsen-Pixel -> kleinere Zeitindexschritte pro Pixel. Der aktuelle x-Wert entspricht hier einfach der Zählvariablen + 1, wird also bei jedem Durchlauf um eins hochgezählt. Um den aktuellen y-Wert im richtigen Format (also zwischen 0 und dem Maximalwert der y-Achse und positiv) zu erhalten, ist noch etwas Jonglieren nötig. Besonders auch, da beim Mixen mehrerer Sounds ein gültiger Gesamtwert herauskommen soll. Das Zusammenrechnen und Ausgeben der endgültigen, gemischten Werte ist dann nicht mehr schwierig.

Die durch das Argument -a angeforderte “animierte” Darstellung wird durch dauerndes Auffrischen des Bildschirms erreicht. Um die verschiedenen Töne in unterschiedlichen Farben darzustellen, werden bei jedem Durchlauf die Werte für Rot, Grün und Blau geändert.

j) Makefile

Das sogenannte Makefile ist die Steuerdatei für das Kommando make. Sie enthält Informationen darüber, welche Programme bei Änderung welcher Sourcecodedateien neu Compiliert werden müssen. Dies ist sehr nützlich, wenn man nicht jedesmal wegen einer kleinen Änderung im Sourcecode alles neu compilieren oder einen komplexen Befehl aufrufen will. Ein einfaches make genügt. Durch Angabe eines s.g. Targets kann man auch nur einzelne Programme compilieren lassen. Der Befehl make fmfile z.B. erstellt nur das Programm fmfile. Mit make clean entfernt man die ungelinkten *.o-Dateien und zusätzlich temporäre Dateien meines Lieblings-Konsolen-Editors vim.

3) Dateiformate

a) Modus 0 - jeder Wert

Das Format 0 (in der Kommunikation mit dem User Format 1 genannt) ist die einfachste denkbare Möglichkeit zur Speicherund eines Oszillogrammes. Es wird dabei einfach jeder Wert (natürlich mit einem bestimmten, mehr oder weniger groben, Raster) in die Datei geschrieben. Da für diese Art der Speicherung relativ viel Festplattenplatz benötigt wird, besonders wenn das Ergebnis genau sein soll, ist es nur noch in Sonderfällen empfehlenswert, Dateien im Format 0 zu speichern. Das komplette Format baut sich byteweise wie folgt auf, wobei Werte in eckigen Klammern für beispielhafte Stellen stehen:

Stelle  Bedeutung  Beispiel
0  Modus, beim Modus 0 natürlich immer 0  0
1  Anzahl der Buchstaben der Bezeichnung  8
2[-9]  Bezeichnung  Sägezahn
[10]  Anzahl der Werte, 65536er-Stelle  0
[11]  Anzahl der Werte, 256er-Stelle  1
[12]  Anzahl der Werte, 1er-Stelle  44
[13]  Anzahl der Bytes pro Wert  1
[14-313]  Werte  0 ... 255

Die komplette Anzahl der Werte setzt sich also aus drei Stellen eines 256er-Systems zusammen. Bisher wird keine andere Bytezahl pro Wert als 1 unterstützt.

a) Modus 1 - einzelne Werte, verbunden mit

Geraden und Sinuskurvenabschnitten

Das Format 1 (in der Kommunikation mit dem User Format 2 genannt) ist in Hinsicht auf Speicherplatz und Wiedergabe von Kurven sehr viel besser geeignet. Es werden nur wenige Werte gespeichert, zusammen mit ihrem Zeitindex und dem darauffolgenden Linienstil. Da die eigentlichen Ausgabewerte zur Laufzeit berechnet werden, benötigt das Abspielen mehr Prozessorleistung, allerdings ist die Platzersparnis und die bessere Kurvenwiedergabe das wert. Das Format setzt sich byteweise wie folgt zusammen:

Stelle  Bedeutung  Beispiel
0  Modus, beim Modus 1 natürlich immer 1  1
1  Anzahl der Buchstaben der Bezeichnung  5
2[-6]  Bezeichnung  Sinus
[7]  Anzahl der gespeicherten Werte, 65536er-Stelle  0
[8]  Anzahl der gespeicherten Werte, 256er-Stelle  0
[9]  Anzahl der gespeicherten Werte, 1er-Stelle  5
[10]  Anzahl der Bytes pro Wert  1
[11-35]  Werte  siehe unten

Die Anzahl der Bytes pro Wert gibt nur die Bytes für den eigentlichen Wert, nicht aber die für Zeitindex und Linienstil an. Dabei setzt sich ein Wertekomplex immer aus fünf Bytes wie folgt zusammen:

Stelle  Bedeutung  Beispiel
0  Zeitindex, 65536er-Stelle  0
1  Zeitindex, 256er-Stelle  0
2  Zeitindex, 1er-Stelle  1
3  Wert  255
4  darauffolgender Linienstil siehe unten

Die 6 Linienstile sind:

Nr.  Bedeutung
1  gerade Linie
2  Sinuskurvenabschnitt, aufwärts, linearer Anfang
3 Sinuskurvenabschnitt, abwärts, gekrümmter Anfang
4  Sinuskurvenabschnitt, abwärts, linearer Anfang
5 Sinuskurvenabschnitt, aufwärts, gekrümmter Anfang
0  letzter Wert

Durch diese Linienstile werden die angegebenen Punkte verbunden.

Daniel Grün, 28.06.2002