/dev/dsp - Programmierung

1) Vorwort und Einleitung

Linux bietet mit dem Device /dev/dsp ein Interface zwischen Programmierer und DSP-Chip der Soundkarte, das sehr gut geeignet ist, einfache Frequenzmuster, aber auch ganze Wave-Dateien über die Lautsprecher auszugeben. Dies mit einem kleinen C - oder C++ - Programm zu tun ist nicht sehr schwierig, nur mangelt es sehr an verständlichen und komplett an deutschsprachigen Tutorials dazu. Deshalb habe ich mich entschlossen, dieses Dokument zu schreiben. Es wird von mir unter der GPL veröffentlicht. Diese Lizenz ist z.B. unter http://opensource.org/licenses/gpl-license.html einzusehen. Das Beispielprogramm, das ich erkläre, ist in C++ geschrieben. Es gibt auf dem rechten und dem linken Kanal zwei in der Frequenz beliebig wählbare Sinuswellen aus.

2) Benötigte Header

Um das Device zu initialisieren und mit den benötigten Informationen zu versorgen, benutzen wir so genannte I/O Controls: ioctls. Dazu benötigen wir die Header sys/ioctl.h und sys/soundcard.h, die die für die Soundkarte gedachten ioctls enthält. Für den Einsatz der Systemcalls open, close und write brauchen wir unistd.h und fcntl.h, für die Ausgabe von Fehlern errno.h, für den Kontakt mit dem Benutzer iostream und für die Sinusfunktion math.h. Zum Berechnen von Sinus brauchen wir die Zahl Pi, die am besten am Anfang als Konstante eingeführt wird. Hier also die ersten Zeilen des Programms:

   #include <sys/soundcard.h>
   #include <sys/ioctl.h>
   #include <unistd.h>
   #include <fcntl.h>
   #include <errno.h>
   #include <iostream>
   #include <math.h>
   #define PI 3.141592654

3) Variablen

Zu Beginn führen wir erst einmal einige Variablen ein und legen sie nötigenfalls fest. Zunächst einmal benötigen wir die Integervariablen handle und i als Fehlerkontrolle und Zählvariable. Die Floatvariablen rfreq, lfreq, rfaktor und lfaktor werden für die verschiedenen Frequenzen und Multiplikatoren benötigt. Die Unsigned-Char-Pointer data und ptr dienen dem Zugriff auf den Speicherbereich mit den Sounddaten. Weiterhin benötigen wir int channels =

1 (für Stereo), int rate = 44100 (Sampling Rate) und int format = AFMT_U8

(Sample Format). Die Bedeutung dieser Werte wird später noch erläutert.

4) Berechnung der Faktoren

Die an den DSP ausgegebenen Daten kann man sich ungefähr als “angestrebte Lautsprechermembranpositionen” vorstellen. Es handelt sich um Unsigned Char-Variablen, also Werte von 0 bis 255. Führt man die Sinusfunktion mit der einfachen Zählvariable durch, so wiederholt sich die Wellenstruktur alle 360 Durchläufe, bei 44100 Werten pro Sekunde also eine Frequenz von \frac{44100}{360} = 122.5Hz . Die Faktoren berechnen sich dementsprechend nach der Formel:

F aktor = \frac{360 ⋆ F requenz}{Samplingrate}

Wollen wir also zwei verschiedene Frequenzen ausgeben, müssen wir die Zahlenwerte in rfreq und lfreq einlesen und nach o.g. Formel rfaktor und lfaktor berechnen.

5) Initialisierung von /dev/dsp

Bevor wir mit der Erstellung der Daten im Arbeitsspeicher beginnen können, muss zunächst /dev/dsp mit den zur Initialisierung notwendigen Daten versorgt werden:

   if ( (handle = open("/dev/dsp",O_WRONLY)) == -1 ) {        // (1)
        cerr << "open /dev/dsp";
        return -1;
   }
   if ( ioctl(handle, SNDCTL_DSP_STEREO, &channels) == -1 ) { // (2)
        cerr << "ioctl stereo";
        return errno;
   }
   if ( ioctl(handle, SNDCTL_DSP_SETFMT, &format) == -1 ) {   // (3)
        cerr << "ioctl format";
        return errno;

   }
   if ( ioctl(handle, SNDCTL_DSP_SPEED, &rate) == -1 ) {      // (4)
        cerr << "ioctl sample rate";
        return errno;
   }

Zu (1):

Als erstes wenden wir den System-Call open auf /dev/dsp an. Da wir nur Daten schreiben wollen, genügt der WriteOnly-Modus. Es ist generell zu empfehlen, auf /dev/dsp entweder im ReadOnly- oder im WriteOnly-Modus zuzugreifen. Wenn es Probleme gibt, wird eine Fehlermeldung ausgegeben und das Programm endet.

Zu (2):

Der ioctl SNDCTL_DSP_STEREO legt fest, ob der DSP mit Mono- oder Stereodaten versorgt werden soll. Für unsere Zwecke verwenden wir Stereo (vorher in der Variablen channels mit 1 festgelegt, Mono wäre 0). Gibt es Probleme, endet das Programm wieder mit einer Fehlermeldung.

Zu (3):

Hier muss mit Hilfe des ioctls SDNCTL_DSP_SETFMT das Format der ausgegebenen Audio-Daten (Bitzahl, ...) angegeben werden. Dazu benutzt man am besten die vorgefertigten Datenstrukturen aus soundcard.h. AFMT_U8 ist ein Standartwert für 8 Bit (1 Byte) pro Sample, der von den allermeisten Soundkarten unterstützt wird. U steht dabei für unsigned, d.h. die ausgegebenen Werte können nicht negativ sein. Im Internet finden sich Listen mit den Beschreibungen der verschiedenen Formate. Spielen Sie aber besser nicht damit herum, wenn Sie nicht wissen was Sie tun.

Zu (4):

Ein weiterer wichtiger Parameter ist die Sampling Rate, die Anzahl pro Sekunde und Kanal ausgegebener Bytes. Oft sind hier 22050 empfehlenswert, wollen wir aber auch sehr hohe und trotzdem saubere Sinusfrequenzen ausgeben, müssen wir höhere Werte wählen. 44100 ist eine von den allermeisten Soundkarten unterstützte Sampling Rate, die mit annehmbar viel Speicherplatz im RAM auskommt.

6) Sounddaten in den Arbeitsspeicher schreiben

Damit nachher die Soundbytes flüssig wiedergegeben werden können, erstellen wir das komplette Abbild davon im Arbeitsspeicher. Dazu müssen wir erst einmal Platz schaffen. Allerdings müssen wir dafür wissen, wie viel Platz wir brauchen. Sagen wir, unsere Töne sollen 4 Sekunden anhalten. Für den rechten und den linken Kanal brauchen wir also jeweils 4⋆44100, zusammen dann natürlich 2 ⋆ 4 ⋆ 44100 Bytes. Der Speicherbedarf berechnet sich also nach folgender Formel:

Channels ⋆ Samplingrate ⋆ Sekunden

Mit Hilfe des C++-Befehls new und den beiden Char Pointern data und ptr ist das nötige in zwei Zeilen zu programmieren:

   data = new unsigned char[2 * rate * 4];
   ptr = data;

Nun kann die eigentliche Schleife zur Erstellung der Daten beginnen. Sie muss insgesamt

Samplingrate ⋆ Sekunden

mal ausgeführt werden. Bei jedem Durchlauf muss für jeden Kanal der Pointer um eins weitergesetzt werden und der gewünschte Wert mit Hilfe der Funktionen sin und int (es sind ja nur ganzzahlige Werte erlaubt) in den Speicher geschrieben werden. Folgende Schleife erfüllt diesen Zweck:

   for (i = 0; i < 4 * rate; i++) {
        *ptr++ = int((sin(i * lfaktor * (PI / 180)) + 1) * 128);
        *ptr++ = int((sin(i * rfaktor * (PI / 180)) + 1) * 128);
   }

Dabei ist zu beachten, dass die von der sin-Funktion ausgegebenen Werte nicht denen entsprechen, die ein Taschenrechner bei Eingabe einer Zahl und Drücken der SIN-Taste berechnet. Nur indem man die Ausgangszahl mit \frac{π}{180} malnimmt, ist die Ausgabe die selbe.

7) Sounddaten in /dev/dsp schreiben und Programm beenden

Durch den System-Call write mit der gleichen Speicherangabe wie bei der Speicherreservierung wird der Inhalt des erstellten Arrays in /dev/dsp geschrieben. Wir müssen uns dabei um nichts mehr kümmern, da der Kernel die Daten im Fall des zu schnellen Schreibens puffert. Um zu verhindern, dass wir das Programm beenden bevor der Puffer leer ist, müssen wir nur der ioctl SND-CTL_DSP_SYNC aufrufen, der bis zu diesem Zeitpunkt wartet. Zum Schluss geben wir den Speicher wieder frei und sind fertig:

   write(handle, data, 4 * rate * 2);
   if (ioctl(handle, SNDCTL_DSP_SYNC) == -1) {
        cerr << "ioctl sync";
        return errno;
   }

delete []data;

8) Komplettes Beispielprogramm

#include <sys/soundcard.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <iostream>
#include <math.h>
#define  PI 3.141592654
int main() {
int handle = -1;
int i;
float rfreq, lfreq;
float rfaktor, lfaktor;
unsigned char* data;
unsigned char* ptr;
int channels = 1;         // 0=mono 1=stereo
int format = AFMT_U8;     // Unsigned 8Bit Samples
int rate = 44100;         // Sampling Rate
cout << "Sinus Wave Generator\n"
     << "Bitte Frequenz für linken Lautsprecher (Hz) eingeben:"
     << flush;
cin  >> lfreq;
cout << "Bitte Frequenz für rechten Lautsprecher (Hz) eingeben:"
     << flush;
cin  >> rfreq;
if(rfreq > 10000 || lfreq > 10000)
    cout << "So hohe Frequenzen können bedingt durch die Sampling-Rate"
         << " oft nicht richtig dargestellt werden.  Meine persönliche"
         << " Lieblings-Frequenz von 17kHz funktioniert aber.\n"
         << flush;
rfaktor = (rfreq * 360) / rate;
lfaktor = (lfreq * 360) / rate;
cout << "Frequenzen:  " << lfreq  << "\t" << rfreq

     << "\nSampling-Rate:  " << rate << "\nFaktoren:  "
     << lfaktor << "\t" << rfaktor << "\n";
if ( (handle = open("/dev/dsp",O_WRONLY)) == -1 ) {
    cerr << "open /dev/dsp";
    return -1;
}
if ( ioctl(handle, SNDCTL_DSP_STEREO, &channels) == -1 ) {
    cerr << "ioctl stereo";
    return errno;
}
if ( ioctl(handle, SNDCTL_DSP_SETFMT, &format) == -1 ) {
    cerr << "ioctl format";
    return errno;
}
if ( ioctl(handle, SNDCTL_DSP_SPEED, &rate) == -1 ) {
    cerr << "ioctl sample rate";
    return errno;
}
data = new unsigned char[4 * rate * 2];
ptr = data;
for (i = 0; i < 4 * rate; i++) {
    *ptr++ = int((sin(i * lfaktor * (PI / 180)) + 1) * 128);
    *ptr++ = int((sin(i * rfaktor * (PI / 180)) + 1) * 128);
}
cout << "Gebe Sinus-Wellen wieder, Frequenzen " << lfreq
     << "Hz und " << rfreq << "Hz.\n" << flush;
write(handle, data, 4 * rate * 2);
if (ioctl(handle, SNDCTL_DSP_SYNC) == -1) {
    cerr << "ioctl sync";
    return errno;
}
delete []data;    //durch die [] wird das ganze Array gelöscht
return 0;
}

9) Dank

Ich möchte danken: - allen Programmierern und Kernel-Hackern, die zur Sound-Unterstützung unter Linux beigetragen haben

- Eelke Klein für ein englisches Tutorial, das mir sehr geholfen hat - Manfred Deffner für die Anregungen

- Ihnen, weil Sie sich das hier bis zum Ende durchgelesen haben

Daniel Grün, 20.03.2002