Search code examples
c++audioqthreadqmediaplayer

How to run QSoundEffect or QMediaPlayer in a loop that is in a thread?


The problem is that I am unable to play sound from a thread using QSoundEffect or QMediaPlayer. The only way that I got it to play sound once per program start is when I added:

QEventLoop loop;
loop.exec();

But this approach does not fit my need since I need the sound to be able to play more than once. Infinite loop count not what I need, but when used sound is played repeatedly til I end the program. No errors with neither of my approaches. So, what am I missing or using not the correct way?

By the way QSound works but I can't control loop count and volume with that, so I'm trying to get QSoundEffect or QMediaPlayer to work since they have this ability.

// main.cpp

QThread sound_thread;
QTimer sound_loop_timer;
Sound sound;
QObject::connect(&sound_loop_timer, SIGNAL(timeout()), &sound, SLOT(exec()));
sound_loop_timer.start(500);
sound_loop_timer.moveToThread(&sound_thread);
sound.moveToThread(&sound_thread);
sound_thread.start();
// Sound.h

class Sound : public QObject
{
    Q_OBJECT
public:
    Sound();

private slots:
    void exec();

private:
    void playSound();
    QSoundEffect *sound_effect;

};
// Sound.cpp

Sound::Sound()
{

}

void Sound::exec(){
    //...
    playSound();
}
void Sound::playSound(){
    sound_effect = new QSoundEffect;
    sound_effect->setSource(QUrl("qrc:/sounds/audio/test.wav"));
//    sound_effect->setLoopCount(QSoundEffect::Infinite);
    sound_effect->setVolume(0.9);
    sound_effect->play();
//  QEventLoop loop;
//  loop.exec();

    QMediaPlayer player;// = new QMediaPlayer;
    player.setMedia(QUrl::fromLocalFile("/home/path/audio/test.wav"));
    player.setVolume(50);
    player.play();
    QEventLoop loop;
//  loop.exec();
//  loop.exit();
}


Solution

  • You don't have to handle the event loop by yourself since the default implementation of QThread::run() do it for you.

    I have made a simple example that plays a sound using QSoundEffect in another thread.

    As I was not sure about what exactly you want to do, I assumed the following statements:

    • A SoundHandler will handle the QSoundEffect object.
    • On click on the start button, the sound is played in a separate thread.
    • The sound is played with an infinite loop and is stopped after the timeout of a timer.

    The below code is only meant to show you how to play a sound in a separate thread (what you asked for). If the above specifications does not fit the requirements of your use case, I think you can easily adapt the code to make it suit your needs.

    test.h:

    #ifndef TEST_H
    #define TEST_H
    
    #include <QMainWindow>
    #include <QPushButton>
    #include <QSoundEffect>
    #include <QTimer>
    #include <QThread>
    
    class SoundHandler final : public QSoundEffect
    {
        Q_OBJECT
    
        private:
            QTimer * life_time_handler;
    
        public:
            SoundHandler(const QUrl & sound_path, int life_time_ms, QObject * parent = nullptr);
    
        public slots:
            void playSound();
            void stopSound();
    
        signals:
            void hasFinished();
    };
    
    class TestWindow : public QMainWindow
    {
        Q_OBJECT
    
        protected:
            QPushButton * start_sound_thread;
            QThread sound_thread;
            SoundHandler * sound_effect;
    
        public:
            TestWindow();
            ~TestWindow();
    };
    
    #endif // TEST_H
    

    test.cpp:

    #include "test.h"
    
    #include <QApplication>
    
    SoundHandler::SoundHandler(const QUrl & sound_path, int life_time_ms, QObject * parent) : QSoundEffect(parent)
    {
        setSource(sound_path);
        setVolume(0.5);
        setLoopCount(QSoundEffect::Infinite);
        life_time_handler = new QTimer(this);
        life_time_handler->setInterval(life_time_ms);
        life_time_handler->setSingleShot(true);
    
        connect(life_time_handler, &QTimer::timeout, this, &SoundHandler::stopSound);
    }
    void SoundHandler::playSound()
    {
        life_time_handler->start();
        play();
    }
    void SoundHandler::stopSound()
    {
        stop();
        emit hasFinished();
    }
    
    TestWindow::TestWindow()
    {
        start_sound_thread = new QPushButton("Start");
        this->setCentralWidget(start_sound_thread);
    
        sound_effect = new SoundHandler(QUrl::fromLocalFile("../test/audio/test.wav"), 4000);
        sound_effect->moveToThread(&sound_thread);
        connect(&sound_thread, &QThread::finished, [&](){sound_effect->deleteLater();});
        connect(&sound_thread, &QThread::started, sound_effect, &SoundHandler::playSound);
    
        // Handle the thread termination
        connect(sound_effect, &SoundHandler::hasFinished, [&](){
            sound_thread.quit();
            sound_thread.wait();
        });
    
        // Handle the thread launch
        connect(start_sound_thread, &QPushButton::clicked, [&](){
            sound_thread.start();
            start_sound_thread->setEnabled(false);
        });
    }
    TestWindow::~TestWindow()
    {
        if(sound_thread.isRunning())
        {
            sound_thread.quit();
            sound_thread.wait();
        }
    }
    
    int main(int argc, char ** argv)
    {
        QApplication app(argc, argv);
    
        TestWindow tw;
        tw.show();
    
        return app.exec();
    }
    

    I've tested it and it worked fine.

    Notes:

    • I choosed here to make the SoundHandler a subclass of QSoundEffect for convenience purposes but it is not required (it could have a QSoundEffect object as a member instead of subclassing it).
    • The TestWindow contains only a single QPushButton to launch the sound in a separate thread. Once launched, the button becomes disabled.

    I hope it helps.