Browse Source

Added event for sound playback finished. Deprecate SoundSource::SetAutoRemove() and change samples + NinjaSnowWar to not use it. Fix network replication of SoundSource to work with repeated playback on the same source.

Lasse Öörni 10 years ago
parent
commit
29e62526eb

+ 4 - 3
Docs/Reference.dox

@@ -1727,7 +1727,7 @@ For purposes of volume control, each SoundSource can be classified into a user d
 
 
 To control the category volumes, use \ref Audio::SetMasterGain "SetMasterGain()", which defines the category if it didn't already exist.
 To control the category volumes, use \ref Audio::SetMasterGain "SetMasterGain()", which defines the category if it didn't already exist.
 
 
-The SoundSource components support automatic removal from the node they belong to, once playback is finished. To use, call \ref SoundSource::SetAutoRemove "SetAutoRemove()" on them. This may be useful when a game object plays several "fire and forget" sound effects.
+Note that the Audio subsystem is always instantiated, but in headless mode the playback of sounds is simulated, taking the sound length and frequency into account. This allows basing logic on whether a specific sound is still playing or not, even in server code.
 
 
 \section Audio_Parameters Sound parameters
 \section Audio_Parameters Sound parameters
 
 
@@ -1742,14 +1742,15 @@ A standard WAV file can not tell whether it should loop, and raw audio does not
 
 
 The frequency is in Hz, and loop start and end are bytes from the start of audio data. If a loop is enabled without specifying the start and end, it is assumed to be the whole sound. Ogg Vorbis compressed sounds do not support specifying the loop range, only whether whole sound looping is enabled or disabled.
 The frequency is in Hz, and loop start and end are bytes from the start of audio data. If a loop is enabled without specifying the start and end, it is assumed to be the whole sound. Ogg Vorbis compressed sounds do not support specifying the loop range, only whether whole sound looping is enabled or disabled.
 
 
-The Audio subsystem is always instantiated, but in headless mode it is not active. In headless mode the playback of sounds is simulated, taking the sound length and frequency into account. This allows basing logic on whether a specific sound is still playing or not, even in server code.
-
 \section Audio_Stream Sound streaming
 \section Audio_Stream Sound streaming
 
 
 In addition to playing existing sound resources, sound can be generated during runtime using the SoundStream class and its subclasses. To start playback of a stream on a SoundSource, call \ref SoundSource::Play(SoundStream* stream) "Play(SoundStream* stream)".
 In addition to playing existing sound resources, sound can be generated during runtime using the SoundStream class and its subclasses. To start playback of a stream on a SoundSource, call \ref SoundSource::Play(SoundStream* stream) "Play(SoundStream* stream)".
 
 
 %Sound streaming is used internally to implement on-the-fly Ogg Vorbis decoding. It is only available in C++ code and not scripting due to its low-level nature. See the SoundSynthesis C++ sample for an example of using the BufferedSoundStream subclass, which allows the sound data to be queued for playback from the main thread.
 %Sound streaming is used internally to implement on-the-fly Ogg Vorbis decoding. It is only available in C++ code and not scripting due to its low-level nature. See the SoundSynthesis C++ sample for an example of using the BufferedSoundStream subclass, which allows the sound data to be queued for playback from the main thread.
 
 
+\section Audio_Events Audio events
+
+A sound source will send the E_SOUNDFINISHED event through its scene node when the playback of a sound has ended. This can be used for example to know when to remove a temporary node created just for playing a sound effect, or for tying game events to sound playback.
 
 
 \page Physics Physics
 \page Physics Physics
 
 

+ 15 - 2
Source/Samples/14_SoundEffects/SoundEffects.cpp

@@ -21,6 +21,7 @@
 //
 //
 
 
 #include <Urho3D/Audio/Audio.h>
 #include <Urho3D/Audio/Audio.h>
+#include <Urho3D/Audio/AudioEvents.h>
 #include <Urho3D/Audio/Sound.h>
 #include <Urho3D/Audio/Sound.h>
 #include <Urho3D/Audio/SoundSource.h>
 #include <Urho3D/Audio/SoundSource.h>
 #include <Urho3D/Engine/Engine.h>
 #include <Urho3D/Engine/Engine.h>
@@ -181,8 +182,11 @@ void SoundEffects::HandlePlaySound(StringHash eventType, VariantMap& eventData)
         soundSource->Play(sound);
         soundSource->Play(sound);
         // In case we also play music, set the sound volume below maximum so that we don't clip the output
         // In case we also play music, set the sound volume below maximum so that we don't clip the output
         soundSource->SetGain(0.75f);
         soundSource->SetGain(0.75f);
-        // Set the sound component to automatically remove its scene node from the scene when the sound is done playing
-        soundSource->SetAutoRemove(true);
+
+        // Subscribe to the "sound finished" event generated by the SoundSource for removing the node once the sound has played
+        // Note: the event is sent through the Node (similar to e.g. node physics collision and animation trigger events)
+        // to not require subscribing to the particular component
+        SubscribeToEvent(soundNode, E_SOUNDFINISHED, URHO3D_HANDLER(SoundEffects, HandleSoundFinished));
     }
     }
 }
 }
 
 
@@ -226,3 +230,12 @@ void SoundEffects::HandleMusicVolume(StringHash eventType, VariantMap& eventData
     float newVolume = eventData[P_VALUE].GetFloat();
     float newVolume = eventData[P_VALUE].GetFloat();
     GetSubsystem<Audio>()->SetMasterGain(SOUND_MUSIC, newVolume);
     GetSubsystem<Audio>()->SetMasterGain(SOUND_MUSIC, newVolume);
 }
 }
+
+void SoundEffects::HandleSoundFinished(StringHash eventType, VariantMap& eventData)
+{
+    using namespace SoundFinished;
+
+    Node* soundNode = static_cast<Node*>(eventData[P_NODE].GetPtr());
+    if (soundNode)
+        soundNode->Remove();
+}

+ 2 - 0
Source/Samples/14_SoundEffects/SoundEffects.h

@@ -80,6 +80,8 @@ private:
     void HandleSoundVolume(StringHash eventType, VariantMap& eventData);
     void HandleSoundVolume(StringHash eventType, VariantMap& eventData);
     /// Handle music volume slider change.
     /// Handle music volume slider change.
     void HandleMusicVolume(StringHash eventType, VariantMap& eventData);
     void HandleMusicVolume(StringHash eventType, VariantMap& eventData);
+    /// Handle sound effect finished.
+    void HandleSoundFinished(StringHash eventType, VariantMap& eventData);
 };
 };
 
 
 
 

+ 38 - 0
Source/Urho3D/Audio/AudioEvents.h

@@ -0,0 +1,38 @@
+//
+// Copyright (c) 2008-2015 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "../Core/Object.h"
+
+namespace Urho3D
+{
+
+/// Sound playback finished. Sent through the SoundSource's Node.
+URHO3D_EVENT(E_SOUNDFINISHED, SoundFinished)
+{
+    URHO3D_PARAM(P_NODE, Node);                     // Node pointer
+    URHO3D_PARAM(P_SOUNDSOURCE, SoundSource);       // SoundSource pointer
+    URHO3D_PARAM(P_SOUND, Sound);                   // Sound pointer
+}
+
+}

+ 46 - 1
Source/Urho3D/Audio/SoundSource.cpp

@@ -23,11 +23,15 @@
 #include "../Precompiled.h"
 #include "../Precompiled.h"
 
 
 #include "../Audio/Audio.h"
 #include "../Audio/Audio.h"
+#include "../Audio/AudioEvents.h"
 #include "../Audio/Sound.h"
 #include "../Audio/Sound.h"
 #include "../Audio/SoundSource.h"
 #include "../Audio/SoundSource.h"
 #include "../Audio/SoundStream.h"
 #include "../Audio/SoundStream.h"
 #include "../Core/Context.h"
 #include "../Core/Context.h"
+#include "../IO/Log.h"
 #include "../Resource/ResourceCache.h"
 #include "../Resource/ResourceCache.h"
+#include "../Scene/Node.h"
+#include "../Scene/ReplicationState.h"
 
 
 #include "../DebugNew.h"
 #include "../DebugNew.h"
 
 
@@ -106,6 +110,7 @@ SoundSource::SoundSource(Context* context) :
     panning_(0.0f),
     panning_(0.0f),
     autoRemoveTimer_(0.0f),
     autoRemoveTimer_(0.0f),
     autoRemove_(false),
     autoRemove_(false),
+    sendFinishedEvent_(false),
     position_(0),
     position_(0),
     fractPosition_(0),
     fractPosition_(0),
     timePosition_(0.0f),
     timePosition_(0.0f),
@@ -159,6 +164,20 @@ void SoundSource::Play(Sound* sound)
     else
     else
         PlayLockless(sound);
         PlayLockless(sound);
 
 
+    // Forget the Sound & Is Playing attribute previous values so that they will be sent again, triggering
+    // the sound correctly on network clients even after the initial playback
+    if (networkState_ && networkState_->attributes_ && networkState_->previousValues_.Size())
+    {
+        for (unsigned i = 1; i < networkState_->previousValues_.Size(); ++i)
+        {
+            // The indexing is different for SoundSource & SoundSource3D, as SoundSource3D removes two attributes,
+            // so go by attribute types
+            VariantType type = networkState_->attributes_->At(i).type_;
+            if (type == VAR_RESOURCEREF || type == VAR_BOOL)
+                networkState_->previousValues_[i] = Variant::EMPTY;
+        }
+    }
+
     MarkNetworkUpdate();
     MarkNetworkUpdate();
 }
 }
 
 
@@ -266,6 +285,9 @@ void SoundSource::SetPanning(float panning)
 
 
 void SoundSource::SetAutoRemove(bool enable)
 void SoundSource::SetAutoRemove(bool enable)
 {
 {
+    if (enable == true)
+        URHO3D_LOGWARNING("SoundSource::SetAutoRemove is deprecated. Consider using the SoundFinished event instead");
+
     autoRemove_ = enable;
     autoRemove_ = enable;
 }
 }
 
 
@@ -297,10 +319,31 @@ void SoundSource::Update(float timeStep)
     if (soundStream_ && !position_)
     if (soundStream_ && !position_)
         StopLockless();
         StopLockless();
 
 
+    bool playing = IsPlaying();
+
+    if (!playing && sendFinishedEvent_)
+    {
+        sendFinishedEvent_ = false;
+
+        // Make a weak pointer to self to check for destruction during event handling
+        WeakPtr<SoundSource> self(this);
+
+        using namespace SoundFinished;
+
+        VariantMap& eventData = context_->GetEventDataMap();
+        eventData[P_NODE] = node_;
+        eventData[P_SOUNDSOURCE] = this;
+        eventData[P_SOUND] = sound_;
+        node_->SendEvent(E_SOUNDFINISHED, eventData);
+
+        if (self.Expired())
+            return;
+    }
+
     // Check for autoremove
     // Check for autoremove
     if (autoRemove_)
     if (autoRemove_)
     {
     {
-        if (!IsPlaying())
+        if (!playing)
         {
         {
             autoRemoveTimer_ += timeStep;
             autoRemoveTimer_ += timeStep;
             if (autoRemoveTimer_ > AUTOREMOVE_DELAY)
             if (autoRemoveTimer_ > AUTOREMOVE_DELAY)
@@ -479,6 +522,7 @@ void SoundSource::PlayLockless(Sound* sound)
                 sound_ = sound;
                 sound_ = sound;
                 position_ = start;
                 position_ = start;
                 fractPosition_ = 0;
                 fractPosition_ = 0;
+                sendFinishedEvent_ = true;
                 return;
                 return;
             }
             }
         }
         }
@@ -516,6 +560,7 @@ void SoundSource::PlayLockless(SharedPtr<SoundStream> stream)
         unusedStreamSize_ = 0;
         unusedStreamSize_ = 0;
         position_ = streamBuffer_->GetStart();
         position_ = streamBuffer_->GetStart();
         fractPosition_ = 0;
         fractPosition_ = 0;
+        sendFinishedEvent_ = true;
         return;
         return;
     }
     }
 
 

+ 5 - 3
Source/Urho3D/Audio/SoundSource.h

@@ -70,8 +70,8 @@ public:
     void SetAttenuation(float attenuation);
     void SetAttenuation(float attenuation);
     /// Set stereo panning. -1.0 is full left and 1.0 is full right.
     /// Set stereo panning. -1.0 is full left and 1.0 is full right.
     void SetPanning(float panning);
     void SetPanning(float panning);
-    /// Set whether sound source will be automatically removed from the scene node when playback stops.
-    void SetAutoRemove(bool enable);
+    /// Set whether sound source will be automatically removed from the scene node when playback stops. Note: this is deprecated, consider subscribing to the SoundFinished event instead.
+    URHO3D_DEPRECATED void SetAutoRemove(bool enable);
     /// Set new playback position.
     /// Set new playback position.
     void SetPlayPosition(signed char* pos);
     void SetPlayPosition(signed char* pos);
 
 
@@ -100,7 +100,7 @@ public:
     float GetPanning() const { return panning_; }
     float GetPanning() const { return panning_; }
 
 
     /// Return autoremove mode.
     /// Return autoremove mode.
-    bool GetAutoRemove() const { return autoRemove_; }
+    URHO3D_DEPRECATED bool GetAutoRemove() const { return autoRemove_; }
 
 
     /// Return whether is playing.
     /// Return whether is playing.
     bool IsPlaying() const;
     bool IsPlaying() const;
@@ -144,6 +144,8 @@ protected:
     float masterGain_;
     float masterGain_;
     /// Autoremove flag.
     /// Autoremove flag.
     bool autoRemove_;
     bool autoRemove_;
+    /// Whether finished event should be sent on playback stop.
+    bool sendFinishedEvent_;
 
 
 private:
 private:
     /// Play a sound without locking the audio mutex. Called internally.
     /// Play a sound without locking the audio mutex. Called internally.

+ 1 - 0
Source/Urho3D/Graphics/AnimationState.cpp

@@ -301,6 +301,7 @@ void AnimationState::AddTime(float delta)
 
 
                 VariantMap& eventData = senderNode->GetEventDataMap();
                 VariantMap& eventData = senderNode->GetEventDataMap();
                 eventData[P_NODE] = senderNode;
                 eventData[P_NODE] = senderNode;
+                eventData[P_ANIMATION] = animation_;
                 eventData[P_NAME] = animation_->GetAnimationName();
                 eventData[P_NAME] = animation_->GetAnimationName();
                 eventData[P_TIME] = i->time_;
                 eventData[P_TIME] = i->time_;
                 eventData[P_DATA] = i->data_;
                 eventData[P_DATA] = i->data_;

+ 1 - 0
Source/Urho3D/Graphics/DrawableEvents.h

@@ -37,6 +37,7 @@ URHO3D_EVENT(E_BONEHIERARCHYCREATED, BoneHierarchyCreated)
 URHO3D_EVENT(E_ANIMATIONTRIGGER, AnimationTrigger)
 URHO3D_EVENT(E_ANIMATIONTRIGGER, AnimationTrigger)
 {
 {
     URHO3D_PARAM(P_NODE, Node);                    // Node pointer
     URHO3D_PARAM(P_NODE, Node);                    // Node pointer
+    URHO3D_PARAM(P_ANIMATION, Animation);          // Animation pointer
     URHO3D_PARAM(P_NAME, Name);                    // String
     URHO3D_PARAM(P_NAME, Name);                    // String
     URHO3D_PARAM(P_TIME, Time);                    // Float
     URHO3D_PARAM(P_TIME, Time);                    // Float
     URHO3D_PARAM(P_DATA, Data);                    // User-defined data type
     URHO3D_PARAM(P_DATA, Data);                    // User-defined data type

+ 1 - 1
Source/Urho3D/Scene/Scene.h

@@ -220,7 +220,7 @@ public:
     void CleanupConnection(Connection* connection);
     void CleanupConnection(Connection* connection);
     /// Mark a node for attribute check on the next network update.
     /// Mark a node for attribute check on the next network update.
     void MarkNetworkUpdate(Node* node);
     void MarkNetworkUpdate(Node* node);
-    /// Mark a comoponent for attribute check on the next network update.
+    /// Mark a component for attribute check on the next network update.
     void MarkNetworkUpdate(Component* component);
     void MarkNetworkUpdate(Component* component);
     /// Mark a node dirty in scene replication states. The node does not need to have own replication state yet.
     /// Mark a node dirty in scene replication states. The node does not need to have own replication state yet.
     void MarkReplicationDirty(Node* node);
     void MarkReplicationDirty(Node* node);

+ 10 - 2
bin/Data/LuaScripts/14_SoundEffects.lua

@@ -113,8 +113,11 @@ function HandlePlaySound(sender, eventType, eventData)
         soundSource:Play(sound)
         soundSource:Play(sound)
         -- In case we also play music, set the sound volume below maximum so that we don't clip the output
         -- In case we also play music, set the sound volume below maximum so that we don't clip the output
         soundSource.gain = 0.7
         soundSource.gain = 0.7
-        -- Set the sound component to automatically remove its scene node from the scene when the sound is done playing
-        soundSource.autoRemove = true
+
+        -- Subscribe to the "sound finished" event generated by the SoundSource for removing the node once the sound has played
+        -- Note: the event is sent through the Node (similar to e.g. node physics collision and animation trigger events)
+        -- to not require subscribing to the particular component
+        SubscribeToEvent(soundNode, "SoundFinished", "HandleSoundFinished");
     end
     end
 end
 end
 
 
@@ -152,6 +155,11 @@ function HandleMusicVolume(eventType, eventData)
     audio:SetMasterGain(SOUND_MUSIC, newVolume)
     audio:SetMasterGain(SOUND_MUSIC, newVolume)
 end
 end
 
 
+function HandleSoundFinished(eventType, eventData)
+    local soundNode = eventData["Node"]:GetPtr("Node")
+    soundNode:Remove()
+end
+
 -- Create XML patch instructions for screen joystick layout specific to this sample app
 -- Create XML patch instructions for screen joystick layout specific to this sample app
 function GetScreenJoystickPatchString()
 function GetScreenJoystickPatchString()
     return
     return

+ 11 - 2
bin/Data/Scripts/14_SoundEffects.as

@@ -120,8 +120,11 @@ void HandlePlaySound(StringHash eventType, VariantMap& eventData)
         soundSource.Play(sound);
         soundSource.Play(sound);
         // In case we also play music, set the sound volume below maximum so that we don't clip the output
         // In case we also play music, set the sound volume below maximum so that we don't clip the output
         soundSource.gain = 0.7f;
         soundSource.gain = 0.7f;
-        // Set the sound component to automatically remove its scene node from the scene when the sound is done playing
-        soundSource.autoRemove = true;
+
+        // Subscribe to the "sound finished" event generated by the SoundSource for removing the node once the sound has played
+        // Note: the event is sent through the Node (similar to e.g. node physics collision and animation trigger events)
+        // to not require subscribing to the particular component
+        SubscribeToEvent(soundNode, "SoundFinished", "HandleSoundFinished");
     }
     }
 }
 }
 
 
@@ -161,6 +164,12 @@ void HandleMusicVolume(StringHash eventType, VariantMap& eventData)
     audio.masterGain[SOUND_MUSIC] = newVolume;
     audio.masterGain[SOUND_MUSIC] = newVolume;
 }
 }
 
 
+void HandleSoundFinished(StringHash eventType, VariantMap& eventData)
+{
+    Node@ soundNode = eventData["Node"].GetPtr();
+    soundNode.Remove();
+}
+
 // Create XML patch instructions for screen joystick layout specific to this sample app
 // Create XML patch instructions for screen joystick layout specific to this sample app
 String patchInstructions =
 String patchInstructions =
         "<patch>" +
         "<patch>" +

+ 8 - 2
bin/Data/Scripts/NinjaSnowWar/GameObject.as

@@ -70,13 +70,19 @@ class GameObject : ScriptObject
 
 
     void PlaySound(const String&in soundName)
     void PlaySound(const String&in soundName)
     {
     {
-        // Create the sound channel
         SoundSource3D@ source = node.CreateComponent("SoundSource3D");
         SoundSource3D@ source = node.CreateComponent("SoundSource3D");
         Sound@ sound = cache.GetResource("Sound", soundName);
         Sound@ sound = cache.GetResource("Sound", soundName);
+        // Subscribe to sound finished for cleaning up the source
+        SubscribeToEvent(node, "SoundFinished", "HandleSoundFinished");
 
 
         source.SetDistanceAttenuation(2, 50, 1);
         source.SetDistanceAttenuation(2, 50, 1);
         source.Play(sound);
         source.Play(sound);
-        source.autoRemove = true;
+    }
+    
+    void HandleSoundFinished(StringHash eventType, VariantMap& eventData)
+    {
+        SoundSource3D@ source = eventData["SoundSource"].GetPtr();
+        source.Remove();
     }
     }
 
 
     void HandleNodeCollision(StringHash eventType, VariantMap& eventData)
     void HandleNodeCollision(StringHash eventType, VariantMap& eventData)