Leviathan  0.8.0.0
Leviathan game engine
SoundDevice.cpp
Go to the documentation of this file.
1 // ------------------------------------ //
2 #include "SoundDevice.h"
3 
4 #include "AudioBuffer.h"
5 
8 #include "Engine.h"
10 #include "TimeIncludes.h"
11 
12 #include "alure2.h"
13 
14 #include <boost/filesystem.hpp>
15 
16 #include <algorithm>
17 using namespace Leviathan;
18 using namespace Leviathan::Sound;
19 // ------------------------------------ //
20 
21 class SoundMessageHandler : public alure::MessageHandler {
22 public:
23  virtual void deviceDisconnected(alure::Device device) noexcept override
24  {
25  LOG_INFO("[SOUND] Device disconnected: " + device.getName());
26  }
27 
28  virtual void sourceForceStopped(alure::Source source) noexcept override
29  {
30  LOG_WARNING("[SOUND] Source force stopped.");
31  }
32 
33  virtual alure::String resourceNotFound(alure::StringView name) noexcept override
34  {
35  LOG_ERROR(std::string("[SOUND] resource not found: ") + name);
36  return "";
37  }
38 };
39 
41 
42  alure::DeviceManager DeviceManager;
43  alure::Device Device;
44  alure::Context Context;
45  alure::Listener Listener;
46  Float3 PreviousPosition = {0, 0, 0};
47 
49  std::vector<AudioSource::pointer> HandledAudioSources;
50 
52  std::unordered_map<std::string, std::tuple<AudioBuffer::pointer, TimePoint>> BufferCache;
53 
57  std::unordered_map<std::string, AudioBuffer::pointer> FilenameOpenBuffers;
58 
59  SecondDuration CacheSoundSeconds{30.f};
60 };
61 
62 
63 // ------------------------------------ //
64 SoundDevice::SoundDevice() : Pimpl(std::make_unique<Implementation>()) {}
66 {
67  Release();
68  Pimpl.reset();
69 }
70 // ------------------------------------ //
71 bool SoundDevice::Init(bool simulatesound /*= false*/)
72 {
73  if(simulatesound) {
74  LOG_WARNING("SoundDevice: simulating not having a playing device");
75  return true;
76  }
77 
78  Pimpl->DeviceManager = alure::DeviceManager::getInstance();
79 
80  const auto defaultChoice =
81  Pimpl->DeviceManager.defaultDeviceName(alure::DefaultDeviceType::Full);
82 
83  auto devices = Pimpl->DeviceManager.enumerate(alure::DeviceEnumeration::Full);
84 
85  if(defaultChoice.empty()) {
86 
87  LOG_ERROR("SoundDevice: no default sound device detected");
88  return false;
89  }
90 
91  if(devices.empty()) {
92 
93  LOG_ERROR("SoundDevice: no audio devices detected");
94  return false;
95  }
96 
97  std::string selectedDevice;
98  // There's no print error here if missing to make tests run
99  ObjectFileProcessor::LoadValueFromNamedVars<std::string>(
100  Engine::Get()->GetDefinition()->GetValues(), "AudioDevice", selectedDevice, "default");
101 
102  if(selectedDevice == "default") {
103 
104  selectedDevice = defaultChoice;
105 
106  } else if(std::find(devices.begin(), devices.end(), selectedDevice) == devices.end()) {
107  LOG_ERROR("SoundDevice: selected audio device \"" + selectedDevice +
108  "\" doesn't exists. Using default");
109  selectedDevice = defaultChoice;
110  }
111 
112  LOG_INFO("SoundDevice: Initializing sound with device: " + selectedDevice);
113 
114  std::stringstream sstream;
115 
116  sstream << "Start of audio system information:\n"
117  << "// ------------------------------------ //\n";
118 
119  auto defaultDeviceName =
120  Pimpl->DeviceManager.defaultDeviceName(alure::DefaultDeviceType::Basic);
121  devices = Pimpl->DeviceManager.enumerate(alure::DeviceEnumeration::Basic);
122 
123  sstream << "Available basic devices:\n";
124  for(const auto& name : devices) {
125  sstream << "\t> " << name;
126  if(name == defaultDeviceName)
127  sstream << " [DEFAULT]";
128  sstream << "\n";
129  }
130 
131  sstream << "\n";
132 
133  devices = Pimpl->DeviceManager.enumerate(alure::DeviceEnumeration::Full);
134  defaultDeviceName = Pimpl->DeviceManager.defaultDeviceName(alure::DefaultDeviceType::Full);
135 
136  sstream << "Available devices:\n";
137  for(const auto& name : devices) {
138  sstream << "\t> " << name;
139  if(name == defaultDeviceName)
140  sstream << " [DEFAULT]";
141  sstream << "\n";
142  }
143  sstream << "\n";
144 
145  devices = Pimpl->DeviceManager.enumerate(alure::DeviceEnumeration::Capture);
146  defaultDeviceName =
147  Pimpl->DeviceManager.defaultDeviceName(alure::DefaultDeviceType::Capture);
148  sstream << "Available capture devices:\n";
149  for(const auto& name : devices) {
150  sstream << "\t> " << name;
151  if(name == defaultDeviceName)
152  sstream << " [DEFAULT]";
153  sstream << "\n";
154  }
155  sstream << "\n";
156 
157  try {
158  Pimpl->Device = Pimpl->DeviceManager.openPlayback(selectedDevice);
159  } catch(const std::exception& e) {
160  LOG_INFO(sstream.str());
161  LOG_ERROR("SoundDevice: opening playback failed: " + std::string(e.what()));
162  return false;
163  }
164 
165  sstream << "Info for device \"" << Pimpl->Device.getName(alure::PlaybackName::Full)
166  << "\":" << std::endl;
167  auto version = Pimpl->Device.getALCVersion();
168  sstream << "ALC version: " << version.getMajor() << "." << version.getMinor() << "\n";
169  version = Pimpl->Device.getEFXVersion();
170  if(!version.isZero()) {
171  sstream << "EFX version: " << version.getMajor() << "." << version.getMinor() << "\n";
172  sstream << "Max auxiliary sends: " << Pimpl->Device.getMaxAuxiliarySends() << "\n";
173  } else
174  sstream << "EFX not supported"
175  << "\n";
176 
177  // TODO: extensions
178  // Pimpl->Device.queryExtension(const String &name)
179 
180  sstream << "// ------------------------------------ //";
181 
182  LOG_INFO(sstream.str());
183 
184  try {
185  Pimpl->Context = Pimpl->Device.createContext();
186 
187  if(!Pimpl->Context)
188  throw Exception("returned context is null");
189 
190  } catch(const std::exception& e) {
191  LOG_ERROR("SoundDevice: opening context failed: " + std::string(e.what()));
192  return false;
193  }
194 
195  alure::Context::MakeCurrent(Pimpl->Context);
196  Pimpl->Listener = Pimpl->Context.getListener();
197 
198  Pimpl->Context.setMessageHandler(alure::MakeShared<SoundMessageHandler>());
199 
200  // Setup global volume //
201  SetGlobalVolume(1.f);
202 
203  Pimpl->Listener.setPosition({0, 0, 0});
204  Pimpl->Listener.set3DParameters(
205  // Pos
206  {0, 0, 0},
207  // Velocity
208  {0, 0, 0},
209  // Orientation, at and up
210  {{0, 0, 0}, {0, 1, 0}});
211 
212  return true;
213 }
214 
216 {
217  if(!Pimpl->Device)
218  return;
219 
220  Pimpl->HandledAudioSources.clear();
221  Pimpl->BufferCache.clear();
222  Pimpl->FilenameOpenBuffers.clear();
223 
224  alure::Context::MakeCurrent(nullptr);
225  Pimpl->Listener = nullptr;
226  Pimpl->Context.destroy();
227  Pimpl->Device.close();
228 
229  Pimpl->Device = nullptr;
230 }
231 // ------------------------------------ //
232 void SoundDevice::Tick(float elapsed)
233 {
234  ElapsedSinceLastClean += elapsed;
235 
236  Pimpl->Context.update();
237 
238  if(ElapsedSinceLastClean > 0.2f) {
239  ElapsedSinceLastClean = 0;
240 
241  for(auto iter = Pimpl->HandledAudioSources.begin();
242  iter != Pimpl->HandledAudioSources.end();) {
243 
244  if(!(*iter)->IsPlaying()) {
245 
246  iter = Pimpl->HandledAudioSources.erase(iter);
247  } else {
248  ++iter;
249  }
250  }
251 
252  for(auto iter = Pimpl->FilenameOpenBuffers.begin();
253  iter != Pimpl->FilenameOpenBuffers.end();) {
254 
255  if(iter->second->GetRefCount() == 1) {
256  iter = Pimpl->FilenameOpenBuffers.erase(iter);
257  } else {
258  ++iter;
259  }
260  }
261 
262  const auto now = Time::GetCurrentTimePoint();
263 
264  // Clear cache entries
265  for(auto iter = Pimpl->BufferCache.begin(); iter != Pimpl->BufferCache.end();) {
266  if(now - std::get<1>(iter->second) > Pimpl->CacheSoundSeconds) {
267  // Remove from cache
268 
269  iter = Pimpl->BufferCache.erase(iter);
270  } else {
271  ++iter;
272  }
273  }
274  }
275 }
276 
278  const Float3& pos, const Float4& orientation)
279 {
280  if(!Pimpl->Listener)
281  return;
282 
283  bs::Quaternion quaternion(orientation);
284 
285  bs::Radian angle;
286  bs::Vector3 direction;
287 
288  // TODO: does this do the right thing?
289  quaternion.toAxisAngle(direction, angle); // toAngleAxis(angle, direction);
290 
291  alure::Vector3 at(0, 0, 0);
292 
293  // TODO: better velocity calculation
294  const auto temp = pos - Pimpl->PreviousPosition;
295  alure::Vector3 velocity(temp.X, temp.Y, temp.Z);
296 
297  Pimpl->Listener.set3DParameters(
298  {pos.X, pos.Y, pos.Z}, velocity, {at, {direction.x, direction.y, direction.z}});
299 
300  Pimpl->PreviousPosition = pos;
301 }
302 
304 {
305  vol = std::clamp(vol, 0.f, 1.f);
306 
307  Pimpl->Listener.setGain(vol);
308 }
309 // ------------------------------------ //
310 DLLEXPORT void SoundDevice::Play2DSoundEffect(const std::string& filename)
311 {
313 
314  const auto buffer = GetBufferFromFile(filename);
315  if(!buffer)
316  return;
317 
318  const auto source = GetAudioSource();
319 
320  if(!source)
321  return;
322 
323  source->Play2D(buffer);
324 
325  Engine::Get()->RunOnMainThread([=]() { this->BabysitAudio(source); });
326 }
327 
329  const std::string& filename, bool looping)
330 {
332 
333  const auto buffer = GetBufferFromFile(filename);
334  if(!buffer)
335  return nullptr;
336 
337  const auto source = GetAudioSource();
338 
339  if(!source)
340  return nullptr;
341 
342  source->Play2D(buffer);
343 
344  if(looping)
345  source->SetLooping(true);
346  return source;
347 }
348 
350  const ProceduralSoundData::pointer& data, size_t chunksize /*= 56000*/,
351  size_t chunkstoqueue /*= 4*/)
352 {
354 
355  if(!Pimpl || !data)
356  return nullptr;
357 
358  const auto source = GetAudioSource();
359 
360  if(!source)
361  return nullptr;
362 
363  source->PlayWithDecoder(data, chunksize, chunkstoqueue);
364  return source;
365 }
366 // ------------------------------------ //
367 DLLEXPORT Sound::AudioBuffer::pointer SoundDevice::GetBufferFromFile(
368  const std::string& filename, bool cache /*= true*/)
369 {
371 
372  const auto now = Time::GetCurrentTimePoint();
373 
374  AudioBuffer::pointer buffer;
375 
376  // Find buffer from cache
377  if(cache) {
378  const auto found = Pimpl->BufferCache.find(filename);
379 
380  if(found != Pimpl->BufferCache.end()) {
381  buffer = std::get<0>(found->second);
382  std::get<1>(found->second) = now;
383  }
384  }
385 
386  if(!buffer) {
387  // Not cached
388 
389  const auto found = Pimpl->FilenameOpenBuffers.find(filename);
390 
391  if(found != Pimpl->FilenameOpenBuffers.end()) {
392  return found->second;
393  }
394 
395  try {
396  auto alureBuffer = Pimpl->Context.getBuffer(filename);
397 
398  if(!alureBuffer || alureBuffer.getFrequency() < 1) {
399  LOG_ERROR("SoundDevice: failed to create buffer from file: " + filename);
400  return nullptr;
401  }
402 
403  buffer = AudioBuffer::MakeShared<AudioBuffer>(std::move(alureBuffer), this);
404 
405  } catch(const std::runtime_error& e) {
406  LOG_ERROR("SoundDevice: failed to create buffer from file: " + filename +
407  ", exception: " + e.what());
408  return nullptr;
409  }
410 
411  if(cache)
412  Pimpl->BufferCache[filename] = {buffer, now};
413  Pimpl->FilenameOpenBuffers[filename] = buffer;
414  }
415 
416  return buffer;
417 }
418 
420 {
422 
423  auto alureSource = Pimpl->Context.createSource();
424 
425  if(!alureSource) {
426  LOG_ERROR("SoundDevice: GetAudioSource: couldn't create alure source");
427  return nullptr;
428  }
429 
430  return AudioSource::MakeShared<AudioSource>(alureSource);
431 }
432 // ------------------------------------ //
434 {
436 
437  Pimpl->HandledAudioSources.push_back(audio);
438 }
439 // ------------------------------------ //
440 DLLEXPORT void SoundDevice::ReportDestroyedBuffer(const alure::Buffer& buffer)
441 {
442  if(!buffer)
443  return;
444 
446 
447  LEVIATHAN_ASSERT(buffer.getName() != "test stuff", "test compare failed");
448 
449  Pimpl->Context.removeBuffer(buffer);
450 }
DLLEXPORT void Tick(float elapsed)
virtual void deviceDisconnected(alure::Device device) noexcept override
Definition: SoundDevice.cpp:23
DLLEXPORT Sound::AudioBuffer::pointer GetBufferFromFile(const std::string &filename, bool cache=true)
Creates a sound buffer from a file.
DLLEXPORT void ReportDestroyedBuffer(const alure::Buffer &buffer)
#define LOG_INFO(x)
Definition: Define.h:90
virtual void sourceForceStopped(alure::Source source) noexcept override
Definition: SoundDevice.cpp:28
#define LOG_ERROR(x)
Definition: Define.h:92
std::vector< AudioSource::pointer > HandledAudioSources
List of audio sources that this class will close on tick if they have stopped.
Definition: SoundDevice.cpp:49
DLLEXPORT AudioSource::pointer Play2DSound(const std::string &filename, bool looping)
Plays a 2d sound with options.
DLLEXPORT void SetGlobalVolume(float vol)
std::unordered_map< std::string, AudioBuffer::pointer > FilenameOpenBuffers
Definition: SoundDevice.cpp:57
Base class for all exceptions thrown by Leviathan.
Definition: Exceptions.h:10
#define LOG_WARNING(x)
Definition: Define.h:91
alure::SharedPtr< ProceduralSoundData > pointer
DLLEXPORT AudioSource::pointer GetAudioSource()
Creates an audio source with no settings applied.
DLLEXPORT void RunOnMainThread(const std::function< void()> &function)
Runs the function now if on the main thread otherwise calls Invoke.
Definition: Engine.cpp:1190
static DLLEXPORT TimePoint GetCurrentTimePoint()
DLLEXPORT bool Init(bool simulatesound=false)
Definition: SoundDevice.cpp:71
#define LEVIATHAN_ASSERT(x, msg)
Definition: Define.h:100
DLLEXPORT void BabysitAudio(AudioSource::pointer audio)
This class holds the audio source until it has finished playing and then releases the reference.
DLLEXPORT AudioSource::pointer CreateProceduralSound(const Sound::ProceduralSoundData::pointer &data, size_t chunksize=56000, size_t chunkstoqueue=4)
Opens an audio source from a procedural data stream.
uint8_t clamp(int16_t value)
Definition: YUVToRGB.cpp:17
DLLEXPORT void Play2DSoundEffect(const std::string &filename)
Plays a 2d sound without possibility of interrupting.
std::unordered_map< std::string, std::tuple< AudioBuffer::pointer, TimePoint > > BufferCache
Cache of buffers for short sounds.
Definition: SoundDevice.cpp:52
DLLEXPORT void SetSoundListenerPosition(const Float3 &pos, const Float4 &orientation)
Loads the file and plays the sound.
DLLEXPORT ~SoundDevice()
Definition: SoundDevice.cpp:65
boost::intrusive_ptr< JSProxyable > pointer
Definition: JSProxyable.h:15
#define DLLEXPORT
Definition: Include.h:84
DLLEXPORT void Release()
static DLLEXPORT Engine * Get()
Definition: Engine.cpp:86
The access mask controls which registered functions and classes a script sees.
Definition: GameModule.h:12
DLLEXPORT void AssertIfNotMainThread() const
Asserts if not called on the main thread.
Definition: Engine.h:111
std::chrono::duration< float, std::ratio< 1 > > SecondDuration
Definition: TimeIncludes.h:16
virtual alure::String resourceNotFound(alure::StringView name) noexcept override
Definition: SoundDevice.cpp:33