feat: implement volume step feature

- Add volume step functionality with VolumeStepResolver class
- Add VolumeStepThreshold, Volume, and VolumeStep properties to AudioObject
- Integrate volume step resolution in SfxSystem for dynamic audio volume control
This commit is contained in:
2026-04-13 16:56:34 +08:00
parent 0f204aa794
commit 31d117851e
15 changed files with 192 additions and 11 deletions
@@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using OCES.Audio.HandWritten;
using UnityEngine;
using UnityEngine.Audio;
@@ -33,6 +34,7 @@ namespace OCES.Audio
AudioSourcePool m_pool;
AudioContainerSelector m_containerSelector;
PitchStepResolver m_pitchStepResolver;
VolumeStepResolver m_volumeStepResolver;
#if UNITY_EDITOR
void Update()
@@ -74,6 +76,7 @@ namespace OCES.Audio
this.m_pool = pool;
this.m_containerSelector = new AudioContainerSelector();
this.m_pitchStepResolver = new PitchStepResolver();
this.m_volumeStepResolver = new VolumeStepResolver();
AudioMixerGroup[] sfx = Find("Master/Regular/SFX");
if (sfx.Length > 0) this.m_sfxGroup = sfx[0];
@@ -173,7 +176,8 @@ namespace OCES.Audio
// 执行播放
float pitch = this.m_pitchStepResolver.ResolvePitch(audioObject, now); //算一下用不用变调
PlayNewSound(audioObject, pitch);
float volume = this.m_volumeStepResolver.ResolveVolume(audioObject, now);
PlayNewSound(audioObject, pitch, volume);
this.m_lastPlayTime[audioObject.Id] = now;
onPlay?.Invoke();
}
@@ -182,12 +186,13 @@ namespace OCES.Audio
// 播放逻辑
// ─────────────────────────────────────────────
void PlayNewSound(AudioObject audioObject, float pitch)
void PlayNewSound(AudioObject audioObject, float pitch, float volume)
{
ActiveSound active = new()
{
AudioObject = audioObject,
Pitch = pitch,
Volume = volume,
State = ActiveSoundState.Pending,
StartTime = Time.realtimeSinceStartupAsDouble,
};
@@ -207,7 +212,7 @@ namespace OCES.Audio
/// <summary>
/// 连续容器播放协程(Random / Sequence 持续模式)
/// </summary>
IEnumerator PlayContainerContinuous(AudioSource source, AudioObject audioObject, ActiveSound chainActive, int startIndex, float pitch)
IEnumerator PlayContainerContinuous(AudioSource source, AudioObject audioObject, ActiveSound chainActive, int startIndex, float pitch, float volume)
{
bool isRandom = audioObject.ContainerType == ContainerType.Random;
@@ -229,7 +234,7 @@ namespace OCES.Audio
limitRepetition);
// 配置并播放
if (!SetupSource(source, audioObject, pitch, index))
if (!SetupSource(source, audioObject, pitch, volume,index))
{
Debug.LogError($"音频文件未找到:{audioObject.Name[index]}");
yield break;
@@ -334,7 +339,7 @@ namespace OCES.Audio
return this.m_sfxGroup;
}
bool SetupSource(AudioSource source, AudioObject audioObject, float pitch, int clipIndex = 0)
bool SetupSource(AudioSource source, AudioObject audioObject, float pitch, float volume, int clipIndex = 0)
{
AudioClip clip = Resources.Load<AudioClip>($"Audios/{audioObject.Name[clipIndex]}"); // TODO 抽象同一资源加载接口
if (!clip)
@@ -348,6 +353,7 @@ namespace OCES.Audio
source.priority = audioObject.Priority;
source.outputAudioMixerGroup = GetMixerGroup(audioObject.MixingType);
source.pitch = pitch;
source.volume = volume;
return true;
}
@@ -371,6 +377,7 @@ namespace OCES.Audio
{
AudioObject audioObject = active.AudioObject;
float pitch = active.Pitch;
float volume = active.Volume;
// =======================
// Blend(每个clip一个ActiveSound
@@ -385,10 +392,11 @@ namespace OCES.Audio
{
AudioObject = audioObject,
Pitch = pitch,
Volume = volume,
State = ActiveSoundState.Playing
};
if (!SetupSource(source, audioObject, pitch, i))
if (!SetupSource(source, audioObject, pitch, volume, i))
{
this.m_pool.ReturnToPool(source.gameObject);
continue;
@@ -424,7 +432,7 @@ namespace OCES.Audio
int start = audioObject.ContainerType == ContainerType.Random ? -1 : 0;
active.Coroutine = StartCoroutine(
PlayContainerContinuous(sourceSingle, audioObject, active, start, pitch)
PlayContainerContinuous(sourceSingle, audioObject, active, start, pitch, volume)
);
return;
@@ -440,7 +448,7 @@ namespace OCES.Audio
_ => 0
};
if (!SetupSource(sourceSingle, audioObject, pitch, index))
if (!SetupSource(sourceSingle, audioObject, pitch, volume, index))
{
m_pool.ReturnToPool(sourceSingle.gameObject);
return;
@@ -506,6 +514,7 @@ namespace OCES.Audio
public int CurrentLoopCount;
public Coroutine Coroutine;
public float Pitch;
public float Volume;
public ActiveSoundState State;
}
}
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using UnityEngine;
namespace OCES.Audio.HandWritten
{
public class VolumeStepResolver
{
readonly Dictionary<uint, int> m_volumeStepCounts = new();
readonly Dictionary<uint, double> m_volumeStepLastTime = new();
internal float ResolveVolume(AudioObject audioObject, double time)
{
// 计算一下配置的音量是多少
float baseVolume = Mathf.Pow(10,audioObject.Volume / 20f);
// 优化版。看看表现,要是确实Mathf.Pow造成性能卡点了就用这个。
//float baseVolume = audioObject.Volume == 0 ? 1f : Mathf.Pow(10,audioObject.Volume / 20f);
if (audioObject.VolumeStepThreshold == 0) //没配置VolumeStep
{
return baseVolume;
}
// 超时了,或者没播过
if (!this.m_volumeStepLastTime.TryGetValue(audioObject.Id, out double lastVolumeStepTime)
|| time - lastVolumeStepTime > audioObject.VolumeStepThreshold)
{
this.m_volumeStepCounts[audioObject.Id] = 0;
this.m_volumeStepLastTime[audioObject.Id] = time;
return baseVolume;
}
//命中了
int volumeStepCount = this.m_volumeStepCounts[audioObject.Id];
volumeStepCount = this.m_volumeStepCounts[audioObject.Id] = volumeStepCount + 1;
this.m_volumeStepLastTime[audioObject.Id] = time;
return Mathf.Clamp(Mathf.Pow(10, (audioObject.Volume + audioObject.VolumeStep * volumeStepCount) / 20f), 0f, 1f);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0059d6fc851c474a97bc05f6385f9a48
timeCreated: 1776067889