Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Sound played with a small delay on Android #184

Open
ThomasEcalle opened this issue Feb 18, 2025 · 3 comments
Open

fix: Sound played with a small delay on Android #184

ThomasEcalle opened this issue Feb 18, 2025 · 3 comments
Labels
help wanted Extra attention is needed

Comments

@ThomasEcalle
Copy link

Description

Hi!

First of all, thank you a lot for your package and your time!

I faced an issue using it in one of my apps and I am note sure if it is a package issue or if what I want to achieve may be impossible (oar mayybe a skill issue of mine 😅)

What I am trying to build is a sport app that gives the user the possibility to do stepper with a certain cadency.
During the exercice, I show the user a stepper animation that evolves accordingly with a sound.

What I have found is that I do not succeed in playing a sound in parallel with the animation.
Even if I await the sound, it seems that the line just after is called before the sound is really played, causing a mismatch between sound and image.

I have built a small example that reproduces the same issue.
In this example, I try to change a color each time the sound is played.
Even if we call setState after the sound is played, the color is shown before the sound.

Please note that it seems to appear only on Android, as I have note succeed to reproduce it on iOS, which works fine.
Also, I have a Pixel 9, I don't know if it could be related to this issue but I found it curious.

Steps To Reproduce

Here is the smallest code I have built to show the issue:

import 'package:flutter/material.dart';
import 'package:flutter_soloud/flutter_soloud.dart';

void main() {
  runApp(const MaterialApp(home: MyApp()));
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int _counter = 0;
  AudioSource? _soundSource;

  @override
  void initState() {
    super.initState();
    _initialize();
  }

  void _initialize() async {
    final soLoud = SoLoud.instance;
    await soLoud.init(
      bufferSize: 512,
      channels: Channels.mono,
    );

    _soundSource = await soLoud.loadAsset('assets/sound.mp3');
    await soLoud.play(_soundSource!, paused: true);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              Container(
                height: 200,
                color: _counter % 2 == 0 ? Colors.blue : Colors.transparent,
                child: Text('Counter: $_counter'),
              ),
              Container(
                height: 200,
                color: _counter % 2 == 1 ? Colors.blue : Colors.transparent,
                child: Text('Counter: $_counter'),
              ),
            ],
          ),
          ElevatedButton(
            child: const Text('Play sound'),
            onPressed: () => _onTap(context),
          ),
        ],
      ),
    );
  }

  void _onTap(BuildContext context) async {
    if (_soundSource == null) return;
    await SoLoud.instance.play(_soundSource!);
   // setState will be called before the sound is played
    setState(() {
      _counter++;
    });
  }
}

Expected Behavior

I would like to be able to show something (in the example the color change, and in my app the stepper animation) during the sound.
We should see the blue color change at the same time.

Additional Context

This is the first time I develop an app with that necessity to be able to control the sound as finely.

Here I assume that I can just “wait” for the future of the “play” method. I'm assuming that the future only ends once the sound has actually been played.

But maybe that's not how it works?
Maybe we'll never really know when the sound has been played?

@ThomasEcalle ThomasEcalle added the bug Something isn't working label Feb 18, 2025
@alnitak
Copy link
Owner

alnitak commented Feb 18, 2025

Hi @ThomasEcalle and thanks for the detailed description.

Your approach seems correct to me.

I don't think this issue is related to the one you mentioned. The other one is caused by the long time the app has been put in background.

I tried your code without noticing much lag. I have added a StopWatch to see how much it takes since the play is called and it prints from 1 to 4 milliseconds delay. I tried on a Samsung Galaxy S22. Could you please try to see how much it takes on your Pixel?

Also, I have tried with this tic sound which has maybe 1 ms of silence before the "tic" sound. Could you also check, maybe with Audacity, if your sound starts with some silence?

here the code with the StopWatch
import 'package:flutter/material.dart';
import 'package:flutter_soloud/flutter_soloud.dart';

void main() {
  runApp(const MaterialApp(home: MyApp()));
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int _counter = 0;
  AudioSource? _soundSource;
  Stopwatch stopwatch = Stopwatch();

  @override
  void initState() {
    super.initState();
    _initialize();
  }

  void _initialize() async {
    final soLoud = SoLoud.instance;
    await soLoud.init(
      bufferSize: 512,
      channels: Channels.mono,
    );

    _soundSource = await soLoud.loadAsset('assets/audio/tic-2.wav');
    await soLoud.play(_soundSource!, paused: true);
  }

  @override
  Widget build(BuildContext context) {
    print("stopwatch.elapsedMilliseconds: ${stopwatch.elapsedMilliseconds}");
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              Container(
                height: 200,
                color: _counter % 2 == 0 ? Colors.blue : Colors.transparent,
                child: Text('Counter: $_counter'),
              ),
              Container(
                height: 200,
                color: _counter % 2 == 1 ? Colors.blue : Colors.transparent,
                child: Text('Counter: $_counter'),
              ),
            ],
          ),
          ElevatedButton(
            child: const Text('Play sound'),
            onPressed: () => _onTap(context),
          ),
        ],
      ),
    );
  }

  void _onTap(BuildContext context) async {
    if (_soundSource == null) return;
    stopwatch.reset();
    stopwatch.start();
    await SoLoud.instance.play(_soundSource!);
    // setState will be called before the sound is played
    setState(() {
      _counter++;
    });
  }
}

Here I assume that I can just “wait” for the future of the “play” method. I'm assuming that the future only ends once the sound has actually been played.

yes, the sound has been started when the code returns from the await play method.

@ThomasEcalle
Copy link
Author

ThomasEcalle commented Feb 19, 2025

Hi @alnitak and thank you a LOT for your quick answer and your time, really appreciate.

I think I have found the issue!

Actually I wanted to make a video to show you the issue, but it did not work.. because there was no delay and the app worked again.

One hour later, I tried again, and the delay was back again.
Some times after, I tried, and the delay was gone...

I was being crazy because the behavior changed often with absolutely no changes in code.

I feel really dumb but the issue was... my bluetooth headphones.

I finally realized that the common thread running through all my attempts, successful or not, was actually the fact that I was on speakerphone on my phone or with bluetooth-connected headphones.

The logical explanation must surely be that the sound is played on the phone when the Future finishes, but then there's a short delay between the moment the sound is played by the phone and the moment the headphones receive it, due to bluetooth latency.

Now that I've found the reason, I'm able to reproduce 100% of the time the sound without delay on the loudspeaker and with delay on my headphones.

Do you have any wireless headphones at home that you could test and see if you have the same problem?

For the record:

  • I haven't tried wired headphones.
  • I haven't tried bluetooth headphones yet (maybe the delay also depends on the quality of the headphones / their sound transfer speed).

I really don't know much about sound, could I have guessed that this is a common problem?

Do sound utilities like flutter_soloud have the ability to know when the sound is received by the target device (headphones) or only when it's played by the phone?

If this is the problem, does this mean that it's impossible to develop an app that requires a perfect match between sound and image with bluetooth headphones?

Anyway, I find this a very interesting problem and I'm really glad to have found a “logical” reason for these differences in behavior despite the unchanged code.

But maybe I'm a bit of an idiot because I should have known better 😅, but at least I hope it will be useful to others.

@alnitak
Copy link
Owner

alnitak commented Feb 19, 2025

Oh well, I hadn't run into this problem yet!

I tried some BT headphones (Pixel Buds) and I have the same problem as you. With the headphones connected via cable there is no latency.

I did some research and this is a very common problem. Especially for video player applications.

The problem is that you can't recover a return timestamp and therefore a latency. Somehow ExoPlayer and Youtube solve it but I don't understand how. Also many video players have the ability to manually adjust the audio delay, maybe you could think of a solution like this for now.

I think you should investigate more deeply or ask the authors of some BT plugins if they have more information.

But I thought that even knowing how much the latency is and adjusting the color change of the box in the example you gave me, you would still notice a latency when you press the button! Which could even be 150~300 ms.

I'm leaving this open in the hopes that someone else might have some more thoughts.

update: best discussion I found regarding this issue here

@alnitak alnitak added help wanted Extra attention is needed and removed bug Something isn't working labels Feb 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants