Informazioni su questo codelab
1. Prima di iniziare
I giochi sono esperienze audiovisive. Flutter è un ottimo strumento per creare immagini bellissime e un'interfaccia utente solida, quindi ti aiuta molto dal punto di vista visivo. L'ingrediente mancante è l'audio. In questo codelab imparerai a utilizzare il plug-in flutter_soloud
per inserire audio e musica a bassa latenza nel tuo progetto. Inizia con uno scheletro di base per passare direttamente alle parti interessanti.
Ovviamente, puoi utilizzare ciò che impari qui per aggiungere audio alle tue app, non solo ai giochi. Tuttavia, mentre quasi tutti i giochi richiedono audio e musica, la maggior parte delle app no, quindi questo codelab si concentra sui giochi.
Prerequisiti
- Familiarità di base con Flutter.
- Conoscenza di come eseguire ed eseguire il debug di app Flutter.
Cosa imparerai
- Come riprodurre i suoni una tantum.
- Come riprodurre e personalizzare i loop musicali senza interruzioni.
- Come attenuare e aumentare gradualmente i suoni.
- Come applicare effetti ambientali ai suoni.
- Come gestire le eccezioni.
- Come incapsulare tutte queste funzionalità in un unico controller audio.
Cosa serve
- L'SDK Flutter
- Un editor di codice a tua scelta
2. Configura
- Scarica i seguenti file. Se la connessione è lenta, non preoccuparti. Poiché avrai bisogno dei file effettivi, puoi lasciarli scaricare mentre lavori.
- Crea un progetto Flutter con un nome a tua scelta.
- Crea un file
lib/audio/audio_controller.dart
nel progetto. - Nel file, inserisci il seguente codice:
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
}
}
Come puoi vedere, si tratta solo di uno scheletro per le funzionalità future. Lo implementeremo tutto durante questo codelab.
- Successivamente, apri il file
lib/main.dart
e sostituisci i relativi contenuti con il seguente codice:
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();
}
},
),
],
),
],
),
),
);
}
}
- Dopo aver scaricato i file audio, crea una directory nella directory principale del progetto denominata
assets
. - Nella directory
assets
, crea due sottodirectory, una denominatamusic
e l'altrasounds
. - Sposta i file scaricati nel progetto in modo che il file del brano sia nel file
assets/music/looped-song.ogg
e i suoni della panca siano nei seguenti file:
assets/sounds/pew1.mp3
assets/sounds/pew2.mp3
assets/sounds/pew3.mp3
La struttura del progetto dovrebbe ora essere simile a questa:
Ora che i file sono presenti, devi comunicarli a Flutter.
- Apri il file
pubspec.yaml
e sostituisci la sezioneflutter:
in fondo al file con quanto segue:
pubspec.yaml
...
flutter:
uses-material-design: true
assets:
- assets/music/
- assets/sounds/
- Aggiungi una dipendenza dal pacchetto
flutter_soloud
e dal pacchettologging
.
flutter pub add flutter_soloud logging
Il file pubspec.yaml
ora dovrebbe avere dipendenze aggiuntive dai pacchetti flutter_soloud
e logging
.
pubspec.yaml
...
dependencies:
flutter:
sdk: flutter
flutter_soloud: ^3.1.10
logging: ^1.3.0
...
- Esegui il progetto. Non funziona ancora perché aggiungi la funzionalità nelle sezioni seguenti.
3. Inizializzazione e arresto
Per riprodurre l'audio, utilizza il plug-in flutter_soloud
. Questo plug-in si basa sul progetto SoLoud, un motore audio C++ per giochi utilizzato, tra gli altri, da Nintendo SNES Classic.
Per inizializzare il motore audio SoLoud, segui questi passaggi:
- Nel file
audio_controller.dart
, importa il pacchettoflutter_soloud
e aggiungi un campo_soloud
privato alla classe.
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
}
...
Il controller audio gestisce il motore SoLoud sottostante tramite questo campo e inoltra tutte le chiamate.
- Nel metodo
initialize()
, inserisci il seguente codice:
lib/audio/audio_controller.dart
...
Future<void> initialize() async {
_soloud = SoLoud.instance;
await _soloud!.init();
}
...
Il campo _soloud
viene compilato e attende l'inizializzazione. Tieni presente quanto segue:
- SoLoud fornisce un campo singleton
instance
. Non è possibile creare più istanze di SoLoud. Questo non è consentito dal motore C++, quindi non è consentito nemmeno dal plug-in Dart. - L'inizializzazione del plug-in è asincrona e non è completata finché non viene restituito il metodo
init()
. - Per brevità, in questo esempio non vengono rilevati errori in un blocco
try/catch
. Nel codice di produzione, vuoi farlo e segnalare eventuali errori all'utente.
- Nel metodo
dispose()
, inserisci il seguente codice:
lib/audio/audio_controller.dart
...
void dispose() {
_soloud?.deinit();
}
...
È buona norma arrestare SoLoud all'uscita dall'app, anche se tutto dovrebbe funzionare correttamente anche se non lo fai.
- Tieni presente che il metodo
AudioController.initialize()
è già chiamato dalla funzionemain()
. Ciò significa che il riavvio a caldo del progetto inizializza SoLoud in background, ma non ti sarà utile prima di riprodurre effettivamente alcuni suoni.
4. Riproduci suoni one-shot
Caricare una risorsa e riprodurla
Ora che sai che SoLoud viene inizializzato all'avvio, puoi chiedergli di riprodurre suoni.
SoLoud distingue tra un'origine audio, ovvero i dati e i metadati utilizzati per descrivere un suono, e le relative "istanze audio", ovvero i suoni effettivamente riprodotti. Un esempio di sorgente audio può essere un file MP3 caricato in memoria, pronto per essere riprodotto e rappresentato da un'istanza della classe AudioSource
. Ogni volta che riproduci questa sorgente audio, SoLoud crea un'"istanza audio" rappresentata dal tipo SoundHandle
.
Puoi ottenere un'istanza AudioSource
caricandola. Ad esempio, se hai un file MP3 tra le risorse, puoi caricarlo per ottenere un AudioSource
. Poi chiedi a SoLoud di riprodurre questo AudioSource
. Puoi giocarci più volte, anche contemporaneamente.
Quando non hai più bisogno di un'origine audio, puoi rimuoverla con il metodo SoLoud.disposeSource()
.
Per caricare un asset e riprodurlo:
- Nel metodo
playSound()
della classeAudioController
, inserisci il seguente codice:
lib/audio/audio_controller.dart
...
Future<void> playSound(String assetKey) async {
final source = await _soloud!.loadAsset(assetKey);
await _soloud!.play(source);
}
...
- Salva il file, esegui il ricaricamento rapido e seleziona Riproduci suono. Dovresti sentire un suono buffo. Tieni presente quanto segue:
- L'argomento
assetKey
fornito è simile aassets/sounds/pew1.mp3
, la stessa stringa che forniresti a qualsiasi altra API Flutter di caricamento di asset, ad esempio il widgetImage.asset()
. - L'istanza SoLoud fornisce un metodo
loadAsset()
che carica in modo asincrono un file audio dalle risorse del progetto Flutter e restituisce un'istanza della classeAudioSource
. Esistono metodi equivalenti per caricare un file dal file system (metodoloadFile()
) e per caricarlo tramite la rete da un URL (metodoloadUrl()
). - L'istanza
AudioSource
appena acquisita viene poi passata al metodoplay()
di SoLoud. Questo metodo restituisce un'istanza del tipoSoundHandle
che rappresenta l'audio appena riprodotto. Questo handle può essere passato ad altri metodi SoLoud per eseguire operazioni come mettere in pausa, interrompere o modificare il volume dell'audio. - Sebbene
play()
sia un metodo asincrono, la riproduzione inizia praticamente istantaneamente. Il pacchettoflutter_soloud
utilizza l'interfaccia di funzione esterna (FFI) di Dart per chiamare il codice C in modo diretto e sincrono. I soliti messaggi tra il codice Dart e il codice della piattaforma, caratteristici della maggior parte dei plug-in Flutter, non sono presenti. L'unico motivo per cui alcuni metodi sono asincroni è che parte del codice del plug-in viene eseguita nel proprio isolato e la comunicazione tra gli istanze Dart è asincrona. - Affermi che il campo
_soloud
non è nullo con_soloud!
. Anche in questo caso, per brevità. Il codice di produzione deve gestire in modo elegante la situazione in cui lo sviluppatore tenta di riprodurre un suono prima che il controller audio abbia avuto la possibilità di inizializzarsi completamente.
Gestire le eccezioni
Potresti aver notato che, ancora una volta, stai ignorando possibili eccezioni. È giunto il momento di correggere questo problema per questo metodo specifico a scopo didattico. Per brevità, il codelab torna a ignorare le eccezioni dopo questa sezione.
- Per gestire le eccezioni in questo caso, inserisci le due righe del metodo
playSound()
in un bloccotry/catch
e rileva solo le istanze diSoLoudException
.
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 genera varie eccezioni, ad esempio le eccezioni SoLoudNotInitializedException
o SoLoudTemporaryFolderFailedException
. La documentazione dell'API di ogni metodo elenca i tipi di eccezioni che potrebbero essere lanciate.
SoLoud fornisce anche una classe principale per tutte le sue eccezioni, l'eccezione SoLoudException
, in modo da poter rilevare tutti gli errori relativi alla funzionalità del motore audio. Questo è particolarmente utile nei casi in cui la riproduzione dell'audio non è fondamentale. Ad esempio, quando non vuoi che la sessione di gioco del giocatore abbia un arresto anomalo solo perché non è stato possibile caricare uno dei suoni pew-pew.
Come probabilmente previsto, il metodo loadAsset()
può anche generare un errore FlutterError
se fornisci una chiave asset non esistente. In genere, dovresti risolvere i problemi relativi al caricamento di asset non inclusi nel gioco, pertanto si tratta di un errore.
Riproduci suoni diversi
Potresti aver notato che riproduci solo il file pew1.mp3
, ma nella directory delle risorse sono presenti altre due versioni dell'audio. Spesso sembra più naturale quando i giochi hanno più versioni dello stesso suono e le riproducono in modo casuale o a rotazione. In questo modo, ad esempio, i passi e gli spari non risulteranno troppo uniformi e quindi falsi.
- Come esercizio facoltativo, modifica il codice in modo da riprodurre un suono diverso ogni volta che viene toccato il pulsante.
5. Riprodurre loop musicali
Gestire gli audio più lunghi
Alcuni contenuti audio sono pensati per essere riprodotti per periodi di tempo prolungati. La musica è l'esempio più ovvio, ma molti giochi riproducono anche l'ambiente, ad esempio il vento che ulula nei corridoi, il canto lontano dei monaci, il cigolio di metallo secolare o le tossi lontane dei pazienti.
Si tratta di sorgenti audio con durate che possono essere misurate in minuti. Devi monitorarli per poterli mettere in pausa o interrompere quando necessario. Inoltre, spesso sono supportati da file di grandi dimensioni e possono consumare molta memoria. Un altro motivo per monitorarli è che puoi eliminare l'istanza di AudioSource
quando non è più necessaria.
Per questo motivo, dovrai introdurre un nuovo campo privato in AudioController
. Si tratta di un handle del brano in riproduzione, se presente. Aggiungi la seguente riga:
lib/audio/audio_controller.dart
...
class AudioController {
static final Logger _log = Logger('AudioController');
SoLoud? _soloud;
SoundHandle? _musicHandle; // ← Add this.
...
Avvia la musica
In sostanza, riprodurre musica non è diverso dalla riproduzione di un suono una tantum. Devi comunque prima caricare il file assets/music/looped-song.ogg
come istanza della classe AudioSource
, quindi utilizzare il metodo play()
di SoLoud per riprodurlo.
Questa volta, però, prendi il handle audio restituito dal metodo play()
per manipolare l'audio durante la riproduzione.
- Se vuoi, puoi implementare il metodo
AudioController.startMusic()
autonomamente. Non preoccuparti se non ricordi alcuni dettagli. L'importante è che la musica inizi quando selezioni Avvia musica.
Ecco un'implementazione di riferimento:
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,
);
}
...
Tieni presente che carichi il file musicale in modalità disco (l'enum LoadMode.disk
). Ciò significa che il file viene caricato solo a blocchi, in base alle esigenze. Per l'audio di durata più lunga, in genere è meglio caricare in modalità disco. Per gli effetti sonori brevi, ha più senso caricarli e decomprimerli in memoria (l'enum LoadMode.memory
predefinito).
Tuttavia, ci sono un paio di problemi. Innanzitutto, la musica è troppo alta e copre i suoni. Nella maggior parte dei giochi, la musica è in sottofondo la maggior parte del tempo, lasciando spazio agli elementi audio più informativi, come i dialoghi e gli effetti sonori. Questo serve a correggere l'utilizzo del parametro volume del metodo play. Ad esempio, puoi provare a dire _soloud!.play(musicSource, volume: 0.6)
per riprodurre il brano con il volume al 60%. In alternativa, puoi impostare il volume in un secondo momento con un comando come _soloud!.setVolume(_musicHandle, 0.6)
.
Il secondo problema è che il brano si interrompe bruscamente. Questo perché si tratta di un brano che dovrebbe essere riprodotto in loop e il punto di partenza del loop non è l'inizio del file audio.
Si tratta di una scelta popolare per la musica di gioco perché il brano inizia con un'introduzione naturale e viene riprodotto per tutto il tempo necessario senza un punto di loop evidente. Quando il gioco deve uscire dal brano in riproduzione, il brano viene attenuato.
Fortunatamente, SoLoud offre modi per riprodurre l'audio in loop. Il metodo play()
accetta un valore booleano per il parametro looping
e il valore per il punto di partenza del ciclo come parametro loopingStartAt
. Il codice risultante sarà simile al seguente:
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),
);
...
Se non imposti il parametro loopingStartAt
, il valore predefinito è Duration.zero
(in altre parole, l'inizio del file audio). Se hai una traccia musicale che è un loop perfetto senza introduzione, è quello che ti serve.
- Per verificare che l'origine audio venga eliminata correttamente al termine della riproduzione, ascolta lo stream
allInstancesFinished
fornito da ogni origine audio. Con le chiamate di log aggiunte, il metodostartMusic()
avrà il seguente aspetto:
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),
);
}
...
Dissolvenza audio
Il problema successivo è che la musica non finisce mai. È il momento di implementare una dissolvenza.
Un modo per implementare l'attenuazione è avere una sorta di funzione chiamata più volte al secondo, ad esempio Ticker
o Timer.periodic
, e abbassare il volume della musica con piccoli decrementi. Funziona, ma richiede molto lavoro.
Fortunatamente, SoLoud offre comodi metodi di esecuzione che fanno tutto per te. Ecco come puoi attenuare la musica nel corso di cinque secondi e poi interrompere l'istanza audio in modo che non consumi risorse della CPU inutilmente. Sostituisci il metodo fadeOutMusic()
con questo codice:
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. Applicare effetti
Un enorme vantaggio di avere a disposizione un motore audio adeguato è che puoi eseguire l'elaborazione audio, ad esempio instradare alcuni suoni tramite un riverbero, un equalizzatore o un filtro passa basso.
Nei giochi, può essere utilizzato per la differenziazione uditiva delle posizioni. Ad esempio, un applauso suona in modo diverso in una foresta rispetto a un bunker di cemento. Mentre una foresta aiuta a dissipare e assorbire il suono, le pareti spoglie di un bunker riflettono le onde sonore, causando il riverbero. Analogamente, le voci delle persone suonano in modo diverso quando vengono ascoltate attraverso una parete. Le frequenze più alte di questi suoni vengono attenuate maggiormente mentre si propagano attraverso il mezzo solido, con un effetto di filtro passa basso.
SoLoud offre diversi effetti audio che puoi applicare all'audio.
- Per far sembrare che il tuo giocatore si trovi in una grande stanza, come una cattedrale o una grotta, utilizza il campo
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();
}
...
Il campo SoLoud.filters
ti consente di accedere a tutti i tipi di filtri e ai relativi parametri. Ogni parametro ha anche funzionalità integrate come l'oscillazione e lo sbiadimento graduale.
Nota: _soloud!.filters
espone i filtri globali. Se vuoi applicare filtri a una singola origine, utilizza l'opzione AudioSource.filters
, che ha lo stesso comportamento.
Con il codice precedente, esegui le seguenti operazioni:
- Attiva il filtro freeverb a livello globale.
- Imposta il parametro Wet su
0.2
, il che significa che l'audio risultante sarà per l'80% originale e per il 20% l'output dell'effetto riverbero. Se imposti questo parametro su1.0
, è come sentire solo le onde sonore che ti ritornano dalle pareti lontane della stanza e nessun audio originale. - Imposta il parametro Dimensioni della camera su
0.9
. Puoi modificare questo parametro in base alle tue esigenze o addirittura cambiarlo in modo dinamico.1.0
è una grotta enorme, mentre0.0
è un bagno.
- Se vuoi, modifica il codice e applica uno dei seguenti filtri o una combinazione di questi:
biquadFilter
(può essere utilizzato come filtro passa basso)pitchShiftFilter
equalizerFilter
echoFilter
lofiFilter
flangerFilter
bassboostFilter
waveShaperFilter
robotizeFilter
7. Complimenti
Hai implementato un controller audio che riproduce suoni, riproduce musica in loop e applica effetti.
Scopri di più
- Prova a migliorare il controller audio con funzionalità come il precaricamento dei suoni all'avvio, la riproduzione dei brani in sequenza o l'applicazione graduale di un filtro nel tempo.
- Leggi la documentazione del pacchetto di
flutter_soloud
. - Leggi la home page della libreria C++ sottostante.
- Scopri di più su Dart FFI, la tecnologia utilizzata per l'interfaccia con la libreria C++.
- Guarda il talk di Guy Somberg sulla programmazione audio dei giochi per trovare ispirazione. Esiste anche una versione più lunga. Quando Guy parla di "middleware", si riferisce a librerie come SoLoud e FMOD. Il resto del codice tende ad essere specifico per ogni gioco.
- Crea e rilascia il tuo gioco.