12 using namespace swift::misc::audio;
13 using namespace swift::sound;
15 namespace swift::sound
17 CThreadedTonePairPlayer::CThreadedTonePairPlayer(QObject *owner,
const QString &name,
24 QPointer<CThreadedTonePairPlayer> myself(
this);
25 QMutexLocker ml(&m_mutex);
26 if (m_audioOutput->state() != QAudio::StoppedState) {
return; }
28 m_bufferData = this->getAudioByTonePairs(tonePairs);
29 m_audioOutput->setVolume(
static_cast<qreal
>(0.01 * volume));
31 if (myself) { myself->playBuffer(); }
39 QMutexLocker ml(&m_mutex);
40 m_deviceInfo = device;
48 QMutexLocker ml(&m_mutex);
54 QMutexLocker ml(&m_mutex);
58 format.setSampleRate(44100);
59 format.setChannelCount(1);
60 format.setSampleFormat(QAudioFormat::Int16);
61 static_assert(Q_BYTE_ORDER == Q_LITTLE_ENDIAN);
64 const QAudioDevice selectedDevice = getHighestCompatibleOutputDevice(m_deviceInfo, format);
65 m_audioFormat = format;
66 m_audioOutput =
new QAudioSink(selectedDevice, m_audioFormat,
this);
67 connect(m_audioOutput, &QAudioSink::stateChanged,
this, &CThreadedTonePairPlayer::handleStateChanged);
72 QMutexLocker ml(&m_mutex);
77 m_audioOutput->stop();
78 m_audioOutput->disconnect();
84 void CThreadedTonePairPlayer::handleStateChanged(QAudio::State newState)
86 QMutexLocker ml(&m_mutex);
89 case QAudio::IdleState: m_audioOutput->stop();
break;
94 void CThreadedTonePairPlayer::playBuffer()
96 QMutexLocker ml(&m_mutex);
97 if (!m_audioOutput || m_audioOutput->state() == QAudio::ActiveState) {
return; }
99 m_buffer.setBuffer(&m_bufferData);
100 m_buffer.open(QIODevice::ReadOnly);
101 m_audioOutput->start(&m_buffer);
104 QByteArray CThreadedTonePairPlayer::getAudioByTonePairs(
const QList<CTonePair> &tonePairs)
106 Q_ASSERT(!tonePairs.isEmpty());
107 QByteArray finalBufferData;
109 for (
const auto &tonePair : std::as_const(tonePairs))
111 if (m_tonePairCache.contains(tonePair))
113 QByteArray bufferData;
114 bufferData = m_tonePairCache.value(tonePair);
115 finalBufferData.append(bufferData);
119 QByteArray bufferData;
120 bufferData = generateAudioFromTonePairs(tonePair);
121 m_tonePairCache.insert(tonePair, bufferData);
122 finalBufferData.append(bufferData);
125 return finalBufferData;
128 QByteArray CThreadedTonePairPlayer::generateAudioFromTonePairs(
const CTonePair &tonePair)
130 const int bytesPerSample = m_audioFormat.bytesPerSample();
131 const int bytesForAllChannels = m_audioFormat.bytesPerFrame();
133 QByteArray bufferData;
134 qint64 bytesPerTonePair =
135 m_audioFormat.sampleRate() * bytesForAllChannels * tonePair.
getDurationMs().count() / 1000;
136 bufferData.resize(
static_cast<int>(bytesPerTonePair));
137 unsigned char *bufferPointer =
reinterpret_cast<unsigned char *
>(bufferData.data());
139 qint64 last0AmplitudeSample = bytesPerTonePair;
140 int sampleIndexPerTonePair = 0;
141 while (bytesPerTonePair)
145 const double pseudoTime =
static_cast<double>(sampleIndexPerTonePair % this->m_audioFormat.sampleRate()) /
146 this->m_audioFormat.sampleRate();
147 double amplitude = 0.0;
155 qSin(M_PI * (tonePair.getFirstFrequencyHz() + tonePair.getSecondFrequencyHz()) * pseudoTime) *
156 qCos(M_PI * (tonePair.getFirstFrequencyHz() - tonePair.getSecondFrequencyHz()) *
161 Q_ASSERT(amplitude <= 1.0 && amplitude >= -1.0);
162 if (amplitude < -1.0) { amplitude = -1.0; }
163 else if (amplitude > 1.0) { amplitude = 1.0; }
164 else if (qAbs(amplitude) < 1.0 / 65535)
167 last0AmplitudeSample = bytesPerTonePair;
171 for (
int i = 0; i < this->m_audioFormat.channelCount(); ++i)
173 this->writeAmplitudeToBuffer(amplitude, bufferPointer);
174 bufferPointer += bytesPerSample;
175 bytesPerTonePair -= bytesPerSample;
177 ++sampleIndexPerTonePair;
181 if (last0AmplitudeSample > 0)
183 bufferPointer -= last0AmplitudeSample;
184 while (last0AmplitudeSample)
186 const double amplitude = 0.0;
189 for (
int i = 0; i < this->m_audioFormat.channelCount(); ++i)
191 this->writeAmplitudeToBuffer(amplitude, bufferPointer);
192 bufferPointer += bytesPerSample;
193 last0AmplitudeSample -= bytesPerSample;
200 void CThreadedTonePairPlayer::writeAmplitudeToBuffer(
double amplitude,
unsigned char *bufferPointer)
202 Q_ASSERT(this->m_audioFormat.sampleFormat() == QAudioFormat::Int16);
203 static_assert(Q_BYTE_ORDER == Q_LITTLE_ENDIAN);
204 const qint16 value =
static_cast<qint16
>(amplitude * 32767);
206 #if Q_BYTE_ORDER == Q_BIG_ENDIAN
207 qToBigEndian<qint16>(value, bufferPointer);
209 qToLittleEndian<qint16>(value, bufferPointer);
Base class for a long-lived worker object which lives in its own thread.
Class for emitting a log message.
Derived & info(const char16_t(&format)[N])
Set the severity to info, providing a format string.
Value object encapsulating information of a audio device.
const QString & getName() const
Get the device name.
swift::misc::audio::CAudioDeviceInfo getAudioDevice() const
Used audio device.
void play(int volume, const QList< swift::sound::CTonePair > &tonePairs)
Play the list of tones. If the player is currently active, this call will be ignored.
bool reinitializeAudio(const swift::misc::audio::CAudioDeviceInfo &device)
Reinitialize audio.
virtual void initialize()
Called when the thread is started.
virtual void beforeQuit() noexcept
Called before quit is called.
int getFirstFrequencyHz() const
Get frequency of the first tone.
int getSecondFrequencyHz() const
Get frequency of the second tone.
std::chrono::milliseconds getDurationMs() const
Get play duration.
Free functions in swift::misc.
auto singleShot(int msec, QObject *target, F &&task)
Starts a single-shot timer which will call a task in the thread of the given object when it times out...