1 /* 2 * zlib License 3 * 4 * (C) 2016 jython234 5 * 6 * This software is provided 'as-is', without any express or implied 7 * warranty. In no event will the authors be held liable for any damages 8 * arising from the use of this software. 9 * 10 * Permission is granted to anyone to use this software for any purpose, 11 * including commercial applications, and to alter it and redistribute it 12 * freely, subject to the following restrictions: 13 * 14 * 1. The origin of this software must not be misrepresented; you must not 15 * claim that you wrote the original software. If you use this software 16 * in a product, an acknowledgment in the product documentation would be 17 * appreciated but is not required. 18 * 2. Altered source versions must be plainly marked as such, and must not be 19 * misrepresented as being the original software. 20 * 3. This notice may not be removed or altered from any source distribution. 21 */ 22 module blocksound.backend.openal; 23 24 version(blocksound_ALBackend) { 25 26 pragma(msg, "-----BlockSound using OpenAL backend-----"); 27 28 import blocksound.core; 29 import blocksound.backend.types; 30 31 import derelict.openal.al; 32 import derelict.sndfile.sndfile; 33 34 import std.concurrency; 35 36 /// Class to manage the OpenAL Audio backend. 37 class ALAudioBackend : AudioBackend { 38 protected ALCdevice* device; 39 protected ALCcontext* context; 40 41 /// Create a new ALBackend. One per thread. 42 this() @trusted { 43 debug(blocksound_verbose) { 44 import std.stdio : writeln; 45 writeln("[BlockSound]: Initializing OpenAL backend..."); 46 } 47 48 device = alcOpenDevice(null); // Open default device. 49 context = alcCreateContext(device, null); 50 51 alcMakeContextCurrent(context); 52 53 debug(blocksound_verbose) { 54 import std.stdio : writeln; 55 writeln("[BlockSound]: OpenAL Backend initialized."); 56 writeln("[BlockSound]: AL_VERSION: ", toDString(alGetString(AL_VERSION)), ", AL_VENDOR: ", toDString(alGetString(AL_VENDOR))); 57 } 58 } 59 60 override { 61 void setListenerLocation(in Vec3 loc) @trusted nothrow { 62 alListener3f(AL_POSITION, loc.x, loc.y, loc.z); 63 } 64 65 void setListenerGain(in float gain) @trusted nothrow { 66 alListenerf(AL_GAIN, gain); 67 } 68 69 void cleanup() @trusted nothrow { 70 alcCloseDevice(device); 71 } 72 } 73 } 74 75 /// OpenAL Source backend 76 class ALSource : Source { 77 package ALuint source; 78 79 package this() @trusted { 80 alGenSources(1, &source); 81 } 82 83 override { 84 protected void _setSound(Sound sound) @trusted { 85 if(auto s = cast(ALSound) sound) { 86 alSourcei(source, AL_BUFFER, s.buffer); 87 } else { 88 throw new Exception("Invalid Sound: not instance of ALSound"); 89 } 90 } 91 92 void setLooping(in bool loop) @trusted { 93 alSourcei(source, AL_LOOPING, loop ? AL_TRUE : AL_FALSE); 94 } 95 96 void play() @trusted nothrow { 97 alSourcePlay(source); 98 } 99 100 void pause() @trusted nothrow { 101 alSourcePause(source); 102 } 103 104 void stop() @trusted nothrow { 105 alSourceStop(source); 106 } 107 108 bool hasFinishedPlaying() @trusted nothrow { 109 ALenum state; 110 alGetSourcei(source, AL_SOURCE_STATE, &state); 111 return state != AL_PLAYING; 112 } 113 114 protected void _cleanup() @system nothrow { 115 alDeleteSources(1, &source); 116 } 117 } 118 } 119 120 /// OpenAL Source backend (for streaming.) 121 class ALStreamingSource : StreamingSource { 122 123 static immutable size_t 124 STREAM_CMD_PLAY = 0, 125 STREAM_CMD_PAUSE = 1, 126 STREAM_CMD_STOP = 2, 127 STREAM_CMD_SET_LOOP_TRUE = 3, 128 STREAM_CMD_SET_LOOP_FALSE = 4, 129 STREAM_IS_PLAYING = 5, 130 STREAM_STATE_PLAYING = 6, 131 STREAM_STATE_STOPPED = 7; 132 133 package ALuint source; 134 135 private Tid streamThread; 136 private ALStreamedSound sound; 137 138 package shared finishedPlaying = false; 139 140 package this() @trusted { 141 alGenSources(1, &source); 142 } 143 144 override { 145 protected void _setSound(Sound sound) @trusted { 146 if(!(this.sound is null)) throw new Exception("Sound already set!"); 147 148 if(auto s = cast(ALStreamedSound) sound) { 149 this.sound = s; 150 151 alSourceQueueBuffers(source, s.numBuffers, s.buffers.ptr); 152 streamThread = spawn(&streamSoundThread, cast(shared) this, cast(shared) this.sound); 153 } else { 154 throw new Exception("Invalid Sound: not instance of ALStreamedSound"); 155 } 156 } 157 158 void setLooping(in bool loop) @trusted { 159 //alSourcei(source, AL_LOOPING, loop ? AL_TRUE : AL_FALSE); 160 streamThread.send(loop ? STREAM_CMD_SET_LOOP_TRUE : STREAM_CMD_SET_LOOP_FALSE); 161 } 162 163 void play() @trusted { 164 alSourcePlay(source); 165 streamThread.send(STREAM_CMD_PLAY); 166 } 167 168 void pause() @trusted { 169 alSourcePause(source); 170 streamThread.send(STREAM_CMD_PAUSE); 171 } 172 173 void stop() @trusted { 174 alSourceStop(source); 175 streamThread.send(STREAM_CMD_STOP); 176 } 177 178 bool hasFinishedPlaying() @trusted nothrow { 179 /* 180 ALenum state; 181 alGetSourcei(source, AL_SOURCE_STATE, &state); 182 return state != AL_PLAYING;*/ 183 return finishedPlaying; 184 } 185 186 protected void _cleanup() @system nothrow { 187 alDeleteSources(1, &source); 188 } 189 } 190 } 191 192 /// The dedicated sound streaming thread. This is used to refill buffers while streaming sound. 193 package void streamSoundThread(shared ALStreamingSource source, shared ALStreamedSound sound) @system { 194 import std.datetime : dur; 195 import core.thread : Thread; 196 197 bool hasFinished = false; 198 bool isPlaying = false; 199 bool loop = false; 200 201 debug(blocksound_verbose) { 202 import std.stdio; 203 writeln("[BlockSound]: Started dedicated streaming thread."); 204 } 205 206 while(true) { 207 receiveTimeout(dur!("msecs")(1), // Check for messages from main thread. 208 (immutable size_t signal) { 209 switch(signal) { 210 case ALStreamingSource.STREAM_CMD_PLAY: 211 isPlaying = true; 212 break; 213 case ALStreamingSource.STREAM_CMD_PAUSE: 214 isPlaying = false; 215 break; 216 case ALStreamingSource.STREAM_CMD_STOP: 217 isPlaying = false; 218 hasFinished = true; 219 break; 220 221 case ALStreamingSource.STREAM_CMD_SET_LOOP_TRUE: 222 loop = true; 223 break; 224 case ALStreamingSource.STREAM_CMD_SET_LOOP_FALSE: 225 loop = false; 226 break; 227 default: 228 break; 229 } 230 }); 231 232 if(isPlaying) { // Check if we are supposed to be playing (refilling buffers) 233 ALint state, processed; 234 235 alGetSourcei(source.source, AL_SOURCE_STATE, &state); // Get the state of the audio. 236 alGetSourcei(source.source, AL_BUFFERS_PROCESSED, &processed); // Get the amount of buffers that OpenAL has played. 237 if(processed > 0) { 238 alSourceUnqueueBuffers(cast(ALuint) source.source, processed, (cast(ALuint[])sound.buffers).ptr); // Unqueue buffers that have been played. 239 240 alDeleteBuffers(processed, (cast(ALuint[])sound.buffers).ptr); // Delete the played buffers 241 242 for(size_t i = 0; i < processed; i++) { // Go through each buffer that was played. 243 try { 244 ALuint buffer = (cast(ALStreamedSound) sound).queueBuffer(); // load a new buffer 245 sound.buffers[i] = buffer; // Add it to the array 246 } catch(EOFException e) { // Check if we have finished reading the sound file 247 if(loop) { // Check if we are looping the sound. 248 (cast(ALStreamedSound) sound).reset(); // Reset the sound to the beginning (seek to zero frames) 249 250 alSourceStop((cast(ALStreamingSource) source).source); 251 debug(blocksound_verbose) { 252 import std.stdio; 253 writeln("[BlockSound]: Dedicated streaming thread reset."); 254 } 255 continue; 256 } else { // We are done here, time to close up shop. 257 hasFinished = true; 258 source.finishedPlaying = true; // Notify main thread that we are done. 259 260 debug(blocksound_verbose) { 261 import std.stdio; 262 writeln("[BlockSound]: Dedicated streaming thread finished."); 263 } 264 break; // Break out of the loop, and exit the thread. 265 } 266 } 267 } 268 269 alSourceQueueBuffers(cast(ALuint) source.source, processed, (cast(ALuint[])sound.buffers).ptr); // Queue the new buffers to OpenAL. 270 } 271 272 if(state != AL_PLAYING) { 273 alSourcePlay(source.source); 274 } 275 276 277 Thread.sleep(dur!("msecs")(50)); // Sleep 50 msecs as to prevent high CPU usage. 278 } 279 280 if(hasFinished) { 281 source.finishedPlaying = true; // Notify main thread that we are done. 282 break; 283 } 284 } 285 286 debug(blocksound_verbose) { 287 import std.stdio; 288 writeln("[BlockSound]: Exiting dedicated streaming thread."); 289 } 290 } 291 292 /// OpenAL Sound backend 293 class ALSound : Sound { 294 private ALuint _buffer; 295 296 @property ALuint buffer() @safe nothrow { return _buffer; } 297 298 protected this(ALuint buffer) @safe nothrow { 299 _buffer = buffer; 300 } 301 302 static ALSound loadSound(in string filename) @trusted { 303 return new ALSound(loadSoundToBuffer(filename)); 304 } 305 306 override void cleanup() @trusted nothrow { 307 alDeleteBuffers(1, &_buffer); 308 } 309 } 310 311 /// OpenAL Sound backend (for streaming) 312 class ALStreamedSound : StreamedSound { 313 private string filename; 314 private SF_INFO soundInfo; 315 private SNDFILE* file; 316 317 package ALuint numBuffers; 318 package ALuint[] buffers; 319 320 private this(in string filename, SF_INFO soundInfo, SNDFILE* file, ALuint numBuffers) @safe { 321 this.filename = filename; 322 this.soundInfo = soundInfo; 323 this.file = file; 324 this.numBuffers = numBuffers; 325 326 buffers = new ALuint[numBuffers]; 327 } 328 329 static ALStreamedSound loadSound(in string filename, in ALuint bufferNumber = 4) @system { 330 import std.exception : enforce; 331 import std.file : exists; 332 333 enforce(INIT, new Exception("BlockSound has not been initialized!")); 334 enforce(exists(filename), new Exception("File \"" ~ filename ~ "\" does not exist!")); 335 336 SF_INFO info; 337 SNDFILE* file; 338 339 file = sf_open(toCString(filename), SFM_READ, &info); 340 341 ALStreamedSound sound = new ALStreamedSound(filename, info, file, bufferNumber); 342 for(size_t i = 0; i < bufferNumber; i++) { 343 ALuint buffer = sound.queueBuffer(); 344 sound.buffers[i] = buffer; 345 } 346 347 return sound; 348 } 349 350 /// Reset the file to the beginning. 351 package void reset() @system { 352 import core.stdc.stdio : SEEK_SET; 353 sf_seek(file, 0, SEEK_SET); 354 } 355 356 private ALuint queueBuffer() @system { 357 ALuint buffer; 358 alGenBuffers(1, &buffer); 359 360 AudioBufferFloat ab = sndfile_readFloats(file, soundInfo, 2400); 361 alBufferData(buffer, soundInfo.channels == 1 ? AL_FORMAT_MONO_FLOAT32 : AL_FORMAT_STEREO_FLOAT32, ab.data.ptr, cast(int) (ab.data.length * float.sizeof), soundInfo.samplerate); 362 return buffer; 363 } 364 365 override { 366 void cleanup() @trusted { 367 alDeleteBuffers(numBuffers, buffers.ptr); 368 sf_close(file); 369 } 370 } 371 } 372 373 /++ 374 Read an amount of shorts from a sound file using libsndfile. 375 +/ 376 deprecated("Reading as shorts can cause cracks in audio") 377 AudioBuffer sndfile_readShorts(SNDFILE* file, SF_INFO info, size_t frames) @system { 378 AudioBuffer ab; 379 380 ab.data = new short[frames * info.channels]; 381 382 if((ab.remaining = sf_read_short(file, ab.data.ptr, ab.data.length)) <= 0) { 383 throw new EOFException("EOF!"); 384 } 385 386 return ab; 387 } 388 389 /++ 390 Read an amount of shorts from a sound file using libsndfile. 391 +/ 392 AudioBufferFloat sndfile_readFloats(SNDFILE* file, SF_INFO info, size_t frames) @system { 393 AudioBufferFloat ab; 394 395 ab.data = new float[frames * info.channels]; 396 397 if((ab.remaining = sf_read_float(file, ab.data.ptr, ab.data.length)) <= 0) { 398 throw new EOFException("EOF!"); 399 } 400 401 return ab; 402 } 403 404 405 deprecated("Reading as shorts can cause cracks in audio") 406 package struct AudioBuffer { 407 short[] data; 408 sf_count_t remaining; 409 } 410 411 package struct AudioBufferFloat { 412 float[] data; 413 sf_count_t remaining; 414 } 415 416 /++ 417 Loads a sound from a file into an OpenAL buffer. 418 Uses libsndfile for file reading. 419 420 Params: 421 filename = The filename where the sound is located. 422 423 Throws: Exception if file is not found, or engine is not initialized. 424 Returns: An OpenAL buffer containing the sound. 425 +/ 426 ALuint loadSoundToBuffer(in string filename) @system { 427 import std.exception : enforce; 428 import std.file : exists; 429 430 enforce(INIT, new Exception("BlockSound has not been initialized!")); 431 enforce(exists(filename), new Exception("File \"" ~ filename ~ "\" does not exist!")); 432 433 SF_INFO info; 434 SNDFILE* file = sf_open(toCString(filename), SFM_READ, &info); 435 436 float[] data; 437 float[] readBuf = new float[2048]; 438 439 long readSize = 0; 440 while((readSize = sf_read_float(file, readBuf.ptr, readBuf.length)) != 0) { 441 data ~= readBuf[0..(cast(size_t) readSize)]; 442 } 443 444 ALuint buffer; 445 alGenBuffers(1, &buffer); 446 debug(blocksound_soundInfo) { 447 import std.stdio : writeln; 448 writeln("Loading sound ", filename, ": has ", info.channels, " channels."); 449 } 450 alBufferData(buffer, info.channels == 1 ? AL_FORMAT_MONO_FLOAT32 : AL_FORMAT_STEREO_FLOAT32, data.ptr, cast(int) (data.length * float.sizeof), info.samplerate); 451 452 sf_close(file); 453 454 return buffer; 455 } 456 457 /++ 458 Loads libraries required by the OpenAL backend. 459 This is called automatically by blocksound's init 460 function. 461 462 Params: 463 skipALload = Skips loading OpenAL from derelict. 464 Set this to true if your application loads 465 OpenAL itself before blocksound does. 466 467 skipSFLoad = Skips loading libsndfile from derelict. 468 Set this to true if your application loads 469 libsdnfile itself before blocksound does. 470 +/ 471 void loadLibraries(bool skipALload = false, bool skipSFload = false) @system { 472 if(!skipALload) { 473 version(Windows) { 474 try { 475 DerelictAL.load(); // Search for system libraries first. 476 debug(blocksound_verbose) notifyLoadLib("OpenAL"); 477 } catch(Exception e) { 478 DerelictAL.load("lib\\openal32.dll"); // Try to use provided library. 479 debug(blocksound_verbose) notifyLoadLib("OpenAL"); 480 } 481 } else { 482 DerelictAL.load(); 483 debug(blocksound_verbose) notifyLoadLib("OpenAL"); 484 } 485 } 486 487 if(!skipSFload) { 488 version(Windows) { 489 try { 490 DerelictSndFile.load(); // Search for system libraries first. 491 debug(blocksound_verbose) notifyLoadLib("libsndfile"); 492 } catch(Exception e) { 493 DerelictSndFile.load("lib\\libsndfile-1.dll"); // Try to use provided library. 494 debug(blocksound_verbose) notifyLoadLib("libsndfile"); 495 } 496 } else { 497 DerelictSndFile.load(); 498 debug(blocksound_verbose) notifyLoadLib("libsndfile"); 499 } 500 } 501 } 502 }