1 module polyplex.core.audio.music; 2 import polyplex.core.audio; 3 import polyplex.core.audio.effect; 4 import ppc.types.audio; 5 import openal; 6 import ppc.backend.cfile; 7 import polyplex.math; 8 import std.concurrency; 9 import std.stdio; 10 import polyplex.utils.logging; 11 import core.thread; 12 import core.time; 13 import core.sync.mutex; 14 15 // This is set to true on application close, so that threads know to quit. 16 protected __gshared bool shouldStop; 17 protected __gshared int openMusicChannels; 18 19 private void iHandlerLaunch() { 20 synchronized { 21 openMusicChannels++; 22 } 23 } 24 25 private void iHandlerStop() { 26 synchronized { 27 openMusicChannels--; 28 } 29 } 30 31 protected void MusicHandlerThread(Music iMus) { 32 try { 33 iHandlerLaunch; 34 int buffersProcessed; 35 ALuint deqBuff; 36 ALint state; 37 byte[] streamBuffer = new byte[iMus.bufferSize]; 38 39 size_t startBufferSize = Music.BufferSize; 40 41 // Stop thread if requested. 42 while (iMus !is null && &shouldStop !is null && !shouldStop && !iMus.shouldStopInternal) { 43 44 // Sleep needed to make the CPU NOT run at 100% at all times 45 Thread.sleep(10.msecs); 46 47 // Check if we have any buffers to fill 48 alGetSourcei(iMus.source, AL_BUFFERS_PROCESSED, &buffersProcessed); 49 iMus.totalProcessed += buffersProcessed; 50 while(buffersProcessed > 0) { 51 52 // Find the ID of the buffer to fill 53 deqBuff = 0; 54 alSourceUnqueueBuffers(iMus.source, 1, &deqBuff); 55 56 // Resize buffers if needed 57 if (startBufferSize != Music.BufferSize) { 58 streamBuffer.length = Music.BufferSize; 59 startBufferSize = Music.BufferSize; 60 iMus.bufferSize = cast(int)Music.BufferSize; 61 } 62 63 // Read audio from stream in 64 size_t readData = iMus.stream.read(streamBuffer.ptr, iMus.bufferSize); 65 if (readData == 0) { 66 if (!iMus.looping) { 67 iMus.playing = false; 68 iHandlerStop; 69 return; 70 } 71 iMus.stream.seek; 72 readData = iMus.stream.read(streamBuffer.ptr, iMus.bufferSize); 73 } 74 75 // Send to OpenAL 76 alBufferData(deqBuff, iMus.fFormat, streamBuffer.ptr, cast(int)readData, cast(int)iMus.stream.info.bitrate); 77 alSourceQueueBuffers(iMus.source, 1, &deqBuff); 78 79 buffersProcessed--; 80 } 81 82 // OpenAL will stop the stream if it runs out of buffers 83 // Ensure that if OpenAL stops, we start it again. 84 alGetSourcei(iMus.source, AL_SOURCE_STATE, &state); 85 if (state != AL_PLAYING) { 86 iMus.xruns++; 87 Logger.Warn("Music buffer X-run!"); 88 alSourcePlay(iMus.source); 89 } 90 } 91 } catch(Exception ex) { 92 Logger.Err("MusicHandlerException: {0}", ex.message); 93 } catch (Error err) { 94 Logger.Err("MusicHandlerError: {0}", err.message); 95 } 96 iHandlerStop; 97 } 98 99 public class Music { 100 private: 101 // Internal thread-communication data. 102 size_t totalProcessed; 103 Thread musicThread; 104 bool shouldStopInternal; 105 106 // Buffer 107 Audio stream; 108 ALuint[] buffer; 109 AudioRenderFormats fFormat; 110 111 // Buffer sizing 112 int bufferSize; 113 int buffers; 114 115 // Settings 116 bool looping; 117 bool paused; 118 bool playing; 119 120 int xruns; 121 122 // Effects & filters 123 AudioEffect attachedEffect; 124 AudioFilter attachedFilter; 125 126 void applyEffectsAndFilters() { 127 ALuint efId = attachedEffect !is null ? attachedEffect.Id : AL_EFFECTSLOT_NULL; 128 ALuint flId = attachedFilter !is null ? attachedFilter.Id : AL_FILTER_NULL; 129 130 Logger.Debug("Applying effect {0} and filter {1} on Music {2}...", efId, flId, source); 131 132 alSource3i(source, AL_AUXILIARY_SEND_FILTER, efId, 0, flId); 133 alSourcei(source, AL_DIRECT_FILTER, flId); 134 ErrCodes err = cast(ErrCodes)alGetError(); 135 136 import std.conv; 137 if (cast(ALint)err != AL_NO_ERROR) throw new Exception("Failed to attach effect and/or filter to SoundEffect instance "~err.to!string); 138 } 139 140 // Source 141 ALuint source; 142 143 public: 144 145 /// Changes the size of the buffers for music playback. 146 /// Changes take effect immidiately 147 static size_t BufferSize = 48_000; 148 149 /// Amount of buffers to provision per audio track 150 /// Takes effect on new audio load. 151 static size_t BufferCount = 16; 152 153 /// Constructs a Music via an Audio source. 154 // TODO: Optimize enough so that we can have fewer buffers 155 this(Audio audio, AudioRenderFormats format = AudioRenderFormats.Auto) { 156 stream = audio; 157 158 // Select format if told to. 159 if (format == AudioRenderFormats.Auto) { 160 import std.conv; 161 if (stream.info.channels == 1) fFormat = AudioRenderFormats.Mono16; 162 else if (stream.info.channels == 2) fFormat = AudioRenderFormats.Stereo16; 163 else throw new Exception("Unsupported amount of channels! " ~ stream.info.channels.to!string); 164 } 165 166 // Clear errors 167 alGetError(); 168 169 // Prepare buffer arrays 170 this.buffers = cast(int)BufferCount; 171 buffer = new ALuint[buffers]; 172 173 this.bufferSize = (cast(int)BufferSize)*stream.info.channels; 174 175 alGenBuffers(buffers, buffer.ptr); 176 177 // Prepare sources 178 alGenSources(1, &source); 179 180 prestream(); 181 182 Logger.VerboseDebug("Created new Music! source={3} buffers={0} bufferSize={1} ids={2}", buffers, bufferSize, buffer, source); 183 } 184 185 ~this() { 186 cleanup(); 187 } 188 189 private void cleanup() { 190 // Cleanup procedure 191 alDeleteSources(1, &source); 192 alDeleteBuffers(buffers, buffer.ptr); 193 } 194 195 private void prestream() { 196 byte[] streamBuffer = new byte[bufferSize]; 197 198 // Pre-read some data. 199 foreach (i; 0 .. buffers) { 200 201 // Read audio from stream in 202 size_t read = stream.read(streamBuffer.ptr, bufferSize); 203 204 // Send to OpenAL 205 alBufferData(buffer[i], this.fFormat, streamBuffer.ptr, cast(int)read, cast(int)stream.info.bitrate); 206 alSourceQueueBuffers(source, 1, &buffer[i]); 207 208 } 209 } 210 211 // Spawns player thread. 212 private void spawnHandler() { 213 if (musicThread !is null && musicThread.isRunning) return; 214 215 musicThread = new Thread({ 216 MusicHandlerThread(this); 217 }); 218 musicThread.start(); 219 } 220 221 /// Play music 222 void Play(bool isLooping = false) { 223 synchronized { 224 if (musicThread is null || !musicThread.isRunning) spawnHandler(); 225 226 this.Looping = isLooping; 227 alSourcePlay(source); 228 229 paused = false; 230 playing = true; 231 } 232 } 233 234 /// Stop playing music 235 /// Does nothing if music isn't playing. 236 void Stop() { 237 synchronized { 238 if (musicThread is null) return; 239 240 // Tell thread to die repeatedly, until it does. 241 while(musicThread.isRunning) shouldStopInternal = true; 242 243 // Stop source and prepare for playing again 244 alSourceStop(source); 245 stream.seek(); 246 prestream(); 247 248 playing = false; 249 } 250 } 251 252 /// Pause music 253 void Pause() { 254 synchronized { 255 alSourcePause(source); 256 paused = true; 257 playing = false; 258 } 259 } 260 261 /// Gets the current position in the music stream (in samples) 262 size_t Tell() { 263 synchronized { 264 return stream.tell; 265 } 266 } 267 268 /// Seeks the music stream (in samples) 269 void Seek(size_t position = 0) { 270 synchronized { 271 if (position >= Length) position = Length-1; 272 // Stop stream 273 alSourceStop(source); 274 275 // Unqueue everything 276 alSourceUnqueueBuffers(source, buffers, buffer.ptr); 277 278 // Refill buffers 279 stream.seekSample(position); 280 prestream(); 281 282 // Resume 283 if (playing) alSourcePlay(source); 284 } 285 } 286 287 /// Gets length of music (in samples) 288 size_t Length() { 289 return stream.info.pcmLength; 290 } 291 292 /// Get amount of unhandled XRuns that has happened. 293 int XRuns() { 294 synchronized { 295 return xruns; 296 } 297 } 298 299 /// Mark XRuns as handled. 300 void HandledXRun() { 301 synchronized { 302 xruns = 0; 303 } 304 } 305 306 /// Gets wether the music is paused. 307 bool Paused() { 308 return paused; 309 } 310 311 /// Gets wether the music is playing 312 bool Playing() { 313 return playing; 314 } 315 316 @property AudioEffect Effect() { 317 return this.attachedEffect; 318 } 319 320 @property void Effect(AudioEffect effect) { 321 attachedEffect = effect; 322 applyEffectsAndFilters(); 323 } 324 325 @property AudioFilter Filter() { 326 return this.attachedFilter; 327 } 328 329 @property void Filter(AudioFilter filter) { 330 attachedFilter = filter; 331 applyEffectsAndFilters(); 332 } 333 334 @property bool Looping() { 335 return looping; 336 } 337 @property void Looping(bool val) { looping = val; } 338 339 @property int ByteOffset() { 340 int v = 0; 341 alGetSourcei(source, AL_BYTE_OFFSET, &v); 342 return v; 343 } 344 345 @property int SecondOffset() { 346 int v = 0; 347 alGetSourcei(source, AL_SEC_OFFSET, &v); 348 return v; 349 } 350 351 @property int SampleOffset() { 352 int v = 0; 353 alGetSourcei(source, AL_SAMPLE_OFFSET, &v); 354 return v; 355 } 356 357 /* 358 PITCH 359 */ 360 @property float Pitch() { 361 float v = 0f; 362 alGetSourcef(source, AL_PITCH, &v); 363 return v; 364 } 365 @property void Pitch(float val) { alSourcef(source, AL_PITCH, val); } 366 367 /* 368 GAIN 369 */ 370 @property float Gain() { 371 float v = 0f; 372 alGetSourcef(source, AL_GAIN, &v); 373 return v; 374 } 375 @property void Gain(float val) { alSourcef(source, AL_GAIN, val); } 376 377 /* 378 MIN GAIN 379 */ 380 @property float MinGain() { 381 float v = 0f; 382 alGetSourcef(source, AL_MIN_GAIN, &v); 383 return v; 384 } 385 @property void MinGain(float val) { alSourcef(source, AL_MIN_GAIN, val); } 386 387 /* 388 MAX GAIN 389 */ 390 @property float MaxGain() { 391 float v = 0f; 392 alGetSourcef(source, AL_MAX_GAIN, &v); 393 return v; 394 } 395 @property void MaxGain(float val) { alSourcef(source, AL_MAX_GAIN, val); } 396 397 /* 398 MAX DISTANCE 399 */ 400 @property float MaxDistance() { 401 float v = 0f; 402 alGetSourcef(source, AL_MAX_DISTANCE, &v); 403 return v; 404 } 405 @property void MaxDistance(float val) { alSourcef(source, AL_MAX_DISTANCE, val); } 406 407 /* 408 ROLLOFF FACTOR 409 */ 410 @property float RolloffFactor() { 411 float v = 0f; 412 alGetSourcef(source, AL_ROLLOFF_FACTOR, &v); 413 return v; 414 } 415 @property void RolloffFactor(float val) { alSourcef(source, AL_ROLLOFF_FACTOR, val); } 416 417 /* 418 CONE OUTER GAIN 419 */ 420 @property float ConeOuterGain() { 421 float v = 0f; 422 alGetSourcef(source, AL_CONE_OUTER_GAIN, &v); 423 return v; 424 } 425 @property void ConeOuterGain(float val) { alSourcef(source, AL_CONE_OUTER_GAIN, val); } 426 427 /* 428 CONE INNER ANGLE 429 */ 430 @property float ConeInnerAngle() { 431 float v = 0f; 432 alGetSourcef(source, AL_CONE_INNER_ANGLE, &v); 433 return v; 434 } 435 @property void ConeInnerAngle(float val) { alSourcef(source, AL_CONE_INNER_ANGLE, val); } 436 437 /* 438 CONE OUTER ANGLE 439 */ 440 @property float ConeOuterAngle() { 441 float v = 0f; 442 alGetSourcef(source, AL_CONE_OUTER_ANGLE, &v); 443 return v; 444 } 445 @property void ConeOuterAngle(float val) { alSourcef(source, AL_CONE_OUTER_ANGLE, val); } 446 447 /* 448 REFERENCE DISTANCE 449 */ 450 @property float ReferenceDistance() { 451 float v = 0f; 452 alGetSourcef(source, AL_REFERENCE_DISTANCE, &v); 453 return v; 454 } 455 @property void ReferenceDistance(float val) { alSourcef(source, AL_REFERENCE_DISTANCE, val); } 456 457 /* 458 POSITION 459 */ 460 @property Vector3 Position() { 461 Vector3 v = 0f; 462 alGetSourcefv(source, AL_POSITION, v.ptr); 463 return v; 464 } 465 @property void Position(Vector3 val) { alSourcefv(source, AL_POSITION, val.ptr); } 466 467 /* 468 VELOCITY 469 */ 470 @property Vector3 Velocity() { 471 Vector3 v = 0f; 472 alGetSourcefv(source, AL_VELOCITY, v.ptr); 473 return v; 474 } 475 @property void Velocity(Vector3 val) { alSourcefv(source, AL_VELOCITY, val.ptr); } 476 477 /* 478 DIRECTION 479 */ 480 @property Vector3 Direction() { 481 Vector3 v = 0f; 482 alGetSourcefv(source, AL_DIRECTION, v.ptr); 483 return v; 484 } 485 @property void Direction(Vector3 val) { alSourcefv(source, AL_DIRECTION, val.ptr); } 486 }