1. Прежде чем начать
Игры — это аудиовизуальные впечатления. Flutter — отличный инструмент для создания красивых визуальных эффектов и надежного пользовательского интерфейса, поэтому он позволяет вам далеко продвинуться в визуальной части вещей. Недостающий ингредиент, который остался, — это аудио. В этой кодовой лаборатории вы узнаете, как использовать плагин flutter_soloud
для внедрения звука и музыки с низкой задержкой в ваш проект. Вы начинаете с базового каркаса, чтобы можно было сразу перейти к интересным частям.
Конечно, вы можете использовать то, чему здесь научитесь, чтобы добавлять аудио в свои приложения , а не только в игры. Но хотя почти все игры требуют звука и музыки, большинству приложений это не нужно, поэтому эта кодовая лаборатория фокусируется на играх.
Предпосылки
- Базовые знания Flutter.
- Знание того, как запускать и отлаживать приложения Flutter.
Что вы узнаете
- Как воспроизводить одноразовые звуки.
- Как воспроизводить и настраивать непрерывные музыкальные циклы.
- Как усиливать и ослаблять звуки.
- Как применять эффекты окружающей среды к звукам.
- Как работать с исключениями.
- Как объединить все эти функции в одном аудиоконтроллере.
Что вам нужно
- Пакет SDK Flutter
- Редактор кода по вашему выбору
2. Настройка
- Загрузите следующие файлы. Если у вас медленное соединение, не волнуйтесь. Сами файлы вам понадобятся позже, поэтому вы можете позволить им загрузиться, пока вы работаете.
- Создайте проект Flutter с именем по вашему выбору.
- Создайте в проекте файл
lib/audio/audio_controller.dart
. - В файле введите следующий код:
lib/audio/audio_controller.dart
import 'dart:async';
import 'package:logging/logging.dart';
class AudioController {
static final Logger _log = Logger('AudioController');
Future<void> initialize() async {
// TODO
}
void dispose() {
// TODO
}
Future<void> playSound(String assetKey) async {
_log.warning('Not implemented yet.');
}
Future<void> startMusic() async {
_log.warning('Not implemented yet.');
}
void fadeOutMusic() {
_log.warning('Not implemented yet.');
}
void applyFilter() {
// TODO
}
void removeFilter() {
// TODO
}
}
Как вы видите, это только скелет будущей функциональности. Мы реализуем все это в ходе этой кодовой лаборатории.
- Затем откройте файл
lib/main.dart
и замените его содержимое следующим кодом:
lib/main.dart
import 'dart:developer' as dev;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'audio/audio_controller.dart';
void main() async {
// The `flutter_soloud` package logs everything
// (from severe warnings to fine debug messages)
// using the standard `package:logging`.
// You can listen to the logs as shown below.
Logger.root.level = kDebugMode ? Level.FINE : Level.INFO;
Logger.root.onRecord.listen((record) {
dev.log(
record.message,
time: record.time,
level: record.level.value,
name: record.loggerName,
zone: record.zone,
error: record.error,
stackTrace: record.stackTrace,
);
});
WidgetsFlutterBinding.ensureInitialized();
final audioController = AudioController();
await audioController.initialize();
runApp(MyApp(audioController: audioController));
}
class MyApp extends StatelessWidget {
const MyApp({required this.audioController, super.key});
final AudioController audioController;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter SoLoud Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.brown),
),
home: MyHomePage(audioController: audioController),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.audioController});
final AudioController audioController;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const _gap = SizedBox(height: 16);
bool filterApplied = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter SoLoud Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
OutlinedButton(
onPressed: () {
widget.audioController.playSound('assets/sounds/pew1.mp3');
},
child: const Text('Play Sound'),
),
_gap,
OutlinedButton(
onPressed: () {
widget.audioController.startMusic();
},
child: const Text('Start Music'),
),
_gap,
OutlinedButton(
onPressed: () {
widget.audioController.fadeOutMusic();
},
child: const Text('Fade Out Music'),
),
_gap,
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Apply Filter'),
Checkbox(
value: filterApplied,
onChanged: (value) {
setState(() {
filterApplied = value!;
});
if (filterApplied) {
widget.audioController.applyFilter();
} else {
widget.audioController.removeFilter();
}
},
),
],
),
],
),
),
);
}
}
- После загрузки аудиофайлов создайте в корне проекта каталог с именем
assets
. - В каталоге
assets
создайте два подкаталога: один с именемmusic
, а другой с именемsounds
. - Переместите загруженные файлы в свой проект так, чтобы файл песни находился в файле
assets/music/looped-song.ogg
, а звуки церковной скамьи — в следующих файлах:
-
assets/sounds/pew1.mp3
-
assets/sounds/pew2.mp3
-
assets/sounds/pew3.mp3
Структура вашего проекта теперь должна выглядеть примерно так:
Теперь, когда файлы есть, вам нужно сообщить о них Flutter.
- Откройте файл
pubspec.yaml
и замените разделflutter:
в нижней части файла следующим:
pubspec.yaml
...
flutter:
uses-material-design: true
assets:
- assets/music/
- assets/sounds/
- Добавьте зависимость от пакета
flutter_soloud
и пакетаlogging
.
flutter pub add flutter_soloud logging
Теперь ваш файл pubspec.yaml
должен иметь дополнительные зависимости от пакетов flutter_soloud
и logging
.
pubspec.yaml
...
dependencies:
flutter:
sdk: flutter
flutter_soloud: ^3.1.10
logging: ^1.3.0
...
- Запустите проект. Пока ничего не работает, потому что вы добавляете функционал в следующих разделах.
3. Инициализация и выключение
Для воспроизведения звука используется плагин flutter_soloud
. Этот плагин основан на проекте SoLoud , звуковом движке C++ для игр, который используется, среди прочего, Nintendo SNES Classic.
Чтобы инициализировать звуковой движок SoLoud, выполните следующие действия:
- В файле
audio_controller.dart
импортируйте пакетflutter_soloud
и добавьте в класс закрытое поле_soloud
.
lib/audio/audio_controller.dart
import 'dart:async';
import 'package:flutter_soloud/flutter_soloud.dart'; // ← Add this...
import 'package:logging/logging.dart';
class AudioController {
static final Logger _log = Logger('AudioController');
SoLoud? _soloud; // ← ... and this.
Future<void> initialize() async {
// TODO
}
...
Аудиоконтроллер управляет базовым движком SoLoud через это поле и перенаправляет все вызовы на него.
- В методе
initialize()
введите следующий код:
lib/audio/audio_controller.dart
...
Future<void> initialize() async {
_soloud = SoLoud.instance;
await _soloud!.init();
}
...
Это заполняет поле _soloud
и ждет инициализации. Обратите внимание на следующее:
- SoLoud предоставляет поле
instance
singleton. Невозможно создать несколько экземпляров SoLoud. Это не то, что позволяет движок C++, поэтому это не допускается и плагином Dart. - Инициализация плагина асинхронна и не завершается до тех пор, пока не будет выполнен метод
init()
. - Для краткости в этом примере вы не перехватываете ошибки в блоке
try/catch
. В производственном коде вы хотите это сделать и сообщить о любых ошибках пользователю.
- В методе
dispose()
введите следующий код:
lib/audio/audio_controller.dart
...
void dispose() {
_soloud?.deinit();
}
...
Хорошей практикой является отключение SoLoud при выходе из приложения, хотя все должно работать нормально, даже если вы этого не сделаете.
- Обратите внимание, что метод
AudioController.initialize()
уже вызывается из функцииmain()
. Это означает, что горячий перезапуск проекта инициализирует SoLoud в фоновом режиме, но это не принесет вам никакой пользы, пока вы не воспроизведете какие-нибудь звуки.
4. Воспроизведение одноразовых звуков
Загрузите актив и воспроизведите его
Теперь, когда вы знаете, что SoLoud инициализируется при запуске, вы можете попросить его воспроизвести звуки.
SoLoud различает источник звука, который представляет собой данные и метаданные, используемые для описания звука, и его «экземпляры звука», которые представляют собой фактически воспроизводимые звуки. Примером источника звука может быть файл mp3, загруженный в память, готовый к воспроизведению и представленный экземпляром класса AudioSource
. Каждый раз, когда вы воспроизводите этот источник звука, SoLoud создает «экземпляр звука», представленный типом SoundHandle
.
Вы получаете экземпляр AudioSource
, загружая его. Например, если у вас есть файл mp3 в ваших активах, вы можете загрузить его, чтобы получить AudioSource
. Затем вы говорите SoLoud воспроизвести этот AudioSource
. Вы можете воспроизвести его много раз, даже одновременно.
Когда работа с источником звука закончена, вы удаляете его с помощью метода SoLoud.disposeSource()
.
Чтобы загрузить ресурс и воспроизвести его, выполните следующие действия:
- В методе
playSound()
классаAudioController
введите следующий код:
lib/audio/audio_controller.dart
...
Future<void> playSound(String assetKey) async {
final source = await _soloud!.loadAsset(assetKey);
await _soloud!.play(source);
}
...
- Сохраните файл, выполните горячую перезагрузку, а затем выберите Play sound . Вы должны услышать глупый звук скамьи. Обратите внимание на следующее:
- Предоставленный аргумент
assetKey
— это что-то вродеassets/sounds/pew1.mp3
— та же самая строка, которую вы бы предоставили любому другому API Flutter для загрузки ресурсов, например виджетуImage.asset()
. - Экземпляр SoLoud предоставляет метод
loadAsset()
, который асинхронно загружает аудиофайл из ресурсов проекта Flutter и возвращает экземпляр классаAudioSource
. Существуют эквивалентные методы для загрузки файла из файловой системы (методloadFile()
) и для загрузки по сети с URL (методloadUrl()
). - Затем полученный экземпляр
AudioSource
передается методуplay()
SoLoud. Этот метод возвращает экземпляр типаSoundHandle
, представляющий новый воспроизводимый звук. Этот дескриптор, в свою очередь, может быть передан другим методам SoLoud для выполнения таких действий, как приостановка, остановка или изменение громкости звука. - Хотя
play()
является асинхронным методом, воспроизведение начинается практически мгновенно. Пакетflutter_soloud
использует интерфейс внешних функций Dart (FFI) для прямого и синхронного вызова кода C. Обычный обмен сообщениями между кодом Dart и кодом платформы, характерный для большинства плагинов Flutter, нигде не встречается. Единственная причина, по которой некоторые методы являются асинхронными, заключается в том, что часть кода плагина выполняется в его собственном изоляте, а связь между изолятами Dart является асинхронной. - Вы утверждаете, что поле
_soloud
не равно null с помощью_soloud!
. Это, опять же, для краткости. Производственный код должен изящно справляться с ситуацией, когда разработчик пытается воспроизвести звук до того, как аудиоконтроллер получит возможность полностью инициализироваться.
Иметь дело с исключениями
Вы могли заметить, что вы снова игнорируете возможные исключения. Пришло время исправить это для этого конкретного метода в учебных целях. (Для краткости после этого раздела codelab возвращается к игнорированию исключений.)
- Чтобы справиться с исключениями в этом случае, заключите две строки метода
playSound()
в блокtry/catch
и перехватывайте только экземплярыSoLoudException
.
lib/audio/audio_controller.dart
...
Future<void> playSound(String assetKey) async {
try {
final source = await _soloud!.loadAsset(assetKey);
await _soloud!.play(source);
} on SoLoudException catch (e) {
_log.severe("Cannot play sound '$assetKey'. Ignoring.", e);
}
}
...
SoLoud выдает различные исключения, такие как SoLoudNotInitializedException
или SoLoudTemporaryFolderFailedException
. В документации API каждого метода перечислены виды исключений, которые могут быть выданы.
SoLoud также предоставляет родительский класс для всех своих исключений, исключение SoLoudException
, чтобы вы могли перехватывать все ошибки, связанные с функциональностью аудиодвижка. Это особенно полезно в случаях, когда воспроизведение звука не является критически важным. Например, когда вы не хотите, чтобы игровой сеанс игрока был прерван только из-за того, что один из звуков пиу-пиу не смог загрузиться.
Как вы, вероятно, ожидаете, метод loadAsset()
также может выдать ошибку FlutterError
, если вы предоставите ключ ассета, который не существует. Попытка загрузить ассеты, которые не связаны с игрой, как правило, является тем, на что следует обратить внимание, поэтому это ошибка .
Воспроизведение разных звуков
Вы могли заметить, что вы воспроизводите только файл pew1.mp3
, но в каталоге assets есть еще две версии звука. Часто он звучит более естественно, когда в играх есть несколько версий одного и того же звука, и разные версии воспроизводятся случайным образом или по очереди. Это предотвращает, например, слишком единообразное звучание шагов и выстрелов, а значит, и фальшивое.
- В качестве дополнительного упражнения измените код так, чтобы он воспроизводил другой звук скамьи при каждом нажатии кнопки.
5. Воспроизведение музыкальных циклов
Управление продолжительными звуками
Некоторые аудиозаписи предназначены для воспроизведения в течение длительного времени. Музыка — очевидный пример, но многие игры также воспроизводят окружение, например, завывание ветра в коридорах, отдаленное пение монахов, скрип многовекового металла или отдаленный кашель пациентов.
Это аудиоисточники с временем воспроизведения, которое можно измерить в минутах. Вам нужно отслеживать их, чтобы иметь возможность приостановить или остановить их, когда это необходимо. Они также часто поддерживаются большими файлами и могут занимать много памяти, поэтому еще одна причина отслеживать их — это возможность избавиться от экземпляра AudioSource
, когда он больше не нужен.
По этой причине вы введете новое частное поле в AudioController
. Это дескриптор для воспроизводимой песни, если таковая имеется. Добавьте следующую строку:
lib/audio/audio_controller.dart
...
class AudioController {
static final Logger _log = Logger('AudioController');
SoLoud? _soloud;
SoundHandle? _musicHandle; // ← Add this.
...
Включить музыку
По сути, воспроизведение музыки ничем не отличается от воспроизведения звука one-shot. Вам все равно сначала нужно загрузить файл assets/music/looped-song.ogg
как экземпляр класса AudioSource
, а затем использовать метод play()
SoLoud для его воспроизведения.
Однако на этот раз вы берете на себя управление звуком, которое возвращает метод play()
чтобы управлять звуком во время его воспроизведения.
- Если хотите, реализуйте метод
AudioController.startMusic()
самостоятельно. Ничего страшного, если вы не сделаете некоторые детали правильно. Важно, чтобы музыка начиналась, когда вы выбираете Start music .
Вот пример реализации:
lib/audio/audio_controller.dart
...
Future<void> startMusic() async {
if (_musicHandle != null) {
if (_soloud!.getIsValidVoiceHandle(_musicHandle!)) {
_log.info('Music is already playing. Stopping first.');
await _soloud!.stop(_musicHandle!);
}
}
final musicSource = await _soloud!.loadAsset(
'assets/music/looped-song.ogg',
mode: LoadMode.disk,
);
}
...
Обратите внимание, что вы загружаете музыкальный файл в дисковом режиме (перечисление LoadMode.disk
). Это означает, что файл загружается только частями по мере необходимости. Для более продолжительного аудио обычно лучше загружать в дисковом режиме. Для коротких звуковых эффектов имеет смысл загружать и распаковывать их в память (перечисление LoadMode.memory
по умолчанию).
Однако у вас есть пара проблем. Во-первых, музыка слишком громкая, перекрывающая звуки. В большинстве игр музыка большую часть времени играет на заднем плане, уступая центральное место более информативному звуку, такому как речь и звуковые эффекты. Это можно исправить с помощью параметра громкости метода воспроизведения. Например, вы можете попробовать _soloud!.play(musicSource, volume: 0.6)
для воспроизведения песни на громкости 60%. В качестве альтернативы вы можете установить громкость в любой момент позже с помощью чего-то вроде _soloud!.setVolume(_musicHandle, 0.6)
.
Вторая проблема заключается в том, что песня резко обрывается. Это происходит потому, что эта песня должна воспроизводиться циклично, а начальная точка цикла не совпадает с началом аудиофайла.
Это популярный выбор для игровой музыки, потому что это означает, что песня начинается с естественного вступления и затем играет столько, сколько нужно, без очевидной точки цикла. Когда игре нужно перейти от звучащей песни, она затухает песню.
К счастью, SoLoud предоставляет способы воспроизведения зацикленного аудио. Метод play()
принимает логическое значение для параметра looping
, а также значение для начальной точки цикла в качестве параметра loopingStartAt
. Результирующий код выглядит следующим образом:
lib/audio/audio_controller.dart
...
_musicHandle = await _soloud!.play(
musicSource,
volume: 0.6,
looping: true,
// ↓ The exact timestamp of the start of the loop.
loopingStartAt: const Duration(seconds: 25, milliseconds: 43),
);
...
Если вы не зададите параметр loopingStartAt
, по умолчанию он будет равен Duration.zero
(другими словами, начало аудиофайла). Если у вас есть музыкальный трек, который представляет собой идеальный цикл без какого-либо вступления, это то, что вам нужно.
- Чтобы убедиться, что источник звука правильно удален после завершения воспроизведения, послушайте поток
allInstancesFinished
, который предоставляет каждый источник звука. С добавленными вызовами журнала методstartMusic()
выглядит следующим образом:
lib/audio/audio_controller.dart
...
Future<void> startMusic() async {
if (_musicHandle != null) {
if (_soloud!.getIsValidVoiceHandle(_musicHandle!)) {
_log.info('Music is already playing. Stopping first.');
await _soloud!.stop(_musicHandle!);
}
}
_log.info('Loading music');
final musicSource = await _soloud!.loadAsset(
'assets/music/looped-song.ogg',
mode: LoadMode.disk,
);
musicSource.allInstancesFinished.first.then((_) {
_soloud!.disposeSource(musicSource);
_log.info('Music source disposed');
_musicHandle = null;
});
_log.info('Playing music');
_musicHandle = await _soloud!.play(
musicSource,
volume: 0.6,
looping: true,
loopingStartAt: const Duration(seconds: 25, milliseconds: 43),
);
}
...
Затухание звука
Ваша следующая проблема в том, что музыка никогда не заканчивается. Пришло время реализовать затухание.
Один из способов реализовать затухание — иметь какую-то функцию, которая вызывается несколько раз в секунду, например, Ticker
или Timer.periodic
, и уменьшать громкость музыки небольшими декрементами. Это сработает, но это требует много работы.
К счастью, SoLoud предоставляет удобные методы «запустить и забыть», которые делают это за вас. Вот как можно затухать музыку в течение пяти секунд, а затем остановить экземпляр звука, чтобы он не потреблял ресурсы ЦП без необходимости. Замените метод fadeOutMusic()
на этот код:
lib/audio/audio_controller.dart
...
void fadeOutMusic() {
if (_musicHandle == null) {
_log.info('Nothing to fade out');
return;
}
const length = Duration(seconds: 5);
_soloud!.fadeVolume(_musicHandle!, 0, length);
_soloud!.scheduleStop(_musicHandle!, length);
}
...
6. Применить эффекты
Огромным преимуществом наличия в вашем распоряжении полноценного звукового движка является то, что вы можете выполнять обработку звука, например, пропускать некоторые звуки через ревербератор, эквалайзер или фильтр нижних частот.
В играх это можно использовать для слуховой дифференциации локаций. Например, хлопок звучит по-разному в лесу и в бетонном бункере. В то время как лес помогает рассеивать и поглощать звук, голые стены бункера отражают звуковые волны обратно, что приводит к реверберации. Аналогично, голоса людей звучат по-разному, если их слушать через стену. Более высокие частоты этих звуков сильнее ослабляются при прохождении через твердую среду, что приводит к эффекту фильтра нижних частот.
SoLoud предоставляет несколько различных аудиоэффектов, которые можно применять к аудио.
- Чтобы создать впечатление, будто ваш плеер находится в большой комнате, например, в соборе или пещере, используйте поле
SoLoud.filters
:
lib/audio/audio_controller.dart
...
void applyFilter() {
_soloud!.filters.freeverbFilter.activate();
_soloud!.filters.freeverbFilter.wet.value = 0.2;
_soloud!.filters.freeverbFilter.roomSize.value = 0.9;
}
void removeFilter() {
_soloud!.filters.freeverbFilter.deactivate();
}
...
Поле SoLoud.filters
дает вам доступ ко всем типам фильтров и их параметрам. Каждый параметр также имеет встроенные функции, такие как постепенное затухание и осцилляция.
Примечание: _soloud!.filters
раскрывает глобальные фильтры. Если вы хотите применить фильтры к одному источнику, используйте аналог AudioSource.filters
, который действует так же.
С помощью предыдущего кода вы делаете следующее:
- Включить фильтр свободных глаголов глобально.
- Установите параметр Wet на
0.2
, что означает, что полученный звук будет на 80% оригинальным и на 20% выходным сигналом эффекта реверберации. Если вы установите этот параметр на1.0
, это будет похоже на то, что вы слышите только звуковые волны, которые возвращаются к вам от далеких стен комнаты, и никакого исходного звука. - Установите параметр « Размер комнаты» на
0.9
. Вы можете настроить этот параметр по своему вкусу или даже изменить его динамически.1.0
— это огромная пещера, а0.0
— это ванная комната.
- Если вы готовы, измените код и примените один из следующих фильтров или комбинацию следующих фильтров:
-
biquadFilter
(может использоваться как фильтр нижних частот) -
pitchShiftFilter
-
equalizerFilter
-
echoFilter
-
lofiFilter
-
flangerFilter
-
bassboostFilter
-
waveShaperFilter
-
robotizeFilter
7. Поздравления
Вы реализовали аудиоконтроллер, который воспроизводит звуки, зацикливает музыку и применяет эффекты.
Узнать больше
- Попробуйте расширить возможности аудиоконтроллера, добавив такие функции, как предварительная загрузка звуков при запуске, последовательное воспроизведение песен или постепенное применение фильтра с течением времени.
- Прочитайте документацию пакета
flutter_soloud
. - Ознакомьтесь с домашней страницей базовой библиотеки C++.
- Узнайте больше о Dart FFI — технологии, используемой для взаимодействия с библиотекой C++.
- Посмотрите выступление Гая Сомберга о программировании игрового звука для вдохновения. (Есть и более длинное выступление .) Когда Гай говорит о «промежуточном программном обеспечении», он имеет в виду библиотеки вроде SoLoud и FMOD. Остальной код, как правило, специфичен для каждой игры.
- Создайте свою игру и выпустите ее.