Audio: drum track generation / QTimer reliability
-
Hi,
I am trying to play drum groove.s Therefore I created a "PercussionWorker" class object with a m_timer that calls a "timeTickerSlot()" every xx ms (here: 25 ms).
I've set the timerType to be a Qt::PreciseTimer and moved "PercussionWorker" to a QThread with QThread::HighestPriority.Unfortunately, the playback is still uneven, on Android far worse than on Windows (perhaps because Linux/Android doesn't know/respect/use QThread::HighestPriority?) .
Does anyone see, where my problem can be and if there is a possible fix?This is my "PercussionWorker" definition:
#include <QObject> #include <QTimer> #include <QSoundEffect> class PercussionWorker : public QObject { Q_OBJECT public: explicit PercussionWorker(QObject *parent = nullptr); qreal bpm() const; void setBpm(const qreal &bpm); public slots: void timeTickerSlot(); void startPlaying(); void stopPlaying() { m_timer->stop(); } private: void setSoundEffect(QSoundEffect &effect, QString fileName); QTimer* m_timer = nullptr; int m_barUnitsCount = 96; // bar time divided by 96 units. quarte note=24, 8th=12 (binary) / 8th = (8/4) (ternary), 16th=6/(4/2), 32th=3/2 units (binary only) qreal m_bpm = 100; // beats per minute QSoundEffect m_percBassDrum2; QSoundEffect m_percSnare; QSoundEffect m_percHiHat; };
Here's the "PercussionWorker" implementation:
#include "percussionworker.h" #include <math.h> #include <QDebug> PercussionWorker::PercussionWorker(QObject *parent) : QObject(parent) { setSoundEffect(m_percBassDrum2, "kick2.wav"); setSoundEffect(m_percSnare, "snare.wav"); setSoundEffect(m_percHiHat, "hihat.wav"); } void PercussionWorker::timeTickerSlot() { static int fractionsCounter = -1; if (fractionsCounter == m_barUnitsCount-1) { fractionsCounter = 0; } else { fractionsCounter++; } if (fractionsCounter == 90 || fractionsCounter == 0 || fractionsCounter == m_barUnitsCount/2) { m_percBassDrum2.play(); } if (fractionsCounter%(m_barUnitsCount/8)==0) { m_percHiHat.play(); } if (fractionsCounter == m_barUnitsCount/4 || fractionsCounter == m_barUnitsCount*3 /4) { m_percSnare.play(); } } void PercussionWorker::startPlaying() { m_timer = new QTimer(); m_timer->setTimerType(Qt::PreciseTimer); connect(m_timer, &QTimer::timeout, this, &PercussionWorker::timeTickerSlot); setBpm(100); m_timer->start(); }
The object is created in my Controller this way:
PercussionWorker* pw = new PercussionWorker(); connect(this, &Controller::startPlaying, pw, &PercussionWorker::startPlaying,Qt::QueuedConnection); pw->moveToThread(&m_thread); m_thread.start(); m_thread.setPriority(QThread::HighestPriority); emit this->startPlaying();
Any ideas welcome. Perhaps I need to take an entirely different approach?
-
Not all platforms support high-precision timers and furthermore there's the timer's jitter you need to worry about. So for this particular purpose, even though it pains me to say it, you need another way to measure the beat/time.
What they typically do in games (for rendering), for example, is to reverse roles - not the code driving the scene/rendering at specific times, but rather the monitor/vsync/hardware driving the code, so even if frames are dropped (due to high latency) you at least get the timing right.
PS.
Perhaps something like: https://doc.qt.io/qt-5/qaudiooutput.html#notify is what could help you. -
In addition to @kshegunov comments, you will never get timing accurate playback by stringing together play() for different effects. IMHO, the only way to make it work is to create your own "work audio stream" of fixed sample rate and format, then insert the effects into your stream at the right intervals, and do a singular play() to send the stream to the backend.
To merge streams in realtime then you'd need to bypass the Qt audio functions and go right to the soundcard API.
-
@kshegunov, @Kent-Dorfman: I see. Bummer. Thanks for the insight!
@SGaist: Thanks - that looks very promising, I'll definitely give it a try!