diff --git a/src/fheroes2/audio/audio_manager.cpp b/src/fheroes2/audio/audio_manager.cpp index 6c98640bf5d..00d071cf915 100644 --- a/src/fheroes2/audio/audio_manager.cpp +++ b/src/fheroes2/audio/audio_manager.cpp @@ -906,6 +906,16 @@ namespace AudioManager g_asyncSoundManager.pushSound( m82, Settings::Get().SoundVolume() ); } + double getSoundDurationMs( const int m82 ) + { + const std::scoped_lock lock( g_asyncSoundManager.resourceMutex() ); + + const std::vector & v = GetWAV( m82 ); + + // Sound duration is: (wavSize - headerSize) / sampleRateKHz / channelCount / byesPerSample. + return ( static_cast( v.size() ) - 44. ) / 22.050; + } + bool isExternalMusicFileAvailable( const int trackId ) { return !getExternalMusicFile( trackId ).empty(); diff --git a/src/fheroes2/audio/audio_manager.h b/src/fheroes2/audio/audio_manager.h index 9aa147db47b..fc461cae334 100644 --- a/src/fheroes2/audio/audio_manager.h +++ b/src/fheroes2/audio/audio_manager.h @@ -93,6 +93,9 @@ namespace AudioManager int PlaySound( const int m82 ); void PlaySoundAsync( const int m82 ); + // Returns specified sound duration in milliseconds. + double getSoundDurationMs( const int m82 ); + // Returns true if an external music file is available for the music track with the specified ID, otherwise returns false. bool isExternalMusicFileAvailable( const int trackId ); diff --git a/src/fheroes2/battle/battle_interface.cpp b/src/fheroes2/battle/battle_interface.cpp index a36809ab271..976b9979a3e 100644 --- a/src/fheroes2/battle/battle_interface.cpp +++ b/src/fheroes2/battle/battle_interface.cpp @@ -3834,12 +3834,15 @@ void Battle::Interface::RedrawActionMove( Unit & unit, const Indexes & path ) Cursor::Get().SetThemes( Cursor::WAR_POINTER ); - Indexes::const_iterator dst = path.begin(); Bridge * bridge = Arena::GetBridge(); + const int walkSoundId = unit.M82Move(); + // Get the time to animate movement for one cell. uint32_t frameDelay = Game::ApplyBattleSpeed( unit.animation.getMoveSpeed() ); - if ( unit.Modes( SP_HASTE ) ) { + const int cellCountForOneSound = static_cast( std::round( AudioManager::getSoundDurationMs( walkSoundId ) / frameDelay ) ) - 1; + + if ( unit.Modes( SP_HASTE ) && frameDelay > 1 ) { frameDelay = frameDelay * 65 / 100; // by 35% faster } else if ( unit.Modes( SP_SLOW ) ) { @@ -3848,6 +3851,7 @@ void Battle::Interface::RedrawActionMove( Unit & unit, const Indexes & path ) // Set the delay between movement animation frames. This delay will be used for all types of movement animations. unit.SwitchAnimation( Monster_Info::MOVING ); + Game::setCustomUnitMovementDelay( frameDelay / unit.animation.animationLength() ); std::string msg = _( "Moved %{monster}: from [%{src}] to [%{dst}]." ); @@ -3863,7 +3867,10 @@ void Battle::Interface::RedrawActionMove( Unit & unit, const Indexes & path ) const bool canFly = unit.isAbilityPresent( fheroes2::MonsterAbilityType::FLYING ); // If it is a wide creature (cache this boolean to use in the loop). const bool isWide = unit.isWide(); + + Indexes::const_iterator dst = path.begin(); const Indexes::const_iterator pathEnd = path.end(); + const Indexes::const_iterator finalStep = pathEnd - 1; const bool isOneStepPath = [&unit, &path]() { if ( path.size() == 1 ) { @@ -3952,15 +3959,22 @@ void Battle::Interface::RedrawActionMove( Unit & unit, const Indexes & path ) } } + // For Battle speed 9 and 10 we play sound by sound without any simultaneous playbacks + // and do not use calculated 'cellCountForOneSound' as the real count may differ as + // some monitors may use 50 Hz refresh rate with V-sync on (or a low CPU may not allow so high FPS). + const bool playSoundBySound = Settings::Get().BattleSpeed() > 8; + int soundStatus = -1; + while ( dst != pathEnd ) { // Check if a wide unit changes its horizontal direction. if ( isWide && unit.GetTailIndex() == *dst ) { // We must not reflect the flyers at the and of the path (just before the landing). - if ( !canFly || ( dst != ( pathEnd - 1 ) ) ) { + if ( !canFly || ( dst != finalStep ) ) { unit.SetReflection( !unit.isReflect() ); } // After changing the direction go to the next step in the path. ++dst; + continue; } @@ -3977,7 +3991,7 @@ void Battle::Interface::RedrawActionMove( Unit & unit, const Indexes & path ) unit.SwitchAnimation( Monster_Info::STAND_STILL ); bridge->ActionDown(); _movingUnit = &unit; - if ( dst == ( pathEnd - 1 ) ) { + if ( dst == finalStep ) { // There is only one cell left to move after standing. unit.SwitchAnimation( Monster_Info::MOVE_QUICK ); } @@ -3992,9 +4006,20 @@ void Battle::Interface::RedrawActionMove( Unit & unit, const Indexes & path ) _movingPos.x -= CELLW; } - // Render the unit movement with the movement sound. - // TODO: adjust sounds calls and synchronize them with frames. Take into account that some sounds (like for Cavalry) consists of a sequence of steps. - AudioManager::PlaySound( unit.M82Move() ); + // Limit the simultaneous walk sounds. + if ( playSoundBySound ) { + if ( soundStatus < 0 || !Mixer::isPlaying( soundStatus ) ) { + soundStatus = AudioManager::PlaySound( walkSoundId ); + } + } + else { + if ( soundStatus < 1 ) { + AudioManager::PlaySound( walkSoundId ); + soundStatus = cellCountForOneSound; + } + --soundStatus; + } + AnimateUnitWithDelay( unit ); unit.SetPosition( *dst ); @@ -4014,7 +4039,7 @@ void Battle::Interface::RedrawActionMove( Unit & unit, const Indexes & path ) unit.SwitchAnimation( Monster_Info::STAND_STILL ); bridge->ActionUp(); _movingUnit = &unit; - if ( dst == ( pathEnd - 1 ) ) { + if ( dst == finalStep ) { // There is only one cell left to move after standing. unit.SwitchAnimation( Monster_Info::MOVE_QUICK ); } @@ -4023,7 +4048,7 @@ void Battle::Interface::RedrawActionMove( Unit & unit, const Indexes & path ) unit.SwitchAnimation( Monster_Info::MOVE_START ); } } - else if ( dst == ( pathEnd - 1 ) ) { + else if ( dst == finalStep ) { // There is only one cell left to move. unit.SwitchAnimation( Monster_Info::MOVE_END ); } @@ -4087,7 +4112,10 @@ void Battle::Interface::RedrawActionFly( Unit & unit, const Position & pos ) const uint32_t step = unit.animation.getFlightSpeed(); uint32_t frameDelay = Game::ApplyBattleSpeed( unit.animation.getMoveSpeed() ); - if ( unit.Modes( SP_HASTE ) ) { + const int flySoundId = unit.M82Move(); + const int cellCountForOneSound = static_cast( std::round( AudioManager::getSoundDurationMs( flySoundId ) / frameDelay ) ) - 1; + + if ( unit.Modes( SP_HASTE ) && frameDelay > 1 ) { frameDelay = frameDelay * 8 / 10; // 20% faster } else if ( unit.Modes( SP_SLOW ) ) { @@ -4129,11 +4157,30 @@ void Battle::Interface::RedrawActionFly( Unit & unit, const Position & pos ) ++currentPoint; } + // For Battle speed 9 and 10 we play sound by sound without any simultaneous playbacks + // and do not use calculated 'cellCountForOneSound' as the real count may differ as + // some monitors may use 50 Hz refresh rate with V-sync on (or a low CPU may not allow so high FPS). + const bool playSoundBySound = Settings::Get().BattleSpeed() > 8; + int soundStatus = -1; + unit.SwitchAnimation( Monster_Info::MOVING ); while ( currentPoint != points.end() ) { _movingPos = *currentPoint; - AudioManager::PlaySound( unit.M82Move() ); + // Limit the simultaneous fly sounds. + if ( playSoundBySound ) { + if ( soundStatus < 0 || !Mixer::isPlaying( soundStatus ) ) { + soundStatus = AudioManager::PlaySound( flySoundId ); + } + } + else { + if ( soundStatus < 1 ) { + AudioManager::PlaySound( flySoundId ); + soundStatus = cellCountForOneSound; + } + --soundStatus; + } + unit.animation.restartAnimation(); AnimateUnitWithDelay( unit );