// Copyright Epic Games, Inc. All Rights Reserved. #include "Quartz/AudioMixerClock.h" #include "Quartz/AudioMixerClockManager.h" #include "AudioMixerSourceManager.h" #include "Sound/QuartzSubscription.h" #include "HAL/UnrealMemory.h" // Memcpy static float HeadlessClockSampleRateCvar = 100000.f; FAutoConsoleVariableRef CVarHeadlessClockSampleRate( TEXT("au.Quartz.HeadlessClockSampleRate"), HeadlessClockSampleRateCvar, TEXT("Sample rate to use for Quartz Clocks/Metronomes when no Mixer Device is present.\n") TEXT("0: Not Enabled, 1: Enabled"), ECVF_Default); namespace Audio { // FQuartzClockProxy Implementation // ctor FQuartzClockProxy::FQuartzClockProxy(TSharedPtr InClock) : ClockId(InClock->GetName()) , SharedQueue(InClock->GetCommandQueue()) , ClockWeakPtr(InClock) { } bool FQuartzClockProxy::IsValid() const { return SharedQueue.Pin().IsValid(); } bool FQuartzClockProxy::DoesClockExist() const { return IsValid(); } bool FQuartzClockProxy::IsClockRunning() const { TSharedPtr ClockPtr = ClockWeakPtr.Pin(); if (!ClockPtr) { return false; } return ClockPtr->IsRunning(); } float FQuartzClockProxy::GetDurationOfQuantizationTypeInSeconds(const EQuartzCommandQuantization& QuantizationType, float Multiplier) const { TSharedPtr ClockPtr = ClockWeakPtr.Pin(); if (!ClockPtr) { return 0.f; } return ClockPtr->GetDurationOfQuantizationTypeInSeconds(QuantizationType, Multiplier); } float FQuartzClockProxy::GetBeatProgressPercent( const EQuartzCommandQuantization& QuantizationType) const { TSharedPtr ClockPtr = ClockWeakPtr.Pin(); if (!ClockPtr) { return 0.f; } return ClockPtr->GetBeatProgressPercent(QuantizationType); } Audio::FQuartzClockTickRate FQuartzClockProxy::GetTickRate() const { TSharedPtr ClockPtr = ClockWeakPtr.Pin(); if (!ClockPtr) { return {}; } return ClockPtr->GetTickRate(); } FQuartzTransportTimeStamp FQuartzClockProxy::GetCurrentClockTimestamp() const { TSharedPtr ClockPtr = ClockWeakPtr.Pin(); if (!ClockPtr) { return {}; } return ClockPtr->GetCurrentTimestamp(); } float FQuartzClockProxy::GetEstimatedClockRunTimeSeconds() const { TSharedPtr ClockPtr = ClockWeakPtr.Pin(); if (!ClockPtr) { return 0.f; } return ClockPtr->GetEstimatedRunTime(); } bool FQuartzClockProxy::SendCommandToClock(TFunction InCommand) { if (FQuartzClockCommandQueuePtr QueuePtr = SharedQueue.Pin()) { QueuePtr->PushLambda( [Command = MoveTemp(InCommand)](Quartz::IQuartzClock& InListener) { Command(static_cast(&InListener)); }); return true; } return false; } // FQuartzClock Implementation FQuartzClock::FQuartzClock(const FName& InName, const FQuartzClockSettings& InClockSettings, FQuartzClockManager* InOwningClockManagerPtr) : Metronome(InClockSettings.TimeSignature, InName) , OwningClockManagerPtr(InOwningClockManagerPtr) , Name(InName) , bIsRunning(false) , bIgnoresFlush(InClockSettings.bIgnoreLevelChange) { FMixerDevice* MixerDevice = GetMixerDevice(); if (MixerDevice) { Metronome.SetSampleRate(MixerDevice->GetSampleRate()); } else { Metronome.SetSampleRate(HeadlessClockSampleRateCvar); } UpdateCachedState(); } FQuartzClock::~FQuartzClock() { Shutdown(); } void FQuartzClock::ChangeTickRate(FQuartzClockTickRate InNewTickRate, int32 NumFramesLeft) { FMixerDevice* MixerDevice = GetMixerDevice(); if (MixerDevice) { InNewTickRate.SetSampleRate(MixerDevice->GetSampleRate()); } else { InNewTickRate.SetSampleRate(HeadlessClockSampleRateCvar); } Metronome.SetTickRate(InNewTickRate, NumFramesLeft); FQuartzClockTickRate CurrentTickRate = Metronome.GetTickRate(); // ratio between new and old rates const double Ratio = InNewTickRate.GetFramesPerTick() / CurrentTickRate.GetFramesPerTick(); // adjust time-till-fire for existing commands for (auto& Command : PendingCommands) { if(Command.Command && !Command.Command->ShouldDeadlineIgnoresBpmChanges()) { Command.NumFramesUntilExec = NumFramesLeft + Ratio * (Command.NumFramesUntilExec - NumFramesLeft); } } for (auto& Command : ClockAlteringPendingCommands) { if(Command.Command && !Command.Command->ShouldDeadlineIgnoresBpmChanges()) { Command.NumFramesUntilExec = NumFramesLeft + Ratio * (Command.NumFramesUntilExec - NumFramesLeft); } } UpdateCachedState(); } void FQuartzClock::ChangeTimeSignature(const FQuartzTimeSignature& InNewTimeSignature) { Metronome.SetTimeSignature(InNewTimeSignature); UpdateCachedState(); } void FQuartzClock::Resume() { if (bIsRunning == false) { for (auto& Command : PendingCommands) { // Update countdown time to each quantized command Command.Command->OnClockStarted(); } for (auto& Command : ClockAlteringPendingCommands) { // Update countdown time to each quantized command Command.Command->OnClockStarted(); } } bIsRunning = true; } void FQuartzClock::Stop(bool CancelPendingEvents) { bIsRunning = false; Metronome.ResetTransport(); TickDelayLengthInFrames = 0; if (CancelPendingEvents) { for (auto& Command : PendingCommands) { Command.Command->Cancel(); } for (auto& Command : ClockAlteringPendingCommands) { Command.Command->Cancel(); } PendingCommands.Reset(); ClockAlteringPendingCommands.Reset(); } } void FQuartzClock::Pause() { if (bIsRunning) { for (auto& Command : PendingCommands) { // Update countdown time to each quantized command Command.Command->OnClockPaused(); } for (auto& Command : ClockAlteringPendingCommands) { // Update countdown time to each quantized command Command.Command->OnClockPaused(); } } bIsRunning = false; } void FQuartzClock::Restart(bool bPause) { bIsRunning = !bPause; TickDelayLengthInFrames = 0; } void FQuartzClock::Shutdown() { for (PendingCommand& PendingCommand : PendingCommands) { PendingCommand.Command->Cancel(); } for (PendingCommand& PendingCommand : ClockAlteringPendingCommands) { PendingCommand.Command->Cancel(); } PendingCommands.Reset(); ClockAlteringPendingCommands.Reset(); } void FQuartzClock::LowResolutionTick(float InDeltaTimeSeconds) { TRACE_CPUPROFILER_EVENT_SCOPE(QuartzClock::Tick_LowRes); UE_LOG(LogAudioQuartz, Verbose, TEXT("Quartz Clock Tick (low-res): %s"), *Name.ToString()); PreTickCommands->PumpCommandQueue(*this); Tick(static_cast(InDeltaTimeSeconds * Metronome.GetTickRate().GetSampleRate())); } void FQuartzClock::Tick(int32 InNumFramesUntilNextTick) { TRACE_CPUPROFILER_EVENT_SCOPE(QuartzClock::Tick); TRACE_CPUPROFILER_EVENT_SCOPE(QuartzClock::GameThreadCommands); UE_LOG(LogAudioQuartz, Verbose, TEXT("Quartz Clock Tick: %s"), *Name.ToString()); PreTickCommands->PumpCommandQueue(*this); if (!bIsRunning) { return; } if (TickDelayLengthInFrames >= InNumFramesUntilNextTick) { TickDelayLengthInFrames -= InNumFramesUntilNextTick; return; } const int32 FramesOfLatency = (ThreadLatencyInMilliseconds / 1000) * Metronome.GetTickRate().GetSampleRate(); int32 FramesToTick = InNumFramesUntilNextTick - TickDelayLengthInFrames; // commands executed in TickInternal may alter "TickDelayLengthInFrames" for the metronome's benefit // for the 2nd TickInternal() call we want to use the unmodified value (OriginalTickDelayLengthInFrames). const int32 OriginalTickDelayLengthInFrames = TickDelayLengthInFrames; TickInternal(FramesToTick, ClockAlteringPendingCommands, FramesOfLatency, OriginalTickDelayLengthInFrames); TickInternal(FramesToTick, PendingCommands, FramesOfLatency, OriginalTickDelayLengthInFrames); // FramesToTick may have been updated by TickInternal, recalculate FramesToTick = InNumFramesUntilNextTick - TickDelayLengthInFrames; Metronome.Tick(FramesToTick, FramesOfLatency); TickDelayLengthInFrames = 0; UpdateCachedState(); } FQuartzClockCommandQueueWeakPtr FQuartzClock::GetCommandQueue() const { if (!PreTickCommands.IsValid()) { PreTickCommands = MakeShared(); } return PreTickCommands; } void FQuartzClock::TickInternal(int32 InNumFramesUntilNextTick, TArray& CommandsToTick, int32 FramesOfLatency, int32 FramesOfDelay) { TRACE_CPUPROFILER_EVENT_SCOPE(QuartzClock::TickInternal); bool bHaveCommandsToRemove = false; // Update all pending commands for (PendingCommand& PendingCommand : CommandsToTick) { // Time to notify game thread? if (PendingCommand.NumFramesUntilExec < FramesOfLatency) { PendingCommand.Command->AboutToStart(); } // Time To execute? if (PendingCommand.NumFramesUntilExec < InNumFramesUntilNextTick) { PendingCommand.Command->OnFinalCallback(PendingCommand.NumFramesUntilExec + FramesOfDelay); PendingCommand.Command.Reset(); bHaveCommandsToRemove = true; } else // not yet executing { PendingCommand.NumFramesUntilExec -= InNumFramesUntilNextTick; PendingCommand.Command->Update(PendingCommand.NumFramesUntilExec); } } // clean up executed commands if (bHaveCommandsToRemove) { for (int32 i = CommandsToTick.Num() - 1; i >= 0; --i) { if (!CommandsToTick[i].Command.IsValid()) { CommandsToTick.RemoveAtSwap(i); } } } } void FQuartzClock::UpdateCachedState() { FScopeLock ScopeLock(&CachedClockStateCritSec); CachedClockState.TickRate = Metronome.GetTickRate(); CachedClockState.TimeStamp = Metronome.GetTimeStamp(); CachedClockState.RunTimeInSeconds = (float)Metronome.GetTimeSinceStart(); const uint64 TempLastCacheTimestamp = CachedClockState.LastCacheTickCpuCycles64; CachedClockState.LastCacheTickCpuCycles64 = Metronome.GetLastTickCpuCycles64(); CachedClockState.LastCacheTickDeltaCpuCycles64 = CachedClockState.LastCacheTickCpuCycles64 - TempLastCacheTimestamp; // copy previous phases (as temp values) FMemory::Memcpy(CachedClockState.MusicalDurationPhaseDeltas, CachedClockState.MusicalDurationPhases); // update current phases Metronome.CalculateDurationPhases(CachedClockState.MusicalDurationPhases); // convert temp copy to deltas constexpr int32 NumDurations = static_cast(EQuartzCommandQuantization::Count); for(int32 i = 0; i < NumDurations; ++i) { CachedClockState.MusicalDurationPhaseDeltas[i] = FMath::Wrap(CachedClockState.MusicalDurationPhases[i] - CachedClockState.MusicalDurationPhaseDeltas[i], 0.f, 1.f); } } void FQuartzClock::SetSampleRate(float InNewSampleRate) { if (FMath::IsNearlyEqual(InNewSampleRate, Metronome.GetTickRate().GetSampleRate())) { return; } // update Tick Rate Metronome.SetSampleRate(InNewSampleRate); UpdateCachedState(); } bool FQuartzClock::IgnoresFlush() const { return bIgnoresFlush; } bool FQuartzClock::DoesMatchSettings(const FQuartzClockSettings& InClockSettings) const { return Metronome.GetTimeSignature() == InClockSettings.TimeSignature; } void FQuartzClock::SubscribeToTimeDivision(FQuartzGameThreadSubscriber InSubscriber, EQuartzCommandQuantization InQuantizationBoundary) { Metronome.SubscribeToTimeDivision(InSubscriber, InQuantizationBoundary); } void FQuartzClock::SubscribeToAllTimeDivisions(FQuartzGameThreadSubscriber InSubscriber) { Metronome.SubscribeToAllTimeDivisions(InSubscriber); } void FQuartzClock::UnsubscribeFromTimeDivision(FQuartzGameThreadSubscriber InSubscriber, EQuartzCommandQuantization InQuantizationBoundary) { Metronome.UnsubscribeFromTimeDivision(InSubscriber, InQuantizationBoundary); } void FQuartzClock::UnsubscribeFromAllTimeDivisions(FQuartzGameThreadSubscriber InSubscriber) { Metronome.UnsubscribeFromAllTimeDivisions(InSubscriber); } void FQuartzClock::AddQuantizedCommand(FQuartzQuantizationBoundary InQuantizationBondary, TSharedPtr InNewEvent) { if (!ensure(InNewEvent.IsValid())) { return; } if (!bIsRunning && InQuantizationBondary.bCancelCommandIfClockIsNotRunning) { InNewEvent->Cancel(); return; } if (InQuantizationBondary.bResetClockOnQueued) { Stop(/* clear pending events = */true); Restart(!bIsRunning); } if (!bIsRunning && InQuantizationBondary.bResumeClockOnQueued) { Resume(); } int32 FramesUntilExec = 0; // if this is un-quantized, execute immediately (even if the clock is paused) if (InQuantizationBondary.Quantization == EQuartzCommandQuantization::None) { UE_LOG(LogAudioQuartz, Verbose, TEXT("Quartz Command:(%s) | Deadline (frames):[%i] | Boundary: [%s]") , *InNewEvent->GetCommandName().ToString() , FramesUntilExec , *InQuantizationBondary.ToString() ); InNewEvent->AboutToStart(); InNewEvent->OnFinalCallback(0); return; } // get number of frames until event (assuming we are at frame 0) FramesUntilExec = FMath::RoundToInt(Metronome.GetFramesUntilBoundary(InQuantizationBondary)); // query metronome (round result to int) const int32 OverriddenFramesUntilExec = FMath::Max(0, InNewEvent->OverrideFramesUntilExec(FramesUntilExec)); // allow command to override the deadline (clamp result) const bool bOverridden = (FramesUntilExec != OverriddenFramesUntilExec); UE_LOG(LogAudioQuartz, Verbose, TEXT("Quartz Command:(%s) | Deadline (frames):[%i%s] | Boundary: [%s]") , *InNewEvent->GetCommandName().ToString() , OverriddenFramesUntilExec , bOverridden? *FString::Printf(TEXT("(overridden from %i)"), FramesUntilExec) : TEXT("") , *InQuantizationBondary.ToString() ); // after the log, use tho Overridden value FramesUntilExec = OverriddenFramesUntilExec; // finalize the requested subscriber offsets and notify the command of their deadline InNewEvent->OnScheduled(Metronome.GetTickRate()); InNewEvent->Update(FramesUntilExec); // if this is going to execute on the next tick, warn Game Thread Subscribers as soon as possible if (FramesUntilExec == 0) { InNewEvent->AboutToStart(); } // add to pending commands list, execute OnQueued() if (InNewEvent->IsClockAltering()) { ClockAlteringPendingCommands.Emplace(PendingCommand(MoveTemp(InNewEvent), FramesUntilExec)); } else { PendingCommands.Emplace(PendingCommand(MoveTemp(InNewEvent), FramesUntilExec)); } } bool FQuartzClock::CancelQuantizedCommand(TSharedPtr InCommandPtr) { if (InCommandPtr->IsClockAltering()) { return CancelQuantizedCommandInternal(InCommandPtr, ClockAlteringPendingCommands); } return CancelQuantizedCommandInternal(InCommandPtr, PendingCommands); } bool FQuartzClock::HasPendingEvents() const { // if container has any events in it. return (NumPendingEvents() > 0); } int32 FQuartzClock::NumPendingEvents() const { return PendingCommands.Num() + ClockAlteringPendingCommands.Num(); } bool FQuartzClock::IsRunning() const { return bIsRunning; } float FQuartzClock::GetDurationOfQuantizationTypeInSeconds(const EQuartzCommandQuantization& QuantizationType, float Multiplier) { FScopeLock ScopeLock(&CachedClockStateCritSec); // if this is unquantized, return 0 if (QuantizationType == EQuartzCommandQuantization::None) { return 0; } // get number of frames until the relevant quantization event double FramesUntilExec = CachedClockState.TickRate.GetFramesPerDuration(QuantizationType); //Translate frames to seconds double SampleRate = CachedClockState.TickRate.GetSampleRate(); if (!FMath::IsNearlyZero(SampleRate)) { return (FramesUntilExec * Multiplier) / SampleRate; } else //Handle potential divide by zero { return INDEX_NONE; } } float FQuartzClock::GetBeatProgressPercent(const EQuartzCommandQuantization& QuantizationType) const { if(CachedClockState.LastCacheTickDeltaCpuCycles64 == 0) { return CachedClockState.MusicalDurationPhases[static_cast(QuantizationType)]; } // anticipate beat progress based on the amount of wall clock time that has passed since the last audio engine update const float LastPhase = CachedClockState.MusicalDurationPhases[static_cast(QuantizationType)]; const float PhaseDelta = CachedClockState.MusicalDurationPhaseDeltas[static_cast(QuantizationType)]; const uint64 CyclesSinceLastTick = FPlatformTime::Cycles64() - CachedClockState.LastCacheTickCpuCycles64; const float EstimatedPercentToNextTick = static_cast(CyclesSinceLastTick) / static_cast(CachedClockState.LastCacheTickDeltaCpuCycles64); return LastPhase + PhaseDelta * EstimatedPercentToNextTick; } FQuartzTransportTimeStamp FQuartzClock::GetCurrentTimestamp() { FScopeLock ScopeLock(&CachedClockStateCritSec); return CachedClockState.TimeStamp; } float FQuartzClock::GetEstimatedRunTime() { FScopeLock ScopeLock(&CachedClockStateCritSec); return CachedClockState.RunTimeInSeconds; } FMixerDevice* FQuartzClock::GetMixerDevice() { checkSlow(OwningClockManagerPtr); if (OwningClockManagerPtr) { return OwningClockManagerPtr->GetMixerDevice(); } return nullptr; } void FQuartzClock::AddQuantizedCommand(FQuartzQuantizedRequestData& InQuantizedRequestData) { float SampleRate = HeadlessClockSampleRateCvar; if (FMixerDevice* MixerDevice = GetMixerDevice()) { SampleRate = MixerDevice->GetSampleRate(); } FQuartzQuantizedCommandInitInfo Info(InQuantizedRequestData, SampleRate); AddQuantizedCommand(Info); } void FQuartzClock::AddQuantizedCommand(FQuartzQuantizedCommandInitInfo& InQuantizationCommandInitInfo) { if (!ensure(InQuantizationCommandInitInfo.QuantizedCommandPtr)) { return; } // this method can't be utilized by play commands because the AudioMixerSource needs a handle in order to stop it. // PlayCommands must be queued via the clock manager in AudioMixerSourceManager. if (!ensure(EQuartzCommandType::PlaySound != InQuantizationCommandInitInfo.QuantizedCommandPtr->GetCommandType())) { return; } // Can this command run without an Audio Device? FMixerDevice* MixerDevice = GetMixerDevice(); if (!MixerDevice && InQuantizationCommandInitInfo.QuantizedCommandPtr->RequiresAudioDevice()) { InQuantizationCommandInitInfo.QuantizedCommandPtr->Cancel(); } // this function is a friend of FQuartzClockManager, so we can use FindClock() directly // to access the shared ptr to "this" InQuantizationCommandInitInfo.SetOwningClockPtr(GetClockManager()->FindClock(GetName())); InQuantizationCommandInitInfo.QuantizedCommandPtr->OnQueued(InQuantizationCommandInitInfo); AddQuantizedCommand(InQuantizationCommandInitInfo.QuantizationBoundary, InQuantizationCommandInitInfo.QuantizedCommandPtr); } FMixerSourceManager* FQuartzClock::GetSourceManager() { FMixerDevice* MixerDevice = GetMixerDevice(); checkSlow(MixerDevice); if (MixerDevice) { return MixerDevice->GetSourceManager(); } return nullptr; } FQuartzClockTickRate FQuartzClock::GetTickRate() { FScopeLock ScopeLock(&CachedClockStateCritSec); return CachedClockState.TickRate; } FName FQuartzClock::GetName() const { return Name; } FQuartzClockManager* FQuartzClock::GetClockManager() { checkSlow(OwningClockManagerPtr); if (OwningClockManagerPtr) { return OwningClockManagerPtr; } return nullptr; } void FQuartzClock::ResetTransport(const int32 NumFramesToTickBeforeReset) { if (NumFramesToTickBeforeReset != 0) { Metronome.Tick(NumFramesToTickBeforeReset); } Metronome.ResetTransport(); } void FQuartzClock::AddToTickDelay(int32 NumFramesOfDelayToAdd) { TickDelayLengthInFrames += NumFramesOfDelayToAdd; } void FQuartzClock::SetTickDelay(int32 NumFramesOfDelay) { TickDelayLengthInFrames = NumFramesOfDelay; } bool FQuartzClock::CancelQuantizedCommandInternal(TSharedPtr InCommandPtr, TArray& CommandsToTick) { for (int32 i = CommandsToTick.Num() - 1; i >= 0; --i) { PendingCommand& PendingCommand = CommandsToTick[i]; if (PendingCommand.Command == InCommandPtr) { PendingCommand.Command->Cancel(); CommandsToTick.RemoveAtSwap(i); return true; } } return false; } } // namespace Audio