AudioSystem (Unity)
Unity-реализация драйвера аудиосистемы.
Назначение
Платформенная адаптация AudioController: воспроизведение звуков и музыки через AudioSource, пул звуков, fade-переходы, ситуативная музыка, система каналов, сохранение настроек, сцено-ориентированные компоненты.
- Воспроизведение звуков через пул с автоматическим освобождением
- Воспроизведение музыки с fade in/out через
AsyncTween - Ситуативная музыка с автоматическим возвратом к основной теме
- Именованные каналы с независимой громкостью и mute
- Сохранение/загрузка настроек через
PlayerPrefs - Компоненты для инспектора: хэндлеры звуков, музыки, UI-переключатели, слайдеры каналов
Вне ответственности: пространственный звук (3D), одновременное микширование нескольких музыкальных дорожек, динамическая подгрузка аудиоресурсов.
Зависимости
Vortex.Core.AudioSystem— шинаAudioController, модели,IDriverVortex.Core.DatabaseSystem—Database,RecordVortex.Unity.AppSystem.System.TimeSystem—TimeController(отложенные вызовы, пул-очистка)Vortex.Unity.UI.PoolSystem—Pool(пул звуковых источников)Vortex.Unity.UI.TweenerSystem.UniTaskTweener—AsyncTween(fade-анимации)Vortex.Unity.DatabaseSystem—RecordPreset, атрибут[DbRecord]Sirenix.OdinInspector— редакторские атрибуты
AudioDriver
Реализация IDriver. Partial-класс из трёх файлов.
Архитектура
AudioDriver (Singleton<AudioDriver>, IDriver)
├── AudioDriver.cs — Init/Destroy, заполнение индексов из Database, Save/LoadSettings
├── AudioDriverExtLoading.cs — [RuntimeInitializeOnLoadMethod] авторегистрация
├── AudioDriverExtEditor.cs — [InitializeOnLoadMethod] регистрация в редакторе
└── AudioDriverExtPlayControl.cs — делегирование IDriver-методов к AudioPlayer
Контракт
Вход:
- Автоматическая регистрация через
[RuntimeInitializeOnLoadMethod] - Заполнение индексов при
Database.OnInit - Загрузка каналов из
AudioChannelsConfig(Resources)
Выход:
- Заполненные реестры
IndexSound/IndexMusicвAudioController - Заполненный
Settings.ChannelsизAudioChannelsConfig - Событие
OnInitпосле заполнения индексов - Настройки в
PlayerPrefs(ключAudioSettings)
Формат сохранения:
MasterOn;MasterVol;MusicOn;MusicVol;SoundOn;SoundVol[;ChName:MuteFlag:Vol]...
Пример: Y;0.8;Y;1;Y;1;dialog:Y:0.7;ambient:Y:0.5
Значения MuteFlag: Y — не замьючен, N — замьючен. Числа — CultureInfo.InvariantCulture.
Гарантии:
- Настройки сохраняются при каждом изменении через подписку на
AudioController.OnSettingsChanged - Загрузка из
PlayerPrefsсtry/catch— при некорректных данных настройки сбрасываются к дефолтам - Каналы из
PlayerPrefs, не совпадающие с текущимAudioChannelsConfig, игнорируются TimeController.RemoveCall(this)приDestroy()— очистка отложенных вызовов
Ограничения:
- Если
AudioController.SetDriverвернулfalse— экземпляр уничтожается (Dispose()) - Зависит от
Database.OnInit— индексы пусты до инициализации базы данных
Каналы
AudioChannelsConfig
ScriptableObject (ICoreAsset), размещается в Resources. Определяет список именованных каналов для проекта.
- Поле
channels: string[]— имена каналов - При изменении в редакторе — автоматический
AudioDriver.ResetChannels()
Меню: Tools/Vortex/Configs/Audio Channels Settings — навигация к конфигу.
AudioChannelNameAttribute
Атрибут [AudioChannelName] для string-полей. Отрисовывает dropdown со списком каналов из AudioController.GetChannelsList().
AudioChannelVolumeSlider
UI-слайдер громкости канала.
- Поле
channelс атрибутом[AudioChannelName]— выбор канала OnEnable— читает текущую громкость канала, подписка наonValueChangedOnDisable— отписка- При ненайденном канале — слайдер устанавливается в 0
Слайдер является источником изменений, не визуализатором. Внешние изменения канала не отражаются на слайдере — это осознанное решение для исключения рекурсивных обновлений.
Канал в пресетах и хэндлерах
Канал назначается в двух точках:
- Пресет (
SoundSamplePreset,MusicSamplePreset) — канал по умолчанию для сэмпла. Используется при воспроизведении через пул. - Хэндлер (
AudioHandler) — канал конкретного экземпляра компонента. Используется при воспроизведении через личныйAudioSource.
При воспроизведении через AudioPlayer (пул) канал определяется пресетом. Параметр defaultChannel в IDriver — fallback, если канал не задан в модели звука.
AudioPlayer
Центральный контроллер воспроизведения. MonoBehaviourSingleton, internal API.
Архитектура
AudioPlayer (MonoBehaviourSingleton<AudioPlayer>)
├── pool — Pool (пул звуковых AudioSource)
├── musicPlayer — MusicPlayer (основная музыка)
├── musicCoverPlayer — MusicPlayer (ситуативная музыка)
├── musicFadeTime — float (0–3с, default 1с)
├── FadeTween — AsyncTween (fade основной музыки)
└── FadeCoverTween — AsyncTween (fade ситуативной музыки)
Воспроизведение звуков
PlaySound(object, bool loop, string channelOverrideName) — pattern matching по типу:
| Тип | Поведение |
|---|---|
string |
Поиск Sound в Database по GUID. Канал: override или из пресета |
Sound |
Прямой доступ к Sample |
AudioClip |
Обёртка в SoundClipFixed |
Создаёт SoundClipFixed, добавляет в пул. Для не-loop звуков — автоматическое удаление через TimeController.Call по длительности клипа.
StopAllSounds(string channel) — при null очищает весь пул, при указанном канале — удаляет из пула только звуки с совпадающим Channel.Name.
Воспроизведение музыки
Одновременно может играть только один основной и один ситуативный трек.
Основная музыка (PlayMusic):
- Если играет текущий трек и
fadingEnd = true— fade out → callback → запуск нового трека - Если
fadingEnd = false— мгновенная остановка → запуск - Новый трек запускается с fade in (при
fadingStart = true) или мгновенно
Ситуативная музыка (PlayCoverMusic):
- Fade out текущей ситуативной или основной музыки
- Запуск ситуативного трека через
musicCoverPlayer - При
StopCoverMusic— ситуативный трек затухает, основная тема восстанавливается с fade in
GetMusicClip — pattern matching: string, Music, SoundClip, AudioClip. Все ветки создают SoundClipFixed с пробросом overrideChannel.
MusicPlayer
Компонент воспроизведения музыки. Один AudioSource.
Контракт
Play(SoundClip)/Play(AudioClip)— запуск с настройками pitch/volume из клипа, сохранение канала клипаStop()— остановка воспроизведенияIsPlay()— проверка состоянияSetVolumeMultiplier(float)/GetVolumeMultiplier()— множитель громкости (для fade)- При
OnEnable— подписка наAudioController.OnSettingsChanged, применение настроек - При
OnDisable— отписка, остановка - Mute/unmute переключается автоматически при изменении настроек. При unmute (
mute → !mute) вызываетсяaudioSource.Play()для возобновления - Итоговая громкость:
GetMusicVolume(channel) × clip.volume × volumeMultiplier
Модели
SoundClip
Аудиоклип с диапазонами pitch и volume. Реализует ICloneable. Каждое воспроизведение — случайные значения из диапазонов.
SoundClip (ICloneable)
├── AudioClips — AudioClip[] (массив клипов для рандомизации)
├── PitchRange — Vector2
├── ValueRange — Vector2
├── Channel — AudioChannel (канал звука)
├── Loop — bool
├── GetPitch() → Random.Range(PitchRange.x, PitchRange.y)
├── GetVolume() → Random.Range(ValueRange.x, ValueRange.y)
├── GetClip() → случайный из массива (или единственный)
└── Clone() → deep clone (новый SoundClip с теми же параметрами)
Конструкторы принимают string channelName или AudioChannel channel. При channelName — резолв через AudioController.GetChannel().
Lazy-загрузка addressable-клипов (#if ENABLE_ADDRESSABLES)
SoundClip поддерживает второй набор конструкторов — от AssetReferenceAudioClip[] (вместо прямого AudioClip[]). В этом режиме:
AudioClips == nullпри создании — клип в память не грузится; запись остаётся лёгкой при загрузке Database.- При первом
GetClip()срабатываетLoadAssets(): для каждой ссылкиLoadAssetAsync<AudioClip>().WaitForCompletion()— синхронная загрузка, без async и без дедлока. Результат кешируется вAudioClips, повторныеGetClip()отдают готовое. - Политика never-release. Загруженные клипы держатся в памяти до конца приложения — release намеренно не делается. Иное потребовало бы счётчика владельцев и размыло бы контракт лёгких записей Database. Память возвращается ОС при выходе.
Цена режима — стопор кадра на время первой загрузки клипа (WaitForCompletion). Приемлемо для локальных бандлов; для редких/тяжёлых звуков латентность первого проигрывания заметна. Clone() для addressable-клипа переносит ссылки (AudioClips == null), не загруженные данные.
Для удалённого контента или агрессивной выгрузки лучше идти через
AssetCacheSystem(owner-based ref-count + LRU).SoundClipже реализует простейший never-release lazy — под локальные звуки, которые нужны всю сессию.
SoundClipFixed
Наследник SoundClip. Значения pitch, volume и клип фиксируются при создании.
SoundClipFixed (: SoundClip)
├── AudioClip — выбранный клип
├── GetPitch() → фиксированное значение
├── GetVolume() → фиксированное значение
├── GetDuration() → clip.length / |pitch| (или float.MaxValue при pitch == 0)
└── GetClip() → фиксированный клип
Конструкторы поддерживают channelOverrideName / channelOverride — переопределение канала.
Sound / Music
Типизированные обёртки для Unity:
Sound : SoundSample<SoundClip>— звуковой эффектMusic : MusicSample<SoundClip>— музыкальный трек
Компоненты (Handlers)
AudioHandler
Компонент воспроизведения звука. Работает с личным AudioSource или ретранслирует на AudioPlayer.
- GUID сэмпла через
[DbRecord(typeof(Sound))] - Канал через
[AudioChannelName]— используется для расчёта громкости личногоAudioSource - При
Play(): еслиaudioSource != null—PlayOneShot; иначе —AudioController.PlaySound SetVolumeMultiplier(float)/GetVolumeMultiplier()— множитель громкости- Итоговая громкость:
GetSoundVolume(channel) × clip.volume × volumeMultiplier - Итоговый mute:
!GetSoundOn(channel) playOnEnable(bool) — если включён,Play()вызывается автоматически вOnEnable(после применения настроек громкости). Удобно для одноразовых эффектов, которые включаются вместе с GameObject (popup-выход, появление UI и т. п.)OnEnable— подписка наAudioController.OnSettingsChanged, применение настроек, опциональноPlay()приplayOnEnableOnDisable— отписка, остановка воспроизведения- Инициализация отложена до
AudioController.OnInitчерезTimeController.Accumulate
MusicHandler
Компонент запуска музыки при активации GameObject.
- GUID сэмпла через
[DbRecord(typeof(Music))] OnEnable→ запуск музыки с задержкойUniTask.DelayFrame(2)для гарантии порядка послеOnDisableOnDisable→ отложенная остановка черезTimeController.Call(обход «горячего рестарта»)- Поле
isCoverMusic— переключение между основной и ситуативной музыкой - Поля
fadeStart/fadeEnd— управление fade-переходами - Инициализация отложена до
AudioController.OnInitчерезTimeController.Accumulate
AudioSourceHandler
Воспроизведение звука из IDataStorage.
[RequireComponent(typeof(AudioSource))]- Поле
dataStorageObject(GameObject) — источникIDataStorage(внешний объект) - Получает
SoundClipчерезIDataStorage.GetData<SoundClip>() - Канал берётся из
SoundClip.Channel - Итоговая громкость:
GetSoundVolume(channel) × clip.volume OnEnable→ Play,OnDisable→ Stop
AudioSwitcher
UI-переключатель вкл/выкл.
- Работает через
UIComponent(SetAction,SetSwitcher) - Тип контроля:
SoundType(Master / Sound / Music)
AudioValueSlider
UI-слайдер громкости.
- Привязан к
UnityEngine.UI.Slider - Тип контроля:
SoundType(Master / Sound / Music) OnEnable— синхронизация значения, подписка наonValueChangedOnDisable— отписка
Пресеты
SoundSamplePreset
ScriptableObject (RecordPreset<Sound>), меню: Database/SoundSample.
AudioClip[]— массив клиповpitchRange/valueRange— диапазоны через[MinMaxSlider]channel— канал через[AudioChannelName]RecordTypes.Singleton(форсируется вOnValidate)- Editor: кнопка
TestSound— создаёт временныйAudioSource, самоуничтожается после воспроизведения
MusicSamplePreset
ScriptableObject (RecordPreset<Music>), меню: Database/MusicSample.
- Один
AudioClip, фиксированные pitch и volume channel— канал через[AudioChannelName]Duration— автоматический расчёт:clip.length / |pitch|RecordTypes.Singleton(форсируется вOnValidate)- Editor: кнопки
TestSound/StopSound
SoundSampleAddressablePreset (#if ENABLE_ADDRESSABLES)
ScriptableObject (RecordPreset<Sound>), меню: Database/SoundSampleAddressable. Вариация SoundSamplePreset, где клипы хранятся как AssetReferenceAudioClip[] (addressable-линк), а не прямым AudioClip[].
- Запись остаётся лёгкой: клип не приезжает в память при загрузке Database, тянется по требованию при первом
GetClip(синхронно, черезWaitForCompletion). - Применять для тяжёлых/редких звуков, чтобы не грузить их на старте.
- Лёгкие всегда-нужные звуки (короткие UI-клики) проще держать в обычном
SoundSamplePreset— addressable-загрузка добавит им латентность на ровном месте.
Границы применения: общесистемные vs внутренние звуки
AudioSystem через Database — это система под общесистемные звуки: те, что шарятся по всему приложению (клики UI, общие SFX, фоновая музыка меню). Они оправданы в шине: доступ по GUID из любого места, единая точка правки.
Внутренние звуки подгружаемых-выгружаемых систем (мини-игры, отдельные сцены, катсцены) не стоит заводить в общий Database — их лучше прописывать через линкованные ассеты звуков в конфиге самой этой системы. Иначе локальный звук мини-игры висит в общей шине весь сеанс, хотя нужен только пока система загружена.
Это прямое применение матрицы размещения данных (см. Core/DatabaseSystem → «Границы расширения»): общесистемный звук — «общее», его место в шине; внутренний звук системы — «частное», его место в собственном конфиге системы по линковке.
Использование
1. Настройка каналов
- Создать
AudioChannelsConfigв Resources - Задать имена каналов:
dialog,ui,ambient,sfxи т.д. - Каналы станут доступны в dropdown
[AudioChannelName]и в APIAudioController
2. Создание пресетов
Create → Database → SoundSample— настроить клипы, диапазоны pitch/volume, каналCreate → Database → MusicSample— настроить клип, pitch, volume, канал- Зарегистрировать пресеты в базе данных (уникальный GUID)
3. Воспроизведение из кода
// Звук по GUID
AudioController.PlaySound("explosion_01");
// Звук по экземпляру
var sound = AudioController.GetSample("explosion_01") as Sound;
AudioController.PlaySound(sound);
// Музыка
AudioController.PlayMusic("main_theme");
// Ситуативная музыка
AudioController.PlayCoverMusic("battle_theme");
AudioController.StopCoverMusic(); // основная тема восстановится
4. Компонент звука
Добавить AudioHandler на GameObject, назначить GUID сэмпла через [DbRecord], выбрать канал через [AudioChannelName]. Опционально добавить AudioSource — если его нет, звук пойдёт через пул AudioPlayer.
5. Компонент музыки
Добавить MusicHandler на GameObject, назначить GUID через [DbRecord(typeof(Music))]. Музыка запускается при OnEnable, останавливается при OnDisable. Для ситуативной музыки — включить isCoverMusic.
6. UI настроек
AudioSwitcher— переключатель вкл/выкл (Master / Sound / Music)AudioValueSlider— слайдер громкости (Master / Sound / Music)AudioChannelVolumeSlider— слайдер громкости канала
Редакторские инструменты
SoundSamplePreset.TestSound()— воспроизведение случайного клипа с случайными pitch/volumeMusicSamplePreset.TestSound()/StopSound()— прослушивание музыкального трекаAudioHandler,MusicPlayer— кнопки Play/Stop в инспекторе (Play Mode)[DbRecord]— picker сэмплов с фильтрацией по типу[AudioChannelName]— dropdown каналовTools/Vortex/Configs/Audio Channels Settings— быстрая навигация к конфигу каналовAudioAssetsCombiner(Assets/Vortex/Create Audio Assets) — editor-тул: создаёт ассеты сэмплов из выделенных аудиоклипов в Project windowSoundSamplePresetConverter(Assets/Vortex/Convert to Addressable Sound,#if ENABLE_ADDRESSABLES) — конвертирует выделенныеSoundSamplePresetвSoundSampleAddressablePresetна месте. Сохраняет framework-GUID (GuidPreset) — ссылки[DbRecord]не рвутся (они хранятGuidPreset, а не Unity-GUID ассета). ПрямыеAudioClipпереводятся вAssetReferenceAudioClip; если клип ещё не addressable — регистрируется в дефолтной группе. Замена деструктивная (с диалогом-подтверждением): дубликатguidв Database недопустим, поэтому исходный ассет заменяется, а не дублируется.SoundSampleAddressablePreset.TestSound()— прослушивание черезAssetReferenceAudioClip.editorAsset(без рантайм-загрузки и ref-count)
Граничные случаи
| Ситуация | Поведение |
|---|---|
PlaySound с несуществующим GUID |
Лог [AudioPlayer] Unknown sound ID, звук не воспроизводится |
AudioHandler с пустым GUID |
Лог [AudioHandler] Empty Sample data. при инициализации |
AudioHandler без AudioSource |
Звук ретранслируется через AudioController.PlaySound (пул) |
PlayMusic при играющей музыке |
Текущий трек fade out → новый трек fade in |
PlayCoverMusic при играющей основной |
Основная приглушается, ситуативная запускается |
StopCoverMusic |
Ситуативная затухает, основная восстанавливается с fade in |
AudioPlayer.Instance == null |
PlaySound — silent return |
Нет настроек в PlayerPrefs |
Значения по умолчанию (всё включено, громкость 1) |
Некорректные данные в PlayerPrefs |
try/catch, настройки сбрасываются к дефолтам |
MusicHandler быстрый disable/enable |
TimeController.RemoveCall отменяет pending stop, UniTask.DelayFrame(2) обеспечивает play после stop |
Первый GetClip() у addressable-звука |
Синхронная загрузка WaitForCompletion — возможен короткий стопор кадра; далее клип закеширован |
| Addressable-клип удалён/отсутствует в билде | WaitForCompletion вернёт null → NRE в SoundClipFixed (fail-fast; проверь, что клип в addressable-группе) |
Конвертация SoundSamplePreset → addressable |
Framework-GUID сохраняется, [DbRecord]-ссылки живут; Unity-GUID ассета меняется |
pitch == 0 в SoundClipFixed |
GetDuration() → float.MaxValue |
StopAllSounds(channel) с null |
Очищает весь пул |
StopAllSounds(channel) с именем |
Удаляет из пула только звуки с совпадающим Channel.Name |
Канал удалён из AudioChannelsConfig |
Старые данные канала в PlayerPrefs игнорируются при загрузке |
AudioChannelVolumeSlider с несуществующим каналом |
Слайдер устанавливается в 0, подписка не создаётся |