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 }