Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#### 4.0.10 (20 Jun 2026)
- wait some ms to pause device when there are no more sounds playing #486

#### 4.0.9 (13 Jun 2026)
- Windows: prevent compiler to complain about `min` and `max` macros. Fixes #483

Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ packages:
path: ".."
relative: true
source: path
version: "4.0.7"
version: "4.0.9"
flutter_test:
dependency: "direct dev"
description: flutter
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: >-
A low-level audio plugin for Flutter,
mainly meant for games and immersive apps.
Based on the SoLoud (C++) audio engine.
version: 4.0.9
version: 4.0.10
homepage: https://github.com/alnitak/flutter_soloud
maintainer: Marco Bavagnoli (@alnitak)
platforms:
Expand Down
2 changes: 0 additions & 2 deletions src/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -770,8 +770,6 @@ FFI_PLUGIN_EXPORT enum PlayerErrors play(unsigned int soundHash, unsigned int bu
FFI_PLUGIN_EXPORT void stop(unsigned int handle) {
if (player.get() == nullptr || !player.get()->isInited())
return;
if (!player.get()->isValidHandle(handle))
return;
player.get()->stop(handle);
voiceEndedCallback(&handle);
}
Expand Down
119 changes: 103 additions & 16 deletions src/player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
#include "synth/basic_wave.h"

#include <algorithm>
#include <chrono>
#include <cstdarg>
#include <cstring>
#include <fstream>
#include <random>
#include <thread>

#ifdef _IS_WIN_
#include <stddef.h> // for size_t
Expand Down Expand Up @@ -130,10 +132,17 @@ PlayerErrors loadOggXiphBufferStream(Player *player,
}
}

Player::Player() : mInited(false), mFilters(&soloud, nullptr, nullptr) {}
Player::Player() : mInited(false), mFilters(&soloud, nullptr, nullptr),
mPauseRequested(false), mStopPauseThread(false),
mPauseThreadRunning(false)
{
}

Player::~Player() {
if (!mInited) {
// If the scheduler was started, stop it before touching Soloud.
stopPauseEngineScheduler();

if (!mInited) {
// dispose() was called properly — Soloud is already deinited and safe.
// Let ~Soloud() run normally to free its remaining allocations.
return;
Expand All @@ -160,6 +169,9 @@ void Player::dispose() {
if (!mInited)
return;

// Stop accepting new pause requests and wake the scheduler so it exits.
stopPauseEngineScheduler();

mInited = false;

// Clean up SoLoud
Expand Down Expand Up @@ -218,6 +230,8 @@ PlayerErrors Player::init(unsigned int sampleRate, unsigned int bufferSize, unsi
mSampleRate = sampleRate;
mBufferSize = bufferSize;
mChannels = channels;
// Start the deferred-pause scheduler now that the engine is in use.
startPauseEngineScheduler();
}
else
result = backendNotInited;
Expand Down Expand Up @@ -754,7 +768,91 @@ void Player::setPause(unsigned int handle, bool pause)
// If no voices are active, pause the audio device to allow the OS
// to properly manage the audio session (important for Control Center
// and remote command handling on iOS).
if (soloud.getActiveVoiceCount() == 0)
pauseEngine();
}
}

// On some platforms (notably iOS) the OS can take a short time to fully
// tear down or hand back the audio session after the last active voice is
// stopped/paused. If we pause the SoLoud engine immediately, a subsequent
// play/resume request can arrive while the audio device is still settling,
// which can cause the OS to keep the Control Center / lock-screen media
// controls in an inconsistent state or to fail to restart playback cleanly.
//
// To avoid this, we defer the engine pause by ~500 ms. This gives the audio
// backend and the OS enough time to stabilize, while still pausing the
// engine promptly once no voices remain active. It also coalesces rapid
// stop/pause events so we don't pause/unpause the device repeatedly. The
// latter happens when stopping many sounds in a short time and new sounds
// are then started causing a lag when starting to play again.
//
// Instead of spawning a detached thread for every request, a single
// persistent scheduler thread handles all pause requests.
void Player::pauseEngine()
{
{
std::lock_guard<std::mutex> lock(mPauseMutex);
if (!mPauseThreadRunning)
return;
mPauseRequested = true;
}
mPauseCv.notify_one();
}

void Player::startPauseEngineScheduler()
{
std::lock_guard<std::mutex> lock(mPauseMutex);
if (mPauseThreadRunning)
return;
mStopPauseThread = false;
mPauseRequested = false;
mPauseThread = std::thread(&Player::pauseEngineScheduler, this);
mPauseThreadRunning = true;
}

void Player::stopPauseEngineScheduler()
{
{
std::lock_guard<std::mutex> lock(mPauseMutex);
if (!mPauseThreadRunning)
return;
mStopPauseThread = true;
}
mPauseCv.notify_all();
if (mPauseThread.joinable()) {
mPauseThread.join();
}
{
std::lock_guard<std::mutex> lock(mPauseMutex);
mPauseThreadRunning = false;
}
}

void Player::pauseEngineScheduler()
{
while (!mStopPauseThread)
{
std::unique_lock<std::mutex> lock(mPauseMutex);
mPauseCv.wait(lock, [this] { return mPauseRequested || mStopPauseThread; });
if (mStopPauseThread)
break;

// A request arrived. Reset it and wait for the delay, but wake early
// if another request arrives (coalescing rapid calls).
mPauseRequested = false;
mPauseCv.wait_for(lock, std::chrono::milliseconds(kPauseEngineDelayMs),
[this] { return mPauseRequested || mStopPauseThread; });

if (mStopPauseThread)
break;

// If another request arrived during the wait, loop back and restart
// the delay so the pause happens only after the burst of requests ends.
if (mPauseRequested)
continue;

lock.unlock();
if (mInited && soloud.getActiveVoiceCount() == 0)
{
soloud.pause();
}
Expand Down Expand Up @@ -869,14 +967,10 @@ PlayerErrors Player::play(
void Player::stop(unsigned int handle)
{
soloud.stop(handle);

// After stopping, check if there are any remaining active voices.
// If no voices are active, pause the audio device to allow the OS
// to properly manage the audio session.
if (soloud.getActiveVoiceCount() == 0)
{
soloud.pause();
}
pauseEngine();
}

void Player::removeHandle(unsigned int handle)
Expand Down Expand Up @@ -907,7 +1001,6 @@ void Player::removeHandle(unsigned int handle)
void Player::disposeSound(unsigned int soundHash)
{
std::unique_ptr<ActiveSound> soundToDestroy;
bool shouldPause = false;

{
std::lock_guard<std::recursive_mutex> lock(sounds_mutex);
Expand Down Expand Up @@ -955,19 +1048,13 @@ void Player::disposeSound(unsigned int soundHash)
// Move the sound out of the vector before erasing
soundToDestroy = std::move(*it);
sounds.erase(it);

// Check if we should pause the device after destroying
shouldPause = (soloud.getActiveVoiceCount() == 0);
}
}
// Sound (and its filters) is destroyed here when soundToDestroy goes out of scope

// After disposing a sound, check if there are any remaining active voices.
// If no voices are active, pause the audio device.
if (shouldPause)
{
soloud.pause();
}
pauseEngine();
}

void Player::disposeAllSound()
Expand Down
23 changes: 23 additions & 0 deletions src/player.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "soloud/src/backend/miniaudio/miniaudio.h"

#include <atomic>
#include <condition_variable>
#include <iostream>
#include <map>
#include <memory>
Expand Down Expand Up @@ -201,6 +202,13 @@ class Player {
/// @param pause whether this sound should be paused or not.
void setPause(unsigned int handle, bool pause);

/// @brief Schedule a deferred pause of the audio device. If no voices
/// remain active after a short delay, the engine is paused. Requests are
/// coalesced so that a burst of stop/pause calls results in a single
/// background pause, giving the audio backend and OS time to stabilize the
/// audio session (e.g. Control Center on iOS).
void pauseEngine();

/// @brief Gets the pause state.
/// @param handle the sound handle.
/// @return true if paused.
Expand Down Expand Up @@ -638,6 +646,21 @@ class Player {

std::map<unsigned int, BusData> busMap;
unsigned int busIdCounter = 0;

// Background scheduler for deferred engine pause. Started lazily on
// init() and stopped on dispose() so that the global Player object can
// be recreated by bindings.cpp without leaving a stray background thread.
static constexpr unsigned int kPauseEngineDelayMs = 500;
std::thread mPauseThread;
std::mutex mPauseMutex;
std::condition_variable mPauseCv;
std::atomic<bool> mPauseRequested{false};
std::atomic<bool> mStopPauseThread{false};
bool mPauseThreadRunning = false;

void pauseEngineScheduler();
void startPauseEngineScheduler();
void stopPauseEngineScheduler();
};

#endif // PLAYER_H
2 changes: 1 addition & 1 deletion web/libflutter_soloud_plugin.js

Large diffs are not rendered by default.

Binary file modified web/libflutter_soloud_plugin.wasm
Binary file not shown.
Loading