예제 #1
0
        /// <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;
                }
            }
        }
예제 #2
0
        /// <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);
        }
예제 #3
0
        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;
        }
예제 #4
0
        /// <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);
        }
예제 #5
0
        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++;
        }
예제 #6
0
        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++;
            }
        }