Line data Source code
1 : /* Copyright (C) 2021 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "SoundGroup.h"
21 : #include "graphics/Camera.h"
22 : #include "graphics/GameView.h"
23 : #include "lib/rand.h"
24 : #include "ps/CLogger.h"
25 : #include "ps/ConfigDB.h"
26 : #include "ps/CStr.h"
27 : #include "ps/Filesystem.h"
28 : #include "ps/Game.h"
29 : #include "ps/Util.h"
30 : #include "ps/XML/Xeromyces.h"
31 : #include "simulation2/components/ICmpVisual.h"
32 : #include "simulation2/system/Component.h"
33 : #include "soundmanager/items/ISoundItem.h"
34 : #include "soundmanager/SoundManager.h"
35 :
36 : #include <algorithm>
37 : #include <random>
38 :
39 : extern CGame *g_Game;
40 :
41 : #if CONFIG2_AUDIO
42 :
43 : constexpr ALfloat DEFAULT_ROLLOFF = 0.5f;
44 : constexpr ALfloat MAX_ROLLOFF = 0.7f;
45 :
46 : /**
47 : * Low randomness, quite-a-lot-faster-than-std::mt19937 random number generator.
48 : * It matches the interface of UniformRandomBitGenerator for use in std::shuffle.
49 : */
50 : class CFastRand
51 : {
52 : public:
53 : using result_type = u32;
54 :
55 : constexpr static result_type min() { return 0; }
56 0 : constexpr static result_type max() { return 0xFFFF; }
57 :
58 0 : static result_type Rand(result_type& seed)
59 : {
60 : // This is a mixed linear congruential random number generator.
61 : // The magic numbers are chosen so that they generate pseudo random numbers over a big enough period (0xFFFF).
62 0 : seed = 214013 * seed + 2531011;
63 0 : return (seed >> 16) & max();
64 : }
65 :
66 0 : static float RandFloat(result_type& seed, float min, float max)
67 : {
68 0 : return (static_cast<float>(Rand(seed)) / (0xFFFF)) * (max - min) + min;
69 : }
70 :
71 : CFastRand() {};
72 0 : CFastRand(result_type init) : m_Seed(init) {};
73 :
74 0 : result_type operator()()
75 : {
76 0 : return Rand(m_Seed);
77 : }
78 :
79 : result_type m_Seed;
80 : };
81 : #endif
82 :
83 0 : void CSoundGroup::SetGain(float gain)
84 : {
85 0 : m_Gain = std::min(gain, 1.0f);
86 0 : }
87 :
88 0 : void CSoundGroup::SetDefaultValues()
89 : {
90 0 : m_CurrentSoundIndex = 0;
91 0 : m_Flags = 0;
92 0 : m_CurTime = 0.f;
93 0 : m_Gain = 0.7f;
94 0 : m_Pitch = 1.f;
95 0 : m_Priority = 60.f;
96 0 : m_PitchUpper = 1.1f;
97 0 : m_PitchLower = 0.9f;
98 0 : m_GainUpper = 1.f;
99 0 : m_GainLower = 0.8f;
100 0 : m_ConeOuterGain = 0.f;
101 0 : m_ConeInnerAngle = 360.f;
102 0 : m_ConeOuterAngle = 360.f;
103 0 : m_Decay = 3.f;
104 0 : m_Seed = 0;
105 0 : m_IntensityThreshold = 3.f;
106 :
107 0 : m_MinDist = 1.f;
108 0 : m_MaxDist = 350.f;
109 : // This is more than the default camera FOV: for now, our soundscape is not realistic anyways.
110 0 : m_MaxStereoAngle = static_cast<float>(M_PI / 6);
111 0 : if (CConfigDB::IsInitialised())
112 : {
113 0 : CFG_GET_VAL("sound.mindistance", m_MinDist);
114 0 : CFG_GET_VAL("sound.maxdistance", m_MaxDist);
115 0 : CFG_GET_VAL("sound.maxstereoangle", m_MaxStereoAngle);
116 : }
117 0 : }
118 :
119 0 : CSoundGroup::CSoundGroup()
120 : {
121 0 : SetDefaultValues();
122 0 : }
123 :
124 0 : CSoundGroup::CSoundGroup(const VfsPath& pathnameXML)
125 : {
126 0 : SetDefaultValues();
127 0 : LoadSoundGroup(pathnameXML);
128 0 : }
129 :
130 0 : CSoundGroup::~CSoundGroup()
131 : {
132 : // clean up all the handles from this group.
133 0 : ReleaseGroup();
134 0 : }
135 :
136 0 : float CSoundGroup::RadiansOffCenter(const CVector3D& position, bool& onScreen, float& itemRollOff)
137 : {
138 : #if !CONFIG2_AUDIO
139 : UNUSED2(position);
140 : UNUSED2(onScreen);
141 : UNUSED2(itemRollOff);
142 : return 0.f;
143 : #else
144 0 : const int screenWidth = g_Game->GetView()->GetCamera()->GetViewPort().m_Width;
145 0 : const int screenHeight = g_Game->GetView()->GetCamera()->GetViewPort().m_Height;
146 0 : const float xBufferSize = screenWidth * 0.1f;
147 0 : const float yBufferSize = 15.f;
148 0 : const float radianCap = m_MaxStereoAngle;
149 :
150 : float x, y;
151 0 : g_Game->GetView()->GetCamera()->GetScreenCoordinates(position, x, y);
152 :
153 0 : onScreen = true;
154 0 : float answer = 0.f;
155 0 : if (x < -xBufferSize)
156 : {
157 0 : onScreen = false;
158 0 : answer = -radianCap;
159 : }
160 0 : else if (x > screenWidth + xBufferSize)
161 : {
162 0 : onScreen = false;
163 0 : answer = radianCap;
164 : }
165 : else
166 : {
167 0 : if (x < 0 || x > screenWidth)
168 0 : itemRollOff = MAX_ROLLOFF;
169 :
170 0 : answer = radianCap * (x * 2 / screenWidth - 1);
171 : }
172 :
173 0 : if (y < -yBufferSize)
174 : {
175 0 : onScreen = false;
176 : }
177 0 : else if (y > screenHeight + yBufferSize)
178 : {
179 0 : onScreen = false;
180 : }
181 0 : else if (y < 0 || y > screenHeight)
182 : {
183 0 : itemRollOff = MAX_ROLLOFF;
184 : }
185 :
186 0 : return answer;
187 : #endif // !CONFIG2_AUDIO
188 : }
189 :
190 0 : void CSoundGroup::UploadPropertiesAndPlay(size_t index, const CVector3D& position, entity_id_t source)
191 : {
192 : #if !CONFIG2_AUDIO
193 : UNUSED2(index);
194 : UNUSED2(position);
195 : UNUSED2(source);
196 : #else
197 0 : if (!g_SoundManager)
198 0 : return;
199 :
200 0 : bool isOnscreen = false;
201 0 : ALfloat itemRollOff = DEFAULT_ROLLOFF;
202 0 : float offset = RadiansOffCenter(position, isOnscreen, itemRollOff);
203 :
204 0 : if (!isOnscreen && !TestFlag(eDistanceless) && !TestFlag(eOmnipresent))
205 0 : return;
206 :
207 0 : if (m_SoundGroups.empty())
208 0 : Reload();
209 :
210 0 : if (m_SoundGroups.size() <= index)
211 0 : return;
212 :
213 0 : CSoundData* sndData = m_SoundGroups[index];
214 0 : if (!sndData)
215 0 : return;
216 :
217 0 : ISoundItem* hSound = static_cast<CSoundManager*>(g_SoundManager)->ItemForEntity(source, sndData);
218 0 : if (!hSound)
219 0 : return;
220 :
221 0 : if (!TestFlag(eOmnipresent))
222 : {
223 0 : CVector3D origin = g_Game->GetView()->GetCamera()->GetOrientation().GetTranslation();
224 0 : float itemDist = (position - origin).Length();
225 :
226 0 : if (TestFlag(eDistanceless))
227 0 : itemRollOff = 0;
228 :
229 0 : if (sndData->IsStereo())
230 0 : LOGWARNING("OpenAL: stereo sounds can't be positioned: %s", sndData->GetFileName().string8());
231 :
232 0 : hSound->SetLocation(CVector3D(itemDist * sin(offset), 0, -itemDist * cos(offset)));
233 0 : hSound->SetRollOff(itemRollOff, m_MinDist, m_MaxDist);
234 : }
235 :
236 0 : CmpPtr<ICmpVisual> cmpVisual(*g_Game->GetSimulation2(), source);
237 0 : if (cmpVisual)
238 0 : m_Seed = cmpVisual->GetActorSeed();
239 :
240 0 : hSound->SetPitch(TestFlag(eRandPitch) ? CFastRand::RandFloat(m_Seed, m_PitchLower, m_PitchUpper) : m_Pitch);
241 :
242 0 : if (TestFlag(eRandGain))
243 0 : m_Gain = CFastRand::RandFloat(m_Seed, m_GainLower, m_GainUpper);
244 :
245 0 : hSound->SetCone(m_ConeInnerAngle, m_ConeOuterAngle, m_ConeOuterGain);
246 0 : static_cast<CSoundManager*>(g_SoundManager)->PlayGroupItem(hSound, m_Gain);
247 : #endif // !CONFIG2_AUDIO
248 : }
249 :
250 0 : static void HandleError(const std::wstring& message, const VfsPath& pathname, Status err)
251 : {
252 : // Open failed because sound is disabled (don't log this)
253 0 : if (err == ERR::AGAIN)
254 0 : return;
255 :
256 0 : LOGERROR("%s: pathname=%s, error=%s", utf8_from_wstring(message), pathname.string8(), GetStatusAsString(err).c_str());
257 : }
258 :
259 0 : void CSoundGroup::PlayNext(const CVector3D& position, entity_id_t source)
260 : {
261 0 : if (m_Filenames.empty())
262 0 : return;
263 :
264 0 : m_CurrentSoundIndex = rand(0, m_Filenames.size());
265 0 : UploadPropertiesAndPlay(m_CurrentSoundIndex, position, source);
266 : }
267 :
268 0 : void CSoundGroup::Reload()
269 : {
270 0 : m_CurrentSoundIndex = 0;
271 : #if CONFIG2_AUDIO
272 0 : ReleaseGroup();
273 :
274 0 : if (!g_SoundManager)
275 0 : return;
276 :
277 0 : for (const std::wstring& filename : m_Filenames)
278 : {
279 0 : VfsPath absolutePath = m_Filepath / filename;
280 0 : CSoundData* itemData = CSoundData::SoundDataFromFile(absolutePath);
281 0 : if (!itemData)
282 0 : HandleError(L"error loading sound", absolutePath, ERR::FAIL);
283 : else
284 0 : m_SoundGroups.push_back(itemData->IncrementCount());
285 : }
286 :
287 0 : if (TestFlag(eRandOrder))
288 0 : std::shuffle(m_SoundGroups.begin(), m_SoundGroups.end(), CFastRand(m_Seed));
289 : #endif
290 : }
291 :
292 0 : void CSoundGroup::ReleaseGroup()
293 : {
294 : #if CONFIG2_AUDIO
295 0 : for (CSoundData* soundGroup : m_SoundGroups)
296 0 : CSoundData::ReleaseSoundData(soundGroup);
297 :
298 0 : m_SoundGroups.clear();
299 : #endif
300 0 : }
301 :
302 0 : void CSoundGroup::Update(float UNUSED(TimeSinceLastFrame))
303 : {
304 0 : }
305 :
306 0 : bool CSoundGroup::LoadSoundGroup(const VfsPath& pathnameXML)
307 : {
308 0 : CXeromyces XeroFile;
309 0 : if (XeroFile.Load(g_VFS, pathnameXML, "sound_group") != PSRETURN_OK)
310 : {
311 0 : HandleError(L"error loading file", pathnameXML, ERR::FAIL);
312 0 : return false;
313 : }
314 :
315 : #define EL(x) int el_##x = XeroFile.GetElementID(#x)
316 0 : EL(soundgroup);
317 0 : EL(gain);
318 0 : EL(looping);
319 0 : EL(omnipresent);
320 0 : EL(heardby);
321 0 : EL(distanceless);
322 0 : EL(pitch);
323 0 : EL(priority);
324 0 : EL(randorder);
325 0 : EL(randgain);
326 0 : EL(randpitch);
327 0 : EL(conegain);
328 0 : EL(coneinner);
329 0 : EL(coneouter);
330 0 : EL(sound);
331 0 : EL(gainupper);
332 0 : EL(gainlower);
333 0 : EL(pitchupper);
334 0 : EL(pitchlower);
335 0 : EL(path);
336 0 : EL(threshold);
337 0 : EL(decay);
338 : #undef EL
339 :
340 0 : XMBElement root = XeroFile.GetRoot();
341 :
342 0 : if (root.GetNodeName() != el_soundgroup)
343 : {
344 0 : LOGERROR("Invalid SoundGroup format (unrecognised root element '%s')", XeroFile.GetElementString(root.GetNodeName()));
345 0 : return false;
346 : }
347 :
348 0 : XERO_ITER_EL(root, child)
349 : {
350 0 : int child_name = child.GetNodeName();
351 :
352 0 : if (child_name == el_gain)
353 : {
354 0 : SetGain(child.GetText().ToFloat());
355 : }
356 0 : else if (child_name == el_looping)
357 : {
358 0 : if (child.GetText().ToInt() == 1)
359 0 : SetFlag(eLoop);
360 : }
361 0 : else if (child_name == el_omnipresent)
362 : {
363 0 : if (child.GetText().ToInt() == 1)
364 0 : SetFlag(eOmnipresent);
365 : }
366 0 : else if (child_name == el_heardby)
367 : {
368 0 : if (child.GetText().FindInsensitive("owner") == 0)
369 0 : SetFlag(eOwnerOnly);
370 : }
371 0 : else if (child_name == el_distanceless)
372 : {
373 0 : if (child.GetText().ToInt() == 1)
374 0 : SetFlag(eDistanceless);
375 : }
376 0 : else if (child_name == el_pitch)
377 : {
378 0 : this->m_Pitch = child.GetText().ToFloat();
379 : }
380 0 : else if (child_name == el_priority)
381 : {
382 0 : this->m_Priority = child.GetText().ToFloat();
383 : }
384 0 : else if (child_name == el_randorder)
385 : {
386 0 : if (child.GetText().ToInt() == 1)
387 0 : SetFlag(eRandOrder);
388 : }
389 0 : else if (child_name == el_randgain)
390 : {
391 0 : if (child.GetText().ToInt() == 1)
392 0 : SetFlag(eRandGain);
393 : }
394 0 : else if (child_name == el_gainupper)
395 : {
396 0 : this->m_GainUpper = child.GetText().ToFloat();
397 : }
398 0 : else if (child_name == el_gainlower)
399 : {
400 0 : this->m_GainLower = child.GetText().ToFloat();
401 : }
402 0 : else if (child_name == el_randpitch)
403 : {
404 0 : if (child.GetText().ToInt() == 1)
405 0 : SetFlag(eRandPitch);
406 : }
407 0 : else if (child_name == el_pitchupper)
408 : {
409 0 : this->m_PitchUpper = child.GetText().ToFloat();
410 : }
411 0 : else if (child_name == el_pitchlower)
412 : {
413 0 : this->m_PitchLower = child.GetText().ToFloat();
414 : }
415 0 : else if (child_name == el_conegain)
416 : {
417 0 : this->m_ConeOuterGain = child.GetText().ToFloat();
418 : }
419 0 : else if (child_name == el_coneinner)
420 : {
421 0 : this->m_ConeInnerAngle = child.GetText().ToFloat();
422 : }
423 0 : else if (child_name == el_coneouter)
424 : {
425 0 : this->m_ConeOuterAngle = child.GetText().ToFloat();
426 : }
427 0 : else if (child_name == el_sound)
428 : {
429 0 : this->m_Filenames.push_back(child.GetText().FromUTF8());
430 : }
431 0 : else if (child_name == el_path)
432 : {
433 0 : m_Filepath = child.GetText().FromUTF8();
434 : }
435 0 : else if (child_name == el_threshold)
436 : {
437 0 : m_IntensityThreshold = child.GetText().ToFloat();
438 : }
439 0 : else if (child_name == el_decay)
440 : {
441 0 : m_Decay = child.GetText().ToFloat();
442 : }
443 : }
444 0 : return true;
445 3 : }
|