summaryrefslogtreecommitdiffstats
path: root/pica-client/toneplayer
diff options
context:
space:
mode:
Diffstat (limited to 'pica-client/toneplayer')
-rw-r--r--pica-client/toneplayer/toneplayer.cpp136
-rw-r--r--pica-client/toneplayer/toneplayer.h13
2 files changed, 123 insertions, 26 deletions
diff --git a/pica-client/toneplayer/toneplayer.cpp b/pica-client/toneplayer/toneplayer.cpp
index d40e1e9..298686a 100644
--- a/pica-client/toneplayer/toneplayer.cpp
+++ b/pica-client/toneplayer/toneplayer.cpp
@@ -16,8 +16,10 @@
*/
#include "toneplayer.h"
#include "../audiodevice.h"
+#include "../nativeaudio.h"
#include <QDebug>
+#include <QVector>
#include <cmath>
// FFmpeg headers
@@ -153,6 +155,29 @@ static QString ff_errstr(int err)
return QString::fromLocal8Bit(buf);
}
+void TonePlayer::renderBlock(const Tone &t, double &phase, int nb_samples, int channels, qint16 *out)
+{
+ const double two_pi = 2.0 * M_PI;
+ double phase_inc = 0.0;
+
+ if (t.frequency > 0.0)
+ phase_inc = two_pi * t.frequency / double(m_sampleRate);
+
+ for (int i = 0; i < nb_samples; ++i) {
+ double sampleVal = 0.0;
+ if (t.frequency > 0.0) {
+ sampleVal = sin(phase) * m_volume;
+ phase += phase_inc;
+ if (phase > 1e9 || phase < -1e9) phase = fmod(phase, two_pi);
+ }
+
+ int16_t ival = static_cast<int16_t>(qBound(-1.0, sampleVal, 1.0) * 32767.0);
+ for (int ch = 0; ch < channels; ++ch) {
+ out[i * channels + ch] = ival;
+ }
+ }
+}
+
void TonePlayer::play()
{
if (m_sequence.isEmpty()) {
@@ -160,6 +185,87 @@ void TonePlayer::play()
return;
}
+ const QString driver = AudioDevice::PlatformDriverName(PLAYBACK);
+
+ if (AudioDevice::IsPlatformDriver(driver))
+ playNative(AudioDevice::PlatformApiName(driver));
+ else
+ playFFmpeg(driver);
+}
+
+void TonePlayer::playNative(const QString &api)
+{
+ NativeAudioBackend *backend = NativeAudioBackend::create(api);
+ if (!backend) {
+ QString msg = QString("No '%1' audio backend in this build").arg(api);
+ qWarning() << msg;
+ emit errorOccured(msg);
+ emit finishedPlaying();
+ return;
+ }
+
+ QString err;
+ if (!backend->openPlayback(m_deviceName, m_sampleRate, m_channels, NativeAudioPlain, &err)) {
+ qWarning() << err;
+ emit errorOccured(err);
+ delete backend;
+ emit finishedPlaying();
+ return;
+ }
+
+ // 20 ms a time, so stop() is acted on promptly without the write loop
+ // spinning.
+ const int blockFrames = m_sampleRate / 50;
+ QVector<qint16> block(blockFrames * m_channels);
+ double phase = 0.0;
+
+ for (int idx = 0; idx < m_sequence.size(); ++idx) {
+ if (m_abort.loadRelaxed()) break;
+
+ const Tone &t = m_sequence[idx];
+ if (t.frequency == 0.0 && t.duration_ms == 0)
+ break;
+ if (t.duration_ms <= 0)
+ continue;
+
+ int64_t total_samples = (int64_t)qRound((m_sampleRate * t.duration_ms) / 1000.0);
+ int64_t samples_written = 0;
+
+ while (samples_written < total_samples) {
+ if (m_abort.loadRelaxed()) break;
+
+ int nb_samples = blockFrames;
+ if (nb_samples > (int)(total_samples - samples_written))
+ nb_samples = int(total_samples - samples_written);
+
+ renderBlock(t, phase, nb_samples, m_channels, block.data());
+
+ if (backend->write(block.constData(), nb_samples * m_channels, 200) < 0) {
+ qWarning() << "Native tone playback write failed";
+ samples_written = total_samples;
+ idx = m_sequence.size();
+ break;
+ }
+
+ samples_written += nb_samples;
+ }
+ }
+
+ // Let what is still sitting in the device buffer be heard rather than
+ // being cut off by close(). One buffer's worth is the most it can be.
+ if (!m_abort.loadRelaxed()) {
+ memset(block.data(), 0, block.size() * sizeof(qint16));
+ backend->write(block.constData(), block.size(), 200);
+ }
+
+ backend->close();
+ delete backend;
+
+ emit finishedPlaying();
+}
+
+void TonePlayer::playFFmpeg(const QString &driver)
+{
avdevice_register_all();
#if LIBAVFORMAT_VERSION_MAJOR < 58
av_register_all();
@@ -169,10 +275,10 @@ void TonePlayer::play()
AVFormatContext *oc = nullptr;
const QByteArray deviceUtf8 = m_deviceName.toUtf8();
const char *device = deviceUtf8.constData();
- const QByteArray driverUtf8 = AudioDevice::PlatformDriverName().toUtf8();
- const char *driver = driverUtf8.constData();
+ const QByteArray driverUtf8 = driver.toUtf8();
+ const char *driverName = driverUtf8.constData();
- int ret = avformat_alloc_output_context2(&oc, nullptr, driver, device);
+ int ret = avformat_alloc_output_context2(&oc, nullptr, driverName, device);
if (ret < 0 || !oc) {
QString msg = QString("Could not allocate '%1' output context: %2").arg(driver, ff_errstr(ret));
qWarning() << msg;
@@ -306,7 +412,6 @@ void TonePlayer::play()
int64_t pts = 0;
double phase = 0.0;
- const double two_pi = 2.0 * M_PI;
#if TP_HAVE_CH_LAYOUT
int chs = codecCtx->ch_layout.nb_channels;
@@ -328,11 +433,6 @@ void TonePlayer::play()
int64_t total_samples = (int64_t)qRound((m_sampleRate * t.duration_ms) / 1000.0);
int64_t samples_written = 0;
- double phase_inc = 0.0;
- if (t.frequency > 0.0) {
- phase_inc = two_pi * t.frequency / double(m_sampleRate);
- }
-
while (samples_written < total_samples) {
if (m_abort.loadRelaxed()) break;
@@ -348,23 +448,7 @@ void TonePlayer::play()
}
frame->nb_samples = nb_samples;
- int16_t *out = reinterpret_cast<int16_t*>(frame->data[0]);
-
- for (int i = 0; i < nb_samples; ++i) {
- double sampleVal = 0.0;
- if (t.frequency > 0.0) {
- sampleVal = sin(phase) * m_volume;
- phase += phase_inc;
- if (phase > 1e9 || phase < -1e9) phase = fmod(phase, two_pi);
- } else {
- sampleVal = 0.0;
- }
-
- int16_t ival = static_cast<int16_t>(qBound(-1.0, sampleVal, 1.0) * 32767.0);
- for (int ch = 0; ch < chs; ++ch) {
- out[i * chs + ch] = ival;
- }
- }
+ renderBlock(t, phase, nb_samples, chs, reinterpret_cast<int16_t*>(frame->data[0]));
frame->pts = pts;
pts += frame->nb_samples;
diff --git a/pica-client/toneplayer/toneplayer.h b/pica-client/toneplayer/toneplayer.h
index b3e237d..5c70e15 100644
--- a/pica-client/toneplayer/toneplayer.h
+++ b/pica-client/toneplayer/toneplayer.h
@@ -72,6 +72,19 @@ private:
// Set the sequence, clear any pending stop request and play it.
void playSequence(const QVector<Tone> &sequence);
+ // The two ways of getting samples to the speaker, picked apart by
+ // AudioDevice::PlatformDriverName(PLAYBACK) - see the comment on it.
+ // Tones always ask for a plain stream rather than a call one: a ringtone
+ // is not a call, and on macOS asking for the voice processing would open
+ // the microphone to play it.
+ void playFFmpeg(const QString &driver);
+ void playNative(const QString &api);
+
+ // Fills out with nb_samples frames of a sine at the tone's frequency
+ // (silence when it is zero), carrying phase across calls so consecutive
+ // blocks of one tone do not click at the seam.
+ void renderBlock(const Tone &t, double &phase, int nb_samples, int channels, qint16 *out);
+
QVector<Tone> m_sequence;
QAtomicInt m_abort;
QString m_deviceName;