Nachdem wir uns im ersten Teil der Serie mit der Theorie des Samplings beschäftigt haben, wird es nun konkret. Wir präsentieren den Quell-Code für ein modulares Rahmenprogramm zum Wandeln von Sample-Files und die ersten zwei Module für das AVR- und das Rohdaten-Format.
Das Programm „VLUN“ soll verschiedene Sample-File-Formate ineinander umwandeln. Damit es leicht an verschiedene Formate anzupassen ist, haben wir ein modulares Konzept entwickelt, das eine Erweiterung jederzeit sehr einfach macht. Jedes Modul enthält nämlich sämtliche zu einem Format gehörigen Funktionen.
Das VLUN.C-Rahmenprogramm soll in seiner jetzigen Form nur als einfaches Beispiel dienen, wie man diese Module sinnvoll miteinander kombiniert. Es sollte aber für jeden Programmierer leicht möglich sein, es um die nötige interaktive Benutzereingabe, eine Kommandozeilenauswertung oder gar eine GEM-Oberfläche zu erweitern. Sämtliche Programm-Module verwenden für das File-I/O die Standard-C-Funktionen, somit läßt sich das Programm auch auf andere Computer (mit Bigendian-Zahlenformat) portieren.
Die Integer-Bit-Größe ist 16 Bit, Long bedeutet 32 Bit. Für GNU C sind also kleine Anpassungen nötig (z.B. #define int short in VLUN.H). Wir liefern das Project-File für Pure C in einer der nächsten Folgen mit.
Zu jedem Formatmodul gehören die folgenden Funktionen:
Header-Test: Diese Funktion überprüft, ob es sich beim Input-File um das File-Format, für das das Modul zuständig ist, handelt und liest, wenn dem so ist, die für die Wandlung nötigen Header-Informationen aus.
Wandlung ins Standardformat: Liest die Sounddaten blockweise und wandelt sie in das interne Standardformat (16 Bit signed Stereo).
Wandlung ins Zielformat: Konvertiert vom Standardformat blockweise ins jeweilige Modulformat und schreibt die Daten.
Header schreiben: Baut einen dem Format entsprechenden Header auf und schreibt ihn an den File-Anfang.
File abschließen: Manchmal sind Zusatzinformationen im File nötig, die erst nach der kompletten Wandlung bekannt sind. Diese werden damit in das File eingefügt.
Für einige Formate sind nicht alle Funktionen nötig. So braucht man für Rohdaten zum Beispiel keinen Header zu schreiben. Andere Funktionen sind für verschiedene Formate gleich.
Intern werden die zur Wandlung nötigen Informationen des Input-Samples in der folgenden Struktur festgehalten:
typedef struct
{
int Typ;
int Format;
long Freq;
long BufLen;
long DataLen;
int SizeFac;
int Div;
} SAMPLE_SPEC;
Der Typ gibt an, um welches File-Format es sich handelt. Mögliche Werte sind die in VLUN.H definierten Konstanten.
In Format ist festgehalten, wie die Sounddaten konkret im File gespeichert sind. Jedes Bit entspricht einer bestimmten Eigenschaft, die als Format-Flags ebenfalls in VLUN.F1 definiert ist. Ein typisches Falcon-Sound-File hätte z.B. in Format die Flags für SIGNED, BIT16, STEREO und BIGENDIAN gesetzt. Das heißt es sind vorzeichenbehaftete 16-Bit-Sample-Werte im Motorola-Zahlenformat mit 2 Kanälen. Das Nichtgesetztsein eines Flags entspricht jeweils dem Fehlen der Eigenschaft.
Die Sample-Frequenz in Hz wird in der Variablen Freq gespeichert.
BufLen gibt an, in welchen Blöcken das File geladen werden muß. Diese Blocklänge in Bytes wird meist nur vom Arbeitsspeicher, den man belegen will, bestimmt und ist in BUFSIZE festgelegt. Bei blockweise komprimierten Sounddaten (wie DSVM-Deltapack) ist es jedoch naheliegend, die BufLen auf die Größe eines Blocks im File zu setzen.
DataLen gibt an wieviele Sounddaten (in Bytes) das Input-File enthält - das ist aber nur in einigen Fällen bekannt und nötig. Bei einigen File-Formaten kann es sein, daß nach den Sounddaten andere Daten folgen. In diesem Fall wird, DataLen als Zähler benutzt, der die noch nicht gewandelte Datenlänge angibt (z.B. WAVE-Format).
SizeFac gibt an, um welchen Faktor die Daten länger werden, wenn sie vom Input-Format ins Standardformat gewandelt werden, z.B. ist SizeFac 4, wenn 8-Bit-Mono-Daten vorliegen (Mono->Stereo, 8 Bit->16 Bit). Diese Information ist nötig, um den für die Wandlung benötigten Buffer bereitzustellen.
Div ist in den bisherigen Formaten nicht benutzt. Sollte es nötig sein, kann man damit formatspezifische Informationen vom Header-Test zu anderen Funktionen übergeben.
Für die Festlegung des Output-Formates sind nur wenige Variablen erforderlich.
typedef struct
{
int Typ;
int Format;
} OUT_FORMAT;
Typ und Format haben dieselbe Belegung wie in SAMPLE_SPEC. Das Output-Format muß natürlich vorgegeben werden. Während es in unserem Rahmenprogramm einfach festgelegt wird, sollte es normalerweise interaktiv vom Benutzer erfragt werden.
Dieser Programmteil ist erfreulich kurz und einfach, weil er nur die Modulroutinen sinnvoll miteinander kombinieren muß. Zunächst müssen das Input- und das Output-File festgelegt und geöffnet und das Output-Format bestimmt werden. Dann müssen alle vorhandenen Header-Test-Routinen der Reihe nach aufgerufen werden, bis eine von ihnen das Format erkennt. Vor jedem Aufruf muß man den Filepointer an den File-Anfang setzen.
Ist das Format erkannt, wird der Buffer, der zur Wandlung eines Blockes nötig ist, alloziert. Als nächstes wird, wenn im Modul dazu die entsprechende Funktion vorhanden ist, der Header des Output-Files geschrieben. Die eigentliche Wandlung findet in einer while-Schleife statt, in der die Sounddaten blockweise ins Standardformat geladen und im Output-Format geschrieben werden. Natürlich ruft man dazu die Ladefunktion des Input- und die Schreibfunktion des Output-Formats auf. Falls nötig, rufen Sie dann die Abschlußfunktion des Output-Formats auf. Zum Schluß müssen nur der Buffer freigegeben und die Files geschlossen werden.
Der zu einem Modul gehörige Funktionssatz wird (zusätzlich mit dem Namen des Formats) in der Struktur FMT_FUNCTION gespeichert. Wenn für ein Format eine Funktion nicht existiert (da unnötig), wird ein Null-Pointer eingetragen.
In der Liste CnvFuncs sind dann alle Funktionen entsprechend ihrer Reihenfolge in VLUN.H enthalten. Dabei ist es wichtig, daß die Funktionen des headerlosen Rohformates als letztes enthalten sind. Denn die ,Header-Test‘-Funktion dieses Formats akzeptiert jedes File als RAW-Format (es gibt ja keinen Header, der dagegen spricht...).
Für die fünf verschiedenen Wandelfunktionen pro Modul werden Typen in VLUN.H definiert. Dabei gibt es für die verschiedenen Funktionen einiges zu beachten:
Header-Test
typedef int (HEAD_FUNC)
(FILE *InFH,
SAMPLE_SPEC *Smp)
InFH ist der Filepointer auf das bereits geöffnete Input-File. Bei erkanntem File-Format muß Smp entsprechend dem Header ausgefüllt und SUPPORTED zurückgeliefert werden. Handelt es sich bei dem File nicht um das Modulformat muß UNKNOWN gemeldet werden. Ist das Format zwar bekannt, wird jedoch diese spezielle Version nicht unterstützt (z.B. 32-Bit-SND-Format), dann wird NONSUPPORTED zurückgeliefert, damit das aufrufende Hauptprogramm keine weiteren Formate überprüfen muß.
typedef long (READ_FUNC)
(FILE *InFH,
SAMPLE_SPEC *Smp,
char *StdBuf,
long StdBufLen);
StdBuf ist der Pointer auf den Beginn des Wandel-Buffers, StdBufLen seine Länge. Die Funktion muß nun einen Block aus InFH laden und ins Standardformat wandeln. Das Hauptprogramm stellt sicher, daß der Buffer lang genug ist, um einen
Input-Format-Block im Standardformat zwischenzuspeichern. Die Daten im Standardformat sollen nach der Wandlung immer am Buffer-Anfang liegen. Zurückgegeben wird die Länge der Daten im Standardformat. Wird weniger als ein Block gelesen, so wird hier auch ein kleinerer Wert zurückgeliefert.
typedef long (WRITE_FUNC)
(FILE *OutFH,
OUT_FORMAT *OutSmp,
long DataLen,
char *StdBuf);
Wandelt einen Standardformat-Datenblock der Länge DataLen ins Zielformat OutSmp um und speichert diesen in OutFH. Außerdem kümmert sich die Funktion darum, daß die Format-Flags vom Zielformat wirklich unterstützt werden. Beim Schreiben von AVR muß man beispielsweise immer BIGENDIAN setzen. Zurückgegeben werden die geschriebenen Bytes.
typedef int (WRT_HEAD_FUNC)
(FILE *OutFH,
SAMPLE_SPEC *InSmp,
OUT_FORMAT *OutSmp);
Baut den Header entsprechend der Information in OutSmp und InSmp auf und schreibt ihn. Zurückgeliefert wird 1, wenn der Header geschrieben werden konnte, 0 andernfalls.
typedef int (FINISH_FUNC)
(FILE *OutFH,
OUT_FORMAT *OutSmp);
Bei einigen File-Formaten steht im Header auch die Länge der Sample-Daten. Da dies erst nach der Wandlung bekannt ist, schreibt diese Funktion in diesen Fällen zum Beispiel die entsprechenden Werte ins File. Einige Module unterhalten dazu eine statische Variable, die die Anzahl der geschriebenen Bytes mitzählt.
Nachdem das Grundkonzept hoffentlich ausreichend dargestellt wurde, kommen wir zum ersten konkreten Soundformat:
Der Hauptanwendungsbereich des AVR Formats liegt im ATARI-Bereich (SAM). Der Header von AVR Files hat folgenden Aufbau:
typedef struct
{
long magic;
char sample_name[8];
int mode;
int resolution;
int sign;
int loop;
int note;
long speed;
long len; long beg_loop;
long end_loop;
int res1,res2,res3;
char extension[20];
char free_area[64];
} AVRHEAD;
magic: | hat immer den Wert ,2BIT‘ (0x32424954L). Er dient als Erkennung für das AVR-Format. |
sample_name: | Name des Samples |
mode: | 0 für Mono Samples, -1 (0xffff) für Stereo |
resolution: | Anzahl der Bits pro Sample-Wert (8 oder 16) |
sign: | 0 für vorzeichenlose Darstellung, -1 für vorzeichenbehaftete |
loop: | 0 schleifenloses Sample, -1 mit Schleife |
note: | -1 keine MIDI-Noten-Zuweisung |
speed: | Sample-Frequenz in Hz in den unteren 3 Bytes. Wenn höchstes Byte ungleich 0xFF ist, steht auch eine Festfrequenz von 0 bis 7 im höchsten Byte |
len: | Sample-Datenlänge in Sample-Werten. Bei Stereo-Samples wird linker und rechter Kanal einzeln gezählt |
beg_loop: | Schleifenanfang (in Sample-Werten) oder 0 |
end_loop: | Schleifenende oder ,len' |
extension: | File-Namenerweiterung |
free_area: | Benutzerdef. Info, z.B. ASCII-Text |
Danach folgen die Sound-Daten. Vor Belegen des Headers sollte er komplett mit Nullen initialisiert werden.
Im AVR-Format kann eine Schleife festgelegt werden, mit deren Hilfe man ein Sample durch Wiederholung verlängern kann. Das Sample wird nach dem end_loop wieder ab begin Joop vorgespielt. Wie bei allen anderen Formaten, wird auch beim AVR-Format die Information über Schleifen ignoriert. Nur die reinen Sample-Werte werden konvertiert, denn sonst würden die entstehenden Files doch recht lang ... Das Modul besteht aus AVR.C und dem AVR.H File.
Die Header-Test-Funktion AvrTstHead überprüft, ob das Input-File im AVR-Format ist und setzt in diesem Fall ,SAMPLE_SPEC‘ entsprechend.
Die Wandlung ins Standardformat wird von der Funktion AllToStd übernommen, die im UTIL.C File (nächste Ausgabe) enthalten ist. Da das AVR-Format so angenehm ist, braucht man hier keine spezielle Funktion.
Das Wandeln ins AVR übernimmt Avr-FromStd, das verhindert, daß MULAW oder DELTAPACK als Format in AVR-Files benutzt werden und außerdem in einer statischen Variablen die gewandelten und geschriebenen Bytes mitzählt. Die eigentliche Wandlung übernimmt auch hier eine allgemeine Funktion AllFromStd.
AvrWrtHead initialisiert den Header, setzt die nötigen Werte und schreibt den Header.
AvrFinish schreibt die nach dem Wandeln bekannte Sample-Länge in den Header und schreibt ihn an den Fileanfang. Die Zähl variable muß auf Null zurückgesetzt werden damit mit einem neuen Wandeln begonnen werden kann.
Rohdaten sind im SUN- und NeXT-Bereich als Mu-Law-Daten und auf dem ST als 8-Bit-Mono-Daten weitverbreitet. Das Problem ist, daß sie keinen Header haben, deshalb muß man das richtige Datenformat und die Frequenz von Hand per Trial and Error herausfinden.
Die Funktion, die sonst testet, ob ein Header einem bestimmten Format entspricht, ist bei Rohdaten verständlicherweise nicht möglich. Diese Funktion heißt deswegen auch RawSetHead, weil sie jedes File als RAWDATA akzeptiert und einfach die Frequenz und das Datenformat auf den typischen ST-Standard setzt. Um das oben genannte Trial-and-Error-Verfahren zu vereinfachen, sollte man diese Funktion etwas ausbauen, um interaktiv das Format und die Frequenz eingeben zu können. Das Wandeln ins RAW-Format übernimmt RawFromStd. Hier wird dafür gesorgt, daß DELTAPACK nicht verwendet wird. Die eigentliche Konvertierung übernimmt wieder AllFromStd. Die Funktionen zum Header schreiben und File abschließen entfallen.
Im nächsten Teil der Serie werden wir dann UTILS.C mit den eigentlichen kurzen Konvertierungsroutinen vorstellen und weitere Sound-Formate besprechen.
/* AVR Routinen zur AVR Konvertierung */
/* Modulname: AVR.C */
/* (c) MAXON Computer 1994 */
/* Autoren: H. Schönfeld, B. Spellenberg */
# include "vlun.h"
# include "avr.h"
/* Zähler für geschriebene Sound-Bytes */
static long ByteCount=0L;
/* Header für Input und Export */
static AVRHEAD AvrHead;
/* AVR-Headertest-Funktion */
int AvrTstHead(FILE *InFH, SAMPLE_SPEC *Smp)
{
/* Soundformatbeschreibung initialisieren */
memset(Smp,0,sizeof(SAMPLE_SPEC));
/* AVR-Header lesen und testen */
if(!fread(&AvrHead,sizeof(AVRHEAD),1L,InFH))
return(UNKNOWN);
if(strncmp(&AvrHead.magic,”2BIT",4))
return(UNKNOWN);
/* Header auswerten und Formatbeschreibung */
/* aufbauen */
Smp->Typ=AVRHEADER;
Smp->Freq=0xFFFFFFL&AvrHead.speed;
Smp->SizeFac=1;
if(AvrHead.mode==-1)
Smp->Format|=STEREO;
else
Smp->SizeFac*=2;
if(AvrHead.resolution==16)
Smp->Format|=BIT16|BIGENDIAN;
else if(AvrHead.resolution==8)
Smp->SizeFac*=2;
else
return(NONSUPPORTED);
if(AvrHead.sign==-1)
8mp->Format|=SIGNED;
Smp->BufLen=BUFSIZE;
return(SUPPORTED);
}
/* Std.format nach AVR konvertieren */
long AvrFromStd(FILE *OutFH,OUT_FORMAT *OutSmp, long DataLen, char *StdBuf)
{
long DataWrite;
/* Mu-law und Deltapack werden von AVR nicht */
/* unterstützt */
OutSmp->Format&=~(MULAW|DELTAPACK);
/* 16Bit Samples immer im Motorolaformat */
if(OutSmp->Format&BIT16)
OutSmp->Format|=BIGENDIAN;
/* nächsten Block konvertieren und speichern */
DataWrite=AllFromStd(OutFH,OutSmp,DataLen,StdBuf);
/* Bytezähler entsprechend erhöhen */
ByteCount+=DataWrite;
return(DataWrite);
}
/* AVR-Header setzen und schreiben */
int AvrWrtHead(FILE *OutFH,SAMPLE_SPEC *InSmp, OUT_FORMAT *OutSmp)
{
int Fmt;
Fmt=OutSmp->Format;
memset(&AvrHead,0,sizeof(AVRHEAD));
strcpy(&AvrHead.magic,"2BIT");
AvrHead.mode=(Fmt&STEREO)?0xffff:0;
AvrHead.resolutions(Fmt&BIT16)?16:8;
AvrHead.sign=(Fmt&SIGNED)?0xffff:0;
AvrHead.speed=0xFF000000L|InSmp-»Freq;
AvrHead.note=0xffff;
return(fwrite(SAvrHead,sizeof(AVRHEAD),1,OutFH));
}
/* Fehlende Information in AVR-Header */
/* schreiben */
int AvrFinish(FILE *OutFH,OUT_FORMAT *OutSmp)
{
/* Anzahl der Samplepunkte bestimmen */
AvrHead.len=ByteCount;
if(OutSmp->Format&BIT16)
AvrHead.len/=2 ;
AvrHead.end_loop=AvrHead.len;
/* Bytezähler für evtl. neue Wandlung */
/* zurücksetzen */
ByteCount=0;
/* kompletten Header an Fileanfang schreiben */
fseek(OutFH,0L,0);
return(fwrite(&AvrHead,sizeof(AVRHEAD),1,OutFH));
}
/* AVR Headerfile zur AVR Konvertierung */
/* Modulname: AVR.H */
/* (c) MAXON Computer 1994 */
/* Autoren: H. Schönfeld, B. Spellenberg */
typedef struct
{
long magic; /* Headerkennung */
char sample_name[8]; /* Filename */
int mode; /* Stereo/Mono */
int resolution; /* 8/16 Bit Auflösung*/
int sign; /* Mit/Ohne Vorz. */
int loop; /* Looping Sample? */
int note; /* MIDI Notes? */
long speed; /* Frequenz */
long len; /* Länge in Samples */
long beg_loop; /* Anfang der Loop */
long end_loop; /* Ende der Loop */
int res1,res2,res3;
char extension[20]; /* Filename-Extension*/
char free_area[64]; /* Zusatzinfo. */
} AVRHEAD;