# 14. Музыка
Table of Contents
14. Музыка
Цель
Добавить непрерывный фоновый музыкальный слой, который начинается при запуске приложения, не обрывается при смене сцены меню ↔ партия и подчиняется сохранённой громкости «Музыка».
Описание выполненной работы
Разделение музыки и эффектов на уровне шин
На шаге 13 все короткие события отправляются на шину SFX. Музыка вынесена на отдельную шину Music, чтобы игрок мог, например, приглушить фон, оставив слышимыми только эффекты, или наоборот — независимо настроить баланс. GameSettings.save() сериализует обе громкости в одном ConfigFile; GameAudio.save(), вызываемый из UI, по-прежнему только делегирует в GameSettings.save(), поэтому появление музыкального плеера не требовало менять формат конфига.
Важно: не вешать длинный луп на шину Master напрямую, если вы планируете глобальные фильтры или паузу всего звука — отдельные шины упрощают отладку.
Почему используется AudioStreamWAV и дубликат ресурса
Файл music_ambient.wav импортируется Godot как AudioStreamWAV. У этого типа в API Godot 4 есть свойство loop_mode. Чтобы включить цикл по всему файлу, мы выставляем AudioStreamWAV.LOOP_FORWARD.
Ресурс загружается через preload в константе STREAM_MUSIC. Если напрямую менять loop_mode у объекта из preload, теоретически можно затронуть кэш редактора и состояние импортированного ресурса в других местах. Поэтому в _prepare_music_stream() вызывается duplicate(): работаем с копией, настраиваем только её и отдаём в AudioStreamPlayer.
Если позже формат сменится на OGG (AudioStreamOggVorbis), цикл задаётся либо флагом loop у потока, либо настройками импортера «Loop» в редакторе — _prepare_music_stream() стоит расширить веткой elif raw is AudioStreamOggVorbis без переименования публичных методов play_music_loop() / stop_music().
Генерация фонового трека в tools/gen_audio_assets.py
Для прототипа трек собирается как сумма нескольких медленных синусоид 55 Hz, 82 Hz и лёгкого верхнего 164 Hz с низкой амплитудой и медленной огибающей pulse по восемисекундному периоду. Частота дискретизации 22050 Hz и длительность около 20 секунд дают файл заметно меньше, чем музыка в 44100 Hz, при этом для фона этого достаточно.
Чтобы получить более «музыкальный» результат без DAW, можно:
- добавить к сумме низкий
triangleилиsquareчерез простую табличную функцию; - слегка модулировать частоту (FM) по второму синусу;
- экспортировать из инструментального sequencer и заменить только
music_ambient.wav.
Скрипт генерации общий для шагов 13 и 14: один запуск восстанавливает все WAV в src/assets/audio/.
Жизненный цикл воспроизведения
В GameAudio._ready() после создания шин вызываются _setup_music_player() и затем play_music_loop(). Это означает:
- При холодном старте приложения музыка начинается на
Main→TitleScreenбез отдельного кода вtitle_screen.gd. - При
change_scene_to_file("res://scenes/game_board.tscn")синглтонGameAudioне уничтожается,_music_player.playingостаётсяtrue, трек не перезапускается с начала — так обычно и хотят для казуальной сессии. - При возврате в
main.tscnситуация та же.
Если в дизайне понадобится другая композиция только на поле, можно завести второй AudioStreamPlayer или хранить два потока и переключать stream с кроссфейдом — публичный API лучше назвать play_music_menu() / play_music_battle(), оставив текущий play_music_loop() как сценарий по умолчанию.
Методы play_music_loop и stop_music
play_music_loop() проверяет _music_player.playing и не вызывает play() повторно. Это защищает от случайных двойных вызовов из будущих сцен. stop_music() добавлен как задел на паузу приложения или экран «тишины» рекламы (шаги рекламы в плане ещё впереди).
На паузу операционной системы Godot может сам приглушать аудио в зависимости от настроек pause режима дерева сцен; если понадобится явное поведение, можно подписаться на NOTIFICATION_APPLICATION_PAUSED в GameAudio и вызывать stop_music() или StreamPlayer.stream_paused = true — это не входило в минимальный объём шага 14, но точка расширения уже есть.
Связь с настройками и UX
Ползунок «Музыка» на титульном экране меняет шину через GameAudio.set_bus_linear("Music", value). При linear близком к нулю шина уходит в −80 dB, что эквивалентно полному выключению без остановки play() — трек теоретически продолжает «играть» внутри движка, но неслышимо. Альтернатива stop() экономила бы CPU, но усложнила бы восстановление позиции лупа; для короткого зацикленного фона текущая схема проще.
Рекомендуется один раз проверить на Android: некоторые прошивки приглушают STREAM_MUSIC при системной громкости ноль независимо от приложения — это не баг игры.
Импорт в редакторе и размер билда
После добавления WAV Godot добавит запись в project.godot не нужно — достаточно preload по res://. В экспортированном apk попадут импортированные бинарные потоки из папки .godot/imported/ при штатном экспорте из редактора; если сборка CI делает экспорт через --export-release, убедитесь, что импорт ресурсов выполнен заранее.
Замена на финальный саундтрек
Когда будет готов финальный трек:
- Экспортируйте его в OGG Vorbis или 16‑bit WAV с явными точками лупа (если луп не seamless, добавьте кроссфейд в DAW).
- Замените файл
music_ambientили добавьте второй ресурс и поменяйтеpreloadвgame_audio.gd. - Уточните
loop_modeили импортныеloop_offsetпараметры.
Тестируйте на минимальной громкости и на максимальной — клип music_ambient сгенерирован с небольшой средней громкостью; финальный мастеринг может потребовать volume_db на AudioStreamPlayer или нормализацию файла.
Итог по архитектуре аудио после шагов 13–14
Автозагрузчик GameAudio теперь закрывает три задачи: создание шин, непрерывная музыка одним плеером и полифонические SFX пулом плееров. Сцены остаются тонкими: они вызывают семантические методы (play_ball_move, play_music_loop и т.д.), что упрощает рефакторинг ассетов и последующую локализацию (если появится озвучка реплик — её логичнее вынести в отдельный класс или шину Voice, не смешивая с SFX).
Ссылка на проект: Third Planet
Ссылка на игру: Google Play Rustore