1 /*
2     Copyright © 2020, Luna Nielsen
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4     
5     Authors: Luna Nielsen
6 */
7 module engine.audio.music;
8 import core.thread;
9 import engine.audio.astream;
10 import bindbc.openal;
11 import engine.math;
12 import std.math : isFinite;
13 import engine;
14 import events;
15 
16 /**
17     The game's main playlist instance
18 */
19 __gshared Playlist GamePlaylist;
20 
21 /**
22     Initializes the game's main playlist instance
23 */
24 void initPlaylist() {
25     GamePlaylist = new Playlist();
26     GamePlaylist.addMusicFrom("assets/music/");
27 }
28 
29 /**
30     A playlist of music
31 */
32 class Playlist {
33 private:
34     bool isStopped = true;
35     size_t current;
36     Music[] songs;
37 
38     // Smarter shuffle function that avoids playing the same track twice
39     size_t smartShuffle() {
40         import std.random : uniform;
41         size_t value;
42         do {
43             value = uniform(0, songs.length);
44         } while(value != current);
45         return value;
46     }
47 
48     void nextSong() {
49 
50         // If we skip to the next song stop the current one from playing
51         if (songs[current].isPlaying) {
52             songs[current].stop();
53         }
54 
55         // Next song is the same
56         if (repeatOne) {
57             songs[current].play();
58             return;
59         }
60 
61         if (shuffle) {
62 
63             // Shuffle to next song using the "smart" algorithm
64             current = smartShuffle();
65         } else {
66             // Increment song id
67             current++;
68 
69             // Constrain ids to the length of the song array
70             current %= songs.length;
71         }
72 
73         // Play the new song
74         songs[current].play();
75         onSongChange(cast(void*)this, songs[current].getInfo());
76     }
77 
78 public:
79 
80     ~this() {
81         foreach(song; songs) {
82             destroy(song);
83         }
84     }
85 
86     /**
87         Repeat a single song
88     */
89     bool repeatOne;
90 
91     /**
92         Shuffle songs
93     */
94     bool shuffle;
95 
96     /**
97         Event called when song changes
98     */
99     Event!AudioInfo onSongChange = new Event!AudioInfo();
100 
101     /**
102         Add Music to the playlist
103     */
104     void addMusic(Music music) {
105         songs ~= music;
106 
107         // We want to roll over to the first song so set it to the last
108         current = songs.length-1;
109     }
110 
111     /**
112         Adds all .ogg files that could be found in a path recursively
113     */
114     void addMusicFrom(string path) {
115         import std.file : dirEntries, SpanMode;
116         import std.algorithm : filter, endsWith;
117         foreach(entry; dirEntries(path, SpanMode.depth).filter!(f => f.name.endsWith(".ogg"))) {
118             addMusic(new Music(entry));
119             AppLog.info("Playlist", "Added song %s...", entry);
120         }
121     }
122 
123     /**
124         Play the playlist
125     */
126     void play() {
127         isStopped = false;
128         nextSong();
129     }
130 
131     /**
132         Stop music from playing
133     */
134     void stop() {
135         isStopped = true;
136         songs[current].stop();
137     }
138 
139     /**
140         Gets whether the current song is playing
141     */
142     bool isCurrentSongPlaying() {
143         return songs[current].isPlaying();
144     }
145 
146     /**
147         Play next song
148     */
149     void next() {
150 
151         // Avoid accidentally starting the playlist again
152         if (isStopped) return;
153 
154         // Play the next song in the playlist
155         nextSong();
156     }
157 
158     /**
159         Updates the playlist
160     */
161     void update() {
162 
163         // Skip if we're stopped
164         if (isStopped) return;
165 
166         // Play next song if the current one is stopped
167         if (!isCurrentSongPlaying) next();
168     }
169 }
170 
171 /**
172     A stream of audio that can be played
173 */
174 class Music {
175 private:
176     enum MUSIC_BUFF_SIZE = 4096;
177     enum MUSIC_BUFF_COUNT = 4;
178 
179     long lastReadLength;
180     AudioStream stream;
181     ALuint sourceId;
182     ALint processed;
183 
184     bool running;
185     bool looping;
186 
187     Thread playerThread;
188     void playThread() {
189 
190         // The processing buffer
191         ALuint pBuf;
192         ubyte[] pBufData = new ubyte[MUSIC_BUFF_SIZE*MUSIC_BUFF_COUNT];
193         ALint state;
194         
195         // The buffer chain
196         ALuint[MUSIC_BUFF_COUNT] buffers;
197         alGenBuffers(MUSIC_BUFF_COUNT, buffers.ptr);
198 
199         // Fill buffers with initial data
200         foreach(i; 0..MUSIC_BUFF_COUNT) {
201             lastReadLength = stream.readSamples(pBufData);
202             alBufferData(buffers[i], stream.format, pBufData.ptr, cast(int)lastReadLength, cast(int)stream.bitrate);
203             alSourceQueueBuffers(sourceId, 1, &buffers[i]);
204         }
205 
206         // Start playing
207         alSourcePlay(sourceId);
208         mainLoop: while(running) {
209 
210             // Check how much data OpenAL has processed
211             alGetSourcei(sourceId, AL_BUFFERS_PROCESSED, &processed);
212             alGetError();
213 
214             while(processed--) {
215 
216                 // Unqueue the most recent cleared buffer
217                 alSourceUnqueueBuffers(sourceId, 1, &pBuf);
218 
219                 // Read samples to buffer
220                 lastReadLength = stream.readSamples(pBufData);
221 
222                 if (lastReadLength == 0) {
223                     // If we're at the end and we should loop then loop. (if possible)
224                     if (looping && stream.canSeek) {
225 
226                         // Seek back to start of stream and read samples
227                         stream.seek(0);
228                         lastReadLength = stream.readSamples(pBufData);
229 
230                         debug AppLog.info("Music Debug", "Music %s looped...", sourceId);
231 
232                     } else {
233                         break mainLoop;
234                     }
235                 }
236 
237                 // Buffer the data to OpenAL
238                 alBufferData(pBuf, stream.format, pBufData.ptr, cast(int)lastReadLength, cast(int)stream.bitrate);
239 
240                 // Re-queue buffer
241                 alSourceQueueBuffers(sourceId, 1, &pBuf);
242 
243                 // Get the current state of the buffer
244                 alGetSourcei(sourceId, AL_SOURCE_STATE, &state);
245 
246                 // If stream is paused keep pausing here
247                 while (state == AL_PAUSED) {
248 
249                     // Quit out if the music is stopped
250                     if (!running) break mainLoop;
251 
252                     // Otherwise wait
253                     alGetSourcei(sourceId, AL_SOURCE_STATE, &state);
254                     Thread.sleep(10.msecs);
255                 }
256 
257                 // Make sure if the buffer stops (due to running out) that we restart it
258                 if (state != AL_PLAYING) {
259                     alSourcePlay(sourceId);
260                 }
261             }
262             
263 
264             // Don't make the thread use all of the cpu
265             Thread.sleep(10.msecs);
266         }
267 
268         // Cleanup
269         stream.seek(0);
270         running = false;
271         alDeleteBuffers(2, buffers.ptr);
272 
273         debug AppLog.info("Music Debug", "Music %s stopped...", sourceId);
274     }
275 
276 public:
277     ~this() {
278         this.stop();
279         alDeleteSources(1, &sourceId);
280     }
281 
282     /**
283         Construct a sound from a file path
284     */
285     this(string file) {
286         this(open(file));
287     }
288 
289     /**
290         Construct a sound
291     */
292     this(AudioStream stream) {
293 
294         // Generate buffer
295         this.stream = stream;
296 
297         // Generate source
298         alGenSources(1, &sourceId);
299         alSourcef(sourceId, AL_PITCH, 1);
300         alSourcef(sourceId, AL_GAIN, 0.5);
301     }
302 
303     /**
304         Play sound
305     */
306     void play(float gain = float.nan, float pitch = float.nan) {
307         
308         // We don't want to start multiple threads playing the same music
309         if (running) return;
310 
311         // Seek back to start of music (just in case)
312         if (lastReadLength == 0) {
313             stream.seek(0);
314         }
315 
316         // Set music start values if needed
317         if (pitch.isFinite) alSourcef(sourceId, AL_PITCH, pitch);
318         if (gain.isFinite) alSourcef(sourceId, AL_GAIN, gain);
319 
320         // Start thread and play music
321         running = true;
322         playerThread = new Thread(&playThread);
323         playerThread.start();
324     }
325 
326     /**
327         Set the pitch of this music
328     */
329     void setPitch(float pitch) {
330         alSourcef(sourceId, AL_PITCH, pitch);
331     }
332 
333     /**
334         Set the pitch of this music
335     */
336     void setGain(float gain) {
337         alSourcef(sourceId, AL_GAIN, gain);
338     }
339 
340     /**
341         Set the pitch of this music
342     */
343     void setLooping(bool loop) {
344         looping = loop;
345     }
346 
347     /**
348         Pause the song
349     */
350     void pause() {
351         alSourcePause(sourceId);
352     }
353 
354     /**
355         Stop sound
356     */
357     void stop() {
358         running = false;
359         alSourceStop(sourceId);
360         if (playerThread !is null) {
361             playerThread.join();
362             playerThread = null;
363         }
364     }
365 
366     /**
367         Gets whether a song is currently playing
368     */
369     bool isPlaying() {
370         return running;
371     }
372 
373     /**
374         Get info about the music
375     */
376     AudioInfo getInfo() {
377         return stream.getInfo();
378     }
379 }