mirror of https://github.com/OpenRCT2/OpenRCT2.git
517 lines
15 KiB
C++
517 lines
15 KiB
C++
/*****************************************************************************
|
|
* Copyright (c) 2014-2024 OpenRCT2 developers
|
|
*
|
|
* For a complete list of all authors, please refer to contributors.md
|
|
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
|
*
|
|
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
|
*****************************************************************************/
|
|
|
|
#include "audio.h"
|
|
|
|
#include "../Context.h"
|
|
#include "../OpenRCT2.h"
|
|
#include "../PlatformEnvironment.h"
|
|
#include "../config/Config.h"
|
|
#include "../core/File.h"
|
|
#include "../core/FileStream.h"
|
|
#include "../core/Memory.hpp"
|
|
#include "../core/String.hpp"
|
|
#include "../entity/Peep.h"
|
|
#include "../interface/Viewport.h"
|
|
#include "../localisation/Language.h"
|
|
#include "../localisation/StringIds.h"
|
|
#include "../object/AudioObject.h"
|
|
#include "../object/ObjectManager.h"
|
|
#include "../ride/Ride.h"
|
|
#include "../ride/RideAudio.h"
|
|
#include "../scenes/intro/IntroScene.h"
|
|
#include "../ui/UiContext.h"
|
|
#include "../util/Util.h"
|
|
#include "../world/Climate.h"
|
|
#include "AudioChannel.h"
|
|
#include "AudioContext.h"
|
|
#include "AudioMixer.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
namespace OpenRCT2::Audio
|
|
{
|
|
struct AudioParams
|
|
{
|
|
bool in_range;
|
|
int32_t volume;
|
|
int32_t pan;
|
|
};
|
|
|
|
static std::vector<std::string> _audioDevices;
|
|
static int32_t _currentAudioDevice = -1;
|
|
static ObjectEntryIndex _soundsAudioObjectEntryIndex = OBJECT_ENTRY_INDEX_NULL;
|
|
static ObjectEntryIndex _soundsAdditionalAudioObjectEntryIndex = OBJECT_ENTRY_INDEX_NULL;
|
|
static ObjectEntryIndex _titleAudioObjectEntryIndex = OBJECT_ENTRY_INDEX_NULL;
|
|
|
|
bool gGameSoundsOff = false;
|
|
int32_t gVolumeAdjustZoom = 0;
|
|
|
|
static std::shared_ptr<IAudioChannel> _titleMusicChannel = nullptr;
|
|
|
|
VehicleSound gVehicleSoundList[kMaxVehicleSounds];
|
|
|
|
bool IsAvailable()
|
|
{
|
|
if (_currentAudioDevice == -1)
|
|
return false;
|
|
if (gGameSoundsOff)
|
|
return false;
|
|
if (!gConfigSound.SoundEnabled)
|
|
return false;
|
|
if (gOpenRCT2Headless)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
void Init()
|
|
{
|
|
auto audioContext = GetContext()->GetAudioContext();
|
|
if (gConfigSound.Device.empty())
|
|
{
|
|
audioContext->SetOutputDevice("");
|
|
_currentAudioDevice = 0;
|
|
}
|
|
else
|
|
{
|
|
audioContext->SetOutputDevice(gConfigSound.Device);
|
|
|
|
PopulateDevices();
|
|
for (int32_t i = 0; i < GetDeviceCount(); i++)
|
|
{
|
|
if (_audioDevices[i] == gConfigSound.Device)
|
|
{
|
|
_currentAudioDevice = i;
|
|
}
|
|
}
|
|
}
|
|
LoadAudioObjects();
|
|
}
|
|
|
|
void LoadAudioObjects()
|
|
{
|
|
auto& objManager = GetContext()->GetObjectManager();
|
|
|
|
Object* baseAudio = objManager.LoadObject(AudioObjectIdentifiers::kRCT2);
|
|
if (baseAudio != nullptr)
|
|
{
|
|
_soundsAudioObjectEntryIndex = objManager.GetLoadedObjectEntryIndex(baseAudio);
|
|
}
|
|
|
|
objManager.LoadObject(AudioObjectIdentifiers::kOpenRCT2Additional);
|
|
_soundsAdditionalAudioObjectEntryIndex = objManager.GetLoadedObjectEntryIndex(
|
|
AudioObjectIdentifiers::kOpenRCT2Additional);
|
|
objManager.LoadObject(AudioObjectIdentifiers::kRCT2Circus);
|
|
}
|
|
|
|
void PopulateDevices()
|
|
{
|
|
auto audioContext = OpenRCT2::GetContext()->GetAudioContext();
|
|
std::vector<std::string> devices = audioContext->GetOutputDevices();
|
|
|
|
// Replace blanks with localised unknown string
|
|
for (auto& device : devices)
|
|
{
|
|
if (device.empty())
|
|
{
|
|
device = LanguageGetString(STR_OPTIONS_SOUND_VALUE_DEFAULT);
|
|
}
|
|
}
|
|
|
|
#ifndef __linux__
|
|
// The first device is always system default on Windows and macOS
|
|
std::string defaultDevice = LanguageGetString(STR_OPTIONS_SOUND_VALUE_DEFAULT);
|
|
devices.insert(devices.begin(), defaultDevice);
|
|
#endif
|
|
|
|
_audioDevices = devices;
|
|
}
|
|
|
|
/**
|
|
* Returns the audio parameters to use when playing the specified sound at a virtual location.
|
|
* @param soundId The sound effect to be played.
|
|
* @param location The location at which the sound effect is to be played.
|
|
* @return The audio parameters to be used when playing this sound effect.
|
|
*/
|
|
static AudioParams GetParametersFromLocation(AudioObject* obj, uint32_t sampleIndex, const CoordsXYZ& location)
|
|
{
|
|
int32_t volumeDown = 0;
|
|
AudioParams params;
|
|
params.in_range = true;
|
|
params.volume = 0;
|
|
params.pan = 0;
|
|
|
|
auto element = MapGetSurfaceElementAt(location);
|
|
if (element != nullptr && (element->GetBaseZ()) - 5 > location.z)
|
|
{
|
|
volumeDown = 10;
|
|
}
|
|
|
|
uint8_t rotation = GetCurrentRotation();
|
|
auto pos2 = Translate3DTo2DWithZ(rotation, location);
|
|
|
|
Viewport* viewport = nullptr;
|
|
while ((viewport = WindowGetPreviousViewport(viewport)) != nullptr)
|
|
{
|
|
if (viewport->flags & VIEWPORT_FLAG_SOUND_ON)
|
|
{
|
|
int16_t vx = pos2.x - viewport->viewPos.x;
|
|
params.pan = viewport->pos.x + viewport->zoom.ApplyInversedTo(vx);
|
|
|
|
auto sampleModifier = obj->GetSampleModifier(sampleIndex);
|
|
auto viewModifier = ((viewport->zoom.ApplyTo(-1024) - 1) * (1 << volumeDown)) + 1;
|
|
params.volume = sampleModifier + viewModifier;
|
|
|
|
if (!viewport->Contains(pos2) || params.volume < -10000)
|
|
{
|
|
params.in_range = false;
|
|
return params;
|
|
}
|
|
}
|
|
}
|
|
|
|
return params;
|
|
}
|
|
|
|
static std::tuple<AudioObject*, uint32_t> GetAudioObjectAndSampleIndex(SoundId id)
|
|
{
|
|
auto& objManager = GetContext()->GetObjectManager();
|
|
AudioObject* audioObject{};
|
|
uint32_t sampleIndex = EnumValue(id);
|
|
if (id >= SoundId::LiftRMC)
|
|
{
|
|
audioObject = static_cast<AudioObject*>(
|
|
objManager.GetLoadedObject(ObjectType::Audio, _soundsAdditionalAudioObjectEntryIndex));
|
|
sampleIndex -= EnumValue(SoundId::LiftRMC);
|
|
}
|
|
else
|
|
{
|
|
audioObject = static_cast<AudioObject*>(
|
|
objManager.GetLoadedObject(ObjectType::Audio, _soundsAudioObjectEntryIndex));
|
|
}
|
|
return std::make_tuple(audioObject, sampleIndex);
|
|
}
|
|
|
|
static void Play(IAudioSource* audioSource, int32_t volume, int32_t pan)
|
|
{
|
|
int32_t mixerPan = 0;
|
|
if (pan != AUDIO_PLAY_AT_CENTRE)
|
|
{
|
|
int32_t x2 = pan << 16;
|
|
uint16_t screenWidth = std::max<int32_t>(64, OpenRCT2::GetContext()->GetUiContext()->GetWidth());
|
|
mixerPan = ((x2 / screenWidth) - 0x8000) >> 4;
|
|
}
|
|
|
|
CreateAudioChannel(audioSource, MixerGroup::Sound, false, DStoMixerVolume(volume), DStoMixerPan(mixerPan), 1, true);
|
|
}
|
|
|
|
void Play3D(SoundId soundId, const CoordsXYZ& loc)
|
|
{
|
|
if (!IsAvailable())
|
|
return;
|
|
|
|
// Get sound from base object
|
|
auto [baseAudioObject, sampleIndex] = GetAudioObjectAndSampleIndex(soundId);
|
|
if (baseAudioObject != nullptr)
|
|
{
|
|
auto params = GetParametersFromLocation(baseAudioObject, sampleIndex, loc);
|
|
if (params.in_range)
|
|
{
|
|
auto source = baseAudioObject->GetSample(sampleIndex);
|
|
if (source != nullptr)
|
|
{
|
|
Play(source, params.volume, params.pan);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Play(SoundId soundId, int32_t volume, int32_t pan)
|
|
{
|
|
if (!IsAvailable())
|
|
return;
|
|
|
|
// Get sound from base object
|
|
auto [baseAudioObject, sampleIndex] = GetAudioObjectAndSampleIndex(soundId);
|
|
if (baseAudioObject != nullptr)
|
|
{
|
|
auto source = baseAudioObject->GetSample(sampleIndex);
|
|
if (source != nullptr)
|
|
{
|
|
Play(source, volume, pan);
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool IsRCT1TitleMusicAvailable()
|
|
{
|
|
auto env = GetContext()->GetPlatformEnvironment();
|
|
auto rct1path = env->GetDirectoryPath(DIRBASE::RCT1);
|
|
return !rct1path.empty();
|
|
}
|
|
|
|
static std::map<TitleMusicKind, std::string_view> GetAvailableMusicMap()
|
|
{
|
|
auto musicMap = std::map<TitleMusicKind, std::string_view>{
|
|
{ TitleMusicKind::OpenRCT2, AudioObjectIdentifiers::kOpenRCT2Title },
|
|
{ TitleMusicKind::RCT2, AudioObjectIdentifiers::kRCT2Title },
|
|
};
|
|
|
|
if (IsRCT1TitleMusicAvailable())
|
|
{
|
|
musicMap.emplace(TitleMusicKind::RCT1, AudioObjectIdentifiers::kRCT1Title);
|
|
}
|
|
|
|
return musicMap;
|
|
}
|
|
|
|
static ObjectEntryDescriptor GetTitleMusicDescriptor(TitleMusicKind musicKind)
|
|
{
|
|
auto musicMap = GetAvailableMusicMap();
|
|
auto it = musicMap.find(musicKind);
|
|
if (musicKind == TitleMusicKind::Random)
|
|
{
|
|
it = std::next(musicMap.begin(), UtilRand() % musicMap.size());
|
|
}
|
|
|
|
if (it != musicMap.end())
|
|
{
|
|
return ObjectEntryDescriptor(ObjectType::Audio, it->second);
|
|
}
|
|
|
|
// No music descriptor for the current setting, intentional for TitleMusicKind::None
|
|
return {};
|
|
}
|
|
|
|
void PlayTitleMusic()
|
|
{
|
|
if (gGameSoundsOff || !(gScreenFlags & SCREEN_FLAGS_TITLE_DEMO) || IntroIsPlaying())
|
|
{
|
|
StopTitleMusic();
|
|
return;
|
|
}
|
|
|
|
if (_titleMusicChannel != nullptr && !_titleMusicChannel->IsDone())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Load title sequence audio object
|
|
auto descriptor = GetTitleMusicDescriptor(gConfigSound.TitleMusic);
|
|
auto& objManager = GetContext()->GetObjectManager();
|
|
auto* audioObject = static_cast<AudioObject*>(objManager.LoadObject(descriptor));
|
|
if (audioObject != nullptr)
|
|
{
|
|
_titleAudioObjectEntryIndex = objManager.GetLoadedObjectEntryIndex(audioObject);
|
|
|
|
// Play first sample from object
|
|
auto source = audioObject->GetSample(0);
|
|
if (source != nullptr)
|
|
{
|
|
_titleMusicChannel = CreateAudioChannel(source, MixerGroup::TitleMusic, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void StopAll()
|
|
{
|
|
StopTitleMusic();
|
|
StopVehicleSounds();
|
|
RideAudio::StopAllChannels();
|
|
PeepStopCrowdNoise();
|
|
ClimateStopWeatherSound();
|
|
}
|
|
|
|
int32_t GetDeviceCount()
|
|
{
|
|
return static_cast<int32_t>(_audioDevices.size());
|
|
}
|
|
|
|
const std::string& GetDeviceName(int32_t index)
|
|
{
|
|
if (index < 0 || index >= GetDeviceCount())
|
|
{
|
|
static std::string InvalidDevice = "Invalid Device";
|
|
return InvalidDevice;
|
|
}
|
|
return _audioDevices[index];
|
|
}
|
|
|
|
int32_t GetCurrentDeviceIndex()
|
|
{
|
|
return _currentAudioDevice;
|
|
}
|
|
|
|
void StopTitleMusic()
|
|
{
|
|
if (_titleMusicChannel != nullptr)
|
|
{
|
|
_titleMusicChannel->Stop();
|
|
_titleMusicChannel = nullptr;
|
|
}
|
|
|
|
// Unload the audio object
|
|
if (_titleAudioObjectEntryIndex != OBJECT_ENTRY_INDEX_NULL)
|
|
{
|
|
auto& objManager = GetContext()->GetObjectManager();
|
|
auto* obj = objManager.GetLoadedObject(ObjectType::Audio, _titleAudioObjectEntryIndex);
|
|
if (obj != nullptr)
|
|
{
|
|
objManager.UnloadObjects({ obj->GetDescriptor() });
|
|
}
|
|
_titleAudioObjectEntryIndex = OBJECT_ENTRY_INDEX_NULL;
|
|
}
|
|
}
|
|
|
|
void InitRideSoundsAndInfo()
|
|
{
|
|
InitRideSounds(0);
|
|
}
|
|
|
|
void InitRideSounds(int32_t device)
|
|
{
|
|
Close();
|
|
for (auto& vehicleSound : gVehicleSoundList)
|
|
{
|
|
vehicleSound.id = kSoundIdNull;
|
|
}
|
|
|
|
_currentAudioDevice = device;
|
|
ConfigSaveDefault();
|
|
}
|
|
|
|
void Close()
|
|
{
|
|
PeepStopCrowdNoise();
|
|
StopTitleMusic();
|
|
RideAudio::StopAllChannels();
|
|
ClimateStopWeatherSound();
|
|
_currentAudioDevice = -1;
|
|
}
|
|
|
|
void ToggleAllSounds()
|
|
{
|
|
gConfigSound.MasterSoundEnabled = !gConfigSound.MasterSoundEnabled;
|
|
if (gConfigSound.MasterSoundEnabled)
|
|
{
|
|
Resume();
|
|
}
|
|
else
|
|
{
|
|
Pause();
|
|
}
|
|
|
|
WindowInvalidateByClass(WindowClass::Options);
|
|
}
|
|
|
|
void Pause()
|
|
{
|
|
gGameSoundsOff = true;
|
|
StopVehicleSounds();
|
|
RideAudio::StopAllChannels();
|
|
PeepStopCrowdNoise();
|
|
ClimateStopWeatherSound();
|
|
StopTitleMusic();
|
|
}
|
|
|
|
void Resume()
|
|
{
|
|
gGameSoundsOff = false;
|
|
PlayTitleMusic();
|
|
}
|
|
|
|
void StopVehicleSounds()
|
|
{
|
|
if (!IsAvailable())
|
|
return;
|
|
|
|
for (auto& vehicleSound : gVehicleSoundList)
|
|
{
|
|
if (vehicleSound.id != kSoundIdNull)
|
|
{
|
|
vehicleSound.id = kSoundIdNull;
|
|
if (vehicleSound.TrackSound.Id != SoundId::Null)
|
|
{
|
|
vehicleSound.TrackSound.Channel->Stop();
|
|
}
|
|
if (vehicleSound.OtherSound.Id != SoundId::Null)
|
|
{
|
|
vehicleSound.OtherSound.Channel->Stop();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static IAudioMixer* GetMixer()
|
|
{
|
|
auto audioContext = GetContext()->GetAudioContext();
|
|
return audioContext->GetMixer();
|
|
}
|
|
|
|
std::shared_ptr<IAudioChannel> CreateAudioChannel(
|
|
SoundId id, bool loop, int32_t volume, float pan, double rate, bool forget)
|
|
{
|
|
// Get sound from base object
|
|
auto [baseAudioObject, sampleIndex] = GetAudioObjectAndSampleIndex(id);
|
|
if (baseAudioObject != nullptr)
|
|
{
|
|
auto source = baseAudioObject->GetSample(sampleIndex);
|
|
if (source != nullptr)
|
|
{
|
|
return CreateAudioChannel(source, MixerGroup::Sound, loop, volume, pan, rate, forget);
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::shared_ptr<IAudioChannel> CreateAudioChannel(
|
|
IAudioSource* source, MixerGroup group, bool loop, int32_t volume, float pan, double rate, bool forget)
|
|
{
|
|
auto* mixer = GetMixer();
|
|
if (mixer == nullptr)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
mixer->Lock();
|
|
auto channel = mixer->Play(source, loop ? kMixerLoopInfinite : kMixerLoopNone, forget);
|
|
if (channel != nullptr)
|
|
{
|
|
channel->SetGroup(group);
|
|
channel->SetVolume(volume);
|
|
channel->SetPan(pan);
|
|
channel->SetRate(rate);
|
|
channel->UpdateOldVolume();
|
|
}
|
|
mixer->Unlock();
|
|
return channel;
|
|
}
|
|
|
|
int32_t DStoMixerVolume(int32_t volume)
|
|
{
|
|
return static_cast<int32_t>(kMixerVolumeMax * (std::pow(10.0f, static_cast<float>(volume) / 2000)));
|
|
}
|
|
|
|
float DStoMixerPan(int32_t pan)
|
|
{
|
|
constexpr int32_t DSBPAN_LEFT = -10000;
|
|
constexpr int32_t DSBPAN_RIGHT = 10000;
|
|
return ((static_cast<float>(pan) + -DSBPAN_LEFT) / DSBPAN_RIGHT) / 2;
|
|
}
|
|
|
|
double DStoMixerRate(int32_t frequency)
|
|
{
|
|
return static_cast<double>(frequency) / 22050;
|
|
}
|
|
|
|
} // namespace OpenRCT2::Audio
|