#include "Audio/DescentAudio.h" #include "Core/DescentEnvQuery.h" #include "Core/DescentMath.h" #include "Core/DescentSimSubsystem.h" #include "Debug/DescentStateRegistry.h" #include "Player/DescentCharacter.h" #include "Player/DescentPlayerController.h" #include "Systems/DescentBio.h" #include "Systems/DescentCargo.h" #include "Systems/DescentHorror.h" #include "Vehicles/DescentCarrier.h" #include "Vehicles/KazahTender.h" #include "Vehicles/TenderFlight.h" #include "TheDescent.h" #include "Camera/PlayerCameraManager.h" #include "Components/AudioComponent.h" #include "Engine/Engine.h" #include "Engine/World.h" #include "Sound/SoundWaveProcedural.h" namespace DA = Descent::Audio; namespace Descent::Audio { const TArray& LoopNames() { static const TArray Names = { TEXT("drone"), TEXT("sea"), TEXT("hum"), TEXT("tender_engine"), TEXT("prop_wash"), TEXT("carrier_engine"), TEXT("drill"), TEXT("laser"), TEXT("suction"), TEXT("scrape"), TEXT("static"), TEXT("shear"), TEXT("pipe_flow") }; return Names; } bool Render(FName Name, TArray& Out) { // The recipes of the v2 cue list, reduced to what a few lines of synthesis give (fixed seed: the same sound every run). const bool bLoop = LoopNames().Contains(Name); static const TMap ShotLen = { { TEXT("ping"), 2.6 }, { TEXT("creak"), 0.9 }, { TEXT("groan"), 1.8 }, { TEXT("heartbeat"), 0.7 }, { TEXT("whisper"), 1.3 }, { TEXT("leviathan"), 3.5 }, { TEXT("radio_blip"), 0.09 }, { TEXT("static_burst"), 0.35 }, { TEXT("craft"), 0.4 }, { TEXT("place_ok"), 0.12 }, { TEXT("place_bad"), 0.18 }, { TEXT("jack"), 0.3 }, { TEXT("box_pack"), 0.25 }, { TEXT("alarm"), 0.6 }, { TEXT("splash"), 0.5 }, { TEXT("click"), 0.04 } }; const double* Len = ShotLen.Find(Name); if (!bLoop && !Len) { return false; } const double Seconds = bLoop ? 4.0 : *Len; const int32 N = FMath::RoundToInt32(Seconds * SampleRate); Out.SetNumUninitialized(N); uint32 Seed = GetTypeHash(Name) | 1u; auto Noise = [&Seed]() { Seed = Seed * 1664525u + 1013904223u; return (static_cast(Seed >> 8) & 0xffff) / 32768.0 - 1.0; }; auto Saw = [](double F, double T) { const double X = F * T; return 2.0 * (X - FMath::FloorToDouble(X + 0.5)); }; auto Sin = [](double F, double T) { return FMath::Sin(UE_DOUBLE_TWO_PI * F * T); }; double Brown = 0.0, Lp = 0.0; for (int32 I = 0; I < N; ++I) { const double T = static_cast(I) / SampleRate, U = T / Seconds; const double W = Noise(); Brown = FMath::Clamp(Brown + W * 0.04, -1.0, 1.0); double S = 0.0; if (Name == TEXT("drone")) { Lp += ((Saw(36.0, T) + Saw(28.25, T)) * 0.5 - Lp) * 0.04; S = Lp * 1.6; } else if (Name == TEXT("sea")) { Lp += (Brown - Lp) * 0.08; S = Lp * 1.4; } else if (Name == TEXT("hum")) { S = 0.5 * Sin(55.0, T) + 0.3 * Sin(110.0, T); } else if (Name == TEXT("tender_engine")) { Lp += (Saw(60.0, T) - Lp) * 0.12; S = Lp; } else if (Name == TEXT("prop_wash")) { Lp += (W - Lp) * 0.12; S = Lp * (0.7 + 0.3 * Sin(9.0, T)); } else if (Name == TEXT("carrier_engine")) { Lp += ((Saw(31.0, T) + Brown) * 0.5 - Lp) * 0.05; S = Lp * 1.8; } else if (Name == TEXT("drill")) { S = (0.4 * Saw(110.0, T) + 0.25 * FMath::Sign(Sin(55.0, T)) + 0.25 * W) * (0.75 + 0.25 * Sin(7.0, T)); } else if (Name == TEXT("laser")) { S = 0.35 * Sin(880.0, T) + 0.3 * Sin(441.0, T) + 0.1 * W; } else if (Name == TEXT("suction")) { Lp += (W - Lp) * (0.15 + 0.1 * Sin(1.5, T)); S = Lp * 1.2; } else if (Name == TEXT("scrape")) { S = Brown * (0.5 + 0.5 * Sin(13.0, T)); } else if (Name == TEXT("static")) { Lp += (W - Lp) * 0.5; S = (W - Lp) * 0.8; } else if (Name == TEXT("shear")) { Lp += (Brown - Lp) * 0.03; S = Lp * 2.0 * (0.6 + 0.4 * Sin(0.25, T)); } else if (Name == TEXT("pipe_flow")) { S = (FMath::Fmod(T, 0.23) < 0.012 ? W : 0.0) * 0.8; } else if (Name == TEXT("ping")) { for (int32 E = 0; E < 5; ++E) { const double Te = T - 0.42 * E; if (Te >= 0.0) { S += (0.4 * Sin(1180.0, Te) + 0.4 * Sin(1192.0, Te) + 0.2 * Sin(2360.0, Te)) * FMath::Exp(-6.0 * Te) * FMath::Pow(0.45, E); } } } else if (Name == TEXT("creak")) { S = (0.6 * Saw(90.0 - 30.0 * U, T) * (0.5 + 0.5 * Sin(23.0, T)) + 0.15 * W) * FMath::Sin(UE_DOUBLE_PI * U); } else if (Name == TEXT("groan")) { S = (0.8 * Sin(40.0 - 10.0 * U, T) + 0.2 * Brown) * FMath::Sin(UE_DOUBLE_PI * U); } else if (Name == TEXT("heartbeat")) { const double T2 = T - 0.28; S = Sin(52.0, T) * FMath::Exp(-18.0 * T) + (T2 >= 0.0 ? 0.8 * Sin(46.0, T2) * FMath::Exp(-16.0 * T2) : 0.0); } else if (Name == TEXT("whisper")) { Lp += (W - Lp) * 0.35; S = (W - Lp) * (0.5 + 0.5 * Sin(5.5, T)) * FMath::Sin(UE_DOUBLE_PI * U) * 0.7; } else if (Name == TEXT("leviathan")) { S = (0.7 * Sin(70.0 - 25.0 * U + 3.0 * Sin(4.0, T), T) + 0.2 * Sin(140.0 - 50.0 * U, T)) * FMath::Sin(UE_DOUBLE_PI * U); } else if (Name == TEXT("radio_blip")) { S = 0.5 * Sin(1200.0, T); } else if (Name == TEXT("static_burst")) { S = W * (1.0 - U); } else if (Name == TEXT("craft")) { S = 0.5 * Sin(520.0 + 500.0 * U, T) * (1.0 - U); } else if (Name == TEXT("place_ok")) { S = 0.5 * Sin(660.0, T) * (1.0 - U); } else if (Name == TEXT("place_bad")) { S = 0.5 * Saw(180.0, T) * (1.0 - U); } else if (Name == TEXT("jack")) { S = (T < 0.02 ? W : 0.4 * Sin(300.0, T)) * (1.0 - U); } else if (Name == TEXT("box_pack")) { S = (0.6 * W * FMath::Exp(-30.0 * T) + 0.4 * Sin(140.0, T) * FMath::Exp(-12.0 * T)); } else if (Name == TEXT("alarm")) { S = 0.5 * FMath::Sign(Sin(880.0, T)) * (FMath::Fmod(T, 0.2) < 0.1 ? 1.0 : 0.0); } else if (Name == TEXT("splash")) { Lp += (W - Lp) * 0.3; S = Lp * (1.0 - U) * 1.5; } else { S = W * (1.0 - U); } // a loop fades over its seam so it does not click if (bLoop) { const double Edge = FMath::Min(U, 1.0 - U) * Seconds / 0.03; S *= FMath::Clamp(Edge, 0.0, 1.0); } Out[I] = static_cast(FMath::Clamp(S, -1.0, 1.0) * 26000.0); } return true; } } UDescentAudioSubsystem* UDescentAudioSubsystem::Get(const UWorld* World) { return World ? World->GetSubsystem() : nullptr; } void UDescentAudioSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); for (const FName& N : DA::LoopNames()) { Loops.Add(N); } if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(&InWorld)) { FDescentSimRegisterOptions Opt; Opt.Owner = TEXT("Audio"); StepHandle = Sim->RegisterLateStep(EDescentLateStep::Audio, [this](const FDescentSimFrame& F) { Step(F.Dt); }, Opt); } } void UDescentAudioSubsystem::Deinitialize() { if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { Sim->Unregister(StepHandle); } Super::Deinitialize(); } TSharedPtr> UDescentAudioSubsystem::Pcm(FName Name) { if (TSharedPtr>* Found = Cache.Find(Name)) { return *Found; } TSharedPtr> Data = MakeShared>(); if (!DA::Render(Name, *Data)) { DA::Render(TEXT("click"), *Data); } Cache.Add(Name, Data); return Data; } void UDescentAudioSubsystem::EnsureComponents() { UWorld* World = GetWorld(); if (Owner || !World) { return; } bDevice = GEngine && GEngine->UseSound() && World->GetAudioDeviceRaw() != nullptr; if (!bDevice) { return; } // scenarios and tests run with -nosound: the model below is still computed Owner = World->SpawnActor(); USceneComponent* Root = NewObject(Owner, TEXT("Root")); Owner->SetRootComponent(Root); Root->RegisterComponent(); auto MakeWave = [this](bool bLooping) { USoundWaveProcedural* Wave = NewObject(this); Wave->SetSampleRate(DA::SampleRate); Wave->NumChannels = 1; Wave->Duration = 10000.f; // the engine's "loops for ever" duration Wave->SoundGroup = SOUNDGROUP_Default; Wave->bLooping = bLooping; return Wave; }; auto MakeComp = [this, Root](USoundWaveProcedural* Wave) { UAudioComponent* Comp = NewObject(Owner); Comp->SetupAttachment(Root); Comp->bAllowSpatialization = false; Comp->bIsUISound = true; // keeps playing while the game is paused Comp->bAutoActivate = false; Comp->SetSound(Wave); Comp->RegisterComponent(); return Comp; }; for (const FName& N : DA::LoopNames()) { USoundWaveProcedural* Wave = MakeWave(true); const TSharedPtr> Data = Pcm(N); // the audio thread asks for more whenever the queue runs dry: the same four seconds again Wave->OnSoundWaveProceduralUnderflow = FOnSoundWaveProceduralUnderflow::CreateLambda([Data](USoundWaveProcedural* W, int32) { W->QueueAudio(reinterpret_cast(Data->GetData()), Data->Num() * sizeof(int16)); }); UAudioComponent* Comp = MakeComp(Wave); Comp->SetVolumeMultiplier(0.001f); Comp->SetLowPassFilterEnabled(true); Comp->Play(); LoopWaves.Add(Wave); LoopComps.Add(N, Comp); } for (int32 I = 0; I < 6; ++I) { USoundWaveProcedural* Wave = MakeWave(false); ShotWaves.Add(Wave); ShotComps.Add(MakeComp(Wave)); } } bool UDescentAudioSubsystem::HadOneShot(FName Name) const { for (const FDescentOneShot& S : OneShots) { if (S.Name == Name) { return true; } } return false; } void UDescentAudioSubsystem::OneShot(FName Name, float Volume) { const UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld()); OneShots.Add({ Name, Sim ? Sim->SimTime() : 0.0, Volume }); if (OneShots.Num() > 20) { OneShots.RemoveAt(0, OneShots.Num() - 20); } EnsureComponents(); if (!bDevice || ShotComps.Num() == 0) { return; } const int32 I = NextShot++ % ShotComps.Num(); const TSharedPtr> Data = Pcm(Name); ShotWaves[I]->ResetAudio(); ShotWaves[I]->QueueAudio(reinterpret_cast(Data->GetData()), Data->Num() * sizeof(int16)); ShotComps[I]->SetVolumeMultiplier(FMath::Clamp(Volume, 0.05f, 2.f) * 0.5f); if (!ShotComps[I]->IsPlaying()) { ShotComps[I]->Play(); } } void UDescentAudioSubsystem::Step(double Dt) { UWorld* World = GetWorld(); const ADescentPlayerController* PC = World ? Cast(World->GetFirstPlayerController()) : nullptr; if (!PC) { return; } EnsureComponents(); const EDescentMode Mode = PC->GetMode(); const FVector Ref = PC->RefPointM(); const double Dz = Descent::Horror::DepthFactor(Descent::DepthOfZ(Ref.Z)); const bool bSwim = Mode == EDescentMode::Swim; const bool bTender = Mode == EDescentMode::Sub || Mode == EDescentMode::Docked || Mode == EDescentMode::Launch || Mode == EDescentMode::Dock; const bool bInside = !bSwim && !(Mode == EDescentMode::Sub); // on foot, at the helm, in a docked Tender: inside the carrier's dry volume MasterLowPassHz = bSwim ? DA::SwimLowPassHz : (bTender ? DA::TenderLowPassHz : DA::AirLowPassHz); auto Set = [this](const TCHAR* Name, double Volume, double Pitch = 1.0) { FDescentLoopState& L = Loops.FindOrAdd(Name); L.Volume = static_cast(FMath::Clamp(Volume, 0.0, 1.0)); L.Pitch = static_cast(FMath::Clamp(Pitch, 0.3, 3.0)); }; const ADescentCarrier* Carrier = UDescentCarrierSubsystem::GetCarrier(World); const AKazahTender* T = PC->GetTender(); const UDescentHorrorSubsystem* Horror = UDescentHorrorSubsystem::Get(World); const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(World); const UDescentBioSubsystem* Bio = UDescentBioSubsystem::Get(World); Set(TEXT("drone"), (0.05 + 0.11 * Dz) * (bInside ? 0.5 : 1.0)); Set(TEXT("sea"), bInside ? 0.04 : 0.10); Set(TEXT("hum"), bInside ? 0.08 : 0.0); const bool bOut = T && Mode == EDescentMode::Sub; const double Speed = bOut ? T->Flight->VelMps.Size() : 0.0; Set(TEXT("tender_engine"), bOut ? 0.03 + 0.08 * FMath::Min(1.0, T->Flight->Thrust) : 0.0, 1.0 + 0.1 * Speed); Set(TEXT("prop_wash"), bOut ? 0.08 * FMath::Min(1.0, Speed / 9.0) : 0.0, 0.8 + 0.06 * Speed); Set(TEXT("carrier_engine"), Carrier ? 0.2 * FMath::Abs(Carrier->Helm.Throttle) * (bInside ? 1.0 : 0.3) : 0.0); Set(TEXT("drill"), bOut && T->Flight->bDrilling && !T->Flight->bLaserHead ? 0.14 : 0.0); Set(TEXT("laser"), bOut && T->Flight->bDrilling && T->Flight->bLaserHead ? 0.10 : 0.0); Set(TEXT("suction"), bOut && T->Flight->bSucking ? 0.10 : 0.0); Set(TEXT("scrape"), FMath::Max(Carrier ? 0.3 * Carrier->Helm.Scrape : 0.0, bOut && T->Flight->Contacts > 0 ? 0.12 : 0.0)); Set(TEXT("static"), Horror ? 0.1 * Horror->Hallu : 0.0); Set(TEXT("shear"), Ref.Z > 10.0 ? 0.2 : 0.0); Set(TEXT("pipe_flow"), bInside && ((Cargo && Cargo->NumPipePieces() > 0) || (Bio && Bio->PipePieces() > 0)) ? 0.06 : 0.0); if (!bDevice) { return; } for (const TPair>& P : LoopComps) { const FDescentLoopState* L = Loops.Find(P.Key); if (!P.Value || !L) { continue; } P.Value->SetVolumeMultiplier(FMath::Max(0.001f, L->Volume)); P.Value->SetPitchMultiplier(L->Pitch); P.Value->SetLowPassFilterFrequency(static_cast(MasterLowPassHz)); } } // ---- state section `audio` (section 9.4): tests cannot hear --------------------------------------------------------------- static FDescentStateProviderRegistrar GAudioStateSection(TEXT("audio"), [](UWorld* World, FJsonObject& Out) { const UDescentAudioSubsystem* A = UDescentAudioSubsystem::Get(World); Out.SetBoolField(TEXT("available"), A != nullptr); if (!A) { return; } Out.SetBoolField(TEXT("device"), A->bDevice); Out.SetNumberField(TEXT("master_lowpass_hz"), A->MasterLowPassHz); TSharedRef Loops = MakeShared(); for (const FName& N : Descent::Audio::LoopNames()) { const FDescentLoopState* L = A->FindLoop(N); TSharedRef J = MakeShared(); J->SetNumberField(TEXT("volume"), Descent::Json::Round(L ? L->Volume : 0.0)); J->SetNumberField(TEXT("pitch"), Descent::Json::Round(L ? L->Pitch : 1.0)); Loops->SetObjectField(N.ToString(), J); } Out.SetObjectField(TEXT("loops"), Loops); TArray> Shots; for (const FDescentOneShot& S : A->OneShots) { TSharedRef J = MakeShared(); J->SetStringField(TEXT("name"), S.Name.ToString()); J->SetNumberField(TEXT("t"), Descent::Json::Round(S.SimTime)); Shots.Add(MakeShared(J)); } Out.SetArrayField(TEXT("one_shots"), Shots); });