Adicionar som e música ao seu jogo do Flutter

Adicionar sons e músicas ao seu jogo Flutter

Sobre este codelab

subjectÚltimo jun. 6, 2025 atualizado
account_circleEscrito por Filip Hracek

1. Antes de começar

Os jogos são experiências audiovisuais. O Flutter é uma ótima ferramenta para criar recursos visuais incríveis e uma interface sólida, então ele ajuda muito no lado visual. O ingrediente que falta é o áudio. Neste codelab, você vai aprender a usar o plug-in flutter_soloud para introduzir som e música de baixa latência no seu projeto. Você começa com um scaffolding básico para poder ir direto às partes interessantes.

Uma ilustração desenhada à mão de fones de ouvido.

Você pode usar o que aprendeu aqui para adicionar áudio aos seus apps, não apenas aos jogos. No entanto, quase todos os jogos exigem som e música, mas a maioria dos apps não. Por isso, este codelab se concentra em jogos.

Pré-requisitos

  • Noções básicas do Flutter.
  • Saber como executar e depurar apps do Flutter.

Conteúdo do laboratório

  • Como tocar sons únicos.
  • Como tocar e personalizar loops de música sem interrupções.
  • Como fazer sons aparecerem e desaparecerem.
  • Como aplicar efeitos ambientais aos sons.
  • Como lidar com exceções.
  • Como encapsular todos esses recursos em um único controlador de áudio.

O que é necessário

  • O SDK do Flutter
  • Um editor de código de sua escolha

2. Configurar

  1. Faça o download dos seguintes arquivos. Se você tiver uma conexão lenta, não se preocupe. Você vai precisar dos arquivos mais tarde, então pode fazer o download deles enquanto trabalha.
  1. Crie um projeto do Flutter com o nome que quiser.
  1. Crie um arquivo lib/audio/audio_controller.dart no projeto.
  2. Insira este código no arquivo:

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
 
}
}

Como você pode ver, esse é apenas um esqueleto para funcionalidades futuras. Vamos implementar tudo durante este codelab.

  1. Em seguida, abra o arquivo lib/main.dart e substitua o conteúdo por este código:

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();
                   
}
                 
},
               
),
             
],
           
),
         
],
       
),
     
),
   
);
 
}
}
  1. Depois que os arquivos de áudio forem transferidos por download, crie um diretório na raiz do projeto chamado assets.
  2. No diretório assets, crie dois subdiretórios, um chamado music e outro sounds.
  3. Mova os arquivos transferidos para o projeto para que o arquivo da música esteja no arquivo assets/music/looped-song.ogg e os sons de bancos estejam nos seguintes arquivos:
  • assets/sounds/pew1.mp3
  • assets/sounds/pew2.mp3
  • assets/sounds/pew3.mp3

A estrutura do projeto vai ficar assim:

Uma visualização em árvore do projeto, com pastas como &quot;android&quot;, &quot;ios&quot;, arquivos como &quot;README.md&quot; e &quot;analysis_options.yaml&quot;. Entre eles, podemos ver o diretório &quot;assets&quot; com os subdiretórios &quot;music&quot; e &quot;sounds&quot;, o diretório &quot;lib&quot; com &quot;main.dart&quot; e um subdiretório &quot;audio&quot; com &quot;audio_controller.dart&quot;, e o arquivo &quot;pubspec.yaml&quot;.  As setas apontam para os novos diretórios e os arquivos que você tocou até agora.

Agora que os arquivos estão lá, você precisa informar o Flutter sobre eles.

  1. Abra o arquivo pubspec.yaml e substitua a seção flutter: na parte de baixo do arquivo pelo seguinte:

pubspec.yaml

...

flutter
:
  uses
-material-design: true

  assets
:
   
- assets/music/
   
- assets/sounds/
  1. Adicione uma dependência ao pacote flutter_soloud e ao pacote logging.
flutter pub add flutter_soloud logging

Agora, o arquivo pubspec.yaml tem dependências adicionais nos pacotes flutter_soloud e logging.

pubspec.yaml

...

dependencies
:
  flutter
:
    sdk
: flutter

  flutter_soloud
: ^3.1.10
  logging
: ^1.3.0

...
  1. Executar o projeto. Nada funciona ainda porque você vai adicionar a funcionalidade nas próximas seções.

10f0f751c9c47038.png

3. Inicializar e encerrar

Para tocar áudio, use o plug-in flutter_soloud. Esse plug-in é baseado no projeto SoLoud, um mecanismo de áudio C++ para jogos usado, entre outros, pelo Nintendo SNES Classic.

7ce23849b6d0d09a.png

Para inicializar o mecanismo de áudio SoLoud, siga estas etapas:

  1. No arquivo audio_controller.dart, importe o pacote flutter_soloud e adicione um campo _soloud particular à 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
 
}

 
...

O controlador de áudio gerencia o mecanismo SoLoud subjacente por meio desse campo e encaminha todas as chamadas para ele.

  1. No método initialize(), insira o seguinte código:

lib/audio/audio_controller.dart

...

 
Future<void> initialize() async {
    _soloud
= SoLoud.instance;
    await _soloud
!.init();
 
}

...

Isso preenche o campo _soloud e aguarda a inicialização. Observe o seguinte:

  • O SoLoud fornece um campo instance único. Não há como instanciar várias instâncias do SoLoud. Isso não é algo permitido pelo mecanismo C++, então também não é permitido pelo plug-in do Dart.
  • A inicialização do plug-in é assíncrona e não é concluída até que o método init() seja retornado.
  • Para encurtar este exemplo, não vamos capturar erros em um bloco try/catch. No código de produção, você quer fazer isso e informar os erros ao usuário.
  1. No método dispose(), insira o seguinte código:

lib/audio/audio_controller.dart

...

 
void dispose() {
    _soloud
?.deinit();
 
}

...

É uma boa prática desativar o SoLoud ao sair do app, mas tudo deve funcionar bem mesmo que você se esqueça de fazer isso.

  1. O método AudioController.initialize() já é chamado pela função main(). Isso significa que a reinicialização a quente do projeto inicializa o SoLoud em segundo plano, mas não vai funcionar antes que você toque alguns sons.

4. Tocar sons únicos

Carregar um recurso e reproduzi-lo

Agora que você sabe que o SoLoud é inicializado na inicialização, pode pedir para ele tocar sons.

O SoLoud diferencia uma fonte de áudio, que são os dados e metadados usados para descrever um som, e as "instâncias de som", que são os sons realmente reproduzidos. Um exemplo de fonte de áudio pode ser um arquivo mp3 carregado na memória, pronto para ser reproduzido e representado por uma instância da classe AudioSource. Sempre que você reproduz essa fonte de áudio, o SoLoud cria uma "instância de som", que é representada pelo tipo SoundHandle.

Para acessar uma instância AudioSource, carregue-a. Por exemplo, se você tiver um arquivo mp3 nos seus recursos, poderá carregá-lo para receber um AudioSource. Em seguida, você diz ao SoLoud para tocar esse AudioSource. Você pode assistir várias vezes, até mesmo simultaneamente.

Quando você terminar de usar uma fonte de áudio, descarte-a com o método SoLoud.disposeSource().

Para carregar um recurso e reproduzi-lo, siga estas etapas:

  1. No método playSound() da classe AudioController, insira o seguinte código:

lib/audio/audio_controller.dart

  ...

  Future<void> playSound(String assetKey) async {
    final source = await _soloud!.loadAsset(assetKey);
    await _soloud!.play(source);
  }

  ...
  1. Salve o arquivo, faça o reload rápido e selecione Play sound. Você vai ouvir um som de banco de igreja. Observe o seguinte:
  • O argumento assetKey fornecido é semelhante a assets/sounds/pew1.mp3, a mesma string que você forneceria para qualquer outra API de carregamento de recursos do Flutter, como o widget Image.asset().
  • A instância do SoLoud oferece um método loadAsset() que carrega assíncronamente um arquivo de áudio dos recursos do projeto do Flutter e retorna uma instância da classe AudioSource. Há métodos equivalentes para carregar um arquivo do sistema de arquivos (o método loadFile()) e para carregar pela rede de um URL (o método loadUrl()).
  • A instância AudioSource recém-adquirida é transmitida para o método play() do SoLoud. Esse método retorna uma instância do tipo SoundHandle que representa o som que está sendo reproduzido. Esse identificador pode ser transmitido a outros métodos do SoLoud para pausar, parar ou modificar o volume do som.
  • Embora play() seja um método assíncrono, a reprodução começa basicamente instantaneamente. O pacote flutter_soloud usa a interface de função externa (FFI) do Dart para chamar o código C de forma direta e síncrona. A troca de mensagens entre o código Dart e o código da plataforma, que é característica da maioria dos plug-ins do Flutter, não está disponível. O único motivo para alguns métodos serem assíncronos é que parte do código do plug-in é executado no próprio isolamento, e a comunicação entre os isolamentos do Dart é assíncrona.
  • Você declara que o campo _soloud não é nulo com _soloud!. Isso é, novamente, para fins de brevidade. O código de produção precisa lidar com a situação em que o desenvolvedor tenta reproduzir um som antes que o controlador de áudio tenha a chance de inicializar totalmente.

Lidar com exceções

Você pode ter notado que está ignorando possíveis exceções. É hora de corrigir isso para este método específico para fins de aprendizado. Para facilitar, o codelab volta a ignorar exceções após esta seção.

  • Para lidar com exceções nesse caso, envolva as duas linhas do método playSound() em um bloco try/catch e capture apenas instâncias de 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);
    }
  }

  ...

O SoLoud gera várias exceções, como SoLoudNotInitializedException ou SoLoudTemporaryFolderFailedException. Os documentos da API de cada método listam os tipos de exceções que podem ser geradas.

O SoLoud também fornece uma classe mãe para todas as exceções, a exceção SoLoudException, para que você possa detectar todos os erros relacionados à funcionalidade do mecanismo de áudio. Isso é especialmente útil nos casos em que a reprodução de áudio não é essencial. Por exemplo, quando você não quer que a sessão de jogo do jogador falhe apenas porque um dos sons não foi carregado.

Como você provavelmente espera, o método loadAsset() também pode gerar um erro FlutterError se você fornecer uma chave de recurso que não existe. Tentar carregar recursos que não estão incluídos no jogo geralmente é algo que você precisa resolver. Portanto, é um erro.

Tocar sons diferentes

Você pode ter notado que só é possível reproduzir o arquivo pew1.mp3, mas há duas outras versões do som no diretório de recursos. Muitas vezes, os jogos soam mais naturais quando têm várias versões do mesmo som e tocam as diferentes versões de forma aleatória ou alternada. Isso evita, por exemplo, que os passos e os tiros soem muito uniformes e, portanto, falsos.

  • Como exercício opcional, modifique o código para reproduzir um som de banco diferente sempre que o botão for tocado.

Uma ilustração de

5. Tocar loops de música

Gerenciar sons mais longos

Alguns áudios são destinados a serem reproduzidos por períodos mais longos. A música é o exemplo óbvio, mas muitos jogos também tocam ambientação, como o vento soprando pelos corredores, o canto distante de monges, o rangido de metal centenário ou a tosse distante de pacientes.

São fontes de áudio com tempos de reprodução que podem ser medidos em minutos. É preciso acompanhar o uso para pausar ou interromper quando necessário. Elas também costumam ser apoiadas por arquivos grandes e podem consumir muita memória. Portanto, outra razão para rastreá-las é para que você possa descartar a instância AudioSource quando ela não for mais necessária.

Por isso, você vai introduzir um novo campo privado em AudioController. É um identificador da música em reprodução, se houver. Adicione a linha abaixo:

lib/audio/audio_controller.dart

...

class AudioController {
 
static final Logger _log = Logger('AudioController');

 
SoLoud? _soloud;

 
SoundHandle? _musicHandle;    // ← Add this.

 
...

Iniciar música

Em essência, tocar música não é diferente de tocar um som único. Primeiro, você precisa carregar o arquivo assets/music/looped-song.ogg como uma instância da classe AudioSource e, em seguida, usar o método play() do SoLoud para reproduzir.

Mas, dessa vez, você vai usar o identificador de som que o método play() retorna para manipular o áudio enquanto ele está sendo reproduzido.

  • Se quiser, implemente o método AudioController.startMusic() por conta própria. Não tem problema se você não entender alguns detalhes. O importante é que a música comece quando você selecionar Start music.

Confira uma implementação de referência:

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,
    );
  }

...

Observe que você carrega o arquivo de música no modo de disco (o tipo enumerado LoadMode.disk). Isso significa que o arquivo só é carregado em partes conforme necessário. Para áudios mais longos, geralmente é melhor carregar no modo de disco. Para efeitos sonoros curtos, faz mais sentido carregá-los e descompactá-los na memória (o tipo enumerado LoadMode.memory padrão).

No entanto, você tem alguns problemas. Primeiro, a música está muito alta, anulando os sons. Na maioria dos jogos, a música fica em segundo plano a maior parte do tempo, destaque ao áudio mais informativo, como fala e efeitos sonoros. Isso é para corrigir o uso do parâmetro de volume do método de reprodução. Por exemplo, você pode usar _soloud!.play(musicSource, volume: 0.6) para tocar a música com 60% de volume. Como alternativa, defina o volume em qualquer momento posterior com algo como _soloud!.setVolume(_musicHandle, 0.6).

O segundo problema é que a música para abruptamente. Isso acontece porque é uma música que deve ser tocada em loop, e o ponto de partida do loop não é o início do arquivo de áudio.

88d2c57fffdfe996.png

Essa é uma escolha popular para músicas de jogos, porque significa que a música começa com uma introdução natural e depois toca pelo tempo necessário sem um ponto de repetição óbvio. Quando o jogo precisa sair da música em execução, ela é atenuada.

Felizmente, o SoLoud oferece maneiras de reproduzir áudio em loop. O método play() usa um valor booleano para o parâmetro looping e também o valor para o ponto de início do loop como o parâmetro loopingStartAt. O código resultante vai ficar assim:

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 você não definir o parâmetro loopingStartAt, ele vai ser definido como Duration.zero (ou seja, o início do arquivo de áudio). Se você tiver uma faixa de música que seja um loop perfeito sem introdução, esse é o ideal.

  • Para verificar se a fonte de áudio é descartada corretamente após a reprodução, ouça o stream allInstancesFinished fornecido por cada fonte de áudio. Com as chamadas de registro adicionadas, o método startMusic() fica assim:

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),
    );
  }

...

Esmaecer som

O próximo problema é que a música nunca acaba. É hora de implementar um efeito de desbotamento.

Uma maneira de implementar o fade seria ter algum tipo de função chamada várias vezes por segundo, como Ticker ou Timer.periodic, e diminuir o volume da música em pequenas decréscimos. Isso funcionaria, mas seria muito trabalhoso.

Felizmente, o SoLoud oferece métodos convenientes de disparo e esquecimento que fazem isso por você. Veja como você pode diminuir o volume da música ao longo de cinco segundos e, em seguida, interromper a instância de som para que ela não consuma recursos da CPU desnecessariamente. Substitua o método fadeOutMusic() por este código:

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. Aplicar efeitos

Uma grande vantagem de ter um mecanismo de áudio adequado à sua disposição é que você pode fazer o processamento de áudio, como encaminhar alguns sons por um reverb, um equalizador ou um filtro passa-baixa.

Em jogos, isso pode ser usado para diferenciar auditivamente os locais. Por exemplo, um aplauso soa diferente em uma floresta do que em um bunker de concreto. Enquanto uma floresta ajuda a dissipar e absorver o som, as paredes nuas de um bunker refletem as ondas sonoras, causando reverberação. Da mesma forma, as vozes das pessoas soam diferentes quando ouvidas por uma parede. As frequências mais altas desses sons são mais atenuadas à medida que viajam pelo meio sólido, resultando em um efeito de filtro passa-baixa.

Uma ilustração de duas pessoas conversando em uma sala. As ondas sonoras não só vão de uma pessoa para a outra diretamente, como também rebatem nas paredes e no teto.

O SoLoud oferece vários efeitos de áudio diferentes, que podem ser aplicados ao áudio.

  • Para fazer com que o jogador pareça estar em um ambiente grande, como uma catedral ou uma caverna, use o 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();
 
}

...

O campo SoLoud.filters dá acesso a todos os tipos de filtro e aos parâmetros deles. Cada parâmetro também tem funcionalidades integradas, como desbotamento e oscilação gradual.

Observação : _soloud!.filters expõe filtros globais. Se você quiser aplicar filtros a uma única fonte, use a contraparte AudioSource.filters, que funciona da mesma forma.

Com o código anterior, faça o seguinte:

  • Ative o filtro freeverb globalmente.
  • Defina o parâmetro Wet como 0.2, o que significa que o áudio resultante será 80% original e 20% da saída do efeito de reverb. Se você definir esse parâmetro como 1.0, será como ouvir apenas as ondas sonoras que retornam a você das paredes distantes da sala, e nenhum áudio original.
  • Defina o parâmetro Room Size como 0.9. Você pode ajustar esse parâmetro de acordo com sua preferência ou até mesmo mudá-lo dinamicamente. 1.0 é uma caverna enorme, e 0.0 é um banheiro.
  • Se quiser, mude o código e aplique um ou uma combinação dos seguintes filtros:
  • biquadFilter (pode ser usado como um filtro de passagem baixa)
  • pitchShiftFilter
  • equalizerFilter
  • echoFilter
  • lofiFilter
  • flangerFilter
  • bassboostFilter
  • waveShaperFilter
  • robotizeFilter

7. Parabéns

Você implementou um controlador de áudio que reproduz sons, faz loops de música e aplica efeitos.

Saiba mais

  • Tente aproveitar ainda mais o controle de áudio com recursos como pré-carregar sons na inicialização, tocar músicas em sequência ou aplicar um filtro gradualmente ao longo do tempo.
  • Leia a documentação do pacote do flutter_soloud.
  • Leia a página inicial da biblioteca C++.
  • Leia mais sobre a FFI do Dart, a tecnologia usada para fazer a interface com a biblioteca C++.
  • Assista à palestra de Guy Somberg sobre programação de áudio de jogos para se inspirar. Há também uma mais longa. Quando Guy fala sobre "middleware", ele se refere a bibliotecas como SoLoud e FMOD. O restante do código tende a ser específico para cada jogo.
  • Crie e lance seu jogo.

Uma ilustração de fones de ouvido