Revision control

++ // 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