jfversluis / Plugin.Maui.Audio

Plugin.Maui.Audio provides the ability to play audio inside a .NET MAUI application
MIT License
261 stars 46 forks source link

Allow for gapless audio looping (on Android) #44

Open petrdrabek opened 1 year ago

petrdrabek commented 1 year ago

I am unable to achieve gapless audio loop on Android. Used media player have significant gap between loops in playing audio.

Possible solution is add custom "loop manager". In CompletionListener create another player and play from it. (https://stackoverflow.com/questions/26274182/not-able-to-achieve-gapless-audio-looping-so-far-on-android)

rbrettj commented 1 year ago

Just want to second this request. This is the difference between using this for my game, and having to use ExoPlayer in the native platform implementation.

bijington commented 1 year ago

@petrdrabek And @rbrettj what is the reason looping doesn't currently work for you? Do the audio files have a silent gap at the end that you don't want? Does the player somehow add a gap by being slow to loop? Are you able to provide a sample audio file that shows the issue?

petrdrabek commented 1 year ago

@bijington As I said in issue description - the player adds gap between individual plays in a loop. It affects all audio files.

rbrettj commented 1 year ago

As @petrdrabek said, the MediaPlayer on Android always has a small gap when looping an audio file. It has always been like this and will never be fixed. The solution is linked in the original comment and requires setting up a sort of round-robin scenario where a queued MediaPlayer starts as soon as the first ends, and they take turns playing through the file. It's annoying, but otherwise seems to work.

This audio gap is noticeable and a dealbreaker for a game with looping music. It has nothing to do with the type of file, and is an issue with Android's MediaPlayer. If it cannot be addressed, it will mean abandoning this otherwise perfect package and using ExoPlayer instead.

bijington commented 1 year ago

Is it not possible to do something like this?

private IAudioPlayer AudioPlayer;

void Play()
{
    audioPlayer.PlaybackEnded += OnPlaybackEnded;

    audioPlayer.Play();
}

void OnPlaybackEnded(object sender, EventArgs args)
{
    audioPlayer.Play();
}

I've written this on my phone so apologies if it's not syntactically correct

rbrettj commented 1 year ago

@bijington, of course it's possible, but that doesn't solve the issue as it is a problem with the underlying platform architecture, Android's MediaPlayer. I would also argue that needing to do something like that just to get looping behavior would defeat the purpose, as there is already a flag that handles this successfully in the package.

I feel like there is a misunderstanding somewhere. The package works fine, with the exception of the small gap in between the end of an audio track and the beginning of it when looping on Android because of Android's MediaPlayer. The way to fix this is to update the underlying architecture based off of @petrdrabek's original comment.

bijington commented 1 year ago

@rbrettj apologies it seems I hadn't finished writing everything before I hit Comment. If my suggestion works we could conceivably do the same down on the Android layer which should be much more efficient than creating multiple MediaPlayers. I don't have the bandwidth to deal with this right now as I'm deep in dealing with the recording side of things. If one of you could test my suggestion it would be a real help to determining whether it could be the solution.

rbrettj commented 1 year ago

I understand. Unfortunately, your suggestion doesn't work. I actually tried it earlier to make sure I was telling you correctly. To confirm, when trying your suggestion, the gap is still there. There is an issue with how MediaPlayer reports the end of playback that gives it that ~0.5 second gap. As you can see in the stackoverflow link @petrdrabek posted, this is not a new issue with Android and has been around for a long time. It looks like the best way to solve this is documented in that same link.

I agree that having to have two alternating players is inefficient, but Google isn't going to fix an issue that's been around for 10 years now. I understand this is not higher priority, but it is the difference between me using this plugin or using ExoPlayer bindings and just doing it myself. Hope that clarifies a bit.

bijington commented 1 year ago

I really hate issues that have lived for so long like this. I will aim to take a look at this at some point as I am intrigued as to how the solution could work if the MediaPlayer reports that the playback has finished incorrectly. I can't guarantee when I will find time though.

rbrettj commented 1 year ago

I understand. One thing to note is that the proposed solution is actually using the .setNextMediaPlayer() method, which appears to work more efficiently, as opposed to waiting on the playback completed callback.

bijington commented 1 year ago

I understand. One thing to note is that the proposed solution is actually using the .setNextMediaPlayer() method, which appears to work more efficiently, as opposed to waiting on the playback completed callback.

That sounds like this is Googles "fix" then doesn't it. Hmmm ok perhaps we will have to do it. I wonder if it is efficient enough to only create one if Loop is set and therefore others won't experience any difference if they don't wish to loop.

For the record I will have to deal with this as I am building a game too 🙂. Are you able to share what you are working on?

rbrettj commented 1 year ago

Yeah, the best solution would be one that only does that if looping is set to true. Considering the user can change that whenever they want, it might be tricky to do it without breaking something.

I've built an Android prototype using Xamarin, and am in the process of switching over to .NET MAUI for cross-platform support now that Xamarin has gone the way of the dodo. If you're interested, you can find the in-progress prototype on the Google Play store for download if you search for Retaking Sanctuary.

petrdrabek commented 1 year ago

I did tested out that suggested solution is working correctly. There is some hints:

In AudioPlayer constructor after main player is created call PrepareNextPlayer()

        void PrepareNextPlayer()
    {
        nextPlayer = new MediaPlayer();
        nextPlayer.Completion += OnPlaybackEnded;

        AssetFileDescriptor afd = Android.App.Application.Context.Assets?.OpenFd(currentFileName)
            ?? throw new FailedToLoadAudioException("Unable to create AssetFileDescriptor.");

        nextPlayer.SetDataSource(afd.FileDescriptor, afd.StartOffset, afd.Length);

        nextPlayer.Prepare();
        player.SetNextMediaPlayer(nextPlayer);
    }

And in OnPlaybackEnded add this code:

    void OnPlaybackEnded(object? sender, EventArgs e)
    {
        PlaybackEnded?.Invoke(this, e);

    player.Completion -= OnPlaybackEnded;
    player.Release();
    player.Dispose();
    player = nextPlayer;

    PrepareNextPlayer();

        //this improves stability on older devices but has minor performance impact
        // We need to check whether the player is null or not as the user might have dipsosed it in an event handler to PlaybackEnded above.
        if (Android.OS.Build.VERSION.SdkInt < Android.OS.BuildVersionCodes.M)
        {
            player.SeekTo(0);
            player.Stop();
            player.Prepare();
        }
    }

All of this only if Loop is set to true. And dont forget to release both players in Dispose(). Hope this will make implementation of the solution easier.