Unterstützung für 13 Sound-Systeme (also auch das "neue" 10.2 Surround)
-
Mit dem Mixer wäre ich vorsichtig. Der steuert z.T. direkt die Hardware an, und die macht z.T. was sie will. Manche Chips meinen dass sie Lautstärke linear zu verstehen haben, andere logarithmisch und vermutlich gibt es sogar welche die noch ganz was anderes machen.
Es gibt zwar z.T. Standards (z.B. USB Audio), aber daran halten sich leider nicht alle Geräte. Ich hab' selbst zwei verschiedene USB Audio Geräte, und die verhalten sich krass unterschiedlich. Ich vermute dass eines linear regelt (weil es bei 10% Master-Volume-Slider Einstellung bereits fast volle gehörte Lautstärke hat), und das andere logarithmisch (weil die gehörte Lautstärke sich "stimmig" mit dem Master-Volume-Slider ändert). Was korrekt ist weiss ich nicht genau (ich vermute logarithmisch). Es können aber nicht beide richtig machen, sonst wäre ja kein Unterschied
Wenn du also die relative Lautsträke der einzelnen Kanäle zueinander exakt steuern willst, dann multipliziere einfach. Oder verwende eine API die es garantierterweise in Software macht.
Ich glaube auch, dass es kein nennenswerter Vorteil wäre, die relative Lautstärke über etwas zu steuern was die Multiplikation analog durchführen kann. Wenn du intern mit 24 Bit Integer rechnest (oder gleich 32 Bit Float), und in der "Ausgangsstufe" (nach der Lautsträke-Multiplikation) ggf. auf 16 Bit runter-ditherst wird der Qualitätsverlust durch die Lautsträke-Multiplikation vermutlich nicht hörbar sein. (Ausser natürlich von GOFs (Goldohrfraktion), die hören aber auch das Gras wachsen)
(Falls das Ausgabegerät 24 Bit wiedergeben kann macht das Dithern IMO keinen Sinn mehr, auch nicht wenn du vorher mit 32 Bit Float rechnest. Kein mir bekanntes Ausgabegerät schafft auch nur annähernd einen Rauschabstand, bei dem man den Unterschied zwischen geditherten und ungeditherten 24 Bit noch messen geschweige denn hören könnte.)
-
Ich habe mir alles nochmals genau angeschaut, ein wenig experimentiert und alles genau überlegt.
Von den Mixern werde ich sowieso die finger weg lassen. Einzig werde ich die Devices ermitteln und gegebenenfalls via waveOutOpen öffnen. Das war es auch schon.
Sollen mehrere Sound gleichzeitig gespielt werden, dann werden diese in ein Stream verfrachtet. Den Stream selbst lasse ich das Doublebuffering beibehalten.
Weil ich später eventuell Multithreading einbauen werde bzw. die Möglichkeit dafür erschaffen, werde ich ein weiteres Buffer einbauen. So habe ich folgende Aufteilung:
1. Thread = Loader & Memory manager
2. Thread = Core bzw. Mischer & SenderMan könnte noch für ein 3. Thread vorbauen, welches nur für die Umwandlung zuständig wäre. Da müsste man aber einen weiteren Buffer einbauen. Deshalb denke ich, dass da eine direkte Umwandlung in der Ausführung schneller wäre.
Der mit waveOutOpen erstellte Ausgabe-Kanal wird genau die Anzahl an Kanäle haben, wie das weilige Device dies auch unterstützt. Auf diesen Weg kann ich dann auch direkt ganz einfach die Sound-Boxen ansprechen (eben durch die entsprechende Kanäle) und eventuell sogar ganz einfach 2.1 Stereo, 3-Kanal in Mono umwandeln.
Lautstärke werde ich mit einfacher Multiplikation realisieren. Dazu gepaart mit einen kleinen Rauschentferner (eventuell wieder einmal Perlin Noise alg.) und Nebeneffekte sind kaum hörbar (dafür wohl aber auch ganze leise Töne).
Echtes Threading werde ich nicht einbauen (der eigenständige Thread von wavOutOpen genügt) aber, wie bereits beschrieben, die Aufteilung in diese Ebenen tätigen.
Ansonsten werde ich wohl mit 16 Bit als Standard Soundausgabe arbeiten. 32 Bit würde demnach in 16 Bit umgewandelt bzw. zugleich der Sound wohl auch "etwas normalisiert" werden.
Ganz klar werde ich das genannte 32 Bit Float nicht verwenden, weil am Ende zu ungenau.
In der Umsetzung würde ich Vollzeit (8h am Tag) etwa ein oder zwei Wochen benötigen, speziell weil ich noch nicht genau weiß was mich bzgl. Rauschentfernung erwartet.
-
Ich habe jetzt mal einen kleinen Anfang gemacht. Mit drei Befehlen können nun schon einmal die Devices abgefragt werden. Bis der Rest drin ist ("device starten" usw.), kann es aber noch dauern...
Edit: Wenn sich jemand fragt, wieso ich die Strings so extrabescheuert behandle, dann liegt das daran, dass ich nicht möchte, dass meine Soundlib anfällig für Fake-Sound-Treiber (Trojaner getarnt als Treiber) wird.
MAIN_SOUND_FUNC void soundManager_refreshSoundDevicesList() { if ( soundManager_soundDeviceListCount > 0 ) { for(int i = 0; i < soundManager_soundDeviceListCount; i++) delete soundManager_soundDeviceListEntry[i]; soundManager_soundDeviceListCount = 0; } HMIXER hMixer = NULL; MMRESULT mmResult; MIXERLINE Line; int stringLen; int numDevices = mixerGetNumDevs(); for( int deviceId = 0; deviceId < numDevices; deviceId++) { mmResult = mixerOpen(&hMixer, deviceId, 0, 0, MIXER_OBJECTF_MIXER); if ( mmResult == MMSYSERR_NOERROR ) { Line.cbStruct = sizeof(MIXERLINE); Line.dwComponentType = MIXERLINE_COMPONENTTYPE_DST_SPEAKERS; mmResult = mixerGetLineInfo( (HMIXEROBJ)hMixer, &Line, MIXER_OBJECTF_HMIXER | MIXER_GETLINEINFOF_COMPONENTTYPE ); if ( mmResult == MMSYSERR_NOERROR ) { soundDeviceListEntry *newEntry = new soundDeviceListEntry; newEntry->channels = (int)Line.cChannels; newEntry->controls = (int)Line.cControls; memset(newEntry->productName, 0, sizeof(newEntry->productName) ); memset(newEntry->shortName, 0, sizeof(newEntry->shortName) ); memset(newEntry->fullName, 0, sizeof(newEntry->fullName) ); stringLen = 0; while( stringLen++ < 254 && (char)Line.Target.szPname[stringLen] != 0 ) { } if ( stringLen > 0 ) memcpy(newEntry->productName, Line.Target.szPname, stringLen - 1); stringLen = 0; while( stringLen++ < 254 && (char)Line.szShortName[stringLen] != 0 ) { } if ( stringLen > 0 ) memcpy(newEntry->shortName, Line.szShortName, stringLen - 1); stringLen = 0; while( stringLen++ < 254 && (char)Line.szName[stringLen] != 0 ) { } if ( stringLen > 0 ) memcpy(newEntry->fullName, Line.szName, stringLen - 1); soundManager_soundDeviceListEntry[soundManager_soundDeviceListCount] = newEntry; soundManager_soundDeviceListCount++; } mixerClose(hMixer); } } } MAIN_SOUND_FUNC int soundManager_getSoundDeviceCount() { return( soundManager_soundDeviceListCount ); } MAIN_SOUND_FUNC std::string soundManager_getSoundDeviceTitle_bySoundIndex( int soundDeviceIndex ) { if ( soundDeviceIndex > -1 && soundDeviceIndex < soundManager_soundDeviceListCount ) return( std::string(soundManager_soundDeviceListEntry[soundDeviceIndex]->productName) ); else return std::string(""); }
-
ShadowTurtle schrieb:
Fake-Sound-Treiber (Trojaner getarnt als Treiber)
Du weisst schon dass Treiber sowieso mehr können als irgendein popeliges Programm, selbst wenn es mit Admin-Rechten läuft?
Und wo wäre das Problem bei
std::string(Line.Target.szPname, strnlen(Line.Target.szPname, 254))
?
-
Und eins kann ich dir auch gleich sagen: ICH würde deine Lib nicht mit der Kneifzange angreifen, wenn sie Funktionen + globalen State verwendet
-
Dann bist du selbst schuld
Edit: Oder um mal sachlich zu bleiben ^^ : Wie würdest du das lösen und gleichzeitig möglichst kompatiblen C Code erzeugen? Würdest du so eine art Master-Struktur erzeugen und verwenden?
Wenn du denkst, dass ich alles via extern regle und somit auf eine jeweilige Unit beschränkt bin, dann irrst du dich. Die Header File habe ich schon soweit vorbereitet entsprechend flexibel zu sein.Ansonsten muss ich zugeben, dass ich wohl propitären C und/oder C++ Quellcode erzeuge, dass aber immerhin zumindest einwandfrei funktioniert. Und sollte es nicht funktionieren, dann nehme ich gerne ein Tipp an.
Aber die Sache mit den String lasse ich jetzt erst einmal so. Ich schätze das ich später nicht einmal mehr std::string verwenden werde (weil in C einfach nicht da) und meine lib später auch Fit für Homebrew zeugs machen möchte.
In der Not kann ich auch eine vorkompilierte .lib-File erstellen mit einen schönen Header.
-
Ok, es war nie mein Ziel diverse Bestandteile universal verfügbar zu halten, weil diese Library relativ wenige global deklarierte Variablen hat (gerade einmal
und somit noch Übersicht und Verwaltung gewährleistet bleibt. Für größeres ist diese Lib auch nicht angedacht und falls doch, dann wäre es für den Kern in Ordnung.
Erweiterungen (beispielsweise Soundfilter) würden über Plugins gemacht und der Maintainer wäre wohl dann doch eine Klasse inkl. zusätzlichen PluginObject. Das ist aber noch zu arg Zukunftsmusik und auch nicht für eine Umsetzung gedacht (es existiert eben nur die Idee).Trotzdem muss ich eingestehen, dass falls ich wirklich Threading nutzen möchte, dass ich dann zumindest eine Master-Struktur haben sollte. Die Funktionen bleiben aber statisch deklariert bzw. member gibt es keine.
Edit: Wenn die Nachfrage besteht, dann kann ich nach der Fertigstellung einen Wrapper für Objektorientiertes Programmieren erstellen. Ich möchte gar nicht einmal wissen, wieviele Programmierer schon flach programmiert und erst zum Schluss einfach nur die entsprechenden Funktionsaufrufen in Klassen untergebracht haben.
Das ist nicht die feine Art, wird aber so praktiziert. Ich gehe damit zumindest offen um.
-
ShadowTurtle schrieb:
Edit: Wenn die Nachfrage besteht, dann kann ich nach der Fertigstellung einen Wrapper für Objektorientiertes Programmieren erstellen. Ich möchte gar nicht einmal wissen, wieviele Programmierer schon flach programmiert und erst zum Schluss einfach nur die entsprechenden Funktionsaufrufen in Klassen untergebracht haben.
Das geht nachträglich aber nur sehr bedingt. Das wird dann vermutlich auf ein Singleton hinauslaufen, und dann gewinnst quasi gar nichts gegenüber freien Funktionen und globalem State. Ein Vorteil bei Objekten wäre zum Beispiel, dass man mehr als ein Objekt erstellen kann, um mehrere Audio-Ausgabe-Geräte parallel anzusteuern.
Ich weiß aber nicht, ob hustbaer auf diesen Nachteil hinauswollte oder auf etwas anderes.
-
Christoph schrieb:
Ein Vorteil bei Objekten wäre zum Beispiel, dass man mehr als ein Objekt erstellen kann, um mehrere Audio-Ausgabe-Geräte parallel anzusteuern.
Das stimmt vollkommen. Da ich aber mit meiner Soundlib nicht in irgendeiner Form in Zeitdruck stehe, kann ich auch Zeit darauf verwenden alles erdenkliche in Strukturen zu verpacken. Nach dem öffnen eines Devices erhält man einen Pointer zu einer instanzierten Struktur zurück. Diese Instanz muss man dann auch fleißig weiter geben (z.B. bei ...playSound).
Aber vielen Dank für die beherzte Erinnerung!
Ansonsten werde ich schon darauf achten, dass ich Objekte, Strukturen oder ähnliches schon nicht falsch Initialisiere. Eventuell lade ich nach der Fertigstellung einfach alles auf GIT hoch und mache ein Eintrag auf Codeproject. Der Rest ist Sache der Community und kann aus dem Framework dann machen was es möchte:
- Plugin Funktionalität integrieren und entsprechendes SDK anbieten
- Echtes Threading (was abhängig vom System ist) einbauen
- Weitere Sound-Formate
- Codeoptimierung (verwendung von Bitshifting)
- Sound-Engine auf ein freien Prozessor-Kern laufen lassen
- Und so weiter
Das ganze geht meiner Meinung nach nun mal mit Funktional geschriebenen Quellcode am besten, wegen der einfacheren Struktur. Und eventuell auch wegen der einfacheren Verwaltung.
Den Quellcode würde ich selbstverständlich dann auch für das Gemeinschaftsprojekt OS-Development PrettyOS zur Verfügung stellen. Dann könnte man nämlich mal eine richtige Applikation zusammenbauen (WAV Player).
-
ShadowTurtle schrieb:
(...) alles erdenkliche in Strukturen zu verpacken. Nach dem öffnen eines Devices erhält man einen Pointer zu einer instanzierten Struktur zurück. Diese Instanz muss man dann auch fleißig weiter geben (z.B. bei ...playSound).
Wenn du das überall durchziehst, so dass es überhaupt keine globalen Variablen mehr gibt, dann ist es OK.
Also auch das Enumerieren von Sound-Devices etc. sollte keine globalen Variablen verwenden. Wenn doch, dann sicher' es mit Mutexen ab. Sonst bekommt man Probleme wenn man z.B. in zwei Threads gleichzeitig die Sound-Devices enumerieren will, weil sich deine Library dann die internen globalen Datenstrukturen zerschiesst.
-
Soooo. Da habe ich mir also etwas vorgenommen, als ich nicht genau wusste, dass waveOutOpen so ziemlich eingescrhänkt ist (ein Stream pro Prozess). Deshalb erstelle ich für jeden Stream aus dem Speicher ein neuer Prozess welches ausgeführt wird und fertig.
Ne quatsch, dass wäre schließlich zu einfach und konnte sicherlich nicht schon früher zu DOS-Zeiten angewandt werden.
Jetzt habe ich also ein Prozess (das Programm oder das Spiel) und einen einzigen Ausgabe-Kanal (mit waveOutOpen) je Device.
Während der Laufzeit wird die Sound-Datei als Sound-Objekt in den Speicher geladen.
Wird ...playSound aufgerufen, dann wird eine neue Instanz geschaffen für einen Sound-Player geschaffen. Der Sound-Player sammelt die benötigten WAV-Daten zum abspielen via streaming (Datenmaterial wird vom Sound-Objekt geklaut).Dann gibt es die andere Abteilung: Ein WAV-Objekt sorgt dafür, dass Datenmaterial für z.B. 1 Sek. gesammelt und an den Main-WAV-Sound-Streamer gesendet wird. Jede Sekunde wird im sogenannten SoundFrame alle abzuspielenden WAV-Objekte übereinander gelegt und zum abspielen gebracht.
Kein Problem. Was ist aber, wenn etwa nach 10.5 Sekunden plötzlich die Lautstärke abgeändert werden soll? Im Buffer wäre dann noch die berechneten Daten der anderen 0.5 Sekunden gespeichert. Dies könnte zu einen Problem führen.
Ich habe folgenden Lösungsansatz: Restlichen Buffer löschen und eine Neuanforderung der Daten senden. Genau ab hier macht das so genannte Quad-Buffering Sinn, sollte wirklich mal exzessives Threading eingebaut werden wollen.
Gibt es für das spezielle Problem eventuell bessere Lösungen?