Dieses Beispiel zeigt wie Audiodateien geladen und abgespielt werden können.
Version 1: Audio Source, Audio Sequence, Timeline, Listener
Audioformate
Um beim Abspielen von Audiodaten auch Positionen berücksichtigen zu können, werden bei der Murl Engine sowohl Audio-Quellen als auch Audio-Senken (Listener) als Graphenknoten an einem beliebigen Ort in der virtuellen Welt platziert. Dadurch ist der Sound je nach Position stärker am linken bzw. rechten Lautsprecher zu hören und wird mit der Entfernung leiser.
- Zu beachten
- Achtung: Nur Mono-Sounds werden positionsabhängig abgespielt. Stereo-Sounds werden immer mit der gleichen Lautstärke und mit der im Soundfile definierten Links-Rechts-Aufteilung abgespielt.
Als Audioformate werden .wav
- und .ogg
-Dateien mit Integer-Werten unterstützt. Für Testzwecke laden wir je eine Monodatei und eine Stereodatei von der Freesound-Datenbank https://www.freesound.org :
https://www.freesound.org/people/HerbertBoland/sounds/33637/
https://www.freesound.org/people/THE_bizniss/sounds/39458/
Wir benennen die Dateien um und speichern sie im Unterverzeichnis sounds
im Ressourcen-Ordner des Projekts:
data/packages/main.murlres/sounds/sfx_boom_stereo.wav
data/packages/main.murlres/sounds/sfx_laser_mono.wav
In der Datei package.xml
werden die Dateien mit eindeutigen id
-Attributen bekannt gemacht:
<?xml version="1.0" ?> <Package id="package_main"> <!-- Sound resources --> <Resource id="sfx_laser" fileName="sounds/sfx_laser_mono.wav"/> <Resource id="sfx_boom" fileName="sounds/sfx_boom_stereo.wav"/> <!-- Graph resources --> <Resource id="graph_main" fileName="graph_main.xml"/> <!-- Graph instances --> <Instance graphResourceId="graph_main"/> </Package>
Audio
Um eine Audiodatei abspielen zu können, sind folgende Schritte notwendig:
- Audio-Ressource im Graphen verfügbar machen (
Graph::AudioSource
-Knoten). - Eine Audioquelle in der virtuellen Welt positionieren (
Graph::AudioSequence
-Knoten). - Einen zeitlichen Kontext erstellen um die Audioquelle steuern zu können (
Graph::Timeline
-Knoten). - Eine Audio-Senke (Zuhörer) in der virtuellen Welt positionieren (
Graph::Listener
-Knoten).
Der Graphenknoten AudioSource
erzeugt eine Instanz der Audio-Ressource und macht diese im Graphen verfügbar:
<AudioSource id="soundBoom" audioResourceId="package_main:sfx_boom"/> <AudioSource id="soundLaser" audioResourceId="package_main:sfx_laser"/>
Der Graphenknoten AudioSequence
stellt eine Audioquelle in der virtuellen Welt dar. Mit dem Attribut audioSourceIds
wird angegeben, welche Audio-Ressource(n) von dieser Audioquelle verwendet werden soll(en).
Mit Komma getrennt können mehrere id
-Namen angegeben werden. Diese werden dann nahtlos hintereinander in der gegebenen Reihenfolge abgespielt:
<AudioSequence audioSourceIds="soundLaser,soundBoom" />
Alternativ kann auch das audioSourceId
Attribut mit explizitem Index (beginnend bei 0) verwendet werden:
<AudioSequence audioSourceId.0="soundLaser" audioSourceId.1="soundBoom" />
Das Sampleformat des Audio-Buffers wird standardmäßig von der ersten über audioSourceIds
angegebenen Sounddatei übernommen. Alternativ kann über den Parameter sampleFormat
das Format des Audio-Buffers angegeben werden. Gültige Werte für den Parameter sampleFormat
sind:
Diese String-Werte beziehen sich direkt auf die verfügbaren Enumeration-Werte in IEnums::SampleFormat
.
- Zu beachten
- Achtung: Werden mehrere Audiodateien in einer
AudioSequence
abgespielt, sollten diese dasselbe Audioformat haben. Unterschiede in der Sampleauflösung (z.B. 16 Bit/8Bit) werden automatisch angepasst, aber die Abtastrate (und damit die Abspielgeschwindigkeit bzw. Tonhöhe) wird nicht geändert.
Mit dem Attribut volume
kann die Lautstärke angepasst werden, wobei nur Werte im Bereich von 0.0 bis 1.0 Sinn machen. Der Wert für das Attribut rolloffFactor
fließt in die Berechnung der Lautstärke ein. Das genaue Berechnungsmodell kann in der Graph::IListener
-Beschreibung nachgeschlagen werden.
Wir erzeugen nun einen AudioSequence
-Knoten mit einer einzelnen abzuspielenden Audioquelle, eingebettet in einen Graph::Timeline
-Knoten:
<Timeline id="timelineBoom" startTime="0.0" endTime="7.5" autoRewind="yes" > <AudioSequence id="sequenceBoom" audioSourceIds="soundBoom" volume="1.0" rolloffFactor="0.0" posX="0" posY="0" posZ="800" /> </Timeline>
Mit dem Graphenknoten Timeline
wird ein zeitlicher Kontext hergestellt. Im Programm verwenden wir diesen Knoten um die Audiodatei abzuspielen. Die Attribute startTime
und endTime
definieren den gewünschten Zeitausschnitt der Audiodatei, welcher abgespielt werden soll. Die Angabe erfolgt in Sekunden. Mit einer negativen startTime
kann auch ein verzögertes Abspielen erreicht werden.
Um eine Audio-Senke zu erzeugen, sind ähnlich wie bei der Kamera ein Listener
, ein ListenerTransform
und ein ListenerState
Knoten notwendig:
<Listener id="listener" viewId="view" /> <ListenerTransform listenerId="listener" posX="0" posY="0" posZ="800" /> <ListenerState listenerId="listener" />
Der Graphenknoten Listener
erzeugt einen Zuhörer für einen bestimmten View. Wir definieren nur das Attribut viewId
und verwenden das Standard-Distanzmodell INVERSE_CLAMPED
.
Mit dem ListenerTransform
-Knoten positionieren wir den Listener in der virtuellen Welt und mit dem ListenerState
-Knoten aktivieren wir den Listener für alle nachfolgenden Audioquellen.
Anstelle des Standard-Distanzmodells INVERSE_CLAMPED
kann mit dem Attribut distanceModel
eines der folgenden Distanzmodelle ausgewählt werden:
Ähnlich wie beim oben beschriebenen Sampleformat beziehen sich diese String-Werte ebenfalls auf die verfügbaren Enumeration-Werte in IEnums::DistanceModel
. Die genaue Formel zur Berechnung der Lautstärke findet sich wiederum in der Beschreibung des Graph::IListener
-Interfaces.
In der Datei graph_main.xml
fassen wir alle Knoten zusammen:
<?xml version="1.0" ?> <Graph> <View id="view"/> <Camera id="camera" fieldOfViewX="400" viewId="view" nearPlane="400" farPlane="2500" clearColorBuffer="yes" /> <CameraTransform cameraId="camera" posX="0" posY="0" posZ="800" /> <CameraState cameraId="camera" /> <Listener id="listener" viewId="view" /> <ListenerTransform listenerId="listener" posX="0" posY="0" posZ="800" /> <ListenerState listenerId="listener" /> <AudioSource id="soundBoom" audioResourceId="package_main:sfx_boom"/> <AudioSource id="soundLaser" audioResourceId="package_main:sfx_laser"/> <Timeline id="timelineBoom" startTime="0.0" endTime="7.5" autoRewind="yes" > <AudioSequence id="sequenceBoom" audioSourceIds="soundBoom" volume="1.0" rolloffFactor="0.0" posX="0" posY="0" posZ="800" /> </Timeline> <Timeline id="timelineLaser" startTime="0.0" endTime="0.4" autoRewind="yes" > <AudioSequence id="sequenceLaser" audioSourceIds="soundLaser" volume="1.0" rolloffFactor="1.0" posX="0" posY="0" posZ="800" /> </Timeline> </Graph>
Timeline-Knoten
Um den Sound abspielen zu können, erstellen wir für jeden Timeline
-Graphenknoten ein zugehöriges Logic::TimelineNode
-Objekt:
Logic::TimelineNode mSFXBoom; Logic::TimelineNode mSFXLaser;
Bool App::SoundLogic::OnInit(const Logic::IState* state) { state->GetLoader()->UnloadPackage("startup"); Graph::IRoot* root = state->GetGraphRoot(); AddGraphNode(mSFXBoom.GetReference(root, "timelineBoom")); AddGraphNode(mSFXLaser.GetReference(root, "timelineLaser")); if (!AreGraphNodesValid()) { return false; } state->SetUserDebugMessage("Press left/right Mouse Button to play Sound"); return true; }
In der Methode OnProcessTick()
verwenden wir die Logic::TimelineNode
-Objekte, um das Abspielen zu steuern. Beim Drücken der rechten bzw. der linken Maustaste wird die Timeline zuerst auf Anfang gesetzt und dann das Abspielen gestartet.
void App::SoundLogic::OnProcessTick(const Logic::IState* state) { Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler(); if (deviceHandler->WasMouseButtonPressed(IEnums::MOUSE_BUTTON_LEFT)) { mSFXBoom->Rewind(); mSFXBoom->Start(); } if (deviceHandler->WasMouseButtonPressed(IEnums::MOUSE_BUTTON_RIGHT)) { mSFXLaser->Rewind(); mSFXLaser->Start(); } if (deviceHandler->WasRawKeyPressed(RAWKEY_ESCAPE) || deviceHandler->WasRawButtonPressed(RAWBUTTON_BACK)) { deviceHandler->TerminateApp(); } }
Wenn die Gesamtlänge der Sounddateien unbekannt ist, kann sie einfach über die Methode GetTotalDuration()
vom AudioSequence
-Knoten abgefragt werden. Der neue Start- und Endwert kann einfach der Methode Start()
vom Timeline
-Knoten übergeben werden. Bei Aufruf dieser Methode mit neuem Start/Endwert wird automatisch auch ein Rewind()
aufgerufen.
mTimelineNode->Start(0, mAudioSequenceNode->GetTotalDuration());
Übungen
- Variiere die Parameter für Position, Volume und Rolloff-Faktor.
- Ändere die
OnProcessTick()
-Methode, sodass die Stop-Zeiten derTimeline
-Knoten von den passendenAudioSequence
Knoten gelesen werden.
Version 2: Strukturieren
Um bei größeren Projekten noch die Übersicht zu behalten, ist eine Aufteilung in einzelne Dateien sinnvoll. Dafür erstellen wir die beiden Dateien graph_sound_instance.xml
und graph_sounds.xml
und machen diese in der Datei package.xml
mit einer eindeutigen id
bekannt:
<?xml version="1.0" ?> <Package id="package_main"> <!-- Sound resources --> <Resource id="sfx_laser" fileName="sounds/sfx_laser_mono.wav"/> <Resource id="sfx_boom" fileName="sounds/sfx_boom_stereo.wav"/> <!-- Graph resources --> <Resource id="graph_main" fileName="graph_main.xml"/> <Resource id="graph_sound_instance" fileName="graph_sound_instance.xml"/> <Resource id="graph_sounds" fileName="graph_sounds.xml"/> <!-- Graph instances --> <Instance graphResourceId="graph_main"/> </Package>
In der Datei graph_sound_instance.xml
kapseln wir die Knoten Timeline
, AudioSource
und AudioSequence
. Um Namenskonflikte zu vermeiden, verpacken wir alle Knoten in einen Namespace
mit dem benutzerdefinierten Attribut audioId
und verwenden zusätzlich die benutzerdefinierten Attribute duration
und packageId:
<?xml version="1.0" ?> <Graph duration="1.0" packageId="package_main"> <Namespace id="{audioId}" > <AudioSource id="sound" audioResourceId="{packageId}:{audioId}"/> <Timeline id="timeline" startTime="0.0" endTime="{duration}" autoRewind="yes"> <AudioSequence id="sequence" audioSourceIds="sound" volume="1.0" rolloffFactor="0.0"/> </Timeline> </Namespace> </Graph>
In der Datei graph_sounds.xml
instanzieren wir die einzelnen Sounddateien unter Verwendung des in der Datei graph_sound_instance.xml
definierten Teilgraphen:
<?xml version="1.0" ?> <Graph> <Namespace id="sounds"> <Instance graphResourceId="package_main:graph_sound_instance" audioId="sfx_boom"/> <Instance graphResourceId="package_main:graph_sound_instance" audioId="sfx_laser"/> </Namespace> </Graph>
Diese Datei kann dann einfach in der Datei graph_main.xml
instanziert und damit dem Graphen hinzugefügt werden:
<?xml version="1.0" ?> <Graph> <View id="view"/> <Listener id="listener" viewId="view"/> <ListenerState listenerId="listener"/> <Instance graphResourceId="package_main:graph_sounds"/> <Camera id="camera" viewId="view" fieldOfViewX="400" clearColorBuffer="yes" /> <CameraState cameraId="camera"/> </Graph>
Die Timeline kann dann einfach über den Namespace-Pfad referenziert werden.
AddGraphNode(mSFXBoom.GetReference(root, "sounds/sfx_boom/timeline")); AddGraphNode(mSFXLaser.GetReference(root, "sounds/sfx_laser/timeline"));