1 /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */
2
3 #include "Sound.h"
4
5 #include <cstdlib>
6 #include <cmath>
7 #include <alc.h>
8 #ifndef ALC_ALL_DEVICES_SPECIFIER
9 //needed for ALC_ALL_DEVICES_SPECIFIER on some special *nix
10 #include <alext.h>
11 #endif
12 #include <boost/cstdint.hpp>
13 #include <boost/thread/thread.hpp>
14
15 #include "System/Sound/ISoundChannels.h"
16 #include "System/Sound/SoundLog.h"
17 #include "SoundSource.h"
18 #include "SoundBuffer.h"
19 #include "SoundItem.h"
20 #include "ALShared.h"
21 #include "EFX.h"
22 #include "EFXPresets.h"
23
24 #include "System/TimeProfiler.h"
25 #include "System/Config/ConfigHandler.h"
26 #include "System/Exceptions.h"
27 #include "System/FileSystem/FileHandler.h"
28 #include "Lua/LuaParser.h"
29 #include "Map/Ground.h"
30 #include "Sim/Misc/GlobalConstants.h"
31 #include "System/myMath.h"
32 #include "System/Util.h"
33 #include "System/Platform/Watchdog.h"
34
35 #include "System/float3.h"
36
37 CONFIG(int, MaxSounds).defaultValue(128).minimumValue(0).description("Maximum parallel played sounds.");
38 CONFIG(bool, PitchAdjust).defaultValue(false).description("When enabled adjust sound speed/pitch to game speed.");
39 CONFIG(int, snd_volmaster).defaultValue(60).minimumValue(0).maximumValue(200).description("Master sound volume.");
40 CONFIG(int, snd_volgeneral).defaultValue(100).minimumValue(0).maximumValue(200).description("Volume for \"general\" sound channel.");
41 CONFIG(int, snd_volunitreply).defaultValue(100).minimumValue(0).maximumValue(200).description("Volume for \"unit reply\" sound channel.");
42 CONFIG(int, snd_volbattle).defaultValue(100).minimumValue(0).maximumValue(200).description("Volume for \"battle\" sound channel.");
43 CONFIG(int, snd_volui).defaultValue(100).minimumValue(0).maximumValue(200).description("Volume for \"ui\" sound channel.");
44 CONFIG(int, snd_volmusic).defaultValue(100).minimumValue(0).maximumValue(200).description("Volume for \"music\" sound channel.");
45 CONFIG(std::string, snd_device).defaultValue("").description("Sets the used output device. See \"Available Devices\" section in infolog.txt.");
46
47 boost::recursive_mutex soundMutex;
48
49
CSound()50 CSound::CSound()
51 : myPos(ZeroVector)
52 , prevVelocity(ZeroVector)
53 , soundThread(NULL)
54 , soundThreadQuit(false)
55 {
56 boost::recursive_mutex::scoped_lock lck(soundMutex);
57 mute = false;
58 appIsIconified = false;
59 int maxSounds = configHandler->GetInt("MaxSounds");
60 pitchAdjust = configHandler->GetBool("PitchAdjust");
61
62 masterVolume = configHandler->GetInt("snd_volmaster") * 0.01f;
63 Channels::General->SetVolume(configHandler->GetInt("snd_volgeneral") * 0.01f);
64 Channels::UnitReply->SetVolume(configHandler->GetInt("snd_volunitreply") * 0.01f);
65 Channels::UnitReply->SetMaxConcurrent(1);
66 Channels::UnitReply->SetMaxEmmits(1);
67 Channels::Battle->SetVolume(configHandler->GetInt("snd_volbattle") * 0.01f);
68 Channels::UserInterface->SetVolume(configHandler->GetInt("snd_volui") * 0.01f);
69 Channels::BGMusic->SetVolume(configHandler->GetInt("snd_volmusic") * 0.01f);
70
71 SoundBuffer::Initialise();
72 soundItemDef temp;
73 temp["name"] = "EmptySource";
74 sounds.push_back(NULL);
75
76 if (maxSounds <= 0) {
77 LOG_L(L_WARNING, "MaxSounds set to 0, sound is disabled");
78 } else {
79 soundThread = new boost::thread(boost::bind(&CSound::StartThread, this, maxSounds));
80 }
81
82 configHandler->NotifyOnChange(this);
83 }
84
~CSound()85 CSound::~CSound()
86 {
87 soundThreadQuit = true;
88 configHandler->RemoveObserver(this);
89
90 LOG_L(L_INFO, "[%s][1] soundThread=%p", __FUNCTION__, soundThread);
91
92 if (soundThread != NULL) {
93 soundThread->join();
94 delete soundThread;
95 soundThread = NULL;
96 }
97
98 LOG_L(L_INFO, "[%s][2]", __FUNCTION__);
99
100 for (soundVecT::iterator it = sounds.begin(); it != sounds.end(); ++it)
101 delete *it;
102
103 sounds.clear();
104 SoundBuffer::Deinitialise();
105
106 LOG_L(L_INFO, "[%s][3]", __FUNCTION__);
107 }
108
HasSoundItem(const std::string & name) const109 bool CSound::HasSoundItem(const std::string& name) const
110 {
111 soundMapT::const_iterator it = soundMap.find(name);
112 if (it != soundMap.end())
113 {
114 return true;
115 }
116 else
117 {
118 soundItemDefMap::const_iterator it = soundItemDefs.find(StringToLower(name));
119 if (it != soundItemDefs.end())
120 return true;
121 else
122 return false;
123 }
124 }
125
GetSoundId(const std::string & name)126 size_t CSound::GetSoundId(const std::string& name)
127 {
128 boost::recursive_mutex::scoped_lock lck(soundMutex);
129
130 if (sources.empty())
131 return 0;
132
133 soundMapT::const_iterator it = soundMap.find(name);
134 if (it != soundMap.end()) {
135 // sounditem found
136 return it->second;
137 } else {
138 soundItemDefMap::const_iterator itemDefIt = soundItemDefs.find(StringToLower(name));
139 if (itemDefIt != soundItemDefs.end()) {
140 return MakeItemFromDef(itemDefIt->second);
141 } else {
142 if (LoadSoundBuffer(name) > 0) // maybe raw filename?
143 {
144 soundItemDef temp = defaultItem;
145 temp["file"] = name;
146 return MakeItemFromDef(temp);
147 } else {
148 LOG_L(L_ERROR, "CSound::GetSoundId: could not find sound: %s", name.c_str());
149 return 0;
150 }
151 }
152 }
153 }
154
GetSoundItem(size_t id) const155 SoundItem* CSound::GetSoundItem(size_t id) const {
156 //! id==0 is a special id and invalid
157 if (id == 0 || id >= sounds.size())
158 return NULL;
159 return sounds[id];
160 }
161
GetNextBestSource(bool lock)162 CSoundSource* CSound::GetNextBestSource(bool lock)
163 {
164 boost::recursive_mutex::scoped_lock lck(soundMutex, boost::defer_lock);
165 if (lock)
166 lck.lock();
167
168 if (sources.empty())
169 return NULL;
170
171 CSoundSource* bestPos = NULL;
172 for (sourceVecT::iterator it = sources.begin(); it != sources.end(); ++it)
173 {
174 if (!it->IsPlaying())
175 {
176 return &(*it);
177 }
178 else if (it->GetCurrentPriority() <= (bestPos ? bestPos->GetCurrentPriority() : INT_MAX))
179 {
180 bestPos = &(*it);
181 }
182 }
183 return bestPos;
184 }
185
PitchAdjust(const float newPitch)186 void CSound::PitchAdjust(const float newPitch)
187 {
188 boost::recursive_mutex::scoped_lock lck(soundMutex);
189 if (pitchAdjust)
190 CSoundSource::SetPitch(newPitch);
191 }
192
ConfigNotify(const std::string & key,const std::string & value)193 void CSound::ConfigNotify(const std::string& key, const std::string& value)
194 {
195 boost::recursive_mutex::scoped_lock lck(soundMutex);
196 if (key == "snd_volmaster")
197 {
198 masterVolume = std::atoi(value.c_str()) * 0.01f;
199 if (!mute && !appIsIconified)
200 alListenerf(AL_GAIN, masterVolume);
201 }
202 else if (key == "snd_eaxpreset")
203 {
204 efx->SetPreset(value);
205 }
206 else if (key == "snd_filter")
207 {
208 float gainlf = 1.f;
209 float gainhf = 1.f;
210 sscanf(value.c_str(), "%f %f", &gainlf, &gainhf);
211 efx->sfxProperties->filter_properties_f[AL_LOWPASS_GAIN] = gainlf;
212 efx->sfxProperties->filter_properties_f[AL_LOWPASS_GAINHF] = gainhf;
213 efx->CommitEffects();
214 }
215 else if (key == "UseEFX")
216 {
217 bool enable = (std::atoi(value.c_str()) != 0);
218 if (enable)
219 efx->Enable();
220 else
221 efx->Disable();
222 }
223 else if (key == "snd_volgeneral")
224 {
225 Channels::General->SetVolume(std::atoi(value.c_str()) * 0.01f);
226 }
227 else if (key == "snd_volunitreply")
228 {
229 Channels::UnitReply->SetVolume(std::atoi(value.c_str()) * 0.01f);
230 }
231 else if (key == "snd_volbattle")
232 {
233 Channels::Battle->SetVolume(std::atoi(value.c_str()) * 0.01f);
234 }
235 else if (key == "snd_volui")
236 {
237 Channels::UserInterface->SetVolume(std::atoi(value.c_str()) * 0.01f);
238 }
239 else if (key == "snd_volmusic")
240 {
241 Channels::BGMusic->SetVolume(std::atoi(value.c_str()) * 0.01f);
242 }
243 else if (key == "PitchAdjust")
244 {
245 bool tempPitchAdjust = (std::atoi(value.c_str()) != 0);
246 if (!tempPitchAdjust)
247 PitchAdjust(1.0);
248 pitchAdjust = tempPitchAdjust;
249 }
250 }
251
Mute()252 bool CSound::Mute()
253 {
254 boost::recursive_mutex::scoped_lock lck(soundMutex);
255 mute = !mute;
256 if (mute)
257 alListenerf(AL_GAIN, 0.0);
258 else
259 alListenerf(AL_GAIN, masterVolume);
260 return mute;
261 }
262
IsMuted() const263 bool CSound::IsMuted() const
264 {
265 return mute;
266 }
267
Iconified(bool state)268 void CSound::Iconified(bool state)
269 {
270 boost::recursive_mutex::scoped_lock lck(soundMutex);
271 if (appIsIconified != state && !mute)
272 {
273 if (state == false)
274 alListenerf(AL_GAIN, masterVolume);
275 else if (state == true)
276 alListenerf(AL_GAIN, 0.0);
277 }
278 appIsIconified = state;
279 }
280
281 __FORCE_ALIGN_STACK__
StartThread(int maxSounds)282 void CSound::StartThread(int maxSounds)
283 {
284 {
285 boost::recursive_mutex::scoped_lock lck(soundMutex);
286
287 // alc... will create its own thread it will copy the name from the current thread.
288 // Later we finally rename `our` audio thread.
289 Threading::SetThreadName("openal");
290
291 // NULL -> default device
292 const ALchar* deviceName = NULL;
293 std::string configDeviceName = "";
294
295 // we do not want to set a default for snd_device,
296 // so we do it like this ...
297 if (configHandler->IsSet("snd_device"))
298 {
299 configDeviceName = configHandler->GetString("snd_device");
300 deviceName = configDeviceName.c_str();
301 }
302
303 ALCdevice* device = alcOpenDevice(deviceName);
304
305 if ((device == NULL) && (deviceName != NULL))
306 {
307 LOG_L(L_WARNING,
308 "Could not open the sound device \"%s\", trying the default device ...",
309 deviceName);
310 configDeviceName = "";
311 deviceName = NULL;
312 device = alcOpenDevice(deviceName);
313 }
314
315 if (device == NULL)
316 {
317 LOG_L(L_ERROR, "Could not open a sound device, disabling sounds");
318 CheckError("CSound::InitAL");
319 return;
320 }
321 else
322 {
323 ALCcontext *context = alcCreateContext(device, NULL);
324 if (context != NULL)
325 {
326 alcMakeContextCurrent(context);
327 CheckError("CSound::CreateContext");
328 }
329 else
330 {
331 alcCloseDevice(device);
332 LOG_L(L_ERROR, "Could not create OpenAL audio context");
333 return;
334 }
335 }
336 maxSounds = GetMaxMonoSources(device, maxSounds);
337
338 LOG("OpenAL info:");
339 if(alcIsExtensionPresent(NULL, "ALC_ENUMERATION_EXT"))
340 {
341 LOG(" Available Devices:");
342 const char* deviceSpecifier = alcGetString(NULL, ALC_ALL_DEVICES_SPECIFIER);
343 while (*deviceSpecifier != '\0') {
344 LOG(" %s", deviceSpecifier);
345 while (*deviceSpecifier++ != '\0')
346 ;
347 }
348 LOG(" Device: %s", (const char*)alcGetString(device, ALC_DEVICE_SPECIFIER));
349 }
350 LOG(" Vendor: %s", (const char*)alGetString(AL_VENDOR));
351 LOG(" Version: %s", (const char*)alGetString(AL_VERSION));
352 LOG(" Renderer: %s", (const char*)alGetString(AL_RENDERER));
353 LOG(" AL Extensions: %s", (const char*)alGetString(AL_EXTENSIONS));
354 LOG(" ALC Extensions: %s", (const char*)alcGetString(device, ALC_EXTENSIONS));
355
356 // Init EFX
357 efx = new CEFX(device);
358
359 // Generate sound sources
360 for (int i = 0; i < maxSounds; i++)
361 {
362 CSoundSource* thenewone = new CSoundSource();
363 if (thenewone->IsValid()) {
364 sources.push_back(thenewone);
365 } else {
366 maxSounds = std::max(i-1, 0);
367 LOG_L(L_WARNING,
368 "Your hardware/driver can not handle more than %i soundsources",
369 maxSounds);
370 delete thenewone;
371 break;
372 }
373 }
374 LOG(" Max Sounds: %i", maxSounds);
375
376 // Set distance model (sound attenuation)
377 alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);
378 alDopplerFactor(0.2f);
379
380 alListenerf(AL_GAIN, masterVolume);
381 }
382
383 Threading::SetThreadName("audio");
384 Watchdog::RegisterThread(WDT_AUDIO);
385
386 while (!soundThreadQuit) {
387 boost::this_thread::sleep(boost::posix_time::millisec(50)); //! 20Hz
388 Watchdog::ClearTimer(WDT_AUDIO);
389 Update();
390 }
391
392 Watchdog::DeregisterThread(WDT_AUDIO);
393
394 sources.clear(); // delete all sources
395 delete efx; // must happen after sources and before context
396 efx = NULL;
397 ALCcontext* curcontext = alcGetCurrentContext();
398 ALCdevice* curdevice = alcGetContextsDevice(curcontext);
399 alcMakeContextCurrent(NULL);
400 alcDestroyContext(curcontext);
401 alcCloseDevice(curdevice);
402 }
403
Update()404 void CSound::Update()
405 {
406 boost::recursive_mutex::scoped_lock lck(soundMutex); // lock
407 for (sourceVecT::iterator it = sources.begin(); it != sources.end(); ++it)
408 it->Update();
409 CheckError("CSound::Update");
410 }
411
MakeItemFromDef(const soundItemDef & itemDef)412 size_t CSound::MakeItemFromDef(const soundItemDef& itemDef)
413 {
414 //! MakeItemFromDef is private. Only caller is LoadSoundDefs and it sets the mutex itself.
415 //boost::recursive_mutex::scoped_lock lck(soundMutex);
416 const size_t newid = sounds.size();
417 soundItemDef::const_iterator it = itemDef.find("file");
418 if (it == itemDef.end())
419 return 0;
420
421 boost::shared_ptr<SoundBuffer> buffer = SoundBuffer::GetById(LoadSoundBuffer(it->second));
422
423 if (!buffer)
424 return 0;
425
426 SoundItem* buf = new SoundItem(buffer, itemDef);
427 sounds.push_back(buf);
428 soundMap[buf->Name()] = newid;
429 return newid;
430 }
431
UpdateListener(const float3 & campos,const float3 & camdir,const float3 & camup,float lastFrameTime)432 void CSound::UpdateListener(const float3& campos, const float3& camdir, const float3& camup, float lastFrameTime)
433 {
434 boost::recursive_mutex::scoped_lock lck(soundMutex);
435 if (sources.empty())
436 return;
437
438 myPos = campos;
439 const float3 myPosInMeters = myPos * ELMOS_TO_METERS;
440 alListener3f(AL_POSITION, myPosInMeters.x, myPosInMeters.y, myPosInMeters.z);
441
442 //! reduce the rolloff when the camera is high above the ground (so we still hear something in tab mode or far zoom)
443 //! for altitudes up to and including 600 elmos, the rolloff is always clamped to 1
444 const float camHeight = std::max(1.0f, campos.y - CGround::GetHeightAboveWater(campos.x, campos.z));
445 const float newMod = std::min(600.0f / camHeight, 1.0f);
446
447 CSoundSource::SetHeightRolloffModifer(newMod);
448 efx->SetHeightRolloffModifer(newMod);
449
450 //! Result were bad with listener related doppler effects.
451 //! The user experiences the camera/listener not as a world-interacting object.
452 //! So changing sounds on camera movements were irritating, esp. because zooming with the mouse wheel
453 //! often is faster than the speed of sound, causing very high frequencies.
454 //! Note: soundsource related doppler effects are not deactivated by this! Flying cannon shoots still change their frequencies.
455 //! Note2: by not updating the listener velocity soundsource related velocities are calculated wrong,
456 //! so even if the camera is moving with a cannon shoot the frequency gets changed.
457 /*
458 const float3 velocity = (myPos - prevPos) / (lastFrameTime);
459 float3 velocityAvg = velocity * 0.6f + prevVelocity * 0.4f;
460 prevVelocity = velocityAvg;
461 velocityAvg *= ELMOS_TO_METERS;
462 velocityAvg.y *= 0.001f; //! scale vertical axis separatly (zoom with mousewheel is faster than speed of sound!)
463 velocityAvg *= 0.15f;
464 alListener3f(AL_VELOCITY, velocityAvg.x, velocityAvg.y, velocityAvg.z);
465 */
466
467 ALfloat ListenerOri[] = {camdir.x, camdir.y, camdir.z, camup.x, camup.y, camup.z};
468 alListenerfv(AL_ORIENTATION, ListenerOri);
469 CheckError("CSound::UpdateListener");
470 }
471
PrintDebugInfo()472 void CSound::PrintDebugInfo()
473 {
474 boost::recursive_mutex::scoped_lock lck(soundMutex);
475
476 LOG_L(L_DEBUG, "OpenAL Sound System:");
477 LOG_L(L_DEBUG, "# SoundSources: %i", (int)sources.size());
478 LOG_L(L_DEBUG, "# SoundBuffers: %i", (int)SoundBuffer::Count());
479
480 LOG_L(L_DEBUG, "# reserved for buffers: %i kB", (int)(SoundBuffer::AllocedSize() / 1024));
481 LOG_L(L_DEBUG, "# PlayRequests for empty sound: %i", numEmptyPlayRequests);
482 LOG_L(L_DEBUG, "# Samples disrupted: %i", numAbortedPlays);
483 LOG_L(L_DEBUG, "# SoundItems: %i", (int)sounds.size());
484 }
485
LoadSoundDefsImpl(const std::string & fileName)486 bool CSound::LoadSoundDefsImpl(const std::string& fileName)
487 {
488 //! can be called from LuaUnsyncedCtrl too
489 boost::recursive_mutex::scoped_lock lck(soundMutex);
490
491 LuaParser parser(fileName, SPRING_VFS_MOD, SPRING_VFS_ZIP);
492 parser.Execute();
493 if (!parser.IsValid())
494 {
495 LOG_L(L_WARNING, "Could not load %s: %s",
496 fileName.c_str(), parser.GetErrorLog().c_str());
497 return false;
498 }
499 else
500 {
501 const LuaTable soundRoot = parser.GetRoot();
502 const LuaTable soundItemTable = soundRoot.SubTable("SoundItems");
503 if (!soundItemTable.IsValid())
504 {
505 LOG_L(L_WARNING, "CSound(): could not parse SoundItems table in %s", fileName.c_str());
506 return false;
507 }
508 else
509 {
510 std::vector<std::string> keys;
511 soundItemTable.GetKeys(keys);
512 for (std::vector<std::string>::const_iterator it = keys.begin(); it != keys.end(); ++it)
513 {
514 std::string name(*it);
515
516 soundItemDef bufmap;
517 const LuaTable buf(soundItemTable.SubTable(name));
518 buf.GetMap(bufmap);
519 bufmap["name"] = name;
520 soundItemDefMap::const_iterator sit = soundItemDefs.find(name);
521
522 if (name == "default") {
523 defaultItem = bufmap;
524 defaultItem.erase("name"); //must be empty for default item
525 defaultItem.erase("file");
526 continue;
527 }
528
529 if (sit != soundItemDefs.end())
530 LOG_L(L_WARNING, "Sound %s gets overwritten by %s", name.c_str(), fileName.c_str());
531
532 if (!buf.KeyExists("file")) {
533 // no file, drop
534 LOG_L(L_WARNING, "Sound %s is missing file tag (ignoring)", name.c_str());
535 continue;
536 } else {
537 soundItemDefs[name] = bufmap;
538 }
539
540 if (buf.KeyExists("preload")) {
541 MakeItemFromDef(bufmap);
542 }
543 }
544 LOG(" parsed %i sounds from %s", (int)keys.size(), fileName.c_str());
545 }
546 }
547
548 //FIXME why do sounds w/o an own soundItemDef create (!=pointer) a new one from the defaultItem?
549 for (soundItemDefMap::iterator it = soundItemDefs.begin(); it != soundItemDefs.end(); ++it) {
550 soundItemDef& snddef = it->second;
551 if (snddef.find("name") == snddef.end()) {
552 // uses defaultItem! update it!
553 const std::string file = snddef["file"];
554 snddef = defaultItem;
555 snddef["file"] = file;
556 }
557 }
558
559 return true;
560 }
561
562 //! only used internally, locked in caller's scope
LoadSoundBuffer(const std::string & path)563 size_t CSound::LoadSoundBuffer(const std::string& path)
564 {
565 const size_t id = SoundBuffer::GetId(path);
566
567 if (id > 0) {
568 return id; // file is loaded already
569 } else {
570 CFileHandler file(path);
571
572 if (!file.FileExists()) {
573 LOG_L(L_ERROR, "Unable to open audio file: %s", path.c_str());
574 return 0;
575 }
576
577 std::vector<boost::uint8_t> buf(file.FileSize());
578 file.Read(&buf[0], file.FileSize());
579
580 boost::shared_ptr<SoundBuffer> buffer(new SoundBuffer());
581 bool success = false;
582 const std::string ending = file.GetFileExt();
583 if (ending == "wav") {
584 success = buffer->LoadWAV(path, buf);
585 } else if (ending == "ogg") {
586 success = buffer->LoadVorbis(path, buf);
587 } else {
588 LOG_L(L_WARNING, "CSound::LoadALBuffer: unknown audio format: %s",
589 ending.c_str());
590 }
591
592 CheckError("CSound::LoadALBuffer");
593 if (!success) {
594 LOG_L(L_WARNING, "Failed to load file: %s", path.c_str());
595 return 0;
596 }
597
598 return SoundBuffer::Insert(buffer);
599 }
600 }
601
NewFrame()602 void CSound::NewFrame()
603 {
604 Channels::General->UpdateFrame();
605 Channels::Battle->UpdateFrame();
606 Channels::UnitReply->UpdateFrame();
607 Channels::UserInterface->UpdateFrame();
608 }
609
610
611 // try to get the maximum number of supported sounds, this is similar to code CSound::StartThread
612 // but should be more safe
GetMaxMonoSources(ALCdevice * device,int maxSounds)613 int CSound::GetMaxMonoSources(ALCdevice* device, int maxSounds)
614 {
615 ALCint size;
616 alcGetIntegerv(device, ALC_ATTRIBUTES_SIZE, 1, &size);
617 std::vector<ALCint> attrs(size);
618 alcGetIntegerv(device, ALC_ALL_ATTRIBUTES, size, &attrs[0] );
619 for (int i=0; i<attrs.size(); ++i){
620 if (attrs[i] == ALC_MONO_SOURCES) {
621 const int maxMonoSources = attrs.at(i + 1);
622 if (maxMonoSources < maxSounds) {
623 LOG_L(L_WARNING, "Hardware supports only %d Sound sources, MaxSounds=%d, using Hardware Limit", maxMonoSources, maxSounds);
624 }
625 return std::min(maxSounds, maxMonoSources);
626 }
627 }
628 return maxSounds;
629 }
630
631