Even if I am a person who mutes all sounds my devices make, I want sounds and background music for my flutter game. After gathering the files, it was half a day of work to make this puzzle sing to my ears.

Sound Files

For sound effects, I looked for free sound samples. I ended up using some from mixkit.co.

As a looping background music, my husband used Sonic Pi to create something for me. No link to that, sorry, but credits to him.

The audio files are stored as .wav in my assets/audio/ directory and added to pubspec.yaml.

AudioPlayers

I ended up with AudioCache from the AudioPlayers Plugin. And to centralize my sounds, I created a SoundModule class (although I’m not happy with the name and open for suggestions). Additionally I want to save the chosen volume settings in Shared Preferences.

First, here is the SoundModule code:

import 'package:audioplayers/audioplayers.dart';
import 'package:ruby_robbery/helper/preferences.dart';

class SoundModule {

  static final SoundModule _soundModule = SoundModule._internal();

  Preferences preferences = Preferences();
  bool mute = false;

  late AudioCache effectCache;
  late AudioCache backgroundCache;
  late AudioPlayer backgroundPlayer;

  String LEVEL_UNLOCK_SOUND = "audio/sound1.wav";
  String LEVEL_COMPLETE_SOUND = "audio/sound2.wav";
  String TILE_MOVED_SOUND = "audio/sound3.wav";

  String BACKGROUND_MUSIC = "audio/background_music.wav";

  factory SoundModule() {
    return _soundModule;
  }

  SoundModule._internal() {
    effectCache = AudioCache();
    backgroundCache = AudioCache();
    mute = preferences.isMute();
  }

  void startBackgroundMusic() async {
    backgroundPlayer = await backgroundCache.loop(BACKGROUND_MUSIC, volume: preferences.getBackgroundVolume());
  }

  void muteBackground() async {
    await backgroundPlayer.setVolume(0.0);
    preferences.setBackgroundVolume(0.0);
  }

  void playSound(String sound) async {
    if (mute) return;
    try {
      await effectCache.play( sound, volume: preferences.getEffectVolume() );
    } catch (err) {
      print(err);
    }
  }

  void updateBackgroundMusic() async {
    backgroundPlayer.setVolume(preferences.getBackgroundVolume());
  }

  void muteSound() {
    mute = true;
    backgroundPlayer.setVolume(0.0);
  }
}

The code is used when a tile is dragged, a level is completed or unlocked. And the background music starts when the app is opened. A call looks like this:

void playEffectSound() {
    SoundModule soundModule = SoundModule();
    soundModule.playSound(soundModule.TILE_MOVED_SOUND);
  }

Settings

The volume must be variable by the users choice, so I expanded my SettingPage with the first real setting. As you can see I used some fancy Sliders.

Volume Settings Sliders
Widget audio() {
    double backgroundVolume = preferences.getBackgroundVolume()*10;
    double effectVolume = preferences.getBackgroundVolume()*10;

    return Column(
      children: [
        Text(context.l10n.volume),
        Row(
          children: [
            Text(context.l10n.volumeEffect),
            Slider(
              max: 10.0,
              divisions: 10,
              value: effectVolume,
              onChanged: (double value) => setState(() {
                effectVolume = value;
                preferences.setEffectVolume(value/10.0);
                playEffectSound();
              })
            )
          ],
        ),
        Row(
          children: [
            Text(context.l10n.volumeBackground),
            Slider(
              max: 10.0,
              divisions: 10,
              value: backgroundVolume,
              onChanged: (double value) => setState(() {
                backgroundVolume = value;
                preferences.setBackgroundVolume(value/10.0);
              })
            )
          ],
        )
      ],
    );
  }

Changing the effect volume will play a sound to give feedback to the user, whereas the background volume change affects the current player:

void setBackgroundVolume(double volume) {
    backgroundVolume = volume;
    sharedPreferences.setDouble('background_volume', backgroundVolume);
    SoundModule soundModule = SoundModule();
    soundModule.updateBackgroundMusic();
  }

Autoplay on the web

Then I stumbled into the problem that the Google Chrome blocks autoplay of sound (and video) as described here.

So I wrapped the start of background music in a condition to exclude web:

@override
  void initState() {
    super.initState();
    if (!kIsWeb) {
      SoundModule soundModule = SoundModule();
      soundModule.startBackgroundMusic();
    }
  }

… and I introduced a variable that is checked whenever a button on the main page is pressed:

bool startedBackgroundMusic = false;
...
void checkBackgroundMusic() {
    if (!startedBackgroundMusic) {
      startedBackgroundMusic = true;
      SoundModule soundModule = SoundModule();
      soundModule.startBackgroundMusic();
    }
  }

I will insert some dialogs in the future to warn the user of upcoming background music…

Mute Button

As part of web accessibility guidelines and as long as I haven’t insert my warning dialogs, there has to be a present mute button to stop any sound immediately. Again, as someone who mutes all sounds, I’m totally behind this.

Widget muteButton(BuildContext context) {
    checkBackgroundMusic();
    bool volumeOff = prefs.isMute();
    return Positioned(
      right: 30.0,
      bottom: 0,
      child: IconButton(
        icon: volumeOff ? const Icon(Icons.volume_up) : const Icon(Icons.volume_off),
        color: PuzzleColors.primary0,
        onPressed: () => prefs.setMute(volumeOff),
      ),
    );
  }

Yes, I already see all the places where optimization is possible, but one step after the other.
Right now I’m super happy that I created my very first applications that makes some noise.

My Flutter Flight – Add some sound

Post navigation


Leave a Reply

Your email address will not be published.