Source code

Revision control

Copy as Markdown

// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "audiooutput.h"
#include <QVBoxLayout>
namespace {
// IIR filter with poles on the unit circle to generate a sine wave
struct SineOscillator
{
SineOscillator(float frequency, float sampleRate)
{
float omega = 2.0 * M_PI * frequency / sampleRate;
b1 = 2.0f * qCos(omega);
float initialPhase = 0.0f;
y1 = qSin(initialPhase - omega);
y2 = qSin(initialPhase - 2.0f * omega);
}
float nextSample()
{
double y0 = b1 * y1 - y2;
y2 = y1;
y1 = y0;
return float(std::clamp(y0, -1.0, 1.0));
}
private:
double b1{};
double y1{};
double y2{};
};
uint8_t asUint8(float normalizedFloat)
{
return quint8((1.0 + normalizedFloat) / 2 * 255);
}
uint16_t asInt16(float normalizedFloat)
{
return qint16(normalizedFloat * 32767);
}
uint32_t asInt32(float normalizedFloat)
{
return qint32(normalizedFloat * double(std::numeric_limits<qint32>::max()));
}
QSpan<char> writeFromSampleValue(QSpan<char> buffer, float value, QAudioFormat::SampleFormat format)
{
switch (format) {
case QAudioFormat::UInt8: {
*reinterpret_cast<quint8 *>(buffer.data()) = asUint8(value);
return buffer.subspan(sizeof(quint8));
}
case QAudioFormat::Int16: {
*reinterpret_cast<qint16 *>(buffer.data()) = asInt16(value);
return buffer.subspan(sizeof(qint16));
}
case QAudioFormat::Int32: {
*reinterpret_cast<qint32 *>(buffer.data()) = asInt32(value);
return buffer.subspan(sizeof(qint32));
}
case QAudioFormat::Float: {
*reinterpret_cast<float *>(buffer.data()) = value;
return buffer.subspan(sizeof(float));
}
default:
Q_UNREACHABLE_RETURN(buffer);
}
}
} // namespace
Generator::Generator(const QAudioFormat &format, qint64 durationUs, int frequency)
{
if (format.isValid())
generateData(format, durationUs, frequency);
}
void Generator::start()
{
open(QIODevice::ReadOnly);
}
void Generator::stop()
{
m_pos = 0;
close();
}
void Generator::generateData(const QAudioFormat &format, qint64 durationUs, int frequency)
{
qint64 bytes = format.bytesForDuration(durationUs);
m_buffer.resize(bytes);
QSpan<char> buffer(m_buffer.data(), m_buffer.size());
auto osc = SineOscillator(frequency, format.sampleRate());
while (!buffer.empty()) {
const float sampleValue = osc.nextSample(); // Produces value (-1..1)
for (int i = 0; i < format.channelCount(); ++i)
buffer = writeFromSampleValue(buffer, sampleValue, format.sampleFormat());
}
}
qint64 Generator::readData(char *data, qint64 len)
{
qint64 total = 0;
if (!m_buffer.isEmpty()) {
while (len - total > 0) {
const qint64 chunk = qMin((m_buffer.size() - m_pos), len - total);
memcpy(data + total, m_buffer.constData() + m_pos, chunk);
m_pos = (m_pos + chunk) % m_buffer.size();
total += chunk;
}
}
return total;
}
qint64 Generator::writeData([[maybe_unused]] const char *data, [[maybe_unused]] qint64 len)
{
return 0;
}
qint64 Generator::bytesAvailable() const
{
return m_buffer.size() + QIODevice::bytesAvailable();
}
using namespace Qt::Literals::StringLiterals;
static QString sampleFormatToString(QAudioFormat::SampleFormat f)
{
switch (f) {
case QAudioFormat::UInt8: return u"UInt8"_s;
case QAudioFormat::Int16: return u"Int16"_s;
case QAudioFormat::Int32: return u"Int32"_s;
case QAudioFormat::Float: return u"Float"_s;
default: return u"Unknown"_s;
}
}
static constexpr std::array allSupportedSampleRates{
8'000, 11'025, 12'000, 16'000, 22'050, 24'000, 32'000, 44'100,
48'000, 64'000, 88'200, 96'000, 128'000, 176'400, 192'000,
};
template <typename T>
static void setCurrentValue(QComboBox *box, const T &value)
{
int idx = box->findData(QVariant::fromValue(value));
if (idx >= 0)
box->setCurrentIndex(idx);
}
static void syncFormatGui(QComboBox *m_formatBox, QComboBox *m_channelsBox, QComboBox *m_rateBox,
const QAudioFormat &format)
{
setCurrentValue(m_formatBox, format.sampleFormat());
setCurrentValue(m_rateBox, format.sampleRate());
setCurrentValue(m_channelsBox, format.channelCount());
}
AudioTest::AudioTest() : m_devices(new QMediaDevices(this)), m_pushTimer(new QTimer(this))
{
initializeWindow();
// deviceChanged will kickstart the QAudioSink
deviceChanged(m_deviceBox->currentIndex());
}
AudioTest::~AudioTest()
{
m_pushTimer->stop();
cleanupAudioSink();
}
void AudioTest::initializeWindow()
{
QWidget *window = new QWidget;
QVBoxLayout *layout = new QVBoxLayout;
m_deviceBox = new QComboBox(this);
QAudioDevice defaultDevice = QMediaDevices::defaultAudioOutput();
for (auto &deviceInfo : QMediaDevices::audioOutputs())
m_deviceBox->addItem(deviceInfo.description(), QVariant::fromValue(deviceInfo));
auto defaultDeviceIndex = m_deviceBox->findData(QVariant::fromValue(defaultDevice));
m_deviceBox->setCurrentIndex(defaultDeviceIndex);
connect(m_deviceBox, &QComboBox::currentIndexChanged, this, &AudioTest::deviceChanged);
connect(m_devices, &QMediaDevices::audioOutputsChanged, this, &AudioTest::updateAudioDevices);
layout->addWidget(m_deviceBox);
m_modeBox = new QComboBox(this);
m_modeBox->addItem(tr("Pull Mode"));
m_modeBox->addItem(tr("Push Mode"));
m_modeBox->addItem(tr("Callback Mode"));
connect(m_modeBox, &QComboBox::currentIndexChanged, this, [this](int index) {
m_mode = AudioTestMode{ index };
restartAudioStream();
});
layout->addWidget(m_modeBox);
m_modeBox->setCurrentIndex(qToUnderlying(m_mode));
m_suspendResumeButton = new QPushButton(this);
connect(m_suspendResumeButton, &QPushButton::clicked, this, &AudioTest::toggleSuspendResume);
layout->addWidget(m_suspendResumeButton);
QHBoxLayout *volumeBox = new QHBoxLayout;
m_volumeLabel = new QLabel;
m_volumeLabel->setText(tr("Volume:"));
m_volumeSlider = new QSlider(Qt::Horizontal);
m_volumeSlider->setMinimum(0);
m_volumeSlider->setMaximum(100);
m_volumeSlider->setSingleStep(10);
connect(m_volumeSlider, &QSlider::valueChanged, this, &AudioTest::volumeChanged);
volumeBox->addWidget(m_volumeLabel);
volumeBox->addWidget(m_volumeSlider);
layout->addLayout(volumeBox);
// Sample Format selector
QHBoxLayout *formatBox = new QHBoxLayout;
QLabel *formatLabel = new QLabel;
formatLabel->setText(tr("Sample Format:"));
m_formatBox = new QComboBox(this);
//Sample rate button
QLabel *rateLabel = new QLabel;
rateLabel->setText(tr("Sample Rate:"));
m_rateBox = new QComboBox(this);
// setting channel count
QLabel *chLabel = new QLabel;
chLabel->setText(tr("Channels:"));
m_channelsBox = new QComboBox(this);
for (auto *box : { m_channelsBox, m_rateBox, m_formatBox }) {
connect(box, &QComboBox::activated, this, [this, box]() {
formatChanged(box);
});
}
// add all to the same row
formatBox->addWidget(formatLabel);
formatBox->addWidget(m_formatBox);
formatBox->addSpacing(12);
formatBox->addWidget(rateLabel);
formatBox->addWidget(m_rateBox);
formatBox->addSpacing(12);
formatBox->addWidget(chLabel);
formatBox->addWidget(m_channelsBox);
layout->addLayout(formatBox);
// Native period frame count selector
QHBoxLayout *periodBox = new QHBoxLayout;
QLabel *periodLabel = new QLabel(tr("Period Size (in frames):"));
m_periodBox = new QComboBox(this);
m_periodBox->addItem(tr("Default"), -1);
for (int v = 32; v <= 4096; v *= 2)
m_periodBox->addItem(QString::number(v), v);
connect(m_periodBox, &QComboBox::activated, this, [this]() {
restartAudioStream();
});
periodBox->addWidget(periodLabel);
periodBox->addWidget(m_periodBox);
layout->addLayout(periodBox);
window->setLayout(layout);
setCentralWidget(window);
window->show();
}
void AudioTest::startAudioSink(const QAudioDevice &device, const QAudioFormat &format)
{
if (m_audioSink)
cleanupAudioSink();
m_audioSink = std::make_unique<QAudioSink>(device, format);
m_audioSink->setVolume(0.25f); // roughly -12dB
m_audioSink->setNativePeriodFrameCount(m_periodBox->currentData().toInt());
m_currentDevice = device;
syncFormatGui(m_formatBox, m_channelsBox, m_rateBox, m_audioSink->format());
// handle startup/runtime errors and success negotiation
connect(m_audioSink.get(), &QAudioSink::stateChanged, this, [this, device](QAudio::State s) {
switch (s) {
case QAudio::ActiveState:
m_suspendResumeButton->setText(tr("Suspend playback"));
return;
case QAudio::SuspendedState:
m_suspendResumeButton->setText(tr("Resume playback"));
return;
default:
break;
}
const auto err = m_audioSink->error();
// startup failure (format rejected or device unavailable)
if (err == QAudio::OpenError && s == QAudio::StoppedState) {
QMessageBox::warning(this, tr("Audio start failed"),
tr("Device rejected the format or is unavailable."));
return;
}
// runtime I/O or fatal device error (disconnects, etc.)
if (err == QAudio::IOError || err == QAudio::FatalError) {
if (m_currentDevice == device) {
m_currentDevice = {};
m_deviceBox->setCurrentIndex(-1);
}
QMessageBox::warning(this, tr("Audio error"), tr("Audio device error."));
return;
}
});
// set initial volume and kick the stream
qreal initialVolume = QAudio::convertVolume(m_audioSink->volume(),
QAudio::LinearVolumeScale,
QAudio::LogarithmicVolumeScale);
m_volumeSlider->setValue(qRound(initialVolume * 100));
restartAudioStream();
}
void AudioTest::deviceChanged(int index)
{
QAudioDevice dev = m_deviceBox->itemData(index).value<QAudioDevice>();
// formats
m_formatBox->clear();
m_channelsBox->clear();
m_rateBox->clear();
if (!dev.isNull()) {
const auto formats = dev.supportedSampleFormats();
for (const QAudioFormat::SampleFormat sf : formats)
m_formatBox->addItem(sampleFormatToString(sf), QVariant::fromValue(sf));
// channels
for (int ch = dev.minimumChannelCount(); ch <= dev.maximumChannelCount(); ++ch)
m_channelsBox->addItem(QString::number(ch), ch);
// populate from the hardcoded list in this cpp file
for (int rate : allSupportedSampleRates) {
if (rate < dev.minimumSampleRate() || rate > dev.maximumSampleRate())
continue;
m_rateBox->addItem(QString::number(rate), rate);
}
}
if (dev != m_currentDevice) {
cleanupAudioSink();
if (!dev.isNull()) {
QAudioFormat format = dev.preferredFormat();
startAudioSink(dev, format);
}
}
}
void AudioTest::volumeChanged(int value)
{
qreal linearVolume = QAudio::convertVolume(value / qreal(100),
QAudio::LogarithmicVolumeScale,
QAudio::LinearVolumeScale);
m_audioSink->setVolume(linearVolume);
}
void AudioTest::formatChanged(QComboBox *box)
{
QAudioDevice device = m_deviceBox->currentData().value<QAudioDevice>();
QAudioFormat newFormat = m_audioSink->format();
if (box == m_formatBox) {
newFormat.setSampleFormat(
static_cast<QAudioFormat::SampleFormat>(box->currentData().toInt()));
} else if (box == m_rateBox) {
newFormat.setSampleRate(box->currentData().toInt());
} else if (box == m_channelsBox) {
newFormat.setChannelCount(box->currentData().toInt());
}
startAudioSink(device, newFormat);
}
void AudioTest::updateAudioDevices()
{
QSignalBlocker blockUpdates(m_deviceBox);
m_deviceBox->clear();
const QList<QAudioDevice> devices = QMediaDevices::audioOutputs();
for (const QAudioDevice &deviceInfo : devices)
m_deviceBox->addItem(deviceInfo.description(), QVariant::fromValue(deviceInfo));
const int currentDeviceIndex = m_deviceBox->findData(QVariant::fromValue(m_currentDevice));
if (currentDeviceIndex != -1) {
// select previous device
m_deviceBox->setCurrentIndex(currentDeviceIndex);
} else {
blockUpdates.unblock();
// select default device
QAudioDevice defaultDevice = QMediaDevices::defaultAudioOutput();
const int defaultDeviceIndex = m_deviceBox->findData(QVariant::fromValue(defaultDevice));
const int currentIndex = m_deviceBox->currentIndex();
m_deviceBox->setCurrentIndex(defaultDeviceIndex);
if (defaultDeviceIndex == currentIndex) {
// device changed, reinitialize audio
deviceChanged(defaultDeviceIndex);
}
}
}
void AudioTest::restartAudioStream()
{
m_pushTimer->stop();
// Reset audiosink
m_audioSink->reset();
m_audioSink->setNativePeriodFrameCount(m_periodBox->currentData().toInt());
m_generator.reset();
qreal initialVolume = QAudio::convertVolume(m_audioSink->volume(), QAudio::LinearVolumeScale,
QAudio::LogarithmicVolumeScale);
m_volumeSlider->setValue(qRound(initialVolume * 100));
switch (m_mode) {
case AudioTestMode::Pull:
case AudioTestMode::Push: {
// rebuild generator and sink with the requested format
const int durationSeconds = 1;
const int toneFrequencyInHz = 600;
m_generator = std::make_unique<Generator>(m_audioSink->format(), durationSeconds * 1000000,
toneFrequencyInHz);
m_generator->start();
break;
}
case AudioTestMode::Callback: {
// we don't use the QIODevice based generator in callback mode, but capture the oscillator
break;
}
}
switch (m_mode) {
case AudioTestMode::Pull: {
m_audioSink->start(m_generator.get());
break;
}
case AudioTestMode::Push: {
// push mode: periodically push to QAudioSink using a timer
auto *io = m_audioSink->start();
m_pushTimer->disconnect();
connect(m_pushTimer, &QTimer::timeout, this, [this, io]() {
if (m_audioSink->state() == QAudio::StoppedState)
return;
int len = m_audioSink->bytesFree();
QByteArray buffer(len, 0);
len = m_generator->read(buffer.data(), len);
if (len)
io->write(buffer.data(), len);
});
m_pushTimer->start(10);
break;
}
case AudioTestMode::Callback: {
auto generator = SineOscillator(600.0f, float(m_audioSink->format().sampleRate()));
int numberOfChannels = m_audioSink->format().channelCount();
switch (m_audioSink->format().sampleFormat()) {
case QAudioFormat::UInt8: {
m_audioSink->start(
[generator, numberOfChannels](QSpan<uint8_t> buffer) mutable -> void {
while (!buffer.isEmpty()) {
float sampleValue = generator.nextSample();
QSpan<uint8_t> currentFrame = buffer.first(numberOfChannels);
Q_ASSERT(currentFrame.size() == numberOfChannels);
std::fill(currentFrame.begin(), currentFrame.end(), asUint8(sampleValue));
buffer = buffer.subspan(numberOfChannels);
}
});
break;
}
case QAudioFormat::Int16: {
m_audioSink->start([generator, numberOfChannels](QSpan<qint16> buffer) mutable -> void {
while (!buffer.isEmpty()) {
float sampleValue = generator.nextSample();
QSpan<qint16> currentFrame = buffer.first(numberOfChannels);
Q_ASSERT(currentFrame.size() == numberOfChannels);
std::fill(currentFrame.begin(), currentFrame.end(), asInt16(sampleValue));
buffer = buffer.subspan(numberOfChannels);
}
});
break;
}
case QAudioFormat::Int32: {
m_audioSink->start([generator, numberOfChannels](QSpan<qint32> buffer) mutable -> void {
while (!buffer.isEmpty()) {
float sampleValue = generator.nextSample();
QSpan<qint32> currentFrame = buffer.first(numberOfChannels);
Q_ASSERT(currentFrame.size() == numberOfChannels);
std::fill(currentFrame.begin(), currentFrame.end(), asInt32(sampleValue));
buffer = buffer.subspan(numberOfChannels);
}
});
break;
}
case QAudioFormat::Float: {
m_audioSink->start([generator, numberOfChannels](QSpan<float> buffer) mutable -> void {
while (!buffer.isEmpty()) {
float sampleValue = generator.nextSample();
QSpan<float> currentFrame = buffer.first(numberOfChannels);
Q_ASSERT(currentFrame.size() == numberOfChannels);
std::fill(currentFrame.begin(), currentFrame.end(), sampleValue);
buffer = buffer.subspan(numberOfChannels);
}
});
break;
}
default:
Q_UNREACHABLE();
}
break;
}
default:
Q_UNREACHABLE();
}
}
void AudioTest::toggleSuspendResume()
{
switch (m_audioSink->state()) {
case QAudio::SuspendedState:
m_audioSink->resume();
return;
case QAudio::ActiveState:
m_audioSink->suspend();
return;
default:
return;
}
}
void AudioTest::cleanupAudioSink()
{
if (m_audioSink) {
m_audioSink->stop();
m_audioSink->disconnect(this);
}
m_audioSink.reset();
m_generator.reset();
m_currentDevice = {};
}
#include "moc_audiooutput.cpp"