1. Prima di iniziare
I giochi sono esperienze audiovisive. Flutter è un ottimo strumento per creare splendide immagini e una UI solida, così puoi sfruttare appieno le potenzialità visive. L'ingrediente mancante è l'audio. In questo codelab, imparerai a utilizzare il plug-in flutter_soloud
per introdurre audio e musica a bassa latenza nel tuo progetto. Si inizia con un'impilamento di base in modo da poter passare direttamente alle parti interessanti.
Ovviamente puoi usare ciò che impari qui per aggiungere audio alle tue app, non solo ai giochi. Tuttavia, anche se quasi tutti i giochi richiedono audio e musica, la maggior parte delle app non lo richiedono, quindi questo codelab è incentrato sui giochi.
Prerequisiti
- Familiarità di base con Flutter.
- Conoscenza di come eseguire ed eseguire il debug delle app Flutter.
Cosa imparerai
- Come riprodurre i suoni una tantum.
- Come riprodurre e personalizzare i loop musicali senza interruzioni.
- Come far sfumare i suoni in entrata e in uscita.
- Come applicare effetti ambientali ai suoni.
- Come gestire le eccezioni.
- Come incapsulare tutte queste funzionalità in un unico controller audio.
Cosa serve
- 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 codice seguente:
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. 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),
useMaterial3: true,
),
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 radice del progetto chiamata
assets
. - Nella directory
assets
, crea due sottodirectory, una denominatamusic
e l'altra denominatasounds
. - 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 tuo progetto ora dovrebbe essere simile alla seguente:
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
.
pubspec.yaml
...
dependencies:
flutter:
sdk: flutter
flutter_soloud: ^2.0.0
logging: ^1.2.0
...
- Esegui il progetto. Non funziona ancora perché hai aggiunto la funzionalità nelle sezioni seguenti.
/flutter_soloud/src/filters/filters.cpp:21:24: warning: implicit conversion loses integer precision: 'decltype(__x.base() - __y.base())' (aka 'long') to 'int' [-Wshorten-64-to-32];
Questi provengono dalla libreria C++ SoLoud
sottostante. Non hanno alcun effetto sulla funzionalità e possono essere ignorati in tutta sicurezza.
3. Inizializza e arresta
Per riprodurre l'audio, usa 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:ui';
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();
}
...
Questa operazione compila il campo _soloud
e attende l'inizializzazione. Tieni presente quanto segue:
- SoLoud fornisce un campo
instance
singleton. 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 termina finché non viene restituito il metodo
init()
. - Per brevità, in questo esempio non stai individuando 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 spegnere SoLoud all'uscita dall'app, anche se non lo fai.
- Nota 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
Carica un asset e riproducilo
Ora che sai che SoLoud è inizializzato all'avvio, puoi chiedergli di riprodurre i suoni.
SoLoud fa distinzione tra una sorgente audio, ovvero i dati e i metadati utilizzati per descrivere un suono, e le sue "istanze di suono", 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 sonora" rappresentato 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 riprodurlo 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, ricarica a caldo 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, come 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 (il metodoloadFile()
) e per caricarlo sulla rete da un URL (il metodoloadUrl()
). - L'istanza
AudioSource
appena acquisita viene poi passata al metodoplay()
di SoLoud. Questo metodo restituisce un'istanza del tipoSoundHandle
che rappresenta il suono 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 una parte del codice del plug-in viene eseguita nel proprio isolato e la comunicazione tra gli isolati Dart è asincrona. - Devi semplicemente dichiarare che il campo
_soloud
non è nullo in_soloud!
. Anche in questo caso, per brevità. Il codice di produzione dovrebbe gestire agevolmente la situazione in cui lo sviluppatore tenta di riprodurre un suono prima che il controller audio abbia avuto la possibilità di inizializzarlo completamente.
Gestire le eccezioni
Avrai notato che stai ancora ignorando le possibili eccezioni. Risolviamo il problema per questo particolare metodo per scopi di apprendimento. Per brevità, il codelab torna a ignorare le eccezioni dopo questa sezione.
- Per gestire le eccezioni in questo caso, aggrega 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 padre per tutte le eccezioni, l'eccezione SoLoudException
, in modo che tu possa individuare tutti gli errori relativi alla funzionalità del motore audio. Ciò è 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 che non esiste. In genere, provare a caricare asset non inclusi nel gioco è un problema che devi risolvere, quindi 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 tocchi il pulsante.
5. Riproduci loop musicali
Gestire gli audio più lunghi
Parte dell'audio è pensata per essere riprodotta per lunghi periodi di tempo. La musica è l'esempio ovvio, ma molti giochi fanno anche atmosfera, come il vento che ulula attraverso i corridoi, i canti lontani dei monaci, i scricchiolii di metalli secolari o i lontani tosse dei pazienti.
Si tratta di sorgenti audio con durate che possono essere misurate in minuti. È necessario tenerne traccia in modo da 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 AudioSource
quando non è più necessaria.
Per questo motivo, introdurrerai un nuovo campo privato in AudioController
. È un handle per l'eventuale brano in riproduzione. 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 da un suono one-shot. 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);
_musicHandle = await _soloud!.play(musicSource);
}
...
Tieni presente che carichi il file musicale in modalità disco (l'enum LoadMode.disk
). Ciò significa semplicemente che il file viene caricato solo a blocchi, in base alle esigenze. Per audio più lunghi, è generalmente preferibile caricare i file in modalità disco. Per gli effetti sonori brevi, ha più senso caricarli e decomprimerli in memoria (l'enum LoadMode.memory
predefinito).
Tuttavia, hai un paio di problemi. Innanzitutto, la musica è troppo forte e sovraccarica i suoni. Nella maggior parte dei giochi, la musica è in sottofondo la maggior parte delle volte, dando la priorità all'audio più informativo, come il parlato e gli effetti sonori. Il problema è facile da risolvere utilizzando il 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 simile a _soloud!.setVolume(_musicHandle, 0.6)
.
Il secondo problema è che la canzone si interrompe bruscamente. Il motivo è che 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 molto diffusa per la musica dei giochi, perché significa che il brano inizia con un'introduzione naturale e poi viene riprodotto per tutto il tempo necessario senza un punto loop evidente. Quando il gioco deve uscire dal brano in riproduzione, il brano viene semplicemente attenuato.
Fortunatamente, SoLoud offre alcuni modi per riprodurre 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 ha il seguente aspetto:
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 assicurarti che la sorgente audio venga smaltita correttamente al termine della riproduzione, ascolta lo stream
allInstancesFinished
fornito da ogni sorgente 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 prossimo problema è che la musica non finisce mai. Implementiamo 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. Questa soluzione andrebbe bene, ma è molto impegnativo.
Fortunatamente, SoLoud offre pratici metodi da usare in modo automatico che permettono di farlo 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. Applica 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 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 più facilmente 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
consente di accedere a tutti i tipi di filtro e ai relativi parametri. Ogni parametro ha anche funzionalità integrate come dissolvenza graduale e oscillazione.
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 di 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 stanza su
0.9
. Puoi modificare questo parametro in base alle tue esigenze o addirittura cambiarlo in modo dinamico.1.0
è un'enorme caverna, 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 in loop la musica 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 discorso di Guy Somberg sulla programmazione audio dei giochi per trovare l'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 il tuo gioco e rilascialo.