| /**************************************************************************** |
| ** |
| ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). |
| ** All rights reserved. |
| ** Contact: Nokia Corporation (qt-info@nokia.com) |
| ** |
| ** This file is part of the QtMultimedia module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** GNU Lesser General Public License Usage |
| ** This file may be used under the terms of the GNU Lesser General Public |
| ** License version 2.1 as published by the Free Software Foundation and |
| ** appearing in the file LICENSE.LGPL included in the packaging of this |
| ** file. Please review the following information to ensure the GNU Lesser |
| ** General Public License version 2.1 requirements will be met: |
| ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. |
| ** |
| ** In addition, as a special exception, Nokia gives you certain additional |
| ** rights. These rights are described in the Nokia Qt LGPL Exception |
| ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU General |
| ** Public License version 3.0 as published by the Free Software Foundation |
| ** and appearing in the file LICENSE.GPL included in the packaging of this |
| ** file. Please review the following information to ensure the GNU General |
| ** Public License version 3.0 requirements will be met: |
| ** http://www.gnu.org/copyleft/gpl.html. |
| ** |
| ** Other Usage |
| ** Alternatively, this file may be used in accordance with the terms and |
| ** conditions contained in a signed written agreement between you and Nokia. |
| ** |
| ** |
| ** |
| ** |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qaudiooutput_symbian_p.h" |
| |
| QT_BEGIN_NAMESPACE |
| |
| //----------------------------------------------------------------------------- |
| // Constants |
| //----------------------------------------------------------------------------- |
| |
| const int UnderflowTimerInterval = 50; // ms |
| |
| |
| //----------------------------------------------------------------------------- |
| // Private class |
| //----------------------------------------------------------------------------- |
| |
| SymbianAudioOutputPrivate::SymbianAudioOutputPrivate( |
| QAudioOutputPrivate *audioDevice) |
| : m_audioDevice(audioDevice) |
| { |
| |
| } |
| |
| SymbianAudioOutputPrivate::~SymbianAudioOutputPrivate() |
| { |
| |
| } |
| |
| qint64 SymbianAudioOutputPrivate::readData(char *data, qint64 len) |
| { |
| Q_UNUSED(data) |
| Q_UNUSED(len) |
| return 0; |
| } |
| |
| qint64 SymbianAudioOutputPrivate::writeData(const char *data, qint64 len) |
| { |
| qint64 totalWritten = 0; |
| |
| if (m_audioDevice->state() == QAudio::ActiveState || |
| m_audioDevice->state() == QAudio::IdleState) { |
| |
| while (totalWritten < len) { |
| const qint64 written = m_audioDevice->pushData(data + totalWritten, |
| len - totalWritten); |
| if (written > 0) |
| totalWritten += written; |
| else |
| break; |
| } |
| } |
| |
| return totalWritten; |
| } |
| |
| |
| //----------------------------------------------------------------------------- |
| // Public functions |
| //----------------------------------------------------------------------------- |
| |
| QAudioOutputPrivate::QAudioOutputPrivate(const QByteArray &device, |
| const QAudioFormat &format) |
| : m_device(device) |
| , m_format(format) |
| , m_clientBufferSize(SymbianAudio::DefaultBufferSize) |
| , m_notifyInterval(SymbianAudio::DefaultNotifyInterval) |
| , m_notifyTimer(new QTimer(this)) |
| , m_error(QAudio::NoError) |
| , m_internalState(SymbianAudio::ClosedState) |
| , m_externalState(QAudio::StoppedState) |
| , m_pullMode(false) |
| , m_source(0) |
| , m_devSound(0) |
| , m_devSoundBuffer(0) |
| , m_devSoundBufferSize(0) |
| , m_bytesWritten(0) |
| , m_pushDataReady(false) |
| , m_bytesPadding(0) |
| , m_underflow(false) |
| , m_lastBuffer(false) |
| , m_underflowTimer(new QTimer(this)) |
| , m_samplesPlayed(0) |
| , m_totalSamplesPlayed(0) |
| { |
| qRegisterMetaType<CMMFBuffer *>("CMMFBuffer *"); |
| |
| connect(m_notifyTimer.data(), SIGNAL(timeout()), this, SIGNAL(notify())); |
| |
| m_underflowTimer->setInterval(UnderflowTimerInterval); |
| connect(m_underflowTimer.data(), SIGNAL(timeout()), this, |
| SLOT(underflowTimerExpired())); |
| } |
| |
| QAudioOutputPrivate::~QAudioOutputPrivate() |
| { |
| close(); |
| } |
| |
| QIODevice* QAudioOutputPrivate::start(QIODevice *device) |
| { |
| stop(); |
| |
| if (device) { |
| m_pullMode = true; |
| m_source = device; |
| } |
| |
| open(); |
| |
| if (SymbianAudio::ClosedState != m_internalState) { |
| if (device) { |
| connect(m_source, SIGNAL(readyRead()), this, SLOT(dataReady())); |
| } else { |
| m_source = new SymbianAudioOutputPrivate(this); |
| m_source->open(QIODevice::WriteOnly | QIODevice::Unbuffered); |
| } |
| |
| m_elapsed.restart(); |
| } |
| |
| return m_source; |
| } |
| |
| void QAudioOutputPrivate::stop() |
| { |
| close(); |
| } |
| |
| void QAudioOutputPrivate::reset() |
| { |
| m_totalSamplesPlayed += getSamplesPlayed(); |
| m_devSound->stop(); |
| m_bytesPadding = 0; |
| startPlayback(); |
| } |
| |
| void QAudioOutputPrivate::suspend() |
| { |
| if (SymbianAudio::ActiveState == m_internalState |
| || SymbianAudio::IdleState == m_internalState) { |
| m_notifyTimer->stop(); |
| m_underflowTimer->stop(); |
| const qint64 samplesWritten = SymbianAudio::Utils::bytesToSamples( |
| m_format, m_bytesWritten); |
| const qint64 samplesPlayed = getSamplesPlayed(); |
| m_totalSamplesPlayed += samplesPlayed; |
| m_bytesWritten = 0; |
| const bool paused = m_devSound->pause(); |
| if (paused) { |
| setState(SymbianAudio::SuspendedPausedState); |
| } else { |
| m_devSoundBuffer = 0; |
| // Calculate the amount of data dropped |
| const qint64 paddingSamples = samplesWritten - samplesPlayed; |
| Q_ASSERT(paddingSamples >= 0); |
| m_bytesPadding = SymbianAudio::Utils::samplesToBytes(m_format, |
| paddingSamples); |
| setState(SymbianAudio::SuspendedStoppedState); |
| } |
| } |
| } |
| |
| void QAudioOutputPrivate::resume() |
| { |
| if (QAudio::SuspendedState == m_externalState) { |
| if (SymbianAudio::SuspendedPausedState == m_internalState) |
| m_devSound->resume(); |
| else |
| startPlayback(); |
| } |
| } |
| |
| int QAudioOutputPrivate::bytesFree() const |
| { |
| int result = 0; |
| if (m_devSoundBuffer) { |
| const TDes8 &outputBuffer = m_devSoundBuffer->Data(); |
| result = outputBuffer.MaxLength() - outputBuffer.Length(); |
| } |
| return result; |
| } |
| |
| int QAudioOutputPrivate::periodSize() const |
| { |
| return bufferSize(); |
| } |
| |
| void QAudioOutputPrivate::setBufferSize(int value) |
| { |
| // Note that DevSound does not allow its client to specify the buffer size. |
| // This functionality is available via custom interfaces, but since these |
| // cannot be guaranteed to work across all DevSound implementations, we |
| // do not use them here. |
| // In order to comply with the expected bevahiour of QAudioOutput, we store |
| // the value and return it from bufferSize(), but the underlying DevSound |
| // buffer size remains unchanged. |
| if (value > 0) |
| m_clientBufferSize = value; |
| } |
| |
| int QAudioOutputPrivate::bufferSize() const |
| { |
| return m_devSoundBufferSize ? m_devSoundBufferSize : m_clientBufferSize; |
| } |
| |
| void QAudioOutputPrivate::setNotifyInterval(int ms) |
| { |
| if (ms >= 0) { |
| const int oldNotifyInterval = m_notifyInterval; |
| m_notifyInterval = ms; |
| if (m_notifyInterval && (SymbianAudio::ActiveState == m_internalState || |
| SymbianAudio::IdleState == m_internalState)) |
| m_notifyTimer->start(m_notifyInterval); |
| else |
| m_notifyTimer->stop(); |
| } |
| } |
| |
| int QAudioOutputPrivate::notifyInterval() const |
| { |
| return m_notifyInterval; |
| } |
| |
| qint64 QAudioOutputPrivate::processedUSecs() const |
| { |
| int samplesPlayed = 0; |
| if (m_devSound && QAudio::SuspendedState != m_externalState) |
| samplesPlayed = getSamplesPlayed(); |
| |
| // Protect against division by zero |
| Q_ASSERT_X(m_format.frequency() > 0, Q_FUNC_INFO, "Invalid frequency"); |
| |
| const qint64 result = qint64(1000000) * |
| (samplesPlayed + m_totalSamplesPlayed) |
| / m_format.frequency(); |
| |
| return result; |
| } |
| |
| qint64 QAudioOutputPrivate::elapsedUSecs() const |
| { |
| const qint64 result = (QAudio::StoppedState == state()) ? |
| 0 : m_elapsed.elapsed() * 1000; |
| return result; |
| } |
| |
| QAudio::Error QAudioOutputPrivate::error() const |
| { |
| return m_error; |
| } |
| |
| QAudio::State QAudioOutputPrivate::state() const |
| { |
| return m_externalState; |
| } |
| |
| QAudioFormat QAudioOutputPrivate::format() const |
| { |
| return m_format; |
| } |
| |
| |
| //----------------------------------------------------------------------------- |
| // Private functions |
| //----------------------------------------------------------------------------- |
| |
| void QAudioOutputPrivate::dataReady() |
| { |
| // Client-provided QIODevice has data ready to read. |
| |
| Q_ASSERT_X(m_source->bytesAvailable(), Q_FUNC_INFO, |
| "readyRead signal received, but no data available"); |
| |
| if (!m_bytesPadding) |
| pullData(); |
| } |
| |
| void QAudioOutputPrivate::underflowTimerExpired() |
| { |
| const TInt samplesPlayed = getSamplesPlayed(); |
| if (m_samplesPlayed && (samplesPlayed == m_samplesPlayed)) { |
| setError(QAudio::UnderrunError); |
| } else { |
| m_samplesPlayed = samplesPlayed; |
| m_underflowTimer->start(); |
| } |
| } |
| |
| void QAudioOutputPrivate::devsoundInitializeComplete(int err) |
| { |
| Q_ASSERT_X(SymbianAudio::InitializingState == m_internalState, |
| Q_FUNC_INFO, "Invalid state"); |
| |
| if (!err && m_devSound->isFormatSupported(m_format)) |
| startPlayback(); |
| else |
| setError(QAudio::OpenError); |
| } |
| |
| void QAudioOutputPrivate::devsoundBufferToBeFilled(CMMFBuffer *bufferBase) |
| { |
| // Following receipt of this signal, DevSound should not provide another |
| // buffer until we have returned the current one. |
| Q_ASSERT_X(!m_devSoundBuffer, Q_FUNC_INFO, "Buffer already held"); |
| |
| // Will be returned to DevSoundWrapper by bufferProcessed(). |
| m_devSoundBuffer = static_cast<CMMFDataBuffer*>(bufferBase); |
| |
| if (!m_devSoundBufferSize) |
| m_devSoundBufferSize = m_devSoundBuffer->Data().MaxLength(); |
| |
| writePaddingData(); |
| |
| if (m_pullMode && isDataReady() && !m_bytesPadding) |
| pullData(); |
| } |
| |
| void QAudioOutputPrivate::devsoundPlayError(int err) |
| { |
| switch (err) { |
| case KErrUnderflow: |
| m_underflow = true; |
| if (m_pullMode && !m_lastBuffer) |
| setError(QAudio::UnderrunError); |
| else |
| setState(SymbianAudio::IdleState); |
| break; |
| case KErrOverflow: |
| // Silently consume this error when in playback mode |
| break; |
| default: |
| setError(QAudio::IOError); |
| break; |
| } |
| } |
| |
| void QAudioOutputPrivate::open() |
| { |
| Q_ASSERT_X(SymbianAudio::ClosedState == m_internalState, |
| Q_FUNC_INFO, "DevSound already opened"); |
| |
| Q_ASSERT(!m_devSound); |
| m_devSound = new SymbianAudio::DevSoundWrapper(QAudio::AudioOutput, this); |
| |
| connect(m_devSound, SIGNAL(initializeComplete(int)), |
| this, SLOT(devsoundInitializeComplete(int))); |
| connect(m_devSound, SIGNAL(bufferToBeProcessed(CMMFBuffer *)), |
| this, SLOT(devsoundBufferToBeFilled(CMMFBuffer *))); |
| connect(m_devSound, SIGNAL(processingError(int)), |
| this, SLOT(devsoundPlayError(int))); |
| |
| setState(SymbianAudio::InitializingState); |
| m_devSound->initialize(m_format.codec()); |
| } |
| |
| void QAudioOutputPrivate::startPlayback() |
| { |
| bool ok = m_devSound->setFormat(m_format); |
| if (ok) |
| ok = m_devSound->start(); |
| |
| if (ok) { |
| if (isDataReady()) |
| setState(SymbianAudio::ActiveState); |
| else |
| setState(SymbianAudio::IdleState); |
| |
| if (m_notifyInterval) |
| m_notifyTimer->start(m_notifyInterval); |
| m_underflow = false; |
| |
| Q_ASSERT(m_devSound->samplesProcessed() == 0); |
| |
| writePaddingData(); |
| |
| if (m_pullMode && m_source->bytesAvailable() && !m_bytesPadding) |
| dataReady(); |
| } else { |
| setError(QAudio::OpenError); |
| close(); |
| } |
| } |
| |
| void QAudioOutputPrivate::writePaddingData() |
| { |
| // See comments in suspend() |
| |
| while (m_devSoundBuffer && m_bytesPadding) { |
| if (SymbianAudio::IdleState == m_internalState) |
| setState(SymbianAudio::ActiveState); |
| |
| TDes8 &outputBuffer = m_devSoundBuffer->Data(); |
| const qint64 outputBytes = bytesFree(); |
| const qint64 paddingBytes = outputBytes < m_bytesPadding ? |
| outputBytes : m_bytesPadding; |
| unsigned char *ptr = const_cast<unsigned char*>(outputBuffer.Ptr()); |
| Mem::FillZ(ptr, paddingBytes); |
| outputBuffer.SetLength(outputBuffer.Length() + paddingBytes); |
| Q_ASSERT(m_bytesPadding >= paddingBytes); |
| m_bytesPadding -= paddingBytes; |
| |
| if (m_pullMode && m_source->atEnd()) |
| lastBufferFilled(); |
| if ((paddingBytes == outputBytes) || !m_bytesPadding) |
| bufferFilled(); |
| } |
| } |
| |
| qint64 QAudioOutputPrivate::pushData(const char *data, qint64 len) |
| { |
| // Data has been written to SymbianAudioOutputPrivate |
| |
| Q_ASSERT_X(!m_pullMode, Q_FUNC_INFO, |
| "pushData called when in pull mode"); |
| |
| const unsigned char *const inputPtr = |
| reinterpret_cast<const unsigned char*>(data); |
| qint64 bytesWritten = 0; |
| |
| if (SymbianAudio::IdleState == m_internalState) |
| setState(SymbianAudio::ActiveState); |
| |
| while (m_devSoundBuffer && (bytesWritten < len)) { |
| // writePaddingData() is called from BufferToBeFilled(), so we should |
| // never have any padding data left at this point. |
| Q_ASSERT_X(0 == m_bytesPadding, Q_FUNC_INFO, |
| "Padding bytes remaining in pushData"); |
| |
| TDes8 &outputBuffer = m_devSoundBuffer->Data(); |
| |
| const qint64 outputBytes = bytesFree(); |
| const qint64 inputBytes = len - bytesWritten; |
| const qint64 copyBytes = outputBytes < inputBytes ? |
| outputBytes : inputBytes; |
| |
| outputBuffer.Append(inputPtr + bytesWritten, copyBytes); |
| bytesWritten += copyBytes; |
| |
| bufferFilled(); |
| } |
| |
| m_pushDataReady = (bytesWritten < len); |
| |
| // If DevSound is still initializing (m_internalState == InitializingState), |
| // we cannot transition m_internalState to ActiveState, but we must emit |
| // an (external) state change from IdleState to ActiveState. The following |
| // call triggers this signal. |
| setState(m_internalState); |
| |
| return bytesWritten; |
| } |
| |
| void QAudioOutputPrivate::pullData() |
| { |
| Q_ASSERT_X(m_pullMode, Q_FUNC_INFO, |
| "pullData called when in push mode"); |
| |
| // writePaddingData() is called by BufferToBeFilled() before pullData(), |
| // so we should never have any padding data left at this point. |
| Q_ASSERT_X(0 == m_bytesPadding, Q_FUNC_INFO, |
| "Padding bytes remaining in pullData"); |
| |
| qint64 inputBytes = m_source->bytesAvailable(); |
| while (m_devSoundBuffer && inputBytes) { |
| if (SymbianAudio::IdleState == m_internalState) |
| setState(SymbianAudio::ActiveState); |
| |
| TDes8 &outputBuffer = m_devSoundBuffer->Data(); |
| |
| const qint64 outputBytes = bytesFree(); |
| const qint64 copyBytes = outputBytes < inputBytes ? |
| outputBytes : inputBytes; |
| |
| char *outputPtr = (char*)(outputBuffer.Ptr() + outputBuffer.Length()); |
| const qint64 bytesCopied = m_source->read(outputPtr, copyBytes); |
| Q_ASSERT(bytesCopied == copyBytes); |
| outputBuffer.SetLength(outputBuffer.Length() + bytesCopied); |
| inputBytes -= bytesCopied; |
| |
| if (m_source->atEnd()) |
| lastBufferFilled(); |
| else if (copyBytes == outputBytes) |
| bufferFilled(); |
| } |
| } |
| |
| void QAudioOutputPrivate::bufferFilled() |
| { |
| Q_ASSERT_X(m_devSoundBuffer, Q_FUNC_INFO, "No buffer to return"); |
| |
| const TDes8 &outputBuffer = m_devSoundBuffer->Data(); |
| m_bytesWritten += outputBuffer.Length(); |
| |
| m_devSoundBuffer = 0; |
| |
| m_samplesPlayed = getSamplesPlayed(); |
| m_underflowTimer->start(); |
| |
| if (QAudio::UnderrunError == m_error) |
| m_error = QAudio::NoError; |
| |
| m_devSound->bufferProcessed(); |
| } |
| |
| void QAudioOutputPrivate::lastBufferFilled() |
| { |
| Q_ASSERT_X(m_devSoundBuffer, Q_FUNC_INFO, "No buffer to fill"); |
| Q_ASSERT_X(!m_lastBuffer, Q_FUNC_INFO, "Last buffer already sent"); |
| m_lastBuffer = true; |
| m_devSoundBuffer->SetLastBuffer(ETrue); |
| bufferFilled(); |
| } |
| |
| void QAudioOutputPrivate::close() |
| { |
| m_notifyTimer->stop(); |
| m_underflowTimer->stop(); |
| |
| m_error = QAudio::NoError; |
| |
| if (m_devSound) |
| m_devSound->stop(); |
| delete m_devSound; |
| m_devSound = 0; |
| |
| m_devSoundBuffer = 0; |
| m_devSoundBufferSize = 0; |
| |
| if (!m_pullMode) // m_source is owned |
| delete m_source; |
| m_pullMode = false; |
| m_source = 0; |
| |
| m_bytesWritten = 0; |
| m_pushDataReady = false; |
| m_bytesPadding = 0; |
| m_underflow = false; |
| m_lastBuffer = false; |
| m_samplesPlayed = 0; |
| m_totalSamplesPlayed = 0; |
| |
| setState(SymbianAudio::ClosedState); |
| } |
| |
| qint64 QAudioOutputPrivate::getSamplesPlayed() const |
| { |
| qint64 result = 0; |
| if (m_devSound) { |
| const qint64 samplesWritten = SymbianAudio::Utils::bytesToSamples( |
| m_format, m_bytesWritten); |
| |
| if (m_underflow) { |
| result = samplesWritten; |
| } else { |
| // This is necessary because some DevSound implementations report |
| // that they have played more data than has actually been provided to them |
| // by the client. |
| const qint64 devSoundSamplesPlayed(m_devSound->samplesProcessed()); |
| result = qMin(devSoundSamplesPlayed, samplesWritten); |
| } |
| } |
| return result; |
| } |
| |
| void QAudioOutputPrivate::setError(QAudio::Error error) |
| { |
| m_error = error; |
| |
| // Although no state transition actually occurs here, a stateChanged event |
| // must be emitted to inform the client that the call to start() was |
| // unsuccessful. |
| if (QAudio::OpenError == error) { |
| emit stateChanged(QAudio::StoppedState); |
| } else { |
| if (QAudio::UnderrunError == error) |
| setState(SymbianAudio::IdleState); |
| else |
| // Close the DevSound instance. This causes a transition to |
| // StoppedState. This must be done asynchronously in case the |
| // current function was called from a DevSound event handler, in which |
| // case deleting the DevSound instance may cause an exception. |
| QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection); |
| } |
| } |
| |
| void QAudioOutputPrivate::setState(SymbianAudio::State newInternalState) |
| { |
| const QAudio::State oldExternalState = m_externalState; |
| m_internalState = newInternalState; |
| m_externalState = SymbianAudio::Utils::stateNativeToQt(m_internalState); |
| |
| if (m_externalState != oldExternalState) |
| emit stateChanged(m_externalState); |
| } |
| |
| bool QAudioOutputPrivate::isDataReady() const |
| { |
| return (m_source && m_source->bytesAvailable()) |
| || m_bytesPadding |
| || m_pushDataReady; |
| } |
| |
| QT_END_NAMESPACE |