//----------------------------------------------------------------------------- // Copyright (c) 2012 GarageGames, LLC // // 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. //----------------------------------------------------------------------------- #include "sfx/sfxController.h" #include "sfx/sfxPlayList.h" #include "sfx/sfxProfile.h" #include "sfx/sfxSource.h" #include "sfx/sfxSystem.h" #include "sfx/sfxState.h" #include "sfx/sfxDescription.h" #include "console/engineAPI.h" #include "math/mRandom.h" IMPLEMENT_CONOBJECT( SFXController ); ConsoleDocClass( SFXController, "@brief A sound source that drives multi-source playback.\n\n" "This class acts as an interpreter for SFXPlayLists. It goes through the slots of the playlist it is " "attached to and performs the actions described by each of the slots in turn.\n" "As SFXControllers are created implicitly by the SFX system when instantiating a source for a play list it is " "in most cases not necessary to directly deal with the class.\n" "The following example demonstrates how a controller would commonly be created.\n" "@tsexample\n" "// Create a play list from two SFXProfiles.\n" "%playList = new SFXPlayList()\n" "{\n" " // Use a looped description so the list playback will loop.\n" " description = AudioMusicLoop2D;\n" "\n" " track[ 0 ] = Profile1;\n" " track[ 1 ] = Profile2;\n" "};\n" "\n" "// Play the list. This will implicitly create a controller.\n" "sfxPlayOnce( %playList );\n" "@endtsexample\n\n" "@note Play lists are updated at regular intervals by the sound system. This processing determines the granularity at " "which playlist action timing takes place.\n" "@note This class cannot be instantiated directly. Use sfxPlayOnce() or sfxCreateSource() with the playlist " "you want to play to create an instance of this class.\n" "@see SFXPlayList\n" "@ingroup SFX\n" ); //----------------------------------------------------------------------------- SFXController::SFXController( SFXPlayList* playList ) : Parent( playList ), mTrace( playList->trace() ) { VECTOR_SET_ASSOCIATION( mInsns ); VECTOR_SET_ASSOCIATION( mSources ); VECTOR_SET_ASSOCIATION( mParameters ); _compileList( playList ); } //----------------------------------------------------------------------------- SFXController::~SFXController() { } //----------------------------------------------------------------------------- void SFXController::initPersistFields() { addGroup( "Debug" ); addField( "trace", TypeBool, Offset( mTrace, SFXController ), "If true, the controller logs its operation to the console.\n" "This is a non-networked field that will work locally only." ); endGroup( "Debug" ); Parent::initPersistFields(); } //----------------------------------------------------------------------------- SFXController* SFXController::_create( SFXPlayList* playList ) { AssertFatal( playList != NULL, "SFXController::_create() - got a NULL playlist!" ); SFXController* controller = new SFXController( playList ); controller->registerObject(); return controller; } //----------------------------------------------------------------------------- void SFXController::_compileList( SFXPlayList* playList ) { mInsns.clear(); const bool isLooping = playList->getDescription()->mIsLooping; // Create a slot list that determines the order the slots will be // played in. U32 slotList[ SFXPlayList::NUM_SLOTS ]; bool isOrderedRandom = false; switch( playList->getRandomMode() ) { case SFXPlayList::RANDOM_OrderedRandom: isOrderedRandom = true; /* fallthrough */ case SFXPlayList::RANDOM_NotRandom: // Generate sequence 1-NUM_SLOTS. for( U32 i = 0; i < SFXPlayList::NUM_SLOTS; ++ i ) slotList[ i ] = i; if( isOrderedRandom ) { // Randomly exchange slots in the list. for( U32 i = 0; i < SFXPlayList::NUM_SLOTS; ++ i ) swap( slotList[ gRandGen.randI( 0, SFXPlayList::NUM_SLOTS - 1 ) ], slotList[ i ] ); } break; case SFXPlayList::RANDOM_StrictRandom: // Randomly generate NUM_SLOTS slot indices. for( U32 i = 0; i < SFXPlayList::NUM_SLOTS; ++ i ) slotList[ i ] = gRandGen.randI( 0, SFXPlayList::NUM_SLOTS - 1 ); break; } // Generate the instruction list. U32 slotCount = 0; for( U32 i = 0; i < SFXPlayList::NUM_SLOTS; ++ i ) { const U32 slotIndex = slotList[ i ]; const U32 slotStartIp = mInsns.size(); SFXState* state = playList->getSlots().mState[ slotIndex ]; // If there's no track in this slot, ignore it. if( !playList->getSlots().mTrack[ slotIndex ] ) continue; // If this is a looped slot and the list is not set to loop // indefinitly on single slots, start a loop. S32 loopStartIp = -1; if( playList->getSlots().mRepeatCount[ slotIndex ] > 0 && ( !isLooping || playList->getLoopMode() != SFXPlayList::LOOP_Single ) ) { Insn insn( OP_LoopBegin, slotIndex, state ); insn.mArg.mLoopCount = playList->getSlots().mRepeatCount[ slotIndex ]; mInsns.push_back( insn ); loopStartIp = mInsns.size(); } // Add in-delay, if any. if( playList->getSlots().mDelayTimeIn.mValue[ slotIndex ] > 0.0f ) { Insn insn( OP_Delay, slotIndex, state ); insn.mArg.mDelayTime.mValue[ 0 ] = playList->getSlots().mDelayTimeIn.mValue[ slotIndex ]; insn.mArg.mDelayTime.mVariance[ 0 ][ 0 ] = playList->getSlots().mDelayTimeIn.mVariance[ slotIndex ][ 0 ]; insn.mArg.mDelayTime.mVariance[ 0 ][ 1 ] = playList->getSlots().mDelayTimeIn.mVariance[ slotIndex ][ 1 ]; mInsns.push_back( insn ); } // Add the in-transition. const SFXPlayList::ETransitionMode transitionIn = playList->getSlots().mTransitionIn[ slotIndex ]; if( transitionIn != SFXPlayList::TRANSITION_None ) { Insn insn( slotIndex, state ); _genTransition( insn, transitionIn ); mInsns.push_back( insn ); } // Add the play instruction. { Insn insn( OP_Play, slotIndex, state ); mInsns.push_back( insn ); } // Add out-delay, if any. if( playList->getSlots().mDelayTimeOut.mValue[ slotIndex ] > 0.0f ) { Insn insn( OP_Delay, slotIndex, state ); insn.mArg.mDelayTime.mValue[ 0 ] = playList->getSlots().mDelayTimeOut.mValue[ slotIndex ]; insn.mArg.mDelayTime.mVariance[ 0 ][ 0 ] = playList->getSlots().mDelayTimeOut.mVariance[ slotIndex ][ 0 ]; insn.mArg.mDelayTime.mVariance[ 0 ][ 1 ] = playList->getSlots().mDelayTimeOut.mVariance[ slotIndex ][ 1 ]; mInsns.push_back( insn ); } // Add the out-transition. const SFXPlayList::ETransitionMode transitionOut = playList->getSlots().mTransitionOut[ slotIndex ]; if( transitionOut != SFXPlayList::TRANSITION_None ) { Insn insn( slotIndex, state ); _genTransition( insn, transitionOut ); mInsns.push_back( insn ); } // Loop, if necessary. if( loopStartIp != -1 ) { Insn insn( OP_LoopEnd, slotIndex, state ); insn.mArg.mJumpIp = loopStartIp; mInsns.push_back( insn ); } // If the list is on repeat-single, unconditionally // loop over the instruction sequence of each slot. if( isLooping && playList->getLoopMode() == SFXPlayList::LOOP_Single ) { Insn insn( OP_Jump, slotIndex, state ); insn.mArg.mJumpIp = slotStartIp; mInsns.push_back( insn ); } // If we have reached the limit of slots to play, // stop generating. slotCount ++; if( playList->getNumSlotsToPlay() == slotCount ) break; } // Set up for execution. mIp = 0; if( !mInsns.empty() ) _initInsn(); } //----------------------------------------------------------------------------- void SFXController::_genTransition( Insn& insn, SFXPlayList::ETransitionMode transition ) { switch( transition ) { case SFXPlayList::TRANSITION_Wait: insn.mOpcode = OP_WaitSingle; break; case SFXPlayList::TRANSITION_WaitAll: insn.mOpcode = OP_WaitAll; break; case SFXPlayList::TRANSITION_Stop: insn.mOpcode = OP_StopSingle; break; case SFXPlayList::TRANSITION_StopAll: insn.mOpcode = OP_StopAll; break; default: AssertFatal( false, "SFXController::_addTransition() - should not reach here" ); } } //----------------------------------------------------------------------------- void SFXController::_initInsn() { Insn& insn = mInsns[ mIp ]; switch( insn.mOpcode ) { case OP_Delay: mDelayEndTime = Platform::getVirtualMilliseconds() + U32( insn.mArg.mDelayTime.getValue( 0, 0.0f ) * 1000.f ); break; default: break; } if( mTrace ) _printInsn(insn ); } //----------------------------------------------------------------------------- void SFXController::_printInsn( Insn& insn) { switch( insn.mOpcode ) { case OP_Delay: Con::printf( "[SFXController] ip=%d: slot=%d: state=%s: Delay %f:%f:%f", mIp, insn.mSlotIndex, insn.mState ? insn.mState->getName() : "", insn.mArg.mDelayTime.mValue[ 0 ], insn.mArg.mDelayTime.mVariance[ 0 ], insn.mArg.mDelayTime.mVariance[ 1 ] ); break; case OP_WaitSingle: Con::printf( "[SFXController] ip=%d: slot=%d: state=%s: WaitSingle", mIp, insn.mSlotIndex, insn.mState ? insn.mState->getName() : "" ); break; case OP_WaitAll: Con::printf( "[SFXController] ip=%d: slot=%d: state=%s: WaitAll", mIp, insn.mSlotIndex, insn.mState ? insn.mState->getName() : "" ); break; case OP_StopSingle: Con::printf( "[SFXController] ip=%d: slot=%d: state=%s: StopSingle", mIp, insn.mSlotIndex, insn.mState ? insn.mState->getName() : "" ); break; case OP_StopAll: Con::printf( "[SFXController] ip=%d: slot=%d: state=%s: StopAll", mIp, insn.mSlotIndex, insn.mState ? insn.mState->getName() : "" ); break; case OP_Play: Con::printf( "[SFXController] ip=%d: slot=%d: state=%s: Play", mIp, insn.mSlotIndex, insn.mState ? insn.mState->getName() : "" ); break; case OP_Jump: Con::printf( "[SFXController] ip=%d: slot=%d: state=%s: Jump %i", mIp, insn.mSlotIndex, insn.mState ? insn.mState->getName() : "", insn.mArg.mJumpIp ); break; case OP_LoopBegin: Con::printf( "[SFXController] ip=%d: slot=%d: state=%s: LoopBegin %i", mIp, insn.mSlotIndex, insn.mState ? insn.mState->getName() : "", insn.mArg.mLoopCount ); break; case OP_LoopEnd: Con::printf( "[SFXController] ip=%d: slot=%d: state=%s: LoopEnd", mIp, insn.mSlotIndex, insn.mState ? insn.mState->getName() : "" ); break; } } //----------------------------------------------------------------------------- bool SFXController::_execInsn() { bool endUpdate = false; Insn& insn = mInsns[ mIp ]; switch( insn.mOpcode ) { case OP_Delay: { if( Platform::getVirtualMilliseconds() < mDelayEndTime ) endUpdate = true; else _advanceIp(); break; } case OP_Play: { SFXPlayList* playList = getPlayList(); SFXTrack* track = playList->getSlots().mTrack[ insn.mSlotIndex ]; // Handle existing sources playing on this slot and find // whether we need to start a new source. // // Go through the list top-down so we can push sources we re-use // to the top of the list. A side-effect of doing it this way is // that the order of the sources that are preserved gets reversed, // i.e. older sources will end up higher up the stack. bool startNew = true; SFXPlayList::EReplayMode replayMode = playList->getSlots().mReplayMode[ insn.mSlotIndex ]; if( replayMode != SFXPlayList::REPLAY_IgnorePlaying ) for( S32 i = mSources.size() - 1; i >= 0; -- i ) { Source& source = mSources[ i ]; if( source.mSlotIndex != insn.mSlotIndex ) continue; // If the play-once source has expired, remove the entry // and go on. if( source.mPtr == NULL ) { mSources.erase( i ); ++ i; continue; } // Decide what to do with the still-playing source. if( replayMode == SFXPlayList::REPLAY_RestartPlaying || replayMode == SFXPlayList::REPLAY_KeepPlaying ) { // Restart the source or keep playing it. // Either way, move it to the top of the stack. startNew = false; Source src = mSources[ i ]; mSources.erase( i ); //RDTODO: add a method to restart cleanly in the presence of fades; this here // just cuts the current playback short if( replayMode == SFXPlayList::REPLAY_RestartPlaying ) src.mPtr->stop( 0.f ); src.mPtr->play(); // Move the source to the top of the stack. mSources.increment(); mSources.last() = src; } else if( replayMode == SFXPlayList::REPLAY_StartNew ) { // Kill off existing source. source.mPtr->stop(); mSources.erase( i ); ++ i; } else if( replayMode == SFXPlayList::REPLAY_SkipIfPlaying ) { startNew = false; break; } } if( startNew ) { // Create a new source. SFXSource* source = SFX->createSource( track, &getTransform(), &getVelocity() ); // Append the source to the list of playing sources. if( source ) { mSources.increment(); Source& src = mSources.last(); // Determine fade times. F32 fadeInTime = -1; F32 fadeOutTime = -1; if( playList->getSlots().mFadeTimeIn.mValue[ insn.mSlotIndex ] != -1 ) fadeInTime = playList->getSlots().mFadeTimeIn.getValue( insn.mSlotIndex, 0.f ); if( playList->getSlots().mFadeTimeOut.mValue[ insn.mSlotIndex ] != -1 ) fadeOutTime = playList->getSlots().mFadeTimeOut.getValue( insn.mSlotIndex, 0.f ); if( fadeInTime != -1 || fadeOutTime != -1 ) source->setFadeTimes( fadeInTime, fadeOutTime ); // Set up source record. src.mPtr = source; src.mSlotIndex = insn.mSlotIndex; src.mVolumeScale = playList->getSlots().mVolumeScale.getValue( insn.mSlotIndex, 0.f, 1.f ); src.mPitchScale = playList->getSlots().mPitchScale.getValue( insn.mSlotIndex ); src.mFadeInTime = fadeInTime; src.mFadeOutTime = fadeOutTime; SFXPlayList::EStateMode stateMode = playList->getSlots().mStateMode[ insn.mSlotIndex ]; if( stateMode != SFXPlayList::STATE_IgnoreInactive ) src.mState = insn.mState; // Set the source's volume and pitch. Either is scaled by our own // assigned value and the scale factors from the playlist slot. source->setModulativeVolume( mAttenuatedVolume * src.mVolumeScale ); source->setModulativePitch( mEffectivePitch * src.mPitchScale ); // Set min and max range. const SFXPlayList::VariantFloat& minDistance = playList->getSlots().mMinDistance; const SFXPlayList::VariantFloat& maxDistance = playList->getSlots().mMaxDistance; if( minDistance.mValue[ insn.mSlotIndex ] >= 0.f && maxDistance.mValue[ insn.mSlotIndex ] >= 0.f ) source->setMinMaxDistance( minDistance.getValue( insn.mSlotIndex, 0.f ), maxDistance.getValue( insn.mSlotIndex, 0.f ) ); // Start the source. source->play(); SFX->deleteWhenStopped( source ); } } _advanceIp(); break; } case OP_WaitSingle: { if( !mSources.empty() && mSources.last().mPtr != NULL && mSources.last().mPtr->isPlaying() ) endUpdate = true; else { if( !mSources.empty() ) mSources.decrement(); _advanceIp(); } break; } case OP_WaitAll: { for( U32 i = 0; i < mSources.size(); ++ i ) if( mSources[ i ].mPtr != NULL && mSources[ i ].mPtr->isStopped() ) { mSources.erase( i ); -- i; } if( !mSources.empty() ) endUpdate = true; else _advanceIp(); break; } case OP_StopSingle: { if( !mSources.empty() ) { if( mSources.last().mPtr != NULL ) mSources.last().mPtr->stop(); mSources.decrement(); } _advanceIp(); break; } case OP_StopAll: { while( !mSources.empty() ) { if( mSources.last().mPtr != NULL ) mSources.last().mPtr->stop(); mSources.decrement(); } _advanceIp(); break; } case OP_Jump: { mIp = insn.mArg.mJumpIp; _initInsn(); break; } case OP_LoopBegin: { mLoopCounter = insn.mArg.mLoopCount; _advanceIp(); break; } case OP_LoopEnd: { -- mLoopCounter; if( mLoopCounter > 0 ) { mIp = insn.mArg.mJumpIp; _initInsn(); } else _advanceIp(); break; } } return endUpdate; } //----------------------------------------------------------------------------- void SFXController::_advanceIp() { mIp ++; if( mIp < mInsns.size() ) _initInsn(); } //----------------------------------------------------------------------------- void SFXController::_onParameterEvent( SFXParameter* parameter, SFXParameterEvent event ) { Parent::_onParameterEvent( parameter, event ); // Implement cursor semantic. if( event == SFXParameterEvent_ValueChanged && parameter->getChannel() == SFXChannelCursor ) { U32 slot = U32( mFloor( parameter->getValue() ) ); if( slot != getCurrentSlot() ) setCurrentSlot( slot ); } } //----------------------------------------------------------------------------- SFXPlayList* SFXController::getPlayList() const { return static_cast< SFXPlayList* >( mTrack.getPointer() ); } //----------------------------------------------------------------------------- U32 SFXController::getCurrentSlot() const { if( mIp >= mInsns.size() ) return 0; else return mInsns[ mIp ].mSlotIndex; } //----------------------------------------------------------------------------- void SFXController::setCurrentSlot( U32 index ) { mIp = 0; while( mIp < mInsns.size() && mInsns[ mIp ].mSlotIndex != index ) ++ mIp; if( mIp >= mInsns.size() ) mIp = 0; if( !mInsns.empty() ) _initInsn(); } //----------------------------------------------------------------------------- void SFXController::_play() { Parent::_play(); // Unpause sources, if we are paused. if( mStatus == SFXStatusPaused ) { for( U32 i = 0; i < mSources.size(); ++ i ) if( mSources[ i ].mPtr != NULL ) mSources[ i ].mPtr->play( 0.f ); // We want our fade values to take effect. else { mSources.erase( i ); -- i; } } } //----------------------------------------------------------------------------- void SFXController::_pause() { Parent::_pause(); // Pause all playing sources. for( U32 i = 0; i < mSources.size(); ++ i ) if( mSources[ i ].mPtr != NULL ) mSources[ i ].mPtr->pause( 0.f ); // We want our fade values to take effect. else { mSources.erase( i ); -- i; } } //----------------------------------------------------------------------------- void SFXController::_stop() { Parent::_stop(); // Stop all playing sources. while( !mSources.empty() ) { if( mSources.last().mPtr != NULL ) mSources.last().mPtr->stop( 0.f ); // We want our fade values to take effect. mSources.decrement(); } // Reset execution. mIp = 0; if( !mInsns.empty() ) _initInsn(); } //----------------------------------------------------------------------------- void SFXController::_updateVolume( const MatrixF& listener ) { F32 oldAttenuatedVolume = mAttenuatedVolume; Parent::_updateVolume( listener ); // If the attenuated volume has changed, pass it off // as the modulative volume to all our sources. if( oldAttenuatedVolume != mAttenuatedVolume ) for( U32 i = 0; i < mSources.size(); ++ i ) { Source& source = mSources[ i ]; if( source.mPtr != NULL ) source.mPtr->setModulativeVolume( mAttenuatedVolume * source.mVolumeScale ); else { mSources.erase( i ); -- i; } } } //----------------------------------------------------------------------------- void SFXController::_updatePitch() { F32 oldEffectivePitch = mEffectivePitch; Parent::_updatePitch(); if( mEffectivePitch != oldEffectivePitch ) for( U32 i = 0; i < mSources.size(); ++ i ) { Source& source = mSources[ i ]; if( source.mPtr != NULL ) source.mPtr->setModulativePitch( mEffectivePitch * source.mPitchScale ); else { mSources.erase( i ); -- i; } } } //----------------------------------------------------------------------------- void SFXController::_updatePriority() { F32 oldEffectivePriority = mEffectivePriority; Parent::_updatePriority(); if( mEffectivePriority != oldEffectivePriority ) for( U32 i = 0; i < mSources.size(); ++ i ) { Source& source = mSources[ i ]; if( source.mPtr != NULL ) source.mPtr->setModulativePriority( mEffectivePriority ); else { mSources.erase( i ); -- i; } } } //----------------------------------------------------------------------------- void SFXController::_update() { Parent::_update(); SFXPlayList* playList = getPlayList(); // Check all sources against the current state setup and // take appropriate actions. for( U32 i = 0; i < mSources.size(); ++ i ) { Source& source = mSources[ i ]; // If the source has already stopped playing, // remove it. if( !source.mPtr ) { mSources.erase( i ); -- i; continue; } if( !source.mState ) continue; SFXPlayList::EStateMode stateMode = playList->getSlots().mStateMode[ mSources[ i ].mSlotIndex ]; if( !source.mState->isActive() ) { if( source.mPtr->isPlaying() ) { // The source is playing in an incompatible state. if( stateMode == SFXPlayList::STATE_PauseInactive ) source.mPtr->pause(); else if( stateMode == SFXPlayList::STATE_StopInactive ) { source.mPtr->stop(); mSources.erase( i ); -- i; } } } else { // Unpause a source that had its state become active again. if( source.mPtr->isPaused() && stateMode == SFXPlayList::STATE_PauseInactive ) source.mPtr->play(); } } // Update interpreter. bool endUpdate = false; while( !endUpdate ) { if( mIp >= mInsns.size() ) { // End of list reached. if( playList->getDescription()->mIsLooping && playList->getLoopMode() == SFXPlayList::LOOP_All ) { // The play list is set to repeat-all. // If it is also random, generate a new instruction list // so we get a new playing order. Otherwise just reset. if( playList->getRandomMode() != SFXPlayList::RANDOM_NotRandom ) _compileList( playList ); else { mIp = 0; if( !mInsns.empty() ) _initInsn(); } // Reset play timer. mPlayTimer.reset(); mPlayTimer.start(); } else { // Moved to stopped state. mPlayTimer.stop(); _setStatus( SFXStatusStopped ); mIp = 0; } // End this update. This limits playlist to at most one complete // cycle per update. break; } Insn& insn = mInsns[ mIp ]; if( insn.mState && !insn.mState->isActive() ) { // The state associated with the slot is inactive. Skip // the instructions. _advanceIp(); } else endUpdate = _execInsn(); } } //============================================================================= // Console Methods. //============================================================================= // MARK: ---- Console Methods ---- //----------------------------------------------------------------------------- DefineEngineMethod( SFXController, getCurrentSlot, S32, (),, "Get the index of the playlist slot currently processed by the controller.\n" "@return The slot index currently being played.\n" "@see SFXPlayList" ) { return object->getCurrentSlot(); } //----------------------------------------------------------------------------- DefineEngineMethod( SFXController, setCurrentSlot, void, ( S32 index ),, "Set the index of the playlist slot to play by the controller. This can be used to seek in the playlist.\n" "@param index Index of the playlist slot." ) { object->setCurrentSlot( index ); }