[OpenBSD]

Portierung von Audio-Applikationen nach OpenBSD

Dieses Dokument behandelt gegenwärtig nur die Fragen rund um das Gebiet der gesampelten Sounds. Jedoch sind Beiträge zum Thema Synthesizer und Waveforms jederzeit willkommen.

Audioapplikationen sind tendenziell schwer zu portieren, da diese sich in einem nicht standardisierten Bereich befinden. Thematische Annäherungen variieren allerdings nicht erheblich zwischen den verschiedenen Betriebssystemen.

Einsatz von ossaudio

Die ossaudio-Emulation ist zwar möglicherweise die einfachste Wahl, doch nicht immer machbar und für gewöhnlich auch nicht die beste Wahl.

Einsatz von NetBSD- und FreeBSD-basiertem Code

Da wir Teile der Audiogerätedateien (Interfaces) gemeinsam mit NetBSD und FreeBSD benutzen, ist es einleuchtend, mit einem NetBSD-Port als Grundlage zu beginnen. Man sollte sich jedoch im Klaren darüber sein, dass sich einige Pfade in der Verzeichnisstruktur geändert haben, an denen bestimmte Dateien zu finden waren; mancher Eintrag in sys/audioio.h ist bereits überholt ist. Des Weiteren tendieren viele Ports dazu, falsch codiert zu sein, und lassen sich daher nur auf einen Maschinentyp anwenden. Einige Änderungen sind von daher unumgänglich; siehe dazu den nächsten Abschnitt.

Das Schreiben von OpenBSD-Code

Hardwareunabhängigkeit

DU SOLLST KEINE VERMUTUNG DARÜBER ANSTELLEN, WELCHE AUDIOHARDWARE BENUTZT WIRD.
Falscher Code ist Code, der lediglich das a_info.play.precision-Feld auf 8 oder 16 Bits hin überprüft, und Samples unter der Annahme einer Soundblasterumgebung als signed oder unsigned interpretiert. Der Sampletypus sollte ausdrücklich geprüft werden, und demgemäß auch der Code selbst. Ein einfaches Beispiel:

    AUDIO_INIT_INFO(&a_info);
    a_info.play.encoding = AUDIO_ENCODING_SLINEAR;
    a_info.play.precision = 16;
    a_info.play.sample_rate = 22050;
    error = ioctl(audio, AUDIO_SETINFO, &a_info);
    if (error)
	/* deal with it */
    error = ioctl(audio, AUDIO_GETINFO, &a_info);
    switch(a_info.play.encoding)
	{
    case AUDIO_ENCODING_ULINEAR_LE:
    case AUDIO_ENCODING_ULINEAR_BE:
	if (a_info.play.precision == 8)
	    /* ... */
	else
	    /* ... */
	break;
    case ...

    default:
	/* don't forget to deal with what you don't know !!! For instance, */
	fprintf(stderr,
		"Unsupported audio format (%d), ask ports@ about that\n",
		a_info.play.encoding);

	}
    /* now don't forget to check what sampling frequency you actually got */
	

Dieses kleinst mögliche Codefragment umfasst den größten Teil aller möglichen Fälle

16-Bit-Formate und Endianness

Normalerweise wird nur nach einem Kodierungstyp gefragt (also zum Beispiel AUDIO_ENCODING_SLINEAR), und man findet eine Kodierung mit Endianness wieder (z. B. AUDIO_ENCODING_SLINEAR_LE). Unter Berücksichtigung der Tatsache, dass eine Soundkarte nicht dieselbe Endianness benutzen muss wie die Plattform, auf der sie läuft, sollte man gerade auf diesen Umstand vorbereitet sein. Der einfachste Weg wäre gewiss die Einrichtung eines vollen Audiopuffers sowie die Verwendung von swab(3), falls ein Endiannesswechsel erforderlich ist. Der Umgang mit externen Samples läuft meistens auf Folgendes hinaus:
  1. Analyse des Sampleformats,
  2. Übernahme des Samples,
  3. Endianness-Wechsel, wenn es nicht deinem Ursprungsformat entspricht,
  4. Den gewünschten Output in den Puffer rechnen,
  5. Endianness-Wechsel, wenn die Soundkarte nicht deinem Ursprungsformat entspricht,
  6. Den Puffer abspielen.
Selbstverständlich kann man die Schritte 3 und 5 weglassen, wenn man einen Soundsample im Ursprungsformat der Soundkarte spielt.

Audioqualität

Die Hardware kann einige eigenartige Begrenzungen aufweisen, so dass sie zwar nicht fähig ist, über 22050 Hz im Stereobereich hinaus zugehen, wohl aber im Monobereich die 44100-Grenze überschreiten kann. In solchen Fällen sollte man dem Benutzer die Chance geben, seine Präferenzen selbst anzugeben, um dann die bestmögliche Performance umzusetzen. So ist es zum Beispiel einfach nur unsinnig, den Bereich auf 22050 Hz zu limitieren, nur weil man Stereoausgabe erzielen will. Was ist, wenn der Benutzer kein Stereosoundsystem an seine Soundkarte angeschlossen hat?

Ebenso unsinnig ist es, soundblasterähnliche Begrenzungen in deinem Programm zu hardcoden. Man sollte sich darüber im Klaren sein und trotzdem versuchen, die 22050-Hz/Stereo-Barriere zu überwinden und die Ergebnisse anschließend zu überprüfen.

Samplingfrequenz

Man sollte unbedingt die von der Karte zurückgegebene Samplingfrequenz überprüfen. Eine Diskrepanz von 5 % mündet in eine Verschiebung um einen Halbton: einige Menschen haben im Gegensatz zu den meisten, denen dieser Umstand gar nicht auffällt, ein zu feines Gehör und bemerken diese Verschiebung. Deine Applikation sollte im Stande sein, während der Übertragung zu resampeln - möglichst unmittelbar oder zumindest doch mittelbar durch Applikation, die auf Shannons Resamplingformeln basieren.

Der dynamische Bereich

Samples schöpfen nicht immer den ihnen zukommenden Wertebereich komplett aus. Zunächst einmal sind Samples mit geringer Aufnahmeverstärkung auf der Maschine nicht besonders laut, so dass der Benutzer sich genötigt sieht, die Lautstärke zu erhöhen. Des Weiteren bedeutet eine leise Soundausgabe auf Maschinen mit schlecht isolierter Audiohardware, dass man eher den »Herzschlag« der Maschine hört als den erwarteten Sound. Letztendlich kann es passieren, dass man nach unbedarfter Umwandlung von 16 auf 8 Bit nur noch mit 4 Bits brauchbarer Soundausgabe dasteht, was eine wirklich miese Qualität bedeutet.

Die bestmögliche Lösung wäre es, den kompletten Stream, welchen man abspielen möchte, frühst möglich zu scannen und zu skalieren, sodass er den ganzen zur Verfügung stehenden dynamischen Bereich ausfüllen kann. Sollte das nicht funktionieren, dafür aber ein Teil von dem, was du abspielen willst, einsehbar ist, kannst du die Klangverstärkung »on the fly« vornehmen - du musst lediglich klarstellen, dass der Verstärkungsfaktor auf einer niedrigeren Frequenz im Vergleich zum Sound bleibt, der gespielt werden soll. Vermeide jede Art von Überläufen - sie klingen immer viel schlechter als die ursprünglich von dir angestrebte Verbesserung.
Da die Schallpegelwahrnehmung logarithmisch ist, genügt der arithmetische Shiftgebrauch. Sind die Daten signed, dann sollte der Shift ausdrücklich als Division gecodet werden, da der C-Operator >> nicht auf Daten portiert werden kann, die signed sind.

Sollte dies alles nicht greifen, muss dem Benutzer wenigstens die Option der Lautstärkeregelung an die Hand gegeben werden.

Audioperformance

Hinsichtlich der Lowend-Applikationen gibt es nicht viel, um das man sich Gedanken machen müsste. Man sollte aber darauf achten, dass einige von uns OpenBSD auf einem Lowend-Level von 68030 verwenden - und dass eine Soundapplikation auf diesem Level laufen sollte, wenn sie es kann.

Vergiss das Benchmarking nicht. Theoretische Optimierungen sind nichts weiter als das: nämlich theoretisch. Es sollten schon genügend nüchterne Daten ermittelt werden, um entscheiden zu können, was eine wirkliche Verbesserung ist und was nicht.

Hinsichtlich der Highperformance-Audioapplikationen (wie z. B. mpegI-layer3) kommen folgende Punkte in Betracht:

Modellcharakter für optimale Ergebnisse hat die Vorgehensweise, dass eingangs ein kleines Testprogramm kompiliert wird, welches zunächst eine spezifische Audiohardware abfragt, um dann dein Programm weiter so auszukonfigurieren, dass es mit der entsprechenden Hardware bestmöglich harmoniert. Es sollte dir schon klar sein, dass Leute, die eine gute Audioperformance erzielen möchten, deinen Port rekompilieren werden, um ihn auf andere Hardwaresysteme auszuweiten - insofern macht es einen Unterschied.

Echtzeit oder synchronisiert

Trotz des Umstandes, dass OpenBSD kein Echtzeitbetriebssystem ist, möchtest du vermutlich eine Audioapplikation programmieren, die größtenteils im Echtzeitmodus läuft (z. B. für Spiele). In einem solchen Fall solltest du die Blockgröße verkleinern, so dass die erzielten Soundeffekte sich nicht asynchron zum laufenden Spiel verhalten. Problematisch wird es aber dann, wenn das Audiodevice verhungert: dies führt zu einem abscheulichen Resultat.

Sollte hingegen lediglich die Synchronisation auf der Ebene der Audio/Grafik-Ausgabe erzielt werden und das Programmverhalten vorhersehbar ist, dann ist die Synchronisation leichter zu erzielen. Man spielt die Audiosamples einfach ab, befragt dann das Audiodevice mit AUDIO_GETOOFFS, was gerade gespielt wird, und benutzt schließlich diese Information zur grafischen Postsynchronisation. So wird auf diesem Wege eine sehr gute Synchronisation erzielt - vorausgesetzt, dass man häufig genug fragt (sagen wir mal jede zehnte Sekunde) und über genügend Rechenleistung verfügt, um die Anwendung laufen zu lassen. Eventuell müssen die Werte durch ein konstantes Offset optimiert werden, da es eine Differenz zwischen dem gibt, was audiotechnisch im Augenblick gespielt wird, und der verstrichenen Zeit, bis etwas im XWindow ausgegeben wird.

Dem Projekt Quelltexte beisteuern

Im Falle der Audioapplikationen ist die Zusammenarbeit mit dem jeweiligen Programmurheber enorm wichtig. Sollte der Code z. B. in seiner Anwendung auf Soundblasterkarten begrenzt sein, so wird er aller Voraussicht nach auch auf andere Technologien übertragen.

Deine Arbeit ist wertlos, wenn du deine Kommentare nicht dem Programmierer zukommen lässt.

Es kann ja auch sein, dass der Autor die Probleme, mit denen du dich im Augenblick beschäftigst, bereits selbst erkannt hat und diese längst im Development-Tree adressiert hat. Kooperation ist ebenfalls eine hervorragende Idee, wenn die Patches, die du schreibst, sich über mehr als nur ein paar Zeilen erstrecken.


Portierung www@openbsd.org
$OpenBSD: audio-port.html,v 1.6 2008/03/09 13:37:10 tobias Exp $