Revision control

Copy as Markdown

// Copyright (C) 2016 Research In Motion
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qqnxaudiosink_p.h"
#include <private/qaudiohelpers_p.h>
#include <sys/asoundlib.h>
#include <sys/asound_common.h>
#include <algorithm>
#include <limits>
#pragma GCC diagnostic ignored "-Wvla"
QT_BEGIN_NAMESPACE
QQnxAudioSink::QQnxAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent)
: QPlatformAudioSink(std::move(device), format, parent),
m_source(0),
m_pushSource(false),
m_timer(new QTimer(this)),
m_state(QAudio::StoppedState),
m_suspendedInState(QAudio::SuspendedState),
m_periodSize(0),
m_bytesWritten(0),
m_requestedBufferSize(0),
m_pcmNotifier(0)
{
m_timer->setSingleShot(false);
m_timer->setInterval(20);
connect(m_timer, &QTimer::timeout, this, &QQnxAudioSink::pullData);
const std::optional<snd_pcm_channel_info_t> info =
QnxAudioUtils::pcmChannelInfo(m_audioDevice.id(), QAudioDevice::Output);
if (info)
m_requestedBufferSize = info->max_fragment_size;
}
QQnxAudioSink::~QQnxAudioSink()
{
stop();
}
void QQnxAudioSink::start(QIODevice *source)
{
if (m_state != QAudio::StoppedState)
stop();
m_source = source;
m_pushSource = false;
if (open()) {
changeState(QAudio::ActiveState, QAudio::NoError);
m_timer->start();
} else {
changeState(QAudio::StoppedState, QAudio::OpenError);
}
}
QIODevice *QQnxAudioSink::start()
{
if (m_state != QAudio::StoppedState)
stop();
m_source = new QnxPushIODevice(this);
m_source->open(QIODevice::WriteOnly|QIODevice::Unbuffered);
m_pushSource = true;
if (open()) {
changeState(QAudio::IdleState, QAudio::NoError);
} else {
changeState(QAudio::StoppedState, QAudio::OpenError);
}
return m_source;
}
void QQnxAudioSink::stop()
{
if (m_state == QAudio::StoppedState)
return;
changeState(QAudio::StoppedState, QAudio::NoError);
close();
}
void QQnxAudioSink::reset()
{
if (m_pcmHandle)
#if SND_PCM_VERSION < SND_PROTOCOL_VERSION('P',3,0,2)
snd_pcm_playback_drain(m_pcmHandle.get());
#else
snd_pcm_channel_drain(m_pcmHandle.get(), SND_PCM_CHANNEL_PLAYBACK);
#endif
stop();
}
void QQnxAudioSink::suspend()
{
if (!m_pcmHandle)
return;
snd_pcm_playback_pause(m_pcmHandle.get());
suspendInternal(QAudio::SuspendedState);
}
void QQnxAudioSink::resume()
{
if (!m_pcmHandle)
return;
snd_pcm_playback_resume(m_pcmHandle.get());
resumeInternal();
}
void QQnxAudioSink::setBufferSize(qsizetype bufferSize)
{
m_requestedBufferSize = std::clamp<qsizetype>(bufferSize, 0, std::numeric_limits<int>::max());
}
qsizetype QQnxAudioSink::bufferSize() const
{
const std::optional<snd_pcm_channel_setup_t> setup = m_pcmHandle
? QnxAudioUtils::pcmChannelSetup(m_pcmHandle.get(), QAudioDevice::Output)
: std::nullopt;
return setup ? setup->buf.block.frag_size : m_requestedBufferSize;
}
qsizetype QQnxAudioSink::bytesFree() const
{
if (m_state != QAudio::ActiveState && m_state != QAudio::IdleState)
return 0;
const std::optional<snd_pcm_channel_status_t> status = QnxAudioUtils::pcmChannelStatus(
m_pcmHandle.get(), QAudioDevice::Output);
return status ? status->free : 0;
}
qint64 QQnxAudioSink::processedUSecs() const
{
return qint64(1000000) * m_format.framesForBytes(m_bytesWritten) / m_format.sampleRate();
}
QAudio::State QQnxAudioSink::state() const
{
return m_state;
}
void QQnxAudioSink::updateState()
{
const std::optional<snd_pcm_channel_status_t> status = QnxAudioUtils::pcmChannelStatus(
m_pcmHandle.get(), QAudioDevice::Output);
if (!status)
return;
if (state() == QAudio::ActiveState && status->underrun > 0)
changeState(QAudio::IdleState, QAudio::NoError);
else if (state() == QAudio::IdleState && status->underrun == 0)
changeState(QAudio::ActiveState, QAudio::NoError);
}
void QQnxAudioSink::pullData()
{
if (m_state == QAudio::StoppedState
|| m_state == QAudio::SuspendedState)
return;
const int bytesAvailable = bytesFree();
// skip if we have less than 4ms of data
if (m_format.durationForBytes(bytesAvailable) < 4000)
return;
const int frames = m_format.framesForBytes(bytesAvailable);
// The buffer is placed on the stack so no more than 64K or 1 frame
// whichever is larger.
const int maxFrames = qMax(m_format.framesForBytes(64 * 1024), 1);
const int bytesRequested = m_format.bytesForFrames(qMin(frames, maxFrames));
char buffer[bytesRequested];
const int bytesRead = m_source->read(buffer, bytesRequested);
// reading can take a while and stream may have been stopped
if (!m_pcmHandle)
return;
if (bytesRead > 0) {
const qint64 bytesWritten = write(buffer, bytesRead);
if (bytesWritten <= 0) {
close();
changeState(QAudio::StoppedState, QAudio::FatalError);
} else if (bytesWritten != bytesRead) {
m_source->seek(m_source->pos()-(bytesRead-bytesWritten));
}
} else {
// We're done
if (bytesRead == 0)
changeState(QAudio::IdleState, QAudio::NoError);
else
changeState(QAudio::IdleState, QAudio::IOError);
}
}
bool QQnxAudioSink::open()
{
m_pcmHandle = QnxAudioUtils::openPcmDevice(m_audioDevice.id(), QAudioDevice::Output);
if (!m_pcmHandle)
return false;
int errorCode = 0;
if ((errorCode = snd_pcm_nonblock_mode(m_pcmHandle.get(), 0)) < 0) {
qWarning("QQnxAudioSink: open error, couldn't set non block mode (0x%x)", -errorCode);
close();
return false;
}
addPcmEventFilter();
// Necessary so that bytesFree() which uses the "free" member of the status struct works
snd_pcm_plugin_set_disable(m_pcmHandle.get(), PLUGIN_MMAP);
const std::optional<snd_pcm_channel_info_t> info = QnxAudioUtils::pcmChannelInfo(
m_pcmHandle.get(), QAudioDevice::Output);
if (!info) {
close();
return false;
}
const int fragmentSize = std::clamp(m_requestedBufferSize,
info->min_fragment_size, info->max_fragment_size);
snd_pcm_channel_params_t params = QnxAudioUtils::formatToChannelParams(m_format,
QAudioDevice::Output, fragmentSize);
if ((errorCode = snd_pcm_plugin_params(m_pcmHandle.get(), &params)) < 0) {
qWarning("QQnxAudioSink: open error, couldn't set channel params (0x%x)", -errorCode);
close();
return false;
}
if ((errorCode = snd_pcm_plugin_prepare(m_pcmHandle.get(), SND_PCM_CHANNEL_PLAYBACK)) < 0) {
qWarning("QQnxAudioSink: open error, couldn't prepare channel (0x%x)", -errorCode);
close();
return false;
}
const std::optional<snd_pcm_channel_setup_t> setup = QnxAudioUtils::pcmChannelSetup(
m_pcmHandle.get(), QAudioDevice::Output);
if (!setup) {
close();
return false;
}
m_periodSize = qMin(2048, setup->buf.block.frag_size);
m_bytesWritten = 0;
createPcmNotifiers();
return true;
}
void QQnxAudioSink::close()
{
if (!m_pushSource)
m_timer->stop();
destroyPcmNotifiers();
if (m_pcmHandle) {
#if SND_PCM_VERSION < SND_PROTOCOL_VERSION('P',3,0,2)
snd_pcm_plugin_flush(m_pcmHandle.get(), SND_PCM_CHANNEL_PLAYBACK);
#else
snd_pcm_plugin_drop(m_pcmHandle.get(), SND_PCM_CHANNEL_PLAYBACK);
#endif
m_pcmHandle = nullptr;
}
if (m_pushSource) {
delete m_source;
m_source = 0;
}
}
void QQnxAudioSink::changeState(QAudio::State state, QAudio::Error error)
{
if (m_state != state) {
m_state = state;
Q_EMIT stateChanged(state);
}
setError(error);
}
qint64 QQnxAudioSink::pushData(const char *data, qint64 len)
{
const QAudio::State s = state();
if (s == QAudio::StoppedState || s == QAudio::SuspendedState)
return 0;
if (s == QAudio::IdleState)
changeState(QAudio::ActiveState, QAudio::NoError);
qint64 totalWritten = 0;
int retry = 0;
constexpr int maxRetries = 10;
while (totalWritten < len) {
const int bytesWritten = write(data + totalWritten, len - totalWritten);
if (bytesWritten <= 0) {
++retry;
if (retry >= maxRetries) {
close();
changeState(QAudio::StoppedState, QAudio::FatalError);
return totalWritten;
} else {
continue;
}
}
retry = 0;
totalWritten += bytesWritten;
}
return totalWritten;
}
qint64 QQnxAudioSink::write(const char *data, qint64 len)
{
if (!m_pcmHandle)
return 0;
// Make sure we're writing (N * frame) worth of bytes
const int size = m_format.bytesForFrames(qBound(qint64(0), qint64(bytesFree()), len) / m_format.bytesPerFrame());
if (size == 0)
return 0;
int written = 0;
if (volume() < 1.0f) {
char out[size];
QAudioHelperInternal::qMultiplySamples(volume(), m_format, data, out, size);
written = snd_pcm_plugin_write(m_pcmHandle.get(), out, size);
} else {
written = snd_pcm_plugin_write(m_pcmHandle.get(), data, size);
}
if (written > 0) {
m_bytesWritten += written;
return written;
}
return 0;
}
void QQnxAudioSink::suspendInternal(QAudio::State suspendState)
{
if (!m_pushSource)
m_timer->stop();
m_suspendedInState = m_state;
changeState(suspendState, QAudio::NoError);
}
void QQnxAudioSink::resumeInternal()
{
changeState(m_suspendedInState, QAudio::NoError);
m_timer->start();
}
QAudio::State suspendState(const snd_pcm_event_t &event)
{
Q_ASSERT(event.type == SND_PCM_EVENT_AUDIOMGMT_STATUS);
Q_ASSERT(event.data.audiomgmt_status.new_status == SND_PCM_STATUS_SUSPENDED);
return QAudio::SuspendedState;
}
void QQnxAudioSink::addPcmEventFilter()
{
/* Enable PCM events */
snd_pcm_filter_t filter;
memset(&filter, 0, sizeof(filter));
filter.enable = (1<<SND_PCM_EVENT_AUDIOMGMT_STATUS) |
(1<<SND_PCM_EVENT_AUDIOMGMT_MUTE) |
(1<<SND_PCM_EVENT_OUTPUTCLASS) |
(1<<SND_PCM_EVENT_UNDERRUN);
snd_pcm_set_filter(m_pcmHandle.get(), SND_PCM_CHANNEL_PLAYBACK, &filter);
}
void QQnxAudioSink::createPcmNotifiers()
{
// QSocketNotifier::Read for poll based event dispatcher. Exception for
// select based event dispatcher.
m_pcmNotifier = new QSocketNotifier(snd_pcm_file_descriptor(m_pcmHandle.get(),
SND_PCM_CHANNEL_PLAYBACK),
QSocketNotifier::Read, this);
connect(m_pcmNotifier, &QSocketNotifier::activated,
this, &QQnxAudioSink::pcmNotifierActivated);
}
void QQnxAudioSink::destroyPcmNotifiers()
{
if (m_pcmNotifier) {
delete m_pcmNotifier;
m_pcmNotifier = 0;
}
}
void QQnxAudioSink::pcmNotifierActivated(int socket)
{
Q_UNUSED(socket);
snd_pcm_event_t pcm_event;
memset(&pcm_event, 0, sizeof(pcm_event));
while (snd_pcm_channel_read_event(m_pcmHandle.get(), SND_PCM_CHANNEL_PLAYBACK, &pcm_event) == 0) {
if (pcm_event.type == SND_PCM_EVENT_AUDIOMGMT_STATUS) {
if (pcm_event.data.audiomgmt_status.new_status == SND_PCM_STATUS_SUSPENDED)
suspendInternal(suspendState(pcm_event));
else if (pcm_event.data.audiomgmt_status.new_status == SND_PCM_STATUS_RUNNING)
resumeInternal();
else if (pcm_event.data.audiomgmt_status.new_status == SND_PCM_STATUS_PAUSED)
suspendInternal(QAudio::SuspendedState);
} else if (pcm_event.type == SND_PCM_EVENT_UNDERRUN) {
updateState();
}
}
}
QnxPushIODevice::QnxPushIODevice(QQnxAudioSink *output)
: QIODevice(output),
m_output(output)
{
}
QnxPushIODevice::~QnxPushIODevice()
{
}
qint64 QnxPushIODevice::readData(char *data, qint64 len)
{
Q_UNUSED(data);
Q_UNUSED(len);
return 0;
}
qint64 QnxPushIODevice::writeData(const char *data, qint64 len)
{
return m_output->pushData(data, len);
}
bool QnxPushIODevice::isSequential() const
{
return true;
}
QT_END_NAMESPACE