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 }