Шины: музыка, эффекты и настройки громкости
Рабочие ползунки категорий, линейный коэффициент и дБ, mute, маршрутизация и сохранение layout.
Разделите категории
Создайте Music, SFX и UI, направленные в Master. Назначайте AudioStreamPlayer.Bus по категории, а не регулируйте все плееры обходом дерева. Шины существуют для всего проекта. Их индекс меняется при вставке, удалении или перестановке; храните имя и заново вызывайте AudioServer.GetBusIndex(name) перед изменением. Для отсутствующего имени возвращается -1 — не передавайте его в методы настройки.
Плеер с несуществующим именем шины использует Master; это может скрыть опечатку. Проверяйте имена при подготовке проекта. Принимающая шина должна находиться левее отправляющей. Попытка отправить сигнал вправо или в отсутствующее назначение фактически возвращает маршрут в Master; это не способ построить цикл.
Линейный коэффициент и дБ
| Линейная амплитуда | Изменение уровня |
|---|---|
| 1 | 0 dB: усиление не меняется |
| 0.5 | примерно -6.02 dB |
| 0.1 | -20 dB |
| 0 | Тишина: используйте mute, не вычисляйте логарифм нуля |
Для отношения амплитуд dB = 20 × log10(gain). Это не формула мощности и не шкала субъективной громкости: половина амплитуды не гарантирует ощущения «вдвое тише». 0 dB на фейдере означает единичный коэффициент, а 0 dBFS у цифрового пика — полную шкалу сигнала. Это разные величины. Микшер может суммировать значения выше полной шкалы; избегайте перегрузки конечного выхода. Запас уровня и лимитер не заменяют разумный баланс источников.
Запустить готовое меню
Сохраните DemoTone.cs из примера плеера в проекте. Добавьте AudioMix.cs и MixerDemo.cs ниже. Создайте уровень с корнем Control, прикрепите MixerDemo, соберите и запустите F6. Тон играет через Music; кнопка проверяет UI. Ползунок Music не меняет UI, ползунок UI не меняет Music. Ползунок SFX готов для ваших эффектов, в этом примере к нему звук не подключён.
AudioMix.cs
using System;
using Reckfell;
public static class AudioMix
{
public static int EnsureBus(string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("A bus needs a name.", nameof(name));
int existing = AudioServer.GetBusIndex(name);
if (existing >= 0) return existing;
AudioServer.AddBus();
int index = AudioServer.BusCount - 1;
AudioServer.SetBusName(index, name);
AudioServer.SetBusSend(index, "Master");
return index;
}
public static bool SetGain(string name, float linear)
{
if (!float.IsFinite(linear)) return false;
int index = AudioServer.GetBusIndex(name);
if (index < 0) return false;
linear = Mathf.Clamp(linear, 0, 1);
AudioServer.SetBusMute(index, linear == 0);
if (linear > 0) AudioServer.SetBusVolumeDb(index, Mathf.LinearToDb(linear));
return true;
}
public static float GetGain(string name)
{
int index = AudioServer.GetBusIndex(name);
return index < 0 || AudioServer.IsBusMute(index) ? 0 : AudioServer.GetBusVolumeLinear(index);
}
}MixerDemo.cs
using System;
using Reckfell;
public partial class MixerDemo : Control
{
public AudioStreamPlayer Music { get; private set; } = null!;
public AudioStreamPlayer Click { get; private set; } = null!;
private bool _ru;
public override void _Ready()
{
_ru = TranslationServer.GetLocale().StartsWith("ru", StringComparison.OrdinalIgnoreCase);
foreach (string name in new[] { "Music", "SFX", "UI" }) AudioMix.EnsureBus(name);
Music = new AudioStreamPlayer { Name = "Music", Bus = "Music", Stream = DemoTone.Create(220, loop: true), PlaybackType = AudioServer.PlaybackType.Stream };
Click = new AudioStreamPlayer { Name = "Click", Bus = "UI", Stream = DemoTone.Create(880, .08), PlaybackType = AudioServer.PlaybackType.Stream };
AddChild(Music); AddChild(Click);
var layout = new VBoxContainer { Position = new Vector2(24, 24), CustomMinimumSize = new Vector2(280, 0) }; AddChild(layout);
AddSlider(layout, "Music", _ru ? "Музыка" : "Music");
AddSlider(layout, "SFX", _ru ? "Эффекты" : "Effects");
AddSlider(layout, "UI", _ru ? "Интерфейс" : "Interface");
var click = new Button { Text = _ru ? "Проверить UI" : "Test UI" }; layout.AddChild(click);
click.Pressed += () => Click.Play(); Music.Play();
}
private static void AddSlider(VBoxContainer layout, string bus, string text)
{
layout.AddChild(new Label { Text = text });
var slider = new HSlider { MinValue = 0, MaxValue = 1, Step = .01, Value = AudioMix.GetGain(bus) };
layout.AddChild(slider); slider.ValueChanged += value => AudioMix.SetGain(bus, (float)value);
}
}EnsureBus создаёт только отсутствующую шину; существующие эффекты, маршрут и громкость не сбрасывает. SetGain отклоняет NaN/Infinity, проверяет имя и ограничивает свой ползунок диапазоном 0…1. Ноль включает mute, ненулевое значение снимает mute и задаёт коэффициент. Это политика данного меню, а не ограничение API движка: сам микшер допускает усиление выше единицы. Если у вас отдельная кнопка «без звука», храните её состояние отдельно, чтобы движение ползунка случайно не снимало пользовательский mute.
Mute не останавливает музыку: позиция продолжает идти, и после снятия mute слышна текущая часть. Для настоящей паузы используйте StreamPaused у плеера. Solo — инструмент изоляции шины при диагностике, а не настройка громкости категории.
Сохранить схему микшера и настройки игрока
В нижней панели Audio создайте шины, задайте Send и добавьте эффекты через Add Effect. Для новой схемы используйте Save Audio Bus Layout As.... Стандартная схема проекта хранится в asset://default_bus_layout.rdata; выбранная схема задаётся audio/buses/default_bus_layout. Изменения AudioServer во время игры меняют память текущего процесса и сами по себе не записывают проектную схему.
Сохраняйте предпочтения игрока по стабильным именам категорий и значениям ползунков в user:// через ConfigFile. Загружайте их после подготовки шин, проверяйте тип и конечность чисел. Не сохраняйте индексы шин и не перезаписывайте ресурс проекта пользовательскими настройками. При переименовании категории явно перенесите старый ключ.
Если маршрут работает неправильно
| Симптом | Проверка |
|---|---|
| Ползунок управляет не той категорией | Старый индекс после перестройки layout; ищите по имени |
| Ничего не меняется | Bus плеера, опечатка имени, принимающая шина и Master |
| Музыка возвращается с другой позиции | Mute сохраняет ход времени; это ожидаемо |
| В захвате есть звук при mute той же шины | Эффекты выполняются до фейдера/mute этой шины; захватывайте на принимающей шине для учёта уровня дочерней |
| После перезапуска настройки пропали | Runtime-изменения не сохраняются автоматически |