424 lines
11 KiB
C++
424 lines
11 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 "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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|