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 }