Audio unter MacOSX

Ich habe mir jetzt mal vorgenommen, alle technische Probleme, denen ich begegne, hier mal zu dokumentieren. Dann brauch ich nächstes mal nicht stundenlang nachzudenken “da hatte ich doch mal”, sondern brauch nur Google kurz zu bemühen. Und wichtig ist da auch der Hintergedanke “das bringt mir totale Hackerleetness, Fame und ganz viele Comments” 🙂

Als erster Beitrag in der Reihe “Coden für Gedächtnisbeschädigte”: Audio unter MacOSX. Hintergrund ist folgender: ich will für mein FEC Protokoll jetzt gleich den MP3 Player integrieren, damit diese lästige Konvertiererei zwischen MP3 Frames und MP3 ADUs wegfällt, und damit ich auch die Pausen bei Packet Loss genauer kontrollieren kann. Dazu würde ich gerne die Mad Library verwenden, weil der Decoder in mpg123 (den man übrigens in den meisten MP3 Playern unter Unix wiederfindet) wirklich unter aller Sau ist. Genau mit der Library hatte ich vor kurzem was programmiert, und zwar eine Wrapperlibrary um Lame, um mit einer “simplistischen API” MP3s abspielen zu können. Die Library hatte ich ursprünglich unter Linux und FreeBSD geschrieben, wo man zum Ausgeben von Klängen meistens nur “/dev/dsp” öffnen muss. Im Moment arbeite ich wieder auf meinem Powerbook, und da geht das leider nicht so einfach.

Unter MacOSX gibt es verschiedene Möglichkeiten, Rumzuquaken: Carbon, oder CoreAudio, oder Quicktime. Quicktime lassen wir mal aussen aus, und Carbon auch, wir sind ja hier totale Darwin-Cocoa-h4x0r. Bei CoreAudio kann man sich die ID von einem AudioDevice schnappen, auf diesem Device dann verschiedene Properties verändern, und dann eine IO Procedure registrieren. Nachdem das AudioDevice dann “gestartet” wurde, ruft das CoreAudio Framework die IO Procedure regelmässig in einem zusätzlichen Thread auf, und erwartet, dass diese ihr Samples zum abspielen gibt. Hintergrund von diesem ganzen Aufwand ist, dass somit Buffering und ruckelloses Abspielen von Audiodaten viel einfacher geht, als wenn man “PUSH”-mässig die Audiodaten auf ein Device schreibt (und man braucht die Soundausgabe nicht selber zu schedulen). Wenn man nur schnell mal was abspielen will, ist sowas aber schon recht ärgerlich. Erstens muss man die Audiosamples aus dem Decoder (hier aus der Mad-Library) irgendwozwischenspeichern, dann noch auf diese Daten zugreifen können, und zwar Threadsafe (der Audiothread läuft parallel zum Decoden), und schliesslich will man auch noch ein bisschen zwischenpuffern.

Da ich so faul bin, habe ich erstmal geguckt ob ich bei mpg123 und bei madplay, die MacOSX-Backends haben, nicht den code schnell mal rausrippen könnte, aber so in der Form, wie man dort diesen Ansatz vorfindet, will man das eigentlich nicht. Deswegen, neu schreiben (nach dem ADAM-Prinzip: Alles Deppen Ausser Mir):

  • als erstes brauchen wir eine Datenstruktur, um die dekodierten Samples abzuspeichern, wenn sie aus der Mad-Library kommen. Dazu verwenden wir einen Ringpuffer.
  • als zweites: diese Datenstruktur muss thread-safe sein, dazu verwenden wir Pthread Mutexe und Condition Variables.
  • als drittes: drumherum noch ein bisschen CoreAudio Kram.

Die Ringpufferstruktur habe ich so implementiert, dass ich sie später mal kurz Copy-pasten kann, ein bisschen verändere, und dann wiederverwende, weil sowas braucht man oft schnell mal. Der Code (besonders für den Fall, bei dem die eingefügten Daten über die Puffergrenze fallen) ist nicht so ganz prickelnd, aber nun gut. In der Datei rb.h wird die Ringpuffer Datenstruktur definiert, dazu noch ein paar Compile-Time-Konstante und Typen wie rb_elt_t (der Typ der Elemente in dem Ringpuffer), die Grösse des Puffers, und ob der Puffer Threadsafe sein soll oder nicht. Die rb_t Struktur selber ist dann relativ straightforward:

typedef struct rb_s {
  rb_elt_t buf[RB_SIZE];
  unsigned long start;
  unsigned long count;

#ifdef USE_PTHREAD
  pthread_mutex_t mutex;
  pthread_cond_t  cond;
#endif
} rb_t;

Die API dementsprechend auch. rb_init resettet die Felder einer rb_t Struktur, rb_destroy macht dann das Umgekehrte. rb_enqueue fügt Daten in den Ringpuffer ein, und gibt 1 zurück wenn es geklappt hat. rb_dequeue liest Daten aus dem Ringpuffer, und gibt 1 zurück wenn es geklappt hat. Ein kleiner Trick jedoch, rb_enqueue wartet auf einen freien Pufferplatz, wenn Pthreads eingeschaltet ist (das ist der Spezialfall beim Schreiben von Audiodaten: die dürfen auf keinem Fall verloren gehen). Dazu ist dann auch gleich die Funktion rb_wait da. Die Implementierung dieser API könnt ihr in rb.c finden.

void rb_init(rb_t *rb);
int  rb_enqueue(rb_t *rb, rb_elt_t *data, unsigned long count);
int  rb_dequeue(rb_t *rb, rb_elt_t *dest, unsigned long count);
void rb_destroy(rb_t *rb);

#ifdef USE_PTHREAD
void rb_wait(rb_t *rb, unsigned long count);
#endif

Nun geht es weiter mit dem eigentlich Audiocode. Von der Mad-Library wird ein Callback von uns aufgerufen, wenn ein Frame fertig decodet worden ist. Ein MP3 Frame besteht aus 1152 Samples (ist im Standard so festgeschrieben). Mad verwendet intern nur Fixkommazahlen, die den Typ mad_fixed_t haben. Unser Callback kriegt als Argument eine Struktur mad_pcm, die genau 1152 dieser Fixkommazahlen beinhält (bzw. 1152 * 2 wenn es 2 Kanäle gibt). Diese Fixkommazahlen müssen erstmal nach float konvertiert werden, weil CoreAudio float Zahlen haben will. Nachdem diese Zahlen konvertiert worden sind, werden sie in unserem extra an float-Zahlen und MP3 Frames angepassten Ringpuffer eingefügt. Das Ganze sieht dann so aus:

  int ret;
  float buf[1152 * pcm->channels];
  float *ptr = buf;
  int i;
  mad_fixed_t const *left_ch, *right_ch;
  left_ch  = pcm->samples[0];
  right_ch = pcm->samples[1];
  for (i = 0; i length; i++) {
    signed int sample;
    *ptr++ = mad_scale(*left_ch++) / 32768.0;
    *ptr++ = mad_scale(*right_ch++) / 32768.0;
  }
  ret = rb_enqueue(&audio.rb, buf, 1152 * pcm->channels);

Und im Audio-Callback:

  int i;
  for(i = 0; i mNumberBuffers; i++) {
    AudioBuffer *buffer = outOutputData->mBuffers + i;

    if ((buffer->mDataByteSize / sizeof(float)) != (1152 * 2)) {
      memset(buffer->mData, 0, 1152 * 2 * sizeof(float));
      continue;
    }

    int ret;
    ret = rb_dequeue(&audio.rb, buffer->mData, 1152 * 2);
    if (ret == 0)
      memset(buffer->mData, 0, 1152 * 2 * sizeof(float));
  }

Den kompletten Code könnt ihr hier nachchecken, aber das ist im Moment so eine Baustelle :).

This entry was posted in Programmstaub. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *