2

The app plays some background audio (wav) in an endless loop. Audio playback was flawless using just_audio package, but after switching to audioplayers, all looped audio now contain a short gap of silence before they restart at the beginning. Question now is: How to get rid of the gap?

The code to load and play the files is quite simple:

  Future<void> loadAmbient() async {
    _audioAmbient = AudioPlayer();
    await _audioAmbient!.setSource(AssetSource('audio/ambient.wav'));
    await _audioAmbient!.setReleaseMode(ReleaseMode.loop);
  }

  void playAmbient() async {
    if (PlayerState.playing != _audioAmbient?.state) {
      await _audioAmbient?.seek(Duration.zero);
      _audioAmbient?.resume();
    }
  }

According to the audioplayers docs, this seems to be a known issue:

The Getting Started docs state:

Note: there are caveats when looping audio without gaps. Depending on the file format and platform, when audioplayers uses the native implementation of the "looping" feature, there will be gaps between plays, witch might not be noticeable for non-continuous SFX but will definitely be noticeable for looping songs. Please check out the Gapless Loop section on our Troubleshooting Guide for more details.

Following the link to the [Trouble Shooting Guide] (https://github.com/bluefireteam/audioplayers/blob/main/troubleshooting.md) reveals:

Gapless Looping

Depending on the file format and platform, when audioplayers uses the native implementation of the "looping" feature, there will be gaps between plays, witch might not be noticeable for non-continuous SFX but will definitely be noticeable for looping songs.

TODO(luan): break down alternatives here, low latency mode, audio pool, gapless_audioplayer, ocarina, etc

Interesting is the Depending on the file format and platform, ... part, which sounds as if gap-less loops could somehow be doable. Question is, how?

Any ideas how to get audio to loop flawless (i.e. without gap) are very much appreciated. Thank you.

SePröbläm
  • 5,142
  • 6
  • 31
  • 45
  • Have you tried with any other file formats? – spydon Jan 14 '23 at 20:13
  • @spydon Thanks for your help! Just tried with .opus format, but doesn't loop neither. Both .wav and .opus loop perfectly using just_audio. The .wav file also loops flawless in Unity, making it look as if the files are OK. – SePröbläm Jan 18 '23 at 16:48
  • If you can determine the exsct duration of your audio sample… Just an idea for a workaround: you could create your own “looper” that contains two separate instances of the plugin player and loops by alternating playback between them using precise timers. You would have to also find a way to cue the audio so there is virtually zero latency upon calling “play()” – Nerdy Bunz Jan 18 '23 at 21:21
  • @NerdyBunz Thanks for sharing this! Sounds like it could work... But considering how far the Flutter app is away from the audio hardware (App -> method channel -> native Android plugin implementation -> Android OS -> hardware), I'm getting this intuition its going to cause issues - especially on 4/5 years old, (those days) low end devices. It's this gut feeling that tells me this approach ought to be handled way closer to the hardware... Still, thank you very much for bringing up the idea! – SePröbläm Jan 19 '23 at 07:57

2 Answers2

0

You can pass this parameter

_audioAmbient = AudioPlayer(mode: PlayerMode.LOW_LATENCY);

if not solved u can try something new like https://pub.dev/packages/ocarina

Jarvis098
  • 1,420
  • 1
  • 11
  • 16
  • Thanks for sharing this! `LOW_LATENCY` fixes the loop gap, but introduces another bug: After disposing the `AudioPlayer`, re-instantiating it and reloading the audio asset, it won't play any more. Calling `resume()` simply stays quiet most of the time. Occasionally it works, but it is **very unreliable**. – SePröbläm Jan 18 '23 at 17:10
  • Ocarina is unfortunately not an option because the app needs to run on Windows and MacOS... Thanks anyway for pointing to it though! – SePröbläm Jan 18 '23 at 17:12
0

I fixed it this way: Created 2 players, set asset path to both of them and created the method that starts second player before first is completed

import 'package:just_audio/just_audio.dart' as audio;

audio.AudioPlayer player1 = audio.AudioPlayer();
audio.AudioPlayer player2 = audio.AudioPlayer();
bool isFirstPlayerActive = true;
StreamSubscription? subscription;

  audio.AudioPlayer getActivePlayer() {
    return isFirstPlayerActive ? player1 : player2;
  }

  void setPlayerAsset(String asset) async {
    if (isFirstPlayerActive) {
      await player1.setAsset("assets/$asset");
      await player2.setAsset("assets/$asset");
    } else {
      await player2.setAsset("assets/$asset");
      await player1.setAsset("assets/$asset");
    }
    subscription?.cancel();
    _loopPlayer(getActivePlayer(), isFirstPlayerActive ? player2 : player1);
  }


  audio.AudioPlayer getActivePlayer() {
    return isFirstPlayerActive ? player1 : player2;
  }
  
  void _loopPlayer(audio.AudioPlayer player1, audio.AudioPlayer player2) async {
      Duration? duration = await player1.durationFuture;
      subscription = player1.positionStream.listen((event) {
        if (duration != null &&
            event.inMilliseconds >= duration.inMilliseconds - 110) {
          log('finished');
          player2.play();
          isFirstPlayerActive = !isFirstPlayerActive;
          StreamSubscription? tempSubscription;
          tempSubscription = player1.playerStateStream.listen((event) {
            if (event.processingState == audio.ProcessingState.completed) {
              log('completed');
              player1.seek(const Duration(seconds: 0));
              player1.pause();
              tempSubscription?.cancel();
            }
          });
          subscription?.cancel();
          _loopPlayer(player2, player1);
        }
      });
  
  }
artemniy
  • 1
  • 1