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, int64_t>> BufferCache;
53 };
54 
55 
56 // ------------------------------------ //
57 SoundDevice::SoundDevice() : Pimpl(std::make_unique<Implementation>()) {}
59 {
60  Release();
61  Pimpl.reset();
62 }
63 // ------------------------------------ //
64 bool SoundDevice::Init(bool simulatesound /*= false*/)
65 {
66  if(simulatesound) {
67  LOG_WARNING("SoundDevice: simulating not having a playing device");
68  return true;
69  }
70 
71  Pimpl->DeviceManager = alure::DeviceManager::getInstance();
72 
73  const auto defaultChoice =
74  Pimpl->DeviceManager.defaultDeviceName(alure::DefaultDeviceType::Full);
75 
76  auto devices = Pimpl->DeviceManager.enumerate(alure::DeviceEnumeration::Full);
77 
78  if(defaultChoice.empty()) {
79 
80  LOG_ERROR("SoundDevice: no default sound device detected");
81  return false;
82  }
83 
84  if(devices.empty()) {
85 
86  LOG_ERROR("SoundDevice: no audio devices detected");
87  return false;
88  }
89 
90  std::string selectedDevice;
91  // There's no print error here if missing to make tests run
92  ObjectFileProcessor::LoadValueFromNamedVars<std::string>(
93  Engine::Get()->GetDefinition()->GetValues(), "AudioDevice", selectedDevice, "default");
94 
95  if(selectedDevice == "default") {
96 
97  selectedDevice = defaultChoice;
98 
99  } else if(std::find(devices.begin(), devices.end(), selectedDevice) == devices.end()) {
100  LOG_ERROR("SoundDevice: selected audio device \"" + selectedDevice +
101  "\" doesn't exists. Using default");
102  selectedDevice = defaultChoice;
103  }
104 
105  LOG_INFO("SoundDevice: Initializing sound with device: " + selectedDevice);
106 
107  std::stringstream sstream;
108 
109  sstream << "Start of audio system information:\n"
110  << "// ------------------------------------ //\n";
111 
112  auto defaultDeviceName =
113  Pimpl->DeviceManager.defaultDeviceName(alure::DefaultDeviceType::Basic);
114  devices = Pimpl->DeviceManager.enumerate(alure::DeviceEnumeration::Basic);
115 
116  sstream << "Available basic devices:\n";
117  for(const auto& name : devices) {
118  sstream << "\t> " << name;
119  if(name == defaultDeviceName)
120  sstream << " [DEFAULT]";
121  sstream << "\n";
122  }
123 
124  sstream << "\n";
125 
126  devices = Pimpl->DeviceManager.enumerate(alure::DeviceEnumeration::Full);
127  defaultDeviceName = Pimpl->DeviceManager.defaultDeviceName(alure::DefaultDeviceType::Full);
128 
129  sstream << "Available devices:\n";
130  for(const auto& name : devices) {
131  sstream << "\t> " << name;
132  if(name == defaultDeviceName)
133  sstream << " [DEFAULT]";
134  sstream << "\n";
135  }
136  sstream << "\n";
137 
138  devices = Pimpl->DeviceManager.enumerate(alure::DeviceEnumeration::Capture);
139  defaultDeviceName =
140  Pimpl->DeviceManager.defaultDeviceName(alure::DefaultDeviceType::Capture);
141  sstream << "Available capture devices:\n";
142  for(const auto& name : devices) {
143  sstream << "\t> " << name;
144  if(name == defaultDeviceName)
145  sstream << " [DEFAULT]";
146  sstream << "\n";
147  }
148  sstream << "\n";
149 
150  try {
151  Pimpl->Device = Pimpl->DeviceManager.openPlayback(selectedDevice);
152  } catch(const std::exception& e) {
153  LOG_INFO(sstream.str());
154  LOG_ERROR("SoundDevice: opening playback failed: " + std::string(e.what()));
155  return false;
156  }
157 
158  sstream << "Info for device \"" << Pimpl->Device.getName(alure::PlaybackName::Full)
159  << "\":" << std::endl;
160  auto version = Pimpl->Device.getALCVersion();
161  sstream << "ALC version: " << version.getMajor() << "." << version.getMinor() << "\n";
162  version = Pimpl->Device.getEFXVersion();
163  if(!version.isZero()) {
164  sstream << "EFX version: " << version.getMajor() << "." << version.getMinor() << "\n";
165  sstream << "Max auxiliary sends: " << Pimpl->Device.getMaxAuxiliarySends() << "\n";
166  } else
167  sstream << "EFX not supported"
168  << "\n";
169 
170  // TODO: extensions
171  // Pimpl->Device.queryExtension(const String &name)
172 
173  sstream << "// ------------------------------------ //";
174 
175  LOG_INFO(sstream.str());
176 
177  try {
178  Pimpl->Context = Pimpl->Device.createContext();
179 
180  if(!Pimpl->Context)
181  throw Exception("returned context is null");
182 
183  } catch(const std::exception& e) {
184  LOG_ERROR("SoundDevice: opening context failed: " + std::string(e.what()));
185  return false;
186  }
187 
188  alure::Context::MakeCurrent(Pimpl->Context);
189  Pimpl->Listener = Pimpl->Context.getListener();
190 
191  Pimpl->Context.setMessageHandler(alure::MakeShared<SoundMessageHandler>());
192 
193  // Setup global volume //
194  SetGlobalVolume(1.f);
195 
196  Pimpl->Listener.setPosition({0, 0, 0});
197  Pimpl->Listener.set3DParameters(
198  // Pos
199  {0, 0, 0},
200  // Velocity
201  {0, 0, 0},
202  // Orientation, at and up
203  {{0, 0, 0}, {0, 1, 0}});
204 
205  return true;
206 }
207 
209 {
210  if(!Pimpl->Device)
211  return;
212 
213  Pimpl->HandledAudioSources.clear();
214  Pimpl->BufferCache.clear();
215 
216  alure::Context::MakeCurrent(nullptr);
217  Pimpl->Listener = nullptr;
218  Pimpl->Context.destroy();
219  Pimpl->Device.close();
220 
221  Pimpl->Device = nullptr;
222 }
223 // ------------------------------------ //
224 void SoundDevice::Tick(int PassedMs)
225 {
226  ElapsedSinceLastClean += PassedMs;
227 
228  Pimpl->Context.update();
229 
230  if(ElapsedSinceLastClean > 200) {
231  ElapsedSinceLastClean = 0;
232 
233  for(auto iter = Pimpl->HandledAudioSources.begin();
234  iter != Pimpl->HandledAudioSources.end();) {
235 
236  if(!(*iter)->IsPlaying()) {
237 
238  iter = Pimpl->HandledAudioSources.erase(iter);
239  } else {
240  ++iter;
241  }
242  }
243 
244  const auto now = Time::GetTimeMs64();
245 
246  // Clear cache entries
247  for(auto iter = Pimpl->BufferCache.begin(); iter != Pimpl->BufferCache.end();) {
248 
249  if(std::get<1>(iter->second) - now > CacheSoundEffectMilliseconds) {
250  // Remove from cache
251 
252  iter = Pimpl->BufferCache.erase(iter);
253  } else {
254  ++iter;
255  }
256  }
257  }
258 }
259 
261  const Float3& pos, const Float4& orientation)
262 {
263  if(!Pimpl->Listener)
264  return;
265 
266  bs::Quaternion quaternion(orientation);
267 
268  bs::Radian angle;
269  bs::Vector3 direction;
270 
271  // TODO: does this do the right thing?
272  quaternion.toAxisAngle(direction, angle); // toAngleAxis(angle, direction);
273 
274  alure::Vector3 at(0, 0, 0);
275 
276  // TODO: better velocity calculation
277  const auto temp = pos - Pimpl->PreviousPosition;
278  alure::Vector3 velocity(temp.X, temp.Y, temp.Z);
279 
280  Pimpl->Listener.set3DParameters(
281  {pos.X, pos.Y, pos.Z}, velocity, {at, {direction.x, direction.y, direction.z}});
282 
283  Pimpl->PreviousPosition = pos;
284 }
285 
287 {
288  vol = std::clamp(vol, 0.f, 1.f);
289 
290  Pimpl->Listener.setGain(vol);
291 }
292 // ------------------------------------ //
293 DLLEXPORT void SoundDevice::Play2DSoundEffect(const std::string& filename)
294 {
296 
297  const auto buffer = GetBufferFromFile(filename);
298  if(!buffer)
299  return;
300 
301  const auto source = GetAudioSource();
302 
303  if(!source)
304  return;
305 
306  source->Play2D(buffer);
307 
308  Engine::Get()->RunOnMainThread([=]() { this->BabysitAudio(source); });
309 }
310 
312  const std::string& filename, bool looping)
313 {
315 
316  const auto buffer = GetBufferFromFile(filename);
317  if(!buffer)
318  return nullptr;
319 
320  const auto source = GetAudioSource();
321 
322  if(!source)
323  return nullptr;
324 
325  source->Play2D(buffer);
326 
327  if(looping)
328  source->SetLooping(true);
329  return source;
330 }
331 
333  const ProceduralSoundData::pointer& data, size_t chunksize /*= 56000*/,
334  size_t chunkstoqueue /*= 4*/)
335 {
337 
338  if(!Pimpl || !data)
339  return nullptr;
340 
341  const auto source = GetAudioSource();
342 
343  if(!source)
344  return nullptr;
345 
346  source->PlayWithDecoder(data, chunksize, chunkstoqueue);
347  return source;
348 }
349 // ------------------------------------ //
350 DLLEXPORT Sound::AudioBuffer::pointer SoundDevice::GetBufferFromFile(
351  const std::string& filename, bool cache /*= true*/)
352 {
353  const auto now = Time::GetTimeMs64();
354 
355  AudioBuffer::pointer buffer;
356 
357  // Find buffer from cache
358  if(cache) {
359  const auto found = Pimpl->BufferCache.find(filename);
360 
361  if(found != Pimpl->BufferCache.end()) {
362  buffer = std::get<0>(found->second);
363  std::get<1>(found->second) = now;
364  }
365  }
366 
367  if(!buffer) {
368  // Not cached
369  auto alureBuffer = Pimpl->Context.getBuffer(filename);
370 
371  if(!alureBuffer) {
372  LOG_ERROR("SoundDevice: failed to create buffer from file: " + filename);
373  return nullptr;
374  }
375 
376  buffer = AudioBuffer::MakeShared<AudioBuffer>(std::move(alureBuffer), this);
377 
378  if(cache)
379  Pimpl->BufferCache[filename] = {buffer, now};
380  }
381 
382  return buffer;
383 }
384 
386 {
387  auto alureSource = Pimpl->Context.createSource();
388 
389  if(!alureSource) {
390  LOG_ERROR("SoundDevice: GetAudioSource: couldn't create alure source");
391  return nullptr;
392  }
393 
394  return AudioSource::MakeShared<AudioSource>(alureSource);
395 }
396 // ------------------------------------ //
398 {
400 
401  Pimpl->HandledAudioSources.push_back(audio);
402 }
403 // ------------------------------------ //
405 {
406  Pimpl->Context.removeBuffer(buffer.GetBuffer());
407 }
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.
#define LOG_INFO(x)
Definition: Define.h:88
virtual void sourceForceStopped(alure::Source source) noexcept override
Definition: SoundDevice.cpp:28
#define LOG_ERROR(x)
Definition: Define.h:90
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 Tick(int PassedMs)
DLLEXPORT void SetGlobalVolume(float vol)
Base class for all exceptions thrown by Leviathan.
Definition: Exceptions.h:10
#define LOG_WARNING(x)
Definition: Define.h:89
DLLEXPORT void ReportDestroyedBuffer(Sound::AudioBuffer &buffer)
std::unordered_map< std::string, std::tuple< AudioBuffer::pointer, int64_t > > BufferCache
Cache of buffers for short sounds.
Definition: SoundDevice.cpp:52
static DLLEXPORT int64_t GetTimeMs64()
alure::SharedPtr< ProceduralSoundData > pointer
DLLEXPORT AudioSource::pointer GetAudioSource()
Creates an audio source with no settings applied.
Small ReferenceCounted wrapper around an audio buffer.
Definition: AudioBuffer.h:21
alure::Buffer & GetBuffer()
Definition: AudioBuffer.h:40
DLLEXPORT void RunOnMainThread(const std::function< void()> &function)
Runs the function now if on the main thread otherwise calls Invoke.
Definition: Engine.cpp:1183
DLLEXPORT bool Init(bool simulatesound=false)
Definition: SoundDevice.cpp:64
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.
DLLEXPORT void Play2DSoundEffect(const std::string &filename)
Plays a 2d sound without possibility of interrupting.
DLLEXPORT void SetSoundListenerPosition(const Float3 &pos, const Float4 &orientation)
Loads the file and plays the sound.
DLLEXPORT ~SoundDevice()
Definition: SoundDevice.cpp:58
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:85
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:105
virtual alure::String resourceNotFound(alure::StringView name) noexcept override
Definition: SoundDevice.cpp:33