ukasz123 / soundpool

Soundpool plugin for Flutter
87 stars 64 forks source link

Audio becomes choppy when in the background #87

Open ntatko opened 2 years ago

ntatko commented 2 years ago

I'm running soundpool: ^2.2.0 on flutter 2.8.1.

WidgetsBinding.instance?.addObserver(this); is used to keep the event loop running in the background. I'm using a set of recursive timers that call one another to make the sound play, in the foreground and the background.

https://user-images.githubusercontent.com/37306038/148614891-292b727a-9549-4551-b771-6f36410b6bc0.mp4

Looking for some insight. Lemme know what I can do to make this stop, eh?

ukasz123 commented 2 years ago

@ntatko, can you provide a minimal example showing the problem? I don't have idea on how to reproduce the issue.

ntatko commented 2 years ago

Yeah, here's a simple widget that would have this effect - simple being a relative word. My implementation is a bit more complex, but this should hit the main points.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:soundpool/soundpool.dart';
import 'package:flutter/services.dart';

class BreatheBar extends StatefulWidget {
  const BreatheBar({Key? key}) : super(key: key);

  @override
  _BreathBarState createState() => _BreathBarState();
}

class _BreathBarState extends State<BreatheBar> with WidgetsBindingObserver {
  Timer? backgroundTimer;
  late Map<String, Map<String, int>> soundIds;
  Soundpool pool = Soundpool.fromOptions(
      options: const SoundpoolOptions(
          streamType: StreamType.alarm,
          iosOptions: SoundpoolOptionsIos(
              audioSessionCategory: AudioSessionCategory.ambient)));

  void waitingPlaySound() {
    setState(() {
      backgroundTimer = Timer(const Duration(seconds: 5), () {
        playSound(pool, soundIds);
        waitingPlaySound();
      });
    });
  }

  @override
  void initState() {
    loadSounds(pool).then((Map<String, Map<String, int>> codes) {
      soundIds = codes;
    }).then((_) {
      waitingPlaySound();
    });
    WidgetsBinding.instance?.addObserver(this);
    super.initState();
  }

  @override
  void dispose() {
    WidgetsBinding.instance?.removeObserver(this);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        padding: const EdgeInsets.all(30),
        child: const Text("Playing a sound every 5 seconds"));
  }
}

Map<String, Map<String, String>> sounds = {
  'Notify': {
    'inhale': 'assets/sounds/notify_inhale.mp3',
    'exhale': 'assets/sounds/notify_exhale.mp3'
  }
};

Future<Map<String, Map<String, int>>> loadSounds(Soundpool pool) async {
  Map<String, Map<String, int>> loadedSounds = {};
  sounds.forEach((key, value) async {
    int inId =
        await rootBundle.load(value['inhale']!).then((ByteData soundData) {
      return pool.load(soundData);
    });
    int outId =
        await rootBundle.load(value['exhale']!).then((ByteData soundData) {
      return pool.load(soundData);
    });
    loadedSounds[key] = {'inhale': inId, 'exhale': outId};
  });
  return loadedSounds;
}

void playSound(Soundpool pool, Map<String, Map<String, int>> loaded) {
  pool.play(loaded['Notify']!['inhale']!);
}
ukasz123 commented 2 years ago

Thank you for the sample.

Unfortunately I could not reproduce the problem using the code you provided. I tried both on simulator and real device. I noticed that on real device (iPhone SE 2020) the sound was silenced when app was moved to the background unless I had changed the audioSessionCategory to AudioSessionCategory.playback. Can you reproduce the problem with this sample? How long are sounds in your application? I wonder if they may overlap somehow and do this "mixed" sound.

PS. I suggest to change the recursive call chain with Timer.periodic() factory method. I won't promise it would fix anything but it may simplify your code.
ukasz123 commented 2 years ago

I've prepared a sample app using your example. Source code.