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