Play QSoundEffect in QThread

3.9k Views Asked by At

I can not get the QSoundEffect to play in a separate thread. Could you please tell me why is the sound played only by the first code snippet and not by the second?

//main.cpp

#include <QCoreApplication>
#include "SoundThread.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // #1
    QSoundEffect alarmSound;
    alarmSound.setSource(QUrl::fromLocalFile(":/sound"));
    alarmSound.play();

    /* #2
    SoundThread thread;
    thread.start();
    thread.wait();
    */

    return a.exec();
}

and

//SoundThread.h

#ifndef SOUNDTHREAD_H
#define SOUNDTHREAD_H

#include <QThread>
#include <QtMultimedia/QSoundEffect>

class SoundThread : public QThread
{
    Q_OBJECT
    private:
        void run()
        {
            QSoundEffect alarmSound;
            alarmSound.setSource(QUrl::fromLocalFile(":/sound"));
            alarmSound.play();
            while(true){}
        }
};

#endif // SOUNDTHREAD_H
3

There are 3 best solutions below

0
On

Replace while(true){} by QThread::run();, which will launch an internal event loop and will wait (sleep) for events, such as timer events and asynchronous signals calling slots, which is probably what is happening internally in QSoundEffect: When you call QSoundEffect::play() Some event (probably signals/slots) are queued in the event queue from within QThread, but nothing is processing the event queue. Remember: you are overriding virtual void run() and the original implementation was calling QThread::exec() for you. It is always a good idea to always call your super classes virtual function whenever you override them, as long as they are not pure virtual.

    void run()
    {
        QSoundEffect alarmSound;
        alarmSound.setSource(QUrl::fromLocalFile(":/sound"));
        alarmSound.play();
        QThread::run();
    }

Some people suggested that calling QThread::exec() would do the trick. They may be right. I have to check the implementation of QThread to confirm that it is the only thing called in QThread::run() implementation. I personally think (by experience) that it is always safer to call your superclasse's virtual function in case there are other things called (other than QThread::exec() for this particular case).

Another option would be to move your QSoundEffect instance onto the thread and use signals and slots default auto-connection behaviour type to switch threads.

SoundPlayer.h:

#ifndef SOUNDPLAYER_H_
#define SOUNDPLAYER_H_

#include <QObject>
#include <QThread>
#include <QSoundEffect>

class SoundPlayer : public QObject
{
    Q_OBJECT

public:

    SoundPlayer();

signals:

    void play();

private:

    QThread      m_thread;
    QSoundEffect m_alarmSound;

};

#endif

SoundPlayer.cpp :

#include "SoundPlayer.h"

SoundPlayer()
{
    m_alarmSound.setSource(QUrl::fromLocalFile(":/sound"));
    m_alarmSound.moveToThread(&m_thread);
    connect(this, SIGNAL(play()), &m_alarmSound, SLOT(play()));
    m_thread.start(); // QThread::exec() will be called for you, making the thread wait for events
}

And then calling the play() signal would start playing in the correct thread.

SoundPlayer player;
emit player.play(); // m_alarmSound.play() will be called by m_thread
2
On

You enter to an infinite loop at the end of your run function which causes to block the thread and consequently the QSoundEffect not working. It should be like:

    void run()
    {
        QSoundEffect alarmSound;
        alarmSound.setSource(QUrl::fromLocalFile(":/sound"));
        alarmSound.play();
        exec();
    }
2
On

From the Qt documentation on QThread: -

By default, run() starts the event loop by calling exec()

Since you've inherited from QThread, you now have a run function which doesn't call exec(). Therefore, the event loop is not running and is most likely required for playing the sound effect.

Calling exec() should be substituted for the while(true){} as exec() will wait until exit() is called.

Doing it properly, with moving an object to the thread, based on "How to Really Truly Use QThreads..."

class Worker : public QObject 
{
    Q_OBJECT

public:
    Worker();
    ~Worker();

public slots:
    void PlaySoundEffect();

signals:
    void finished();
    void error(QString err);

private:
    // store the sound effect, so we can reuse it multiple times
    QSoundEffect* m_pAlarmSound;

private slots:

};


Worker::Worker() 
{
    m_pAlarmSound = new QSoundEffect;
    m_pAlarmSound.setSource(QUrl::fromLocalFile(":/sound"));       
}

Worker::~Worker() 
{
    delete m_pAlarmSound;
    m_pAlarmSound = nullptr; // C++ 11
}

void Worker::PlaySoundEffect()
{
    m_pAlarmSound->play();
}

// setup the worker and move it to another thread...
MainWindow::MainWindow
{
    QThread* thread = new QThread;
    Worker* worker = new Worker();
    worker->moveToThread(thread);
    connect(worker, SIGNAL(error(QString)), this, SLOT(errorString(QString)));
    connect(thread, SIGNAL(started()), worker, SLOT(PlaySoundEffect()));
    connect(worker, SIGNAL(finished()), thread, SLOT(quit()));
    connect(worker, SIGNAL(finished()), worker, SLOT(deleteLater()));
    connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
    thread->start();

   // We can also connect a signal of an object in the main thread to the PlaySoundEffect slot

   // Assuming MainWindow has declared a signal void Alert();
   connect(this, &MainWindow::Alert, worker, &Worker::PlaySoundEffect);

   // Then play the sound when we want: -

   emit Alert();
}

While this seems like a lot of effort, there are many advantages of doing it this way. If, for example, you have a lot of sound effects, the method of inheriting QThread means that you're creating a thread per sound effect, which isn't ideal.

We could easily extend the above Worker object to hold a list of sound effects and play the one we want, by passing an enum into the PlaySoundEffect slot. As this thread is constantly running, playing sounds will incur less delay; it takes time and resources to create a thread at run-time.