/****************************************************************************
**
** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** This file is part of the Qt Mobility Components.
**
** $QT_BEGIN_LICENSE:LGPL$
** No Commercial Usage
** This file contains pre-release code and may not be distributed.
** You may use this file in accordance with the terms and conditions
** contained in the Technology Preview License Agreement accompanying
** this package.
**
** GNU Lesser General Public License Usage
** Alternatively, 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.
**
** If you have questions regarding the use of this file, please contact
** Nokia at qt-info@nokia.com.
**
**
**
**
**
**
**
**
** $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)
: m_device(device)
, 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_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)
{
connect(m_notifyTimer.data(), SIGNAL(timeout()), this, SIGNAL(notify()));
//SymbianAudio::Utils::formatQtToNative(m_format, m_nativeFourCC,
// m_nativeFormat);
m_underflowTimer->setInterval(UnderflowTimerInterval);
connect(m_underflowTimer.data(), SIGNAL(timeout()), this,
SLOT(underflowTimerExpired()));
}
QAudioOutputPrivate::~QAudioOutputPrivate()
{
close();
}
void QAudioOutputPrivate::setFormat(const QAudioFormat& fmt)
{
m_format = const_cast<QAudioFormat&>(fmt);
SymbianAudio::Utils::formatQtToNative(m_format, m_nativeFourCC,
m_nativeFormat);
}
void QAudioOutputPrivate::start(QIODevice *device)
{
stop();
// We have to set these before the call to open() because of the
// logic in initializingState()
m_pullMode = true;
m_source = device;
open();
if (SymbianAudio::ClosedState != m_internalState) {
connect(m_source, SIGNAL(readyRead()), this, SLOT(dataReady()));
m_elapsed.restart();
}
}
QIODevice* QAudioOutputPrivate::start()
{
stop();
open();
if (SymbianAudio::ClosedState != m_internalState) {
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_bytesWritten = 0;
// CMMFDevSound::Pause() is not guaranteed to work correctly in all
// implementations, for play-mode DevSound sessions. We therefore
// have to implement suspend() by calling CMMFDevSound::Stop().
// Because this causes buffered data to be dropped, we replace the
// lost data with silence following a call to resume(), in order to
// ensure that processedUSecs() returns the correct value.
m_devSound->Stop();
m_totalSamplesPlayed += samplesPlayed;
// Calculate the amount of data dropped
const qint64 paddingSamples = samplesWritten - samplesPlayed;
m_bytesPadding = SymbianAudio::Utils::samplesToBytes(m_format,
paddingSamples);
setState(SymbianAudio::SuspendedState);
}
}
void QAudioOutputPrivate::resume()
{
if (SymbianAudio::SuspendedState == m_internalState)
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_notifyTimer->isActive() && ms != oldNotifyInterval)
m_notifyTimer->start(m_notifyInterval);
}
}
int QAudioOutputPrivate::notifyInterval() const
{
return m_notifyInterval;
}
qint64 QAudioOutputPrivate::processedUSecs() const
{
int samplesPlayed = 0;
if (m_devSound && SymbianAudio::SuspendedState != m_internalState)
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;
}
//-----------------------------------------------------------------------------
// MDevSoundObserver implementation
//-----------------------------------------------------------------------------
void QAudioOutputPrivate::InitializeComplete(TInt aError)
{
Q_ASSERT_X(SymbianAudio::InitializingState == m_internalState,
Q_FUNC_INFO, "Invalid state");
if (KErrNone == aError)
startPlayback();
}
void QAudioOutputPrivate::ToneFinished(TInt aError)
{
Q_UNUSED(aError)
// This class doesn't use DevSound's tone playback functions, so should
// never receive this callback.
Q_ASSERT_X(false, Q_FUNC_INFO, "Unexpected callback");
}
void QAudioOutputPrivate::BufferToBeFilled(CMMFBuffer *aBuffer)
{
// Following receipt of this callback, 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 DevSound by bufferFilled().
m_devSoundBuffer = static_cast<CMMFDataBuffer*>(aBuffer);
if (!m_devSoundBufferSize)
m_devSoundBufferSize = m_devSoundBuffer->Data().MaxLength();
writePaddingData();
if (m_pullMode && isDataReady() && !m_bytesPadding)
pullData();
}
void QAudioOutputPrivate::PlayError(TInt aError)
{
switch (aError) {
case KErrUnderflow:
m_underflow = true;
if (m_pullMode && !m_lastBuffer)
setError(QAudio::UnderrunError);
else
setState(SymbianAudio::IdleState);
break;
default:
setError(QAudio::IOError);
break;
}
}
void QAudioOutputPrivate::BufferToBeEmptied(CMMFBuffer *aBuffer)
{
Q_UNUSED(aBuffer)
// This class doesn't use DevSound in record mode, so should never receive
// this callback.
Q_ASSERT_X(false, Q_FUNC_INFO, "Unexpected callback");
}
void QAudioOutputPrivate::RecordError(TInt aError)
{
Q_UNUSED(aError)
// This class doesn't use DevSound in record mode, so should never receive
// this callback.
Q_ASSERT_X(false, Q_FUNC_INFO, "Unexpected callback");
}
void QAudioOutputPrivate::ConvertError(TInt aError)
{
Q_UNUSED(aError)
// This class doesn't use DevSound's format conversion functions, so
// should never receive this callback.
Q_ASSERT_X(false, Q_FUNC_INFO, "Unexpected callback");
}
void QAudioOutputPrivate::DeviceMessage(TUid aMessageType, const TDesC8 &aMsg)
{
Q_UNUSED(aMessageType)
Q_UNUSED(aMsg)
// Ignore this callback.
}
//-----------------------------------------------------------------------------
// 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::open()
{
Q_ASSERT_X(SymbianAudio::ClosedState == m_internalState,
Q_FUNC_INFO, "DevSound already opened");
QT_TRAP_THROWING( m_devSound.reset(CMMFDevSound::NewL()) )
QScopedPointer<SymbianAudio::DevSoundCapabilities> caps(
new SymbianAudio::DevSoundCapabilities(*m_devSound,
QAudio::AudioOutput));
int err = SymbianAudio::Utils::isFormatSupported(m_format, *caps) ?
KErrNone : KErrNotSupported;
if (KErrNone == err) {
setState(SymbianAudio::InitializingState);
TRAP(err, m_devSound->InitializeL(*this, m_nativeFourCC,
EMMFStatePlaying));
}
if (KErrNone != err) {
setError(QAudio::OpenError);
m_devSound.reset();
}
}
void QAudioOutputPrivate::startPlayback()
{
TRAPD(err, startDevSoundL());
if (KErrNone == err) {
if (isDataReady())
setState(SymbianAudio::ActiveState);
else
setState(SymbianAudio::IdleState);
m_notifyTimer->start(m_notifyInterval);
m_underflow = false;
Q_ASSERT(m_devSound->SamplesPlayed() == 0);
writePaddingData();
if (m_pullMode && m_source->bytesAvailable() && !m_bytesPadding)
dataReady();
} else {
setError(QAudio::OpenError);
close();
}
}
void QAudioOutputPrivate::startDevSoundL()
{
TMMFCapabilities nativeFormat = m_devSound->Config();
m_nativeFormat.iBufferSize = nativeFormat.iBufferSize;
m_devSound->SetConfigL(m_nativeFormat);
m_devSound->PlayInitL();
}
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);
m_bytesPadding -= paddingBytes;
if (m_pullMode && m_source->atEnd())
lastBufferFilled();
if (paddingBytes == outputBytes)
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");
if (m_bytesPadding)
m_bytesPadding = 1;
// 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->PlayData();
}
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();
m_devSound.reset();
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->SamplesPlayed());
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);
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, initializingState());
if (m_externalState != oldExternalState)
emit stateChanged(m_externalState);
}
bool QAudioOutputPrivate::isDataReady() const
{
return (m_source && m_source->bytesAvailable())
|| m_bytesPadding
|| m_pushDataReady;
}
QAudio::State QAudioOutputPrivate::initializingState() const
{
return isDataReady() ? QAudio::ActiveState : QAudio::IdleState;
}
QT_END_NAMESPACE