| /**************************************************************************** |
| ** |
| ** 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$ |
| ** |
| ****************************************************************************/ |
| |
| // |
| // W A R N I N G |
| // ------------- |
| // |
| // This file is not part of the Qt API. It exists for the convenience |
| // of other Qt classes. This header file may change from version to |
| // version without notice, or even be removed. |
| // |
| // We mean it. |
| // |
| |
| #include <CoreServices/CoreServices.h> |
| #include <CoreAudio/CoreAudio.h> |
| #include <AudioUnit/AudioUnit.h> |
| #include <AudioToolbox/AudioToolbox.h> |
| |
| #include <QtCore/qendian.h> |
| #include <QtCore/qbuffer.h> |
| #include <QtCore/qtimer.h> |
| #include <QtCore/qdebug.h> |
| |
| #include <QtMultimedia/qaudiooutput.h> |
| |
| #include "qaudio_mac_p.h" |
| #include "qaudiooutput_mac_p.h" |
| #include "qaudiodeviceinfo_mac_p.h" |
| |
| |
| QT_BEGIN_NAMESPACE |
| |
| |
| namespace QtMultimediaInternal |
| { |
| |
| static const int default_buffer_size = 8 * 1024; |
| |
| |
| class QAudioOutputBuffer : public QObject |
| { |
| Q_OBJECT |
| |
| public: |
| QAudioOutputBuffer(int bufferSize, int maxPeriodSize, QAudioFormat const& audioFormat): |
| m_deviceError(false), |
| m_maxPeriodSize(maxPeriodSize), |
| m_device(0) |
| { |
| m_buffer = new QAudioRingBuffer(bufferSize + (bufferSize % maxPeriodSize == 0 ? 0 : maxPeriodSize - (bufferSize % maxPeriodSize))); |
| m_bytesPerFrame = (audioFormat.sampleSize() / 8) * audioFormat.channels(); |
| m_periodTime = m_maxPeriodSize / m_bytesPerFrame * 1000 / audioFormat.frequency(); |
| |
| m_fillTimer = new QTimer(this); |
| connect(m_fillTimer, SIGNAL(timeout()), SLOT(fillBuffer())); |
| } |
| |
| ~QAudioOutputBuffer() |
| { |
| delete m_buffer; |
| } |
| |
| qint64 readFrames(char* data, qint64 maxFrames) |
| { |
| bool wecan = true; |
| qint64 framesRead = 0; |
| |
| while (wecan && framesRead < maxFrames) { |
| QAudioRingBuffer::Region region = m_buffer->acquireReadRegion((maxFrames - framesRead) * m_bytesPerFrame); |
| |
| if (region.second > 0) { |
| region.second -= region.second % m_bytesPerFrame; |
| memcpy(data + (framesRead * m_bytesPerFrame), region.first, region.second); |
| framesRead += region.second / m_bytesPerFrame; |
| } |
| else |
| wecan = false; |
| |
| m_buffer->releaseReadRegion(region); |
| } |
| |
| if (framesRead == 0 && m_deviceError) |
| framesRead = -1; |
| |
| return framesRead; |
| } |
| |
| qint64 writeBytes(const char* data, qint64 maxSize) |
| { |
| bool wecan = true; |
| qint64 bytesWritten = 0; |
| |
| maxSize -= maxSize % m_bytesPerFrame; |
| while (wecan && bytesWritten < maxSize) { |
| QAudioRingBuffer::Region region = m_buffer->acquireWriteRegion(maxSize - bytesWritten); |
| |
| if (region.second > 0) { |
| memcpy(region.first, data + bytesWritten, region.second); |
| bytesWritten += region.second; |
| } |
| else |
| wecan = false; |
| |
| m_buffer->releaseWriteRegion(region); |
| } |
| |
| if (bytesWritten > 0) |
| emit readyRead(); |
| |
| return bytesWritten; |
| } |
| |
| int available() const |
| { |
| return m_buffer->free(); |
| } |
| |
| void reset() |
| { |
| m_buffer->reset(); |
| m_deviceError = false; |
| } |
| |
| void setPrefetchDevice(QIODevice* device) |
| { |
| if (m_device != device) { |
| m_device = device; |
| if (m_device != 0) |
| fillBuffer(); |
| } |
| } |
| |
| void startFillTimer() |
| { |
| if (m_device != 0) |
| m_fillTimer->start(m_buffer->size() / 2 / m_maxPeriodSize * m_periodTime); |
| } |
| |
| void stopFillTimer() |
| { |
| m_fillTimer->stop(); |
| } |
| |
| signals: |
| void readyRead(); |
| |
| private slots: |
| void fillBuffer() |
| { |
| const int free = m_buffer->free(); |
| const int writeSize = free - (free % m_maxPeriodSize); |
| |
| if (writeSize > 0) { |
| bool wecan = true; |
| int filled = 0; |
| |
| while (!m_deviceError && wecan && filled < writeSize) { |
| QAudioRingBuffer::Region region = m_buffer->acquireWriteRegion(writeSize - filled); |
| |
| if (region.second > 0) { |
| region.second = m_device->read(region.first, region.second); |
| if (region.second > 0) |
| filled += region.second; |
| else if (region.second == 0) |
| wecan = false; |
| else if (region.second < 0) { |
| m_fillTimer->stop(); |
| region.second = 0; |
| m_deviceError = true; |
| } |
| } |
| else |
| wecan = false; |
| |
| m_buffer->releaseWriteRegion(region); |
| } |
| |
| if (filled > 0) |
| emit readyRead(); |
| } |
| } |
| |
| private: |
| bool m_deviceError; |
| int m_maxPeriodSize; |
| int m_bytesPerFrame; |
| int m_periodTime; |
| QIODevice* m_device; |
| QTimer* m_fillTimer; |
| QAudioRingBuffer* m_buffer; |
| }; |
| |
| |
| } |
| |
| class MacOutputDevice : public QIODevice |
| { |
| Q_OBJECT |
| |
| public: |
| MacOutputDevice(QtMultimediaInternal::QAudioOutputBuffer* audioBuffer, QObject* parent): |
| QIODevice(parent), |
| m_audioBuffer(audioBuffer) |
| { |
| open(QIODevice::WriteOnly | QIODevice::Unbuffered); |
| } |
| |
| qint64 readData(char* data, qint64 len) |
| { |
| Q_UNUSED(data); |
| Q_UNUSED(len); |
| |
| return 0; |
| } |
| |
| qint64 writeData(const char* data, qint64 len) |
| { |
| return m_audioBuffer->writeBytes(data, len); |
| } |
| |
| bool isSequential() const |
| { |
| return true; |
| } |
| |
| private: |
| QtMultimediaInternal::QAudioOutputBuffer* m_audioBuffer; |
| }; |
| |
| |
| QAudioOutputPrivate::QAudioOutputPrivate(const QByteArray& device, const QAudioFormat& format): |
| audioFormat(format) |
| { |
| QDataStream ds(device); |
| quint32 did, mode; |
| |
| ds >> did >> mode; |
| |
| if (QAudio::Mode(mode) == QAudio::AudioInput) |
| errorCode = QAudio::OpenError; |
| else { |
| audioDeviceInfo = new QAudioDeviceInfoInternal(device, QAudio::AudioOutput); |
| isOpen = false; |
| audioDeviceId = AudioDeviceID(did); |
| audioUnit = 0; |
| audioIO = 0; |
| startTime = 0; |
| totalFrames = 0; |
| audioBuffer = 0; |
| internalBufferSize = QtMultimediaInternal::default_buffer_size; |
| clockFrequency = AudioGetHostClockFrequency() / 1000; |
| errorCode = QAudio::NoError; |
| stateCode = QAudio::StoppedState; |
| audioThreadState = Stopped; |
| |
| intervalTimer = new QTimer(this); |
| intervalTimer->setInterval(1000); |
| connect(intervalTimer, SIGNAL(timeout()), SIGNAL(notify())); |
| } |
| } |
| |
| QAudioOutputPrivate::~QAudioOutputPrivate() |
| { |
| delete audioDeviceInfo; |
| close(); |
| } |
| |
| bool QAudioOutputPrivate::open() |
| { |
| if (errorCode != QAudio::NoError) |
| return false; |
| |
| if (isOpen) |
| return true; |
| |
| ComponentDescription cd; |
| cd.componentType = kAudioUnitType_Output; |
| cd.componentSubType = kAudioUnitSubType_HALOutput; |
| cd.componentManufacturer = kAudioUnitManufacturer_Apple; |
| cd.componentFlags = 0; |
| cd.componentFlagsMask = 0; |
| |
| // Open |
| Component cp = FindNextComponent(NULL, &cd); |
| if (cp == 0) { |
| qWarning() << "QAudioOutput: Failed to find HAL Output component"; |
| return false; |
| } |
| |
| if (OpenAComponent(cp, &audioUnit) != noErr) { |
| qWarning() << "QAudioOutput: Unable to Open Output Component"; |
| return false; |
| } |
| |
| // register callback |
| AURenderCallbackStruct cb; |
| cb.inputProc = renderCallback; |
| cb.inputProcRefCon = this; |
| |
| if (AudioUnitSetProperty(audioUnit, |
| kAudioUnitProperty_SetRenderCallback, |
| kAudioUnitScope_Global, |
| 0, |
| &cb, |
| sizeof(cb)) != noErr) { |
| qWarning() << "QAudioOutput: Failed to set AudioUnit callback"; |
| return false; |
| } |
| |
| // Set Audio Device |
| if (AudioUnitSetProperty(audioUnit, |
| kAudioOutputUnitProperty_CurrentDevice, |
| kAudioUnitScope_Global, |
| 0, |
| &audioDeviceId, |
| sizeof(audioDeviceId)) != noErr) { |
| qWarning() << "QAudioOutput: Unable to use configured device"; |
| return false; |
| } |
| |
| // Set stream format |
| streamFormat = toAudioStreamBasicDescription(audioFormat); |
| |
| UInt32 size = sizeof(streamFormat); |
| if (AudioUnitSetProperty(audioUnit, |
| kAudioUnitProperty_StreamFormat, |
| kAudioUnitScope_Input, |
| 0, |
| &streamFormat, |
| sizeof(streamFormat)) != noErr) { |
| qWarning() << "QAudioOutput: Unable to Set Stream information"; |
| return false; |
| } |
| |
| // Allocate buffer |
| UInt32 numberOfFrames = 0; |
| size = sizeof(UInt32); |
| if (AudioUnitGetProperty(audioUnit, |
| kAudioDevicePropertyBufferFrameSize, |
| kAudioUnitScope_Global, |
| 0, |
| &numberOfFrames, |
| &size) != noErr) { |
| qWarning() << "QAudioInput: Failed to get audio period size"; |
| return false; |
| } |
| |
| periodSizeBytes = numberOfFrames * streamFormat.mBytesPerFrame; |
| if (internalBufferSize < periodSizeBytes * 2) |
| internalBufferSize = periodSizeBytes * 2; |
| else |
| internalBufferSize -= internalBufferSize % streamFormat.mBytesPerFrame; |
| |
| audioBuffer = new QtMultimediaInternal::QAudioOutputBuffer(internalBufferSize, periodSizeBytes, audioFormat); |
| connect(audioBuffer, SIGNAL(readyRead()), SLOT(inputReady())); // Pull |
| |
| audioIO = new MacOutputDevice(audioBuffer, this); |
| |
| // Init |
| if (AudioUnitInitialize(audioUnit)) { |
| qWarning() << "QAudioOutput: Failed to initialize AudioUnit"; |
| return false; |
| } |
| |
| isOpen = true; |
| |
| return true; |
| } |
| |
| void QAudioOutputPrivate::close() |
| { |
| if (audioUnit != 0) { |
| AudioOutputUnitStop(audioUnit); |
| AudioUnitUninitialize(audioUnit); |
| CloseComponent(audioUnit); |
| } |
| |
| delete audioBuffer; |
| } |
| |
| QAudioFormat QAudioOutputPrivate::format() const |
| { |
| return audioFormat; |
| } |
| |
| QIODevice* QAudioOutputPrivate::start(QIODevice* device) |
| { |
| QIODevice* op = device; |
| |
| if (!audioDeviceInfo->isFormatSupported(audioFormat) || !open()) { |
| stateCode = QAudio::StoppedState; |
| errorCode = QAudio::OpenError; |
| return audioIO; |
| } |
| |
| reset(); |
| audioBuffer->reset(); |
| audioBuffer->setPrefetchDevice(op); |
| |
| if (op == 0) { |
| op = audioIO; |
| stateCode = QAudio::IdleState; |
| } |
| else |
| stateCode = QAudio::ActiveState; |
| |
| // Start |
| errorCode = QAudio::NoError; |
| totalFrames = 0; |
| startTime = AudioGetCurrentHostTime(); |
| |
| if (stateCode == QAudio::ActiveState) |
| audioThreadStart(); |
| |
| emit stateChanged(stateCode); |
| |
| return op; |
| } |
| |
| void QAudioOutputPrivate::stop() |
| { |
| QMutexLocker lock(&mutex); |
| if (stateCode != QAudio::StoppedState) { |
| audioThreadDrain(); |
| |
| stateCode = QAudio::StoppedState; |
| errorCode = QAudio::NoError; |
| QMetaObject::invokeMethod(this, "stateChanged", Qt::QueuedConnection, Q_ARG(QAudio::State, stateCode)); |
| } |
| } |
| |
| void QAudioOutputPrivate::reset() |
| { |
| QMutexLocker lock(&mutex); |
| if (stateCode != QAudio::StoppedState) { |
| audioThreadStop(); |
| |
| stateCode = QAudio::StoppedState; |
| errorCode = QAudio::NoError; |
| QMetaObject::invokeMethod(this, "stateChanged", Qt::QueuedConnection, Q_ARG(QAudio::State, stateCode)); |
| } |
| } |
| |
| void QAudioOutputPrivate::suspend() |
| { |
| QMutexLocker lock(&mutex); |
| if (stateCode == QAudio::ActiveState || stateCode == QAudio::IdleState) { |
| audioThreadStop(); |
| |
| stateCode = QAudio::SuspendedState; |
| errorCode = QAudio::NoError; |
| QMetaObject::invokeMethod(this, "stateChanged", Qt::QueuedConnection, Q_ARG(QAudio::State, stateCode)); |
| } |
| } |
| |
| void QAudioOutputPrivate::resume() |
| { |
| QMutexLocker lock(&mutex); |
| if (stateCode == QAudio::SuspendedState) { |
| audioThreadStart(); |
| |
| stateCode = QAudio::ActiveState; |
| errorCode = QAudio::NoError; |
| QMetaObject::invokeMethod(this, "stateChanged", Qt::QueuedConnection, Q_ARG(QAudio::State, stateCode)); |
| } |
| } |
| |
| int QAudioOutputPrivate::bytesFree() const |
| { |
| return audioBuffer->available(); |
| } |
| |
| int QAudioOutputPrivate::periodSize() const |
| { |
| return periodSizeBytes; |
| } |
| |
| void QAudioOutputPrivate::setBufferSize(int bs) |
| { |
| if (stateCode == QAudio::StoppedState) |
| internalBufferSize = bs; |
| } |
| |
| int QAudioOutputPrivate::bufferSize() const |
| { |
| return internalBufferSize; |
| } |
| |
| void QAudioOutputPrivate::setNotifyInterval(int milliSeconds) |
| { |
| if (intervalTimer->interval() == milliSeconds) |
| return; |
| |
| if (milliSeconds <= 0) |
| milliSeconds = 0; |
| |
| intervalTimer->setInterval(milliSeconds); |
| } |
| |
| int QAudioOutputPrivate::notifyInterval() const |
| { |
| return intervalTimer->interval(); |
| } |
| |
| qint64 QAudioOutputPrivate::processedUSecs() const |
| { |
| return totalFrames * 1000000 / audioFormat.frequency(); |
| } |
| |
| qint64 QAudioOutputPrivate::elapsedUSecs() const |
| { |
| if (stateCode == QAudio::StoppedState) |
| return 0; |
| |
| return (AudioGetCurrentHostTime() - startTime) / (clockFrequency / 1000); |
| } |
| |
| QAudio::Error QAudioOutputPrivate::error() const |
| { |
| return errorCode; |
| } |
| |
| QAudio::State QAudioOutputPrivate::state() const |
| { |
| return stateCode; |
| } |
| |
| void QAudioOutputPrivate::audioThreadStart() |
| { |
| startTimers(); |
| audioThreadState = Running; |
| AudioOutputUnitStart(audioUnit); |
| } |
| |
| void QAudioOutputPrivate::audioThreadStop() |
| { |
| stopTimers(); |
| if (audioThreadState.testAndSetAcquire(Running, Stopped)) |
| threadFinished.wait(&mutex); |
| } |
| |
| void QAudioOutputPrivate::audioThreadDrain() |
| { |
| stopTimers(); |
| if (audioThreadState.testAndSetAcquire(Running, Draining)) |
| threadFinished.wait(&mutex); |
| } |
| |
| void QAudioOutputPrivate::audioDeviceStop() |
| { |
| AudioOutputUnitStop(audioUnit); |
| audioThreadState = Stopped; |
| threadFinished.wakeOne(); |
| } |
| |
| void QAudioOutputPrivate::audioDeviceIdle() |
| { |
| QMutexLocker lock(&mutex); |
| if (stateCode == QAudio::ActiveState) { |
| audioDeviceStop(); |
| |
| errorCode = QAudio::UnderrunError; |
| stateCode = QAudio::IdleState; |
| QMetaObject::invokeMethod(this, "deviceStopped", Qt::QueuedConnection); |
| } |
| } |
| |
| void QAudioOutputPrivate::audioDeviceError() |
| { |
| QMutexLocker lock(&mutex); |
| if (stateCode == QAudio::ActiveState) { |
| audioDeviceStop(); |
| |
| errorCode = QAudio::IOError; |
| stateCode = QAudio::StoppedState; |
| QMetaObject::invokeMethod(this, "deviceStopped", Qt::QueuedConnection); |
| } |
| } |
| |
| void QAudioOutputPrivate::startTimers() |
| { |
| audioBuffer->startFillTimer(); |
| if (intervalTimer->interval() > 0) |
| intervalTimer->start(); |
| } |
| |
| void QAudioOutputPrivate::stopTimers() |
| { |
| audioBuffer->stopFillTimer(); |
| intervalTimer->stop(); |
| } |
| |
| |
| void QAudioOutputPrivate::deviceStopped() |
| { |
| intervalTimer->stop(); |
| emit stateChanged(stateCode); |
| } |
| |
| void QAudioOutputPrivate::inputReady() |
| { |
| QMutexLocker lock(&mutex); |
| if (stateCode == QAudio::IdleState) { |
| audioThreadStart(); |
| |
| stateCode = QAudio::ActiveState; |
| errorCode = QAudio::NoError; |
| |
| QMetaObject::invokeMethod(this, "stateChanged", Qt::QueuedConnection, Q_ARG(QAudio::State, stateCode)); |
| } |
| } |
| |
| |
| OSStatus QAudioOutputPrivate::renderCallback(void* inRefCon, |
| AudioUnitRenderActionFlags* ioActionFlags, |
| const AudioTimeStamp* inTimeStamp, |
| UInt32 inBusNumber, |
| UInt32 inNumberFrames, |
| AudioBufferList* ioData) |
| { |
| Q_UNUSED(ioActionFlags) |
| Q_UNUSED(inTimeStamp) |
| Q_UNUSED(inBusNumber) |
| Q_UNUSED(inNumberFrames) |
| |
| QAudioOutputPrivate* d = static_cast<QAudioOutputPrivate*>(inRefCon); |
| |
| const int threadState = d->audioThreadState.fetchAndAddAcquire(0); |
| if (threadState == Stopped) { |
| ioData->mBuffers[0].mDataByteSize = 0; |
| d->audioDeviceStop(); |
| } |
| else { |
| const UInt32 bytesPerFrame = d->streamFormat.mBytesPerFrame; |
| qint64 framesRead; |
| |
| framesRead = d->audioBuffer->readFrames((char*)ioData->mBuffers[0].mData, |
| ioData->mBuffers[0].mDataByteSize / bytesPerFrame); |
| |
| if (framesRead > 0) { |
| ioData->mBuffers[0].mDataByteSize = framesRead * bytesPerFrame; |
| d->totalFrames += framesRead; |
| } |
| else { |
| ioData->mBuffers[0].mDataByteSize = 0; |
| if (framesRead == 0) { |
| if (threadState == Draining) |
| d->audioDeviceStop(); |
| else |
| d->audioDeviceIdle(); |
| } |
| else |
| d->audioDeviceError(); |
| } |
| } |
| |
| return noErr; |
| } |
| |
| |
| QT_END_NAMESPACE |
| |
| #include "qaudiooutput_mac_p.moc" |
| |