Audio Streaming mit UDP in C

Autor: Andre Adrian
Version 27.Dez.2007

Zusammenfassung

TCP Server und Client werden für Internet Relay Chat oder Remote Shell benutzt. Für Audio- und Video-Streaming wird UDP eingesetzt. Die einfachste UDP-Audio Streaming Applikation ist ein Babyphone: Ein Rechner nimmt die Geräusche über Mikrofon auf und sendet die Geräusche digitalisiert als UDP Pakete zu einem zweiten Rechner welcher die Geräusche über Lautsprecher ausgibt.
Die UDP Kommunikation erfolgt mit der Socket Schnittstelle. Für die Kommunikation mit Mikrophon und Lautsprecher wird unter Linux das Open Sound System (OSS) verwendet. Unter MS-Windows XP und Vista wird Waveform Audio (Windows Multimedia) benutzt.

Inhaltsverzeichnis

Einleitung

Im Babyphone Programm wird die Abtastfrequenz auf 16000Hz eingestellt. Dadurch werden Töne bis zu einer Frequenz von 7kHz übertragen (Abtast Theorem von Claude Shannon). 512 Abtastungen werden in einem UDP Paket übertragen. Dies ergibt 16000/512 = 31.25 UDP Pakete pro Sekunde. Jede Abtastung  benötigt 8 Bit (1Byte). Die Netto-Datenübertragungsrate ist 128kBit/s. Die Brutto-Datenübertragungsrate bestehend aus Nutzdaten (Audio-Stream) und Datenpaket Headers ist 138kBit/s.
Als Audio Codec wird G.711 u-Law verwendet. Dieser Codec erreicht eine Dynamik von 72dB bei nur 8bit - bei einem Codec ohne Kompression sind für 72dB Dynamik 12bit nötig. Mit einer Abtastfrequenz von 8000Hz wird der G.711 Codec im ISDN Telephon eingesetzt.
Unter Windows Vista müssen 1024 Abtastungen in einem UDP Paket zusammengefasst werden. Dies ergibt 16000/1024 = 15.6 UDP Pakete pro Sekunde. Von den drei Kandidaten Linux, Windows XP und Windows Vista hat Vista das schlechteste Echtzeitverhalten für Audioanwendungen.

Das Open Sound System (OSS)

Für die POSIX (UNIX) Betriebssysteme Linux, Solaris und BSD ist OSS ein Audio-Karten Programmier-Interface. Der Programmierer greift über die UNIX-IO Aufrufe open(), ioctl(), read() und write() auf die Datei /dev/dsp zu. Dieser spezielle Dateinamen steht für die Soundkarte. Für das Betriebssystem ist OSS ein Hardware Treiber für das Character Device /dev/dsp. Mit write() wird der Lautsprecher angesprochen, mit read() wird Spracheingabe vom Mikrofon geholt. Mit dem ioctl() Aufruf werden die Betriebsparameter der Audio-Hardware festgelegt. Unter Linux ist OSS seit einiger Zeit nur noch eine dünne Emulationsschicht auf der Advanced Linux Sound Architecture (ALSA).

Compile und Ausführen

Der UDP-Sender Quelltext wird unter Linux mit  cc -o udp_send udp_send.c  übersetzt. Der Sender wird mit   ./udp_send 127.0.0.1 &   im Hintergrund gestartet. Der UDP-Empfänger Quelltext wird mit  cc -o udp_recv udp_recv.c  übersetzt. Für den ersten Test laufen beide Programme auf dem gleichen Rechner. Die Spracheingabe in das Mikrofon hört man im Lautsprecher.
Damit im Lautsprecher etwas zu hören ist müssen die verschiedenen "Lautstärkeregler" im Audio Mixer Programm auf sinnvolle Werte gestellt werden. Hier ein Screenshot des Mixer Programmes gamix. Playback bestimmt die Gesamt-Lautstärke. PCM Playback bestimmt die Lautstärke der vom Programm udp_recv erzeugten Töne. Capture bestimmt den Aufnahmepegel des Mikrofons.




Der Quelltext

Da es bei UDP Kommunikation keinen Verbindungsaufbau gibt, macht es keinen Sinn von UDP Client und UDP Server zu reden. Bei dem UDP Babyphone gibt es ein Programm welches UDP Pakete aussendet und ein Programm welches UDP Pakete empfängt.
Der Quelltext besteht aus den drei Dateien

Babyphone Header Datei

/* udp_common.h
 * UDP Babyphone
 * Andre Adrian
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <linux/soundcard.h>

#define BABYPHONE_PORT 26002      // Der Babyphone well known Port

#define BUFSIZELD 9               // Groesse Soundcard Buffer, Minimum ist 4
#define BUFSIZE   (1<<BUFSIZELD)     

#define AUDIOBUFS     3           // Anzahl Soundcard Buffer, Minimum ist 2
#define AUDIOFORMAT   AFMT_MU_LAW // Audio Codec G.711 u-Law
#define AUDIOSPEED    16000       // Abtastfrequenz
#define AUDIOCHANNELS 1           // 1 ist Mono, 2 ist Stereo

#define exit_if(expr) \
if(expr) { \
  fprintf(stderr, "%s(%d) %s\n", __FILE__, __LINE__, strerror(errno)); \
  exit(1); \
}

Babyphone UDP-Sender Datei

/* udp_send.c
 * UDP Babyphone Sender (Mikrophon)
 * Andre Adrian
 */

#include "udp_common.h"

int main(int argc, char *argv[])
{
  struct sockaddr_in remote;  // UDP Empfaenger IPv4 Adress und Port Struktur
  int remotefd;               // UDP Empfaenger Filedeskriptor
  struct hostent *he;
  int rv;
  int yes = 1;
 
  int fd, i;
  int level;
  int len;
  char buffer[BUFSIZE];
 
  if (argc != 2) {
    fprintf(stderr, "usage: udp_send hostname\n");
    exit(1);
  }

  he = gethostbyname(argv[1]);  // hole UDP Empfaenger IPv4 Adresse
  exit_if(NULL == he);

  remotefd = socket(AF_INET, SOCK_DGRAM, 0); // Socket Filedeskriptor anlegen
  exit_if(remotefd < 0);
 
  // UDP Broadcast erlauben
  rv = setsockopt(remotefd, SOL_SOCKET, SO_BROADCAST, &yes, sizeof(yes));
  exit_if(rv < 0);
 
  memset(&remote, 0, sizeof(remote)); // remote mit 0 fuellen
  remote.sin_family = AF_INET;
  remote.sin_port = htons(BABYPHONE_PORT);
  remote.sin_addr = *((struct in_addr *) he->h_addr);
 
 
  fd = open("/dev/dsp", O_RDONLY);          // Soundcard oeffnen
  exit_if(fd < 0);
 
  // Anzahl und Groesse der Audio-Buffers im Soundcard Treiber setzen
  i = AUDIOBUFS << 16 | BUFSIZELD;
  rv = ioctl(fd, SNDCTL_DSP_SETFRAGMENT, &i);
  exit_if(rv < 0);
 
  i = AUDIOFORMAT;                          // Audio Codec (Format) setzen
  rv = ioctl(fd, SNDCTL_DSP_SETFMT, &i);
  exit_if(rv < 0);
 
  i = AUDIOCHANNELS;                        // Mono oder Stereo setzen
  rv = ioctl(fd, SNDCTL_DSP_CHANNELS, &i);
  exit_if(rv < 0);

  i = AUDIOSPEED;                           // Abtastfrequenz setzen    
  rv = ioctl(fd, SNDCTL_DSP_SPEED, &i);
  exit_if(rv < 0);

  // Aufnahme (Capture) einschalten
  rv = ioctl(fd, MIXER_READ(SOUND_MIXER_IGAIN), &i);
  exit_if(rv < 0);
 
  if (0 == i) {           // i == 0 bedeutet keine Aufnahme (Capture)   
    i = 1 + (1 << 8);
    rv = ioctl(fd, MIXER_WRITE(SOUND_MIXER_IGAIN), &i);
    exit_if(rv < 0);
  }
 
  for (;;) {                                // Endlosschleife
    len = read(fd, buffer, sizeof(buffer)); // Audio holen
    exit_if(len < 0);
   
    rv = sendto(remotefd, buffer, len, 0,
                (struct sockaddr *) &remote, sizeof(remote));
    exit_if(rv != len);
  }

  // hierhin kommt das Programm garnicht..
  close(fd);     
  close(remotefd);

  return 0;
}

Babyphone UDP-Empfänger Datei

/* udp_recv.c
 * UDP Babyphone Empfaenger (Lautsprecher)
 * Andre Adrian
 */

#include "udp_common.h"

int main(void)
{
  struct sockaddr_in local;   // UDP Empfaenger IPv4 Adress und Port Struktur
  int localfd;                // UDP Empfaenger Filedeskriptor
  struct sockaddr_in remote;  // UDP Sender IPv4 Adress und Port Struktur
  int len;
  int rv;
  int fd, i;
  char buffer[BUFSIZE];
 
  localfd = socket(AF_INET, SOCK_DGRAM, 0); // Socket Filedeskriptor anlegen
  exit_if(localfd < 0);

  memset(&local, 0, sizeof(local)); // local mit 0 vollschreiben
  local.sin_family = AF_INET;
  local.sin_port = htons(BABYPHONE_PORT);
  local.sin_addr.s_addr = INADDR_ANY;

  // local Server IPv4 Adresse eintragen
  rv = bind(localfd, (struct sockaddr *) &local, sizeof(local));
  exit_if(rv < 0);

  fd = open("/dev/dsp", O_WRONLY);          // Soundcard oeffnen
  exit_if(fd < 0);
 
  // Anzahl und Groesse der Audio-Buffers im Soundcard Treiber setzen
  i = AUDIOBUFS << 16 | BUFSIZELD;
  rv = ioctl(fd, SNDCTL_DSP_SETFRAGMENT, &i);
  exit_if(rv < 0);
 
  i = AUDIOFORMAT;                          // Audio Codec (Format) setzen
  rv = ioctl(fd, SNDCTL_DSP_SETFMT, &i);
  exit_if(rv < 0);
 
  i = AUDIOCHANNELS;                        // Mono oder Stereo setzen
  rv = ioctl(fd, SNDCTL_DSP_CHANNELS, &i);
  exit_if(rv < 0);

  i = AUDIOSPEED;                           // Abtastfrequenz setzen    
  rv = ioctl(fd, SNDCTL_DSP_SPEED, &i);
  exit_if(rv < 0);

  for (;;) {                                // Endlosschleife
    socklen_t addr_len = sizeof(remote);
   
    len = recvfrom(localfd, buffer, sizeof(buffer), 0,
                    (struct sockaddr *) &remote, &addr_len);
    exit_if(len < 0);
               
    rv = write(fd, buffer, len);               
    exit_if(rv != len);
  }
 
  // hierhin kommt das Programm garnicht..
  close(fd);
  close(localfd);

  return 0;
}

MS-Windows Waveform Audio

Waveform Audio ist der Name der Programmier Schnittstelle für Ton Aufnahme und Wiedergabe unter MS-Windows. Waveform Audio benutzt nicht die "alles ist eine Datei" Abstraktion des Open Sound System sondern läßt den Programmierer mit Audiobuffern hantieren. Dabei besteht eine Audiobuffer Aktivität immer aus zwei Schritten:
Bei der Aufnahme wird mit waveInAddBuffer() ein leerer Audiobuffer an das Betriebssystem übergeben. Das Betriebssystem sendet eine MM_WIM_DATA Meldung an die Applikation wenn die Soundkarte den Audiobuffer gefüllt hat.
Bei der Wiedergabe wird mit waveOutWrite() ein gefüllter Audiobuffer an das Betriebssystem übergeben. Hat die Soundkarte diesen Buffer "leergespielt" sendet das Betriebssystem an die Applikation die MM_WOM_DONE Meldung.
Die Meldungen sind normale MS-Windows Meldungen (Messages).
Für eine Audiowiedergabe ohne "Knackser" reicht ein Audiobuffer nicht aus. Mindestens zwei Aufnahme-Audiobuffer und mindestens zwei Wiedergabe-Audiobuffer sind nötig um die Abläufe in der Applikation von den Abläufen im Betriebssystem zu entkoppeln.

Aufgabe
Waveform Audio Funktion
Soundkarte für Aufnahme öffnen
waveInOpen()
Aufnahme Buffer Header vorbereiten
waveInPrepareHeader()
Aufnahme Buffer Header am Ende der "Aufnahmekette" anhängen
waveInAddBuffer()
Aufnahme starten
waveInStart()
"Aufnahme Buffer ist voll" Windows Meldung
MM_WIM_DATA
Soundkarte für Wiedergabe öffnen
waveOutOpen()
Wiedergabe Buffer Header vorbereiten waveOutPrepareHeader()
Einen Wiedergabe Buffer abspielen
waveOutWrite()
"Wiedergabe Buffer ist leergespielt" Windows Meldung MM_WOM_DONE

Waveform Audio produziert unter bestimmten Bedingungen Knacksen und Knistern. Zum Test die beiden Programme winudp_send und winudp_recv auf einem Rechner starten. Solange nur diese beiden Programme das Netzwerk benutzen gibt es keine Knackser. Nun einen Web-Browser starten und Internet Seiten aufrufen. Hört man beim Aufruf der Internet Seiten Knacksen und Knistern? Mit Microsoft Windows Vista sollen diese Störungen verschwinden. Leider hat Windows Vista bis Ende 2007 dieses Versprechen nicht erfüllt. Vielleicht hilft ja Vista Service Pack 1.
Mit CALLBACK_THREAD läßt sich das Knistern reduzieren. Siehe Abschnitt MS-Windows Quelltext mit Callback Threads.

MS-Windows Quelltext

MS-Windows Babyphone gemeinsame Header Datei

/* winudp_common.h
 * MS-Windows UDP Babyphone
 * Andre Adrian
 */

#include <windows.h>
#include <commctrl.h>
#include <mmsystem.h>
#include <mmreg.h>
#include <stdio.h>
#include <string.h>

#define BABYPHONE_PORT 26002      // Der Babyphone well known Port

#define BUFSIZE       512         // Groesse Soundcard Buffer in Samples

#define AUDIOBUFS     3           // Anzahl Soundcard Buffer, Minimum ist 2
#define AUDIOFORMAT   WAVE_FORMAT_MULAW // Audio Codec G.711 u-Law
#define AUDIOSPEED    16000       // Abtastfrequenz
#define AUDIOCHANNELS 1           // 1 ist Mono, 2 ist Stereo

#define winreturn_if(expr, hWnd, str) \
if(expr) { \
  MessageBox(hWnd, str, "WARNING", MB_OK); \
  return 0; \
}

#define winexit_if(expr, hWnd, str) \
if(expr) { \
  MessageBox(hWnd, str, "FAILURE", MB_OK); \
  EndDialog(hWnd, 0); \
  return 0; \
}

MS-Windows Babyphone gemeinsame Header Datei für Vista

Das Betriebssystem Vista funktioniert noch nicht so gut wie Windows XP. Deshalb muß für Vista die BUFSIZE auf 1024 erhöht werden. Anstelle von

#define BUFSIZE       512         // Groesse Soundcard Buffer in Samples

bei Vista schreiben

#define BUFSIZE       1024        // Groesse Soundcard Buffer in Samples

MS-Windows Babyphone UDP-Sender Datei



Im Programm werden die Audiobuffer eingerichtet und mit waveInAddBuffer() zu einer Kette zusammengehängt. Mit waveInStart() wird die Aufnahme aktiviert. Damit die Aufnahme nicht nach kurzer Zeit aufhört müssen die "verbrauchten" Audiobuffer erneut mit waveInAddBuffer() ans Ende der Aufnahmekette gehängt werden.
Die Netzwerkausgabe erfolgt direkt nach dem Empfang der MM_WIM_DATA Meldung. Ein eigener Thread für die Netzwerkausgabe ist nicht nötig.
Die UDP Empfänger IP-Adresse wird über eine EDITTEXT Control eingegeben. Die Eingabe von "localhost" oder "127.0.0.1" und Click auf Button Connect erlaubt Aufnahme und Wiedergabe auf dem gleichen Rechner.
Die Variable remotefd erfüllt zwei Aufgaben. Einmal enthält sie den Socket Deskriptor. Zum anderen ist remotefd eine Marke (flag). Ist remotefd gleich INVALID_SOCKET besteht noch keine Netzwerkverbindung.

/* winudp_send.c
 * MS-Windows UDP Babyphone Sender (Mikrofon)
 * Andre Adrian
 */

#include "../winudp_common.h"
#include "winudp_send.h"

// Netzwerk Ausgabe
struct sockaddr_in remote;          // UDP Empfaenger IPv4 Adress und Port Struktur
SOCKET remotefd = INVALID_SOCKET;   // UDP Empfaenger Filedeskriptor

// Audio Eingabe (Empfang vom Mikrofon)
HWAVEIN hWaveIn;
WAVEHDR WaveHdrIn[AUDIOBUFS];
BYTE BufferIn[AUDIOBUFS][BUFSIZE];

BOOL CALLBACK DlgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg) {
    case WM_INITDIALOG: {     
      WSADATA wsaData;
      int rv;
      int i;
      WAVEFORMATEX waveform;
     
      // Init Netzwerk
      rv = WSAStartup(MAKEWORD(2, 2), &wsaData);
      winexit_if(rv != 0, hWnd, "WSAStartup failed");

      // Init Audio
      waveform.nChannels = AUDIOCHANNELS;
      waveform.wBitsPerSample = 8;
      waveform.nAvgBytesPerSec = AUDIOSPEED * waveform.nChannels * waveform.wBitsPerSample / 8;
      waveform.wFormatTag = AUDIOFORMAT;
      waveform.nSamplesPerSec = AUDIOSPEED;
      waveform.nBlockAlign = 1;
      waveform.cbSize = 0;

      rv = waveInOpen(&hWaveIn, WAVE_MAPPER, &waveform, MAKELONG(hWnd, 0), 0, CALLBACK_WINDOW);
      winexit_if(rv != MMSYSERR_NOERROR, hWnd, "waveInOpen failed");

      for (i = 0; i < AUDIOBUFS; ++i) {
        WaveHdrIn[i].lpData = BufferIn[i];
        WaveHdrIn[i].dwBufferLength = sizeof(BufferIn[0]);
        WaveHdrIn[i].dwBytesRecorded = 0;
        WaveHdrIn[i].dwUser = 0;
        WaveHdrIn[i].dwFlags = 0;
        WaveHdrIn[i].dwLoops = 0;
        WaveHdrIn[i].lpNext = NULL;
        WaveHdrIn[i].reserved = 0;
        waveInPrepareHeader(hWaveIn, WaveHdrIn + i, sizeof(WaveHdrIn[0]));
        waveInAddBuffer(hWaveIn, WaveHdrIn + i, sizeof(WaveHdrIn[0]));
      }

      waveInStart(hWaveIn);   // starte Aufnahme
      break;
    }

    case MM_WIM_DATA: {
      int rv;
      WAVEHDR *Hdr = (WAVEHDR *) lParam;

      if (remotefd != INVALID_SOCKET) {
        rv = sendto(remotefd, Hdr->lpData, sizeof(BufferIn[0]), 0,
                    (struct sockaddr *) &remote, sizeof(remote));
        if (rv < 0) {
          closesocket(remotefd);
          remotefd = INVALID_SOCKET;
          MessageBox(hWnd, "sendto failed", "WARNING", MB_OK);
        }
      }

      waveInAddBuffer(hWaveIn, Hdr, sizeof(*Hdr));
      break;
    }

    case WM_COMMAND: {
      if (ID_CONNECT == wParam) {
        struct hostent *he;
        int rv;
        char yes = '1';
        char nodename[128];

        if (remotefd != INVALID_SOCKET) {
          closesocket(remotefd);
          remotefd = INVALID_SOCKET;
        }

        // Hole den Nodename aus dem Edit Dialog
        rv = GetDlgItemText(hWnd, ID_NODENAME, nodename, sizeof(nodename));

        he = gethostbyname(nodename);  // hole UDP Empfaenger IPv4 Adresse
        winreturn_if(NULL == he, hWnd, "gethostbyname failed");

        remotefd = socket(AF_INET, SOCK_DGRAM, 0); // Socket Filedeskriptor anlegen
        winreturn_if(remotefd < 0, hWnd, "socket failed");

        // UDP Broadcast erlauben
        rv = setsockopt(remotefd, SOL_SOCKET, SO_BROADCAST, &yes, sizeof(yes));
        winreturn_if(rv < 0, hWnd, "setsockopt failed");

        memset(&remote, 0, sizeof(remote)); // remote mit 0 fuellen
        remote.sin_family = AF_INET;
        remote.sin_port = htons(BABYPHONE_PORT);
        remote.sin_addr = *((struct in_addr *) he->h_addr);
      }

      if (IDCANCEL == wParam) {
        closesocket(remotefd);
        EndDialog(hWnd, 0);
      }
      break;
    }
  }
  return 0;
}

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  InitCommonControls();
  DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), HWND_DESKTOP, DlgProc);
  return 0;
}

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  InitCommonControls();
  DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), HWND_DESKTOP, DlgProc);
  return 0;
}

MS-Windows Babyphone UDP-Sender Resource Datei

Die Resource Datei kann nicht innerhalb der Microsoft Visual C++ 2005 Express Edition Entwicklungsumgebung editiert werden. Wenn die Datei mit einem Texteditor wie Wordpad editiert wird bemerkt die Entwicklungsumgebung die Änderung und reagiert sinnvoll. Weitere Informationen zu Resource Dateien gibt es im MSDN Artikel Resource-Definition Statements .

// winudp_send.rc
//
#include "windows.h"
#include "winver.h"
#include "winudp_send.h"

IDD_DIALOG1 DIALOGEX DISCARDABLE  10, 150, 200, 100
STYLE WS_MINIMIZEBOX | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU |
    WS_THICKFRAME
CAPTION "winudp_send"
FONT 8, "Lucida Console"
BEGIN
    LTEXT           "Nodename:", 1000,    10, 10, 100, 15
    EDITTEXT        ID_NODENAME,          10, 30, 100, 15
    DEFPUSHBUTTON   "Connect",ID_CONNECT, 10, 50, 35,  15, BS_CENTER | BS_VCENTER
END


MS-Windows Babyphone UDP-Sender Header Datei

Die Headerdatei definiert die symbolischen Konstanten welche in der Resource Datei und Quelltext Datei verwendet werden.

// winudp_send.h
// Resource
//
#define IDD_DIALOG1                     101
#define ID_NODENAME                     1001
#define ID_CONNECT                      1002


MS-Windows Babyphone UDP-Empfänger Datei

Der Empfang von Netzwerk Paketen erfolgt in einem eigenen Thread. Hat recvfrom() ein UDP Paket empfangen wird ein leerer Wiedergabe Audiobuffer gesucht. Gibt es im Moment keinen leeren Audiobuffer wird das Netzwerk Datenpaket nicht ausgespielt. Das Verwerfen von Audiopaketen erfolgt ab und zu wenn der Taktgeber in der Soundkarte des Sender-Computers (Mikrofon) mit eine höheren Frequenz als der Taktgeber in der Soundkarte des Empfänger-Computers (Lautsprecher) arbeitet - die Taktgeber sind Quarzoszillatoren und haben eine Toleranz von 10 hoch -5.
Nach dem Empfang der MM_WOM_DATA Meldung wird der Audiobuffer wieder als frei markiert und kann für eine weitere Ausgabe verwendet werden.
Einen VoIP-Jitter Buffer erhält man durch eine grössere Anzahl von Audiobuffern auf der Empfangsseite. Leider bedeuten mehr Audiobuffer auch immer mehr Latenz (Laufzeit) und damit eine schlechtere Verständigung bei Zwei-Wege-Kommunikation.

/* winudp_recv.cpp
 * MS-Windows UDP Babyphone Empfaenger (Lautsprecher)
 * Andre Adrian
 */

#include "../winudp_common.h"
#include "winudp_recv.h"

HWND hWindow;

// Audio Output (send to loudspeaker)
HANDLE WorkerThreadHandleOut;
DWORD WorkerThreadIdOut;
HWAVEOUT hWaveOut;
WAVEHDR WaveHdrOut[AUDIOBUFS];
BYTE BufferOut[AUDIOBUFS][BUFSIZE];

DWORD WINAPI UdpRecvThread (void *Arg)
{
  WSADATA wsaData;
  struct sockaddr_in local;   // UDP Empfaenger IPv4 Adress und Port Struktur
  SOCKET localfd;             // UDP Empfaenger Filedeskriptor
  int len;
  int rv;
 
  /* MS-Windows spezielle Socket Initialisierung */
  rv = WSAStartup(MAKEWORD(2, 2), &wsaData);
  winexit_if(rv != 0, hWindow, "WSAStartup failed");

  localfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // Socket Filedeskriptor anlegen
  winexit_if(localfd < 0, hWindow, "socket failed");

  memset(&local, 0, sizeof(local)); // local mit 0 vollschreiben
  local.sin_family = AF_INET;
  local.sin_port = htons(BABYPHONE_PORT);
  local.sin_addr.s_addr = INADDR_ANY;

  // local Server IPv4 Adresse eintragen
  rv = bind(localfd, (struct sockaddr *) &local, sizeof(local));
  winexit_if(rv < 0, hWindow, "bind failed");

  for (;;) {                                // Endlosschleife
    struct sockaddr_in remote;  // UDP Sender IPv4 Adress und Port Struktur
    int i;
    int addr_len;
    BYTE buffer[BUFSIZE];
   
    addr_len = sizeof(remote);
    len = recvfrom(localfd, buffer, sizeof(buffer), 0, (struct sockaddr *) &remote, &addr_len);
    winexit_if(rv < 0, hWindow, "recvfrom failed");
 
    for (i = 0; i < AUDIOBUFS; ++i) {
      if (0 == WaveHdrOut[i].dwUser) {
        WaveHdrOut[i].dwUser = 1;       // Buffer belegt markieren
        CopyMemory(WaveHdrOut[i].lpData, buffer, sizeof(buffer));
        waveOutWrite(hWaveOut, WaveHdrOut + i, sizeof(WaveHdrOut[0]));
        break;
      }
    }
  }
}


BOOL CALLBACK
DlgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg) {
    case WM_INITDIALOG: {
      int i;
      int rv;
      WAVEFORMATEX waveform;
      HANDLE UdpRecvThreadHandle;
      DWORD UdpRecvThreadId;

      hWindow = hWnd;
     
      // Init Audio
      waveform.nChannels = AUDIOCHANNELS;
      waveform.wBitsPerSample = 8;
      waveform.nAvgBytesPerSec = AUDIOSPEED * waveform.nChannels * waveform.wBitsPerSample / 8;
      waveform.wFormatTag = AUDIOFORMAT;
      waveform.nSamplesPerSec = AUDIOSPEED;
      waveform.nBlockAlign = 1;
      waveform.cbSize = 0;

      rv = waveOutOpen(&hWaveOut, WAVE_MAPPER, &waveform, MAKELONG(hWnd, 0), 0, CALLBACK_WINDOW);
      winexit_if(rv != MMSYSERR_NOERROR, hWnd, "waveOutOpen failed");

      for (i = 0; i < AUDIOBUFS; ++i) {
        WaveHdrOut[i].lpData = (LPSTR) BufferOut[i];
        WaveHdrOut[i].dwBufferLength = sizeof(BufferOut[0]);
        WaveHdrOut[i].dwBytesRecorded = 0;
        WaveHdrOut[i].dwUser = 0;
        WaveHdrOut[i].dwFlags = 0;
        WaveHdrOut[i].dwLoops = 0;
        WaveHdrOut[i].lpNext = NULL;
        WaveHdrOut[i].reserved = 0;
        waveOutPrepareHeader(hWaveOut, WaveHdrOut + i, sizeof(WaveHdrOut[0]));
      }
     
      // Init Network
      UdpRecvThreadHandle = CreateThread (NULL, 0, UdpRecvThread, NULL, 0, &UdpRecvThreadId );
      winexit_if(NULL == UdpRecvThreadHandle, hWindow, "CreateThread failed");
      break;
    }

    case MM_WOM_DONE: {
      WAVEHDR *Hdr = (WAVEHDR *) lParam;

      Hdr->dwUser = 0;  // Buffer frei melden
      break;
    }

    case WM_COMMAND: {
      if (IDCANCEL == wParam) {
        EndDialog(hWnd, 0);
      }
      break;
    }
  }
  return 0;
}

int APIENTRY
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  InitCommonControls();
  DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), HWND_DESKTOP, DlgProc);
  return 0;
}


MS-Windows Babyphone UDP-Empfänger Resource Datei

// winudp_recv.rc
//
#include "windows.h"
#include "winver.h"
#include "winudp_recv.h"

IDD_DIALOG1 DIALOGEX DISCARDABLE  10, 10, 200, 100
STYLE WS_MINIMIZEBOX | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU |
    WS_THICKFRAME
CAPTION "winudp_recv"
FONT 8, "Lucida Console"
BEGIN
END


MS-Windows Babyphone UDP-Empfänger Header Datei

// winudp_recv.h
//
#define IDD_DIALOG1                     101


MS-Windows Quelltext mit Callback Threads

Die Funktionen waveInOpen() und waveOutOpen() können mit dem Parameter CALLBACK_THREAD aufgerufen werden. Dann werden die Windows Meldungen  MM_WIM_DATA und MM_WOM_DONE nicht mehr in die Meldungs-Schlange (message queue) des GUI Threads gesendet sondern in die Meldungs-Schlange eines eigenen Audio Threads. Dieses Aufteilen von Meldungen auf verschiedene Threads führt zu weniger Knacksen und Knistern.
Eine Alternative zu Waveform Audio ist ASIO von der Firma Steinberg. Das ASIO SDK enthält die nötigen Informationen und Header Dateien. Von Michael Tippach gibt es den ASIO4ALL Soundkarten Treiber damit auch einfache Soundkarten ASIO verstehen. Die ASIO Schnittstelle ist bei "Heim Musikstudio Soundkarten" für Harddisk Recording üblich. Leider arbeitet ASIO nur mit Abtastfrequenzen von 44.1kHz, 48kHz und höher. Die bei VoIP üblichen Abtastfrequenzen von 8kHz und 16kHz sind bei ASIO nicht vorhanden.

MS-Windows Babyphone UDP-Sender Datei mit Audio Thread

/* winudp_send.c
 * MS-Windows UDP Babyphone Sender (Mikrofon) mit Audio Thread
 * Andre Adrian
 */

#include "../winudp_common.h"
#include "winudp_send.h"

HWND hWindow;                       // Window Handle

// Netzwerk Ausgabe
struct sockaddr_in remote;                    // UDP Empfaenger IPv4 Adress und Port Struktur
volatile SOCKET remotefd = INVALID_SOCKET;    // UDP Empfaenger Filedeskriptor
CRITICAL_SECTION remotefd_sem;

// Audio Eingabe (Empfang vom Mikrofon)
HWAVEIN hWaveIn;
WAVEHDR WaveHdrIn[AUDIOBUFS];
BYTE BufferIn[AUDIOBUFS][BUFSIZE];

DWORD WINAPI WaveInThread (void *Arg)
{
  MSG Msg;
  int rv;

  rv = SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_TIME_CRITICAL);
  winexit_if(0 == rv, hWindow, "SetThreadPriority failed");

  while (GetMessage (&Msg, NULL, 0, 0) == TRUE) {
    if (MM_WIM_DATA == Msg.message) {
      int rv = 0;
      WAVEHDR *Hdr = (WAVEHDR *) Msg.lParam;

      EnterCriticalSection(&remotefd_sem);
      if (remotefd != INVALID_SOCKET) {
        rv = sendto(remotefd, Hdr->lpData, sizeof(BufferIn[0]), 0,
                             (struct sockaddr *) &remote, sizeof(remote));
        if (rv < 0) {
          closesocket(remotefd);
          remotefd = INVALID_SOCKET;
        }
      }
      LeaveCriticalSection(&remotefd_sem);
      waveInAddBuffer(hWaveIn, Hdr, sizeof(*Hdr));
      if (rv < 0) MessageBox(hWindow, "sendto failed", "WARNING", MB_OK);
    }
  }
  return 0;
}

BOOL CALLBACK DlgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg) {
    case WM_INITDIALOG: {     
      WSADATA wsaData;
      int rv;
      int i;
      WAVEFORMATEX waveform;
      HANDLE WaveInThreadHandle;
      DWORD WaveInThreadId;
     
      hWindow = hWnd;

      // Init Netzwerk
      rv = WSAStartup(MAKEWORD(2, 2), &wsaData);
      winexit_if(rv != 0, hWnd, "WSAStartup failed");

      // Init Audio
      waveform.nChannels = AUDIOCHANNELS;
      waveform.wBitsPerSample = 8;
      waveform.nAvgBytesPerSec = AUDIOSPEED * waveform.nChannels * waveform.wBitsPerSample / 8;
      waveform.wFormatTag = AUDIOFORMAT;
      waveform.nSamplesPerSec = AUDIOSPEED;
      waveform.nBlockAlign = 1;
      waveform.cbSize = 0;

      WaveInThreadHandle = CreateThread(NULL, 0, WaveInThread, NULL, 0, &WaveInThreadId);
      winexit_if(NULL == WaveInThreadHandle, hWnd, "CreateThread failed");

      rv = waveInOpen(&hWaveIn, WAVE_MAPPER, &waveform, (DWORD)WaveInThreadId,0,CALLBACK_THREAD);
      winexit_if(rv != MMSYSERR_NOERROR, hWnd, "waveInOpen failed");

      for (i = 0; i < AUDIOBUFS; ++i) {
        WaveHdrIn[i].lpData = BufferIn[i];
        WaveHdrIn[i].dwBufferLength = sizeof(BufferIn[0]);
        WaveHdrIn[i].dwBytesRecorded = 0;
        WaveHdrIn[i].dwUser = 0;
        WaveHdrIn[i].dwFlags = 0;
        WaveHdrIn[i].dwLoops = 0;
        WaveHdrIn[i].lpNext = NULL;
        WaveHdrIn[i].reserved = 0;
        waveInPrepareHeader(hWaveIn, WaveHdrIn + i, sizeof(WaveHdrIn[0]));
        waveInAddBuffer(hWaveIn, WaveHdrIn + i, sizeof(WaveHdrIn[0]));
      }

      waveInStart(hWaveIn);   // starte Aufnahme
      break;
    }

    case WM_COMMAND: {
      if (ID_CONNECT == wParam) {
        struct hostent *he;
        int rv;
        char yes = '1';
        char nodename[128];
        SOCKET fd;

        EnterCriticalSection(&remotefd_sem);
        if (remotefd != INVALID_SOCKET) {
          closesocket(remotefd);
          remotefd = INVALID_SOCKET;  // remotefd auf nicht-initialisiert setzen
        }
        LeaveCriticalSection(&remotefd_sem);

        // Hole den Nodename aus dem Edit Dialog
        rv = GetDlgItemText(hWnd, ID_NODENAME, nodename, sizeof(nodename));

        he = gethostbyname(nodename);  // hole UDP Empfaenger IPv4 Adresse
        winreturn_if(NULL == he, hWnd, "gethostbyname failed");

        fd = socket(AF_INET, SOCK_DGRAM, 0); // Socket Filedeskriptor anlegen
        winreturn_if(fd < 0, hWnd, "socket failed");

        // UDP Broadcast erlauben
        rv = setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &yes, sizeof(yes));
        winreturn_if(rv < 0, hWnd, "setsockopt failed");

        memset(&remote, 0, sizeof(remote)); // remote mit 0 fuellen
        remote.sin_family = AF_INET;
        remote.sin_port = htons(BABYPHONE_PORT);
        remote.sin_addr = *((struct in_addr *) he->h_addr);

        remotefd = fd;  // Ohne EnterCritical.. - einfache Zuweisung ist atomar
      }

      if (IDCANCEL == wParam) {
        EndDialog(hWnd, 0);
      }
      break;
    }
  }
  return 0;
}

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  InitializeCriticalSection(&remotefd_sem);
  InitCommonControls();
  DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), HWND_DESKTOP, DlgProc);
  return 0;
}


MS-Windows Babyphone UDP-Empfänger Datei mit Audio Thread

/* winudp_recv.cpp
 * MS-Windows UDP Babyphone Empfaenger (Lautsprecher) mit Audio Thread
 * Andre Adrian
 */

#include "../winudp_common.h"
#include "winudp_recv.h"

HWND hWindow;

// Audio Output (send to loudspeaker)
HWAVEOUT hWaveOut;
WAVEHDR WaveHdrOut[AUDIOBUFS];
BYTE BufferOut[AUDIOBUFS][BUFSIZE];

DWORD WINAPI WaveOutThread (void *Arg)
{
  MSG Msg;
  int rv;

  rv = SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_TIME_CRITICAL);
  winexit_if(0 == rv, hWindow, "SetThreadPriority failed");
  while (GetMessage (&Msg, NULL, 0, 0) == TRUE) {
    if (MM_WOM_DONE == Msg.message) {
      WAVEHDR *Hdr = (WAVEHDR *) Msg.lParam;

      Hdr->dwUser = 0;  // Buffer frei melden
    }
  }
  return 0;
}

DWORD WINAPI UdpRecvThread (void *Arg)
{
  WSADATA wsaData;
  struct sockaddr_in local;   // UDP Empfaenger IPv4 Adress und Port Struktur
  SOCKET localfd;             // UDP Empfaenger Filedeskriptor
  int len;
  int rv;
 
  rv = SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_TIME_CRITICAL);
  winexit_if(0 == rv, hWindow, "SetThreadPriority 2 failed");

  /* MS-Windows spezielle Socket Initialisierung */
  rv = WSAStartup(MAKEWORD(2, 2), &wsaData);
  winexit_if(rv != 0, hWindow, "WSAStartup failed");

  localfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // Socket Filedeskriptor anlegen
  winexit_if(localfd < 0, hWindow, "socket failed");

  memset(&local, 0, sizeof(local)); // local mit 0 vollschreiben
  local.sin_family = AF_INET;
  local.sin_port = htons(BABYPHONE_PORT);
  local.sin_addr.s_addr = INADDR_ANY;

  // local Server IPv4 Adresse eintragen
  rv = bind(localfd, (struct sockaddr *) &local, sizeof(local));
  winexit_if(rv < 0, hWindow, "bind failed");

  for (;;) {                                // Endlosschleife
    struct sockaddr_in remote;  // UDP Sender IPv4 Adress und Port Struktur
    int i;
    int addr_len;
    BYTE buffer[BUFSIZE];
   
    addr_len = sizeof(remote);
    len = recvfrom(localfd, buffer, sizeof(buffer), 0, (struct sockaddr *) &remote, &addr_len);
    winexit_if(rv < 0, hWindow, "recvfrom failed");
 
    for (i = 0; i < AUDIOBUFS; ++i) {
      if (0 == WaveHdrOut[i].dwUser) {
        WaveHdrOut[i].dwUser = 1;       // Buffer belegt markieren
        CopyMemory(WaveHdrOut[i].lpData, buffer, sizeof(buffer));
        waveOutWrite(hWaveOut, WaveHdrOut + i, sizeof(WaveHdrOut[0]));
        break;
      }
    }
  }
}

BOOL CALLBACK
DlgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg) {
    case WM_INITDIALOG: {
      int i;
      int rv;
      WAVEFORMATEX waveform;
      HANDLE WaveOutThreadHandle;
      DWORD WaveOutThreadId;
      HANDLE UdpRecvThreadHandle;
      DWORD UdpRecvThreadId;

      hWindow = hWnd;
     
      // Init Audio
      waveform.nChannels = AUDIOCHANNELS;
      waveform.wBitsPerSample = 8;
      waveform.nAvgBytesPerSec = AUDIOSPEED * waveform.nChannels * waveform.wBitsPerSample / 8;
      waveform.wFormatTag = AUDIOFORMAT;
      waveform.nSamplesPerSec = AUDIOSPEED;
      waveform.nBlockAlign = 1;
      waveform.cbSize = 0;

      WaveOutThreadHandle = CreateThread (NULL, 0, WaveOutThread, NULL, 0, &WaveOutThreadId );
      winexit_if(NULL == WaveOutThreadHandle, hWnd, "CreateThread failed");

      rv = waveOutOpen(&hWaveOut,WAVE_MAPPER,&waveform,(DWORD)WaveOutThreadId,0,CALLBACK_THREAD);
      winexit_if(rv != MMSYSERR_NOERROR, hWnd, "waveOutOpen failed");

      for (i = 0; i < AUDIOBUFS; ++i) {
        WaveHdrOut[i].lpData = (LPSTR) BufferOut[i];
        WaveHdrOut[i].dwBufferLength = sizeof(BufferOut[0]);
        WaveHdrOut[i].dwBytesRecorded = 0;
        WaveHdrOut[i].dwUser = 0;
        WaveHdrOut[i].dwFlags = 0;
        WaveHdrOut[i].dwLoops = 0;
        WaveHdrOut[i].lpNext = NULL;
        WaveHdrOut[i].reserved = 0;
        waveOutPrepareHeader(hWaveOut, WaveHdrOut + i, sizeof(WaveHdrOut[0]));
      }
     
      // Init Network
      UdpRecvThreadHandle = CreateThread (NULL, 0, UdpRecvThread, NULL, 0, &UdpRecvThreadId );
      winexit_if(NULL == UdpRecvThreadHandle, hWindow, "CreateThread 2 failed");
      break;
    }

    case WM_COMMAND: {
      if (IDCANCEL == wParam) {
        EndDialog(hWnd, 0);
      }
      break;
    }
  }
  return 0;
}

int APIENTRY
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  InitCommonControls();
  DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), HWND_DESKTOP, DlgProc);
  return 0;
}


MS-Windows Babyphone Software Download

Die Zip Datei enthält die Quelltext Dateien und die Sln und Vcproj Dateien für die Kompilierung. Die Exe Dateien sind ebenfalls eingepackt - für erste Experimente ohne Compiler. Achtung: Die Exe Dateien vor dem ersten Start mit einem aktuellen Anti-Viren Programm untersuchen. Sicher ist sicher...

Download winbabyphone.zip 

Download winbabyphone1.zip mit Audio Threads

Literatur

Die Firma 4Front Technologies ist der Entwickler von OSS. Seit Juni 2007 steht OSS für Linux unter der GPL. Aktuell ist Version 4.0 von OSS. Ein älteres OSS Programmer's Guide von Hannu Savolainen hat mir bei der Programmierung sehr geholfen.

Von Microsoft gibt es den Artikel Using the Waveform Audio Interface im MSDN. Der Artikel ist zwar für Microsoft Windows CE 5.0 geschrieben, passt aber auch zu Win32.

Von Kent Reisdorph gibt es die dreiteiliege Artikelserie "Low-level wave audio" welche ab Juli 1998 in der Zeitschrift C++ Builder Developer's Journal erschienen ist: Low-level wave audio, Part I , Low-level wave audio, part 2 , Low-level wave audio, part 3 .

Der Programmierer gabuzomeuh (Pseudonym) verwendet im MOTEUR AUDIO TEMPS REEL die Funktionen waveInOpen() und waveOutOpen() mit dem Parameter CALLBACK_THREAD.

Von Brian "Beej Jorgensen" Hall gibt es eine gute Einführung in Netzwerk Programmierung mit Sockets unter Beej's Guide to Network Programming.

Von Martin Gräfe gibt es das Buch C und Linux (Hanser Verlag). Im Kapitel 6 wird die Programmierung der Soundkarte besprochen. Nicht so ausführlich wie im OSS Programmer's Guide aber dafür in deutsch. Die im Internet angebotene Leseprobe beschreibt die Programmierung eines CD-Players für Digital Audio Compact Disc.

Von Jeff Tranter stammt das Buch Linux Multimedia Guide (Verlag O'Reilly). Das Buch ist vergriffen. Das Kapitel 14 Programming Sound Devices gibt es als Excerpt im Internet.