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;
}
}