/// <summary>Call this only when PlayCueParameters have been validated</summary> public static void PlayCueSkipMissingCheck(IAudioDefinitions definitions, Cue cue, PlayCueParameters parameters, FadePitchPan fpp, bool loop = false) { fpp.fade = AudioMath.Clamp(fpp.fade * cue.volume, 0, 1); fpp.pitch = AudioMath.Clamp(fpp.pitch + parameters.cuePitch, -1, 1); fpp.pan = AudioMath.Clamp(fpp.pan + cue.pan, -1, 1); if (fpp.fade < 0.01f) { return; // too quiet to care about } if (cue.SoundCount == 0) { return; // <- nothing to play } lock (lockObject) { switch (cue.type) { case CueType.Parallel: { for (var i = 0; i < cue.SoundCount; i++) { AddAndStartPlaying(definitions.GetSound(cue, i), fpp, loop); } } break; case CueType.Serial: { // Build the queue for the cue: var queueHead = -1; for (var i = cue.SoundCount - 1; i >= 1; i--) { var q = AllocateQueuedSound(); queuedSounds[q].sound = definitions.GetSound(cue, i); queuedSounds[q].next = queueHead; queueHead = q; } AddAndStartPlaying(definitions.GetSound(cue, 0), fpp, loop, queueHead); } break; default: { AddAndStartPlaying(definitions.GetSound(cue, parameters.soundIndex), fpp, loop); } break; } } }
/// <param name="random">This parameter will be mutated. Be aware of network-safety!</param> public float SelectPitch(XorShift random) { // Pitch variance: var cuePitch = pitch; if (minPitch.HasValue && maxPitch.HasValue) { // NOTE: This is wrong because it is non-linear. But I can't be bothered fixing it at this point. -AR var p = 1 + cuePitch; // XNA normalizes on 0.0 var min = p * minPitch.GetValueOrDefault(); var max = p * maxPitch.GetValueOrDefault(); var randomValue = random._NetworkUnsafe_UseMeForAudioOnly_NextSingle(); AudioMath.Lerp(min, max, randomValue); cuePitch = pitch - 1; } return(cuePitch); }
public void Update(int localPlayerBits, bool ducking) { if (!AudioDevice.Available) { pendingSources.Clear(); pendingFadePitchPans.Clear(); return; } var fadeAmount = ducking ? -(1f / (SoundEffectManager.slotFadeTime * 60f)) : 1f / (SoundEffectManager.slotFadeTime * 60f); duckFade = AudioMath.Clamp(duckFade + fadeAmount, 0.4f, 1); // <- NOTE: not fading out all the way (sounds better) // // FIRST PASS: Pair sources with already-known associations: // for (var i = 0; i < pendingSources.Count;) { int association; if (previousAssociations.TryGetValue(pendingSources[i], out association)) { var sourceAmbientSound = pendingSources[i].AmbientSound; var liveSound = previousLiveSounds[association]; if (ReferenceEquals(liveSound.soundEffect, sourceAmbientSound.soundEffect.inner) && GameIsReceivingAmbientAudio(localPlayerBits)) { // Update sound pendingFadePitchPans[i].ApplyTo(liveSound.soundEffectInstance, duckFade * SafeSoundEffect.SoundEffectVolume); liveSound.position = pendingSources[i].Position; liveSound.expiryTimer = 0; // Transfer to next list previousLiveSounds[association] = null; // <- Prevent re-use later (cannot remove - indexes are used for associations) nextAssociations.Add(pendingSources[i], nextLiveSounds.Count); nextLiveSounds.Add(liveSound); // This sound source is now delt with (parallel removal) pendingSources.RemoveAtUnordered(i); pendingFadePitchPans.RemoveAtUnordered(i); continue; // <- in-loop removal } // Sound was changed - expire the associated sound immediately (assign a voice to the source later) liveSound.soundEffect = null; // <- prevent re-use in second pass liveSound.expiryTimer = ExpireTime; // <- flag for cleanup } i++; } previousAssociations.Clear(); // // SECOND PASS: Pair remaining pending sources with remaining live sounds based on effect and distance // for (var i = 0; i < pendingSources.Count;) { var sourceAmbientSound = pendingSources[i].AmbientSound; var sourcePosition = pendingSources[i].Position; // Find the closest live sound to assign as a voice var bestDistanceSquared = int.MaxValue; var bestIndex = -1; for (var j = 0; j < previousLiveSounds.Count;) { if (previousLiveSounds[j] == null) // Check for removal by first pass { previousLiveSounds.RemoveAtUnordered(j); // After first pass, we can remove properly continue; } var liveSound = previousLiveSounds[j]; if (ReferenceEquals(liveSound.soundEffect, sourceAmbientSound.soundEffect.inner)) { var distanceSquared = Position.DistanceSquared(liveSound.position, sourcePosition); if (distanceSquared < bestDistanceSquared) { bestDistanceSquared = distanceSquared; bestIndex = j; } } j++; } // At this point, we've found the best available voice if (bestIndex != -1) { var liveSound = previousLiveSounds[bestIndex]; // Update sound pendingFadePitchPans[i].ApplyTo(liveSound.soundEffectInstance, duckFade * SafeSoundEffect.SoundEffectVolume); liveSound.position = pendingSources[i].Position; liveSound.expiryTimer = 0; // Transfer to next list previousLiveSounds.RemoveAtUnordered(bestIndex); // After first pass, we can remove properly nextAssociations.Add(pendingSources[i], nextLiveSounds.Count); nextLiveSounds.Add(liveSound); // This sound source is now delt with (parallel removal) pendingSources.RemoveAtUnordered(i); pendingFadePitchPans.RemoveAtUnordered(i); continue; // <- in-loop removal } i++; } // // Silence remaining live sounds, apply expiration // foreach (var liveSound in previousLiveSounds) { // Note that we attempt to remove these in the previous loop, but that loop may not run if (liveSound == null) { continue; } liveSound.expiryTimer++; if (liveSound.expiryTimer > ExpireTime) { liveSound.soundEffectInstance.Dispose(); // <- this stops the sound // Remove from the live sound list by virtue of not adding it to the next list } else { const float fadeOutFrames = 10f; // <- quickly fade out any remaining sound from ambient sounds that stop existing var volume = liveSound.soundEffectInstance.Volume; liveSound.soundEffectInstance.Volume = Math.Max(0, volume - 1f / fadeOutFrames); nextLiveSounds.Add(liveSound); } } previousLiveSounds.Clear(); // // THIRD PASS: Spawn voices for remaining sound sources // for (var i = 0; i < pendingSources.Count; i++) { var ambientSound = pendingSources[i].AmbientSound; var sei = ambientSound.soundEffect.CreateInstance(); pendingFadePitchPans[i].ApplyTo(sei, duckFade * SafeSoundEffect.SoundEffectVolume); sei.IsLooped = true; sei.Play(); nextLiveSounds.Add(new LiveSound { soundEffect = ambientSound.soundEffect, soundEffectInstance = sei, position = pendingSources[i].Position, expiryTimer = 0 }); } pendingSources.Clear(); pendingFadePitchPans.Clear(); // // Cycle for next call // // These should have been cleared above Debug.Assert(pendingSources.Count == 0); Debug.Assert(pendingFadePitchPans.Count == 0); Debug.Assert(previousLiveSounds.Count == 0); Debug.Assert(previousAssociations.Count == 0); // Swaps: var tempLiveSounds = previousLiveSounds; previousLiveSounds = nextLiveSounds; nextLiveSounds = tempLiveSounds; var tempAssociations = previousAssociations; previousAssociations = nextAssociations; nextAssociations = tempAssociations; }
/// <returns>Returns true if this ambient sound is playable</returns> public static bool GetPlaybackInfoFor( AABB?aabb, Position position, bool facingLeft, int radius, float volume, float pitch, float pan, Camera camera, object gameState, int localPlayerBits, out FadePitchPan fadePitchPan) { // // Global ambient audio // if (radius < 0) { fadePitchPan = new FadePitchPan(1f); return(true); } // // Nominal audio position: // var worldToAudio = WorldToAudio(position, camera); fadePitchPan = new FadePitchPan(worldToAudio.pitch, worldToAudio.pan); fadePitchPan.fade *= volume; fadePitchPan.pitch *= pitch; fadePitchPan.pan *= pan; if (fadePitchPan.fade < 0.001f) // Far off-camera { fadePitchPan.fade = 0; return(false); } // // Distance to a listening player: // if (localPlayerBits == 0) // No one to listen { fadePitchPan.fade = 0; return(false); } var distanceSquared = GetDistanceSquaredToLocalPlayer(aabb, position, facingLeft, gameState, localPlayerBits); if (distanceSquared > radius * radius) { fadePitchPan.fade = 0; return(false); } // // Modulate by listening player distance and apply settings: // // Trying linear fade-out here var listenerFade = AudioMath.Clamp(1f - (float)Math.Sqrt(distanceSquared) / radius, 0f, 1f); fadePitchPan.fade *= listenerFade; return(true); }
private static void AddAndStartPlaying(SafeSoundEffect sound, FadePitchPan fpp, bool loop = false, int queue = -1) { Debug.Assert(playingSoundCount <= playingSounds.Length); if (playingSoundCount == playingSounds.Length) { Array.Resize(ref playingSounds, playingSounds.Length * 2); } Debug.Assert(playingSounds[playingSoundCount].sound == null); // <- got cleared properly Debug.Assert(playingSounds[playingSoundCount].instance == null); // <- got cleared properly Debug.Assert(playingSounds[playingSoundCount].frameCount == 0); // <- got cleared properly // If we are about to play multiple identical sounds at about the same time, stop them from overlapping: var quashFade = 1f; for (var i = playingSoundCount - 1; i >= 0; i--) { if (playingSounds[i].frameCount >= 3) { break; // <- Reaching sounds that are too old } if (ReferenceEquals(playingSounds[i].sound, sound)) { quashFade -= 1f - playingSounds[i].frameCount / 3f; } } // TODO: The following is ugly, because it kills sequential sounds (but odds are they would be killed anyway - and because we just use `fpp.fade`, below, they'd get killed anyway) // If a sound would be quashed completely, just don't play it -- this is required because otherwise the quashed sounds would be taking up simulated channels if (quashFade < 0.1f) { while (queue != -1) // Don't leak the queue, if any { queue = FreeQueuedSound(queue); } return; } // TODO: This is ugly because sequential sounds will inherit this fade level fpp.fade *= AudioMath.Clamp(quashFade, 0f, 1f); if (loop) { playingSounds[playingSoundCount].instance = sound.CreateInstance(); } else { playingSounds[playingSoundCount].instance = sound.SoundEffectManager_GetInstance(); } if (playingSounds[playingSoundCount].instance == null) { while (queue != -1) // Don't leak the queue, if any { queue = FreeQueuedSound(queue); } return; // Failed to create sound instance... oh well. } Debug.Assert(playingSounds[playingSoundCount].instance.IsLooped == false); // <- instance was properly cleared if (loop) // <- Cannot set on used instances (even to the same value) { playingSounds[playingSoundCount].instance.IsLooped = true; } playingSounds[playingSoundCount].sound = sound; playingSounds[playingSoundCount].fpp = fpp; playingSounds[playingSoundCount].queue = queue; playingSounds[playingSoundCount].linkToNext = true; // <- all sounds on a given frame get linked! playingSounds[playingSoundCount].fade = 1f; // <- NOTE: assumed by channel ducking code playingSounds[playingSoundCount].frameCount = 0; fpp.ApplyTo(playingSounds[playingSoundCount].instance, SafeSoundEffect.SoundEffectVolume); playingSounds[playingSoundCount].instance.Play(); playingSoundCount++; }
private static void UpdatePlayingSounds() { // At the update point, stop linking sounds together for the frame: if (playingSoundCount > 0) { playingSounds[playingSoundCount - 1].linkToNext = false; } // Update all live sounds: var lastSoundGotASlot = true; UsedSlots = 0; for (var i = playingSoundCount - 1; i >= 0; i--) { // Handle sounds that have stopped playing if (playingSounds[i].instance.State == SoundState.Stopped) { if (i < rollOffSoundCount) { ReleasePlayingSoundAt(i); } else { FinishedPlayingSoundAt(i); } continue; } // Count how long the sound has been playing for: var framesLeft = playingSounds[i].sound.DurationInFrames(playingSounds[i].fpp.pitch) - playingSounds[i].frameCount; var nearlyDone = framesLeft < soundTailThresholdFrames && !playingSounds[i].instance.IsLooped; // See if this sound gets a slot: var allocateSlot = UsedSlots < maxSlots || playingSounds[i].linkToNext && lastSoundGotASlot; if (allocateSlot) { if (!nearlyDone ) // <- sounds that are nearly done don't count towards a slot (so another sound may fade in over the top) { UsedSlots++; } } lastSoundGotASlot = allocateSlot; // Handle fading: var fadeAmount = allocateSlot ? 1f / (slotFadeTime * 60f) : -(1f / (slotFadeTime * 60f)); if (i < rollOffSoundCount) { fadeAmount = Math.Min(fadeAmount, -(1f / (rollOffTime * 60f))); } playingSounds[i].fade = AudioMath.Clamp(playingSounds[i].fade + fadeAmount, 0, 1); if (i < rollOffSoundCount && playingSounds[i].fade == 0) { ReleasePlayingSoundAt(i); continue; } playingSounds[i].fpp.ApplyTo(playingSounds[i].instance, playingSounds[i].fade * SafeSoundEffect.SoundEffectVolume); playingSounds[i].frameCount++; } }