Files

448 lines
12 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#include "MusicSubsystem.h"
#include "Components/AudioComponent.h"
#include "Engine/Engine.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"
#include "MusicPlaylistDataAsset.h"
#include "Sound/SoundBase.h"
#include "Sound/SoundClass.h"
#include "SurvivalAudioSettings.h"
#include "UObject/UObjectGlobals.h"
UMusicSubsystem* UMusicSubsystem::Get(const UObject* WorldContextObject)
{
if (!GEngine || !WorldContextObject)
{
return nullptr;
}
const UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
UGameInstance* GameInstance = World ? World->GetGameInstance() : nullptr;
return GameInstance ? GameInstance->GetSubsystem<UMusicSubsystem>() : nullptr;
}
void UMusicSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// Le seul endroit d'ou l'on voit passer un changement de scene : le lecteur
// survit aux maps, donc aucun BeginPlay ni aucun EndPlay ne peut jouer ce
// role -- et la map d'arrivee n'a justement pas a connaître le sujet.
PostLoadMapHandle = FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UMusicSubsystem::HandlePostLoadMap);
}
void UMusicSubsystem::Deinitialize()
{
if (PostLoadMapHandle.IsValid())
{
FCoreUObjectDelegates::PostLoadMapWithWorld.Remove(PostLoadMapHandle);
PostLoadMapHandle.Reset();
}
CancelPendingGap();
// Fondu a zero : on quitte le jeu, plus personne n'ecoute.
FadeOutAndRelease(MusicComponent, 0.f);
if (FadingOutComponent)
{
FadingOutComponent->Stop();
FadingOutComponent = nullptr;
}
Super::Deinitialize();
}
void UMusicSubsystem::PlayPlaylist(UMusicPlaylistDataAsset* Playlist, bool bForceRestart)
{
if (!Playlist || Playlist->Tracks.IsEmpty())
{
StopMusic();
return;
}
// Deja en cours : on la laisse tranquille. C'est tout l'interet de faire
// pointer la map du menu et celle du jeu vers le meme asset -- le chargement
// devient inaudible.
if (CurrentPlaylist == Playlist && bIsPlaying && !bForceRestart)
{
return;
}
CancelPendingGap();
// L'ancienne musique s'eteint sur sa propre voie pendant que la nouvelle
// entre : un composant audio ne peut pas se croiser avec lui-meme.
FadeOutAndRelease(MusicComponent, CurrentPlaylist ? CurrentPlaylist->FadeOutDuration : Playlist->FadeOutDuration);
CurrentPlaylist = Playlist;
bShuffleEnabled = Playlist->bShuffle;
LastPlayedTrackIndex = INDEX_NONE;
bIsPlaying = true;
BuildPlayOrder();
if (PlayOrder.IsEmpty())
{
// Que des cases vides dans la liste.
bIsPlaying = false;
return;
}
PlayTrackAtCursor(Playlist->FadeInDuration);
}
void UMusicSubsystem::StopMusic(float FadeOutDuration)
{
CancelPendingGap();
const float Duration = (FadeOutDuration >= 0.f)
? FadeOutDuration
: (CurrentPlaylist ? CurrentPlaylist->FadeOutDuration : 0.f);
FadeOutAndRelease(MusicComponent, Duration);
bIsPlaying = false;
CurrentPlaylist = nullptr;
PlayOrder.Reset();
OrderCursor = INDEX_NONE;
OnMusicTrackChanged.Broadcast(nullptr);
}
void UMusicSubsystem::SkipToNextTrack()
{
if (!bIsPlaying || !CurrentPlaylist)
{
return;
}
CancelPendingGap();
// On LIBERE le composant au lieu de le reutiliser. Stop() et FadeOut()
// rediffusent OnAudioFinished, et cette diffusion transite par le thread audio :
// elle peut donc arriver une frame plus tard, soit APRES le demarrage du morceau
// suivant, ce qui en sauterait un second. Un composant neuf, celui d'avant
// detache, rend ce retour tardif sans effet.
const float SkipFade = FMath::Min(CurrentPlaylist->FadeOutDuration, 1.f);
FadeOutAndRelease(MusicComponent, SkipFade);
AdvanceAndPlay(SkipFade);
}
void UMusicSubsystem::SetShuffleEnabled(bool bEnabled)
{
if (bShuffleEnabled == bEnabled)
{
return;
}
bShuffleEnabled = bEnabled;
if (!CurrentPlaylist)
{
return;
}
// Le morceau en cours continue ; seule la SUITE change. On recale le curseur
// dessus pour qu'en mode ordonne la lecture reprenne juste apres lui, et non
// au debut de la liste.
BuildPlayOrder();
const int32 Found = PlayOrder.IndexOfByKey(LastPlayedTrackIndex);
OrderCursor = (Found != INDEX_NONE) ? Found : 0;
}
void UMusicSubsystem::SetMusicVolume(float NewVolume)
{
MusicVolume = FMath::Clamp(NewVolume, 0.f, 1.f);
if (MusicComponent)
{
// SetVolumeMultiplier est independant du fader utilise par FadeIn/FadeOut :
// regler le volume au milieu d'un fondu ne l'interrompt pas.
MusicComponent->SetVolumeMultiplier(GetEffectiveVolume());
}
}
USoundBase* UMusicSubsystem::GetCurrentTrack() const
{
return MusicComponent ? MusicComponent->Sound : nullptr;
}
void UMusicSubsystem::HandleTrackFinished()
{
// N'est atteint que par une fin naturelle : toute coupure manuelle passe par
// FadeOutAndRelease, qui detache le delegue avant d'arreter le son.
if (!bIsPlaying || !CurrentPlaylist)
{
return;
}
const float MinGap = FMath::Max(0.f, CurrentPlaylist->MinGapBetweenTracks);
const float MaxGap = FMath::Max(MinGap, CurrentPlaylist->MaxGapBetweenTracks);
const float Gap = FMath::FRandRange(MinGap, MaxGap);
if (Gap > KINDA_SMALL_NUMBER)
{
ScheduleGap(Gap);
}
else
{
AdvanceAndPlay(0.f);
}
}
void UMusicSubsystem::HandlePostLoadMap(UWorld* LoadedWorld)
{
// En PIE plusieurs instances peuvent charger des maps en parallele : on ne
// reagit qu'a la nôtre.
if (!LoadedWorld || LoadedWorld->GetGameInstance() != GetGameInstance())
{
return;
}
if (LevelChangeMode != EMusicLevelChangeMode::New || !bIsPlaying)
{
return;
}
// Volontairement apres le chargement et non avant : le nouveau morceau entre
// en meme temps que la nouvelle scene s'affiche, pas pendant l'ecran de
// chargement ou personne ne l'associe a quoi que ce soit.
SkipToNextTrack();
}
void UMusicSubsystem::BuildPlayOrder()
{
PlayOrder.Reset();
OrderCursor = 0;
if (!CurrentPlaylist)
{
return;
}
PlayOrder.Reserve(CurrentPlaylist->Tracks.Num());
for (int32 Index = 0; Index < CurrentPlaylist->Tracks.Num(); ++Index)
{
// Filtre en amont : une case vide ne doit jamais devenir un morceau a jouer,
// sinon il faudrait la sauter au moment de la lecture -- et une liste
// entierement vide y bouclerait a l'infini.
if (CurrentPlaylist->Tracks[Index])
{
PlayOrder.Add(Index);
}
}
if (!bShuffleEnabled || PlayOrder.Num() < 2)
{
return;
}
// Fisher-Yates : chaque permutation equiprobable, une seule passe.
for (int32 Index = PlayOrder.Num() - 1; Index > 0; --Index)
{
PlayOrder.Swap(Index, FMath::RandRange(0, Index));
}
// Un nouveau cycle ne redemarre jamais sur le morceau qui vient de finir.
// C'est la difference entre "aleatoire" et "aleatoire agreable a ecouter".
if (PlayOrder[0] == LastPlayedTrackIndex)
{
PlayOrder.Swap(0, FMath::RandRange(1, PlayOrder.Num() - 1));
}
}
void UMusicSubsystem::AdvanceAndPlay(float FadeInDuration)
{
if (!CurrentPlaylist)
{
return;
}
++OrderCursor;
if (!PlayOrder.IsValidIndex(OrderCursor))
{
if (!CurrentPlaylist->bLoop)
{
StopMusic(CurrentPlaylist->FadeOutDuration);
return;
}
BuildPlayOrder();
if (PlayOrder.IsEmpty())
{
StopMusic(0.f);
return;
}
}
PlayTrackAtCursor(FadeInDuration);
}
void UMusicSubsystem::PlayTrackAtCursor(float FadeInDuration)
{
if (!CurrentPlaylist || !PlayOrder.IsValidIndex(OrderCursor))
{
return;
}
const int32 TrackIndex = PlayOrder[OrderCursor];
USoundBase* Track = CurrentPlaylist->Tracks.IsValidIndex(TrackIndex) ? CurrentPlaylist->Tracks[TrackIndex].Get() : nullptr;
if (!Track)
{
// La playlist a ete editee pendant la lecture : on reconstruit plutot que
// de jouer un index perime.
BuildPlayOrder();
return;
}
// Un curseur de volume ne doit RIEN faire d'autre que changer le volume.
//
// Sans PlayWhenSilent, une source devenue inaudible -- curseur Musique a zero --
// est declaree TERMINEE par le moteur : OnAudioFinished part, HandleTrackFinished
// enchaîne, et le silence entre deux morceaux (MinGapBetweenTracks) demarre. Le
// joueur qui remonte son volume n'entend alors plus rien jusqu'a la piste
// suivante, alors qu'il n'a fait que bouger un reglage. Avec ce mode le morceau
// continue d'avancer sans etre rendu, et redevient simplement audible.
//
// Pose ICI et pas sur les assets, pour la meme raison que la SoundClass dans
// CreateMusicComponent : ajouter une piste a une playlist ne doit rien demander
// de plus. Ecrire ce champ ne salit pas l'asset -- rien n'appelle MarkPackageDirty.
Track->VirtualizationMode = EVirtualizationMode::PlayWhenSilent;
if (!MusicComponent)
{
MusicComponent = CreateMusicComponent(Track);
if (!MusicComponent)
{
bIsPlaying = false;
return;
}
}
else
{
MusicComponent->SetSound(Track);
}
MusicComponent->SetVolumeMultiplier(GetEffectiveVolume());
if (FadeInDuration > 0.f)
{
MusicComponent->FadeIn(FadeInDuration, 1.f);
}
else
{
MusicComponent->Play();
}
LastPlayedTrackIndex = TrackIndex;
OnMusicTrackChanged.Broadcast(Track);
}
UAudioComponent* UMusicSubsystem::CreateMusicComponent(USoundBase* Sound)
{
const UGameInstance* GameInstance = GetGameInstance();
UWorld* World = GameInstance ? GameInstance->GetWorld() : nullptr;
if (!World || !Sound)
{
return nullptr;
}
// bPersistAcrossLevelTransition : le composant est cree sans monde ni acteur
// proprietaire et marque bIgnoreForFlushing, le seul moyen de traverser un
// OpenLevel sans que le peripherique audio ne le coupe.
// bAutoDestroy a faux : on veut le reutiliser d'un morceau au suivant, et
// surtout recevoir son OnAudioFinished plutot que de le voir disparaître.
UAudioComponent* Component = UGameplayStatics::CreateSound2D(
World, Sound,
/*VolumeMultiplier*/ 1.f, /*PitchMultiplier*/ 1.f, /*StartTime*/ 0.f,
/*ConcurrencySettings*/ nullptr,
/*bPersistAcrossLevelTransition*/ true,
/*bAutoDestroy*/ false);
if (!Component)
{
return nullptr;
}
// La classe est imposee ICI plutot que sur chaque asset de morceau : ajouter une
// piste a une playlist ne doit pas demander de penser a la taguer, sinon le
// curseur Musique cesse d'agir sur elle sans le moindre signe.
if (USoundClass* MusicClass = USurvivalAudioSettings::Get()->GetMusicSoundClass())
{
Component->SoundClassOverride = MusicClass;
}
Component->bIsMusic = true;
Component->OnAudioFinished.AddDynamic(this, &UMusicSubsystem::HandleTrackFinished);
return Component;
}
void UMusicSubsystem::FadeOutAndRelease(TObjectPtr<UAudioComponent>& Component, float FadeOutDuration)
{
if (!Component)
{
return;
}
// Detacher AVANT d'arreter : Stop() comme FadeOut() finissent par rediffuser
// OnAudioFinished, qui enchainerait sur le morceau suivant alors qu'on cherche
// justement a couper.
Component->OnAudioFinished.RemoveDynamic(this, &UMusicSubsystem::HandleTrackFinished);
// Une seule voie de sortie : si un croisement precedent n'est pas fini, il est
// coupe net. Empiler les fondus ferait monter le nombre de voix pour rien.
if (FadingOutComponent && FadingOutComponent != Component)
{
FadingOutComponent->Stop();
FadingOutComponent = nullptr;
}
if (FadeOutDuration > 0.f && Component->IsPlaying())
{
Component->FadeOut(FadeOutDuration, 0.f);
FadingOutComponent = Component;
}
else
{
Component->Stop();
}
Component = nullptr;
}
void UMusicSubsystem::ScheduleGap(float DelaySeconds)
{
CancelPendingGap();
TWeakObjectPtr<UMusicSubsystem> WeakThis(this);
GapTickerHandle = FTSTicker::GetCoreTicker().AddTicker(TEXT("MusicSubsystem.Gap"), DelaySeconds,
[WeakThis](float /*DeltaTime*/)
{
if (UMusicSubsystem* Self = WeakThis.Get())
{
Self->GapTickerHandle.Reset();
Self->AdvanceAndPlay(0.f);
}
return false; // un seul declenchement
});
}
void UMusicSubsystem::CancelPendingGap()
{
if (GapTickerHandle.IsValid())
{
FTSTicker::RemoveTicker(GapTickerHandle);
GapTickerHandle.Reset();
}
}
float UMusicSubsystem::GetEffectiveVolume() const
{
return MusicVolume * (CurrentPlaylist ? CurrentPlaylist->VolumeMultiplier : 1.f);
}