Tutorial #04: Audio

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 :

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 der Timeline-Knoten von den passenden AudioSequence 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"));
tut0104_audio.png
Audio Test


Copyright © 2011-2024 Spraylight GmbH.