swift
threadedtonepairplayer.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2016 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #include <QTimer>
7 
8 #include "misc/logmessage.h"
9 #include "sound/audioutilities.h"
10 
11 using namespace swift::misc;
12 using namespace swift::misc::audio;
13 using namespace swift::sound;
14 
15 namespace swift::sound
16 {
17  CThreadedTonePairPlayer::CThreadedTonePairPlayer(QObject *owner, const QString &name,
18  const CAudioDeviceInfo &device)
19  : CContinuousWorker(owner, name), m_deviceInfo(device)
20  {}
21 
22  void CThreadedTonePairPlayer::play(int volume, const QList<CTonePair> &tonePairs)
23  {
24  QPointer<CThreadedTonePairPlayer> myself(this);
25  QMutexLocker ml(&m_mutex);
26  if (m_audioOutput->state() != QAudio::StoppedState) { return; }
27 
28  m_bufferData = this->getAudioByTonePairs(tonePairs);
29  m_audioOutput->setVolume(static_cast<qreal>(0.01 * volume));
30  QTimer::singleShot(0, this, [=] {
31  if (myself) { myself->playBuffer(); }
32  });
33  }
34 
36  {
37  if (this->getAudioDevice() == device) { return false; }
38  {
39  QMutexLocker ml(&m_mutex);
40  m_deviceInfo = device;
41  }
42  this->initialize();
43  return true;
44  }
45 
47  {
48  QMutexLocker ml(&m_mutex);
49  return m_deviceInfo;
50  }
51 
53  {
54  QMutexLocker ml(&m_mutex);
55  CLogMessage(this).info(u"CThreadedTonePairPlayer for device '%1'") << m_deviceInfo.getName();
56 
57  QAudioFormat format;
58  format.setSampleRate(44100);
59  format.setChannelCount(1);
60  format.setSampleFormat(QAudioFormat::Int16);
61  static_assert(Q_BYTE_ORDER == Q_LITTLE_ENDIAN);
62 
63  // find best device
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);
68  }
69 
71  {
72  QMutexLocker ml(&m_mutex);
73  CLogMessage(this).info(u"CThreadedTonePairPlayer quit for '%1'") << m_deviceInfo.getName();
74 
75  if (m_audioOutput)
76  {
77  m_audioOutput->stop();
78  m_audioOutput->disconnect();
79  }
80 
81  m_buffer.close();
82  }
83 
84  void CThreadedTonePairPlayer::handleStateChanged(QAudio::State newState)
85  {
86  QMutexLocker ml(&m_mutex);
87  switch (newState)
88  {
89  case QAudio::IdleState: m_audioOutput->stop(); break;
90  default: break;
91  }
92  }
93 
94  void CThreadedTonePairPlayer::playBuffer()
95  {
96  QMutexLocker ml(&m_mutex);
97  if (!m_audioOutput || m_audioOutput->state() == QAudio::ActiveState) { return; }
98  m_buffer.close();
99  m_buffer.setBuffer(&m_bufferData);
100  m_buffer.open(QIODevice::ReadOnly);
101  m_audioOutput->start(&m_buffer);
102  }
103 
104  QByteArray CThreadedTonePairPlayer::getAudioByTonePairs(const QList<CTonePair> &tonePairs)
105  {
106  Q_ASSERT(!tonePairs.isEmpty());
107  QByteArray finalBufferData;
108 
109  for (const auto &tonePair : std::as_const(tonePairs))
110  {
111  if (m_tonePairCache.contains(tonePair))
112  {
113  QByteArray bufferData;
114  bufferData = m_tonePairCache.value(tonePair);
115  finalBufferData.append(bufferData);
116  }
117  else
118  {
119  QByteArray bufferData;
120  bufferData = generateAudioFromTonePairs(tonePair);
121  m_tonePairCache.insert(tonePair, bufferData);
122  finalBufferData.append(bufferData);
123  }
124  }
125  return finalBufferData;
126  }
127 
128  QByteArray CThreadedTonePairPlayer::generateAudioFromTonePairs(const CTonePair &tonePair)
129  {
130  const int bytesPerSample = m_audioFormat.bytesPerSample();
131  const int bytesForAllChannels = m_audioFormat.bytesPerFrame();
132 
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());
138 
139  qint64 last0AmplitudeSample = bytesPerTonePair; // last sample when amplitude was 0
140  int sampleIndexPerTonePair = 0;
141  while (bytesPerTonePair)
142  {
143  // http://hyperphysics.phy-astr.gsu.edu/hbase/audio/sumdif.html
144  // http://math.stackexchange.com/questions/164369/how-do-you-calculate-the-frequency-perceived-by-humans-of-two-sinusoidal-waves-a
145  const double pseudoTime = static_cast<double>(sampleIndexPerTonePair % this->m_audioFormat.sampleRate()) /
146  this->m_audioFormat.sampleRate();
147  double amplitude = 0.0; // amplitude -1 -> +1 , 0 is silence
148  if (tonePair.getFirstFrequencyHz() > 10)
149  {
150  // the combination of two frequencies actually would have 2*amplitude,
151  // but I have to normalize with amplitude -1 -> +1
152  amplitude =
153  tonePair.getSecondFrequencyHz() == 0 ?
154  qSin(2 * M_PI * tonePair.getFirstFrequencyHz() * pseudoTime) :
155  qSin(M_PI * (tonePair.getFirstFrequencyHz() + tonePair.getSecondFrequencyHz()) * pseudoTime) *
156  qCos(M_PI * (tonePair.getFirstFrequencyHz() - tonePair.getSecondFrequencyHz()) *
157  pseudoTime);
158  }
159 
160  // avoid overflow
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)
165  {
166  amplitude = 0;
167  last0AmplitudeSample = bytesPerTonePair;
168  }
169 
170  // generate this for all channels, usually 1 channel
171  for (int i = 0; i < this->m_audioFormat.channelCount(); ++i)
172  {
173  this->writeAmplitudeToBuffer(amplitude, bufferPointer);
174  bufferPointer += bytesPerSample;
175  bytesPerTonePair -= bytesPerSample;
176  }
177  ++sampleIndexPerTonePair;
178  }
179 
180  // fixes the range from the last 0 pass through
181  if (last0AmplitudeSample > 0)
182  {
183  bufferPointer -= last0AmplitudeSample;
184  while (last0AmplitudeSample)
185  {
186  const double amplitude = 0.0; // amplitude -1 -> +1 , 0 is silence
187 
188  // generate this for all channels, usually 1 channel
189  for (int i = 0; i < this->m_audioFormat.channelCount(); ++i)
190  {
191  this->writeAmplitudeToBuffer(amplitude, bufferPointer);
192  bufferPointer += bytesPerSample;
193  last0AmplitudeSample -= bytesPerSample;
194  }
195  }
196  }
197  return bufferData;
198  }
199 
200  void CThreadedTonePairPlayer::writeAmplitudeToBuffer(double amplitude, unsigned char *bufferPointer)
201  {
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);
205 
206 #if Q_BYTE_ORDER == Q_BIG_ENDIAN
207  qToBigEndian<qint16>(value, bufferPointer);
208 #else
209  qToLittleEndian<qint16>(value, bufferPointer);
210 #endif
211  }
212 } // namespace swift::sound
Base class for a long-lived worker object which lives in its own thread.
Definition: worker.h:275
Class for emitting a log message.
Definition: logmessage.h:27
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.
Tone pair to be played.
Definition: tonepair.h:20
int getFirstFrequencyHz() const
Get frequency of the first tone.
Definition: tonepair.h:28
int getSecondFrequencyHz() const
Get frequency of the second tone.
Definition: tonepair.h:31
std::chrono::milliseconds getDurationMs() const
Get play duration.
Definition: tonepair.h:34
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...
Definition: threadutils.h:30