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; 195 import core.thread : Thread; 196 197 bool hasFinished = false; 198 bool isPlaying = false; 199 bool loop = false; 200 201 //bool waitForLoop = false; // Wait for AL to go through all the buffers. 202 203 debug(blocksound_verbose) { 204 import std.stdio; 205 writeln("[BlockSound]: Started dedicated streaming thread."); 206 } 207 208 while(true) { 209 receiveTimeout(dur!("msecs")(1), // Check for messages from main thread. 210 (immutable size_t signal) { 211 switch(signal) { 212 case ALStreamingSource.STREAM_CMD_PLAY: 213 isPlaying = true; 214 break; 215 case ALStreamingSource.STREAM_CMD_PAUSE: 216 isPlaying = false; 217 break; 218 case ALStreamingSource.STREAM_CMD_STOP: 219 isPlaying = false; 220 hasFinished = true; 221 break; 222 223 case ALStreamingSource.STREAM_CMD_SET_LOOP_TRUE: 224 loop = true; 225 break; 226 case ALStreamingSource.STREAM_CMD_SET_LOOP_FALSE: 227 loop = false; 228 break; 229 default: 230 break; 231 } 232 }); 233 234 isPlayingLabel: 235 if(isPlaying) { // Check if we are supposed to be playing (refilling buffers) 236 ALint state, processed; 237 238 alGetSourcei(source.source, AL_SOURCE_STATE, &state); // Get the state of the audio. 239 alGetSourcei(source.source, AL_BUFFERS_PROCESSED, &processed); // Get the amount of buffers that OpenAL has played. 240 241 if(processed > 0) { 242 alSourceUnqueueBuffers(cast(ALuint) source.source, processed, (cast(ALuint[])sound.buffers).ptr); // Unqueue buffers that have been played. 243 debug(blocksound_verbose2) { 244 import std.stdio; 245 writeln("[BlockSound/DEBUG ", Clock.currTime().second, "]: STATE, ", state, " ", AL_PLAYING, " Unqueued ", processed, " buffers."); 246 } 247 248 alDeleteBuffers(processed, (cast(ALuint[])sound.buffers).ptr); // Delete the played buffers 249 250 for(size_t i = 0; i < processed; i++) { // Go through each buffer that was played. 251 try { 252 ALuint buffer = (cast(ALStreamedSound) sound).queueBuffer(); // load a new buffer 253 sound.buffers[i] = buffer; // Add it to the array 254 } catch(EOFException e) { // Check if we have finished reading the sound file 255 if(loop) { // Check if we are looping the sound. 256 debug(blocksound_verbose2) { 257 import std.stdio; 258 writeln("[BlockSound/DEBUG", Clock.currTime().second, "]: Resetting..."); 259 } 260 //alSourceStop((cast(ALStreamingSource) source).source); 261 (cast(ALStreamedSound) sound).reset(); 262 263 ALuint buffer = (cast(ALStreamedSound) sound).queueBuffer(); // load a new buffer 264 sound.buffers[i] = buffer; // Add it to the array 265 266 debug(blocksound_verbose) { 267 import std.stdio; 268 writeln("[BlockSound ", Clock.currTime().second, "]: Dedicated streaming thread reset."); 269 } 270 break; 271 } else { // We are done here, time to close up shop. 272 hasFinished = true; 273 source.finishedPlaying = true; // Notify main thread that we are done. 274 275 debug(blocksound_verbose) { 276 import std.stdio; 277 writeln("[BlockSound ", Clock.currTime().second, "]: Dedicated streaming thread finished."); 278 } 279 break; // Break out of the loop, and exit the thread. 280 } 281 } 282 } 283 284 alSourceQueueBuffers(cast(ALuint) source.source, processed, (cast(ALuint[])sound.buffers).ptr); // Queue the new buffers to OpenAL. 285 debug(blocksound_verbose2) { 286 import std.stdio; 287 writeln("[BlockSound/DEBUG ", Clock.currTime().second, "]: Queued ", processed, " buffers."); 288 } 289 } 290 291 if(state != AL_PLAYING && !hasFinished) { 292 debug(blocksound_verbose2) { 293 import std.stdio; 294 writeln("[BlockSound/DEBUG ", Clock.currTime().second, "]: Set to play."); 295 } 296 alSourcePlay(source.source); 297 } 298 299 300 Thread.sleep(dur!("msecs")(5)); // Sleep 5 msecs as to prevent high CPU usage. 301 } 302 303 if(hasFinished) { 304 source.finishedPlaying = true; // Notify main thread that we are done. 305 break; 306 } 307 } 308 309 debug(blocksound_verbose) { 310 import std.stdio; 311 writeln("[BlockSound]: Exiting dedicated streaming thread."); 312 } 313 } 314 315 /// OpenAL Sound backend 316 class ALSound : Sound { 317 private ALuint _buffer; 318 319 @property ALuint buffer() @safe nothrow { return _buffer; } 320 321 protected this(ALuint buffer) @safe nothrow { 322 _buffer = buffer; 323 } 324 325 static ALSound loadSound(in string filename) @trusted { 326 return new ALSound(loadSoundToBuffer(filename)); 327 } 328 329 override void cleanup() @trusted nothrow { 330 alDeleteBuffers(1, &_buffer); 331 } 332 } 333 334 /// OpenAL Sound backend (for streaming) 335 class ALStreamedSound : StreamedSound { 336 private string filename; 337 private SF_INFO soundInfo; 338 private SNDFILE* file; 339 340 package ALuint numBuffers; 341 package ALuint[] buffers; 342 343 private this(in string filename, SF_INFO soundInfo, SNDFILE* file, ALuint numBuffers) @safe { 344 this.filename = filename; 345 this.soundInfo = soundInfo; 346 this.file = file; 347 this.numBuffers = numBuffers; 348 349 buffers = new ALuint[numBuffers]; 350 } 351 352 static ALStreamedSound loadSound(in string filename, in ALuint bufferNumber = 4) @system { 353 import std.exception : enforce; 354 import std.file : exists; 355 356 enforce(INIT, new Exception("BlockSound has not been initialized!")); 357 enforce(exists(filename), new Exception("File \"" ~ filename ~ "\" does not exist!")); 358 359 SF_INFO info; 360 SNDFILE* file; 361 362 file = sf_open(toCString(filename), SFM_READ, &info); 363 364 ALStreamedSound sound = new ALStreamedSound(filename, info, file, bufferNumber); 365 for(size_t i = 0; i < bufferNumber; i++) { 366 ALuint buffer = sound.queueBuffer(); 367 sound.buffers[i] = buffer; 368 } 369 370 return sound; 371 } 372 373 /// Reset the file to the beginning. 374 package void reset() @system { 375 import core.stdc.stdio : SEEK_SET; 376 sf_seek(file, 0, SEEK_SET); 377 } 378 379 private ALuint queueBuffer() @system { 380 ALuint buffer; 381 alGenBuffers(1, &buffer); 382 383 AudioBufferFloat ab = sndfile_readFloats(file, soundInfo, 48_000); 384 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); 385 return buffer; 386 } 387 388 override { 389 void cleanup() @trusted { 390 alDeleteBuffers(numBuffers, buffers.ptr); 391 sf_close(file); 392 } 393 } 394 } 395 396 /++ 397 Read an amount of shorts from a sound file using libsndfile. 398 +/ 399 deprecated("Reading as shorts can cause cracks in audio") 400 AudioBuffer sndfile_readShorts(SNDFILE* file, SF_INFO info, size_t frames) @system { 401 AudioBuffer ab; 402 403 ab.data = new short[frames * info.channels]; 404 405 if((ab.remaining = sf_read_short(file, ab.data.ptr, ab.data.length)) <= 0) { 406 throw new EOFException("EOF!"); 407 } 408 409 return ab; 410 } 411 412 /++ 413 Read an amount of shorts from a sound file using libsndfile. 414 +/ 415 AudioBufferFloat sndfile_readFloats(SNDFILE* file, SF_INFO info, size_t frames) @system { 416 AudioBufferFloat ab; 417 418 ab.data = new float[frames * info.channels]; 419 420 if((ab.remaining = sf_read_float(file, ab.data.ptr, ab.data.length)) <= 0) { 421 throw new EOFException("EOF!"); 422 } 423 424 return ab; 425 } 426 427 428 deprecated("Reading as shorts can cause cracks in audio") 429 package struct AudioBuffer { 430 short[] data; 431 sf_count_t remaining; 432 } 433 434 package struct AudioBufferFloat { 435 float[] data; 436 sf_count_t remaining; 437 } 438 439 /++ 440 Loads a sound from a file into an OpenAL buffer. 441 Uses libsndfile for file reading. 442 443 Params: 444 filename = The filename where the sound is located. 445 446 Throws: Exception if file is not found, or engine is not initialized. 447 Returns: An OpenAL buffer containing the sound. 448 +/ 449 ALuint loadSoundToBuffer(in string filename) @system { 450 import std.exception : enforce; 451 import std.file : exists; 452 453 enforce(INIT, new Exception("BlockSound has not been initialized!")); 454 enforce(exists(filename), new Exception("File \"" ~ filename ~ "\" does not exist!")); 455 456 SF_INFO info; 457 SNDFILE* file = sf_open(toCString(filename), SFM_READ, &info); 458 459 float[] data; 460 float[] readBuf = new float[2048]; 461 462 long readSize = 0; 463 while((readSize = sf_read_float(file, readBuf.ptr, readBuf.length)) != 0) { 464 data ~= readBuf[0..(cast(size_t) readSize)]; 465 } 466 467 ALuint buffer; 468 alGenBuffers(1, &buffer); 469 debug(blocksound_soundInfo) { 470 import std.stdio : writeln; 471 writeln("Loading sound ", filename, ": has ", info.channels, " channels."); 472 } 473 alBufferData(buffer, info.channels == 1 ? AL_FORMAT_MONO_FLOAT32 : AL_FORMAT_STEREO_FLOAT32, data.ptr, cast(int) (data.length * float.sizeof), info.samplerate); 474 475 sf_close(file); 476 477 return buffer; 478 } 479 480 /++ 481 Loads libraries required by the OpenAL backend. 482 This is called automatically by blocksound's init 483 function. 484 485 Params: 486 skipALload = Skips loading OpenAL from derelict. 487 Set this to true if your application loads 488 OpenAL itself before blocksound does. 489 490 skipSFLoad = Skips loading libsndfile from derelict. 491 Set this to true if your application loads 492 libsdnfile itself before blocksound does. 493 +/ 494 void loadLibraries(bool skipALload = false, bool skipSFload = false) @system { 495 if(!skipALload) { 496 version(Windows) { 497 try { 498 DerelictAL.load(); // Search for system libraries first. 499 debug(blocksound_verbose) notifyLoadLib("OpenAL"); 500 } catch(Exception e) { 501 DerelictAL.load("lib\\openal32.dll"); // Try to use provided library. 502 debug(blocksound_verbose) notifyLoadLib("OpenAL"); 503 } 504 } else { 505 DerelictAL.load(); 506 debug(blocksound_verbose) notifyLoadLib("OpenAL"); 507 } 508 } 509 510 if(!skipSFload) { 511 version(Windows) { 512 try { 513 DerelictSndFile.load(); // Search for system libraries first. 514 debug(blocksound_verbose) notifyLoadLib("libsndfile"); 515 } catch(Exception e) { 516 DerelictSndFile.load("lib\\libsndfile-1.dll"); // Try to use provided library. 517 debug(blocksound_verbose) notifyLoadLib("libsndfile"); 518 } 519 } else { 520 DerelictSndFile.load(); 521 debug(blocksound_verbose) notifyLoadLib("libsndfile"); 522 } 523 } 524 } 525 }