Best practice for Muzzle Sound Looping

In general i have two common problems when doing sound in Unity:

1. Evenly spaced weapon sounds (automatic/burst type)

When you mix your time based firerate check to call ‘.Play()’ on your audio source the gun will sound crooked. The time between ‘.Play()’ calls will vary slightly depending on your frame rate. One solution is to use looped sounds but with them you have no control on the spacing without altering the sound file or the pitch and your sound file has to have 1/firerate length. Also, your gun sound can never have a longer echo after the loop without starting another Sound clip that might not play seamlessly for the same reasons.

  1. Seamless Music Transitions

Some Tracks have an intro that should enter a never ending loop seamlessly. The only way to achieve that without hiccups is something to do with SetScheduledStartTime if i am correct, but i am not sure about it.

Is there any good approach to those?

Why is this not achievable in Vanilla Unity?
AudioSource class could have two loop points:
float/double loopStart = -1; // -1 means off
float/double loopEnd = -1;
bool loop = true;

Weapon Setup would look like this:
loopStart = -1; loopEnd = 1/firerate; loop = true;
As long as the trigger is held, the sound will loop between the clip start and the loopEnd. When the trigger is released, only loop will be disabled and the sound will play to its very end and stop.

Music Setup
loopStart = 12.5; loopEnd = -1; loop = true;
The Track will start, pass its intro at 12.5s seamlessly and then repeat the last part indefinitely.

I think https://assetstore.unity.com/packages/tools/audio/introloop-51095
just does this but i am not sure if it is suitable for short sounds and mobile

Someone asked me if and how i solved this. I use a custom Audiofilter that loops sounds with the required fire rate.

Its good for auto weapons, but when using high firerate and low frame rates, you will perceive some instances where audio for 2 shots is played but only 1 was released

Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace ShooterSetup{

public class HomogeneousWeaponSound : MonoBehaviour {

    public    bool LOGGING = false;
  
    private AudioSource thisSource;
    public    AudioClip clip;

    public    int marker                = 0;
    private    int dataLength            = 0;
    //private    int clipLength            = 0;
    private    int clipChannelCount    = 0;
    private float[] samples;


    public    int        jitterWidth    = 0;
    public    int        jitter        = 0;
    public    float    jitterSComp    = 1F;//sample rate compensation for jitter width

    public    int[] jitterArr;//array that has an integral of 0 (time offset between jitterless playback is the same after whole array has been used)

    //track played sound samples in order to check if the gun needs to fire an additional round in order to match sound
    public    int firedShots = 0;

    void Start() {
      
    }

    void Update(){
        if(clip == null){ return; }

        jitter = (int)(Random.Range(-jitterWidth,jitterWidth) *jitterSComp);//if frequency is half of platform-sampling rate, pitch will be doubled, so jitter I HAVE NO IDEA WHAT I AM DOING
    //    print( jitterSComp+"\n"+ (clip.frequency *jitterSComp) );
    }

    //void OnValidate() {
    void RefreshSample() {
  
        thisSource = GetComponent<AudioSource>();
        if(clip == null) {return;}
        marker = 0;
        dataLength                = clip.samples * clip.channels;
        //clipLength                = clip.samples;
        clipChannelCount        = clip.channels;
              
        samples = new float[ dataLength ];
        clip.GetData(samples, 0);
    }

    WeaponTemplate lastWeapon = null;
  
    public    void StartPlaying( WeaponTemplate wep, float volume ){

    //    if(!finished){
    //        stop = false;
    //        return;
    //    }

      

        //device sampling rate, clip sampling rate, AudioSource.pitch must be acounted for!      
        if( Application.isEditor || lastWeapon != wep){
            clip = wep.triggerSound;
            RefreshSample();
        }

        float    samplingCompensation    = (48000/AudioSettings.outputSampleRate);
        float    clipSamplingPitchComp    = (wep.triggerSound.frequency/48000F);

        float    pitchCompensation        = samplingCompensation *clipSamplingPitchComp;
        float    intervalCompensation    = wep.triggerPitch *clipChannelCount *samplingCompensation *clipSamplingPitchComp;          
      
        thisSource.pitch                = wep.triggerPitch *pitchCompensation;    //works, clips are at 48kHz, pitch must be acounted for that
        repeatInterval                    = (int)(AudioSettings.outputSampleRate / wep.rateOfFire  *intervalCompensation );      
        lastWeapon = wep;
      
        thisSource.volume                = volume;
      
        jitterSComp = clipSamplingPitchComp;//;1F/pitchCompensation;

        int bufferLength    = 0;
        int numBuffers        = 0;
        AudioSettings.GetDSPBufferSize(out bufferLength, out numBuffers);
      
        if(LOGGING){
            print(    "Start: Device sampleRate:'"+AudioSettings.outputSampleRate+"', clip sampleRate:'"+clip.frequency+"' and '"+clip.channels+"' channels\n" +
                    "samplingCompensation "+samplingCompensation+"F -> autoInterval: "+repeatInterval +" samples\n" +
                    "dspSize(bufferLength/numBuffers):"+bufferLength+"/"+numBuffers
            );
        }

        //CHECK IF WEAPON IS FIRING ALREADY!!

        marker        = 0;
        firedShots++;

        stop        = false;
        finished    = false;

        thisSource.Play();
    }
  
    public    void Stop(){

        if( !stop ){    if(LOGGING){ print("Stopped HomogeneousWeaponSound Playback"); } }
        stop = true;
      

    //stop audio Source when marker has finished!
    //    thisSource.Stop();
    }



  
    private    bool stop        = false;
    public    bool finished    = true;

    public    int repeatInterval = 48000;

    void OnAudioFilterRead(float[] data, int channels) {

        if( dataLength < 1 || finished ){ return; }

        for (int i = 0; i < data.Length; i += channels) {

            if(marker < dataLength){

                data[i] = samples[marker];


                //always has 2 channels because stereo
            //    if (channels == 2){
            //        data[i + 1] = data[i];
            //    }

                //load next sample from clip
                if (clipChannelCount == 2){
                    marker++;
                    data[i + 1] = samples[marker];
                  
                //copy current sample into right ear channel (=mono clip to stereo output), otherwise right channel would be mute
                }else{
                    data[i + 1] = data[i];
                }

            }
            marker++;

            //reset
            if( !stop && marker >= repeatInterval +jitter ){
                firedShots++;
                marker = 0;
                //cannot be called jitter = Random.Range(-jitterWidth,jitterWidth);
            }

            //end
            if( marker == dataLength ){    finished = stop;    }
        }
    }
}}

Template for weapons, needs some changes to work alone

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


using MarrtItemSystem;

namespace ShooterSetup{

public enum WeaponType
{
    None        = 0,
    Projectile    = 1,
    Missile        = 2,
    Ray            = 3,
}

public enum TriggerType
{
    Auto            = 0,    //continuous shooting, auto, burst, beams
    Semi            = 1,    //single shot, on keydown, missiles and stuff, autoRelease rays when chargeUpTime is >0F
    Release            = 2,    //single shot, on release, heavy weapons
}

public enum BarrelMuzzle
{
    LightL,
    LightR,
    Heavy,
    CraftL,
    CraftR
}

public enum BulletVisuals
{
    GlowSprite = 0,
    SpriteTips = 1,
    MeshAndSprite = 2
}



[CreateAssetMenu(menuName = "ShooterSetup/WeaponSetting")]
public class WeaponTemplate : ItemTemplate{

    public void OnValidate() {
       
        if( rateOfFire <= 0F){ rateOfFire = 1F; }
        if( burstPause <= 0F){ burstPause = 0F; }
        if( blastDuration <= 0F){ blastDuration = 0.001F; }

        if( Application.isPlaying && copyReticleSettings ){
            this.reticleSettings.LoadFromCurrent();
            copyReticleSettings = false;
        }
    }

    //conversion of ItemTemplate.GroupType
    public    enum WastelandDashGroupType{
        None    = -1,    //None        = -1,
        Currency=  0,    //Group0    =  0,
        Cargo    =  1,    //Group1    =  1,
        Ship    =  2,    //Group2    =  2,
        WLight    =  3,    //Group3    =  3,
        WHeavy    =  4,    //Group4    =  4,
        WNitra    =  5,    //Group6    =  6
        Instant    =  6,    //Group5    =  5,
    }
    [Header("None    = None    = -1,", order=0)]            [Space(-10, order = 1)]
    [Header("Currency    = Group0    =  0,", order=2)]    [Space(-10, order = 3)]
    [Header("Cargo    = Group1    =  1,", order=4)]        [Space(-10, order = 5)]
    [Header("Ship    = Group2    =  2,", order=6)]        [Space(-10, order = 7)]
    [Header("WLight    = Group3    =  3,", order=8)]        [Space(-10, order = 9)]
    [Header("WHeavy    = Group4    =  4,", order=10)]        [Space(-10, order =11)]
    [Header("WNitra    = Group5    =  5,", order=12)]        [Space(-10, order =13)]
    [Header("Instant    = Group6    =  6 ", order=13)]    [Space( 10, order =15)]

    [Header("Archtype", order=18)]
    public    WeaponType    type            = WeaponType.Projectile;
    public    TriggerType    trigger            = TriggerType.Auto;


    [Header("Rendering")]
    public    BulletVisuals    bulletVisuals            = BulletVisuals.GlowSprite;

    [Header("Charge|Spool Time")]
    /// <summary> Charge Time or SpoolupTime </summary>
    [Range(0F,2F)]
    public    float    chargeUpTime        =   0.0F;

    public    int        damageP                =  10;    //percent of base dmg, 10 = 10% -> baseDMG *0.1F
   
    public    float    muzzleVelocity        = 200.0F;
    public    float    secondaryVelocity    =   0.0F;    //missiles

    /// <summary>Time between bullet instantiations, if burst, time between bursts</summary>
    public    float    rateOfFire            =   5.0F;
    public    float    bulletWidth            =   0.1F;

    public    float    baseNRGCost            =   2.0F;//nrg requirement per dmg percent
    public    float    nrgPerBaseDmgPercent=  10.0F;
    public    float    nrgPerDmg            =   1.0F;

    public    float    projecileLifespan    =   6.0F;

   
    [Header("Accuracy & Feel")]

    //with multiple weapons on the avatar, each weapons recoil increase will
   
    /** <summary>every shot adds this angle of recoil</summary>    **/    public    float    recoilPerShot        =   1.0F;
    /** <summary>DampToTargetLambda control</summary>            **/    public    float    recoilLambda        =   1.0F;    //
    /** <summary>minimum amount of spread</summary>                **/    public    float    minSpread            =   1.0F;

    /** <summary>shield can make first shots ignore recoil</summary>    **/
    public    float    spreadShield                =   0.0F;
    /** <summary>time of idle after last shot that it takes to restorethe shield</summary>**/
    public    float    spreadShieldRestoreDelay    =   0.0F;    //check that this does rely on shot, not trigger up! Overwatch fucked that up for Soldier
       
    public    static float    sleepSpreadThreshold = 0.01F;//spread where recoil residue is completely canceled for the weapon to go to sleep

    [Header("Accuracy & Feel - Visual Only")]
    /** <summary>Reticle should show greater spread than actually is? use this</summary>    **/
    public    float    visualSpreadAdd    =   0.0F;    //check that this does rely on shot, not trigger up! Overwatch fucked that up for Soldier

    public    float    restingSpreadOffset = 0.0F; //spread the gun will return to

    public    float    screenShakePower    = 1.0F;
   
    [Header("Exclusive Trigger Modes")]
   
    [Header("Burst Settings")]
    //public    bool    singleBarrel        = false;
    //public    bool    onlyLeftBarrel        = false;

    /// <summary>Time between bursts as timeBetweenShots factor, 0F should behave as automatic gun </summary>
    public    float    burstPause            = 0.0F;
    public    float    BurstRate{
        get{
            float timeBetweenShots = 1F/rateOfFire;
            return 1F/(timeBetweenShots * burstSize + timeBetweenShots *burstPause);
        }
    }
    public    int        burstSize            = 1;
    public    bool    IsBurst        { get{    return burstSize > 1; }}
   
   
    [Header("Special Attributes")]
    public    bool    isSeeking            = false;
    public    bool    useGravity            = false;
    public    SingleUnityLayer    overwriteLayer;//default

    [Header("Colors")]
    public    Color    bulletColor            = Color.white;
    public    Color    glowColor            = Color.white;

    [Header("ImpactSettings")]
    public    float    impactScale            = 2F;
    public    float    blastDuration        = 0.125F;
    public    Color    blastBulletColor    = Color.white;
    public    Color    blastGlowColor        = Color.white;
   
    [Header("Audio")]
    public    AudioClip    triggerSound    = null; [Range(0.1F,2F)] public float triggerPitch    = 1F;
    public    AudioClip    impactSound        = null;    [Range(0.1F,2F)] public float impactPitch    = 1F;    public float impactDelay    = 0F;

    public    AudioClip    chargeSound        = null;
    [Range(0.1F,2F)]public float chargePitchStart    = 0.5F;
    [Range(0.1F,2F)]public float chargePitchEnd        = 0.9F;

    [Header("Reticle")]
    //EditorHelper
    [SerializeField]
    private bool copyReticleSettings    = false;
    public    bool useDefaultReticle        = false;
    public    Reticle.ReticleSettings    reticleSettings;

   
}
}
1 Like

Thanks
But can you pleas simplify code and just putting main function for playing auto fire sound in Update or Lateupdate ?

You can do it yourself and post it here afterwards,
just fill in some variables of your own wherever you find “wep.”-properties and put the script on a Gameobject with an AudioSource and call StartPlaying(), no big deal.