diff options
Diffstat (limited to 'src/multimedia/darwin')
-rw-r--r-- | src/multimedia/darwin/qcoreaudiosessionmanager.mm | 437 | ||||
-rw-r--r-- | src/multimedia/darwin/qcoreaudiosessionmanager_p.h | 95 | ||||
-rw-r--r-- | src/multimedia/darwin/qcoreaudioutils.mm | 344 | ||||
-rw-r--r-- | src/multimedia/darwin/qcoreaudioutils_p.h | 72 | ||||
-rw-r--r-- | src/multimedia/darwin/qdarwinaudiodevice.mm | 108 | ||||
-rw-r--r-- | src/multimedia/darwin/qdarwinaudiodevice_p.h | 52 | ||||
-rw-r--r-- | src/multimedia/darwin/qdarwinaudiosink.mm | 609 | ||||
-rw-r--r-- | src/multimedia/darwin/qdarwinaudiosink_p.h | 170 | ||||
-rw-r--r-- | src/multimedia/darwin/qdarwinaudiosource.mm | 942 | ||||
-rw-r--r-- | src/multimedia/darwin/qdarwinaudiosource_p.h | 244 | ||||
-rw-r--r-- | src/multimedia/darwin/qdarwinmediadevices.mm | 269 | ||||
-rw-r--r-- | src/multimedia/darwin/qdarwinmediadevices_p.h | 49 | ||||
-rw-r--r-- | src/multimedia/darwin/qmacosaudiodatautils_p.h | 112 |
13 files changed, 3503 insertions, 0 deletions
diff --git a/src/multimedia/darwin/qcoreaudiosessionmanager.mm b/src/multimedia/darwin/qcoreaudiosessionmanager.mm new file mode 100644 index 000000000..cee955905 --- /dev/null +++ b/src/multimedia/darwin/qcoreaudiosessionmanager.mm @@ -0,0 +1,437 @@ +// Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qcoreaudiosessionmanager_p.h" +#import <AVFoundation/AVAudioSession.h> +#import <Foundation/Foundation.h> + +QT_BEGIN_NAMESPACE + +@interface CoreAudioSessionObserver : NSObject +{ + CoreAudioSessionManager *m_sessionManager; + AVAudioSession *m_audioSession; +} + +@property (readonly, getter=sessionManager) CoreAudioSessionManager *m_sessionManager; +@property (readonly, getter=audioSession) AVAudioSession *m_audioSession; + +-(CoreAudioSessionObserver *)initWithAudioSessionManager:(CoreAudioSessionManager *)sessionManager; + +-(BOOL)activateAudio; +-(BOOL)deactivateAudio; + +//Notification handlers +-(void)audioSessionInterruption:(NSNotification *)notification; +-(void)audioSessionRouteChange:(NSNotification *)notification; +-(void)audioSessionMediaServicesWereReset:(NSNotification *)notification; + +@end //interface CoreAudioSessionObserver + +@implementation CoreAudioSessionObserver + +@synthesize m_sessionManager, m_audioSession; + +-(CoreAudioSessionObserver *)initWithAudioSessionManager:(CoreAudioSessionManager *)sessionManager +{ + if (!(self = [super init])) + return nil; + + self->m_sessionManager = sessionManager; + self->m_audioSession = [AVAudioSession sharedInstance]; + + //Set up observers + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(audioSessionInterruption:) + name:AVAudioSessionInterruptionNotification + object:self->m_audioSession]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(audioSessionMediaServicesWereReset:) + name:AVAudioSessionMediaServicesWereResetNotification + object:self->m_audioSession]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(audioSessionRouteChange:) + name:AVAudioSessionRouteChangeNotification + object:self->m_audioSession]; + + return self; +} + +-(void)dealloc +{ +#ifdef QT_DEBUG_COREAUDIO + qDebug() << Q_FUNC_INFO; +#endif + + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVAudioSessionInterruptionNotification + object:self->m_audioSession]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVAudioSessionMediaServicesWereResetNotification + object:self->m_audioSession]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVAudioSessionRouteChangeNotification + object:self->m_audioSession]; + + [super dealloc]; +} + +-(BOOL)activateAudio +{ + NSError *error = nil; + BOOL success = [self->m_audioSession setActive:YES error:&error]; + if (![self->m_audioSession setActive:YES error:&error]) { +#ifdef QT_DEBUG_COREAUDIO + qDebug("audio session activation failed: %s", [[error localizedDescription] UTF8String]); + } else { + qDebug("audio session activated"); +#endif + } + + return success; +} + +-(BOOL)deactivateAudio +{ + NSError *error = nil; + BOOL success = [m_audioSession setActive:NO error:&error]; +#ifdef QT_DEBUG_COREAUDIO + if (!success) { + qDebug("%s", [[error localizedDescription] UTF8String]); + } +#endif + return success; +} + +-(void)audioSessionInterruption:(NSNotification *)notification +{ + NSNumber *type = [[notification userInfo] valueForKey:AVAudioSessionInterruptionTypeKey]; + if ([type intValue] == AVAudioSessionInterruptionTypeBegan) { +#ifdef QT_DEBUG_COREAUDIO + qDebug("audioSession Interuption begain"); +#endif + } else if ([type intValue] == AVAudioSessionInterruptionTypeEnded) { +#ifdef QT_DEBUG_COREAUDIO + qDebug("audioSession Interuption ended"); +#endif + NSNumber *option = [[notification userInfo] valueForKey:AVAudioSessionInterruptionOptionKey]; + if ([option intValue] == AVAudioSessionInterruptionOptionShouldResume) { +#ifdef QT_DEBUG_COREAUDIO + qDebug("audioSession is active and immediately ready to be used."); +#endif + } else { + [self activateAudio]; + } + } +} + +-(void)audioSessionMediaServicesWereReset:(NSNotification *)notification +{ + Q_UNUSED(notification); +#ifdef QT_DEBUG_COREAUDIO + qDebug("audioSession Media Services were reset"); +#endif + //Reactivate audio when this occurs + [self activateAudio]; +} + +-(void)audioSessionRouteChange:(NSNotification *)notification +{ + NSNumber *reason = [[notification userInfo] valueForKey:AVAudioSessionRouteChangeReasonKey]; + NSUInteger reasonEnum = [reason intValue]; + + if (reasonEnum == AVAudioSessionRouteChangeReasonUnknown) { +#ifdef QT_DEBUG_COREAUDIO + qDebug("audioSession route changed. reason: unknown"); +#endif + } else if (reasonEnum == AVAudioSessionRouteChangeReasonNewDeviceAvailable) { +#ifdef QT_DEBUG_COREAUDIO + qDebug("audioSession route changed. reason: new device available"); +#endif + } else if (reasonEnum == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { +#ifdef QT_DEBUG_COREAUDIO + qDebug("audioSession route changed. reason: old device unavailable"); +#endif + } else if (reasonEnum == AVAudioSessionRouteChangeReasonCategoryChange) { +#ifdef QT_DEBUG_COREAUDIO + qDebug("audioSession route changed. reason: category changed"); +#endif + } else if (reasonEnum == AVAudioSessionRouteChangeReasonOverride) { +#ifdef QT_DEBUG_COREAUDIO + qDebug("audioSession route changed. reason: override"); +#endif + } else if (reasonEnum == AVAudioSessionRouteChangeReasonWakeFromSleep) { +#ifdef QT_DEBUG_COREAUDIO + qDebug("audioSession route changed. reason: woken from sleep"); +#endif + } else if (reasonEnum == AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory) { +#ifdef QT_DEBUG_COREAUDIO + qDebug("audioSession route changed. reason: no suitable route for category"); +#endif + } + + m_sessionManager->devicesAvailableChanged(); +} + +@end //implementation CoreAudioSessionObserver + +CoreAudioSessionManager::CoreAudioSessionManager() : + QObject(0) +{ + m_sessionObserver = [[CoreAudioSessionObserver alloc] initWithAudioSessionManager:this]; +} + +CoreAudioSessionManager::~CoreAudioSessionManager() +{ +#ifdef QT_DEBUG_COREAUDIO + qDebug() << Q_FUNC_INFO; +#endif + [m_sessionObserver release]; +} + + +CoreAudioSessionManager &CoreAudioSessionManager::instance() +{ + static CoreAudioSessionManager instance; + return instance; +} + +bool CoreAudioSessionManager::setActive(bool active) +{ + if (active) { + return [m_sessionObserver activateAudio]; + } else { + return [m_sessionObserver deactivateAudio]; + } +} + +bool CoreAudioSessionManager::setCategory(CoreAudioSessionManager::AudioSessionCategorys category, CoreAudioSessionManager::AudioSessionCategoryOptions options) +{ + NSString *targetCategory = nil; + + switch (category) { + case CoreAudioSessionManager::Ambient: + targetCategory = AVAudioSessionCategoryAmbient; + break; + case CoreAudioSessionManager::SoloAmbient: + targetCategory = AVAudioSessionCategorySoloAmbient; + break; + case CoreAudioSessionManager::Playback: + targetCategory = AVAudioSessionCategoryPlayback; + break; + case CoreAudioSessionManager::Record: + targetCategory = AVAudioSessionCategoryRecord; + break; + case CoreAudioSessionManager::PlayAndRecord: + targetCategory = AVAudioSessionCategoryPlayAndRecord; + break; + case CoreAudioSessionManager::AudioProcessing: +#ifndef Q_OS_TVOS + targetCategory = AVAudioSessionCategoryAudioProcessing; +#endif + break; + case CoreAudioSessionManager::MultiRoute: + targetCategory = AVAudioSessionCategoryMultiRoute; + break; + } + + if (targetCategory == nil) + return false; + + return [[m_sessionObserver audioSession] setCategory:targetCategory + withOptions:(AVAudioSessionCategoryOptions)options + error:nil]; +} + +bool CoreAudioSessionManager::setMode(CoreAudioSessionManager::AudioSessionModes mode) +{ + NSString *targetMode = nil; + switch (mode) { + case CoreAudioSessionManager::Default: + targetMode = AVAudioSessionModeDefault; + break; + case CoreAudioSessionManager::VoiceChat: + targetMode = AVAudioSessionModeVoiceChat; + break; + case CoreAudioSessionManager::GameChat: + targetMode = AVAudioSessionModeGameChat; + break; + case CoreAudioSessionManager::VideoRecording: + targetMode = AVAudioSessionModeVideoRecording; + break; + case CoreAudioSessionManager::Measurement: + targetMode = AVAudioSessionModeMeasurement; + break; + case CoreAudioSessionManager::MoviePlayback: + targetMode = AVAudioSessionModeMoviePlayback; + break; + } + + if (targetMode == nil) + return false; + + return [[m_sessionObserver audioSession] setMode:targetMode error:nil]; + +} + +CoreAudioSessionManager::AudioSessionCategorys CoreAudioSessionManager::category() +{ + NSString *category = [[m_sessionObserver audioSession] category]; + AudioSessionCategorys localCategory = Ambient; + + if (category == AVAudioSessionCategoryAmbient) { + localCategory = Ambient; + } else if (category == AVAudioSessionCategorySoloAmbient) { + localCategory = SoloAmbient; + } else if (category == AVAudioSessionCategoryPlayback) { + localCategory = Playback; + } else if (category == AVAudioSessionCategoryRecord) { + localCategory = Record; + } else if (category == AVAudioSessionCategoryPlayAndRecord) { + localCategory = PlayAndRecord; +#ifndef Q_OS_TVOS + } else if (category == AVAudioSessionCategoryAudioProcessing) { + localCategory = AudioProcessing; +#endif + } else if (category == AVAudioSessionCategoryMultiRoute) { + localCategory = MultiRoute; + } + + return localCategory; +} + +CoreAudioSessionManager::AudioSessionModes CoreAudioSessionManager::mode() +{ + NSString *mode = [[m_sessionObserver audioSession] mode]; + AudioSessionModes localMode = Default; + + if (mode == AVAudioSessionModeDefault) { + localMode = Default; + } else if (mode == AVAudioSessionModeVoiceChat) { + localMode = VoiceChat; + } else if (mode == AVAudioSessionModeGameChat) { + localMode = GameChat; + } else if (mode == AVAudioSessionModeVideoRecording) { + localMode = VideoRecording; + } else if (mode == AVAudioSessionModeMeasurement) { + localMode = Measurement; + } else if (mode == AVAudioSessionModeMoviePlayback) { + localMode = MoviePlayback; + } + + return localMode; +} + +QList<QByteArray> CoreAudioSessionManager::inputDevices() +{ + //TODO: Add support for USB input devices + //Right now the default behavior on iOS is to have only one input route + //at a time. + QList<QByteArray> inputDevices; + inputDevices << "default"; + return inputDevices; +} + +QList<QByteArray> CoreAudioSessionManager::outputDevices() +{ + //TODO: Add support for USB output devices + //Right now the default behavior on iOS is to have only one output route + //at a time. + QList<QByteArray> outputDevices; + outputDevices << "default"; + return outputDevices; +} + +float CoreAudioSessionManager::currentIOBufferDuration() +{ + return [[m_sessionObserver audioSession] IOBufferDuration]; +} + +float CoreAudioSessionManager::preferredSampleRate() +{ + return [[m_sessionObserver audioSession] preferredSampleRate]; +} + +#ifdef QT_DEBUG_COREAUDIO +QDebug operator<<(QDebug dbg, CoreAudioSessionManager::AudioSessionCategorys category) +{ + QDebug output = dbg.nospace(); + switch (category) { + case CoreAudioSessionManager::Ambient: + output << "AudioSessionCategoryAmbient"; + break; + case CoreAudioSessionManager::SoloAmbient: + output << "AudioSessionCategorySoloAmbient"; + break; + case CoreAudioSessionManager::Playback: + output << "AudioSessionCategoryPlayback"; + break; + case CoreAudioSessionManager::Record: + output << "AudioSessionCategoryRecord"; + break; + case CoreAudioSessionManager::PlayAndRecord: + output << "AudioSessionCategoryPlayAndRecord"; + break; + case CoreAudioSessionManager::AudioProcessing: + output << "AudioSessionCategoryAudioProcessing"; + break; + case CoreAudioSessionManager::MultiRoute: + output << "AudioSessionCategoryMultiRoute"; + break; + } + return output; +} + +QDebug operator<<(QDebug dbg, CoreAudioSessionManager::AudioSessionCategoryOptions option) +{ + QDebug output = dbg.nospace(); + switch (option) { + case CoreAudioSessionManager::None: + output << "AudioSessionCategoryOptionNone"; + break; + case CoreAudioSessionManager::MixWithOthers: + output << "AudioSessionCategoryOptionMixWithOthers"; + break; + case CoreAudioSessionManager::DuckOthers: + output << "AudioSessionCategoryOptionDuckOthers"; + break; + case CoreAudioSessionManager::AllowBluetooth: + output << "AudioSessionCategoryOptionAllowBluetooth"; + break; + case CoreAudioSessionManager::DefaultToSpeaker: + output << "AudioSessionCategoryOptionDefaultToSpeaker"; + break; + } + return output; +} + +QDebug operator<<(QDebug dbg, CoreAudioSessionManager::AudioSessionModes mode) +{ + QDebug output = dbg.nospace(); + switch (mode) { + case CoreAudioSessionManager::Default: + output << "AudioSessionModeDefault"; + break; + case CoreAudioSessionManager::VoiceChat: + output << "AudioSessionModeVoiceChat"; + break; + case CoreAudioSessionManager::GameChat: + output << "AudioSessionModeGameChat"; + break; + case CoreAudioSessionManager::VideoRecording: + output << "AudioSessionModeVideoRecording"; + break; + case CoreAudioSessionManager::Measurement: + output << "AudioSessionModeMeasurement"; + break; + case CoreAudioSessionManager::MoviePlayback: + output << "AudioSessionModeMoviePlayback"; + break; + } + return output; +} +#endif + +QT_END_NAMESPACE + +#include "moc_qcoreaudiosessionmanager_p.cpp" diff --git a/src/multimedia/darwin/qcoreaudiosessionmanager_p.h b/src/multimedia/darwin/qcoreaudiosessionmanager_p.h new file mode 100644 index 000000000..a6dfe88f4 --- /dev/null +++ b/src/multimedia/darwin/qcoreaudiosessionmanager_p.h @@ -0,0 +1,95 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef IOSAUDIOSESSIONMANAGER_H +#define IOSAUDIOSESSIONMANAGER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QObject> +#ifdef QT_DEBUG_COREAUDIO +# include <QtCore/QDebug> +#endif + +@class CoreAudioSessionObserver; + +QT_BEGIN_NAMESPACE + +class CoreAudioSessionManager : public QObject +{ + Q_OBJECT +public: + enum AudioSessionCategorys { + Ambient, + SoloAmbient, + Playback, + Record, + PlayAndRecord, + AudioProcessing, + MultiRoute + }; + enum AudioSessionCategoryOptions { + None = 0, + MixWithOthers = 1, + DuckOthers = 2, + AllowBluetooth = 4, + DefaultToSpeaker = 8 + }; + enum AudioSessionModes { + Default, + VoiceChat, + GameChat, + VideoRecording, + Measurement, + MoviePlayback + }; + + static CoreAudioSessionManager& instance(); + + bool setActive(bool active); + bool setCategory(AudioSessionCategorys category, AudioSessionCategoryOptions options = None); + bool setMode(AudioSessionModes mode); + + AudioSessionCategorys category(); + AudioSessionModes mode(); + + QList<QByteArray> inputDevices(); + QList<QByteArray> outputDevices(); + + float currentIOBufferDuration(); + float preferredSampleRate(); + +signals: + void activeChanged(); + void categoryChanged(); + void modeChanged(); + void routeChanged(); + void devicesAvailableChanged(); + +private: + CoreAudioSessionManager(); + ~CoreAudioSessionManager(); + CoreAudioSessionManager(CoreAudioSessionManager const ©); + CoreAudioSessionManager& operator =(CoreAudioSessionManager const ©); + + CoreAudioSessionObserver *m_sessionObserver; +}; + +#ifdef QT_DEBUG_COREAUDIO +QDebug operator <<(QDebug dbg, CoreAudioSessionManager::AudioSessionCategorys category); +QDebug operator <<(QDebug dbg, CoreAudioSessionManager::AudioSessionCategoryOptions option); +QDebug operator <<(QDebug dbg, CoreAudioSessionManager::AudioSessionModes mode); +#endif + +QT_END_NAMESPACE + +#endif // IOSAUDIOSESSIONMANAGER_H diff --git a/src/multimedia/darwin/qcoreaudioutils.mm b/src/multimedia/darwin/qcoreaudioutils.mm new file mode 100644 index 000000000..46f98dcc9 --- /dev/null +++ b/src/multimedia/darwin/qcoreaudioutils.mm @@ -0,0 +1,344 @@ +// Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qcoreaudioutils_p.h" +#include <qdebug.h> +#include <mach/mach_time.h> + +QT_BEGIN_NAMESPACE + +double CoreAudioUtils::sFrequency = 0.0; +bool CoreAudioUtils::sIsInitialized = false; + +void CoreAudioUtils::initialize() +{ + struct mach_timebase_info timeBaseInfo; + mach_timebase_info(&timeBaseInfo); + sFrequency = static_cast<double>(timeBaseInfo.denom) / static_cast<double>(timeBaseInfo.numer); + sFrequency *= 1000000000.0; + + sIsInitialized = true; +} + + +quint64 CoreAudioUtils::currentTime() +{ + return mach_absolute_time(); +} + +double CoreAudioUtils::frequency() +{ + if (!sIsInitialized) + initialize(); + return sFrequency; +} + +QAudioFormat CoreAudioUtils::toQAudioFormat(AudioStreamBasicDescription const& sf) +{ + QAudioFormat audioFormat; + // all Darwin HW is little endian, we ignore those formats + if ((sf.mFormatFlags & kAudioFormatFlagIsBigEndian) != 0 && QSysInfo::ByteOrder != QSysInfo::LittleEndian) + return audioFormat; + + // filter out the formats we're interested in + QAudioFormat::SampleFormat format = QAudioFormat::Unknown; + switch (sf.mBitsPerChannel) { + case 8: + if ((sf.mFormatFlags & kAudioFormatFlagIsSignedInteger) == 0) + format = QAudioFormat::UInt8; + break; + case 16: + if ((sf.mFormatFlags & kAudioFormatFlagIsSignedInteger) != 0) + format = QAudioFormat::Int16; + break; + case 32: + if ((sf.mFormatFlags & kAudioFormatFlagIsSignedInteger) != 0) + format = QAudioFormat::Int32; + else if ((sf.mFormatFlags & kAudioFormatFlagIsFloat) != 0) + format = QAudioFormat::Float; + break; + default: + break; + } + + audioFormat.setSampleFormat(format); + audioFormat.setSampleRate(sf.mSampleRate); + audioFormat.setChannelCount(sf.mChannelsPerFrame); + + return audioFormat; +} + +AudioStreamBasicDescription CoreAudioUtils::toAudioStreamBasicDescription(QAudioFormat const& audioFormat) +{ + AudioStreamBasicDescription sf; + + sf.mFormatFlags = kAudioFormatFlagIsPacked; + sf.mSampleRate = audioFormat.sampleRate(); + sf.mFramesPerPacket = 1; + sf.mChannelsPerFrame = audioFormat.channelCount(); + sf.mBitsPerChannel = audioFormat.bytesPerSample() * 8; + sf.mBytesPerFrame = audioFormat.bytesPerFrame(); + sf.mBytesPerPacket = sf.mFramesPerPacket * sf.mBytesPerFrame; + sf.mFormatID = kAudioFormatLinearPCM; + + switch (audioFormat.sampleFormat()) { + case QAudioFormat::Int16: + case QAudioFormat::Int32: + sf.mFormatFlags |= kAudioFormatFlagIsSignedInteger; + break; + case QAudioFormat::Float: + sf.mFormatFlags |= kAudioFormatFlagIsFloat; + break; + case QAudioFormat::UInt8: + /* default */ + case QAudioFormat::Unknown: + case QAudioFormat::NSampleFormats: + break; + } + + return sf; +} + + +static constexpr struct { + QAudioFormat::AudioChannelPosition pos; + AudioChannelLabel label; +} channelMap[] = { + { QAudioFormat::FrontLeft, kAudioChannelLabel_Left }, + { QAudioFormat::FrontRight, kAudioChannelLabel_Right }, + { QAudioFormat::FrontCenter, kAudioChannelLabel_Center }, + { QAudioFormat::LFE, kAudioChannelLabel_LFEScreen }, + { QAudioFormat::BackLeft, kAudioChannelLabel_LeftSurround }, + { QAudioFormat::BackRight, kAudioChannelLabel_RightSurround }, + { QAudioFormat::FrontLeftOfCenter, kAudioChannelLabel_LeftCenter }, + { QAudioFormat::FrontRightOfCenter, kAudioChannelLabel_RightCenter }, + { QAudioFormat::BackCenter, kAudioChannelLabel_CenterSurround }, + { QAudioFormat::LFE2, kAudioChannelLabel_LFE2 }, + { QAudioFormat::SideLeft, kAudioChannelLabel_LeftSurroundDirect }, // ??? + { QAudioFormat::SideRight, kAudioChannelLabel_RightSurroundDirect }, // ??? + { QAudioFormat::TopFrontLeft, kAudioChannelLabel_VerticalHeightLeft }, + { QAudioFormat::TopFrontRight, kAudioChannelLabel_VerticalHeightRight }, + { QAudioFormat::TopFrontCenter, kAudioChannelLabel_VerticalHeightCenter }, + { QAudioFormat::TopCenter, kAudioChannelLabel_CenterTopMiddle }, + { QAudioFormat::TopBackLeft, kAudioChannelLabel_TopBackLeft }, + { QAudioFormat::TopBackRight, kAudioChannelLabel_TopBackRight }, + { QAudioFormat::TopSideLeft, kAudioChannelLabel_LeftTopMiddle }, + { QAudioFormat::TopSideRight, kAudioChannelLabel_RightTopMiddle }, + { QAudioFormat::TopBackCenter, kAudioChannelLabel_TopBackCenter }, +}; + +std::unique_ptr<AudioChannelLayout> CoreAudioUtils::toAudioChannelLayout(const QAudioFormat &format, UInt32 *size) +{ + auto channelConfig = format.channelConfig(); + if (channelConfig == QAudioFormat::ChannelConfigUnknown) + channelConfig = QAudioFormat::defaultChannelConfigForChannelCount(format.channelCount()); + + *size = sizeof(AudioChannelLayout) + int(QAudioFormat::NChannelPositions)*sizeof(AudioChannelDescription); + auto *layout = static_cast<AudioChannelLayout *>(malloc(*size)); + memset(layout, 0, *size); + layout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions; + + for (const auto &m : channelMap) { + if (channelConfig & QAudioFormat::channelConfig(m.pos)) + layout->mChannelDescriptions[layout->mNumberChannelDescriptions++].mChannelLabel = m.label; + } + + if (channelConfig & QAudioFormat::channelConfig(QAudioFormat::BottomFrontCenter)) { + auto &desc = layout->mChannelDescriptions[layout->mNumberChannelDescriptions++]; + desc.mChannelLabel = kAudioChannelLabel_UseCoordinates; + desc.mChannelFlags = kAudioChannelFlags_SphericalCoordinates; + desc.mCoordinates[kAudioChannelCoordinates_Azimuth] = 0.f; + desc.mCoordinates[kAudioChannelCoordinates_Elevation] = -20.; + desc.mCoordinates[kAudioChannelCoordinates_Distance] = 1.f; + } + if (channelConfig & QAudioFormat::channelConfig(QAudioFormat::BottomFrontLeft)) { + auto &desc = layout->mChannelDescriptions[layout->mNumberChannelDescriptions++]; + desc.mChannelLabel = kAudioChannelLabel_UseCoordinates; + desc.mChannelFlags = kAudioChannelFlags_SphericalCoordinates; + desc.mCoordinates[kAudioChannelCoordinates_Azimuth] = -45.f; + desc.mCoordinates[kAudioChannelCoordinates_Elevation] = -20.; + desc.mCoordinates[kAudioChannelCoordinates_Distance] = 1.f; + } + if (channelConfig & QAudioFormat::channelConfig(QAudioFormat::BottomFrontRight)) { + auto &desc = layout->mChannelDescriptions[layout->mNumberChannelDescriptions++]; + desc.mChannelLabel = kAudioChannelLabel_UseCoordinates; + desc.mChannelFlags = kAudioChannelFlags_SphericalCoordinates; + desc.mCoordinates[kAudioChannelCoordinates_Azimuth] = 45.f; + desc.mCoordinates[kAudioChannelCoordinates_Elevation] = -20.; + desc.mCoordinates[kAudioChannelCoordinates_Distance] = 1.f; + } + + return std::unique_ptr<AudioChannelLayout>(layout); +} + +static constexpr struct { + AudioChannelLayoutTag tag; + QAudioFormat::ChannelConfig channelConfig; +} layoutTagMap[] = { + { kAudioChannelLayoutTag_Mono, QAudioFormat::ChannelConfigMono }, + { kAudioChannelLayoutTag_Stereo, QAudioFormat::ChannelConfigStereo }, + { kAudioChannelLayoutTag_StereoHeadphones, QAudioFormat::ChannelConfigStereo }, + { kAudioChannelLayoutTag_MPEG_1_0, QAudioFormat::ChannelConfigMono }, + { kAudioChannelLayoutTag_MPEG_2_0, QAudioFormat::ChannelConfigStereo }, + { kAudioChannelLayoutTag_MPEG_3_0_A, QAudioFormat::channelConfig(QAudioFormat::FrontLeft, + QAudioFormat::FrontRight, + QAudioFormat::FrontCenter) }, + { kAudioChannelLayoutTag_MPEG_4_0_A, QAudioFormat::channelConfig(QAudioFormat::FrontLeft, + QAudioFormat::FrontRight, + QAudioFormat::FrontCenter, + QAudioFormat::BackCenter) }, + { kAudioChannelLayoutTag_MPEG_5_0_A, QAudioFormat::ChannelConfigSurround5Dot0 }, + { kAudioChannelLayoutTag_MPEG_5_1_A, QAudioFormat::ChannelConfigSurround5Dot1 }, + { kAudioChannelLayoutTag_MPEG_6_1_A, QAudioFormat::channelConfig(QAudioFormat::FrontLeft, + QAudioFormat::FrontRight, + QAudioFormat::FrontCenter, + QAudioFormat::LFE, + QAudioFormat::BackLeft, + QAudioFormat::BackRight, + QAudioFormat::BackCenter) }, + { kAudioChannelLayoutTag_MPEG_7_1_A, QAudioFormat::ChannelConfigSurround7Dot1 }, + { kAudioChannelLayoutTag_SMPTE_DTV, QAudioFormat::channelConfig(QAudioFormat::FrontLeft, + QAudioFormat::FrontRight, + QAudioFormat::FrontCenter, + QAudioFormat::LFE, + QAudioFormat::BackLeft, + QAudioFormat::BackRight, + QAudioFormat::TopFrontLeft, + QAudioFormat::TopFrontRight) }, + + { kAudioChannelLayoutTag_ITU_2_1, QAudioFormat::ChannelConfig2Dot1 }, + { kAudioChannelLayoutTag_ITU_2_2, QAudioFormat::channelConfig(QAudioFormat::FrontLeft, + QAudioFormat::FrontRight, + QAudioFormat::BackLeft, + QAudioFormat::BackRight) } +}; + + +QAudioFormat::ChannelConfig CoreAudioUtils::fromAudioChannelLayout(const AudioChannelLayout *layout) +{ + for (const auto &m : layoutTagMap) { + if (m.tag == layout->mChannelLayoutTag) + return m.channelConfig; + } + + quint32 channels = 0; + if (layout->mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelDescriptions) { + // special case 1 and 2 channel configs, as they are often reported without proper descriptions + if (layout->mNumberChannelDescriptions == 1 + && (layout->mChannelDescriptions[0].mChannelLabel == kAudioChannelLabel_Unknown + || layout->mChannelDescriptions[0].mChannelLabel == kAudioChannelLabel_Mono)) + return QAudioFormat::ChannelConfigMono; + if (layout->mNumberChannelDescriptions == 2 && + layout->mChannelDescriptions[0].mChannelLabel == kAudioChannelLabel_Unknown && + layout->mChannelDescriptions[1].mChannelLabel == kAudioChannelLabel_Unknown) + return QAudioFormat::ChannelConfigStereo; + + for (uint i = 0; i < layout->mNumberChannelDescriptions; ++i) { + const auto channelLabel = layout->mChannelDescriptions[i].mChannelLabel; + if (channelLabel == kAudioChannelLabel_Unknown) { + // Any number of unknown channel labels occurs for loopback audio devices. + // E.g. the case is reproduced with installed software Soundflower. + continue; + } + + const auto found = std::find_if(channelMap, std::end(channelMap), + [channelLabel](const auto &labelWithPos) { + return labelWithPos.label == channelLabel; + }); + + if (found == std::end(channelMap)) + qWarning() << "audio device has unrecognized channel, index:" << i + << "label:" << channelLabel; + else + channels |= QAudioFormat::channelConfig(found->pos); + } + } else { + qWarning() << "Channel layout uses unimplemented format, channelLayoutTag:" + << layout->mChannelLayoutTag; + } + return QAudioFormat::ChannelConfig(channels); +} + +// QAudioRingBuffer +CoreAudioRingBuffer::CoreAudioRingBuffer(int bufferSize): + m_bufferSize(bufferSize) +{ + m_buffer = new char[m_bufferSize]; + reset(); +} + +CoreAudioRingBuffer::~CoreAudioRingBuffer() +{ + delete[] m_buffer; +} + +CoreAudioRingBuffer::Region CoreAudioRingBuffer::acquireReadRegion(int size) +{ + const int used = m_bufferUsed.fetchAndAddAcquire(0); + + if (used > 0) { + const int readSize = qMin(size, qMin(m_bufferSize - m_readPos, used)); + + return readSize > 0 ? Region(m_buffer + m_readPos, readSize) : Region(0, 0); + } + + return Region(0, 0); +} + +void CoreAudioRingBuffer::releaseReadRegion(const CoreAudioRingBuffer::Region ®ion) +{ + m_readPos = (m_readPos + region.second) % m_bufferSize; + + m_bufferUsed.fetchAndAddRelease(-region.second); +} + +CoreAudioRingBuffer::Region CoreAudioRingBuffer::acquireWriteRegion(int size) +{ + const int free = m_bufferSize - m_bufferUsed.fetchAndAddAcquire(0); + + Region output; + + if (free > 0) { + const int writeSize = qMin(size, qMin(m_bufferSize - m_writePos, free)); + output = writeSize > 0 ? Region(m_buffer + m_writePos, writeSize) : Region(0, 0); + } else { + output = Region(0, 0); + } +#ifdef QT_DEBUG_COREAUDIO + qDebug("acquireWriteRegion(%d) free: %d returning Region(%p, %d)", size, free, output.first, output.second); +#endif + return output; +} +void CoreAudioRingBuffer::releaseWriteRegion(const CoreAudioRingBuffer::Region ®ion) +{ + m_writePos = (m_writePos + region.second) % m_bufferSize; + + m_bufferUsed.fetchAndAddRelease(region.second); +#ifdef QT_DEBUG_COREAUDIO + qDebug("releaseWriteRegion(%p,%d): m_writePos:%d", region.first, region.second, m_writePos); +#endif +} + +int CoreAudioRingBuffer::used() const +{ + return m_bufferUsed.loadRelaxed(); +} + +int CoreAudioRingBuffer::free() const +{ + return m_bufferSize - m_bufferUsed.loadRelaxed(); +} + +int CoreAudioRingBuffer::size() const +{ + return m_bufferSize; +} + +void CoreAudioRingBuffer::reset() +{ + m_readPos = 0; + m_writePos = 0; + m_bufferUsed.storeRelaxed(0); +} + +QT_END_NAMESPACE diff --git a/src/multimedia/darwin/qcoreaudioutils_p.h b/src/multimedia/darwin/qcoreaudioutils_p.h new file mode 100644 index 000000000..94b6298f3 --- /dev/null +++ b/src/multimedia/darwin/qcoreaudioutils_p.h @@ -0,0 +1,72 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef IOSAUDIOUTILS_H +#define IOSAUDIOUTILS_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <CoreAudio/CoreAudioTypes.h> + +#include <QtMultimedia/QAudioFormat> +#include <QtCore/qglobal.h> + +QT_BEGIN_NAMESPACE + +class CoreAudioUtils +{ +public: + static quint64 currentTime(); + static double frequency(); + static Q_MULTIMEDIA_EXPORT QAudioFormat toQAudioFormat(const AudioStreamBasicDescription& streamFormat); + static AudioStreamBasicDescription toAudioStreamBasicDescription(QAudioFormat const& audioFormat); + + // ownership is transferred to caller, free with ::free() + static Q_MULTIMEDIA_EXPORT std::unique_ptr<AudioChannelLayout> toAudioChannelLayout(const QAudioFormat &format, UInt32 *size); + static QAudioFormat::ChannelConfig fromAudioChannelLayout(const AudioChannelLayout *layout); + +private: + static void initialize(); + static double sFrequency; + static bool sIsInitialized; +}; + +class CoreAudioRingBuffer +{ +public: + typedef QPair<char*, int> Region; + + CoreAudioRingBuffer(int bufferSize); + ~CoreAudioRingBuffer(); + + Region acquireReadRegion(int size); + void releaseReadRegion(Region const& region); + Region acquireWriteRegion(int size); + void releaseWriteRegion(Region const& region); + + int used() const; + int free() const; + int size() const; + + void reset(); + +private: + int m_bufferSize; + int m_readPos; + int m_writePos; + char* m_buffer; + QAtomicInt m_bufferUsed; +}; + +QT_END_NAMESPACE + +#endif // IOSAUDIOUTILS_H diff --git a/src/multimedia/darwin/qdarwinaudiodevice.mm b/src/multimedia/darwin/qdarwinaudiodevice.mm new file mode 100644 index 000000000..8374f4cea --- /dev/null +++ b/src/multimedia/darwin/qdarwinaudiodevice.mm @@ -0,0 +1,108 @@ +// Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdarwinaudiodevice_p.h" +#include "qcoreaudioutils_p.h" +#include <private/qcore_mac_p.h> + +#if defined(Q_OS_IOS) +#include "qcoreaudiosessionmanager_p.h" +#else +#include "qmacosaudiodatautils_p.h" +#endif + +#include <QtCore/QDataStream> +#include <QtCore/QDebug> +#include <QtCore/QSet> +#include <QIODevice> + +QT_BEGIN_NAMESPACE + +#if defined(Q_OS_MACOS) +QCoreAudioDeviceInfo::QCoreAudioDeviceInfo(AudioDeviceID id, const QByteArray &device, QAudioDevice::Mode mode) + : QAudioDevicePrivate(device, mode), + m_deviceId(id) +#else +QCoreAudioDeviceInfo::QCoreAudioDeviceInfo(const QByteArray &device, QAudioDevice::Mode mode) + : QAudioDevicePrivate(device, mode) +#endif +{ + description = getDescription(); + getChannelLayout(); + preferredFormat = determinePreferredFormat(); + minimumSampleRate = 1; + maximumSampleRate = 96000; + minimumChannelCount = 1; + maximumChannelCount = 16; + supportedSampleFormats << QAudioFormat::UInt8 << QAudioFormat::Int16 << QAudioFormat::Int32 << QAudioFormat::Float; + +} + +QAudioFormat QCoreAudioDeviceInfo::determinePreferredFormat() const +{ + QAudioFormat format; + +#if defined(Q_OS_MACOS) + const auto audioDevicePropertyStreamsAddress = + makePropertyAddress(kAudioDevicePropertyStreams, mode); + + if (auto streamIDs = getAudioData<AudioStreamID>(m_deviceId, audioDevicePropertyStreamsAddress, + "propertyStreams")) { + const auto audioDevicePhysicalFormatPropertyAddress = + makePropertyAddress(kAudioStreamPropertyPhysicalFormat, mode); + + for (auto streamID : *streamIDs) { + if (auto streamDescription = getAudioObject<AudioStreamBasicDescription>( + streamID, audioDevicePhysicalFormatPropertyAddress, + "prefferedPhysicalFormat")) { + format = CoreAudioUtils::toQAudioFormat(*streamDescription); + break; + } + } + } + + if (!format.isValid()) +#endif + { + format.setSampleRate(44100); + format.setSampleFormat(QAudioFormat::Int16); + format.setChannelCount(mode == QAudioDevice::Input ? 1 : 2); + } + format.setChannelConfig(channelConfiguration); + + return format; +} + + +QString QCoreAudioDeviceInfo::getDescription() const +{ +#ifdef Q_OS_MACOS + const auto propertyAddress = makePropertyAddress(kAudioObjectPropertyName, mode); + if (auto name = + getAudioObject<CFStringRef>(m_deviceId, propertyAddress, "Device Description")) { + auto deleter = qScopeGuard([&name]() { CFRelease(*name); }); + return QString::fromCFString(*name); + } + + return {}; +#else + return QString::fromUtf8(id); +#endif +} + +void QCoreAudioDeviceInfo::getChannelLayout() +{ +#ifdef Q_OS_MACOS + const auto propertyAddress = + makePropertyAddress(kAudioDevicePropertyPreferredChannelLayout, mode); + if (auto data = getAudioData<char>(m_deviceId, propertyAddress, "prefferedChannelLayout", + sizeof(AudioChannelLayout))) { + const auto *layout = reinterpret_cast<const AudioChannelLayout *>(data->data()); + channelConfiguration = CoreAudioUtils::fromAudioChannelLayout(layout); + } +#else + channelConfiguration = (mode == QAudioDevice::Input) ? QAudioFormat::ChannelConfigMono : QAudioFormat::ChannelConfigStereo; +#endif +} + +QT_END_NAMESPACE diff --git a/src/multimedia/darwin/qdarwinaudiodevice_p.h b/src/multimedia/darwin/qdarwinaudiodevice_p.h new file mode 100644 index 000000000..637c5d197 --- /dev/null +++ b/src/multimedia/darwin/qdarwinaudiodevice_p.h @@ -0,0 +1,52 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef IOSAUDIODEVICEINFO_H +#define IOSAUDIODEVICEINFO_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qaudiosystem_p.h> +#include <private/qaudiodevice_p.h> + +#if defined(Q_OS_MACOS) +# include <CoreAudio/CoreAudio.h> +#endif + +QT_BEGIN_NAMESPACE + +class QCoreAudioDeviceInfo : public QAudioDevicePrivate +{ +public: +#if defined(Q_OS_MACOS) + QCoreAudioDeviceInfo(AudioDeviceID id, const QByteArray &device, QAudioDevice::Mode mode); +#else + QCoreAudioDeviceInfo(const QByteArray &device, QAudioDevice::Mode mode); +#endif + ~QCoreAudioDeviceInfo() {} + + bool isFormatSupported(const QAudioFormat &format) const; + +#if defined(Q_OS_MACOS) + AudioDeviceID deviceID() const { return m_deviceId; } +#endif +private: + QAudioFormat determinePreferredFormat() const; + QString getDescription() const; + void getChannelLayout(); +#if defined(Q_OS_MACOS) + AudioDeviceID m_deviceId; +#endif +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/darwin/qdarwinaudiosink.mm b/src/multimedia/darwin/qdarwinaudiosink.mm new file mode 100644 index 000000000..9242bb2f0 --- /dev/null +++ b/src/multimedia/darwin/qdarwinaudiosink.mm @@ -0,0 +1,609 @@ +// Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "qdarwinaudiosink_p.h" +#include "qcoreaudiosessionmanager_p.h" +#include "qdarwinaudiodevice_p.h" +#include "qcoreaudioutils_p.h" +#include "qdarwinmediadevices_p.h" +#include <qmediadevices.h> + +#include <QtCore/QDataStream> +#include <QtCore/QTimer> +#include <QtCore/QDebug> + +#include <AudioUnit/AudioUnit.h> +#include <AudioToolbox/AudioToolbox.h> +#if defined(Q_OS_MACOS) +# include <AudioUnit/AudioComponent.h> +#endif + +#if defined(Q_OS_IOS) || defined(Q_OS_TVOS) +# include <QtMultimedia/private/qaudiohelpers_p.h> +#endif + +QT_BEGIN_NAMESPACE + +static int audioRingBufferSize(int bufferSize, int maxPeriodSize) +{ + // TODO: review this code + return bufferSize + + (bufferSize % maxPeriodSize == 0 ? 0 : maxPeriodSize - (bufferSize % maxPeriodSize)); +} + +QDarwinAudioSinkBuffer::QDarwinAudioSinkBuffer(int bufferSize, int maxPeriodSize, + const QAudioFormat &audioFormat) + : m_maxPeriodSize(maxPeriodSize), + m_bytesPerFrame(audioFormat.bytesPerFrame()), + m_periodTime(maxPeriodSize / m_bytesPerFrame * 1000 / audioFormat.sampleRate()), + m_buffer( + std::make_unique<CoreAudioRingBuffer>(audioRingBufferSize(bufferSize, maxPeriodSize))) +{ + m_fillTimer = new QTimer(this); + m_fillTimer->setTimerType(Qt::PreciseTimer); + m_fillTimer->setInterval(m_buffer->size() / 2 / m_maxPeriodSize * m_periodTime); + connect(m_fillTimer, &QTimer::timeout, this, &QDarwinAudioSinkBuffer::fillBuffer); +} + +QDarwinAudioSinkBuffer::~QDarwinAudioSinkBuffer() = default; + +qint64 QDarwinAudioSinkBuffer::readFrames(char *data, qint64 maxFrames) +{ + bool wecan = true; + qint64 framesRead = 0; + + while (wecan && framesRead < maxFrames) { + CoreAudioRingBuffer::Region region = m_buffer->acquireReadRegion((maxFrames - framesRead) * m_bytesPerFrame); + + if (region.second > 0) { + // Ensure that we only read whole frames. + region.second -= region.second % m_bytesPerFrame; + + if (region.second > 0) { + memcpy(data + (framesRead * m_bytesPerFrame), region.first, region.second); + framesRead += region.second / m_bytesPerFrame; + } else + wecan = false; // If there is only a partial frame left we should exit. + } + else + wecan = false; + + m_buffer->releaseReadRegion(region); + } + + if (framesRead == 0 && m_deviceError) + framesRead = -1; + + return framesRead; +} + +qint64 QDarwinAudioSinkBuffer::writeBytes(const char *data, qint64 maxSize) +{ + bool wecan = true; + qint64 bytesWritten = 0; + + maxSize -= maxSize % m_bytesPerFrame; + while (wecan && bytesWritten < maxSize) { + CoreAudioRingBuffer::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 QDarwinAudioSinkBuffer::available() const +{ + return m_buffer->free(); +} + +bool QDarwinAudioSinkBuffer::deviceAtEnd() const +{ + return m_deviceAtEnd; +} + +void QDarwinAudioSinkBuffer::reset() +{ + m_buffer->reset(); + setFillingEnabled(false); + setPrefetchDevice(nullptr); +} + +void QDarwinAudioSinkBuffer::setPrefetchDevice(QIODevice *device) +{ + if (std::exchange(m_device, device) == device) + return; + + m_deviceError = false; + m_deviceAtEnd = device && device->atEnd(); + const auto wasFillingEnabled = m_fillingEnabled; + setFillingEnabled(false); + setFillingEnabled(wasFillingEnabled); +} + +QIODevice *QDarwinAudioSinkBuffer::prefetchDevice() const +{ + return m_device; +} + +void QDarwinAudioSinkBuffer::setFillingEnabled(bool enabled) +{ + if (std::exchange(m_fillingEnabled, enabled) == enabled) + return; + + if (!enabled) + m_fillTimer->stop(); + else if (m_device) { + fillBuffer(); + m_fillTimer->start(); + } +} + +void QDarwinAudioSinkBuffer::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) { + CoreAudioRingBuffer::Region region = m_buffer->acquireWriteRegion(writeSize - filled); + + if (region.second > 0) { + region.second = m_device->read(region.first, region.second); + m_deviceAtEnd = m_device->atEnd(); + if (region.second > 0) + filled += region.second; + else if (region.second == 0) + wecan = false; + else if (region.second < 0) { + setFillingEnabled(false); + region.second = 0; + m_deviceError = true; + } + } + else + wecan = false; + + m_buffer->releaseWriteRegion(region); + } + + if (filled > 0) + emit readyRead(); + } +} + +QDarwinAudioSinkDevice::QDarwinAudioSinkDevice(QDarwinAudioSinkBuffer *audioBuffer, QObject *parent) + : QIODevice(parent) + , m_audioBuffer(audioBuffer) +{ + open(QIODevice::WriteOnly | QIODevice::Unbuffered); +} + +qint64 QDarwinAudioSinkDevice::readData(char *data, qint64 len) +{ + Q_UNUSED(data); + Q_UNUSED(len); + + return 0; +} + +qint64 QDarwinAudioSinkDevice::writeData(const char *data, qint64 len) +{ + return m_audioBuffer->writeBytes(data, len); +} + +QDarwinAudioSink::QDarwinAudioSink(const QAudioDevice &device, QObject *parent) + : QPlatformAudioSink(parent), m_audioDeviceInfo(device), m_stateMachine(*this) +{ + QAudioDevice di = device; + if (di.isNull()) + di = QMediaDevices::defaultAudioOutput(); +#if defined(Q_OS_MACOS) + const QCoreAudioDeviceInfo *info = static_cast<const QCoreAudioDeviceInfo *>(di.handle()); + Q_ASSERT(info); + m_audioDeviceId = info->deviceID(); +#endif + m_device = di.id(); + + m_clockFrequency = CoreAudioUtils::frequency() / 1000; + + connect(this, &QDarwinAudioSink::stateChanged, this, &QDarwinAudioSink::updateAudioDevice); +} + +QDarwinAudioSink::~QDarwinAudioSink() +{ + close(); +} + +void QDarwinAudioSink::start(QIODevice *device) +{ + reset(); + + if (!m_audioDeviceInfo.isFormatSupported(m_audioFormat) || !open()) { + m_stateMachine.setError(QAudio::OpenError); + return; + } + + if (!device) { + m_stateMachine.setError(QAudio::IOError); + return; + } + + m_audioBuffer->setPrefetchDevice(device); + + m_pullMode = true; + m_totalFrames = 0; + + m_stateMachine.start(); +} + +QIODevice *QDarwinAudioSink::start() +{ + reset(); + + if (!m_audioDeviceInfo.isFormatSupported(m_audioFormat) || !open()) { + m_stateMachine.setError(QAudio::OpenError); + return m_audioIO; + } + + m_pullMode = false; + m_totalFrames = 0; + + m_stateMachine.start(false); + + return m_audioIO; +} + +void QDarwinAudioSink::stop() +{ + if (m_audioBuffer) + m_audioBuffer->setFillingEnabled(false); + + if (auto notifier = m_stateMachine.stop(QAudio::NoError, true)) { + Q_ASSERT((notifier.prevAudioState() == QAudio::ActiveState) == notifier.isDraining()); + if (notifier.isDraining() && !m_drainSemaphore.tryAcquire(1, 500)) { + const bool wasDraining = m_stateMachine.onDrained(); + + qWarning() << "Failed wait for getting sink drained; was draining:" << wasDraining; + + // handle a rare corner case when the audio thread managed to release the semaphore in + // the time window between tryAcquire and onDrained + if (!wasDraining) + m_drainSemaphore.acquire(); + } + } +} + +void QDarwinAudioSink::reset() +{ + onAudioDeviceDrained(); + m_stateMachine.stopOrUpdateError(); +} + +void QDarwinAudioSink::suspend() +{ + m_stateMachine.suspend(); +} + +void QDarwinAudioSink::resume() +{ + m_stateMachine.resume(); +} + +qsizetype QDarwinAudioSink::bytesFree() const +{ + return m_audioBuffer->available(); +} + +void QDarwinAudioSink::setBufferSize(qsizetype value) +{ + if (state() == QAudio::StoppedState) + m_internalBufferSize = value; +} + +qsizetype QDarwinAudioSink::bufferSize() const +{ + return m_internalBufferSize; +} + +qint64 QDarwinAudioSink::processedUSecs() const +{ + return m_totalFrames * 1000000 / m_audioFormat.sampleRate(); +} + +QAudio::Error QDarwinAudioSink::error() const +{ + return m_stateMachine.error(); +} + +QAudio::State QDarwinAudioSink::state() const +{ + return m_stateMachine.state(); +} + +void QDarwinAudioSink::setFormat(const QAudioFormat &format) +{ + if (state() == QAudio::StoppedState) + m_audioFormat = format; +} + +QAudioFormat QDarwinAudioSink::format() const +{ + return m_audioFormat; +} + +void QDarwinAudioSink::setVolume(qreal volume) +{ + m_cachedVolume = qBound(qreal(0.0), volume, qreal(1.0)); + if (!m_isOpen) + return; + +#if defined(Q_OS_MACOS) + //on OS X the volume can be set directly on the AudioUnit + if (AudioUnitSetParameter(m_audioUnit, + kHALOutputParam_Volume, + kAudioUnitScope_Global, + 0 /* bus */, + m_cachedVolume, + 0) == noErr) + m_volume = m_cachedVolume; +#endif +} + +qreal QDarwinAudioSink::volume() const +{ + return m_cachedVolume; +} + +void QDarwinAudioSink::inputReady() +{ + m_stateMachine.activateFromIdle(); +} + +OSStatus QDarwinAudioSink::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); + + QDarwinAudioSink* d = static_cast<QDarwinAudioSink*>(inRefCon); + + const auto [drained, stopped] = d->m_stateMachine.getDrainedAndStopped(); + + if (drained && stopped) { + ioData->mBuffers[0].mDataByteSize = 0; + } else { + const UInt32 bytesPerFrame = d->m_streamFormat.mBytesPerFrame; + qint64 framesRead; + + Q_ASSERT(ioData->mBuffers[0].mDataByteSize / bytesPerFrame == inNumberFrames); + framesRead = d->m_audioBuffer->readFrames((char*)ioData->mBuffers[0].mData, + ioData->mBuffers[0].mDataByteSize / bytesPerFrame); + + if (framesRead > 0) { + ioData->mBuffers[0].mDataByteSize = framesRead * bytesPerFrame; + d->m_totalFrames += framesRead; + +#if defined(Q_OS_MACOS) + if (stopped) { + qreal oldVolume = d->m_cachedVolume; + // Decrease volume smoothly. + d->setVolume(d->m_volume / 2); + d->m_cachedVolume = oldVolume; + } +#elif defined(Q_OS_IOS) || defined(Q_OS_TVOS) + // on iOS we have to adjust the sound volume ourselves + if (!qFuzzyCompare(d->m_cachedVolume, qreal(1.0f))) { + QAudioHelperInternal::qMultiplySamples(d->m_cachedVolume, + d->m_audioFormat, + ioData->mBuffers[0].mData, /* input */ + ioData->mBuffers[0].mData, /* output */ + ioData->mBuffers[0].mDataByteSize); + } +#endif + + } + else { + ioData->mBuffers[0].mDataByteSize = 0; + if (framesRead == 0) + d->onAudioDeviceIdle(); + else + d->onAudioDeviceError(); + } + } + + return noErr; +} + +bool QDarwinAudioSink::open() +{ +#if defined(Q_OS_IOS) + // Set default category to Ambient (implies MixWithOthers). This makes sure audio stops playing + // if the screen is locked or if the Silent switch is toggled. + CoreAudioSessionManager::instance().setCategory(CoreAudioSessionManager::Ambient, CoreAudioSessionManager::None); + CoreAudioSessionManager::instance().setActive(true); +#endif + + if (error() != QAudio::NoError) + return false; + + if (m_isOpen) { + setVolume(m_cachedVolume); + return true; + } + + AudioComponentDescription componentDescription; + componentDescription.componentType = kAudioUnitType_Output; +#if defined(Q_OS_MACOS) + componentDescription.componentSubType = kAudioUnitSubType_HALOutput; +#else + componentDescription.componentSubType = kAudioUnitSubType_RemoteIO; +#endif + componentDescription.componentManufacturer = kAudioUnitManufacturer_Apple; + componentDescription.componentFlags = 0; + componentDescription.componentFlagsMask = 0; + + AudioComponent component = AudioComponentFindNext(0, &componentDescription); + if (component == 0) { + qWarning() << "QAudioOutput: Failed to find Output component"; + return false; + } + + if (AudioComponentInstanceNew(component, &m_audioUnit) != noErr) { + qWarning() << "QAudioOutput: Unable to Open Output Component"; + return false; + } + + // register callback + AURenderCallbackStruct callback; + callback.inputProc = renderCallback; + callback.inputProcRefCon = this; + + if (AudioUnitSetProperty(m_audioUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Global, + 0, + &callback, + sizeof(callback)) != noErr) { + qWarning() << "QAudioOutput: Failed to set AudioUnit callback"; + return false; + } + +#if defined(Q_OS_MACOS) + //Set Audio Device + if (AudioUnitSetProperty(m_audioUnit, + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, + 0, + &m_audioDeviceId, + sizeof(m_audioDeviceId)) != noErr) { + qWarning() << "QAudioOutput: Unable to use configured device"; + return false; + } +#endif + UInt32 size; + + + // Set stream format + m_streamFormat = CoreAudioUtils::toAudioStreamBasicDescription(m_audioFormat); + size = sizeof(m_streamFormat); + + if (AudioUnitSetProperty(m_audioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + 0, + &m_streamFormat, + size) != noErr) { + qWarning() << "QAudioOutput: Unable to Set Stream information"; + return false; + } + + // Allocate buffer + UInt32 numberOfFrames = 0; +#if defined(Q_OS_MACOS) + size = sizeof(UInt32); + if (AudioUnitGetProperty(m_audioUnit, + kAudioDevicePropertyBufferFrameSize, + kAudioUnitScope_Global, + 0, + &numberOfFrames, + &size) != noErr) { + qWarning() << "QAudioSource: Failed to get audio period size"; + return false; + } +#else //iOS + Float32 bufferSize = CoreAudioSessionManager::instance().currentIOBufferDuration(); + bufferSize *= m_streamFormat.mSampleRate; + numberOfFrames = bufferSize; +#endif + + m_periodSizeBytes = numberOfFrames * m_streamFormat.mBytesPerFrame; + if (m_internalBufferSize < m_periodSizeBytes * 2) + m_internalBufferSize = m_periodSizeBytes * 2; + else + m_internalBufferSize -= m_internalBufferSize % m_streamFormat.mBytesPerFrame; + + m_audioBuffer = std::make_unique<QDarwinAudioSinkBuffer>(m_internalBufferSize, + m_periodSizeBytes, m_audioFormat); + connect(m_audioBuffer.get(), &QDarwinAudioSinkBuffer::readyRead, this, + &QDarwinAudioSink::inputReady); // Pull + + m_audioIO = new QDarwinAudioSinkDevice(m_audioBuffer.get(), this); + + //Init + if (AudioUnitInitialize(m_audioUnit)) { + qWarning() << "QAudioOutput: Failed to initialize AudioUnit"; + return false; + } + + m_isOpen = true; + + setVolume(m_cachedVolume); + + return true; +} + +void QDarwinAudioSink::close() +{ + if (m_audioUnit != 0) { + m_stateMachine.stop(); + + AudioUnitUninitialize(m_audioUnit); + AudioComponentInstanceDispose(m_audioUnit); + m_audioUnit = 0; + } + + m_audioBuffer.reset(); +} + +void QDarwinAudioSink::onAudioDeviceIdle() +{ + const bool atEnd = m_audioBuffer->deviceAtEnd(); + if (!m_stateMachine.updateActiveOrIdle(false, atEnd ? QAudio::NoError : QAudio::UnderrunError)) + onAudioDeviceDrained(); +} + +void QDarwinAudioSink::onAudioDeviceDrained() +{ + if (m_stateMachine.onDrained()) + m_drainSemaphore.release(); +} + +void QDarwinAudioSink::onAudioDeviceError() +{ + m_stateMachine.stop(QAudio::IOError); +} + +void QDarwinAudioSink::updateAudioDevice() +{ + const auto state = m_stateMachine.state(); + + Q_ASSERT(m_audioBuffer); + Q_ASSERT(m_audioUnit); + + if (state == QAudio::StoppedState) + m_audioBuffer->reset(); + else + m_audioBuffer->setFillingEnabled(state != QAudio::SuspendedState); + + const bool unitStarted = state == QAudio::ActiveState; + if (std::exchange(m_audioUnitStarted, unitStarted) != unitStarted) + (unitStarted ? AudioOutputUnitStart : AudioOutputUnitStop)(m_audioUnit); +} + +QT_END_NAMESPACE + +#include "moc_qdarwinaudiosink_p.cpp" diff --git a/src/multimedia/darwin/qdarwinaudiosink_p.h b/src/multimedia/darwin/qdarwinaudiosink_p.h new file mode 100644 index 000000000..1fddcb205 --- /dev/null +++ b/src/multimedia/darwin/qdarwinaudiosink_p.h @@ -0,0 +1,170 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef IOSAUDIOOUTPUT_H +#define IOSAUDIOOUTPUT_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qaudiosystem_p.h> +#include <private/qaudiostatemachine_p.h> + +#if defined(Q_OS_MACOS) +# include <CoreAudio/CoreAudio.h> +#endif +#include <AudioUnit/AudioUnit.h> +#include <CoreAudio/CoreAudioTypes.h> + +#include <QtCore/QIODevice> +#include <qdarwinaudiodevice_p.h> +#include <qsemaphore.h> + +QT_BEGIN_NAMESPACE + +class QDarwinAudioSinkBuffer; +class QTimer; +class QCoreAudioDeviceInfo; +class CoreAudioRingBuffer; + +class QDarwinAudioSinkBuffer : public QObject +{ + Q_OBJECT + +public: + QDarwinAudioSinkBuffer(int bufferSize, int maxPeriodSize, QAudioFormat const& audioFormat); + ~QDarwinAudioSinkBuffer(); + + qint64 readFrames(char *data, qint64 maxFrames); + qint64 writeBytes(const char *data, qint64 maxSize); + + int available() const; + + bool deviceAtEnd() const; + + void reset(); + + void setPrefetchDevice(QIODevice *device); + + QIODevice *prefetchDevice() const; + + void setFillingEnabled(bool enabled); + +signals: + void readyRead(); + +private slots: + void fillBuffer(); + +private: + bool m_deviceError = false; + bool m_fillingEnabled = false; + bool m_deviceAtEnd = false; + const int m_maxPeriodSize = 0; + const int m_bytesPerFrame = 0; + const int m_periodTime = 0; + QIODevice *m_device = nullptr; + QTimer *m_fillTimer = nullptr; + std::unique_ptr<CoreAudioRingBuffer> m_buffer; +}; + +class QDarwinAudioSinkDevice : public QIODevice +{ +public: + QDarwinAudioSinkDevice(QDarwinAudioSinkBuffer *audioBuffer, QObject *parent); + + qint64 readData(char *data, qint64 len); + qint64 writeData(const char *data, qint64 len); + + bool isSequential() const { return true; } + +private: + QDarwinAudioSinkBuffer *m_audioBuffer; +}; + + +class QDarwinAudioSink : public QPlatformAudioSink +{ + Q_OBJECT + +public: + QDarwinAudioSink(const QAudioDevice &device, QObject *parent); + ~QDarwinAudioSink(); + + void start(QIODevice *device); + QIODevice *start(); + void stop(); + void reset(); + void suspend(); + void resume(); + qsizetype bytesFree() const; + void setBufferSize(qsizetype value); + qsizetype bufferSize() const; + qint64 processedUSecs() const; + QAudio::Error error() const; + QAudio::State state() const; + void setFormat(const QAudioFormat &format); + QAudioFormat format() const; + + void setVolume(qreal volume); + qreal volume() const; + +private slots: + void inputReady(); + void updateAudioDevice(); + +private: + enum ThreadState { Running, Draining, Stopped }; + + static OSStatus renderCallback(void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumberFrames, + AudioBufferList *ioData); + + bool open(); + void close(); + void onAudioDeviceIdle(); + void onAudioDeviceError(); + void onAudioDeviceDrained(); + + QAudioDevice m_audioDeviceInfo; + QByteArray m_device; + + static constexpr int DEFAULT_BUFFER_SIZE = 8 * 1024; + + bool m_isOpen = false; + int m_internalBufferSize = DEFAULT_BUFFER_SIZE; + int m_periodSizeBytes = 0; + qint64 m_totalFrames = 0; + QAudioFormat m_audioFormat; + QIODevice *m_audioIO = nullptr; +#if defined(Q_OS_MACOS) + AudioDeviceID m_audioDeviceId; +#endif + AudioUnit m_audioUnit = 0; + bool m_audioUnitStarted = false; + Float64 m_clockFrequency = 0; + AudioStreamBasicDescription m_streamFormat; + std::unique_ptr<QDarwinAudioSinkBuffer> m_audioBuffer; + qreal m_cachedVolume = 1.; +#if defined(Q_OS_MACOS) + qreal m_volume = 1.; +#endif + bool m_pullMode = false; + + QAudioStateMachine m_stateMachine; + QSemaphore m_drainSemaphore; +}; + +QT_END_NAMESPACE + +#endif // IOSAUDIOOUTPUT_H diff --git a/src/multimedia/darwin/qdarwinaudiosource.mm b/src/multimedia/darwin/qdarwinaudiosource.mm new file mode 100644 index 000000000..4c1345fb8 --- /dev/null +++ b/src/multimedia/darwin/qdarwinaudiosource.mm @@ -0,0 +1,942 @@ +// Copyright (C) 2022 The Qt Company Ltd and/or its subsidiary(-ies). +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "qdarwinaudiosource_p.h" +#include "qcoreaudiosessionmanager_p.h" +#include "qdarwinaudiodevice_p.h" +#include "qcoreaudioutils_p.h" +#include "qdarwinmediadevices_p.h" +#include <qmediadevices.h> + +#if defined(Q_OS_MACOS) +# include <AudioUnit/AudioComponent.h> +#endif + +#if defined(Q_OS_IOS) || defined(Q_OS_TVOS) +# include "qcoreaudiosessionmanager_p.h" +#endif + +#include <QtMultimedia/private/qaudiohelpers_p.h> +#include <QtCore/QDataStream> +#include <QtCore/QDebug> + +QT_BEGIN_NAMESPACE + +static const int DEFAULT_BUFFER_SIZE = 4 * 1024; + +QCoreAudioBufferList::QCoreAudioBufferList(const AudioStreamBasicDescription &streamFormat) + : m_owner(false) + , m_streamDescription(streamFormat) +{ + const bool isInterleaved = (m_streamDescription.mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0; + const int numberOfBuffers = isInterleaved ? 1 : m_streamDescription.mChannelsPerFrame; + + m_dataSize = 0; + + m_bufferList = reinterpret_cast<AudioBufferList*>(malloc(sizeof(AudioBufferList) + + (sizeof(AudioBuffer) * numberOfBuffers))); + + m_bufferList->mNumberBuffers = numberOfBuffers; + for (int i = 0; i < numberOfBuffers; ++i) { + m_bufferList->mBuffers[i].mNumberChannels = isInterleaved ? numberOfBuffers : 1; + m_bufferList->mBuffers[i].mDataByteSize = 0; + m_bufferList->mBuffers[i].mData = 0; + } +} + +QCoreAudioBufferList::QCoreAudioBufferList(const AudioStreamBasicDescription &streamFormat, char *buffer, int bufferSize) + : m_owner(false) + , m_streamDescription(streamFormat) + , m_bufferList(0) +{ + m_dataSize = bufferSize; + + m_bufferList = reinterpret_cast<AudioBufferList*>(malloc(sizeof(AudioBufferList) + sizeof(AudioBuffer))); + + m_bufferList->mNumberBuffers = 1; + m_bufferList->mBuffers[0].mNumberChannels = 1; + m_bufferList->mBuffers[0].mDataByteSize = m_dataSize; + m_bufferList->mBuffers[0].mData = buffer; +} + +QCoreAudioBufferList::QCoreAudioBufferList(const AudioStreamBasicDescription &streamFormat, int framesToBuffer) + : m_owner(true) + , m_streamDescription(streamFormat) + , m_bufferList(0) +{ + const bool isInterleaved = (m_streamDescription.mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0; + const int numberOfBuffers = isInterleaved ? 1 : m_streamDescription.mChannelsPerFrame; + + m_dataSize = framesToBuffer * m_streamDescription.mBytesPerFrame; + + m_bufferList = reinterpret_cast<AudioBufferList*>(malloc(sizeof(AudioBufferList) + + (sizeof(AudioBuffer) * numberOfBuffers))); + m_bufferList->mNumberBuffers = numberOfBuffers; + for (int i = 0; i < numberOfBuffers; ++i) { + m_bufferList->mBuffers[i].mNumberChannels = isInterleaved ? numberOfBuffers : 1; + m_bufferList->mBuffers[i].mDataByteSize = m_dataSize; + m_bufferList->mBuffers[i].mData = malloc(m_dataSize); + } +} + +QCoreAudioBufferList::~QCoreAudioBufferList() +{ + if (m_owner) { + for (UInt32 i = 0; i < m_bufferList->mNumberBuffers; ++i) + free(m_bufferList->mBuffers[i].mData); + } + + free(m_bufferList); +} + +char *QCoreAudioBufferList::data(int buffer) const +{ + return static_cast<char*>(m_bufferList->mBuffers[buffer].mData); +} + +qint64 QCoreAudioBufferList::bufferSize(int buffer) const +{ + return m_bufferList->mBuffers[buffer].mDataByteSize; +} + +int QCoreAudioBufferList::frameCount(int buffer) const +{ + return m_bufferList->mBuffers[buffer].mDataByteSize / m_streamDescription.mBytesPerFrame; +} + +int QCoreAudioBufferList::packetCount(int buffer) const +{ + return m_bufferList->mBuffers[buffer].mDataByteSize / m_streamDescription.mBytesPerPacket; +} + +int QCoreAudioBufferList::packetSize() const +{ + return m_streamDescription.mBytesPerPacket; +} + +void QCoreAudioBufferList::reset() +{ + for (UInt32 i = 0; i < m_bufferList->mNumberBuffers; ++i) { + m_bufferList->mBuffers[i].mDataByteSize = m_dataSize; + m_bufferList->mBuffers[i].mData = 0; + } +} + +QCoreAudioPacketFeeder::QCoreAudioPacketFeeder(QCoreAudioBufferList *abl) + : m_audioBufferList(abl) +{ + m_totalPackets = m_audioBufferList->packetCount(); + m_position = 0; +} + +bool QCoreAudioPacketFeeder::feed(AudioBufferList &dst, UInt32 &packetCount) +{ + if (m_position == m_totalPackets) { + dst.mBuffers[0].mDataByteSize = 0; + packetCount = 0; + return false; + } + + if (m_totalPackets - m_position < packetCount) + packetCount = m_totalPackets - m_position; + + dst.mBuffers[0].mDataByteSize = packetCount * m_audioBufferList->packetSize(); + dst.mBuffers[0].mData = m_audioBufferList->data() + (m_position * m_audioBufferList->packetSize()); + + m_position += packetCount; + + return true; +} + +bool QCoreAudioPacketFeeder::empty() const +{ + return m_position == m_totalPackets; +} + +QDarwinAudioSourceBuffer::QDarwinAudioSourceBuffer(int bufferSize, int maxPeriodSize, const AudioStreamBasicDescription &inputFormat, const AudioStreamBasicDescription &outputFormat, QObject *parent) + : QObject(parent) + , m_deviceError(false) + , m_device(0) + , m_audioConverter(0) + , m_inputFormat(inputFormat) + , m_outputFormat(outputFormat) + , m_volume(qreal(1.0f)) +{ + m_maxPeriodSize = maxPeriodSize; + m_periodTime = m_maxPeriodSize / m_outputFormat.mBytesPerFrame * 1000 / m_outputFormat.mSampleRate; + + m_buffer = new CoreAudioRingBuffer(bufferSize); + + m_inputBufferList = new QCoreAudioBufferList(m_inputFormat); + + m_flushTimer = new QTimer(this); + connect(m_flushTimer, SIGNAL(timeout()), this, SLOT(flushBuffer())); + + if (CoreAudioUtils::toQAudioFormat(inputFormat) != CoreAudioUtils::toQAudioFormat(outputFormat)) { + if (AudioConverterNew(&m_inputFormat, &m_outputFormat, &m_audioConverter) != noErr) { + qWarning() << "QAudioSource: Unable to create an Audio Converter"; + m_audioConverter = 0; + } + } + + m_qFormat = CoreAudioUtils::toQAudioFormat(inputFormat); // we adjust volume before conversion +} + +QDarwinAudioSourceBuffer::~QDarwinAudioSourceBuffer() +{ + delete m_buffer; +} + +qreal QDarwinAudioSourceBuffer::volume() const +{ + return m_volume; +} + +void QDarwinAudioSourceBuffer::setVolume(qreal v) +{ + m_volume = v; +} + +qint64 QDarwinAudioSourceBuffer::renderFromDevice(AudioUnit audioUnit, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames) +{ + const bool pullMode = m_device == 0; + + OSStatus err; + qint64 framesRendered = 0; + + m_inputBufferList->reset(); + err = AudioUnitRender(audioUnit, + ioActionFlags, + inTimeStamp, + inBusNumber, + inNumberFrames, + m_inputBufferList->audioBufferList()); + + // adjust volume, if necessary + if (!qFuzzyCompare(m_volume, qreal(1.0f))) { + QAudioHelperInternal::qMultiplySamples(m_volume, + m_qFormat, + m_inputBufferList->data(), /* input */ + m_inputBufferList->data(), /* output */ + m_inputBufferList->bufferSize()); + } + + if (m_audioConverter != 0) { + QCoreAudioPacketFeeder feeder(m_inputBufferList); + + int copied = 0; + const int available = m_buffer->free(); + + while (err == noErr && !feeder.empty()) { + CoreAudioRingBuffer::Region region = m_buffer->acquireWriteRegion(available - copied); + + if (region.second == 0) + break; + + AudioBufferList output; + output.mNumberBuffers = 1; + output.mBuffers[0].mNumberChannels = 1; + output.mBuffers[0].mDataByteSize = region.second; + output.mBuffers[0].mData = region.first; + + UInt32 packetSize = region.second / m_outputFormat.mBytesPerPacket; + err = AudioConverterFillComplexBuffer(m_audioConverter, + converterCallback, + &feeder, + &packetSize, + &output, + 0); + region.second = output.mBuffers[0].mDataByteSize; + copied += region.second; + + m_buffer->releaseWriteRegion(region); + } + + framesRendered += copied / m_outputFormat.mBytesPerFrame; + } + else { + const int available = m_inputBufferList->bufferSize(); + bool wecan = true; + int copied = 0; + + while (wecan && copied < available) { + CoreAudioRingBuffer::Region region = m_buffer->acquireWriteRegion(available - copied); + + if (region.second > 0) { + memcpy(region.first, m_inputBufferList->data() + copied, region.second); + copied += region.second; + } + else + wecan = false; + + m_buffer->releaseWriteRegion(region); + } + + framesRendered = copied / m_outputFormat.mBytesPerFrame; + } + + if (pullMode && framesRendered > 0) + emit readyRead(); + + return framesRendered; +} + +qint64 QDarwinAudioSourceBuffer::readBytes(char *data, qint64 len) +{ + bool wecan = true; + qint64 bytesCopied = 0; + + len -= len % m_maxPeriodSize; + while (wecan && bytesCopied < len) { + CoreAudioRingBuffer::Region region = m_buffer->acquireReadRegion(len - bytesCopied); + + if (region.second > 0) { + memcpy(data + bytesCopied, region.first, region.second); + bytesCopied += region.second; + } + else + wecan = false; + + m_buffer->releaseReadRegion(region); + } + + return bytesCopied; +} + +void QDarwinAudioSourceBuffer::setFlushDevice(QIODevice *device) +{ + if (m_device != device) + m_device = device; +} + +void QDarwinAudioSourceBuffer::startFlushTimer() +{ + if (m_device != 0) { + // We use the period time for the timer, since that's + // around the buffer size (pre conversion >.>) + m_flushTimer->start(qMax(1, m_periodTime)); + } +} + +void QDarwinAudioSourceBuffer::stopFlushTimer() +{ + m_flushTimer->stop(); +} + +void QDarwinAudioSourceBuffer::flush(bool all) +{ + if (m_device == 0) + return; + + const int used = m_buffer->used(); + const int readSize = all ? used : used - (used % m_maxPeriodSize); + + if (readSize > 0) { + bool wecan = true; + int flushed = 0; + + while (!m_deviceError && wecan && flushed < readSize) { + CoreAudioRingBuffer::Region region = m_buffer->acquireReadRegion(readSize - flushed); + + if (region.second > 0) { + int bytesWritten = m_device->write(region.first, region.second); + if (bytesWritten < 0) { + stopFlushTimer(); + m_deviceError = true; + } + else { + region.second = bytesWritten; + flushed += bytesWritten; + wecan = bytesWritten != 0; + } + } + else + wecan = false; + + m_buffer->releaseReadRegion(region); + } + } +} + +void QDarwinAudioSourceBuffer::reset() +{ + m_buffer->reset(); + m_deviceError = false; +} + +int QDarwinAudioSourceBuffer::available() const +{ + return m_buffer->free(); +} + +int QDarwinAudioSourceBuffer::used() const +{ + return m_buffer->used(); +} + +void QDarwinAudioSourceBuffer::flushBuffer() +{ + flush(); +} + +OSStatus QDarwinAudioSourceBuffer::converterCallback(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData) +{ + Q_UNUSED(inAudioConverter); + Q_UNUSED(outDataPacketDescription); + + QCoreAudioPacketFeeder* feeder = static_cast<QCoreAudioPacketFeeder*>(inUserData); + + if (!feeder->feed(*ioData, *ioNumberDataPackets)) + return as_empty; + + return noErr; +} + +QDarwinAudioSourceDevice::QDarwinAudioSourceDevice(QDarwinAudioSourceBuffer *audioBuffer, QObject *parent) + : QIODevice(parent) + , m_audioBuffer(audioBuffer) +{ + open(QIODevice::ReadOnly | QIODevice::Unbuffered); + connect(m_audioBuffer, SIGNAL(readyRead()), this, SIGNAL(readyRead())); +} + +qint64 QDarwinAudioSourceDevice::readData(char *data, qint64 len) +{ + return m_audioBuffer->readBytes(data, len); +} + +qint64 QDarwinAudioSourceDevice::writeData(const char *data, qint64 len) +{ + Q_UNUSED(data); + Q_UNUSED(len); + + return 0; +} + +QDarwinAudioSource::QDarwinAudioSource(const QAudioDevice &device, QObject *parent) + : QPlatformAudioSource(parent) + , m_audioDeviceInfo(device) + , m_isOpen(false) + , m_internalBufferSize(DEFAULT_BUFFER_SIZE) + , m_totalFrames(0) + , m_audioUnit(0) + , m_clockFrequency(CoreAudioUtils::frequency() / 1000) + , m_errorCode(QAudio::NoError) + , m_stateCode(QAudio::StoppedState) + , m_audioBuffer(nullptr) + , m_volume(1.0) +{ + QAudioDevice di = device; + if (di.isNull()) + di = QMediaDevices::defaultAudioInput(); +#if defined(Q_OS_MACOS) + const QCoreAudioDeviceInfo *info = static_cast<const QCoreAudioDeviceInfo *>(di.handle()); + Q_ASSERT(info); + m_audioDeviceId = info->deviceID(); +#endif + m_device = di.id(); +} + + +QDarwinAudioSource::~QDarwinAudioSource() +{ + close(); +} + +bool QDarwinAudioSource::open() +{ +#if defined(Q_OS_IOS) + CoreAudioSessionManager::instance().setCategory(CoreAudioSessionManager::PlayAndRecord, CoreAudioSessionManager::MixWithOthers); + CoreAudioSessionManager::instance().setActive(true); +#endif + + if (m_isOpen) + return true; + + AudioComponentDescription componentDescription; + componentDescription.componentType = kAudioUnitType_Output; +#if defined(Q_OS_MACOS) + componentDescription.componentSubType = kAudioUnitSubType_HALOutput; +#else + componentDescription.componentSubType = kAudioUnitSubType_RemoteIO; +#endif + componentDescription.componentManufacturer = kAudioUnitManufacturer_Apple; + componentDescription.componentFlags = 0; + componentDescription.componentFlagsMask = 0; + + AudioComponent component = AudioComponentFindNext(0, &componentDescription); + if (component == 0) { + qWarning() << "QAudioSource: Failed to find Output component"; + return false; + } + + if (AudioComponentInstanceNew(component, &m_audioUnit) != noErr) { + qWarning() << "QAudioSource: Unable to Open Output Component"; + return false; + } + + // Set mode + // switch to input mode + UInt32 enable = 1; + if (AudioUnitSetProperty(m_audioUnit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Input, + 1, + &enable, + sizeof(enable)) != noErr) { + qWarning() << "QAudioSource: Unable to switch to input mode (Enable Input)"; + return false; + } + + enable = 0; + if (AudioUnitSetProperty(m_audioUnit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Output, + 0, + &enable, + sizeof(enable)) != noErr) { + qWarning() << "QAudioSource: Unable to switch to input mode (Disable output)"; + return false; + } + + // register callback + AURenderCallbackStruct callback; + callback.inputProc = inputCallback; + callback.inputProcRefCon = this; + + if (AudioUnitSetProperty(m_audioUnit, + kAudioOutputUnitProperty_SetInputCallback, + kAudioUnitScope_Global, + 0, + &callback, + sizeof(callback)) != noErr) { + qWarning() << "QAudioSource: Failed to set AudioUnit callback"; + return false; + } + +#if defined(Q_OS_MACOS) + //Set Audio Device + if (AudioUnitSetProperty(m_audioUnit, + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, + 0, + &m_audioDeviceId, + sizeof(m_audioDeviceId)) != noErr) { + qWarning() << "QAudioSource: Unable to use configured device"; + return false; + } +#endif + + //set format + m_streamFormat = CoreAudioUtils::toAudioStreamBasicDescription(m_audioFormat); + +#if defined(Q_OS_MACOS) + UInt32 size = 0; + + if (m_audioFormat == m_audioDeviceInfo.preferredFormat()) { +#endif + + m_deviceFormat = m_streamFormat; + AudioUnitSetProperty(m_audioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, + 1, + &m_deviceFormat, + sizeof(m_deviceFormat)); +#if defined(Q_OS_MACOS) + } else { + size = sizeof(m_deviceFormat); + if (AudioUnitGetProperty(m_audioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + 1, + &m_deviceFormat, + &size) != noErr) { + qWarning() << "QAudioSource: Unable to retrieve device format"; + return false; + } + + if (AudioUnitSetProperty(m_audioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, + 1, + &m_deviceFormat, + sizeof(m_deviceFormat)) != noErr) { + qWarning() << "QAudioSource: Unable to set device format"; + return false; + } + } +#endif + + //setup buffers + UInt32 numberOfFrames; +#if defined(Q_OS_MACOS) + size = sizeof(UInt32); + if (AudioUnitGetProperty(m_audioUnit, + kAudioDevicePropertyBufferFrameSize, + kAudioUnitScope_Global, + 0, + &numberOfFrames, + &size) != noErr) { + qWarning() << "QAudioSource: Failed to get audio period size"; + return false; + } + //BUG: numberOfFrames gets ignored after this point + + AudioValueRange bufferRange; + size = sizeof(AudioValueRange); + + if (AudioUnitGetProperty(m_audioUnit, + kAudioDevicePropertyBufferFrameSizeRange, + kAudioUnitScope_Global, + 0, + &bufferRange, + &size) != noErr) { + qWarning() << "QAudioSource: Failed to get audio period size range"; + return false; + } + + // See if the requested buffer size is permissible + numberOfFrames = qBound((UInt32)bufferRange.mMinimum, m_internalBufferSize / m_streamFormat.mBytesPerFrame, (UInt32)bufferRange.mMaximum); + + // Set it back + if (AudioUnitSetProperty(m_audioUnit, + kAudioDevicePropertyBufferFrameSize, + kAudioUnitScope_Global, + 0, + &numberOfFrames, + sizeof(UInt32)) != noErr) { + qWarning() << "QAudioSource: Failed to set audio buffer size"; + return false; + } +#else //iOS + Float32 bufferSize = CoreAudioSessionManager::instance().currentIOBufferDuration(); + bufferSize *= m_streamFormat.mSampleRate; + numberOfFrames = bufferSize; +#endif + + // Now allocate a few buffers to be safe. + m_periodSizeBytes = m_internalBufferSize = numberOfFrames * m_streamFormat.mBytesPerFrame; + + { + QMutexLocker lock(m_audioBuffer); + m_audioBuffer = new QDarwinAudioSourceBuffer(m_internalBufferSize * 4, + m_periodSizeBytes, + m_deviceFormat, + m_streamFormat, + this); + + m_audioBuffer->setVolume(m_volume); + } + m_audioIO = new QDarwinAudioSourceDevice(m_audioBuffer, this); + + // Init + if (AudioUnitInitialize(m_audioUnit) != noErr) { + qWarning() << "QAudioSource: Failed to initialize AudioUnit"; + return false; + } + + m_isOpen = true; + + return m_isOpen; + +} + +void QDarwinAudioSource::close() +{ + stop(); + if (m_audioUnit != 0) { + AudioOutputUnitStop(m_audioUnit); + AudioUnitUninitialize(m_audioUnit); + AudioComponentInstanceDispose(m_audioUnit); + } + + delete m_audioBuffer; + m_audioBuffer = nullptr; + m_isOpen = false; +} + +void QDarwinAudioSource::start(QIODevice *device) +{ + QIODevice* op = device; + + if (!m_audioDeviceInfo.isFormatSupported(m_audioFormat) || !open()) { + m_stateCode = QAudio::StoppedState; + m_errorCode = QAudio::OpenError; + return; + } + + reset(); + { + QMutexLocker lock(m_audioBuffer); + m_audioBuffer->reset(); + m_audioBuffer->setFlushDevice(op); + } + + if (op == 0) + op = m_audioIO; + + // Start + m_totalFrames = 0; + + m_stateCode = QAudio::IdleState; + m_errorCode = QAudio::NoError; + emit stateChanged(m_stateCode); + + audioThreadStart(); +} + + +QIODevice *QDarwinAudioSource::start() +{ + QIODevice* op = 0; + + if (!m_audioDeviceInfo.isFormatSupported(m_audioFormat) || !open()) { + m_stateCode = QAudio::StoppedState; + m_errorCode = QAudio::OpenError; + return m_audioIO; + } + + reset(); + { + QMutexLocker lock(m_audioBuffer); + m_audioBuffer->reset(); + m_audioBuffer->setFlushDevice(op); + } + + if (op == 0) + op = m_audioIO; + + // Start + m_totalFrames = 0; + + m_stateCode = QAudio::IdleState; + m_errorCode = QAudio::NoError; + emit stateChanged(m_stateCode); + + audioThreadStart(); + + return op; +} + + +void QDarwinAudioSource::stop() +{ + QMutexLocker lock(m_audioBuffer); + if (m_stateCode != QAudio::StoppedState) { + audioThreadStop(); + m_audioBuffer->flush(true); + + m_errorCode = QAudio::NoError; + m_stateCode = QAudio::StoppedState; + QMetaObject::invokeMethod(this, "stateChanged", Qt::QueuedConnection, Q_ARG(QAudio::State, m_stateCode)); + } +} + + +void QDarwinAudioSource::reset() +{ + QMutexLocker lock(m_audioBuffer); + if (m_stateCode != QAudio::StoppedState) { + audioThreadStop(); + + m_errorCode = QAudio::NoError; + m_stateCode = QAudio::StoppedState; + m_audioBuffer->reset(); + QMetaObject::invokeMethod(this, "stateChanged", Qt::QueuedConnection, Q_ARG(QAudio::State, m_stateCode)); + } +} + + +void QDarwinAudioSource::suspend() +{ + QMutexLocker lock(m_audioBuffer); + if (m_stateCode == QAudio::ActiveState || m_stateCode == QAudio::IdleState) { + audioThreadStop(); + + m_errorCode = QAudio::NoError; + m_stateCode = QAudio::SuspendedState; + QMetaObject::invokeMethod(this, "stateChanged", Qt::QueuedConnection, Q_ARG(QAudio::State, m_stateCode)); + } +} + + +void QDarwinAudioSource::resume() +{ + QMutexLocker lock(m_audioBuffer); + if (m_stateCode == QAudio::SuspendedState) { + audioThreadStart(); + + m_errorCode = QAudio::NoError; + m_stateCode = QAudio::ActiveState; + QMetaObject::invokeMethod(this, "stateChanged", Qt::QueuedConnection, Q_ARG(QAudio::State, m_stateCode)); + } +} + + +qsizetype QDarwinAudioSource::bytesReady() const +{ + QMutexLocker lock(m_audioBuffer); + if (!m_audioBuffer) + return 0; + return m_audioBuffer->used(); +} + +void QDarwinAudioSource::setBufferSize(qsizetype value) +{ + m_internalBufferSize = value; +} + + +qsizetype QDarwinAudioSource::bufferSize() const +{ + return m_internalBufferSize; +} + +qint64 QDarwinAudioSource::processedUSecs() const +{ + return m_totalFrames * 1000000 / m_audioFormat.sampleRate(); +} + +QAudio::Error QDarwinAudioSource::error() const +{ + return m_errorCode; +} + + +QAudio::State QDarwinAudioSource::state() const +{ + return m_stateCode; +} + + +void QDarwinAudioSource::setFormat(const QAudioFormat &format) +{ + if (m_stateCode == QAudio::StoppedState) + m_audioFormat = format; +} + + +QAudioFormat QDarwinAudioSource::format() const +{ + return m_audioFormat; +} + + +void QDarwinAudioSource::setVolume(qreal volume) +{ + QMutexLocker lock(m_audioBuffer); + m_volume = volume; + if (m_audioBuffer) + m_audioBuffer->setVolume(m_volume); +} + + +qreal QDarwinAudioSource::volume() const +{ + return m_volume; +} + +void QDarwinAudioSource::deviceStoppped() +{ + stopTimers(); + emit stateChanged(m_stateCode); +} + +void QDarwinAudioSource::audioThreadStart() +{ + startTimers(); + m_audioThreadState.storeRelaxed(Running); + AudioOutputUnitStart(m_audioUnit); +} + +void QDarwinAudioSource::audioThreadStop() +{ + stopTimers(); + if (m_audioThreadState.testAndSetAcquire(Running, Stopped)) + m_audioBuffer->wait(); +} + +void QDarwinAudioSource::audioDeviceStop() +{ + AudioOutputUnitStop(m_audioUnit); + m_audioThreadState.storeRelaxed(Stopped); + m_audioBuffer->wake(); +} + +void QDarwinAudioSource::audioDeviceActive() +{ + if (m_stateCode == QAudio::IdleState) { + QMutexLocker lock(m_audioBuffer); + m_stateCode = QAudio::ActiveState; + emit stateChanged(m_stateCode); + } +} + +void QDarwinAudioSource::audioDeviceFull() +{ + if (m_stateCode == QAudio::ActiveState) { + QMutexLocker lock(m_audioBuffer); + m_errorCode = QAudio::UnderrunError; + m_stateCode = QAudio::IdleState; + emit stateChanged(m_stateCode); + } +} + +void QDarwinAudioSource::audioDeviceError() +{ + if (m_stateCode == QAudio::ActiveState) { + QMutexLocker lock(m_audioBuffer); + audioDeviceStop(); + + m_errorCode = QAudio::IOError; + m_stateCode = QAudio::StoppedState; + QMetaObject::invokeMethod(this, "deviceStopped", Qt::QueuedConnection); + } +} + +void QDarwinAudioSource::startTimers() +{ + m_audioBuffer->startFlushTimer(); +} + +void QDarwinAudioSource::stopTimers() +{ + m_audioBuffer->stopFlushTimer(); +} + +OSStatus QDarwinAudioSource::inputCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) +{ + Q_UNUSED(ioData); + + QDarwinAudioSource* d = static_cast<QDarwinAudioSource*>(inRefCon); + + const int threadState = d->m_audioThreadState.loadAcquire(); + if (threadState == Stopped) + d->audioDeviceStop(); + else { + qint64 framesWritten; + + { + QMutexLocker locker(d->m_audioBuffer); + framesWritten = d->m_audioBuffer->renderFromDevice(d->m_audioUnit, + ioActionFlags, + inTimeStamp, + inBusNumber, + inNumberFrames); + } + + if (framesWritten > 0) { + d->m_totalFrames += framesWritten; + d->audioDeviceActive(); + } else if (framesWritten == 0) + d->audioDeviceFull(); + else if (framesWritten < 0) + d->audioDeviceError(); + } + + return noErr; +} + +QT_END_NAMESPACE + +#include "moc_qdarwinaudiosource_p.cpp" diff --git a/src/multimedia/darwin/qdarwinaudiosource_p.h b/src/multimedia/darwin/qdarwinaudiosource_p.h new file mode 100644 index 000000000..87c3c1070 --- /dev/null +++ b/src/multimedia/darwin/qdarwinaudiosource_p.h @@ -0,0 +1,244 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef IOSAUDIOINPUT_H +#define IOSAUDIOINPUT_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qaudiosystem_p.h> +#include <qdarwinaudiodevice_p.h> + +#include <AudioUnit/AudioUnit.h> +#include <CoreAudio/CoreAudioTypes.h> +#include <AudioToolbox/AudioToolbox.h> + +#include <QtCore/QIODevice> +#include <QtCore/QWaitCondition> +#include <QtCore/QMutex> +#include <QtCore/QTimer> + +QT_BEGIN_NAMESPACE + +class CoreAudioRingBuffer; +class QCoreAudioPacketFeeder; +class QDarwinAudioSourceBuffer; +class QDarwinAudioSourceDevice; + +class QCoreAudioBufferList +{ +public: + QCoreAudioBufferList(AudioStreamBasicDescription const& streamFormat); + QCoreAudioBufferList(AudioStreamBasicDescription const& streamFormat, char *buffer, int bufferSize); + QCoreAudioBufferList(AudioStreamBasicDescription const& streamFormat, int framesToBuffer); + + ~QCoreAudioBufferList(); + + AudioBufferList* audioBufferList() const { return m_bufferList; } + char *data(int buffer = 0) const; + qint64 bufferSize(int buffer = 0) const; + int frameCount(int buffer = 0) const; + int packetCount(int buffer = 0) const; + int packetSize() const; + void reset(); + +private: + bool m_owner; + int m_dataSize; + AudioStreamBasicDescription m_streamDescription; + AudioBufferList *m_bufferList; +}; + +class QCoreAudioPacketFeeder +{ +public: + QCoreAudioPacketFeeder(QCoreAudioBufferList *abl); + + bool feed(AudioBufferList& dst, UInt32& packetCount); + bool empty() const; + +private: + UInt32 m_totalPackets; + UInt32 m_position; + QCoreAudioBufferList *m_audioBufferList; +}; + +class QDarwinAudioSourceBuffer : public QObject +{ + Q_OBJECT + +public: + QDarwinAudioSourceBuffer(int bufferSize, + int maxPeriodSize, + AudioStreamBasicDescription const& inputFormat, + AudioStreamBasicDescription const& outputFormat, + QObject *parent); + + ~QDarwinAudioSourceBuffer(); + + qreal volume() const; + void setVolume(qreal v); + + qint64 renderFromDevice(AudioUnit audioUnit, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumberFrames); + + qint64 readBytes(char *data, qint64 len); + + void setFlushDevice(QIODevice *device); + + void startFlushTimer(); + void stopFlushTimer(); + + void flush(bool all = false); + void reset(); + int available() const; + int used() const; + + void lock() { m_mutex.lock(); } + void unlock() { m_mutex.unlock(); } + + void wait() { m_threadFinished.wait(&m_mutex); } + void wake() { m_threadFinished.wakeOne(); } + +signals: + void readyRead(); + +private slots: + void flushBuffer(); + +private: + QMutex m_mutex; + QWaitCondition m_threadFinished; + + bool m_deviceError; + int m_maxPeriodSize; + int m_periodTime; + QIODevice *m_device; + QTimer *m_flushTimer; + CoreAudioRingBuffer *m_buffer; + QCoreAudioBufferList *m_inputBufferList; + AudioConverterRef m_audioConverter; + AudioStreamBasicDescription m_inputFormat; + AudioStreamBasicDescription m_outputFormat; + QAudioFormat m_qFormat; + qreal m_volume; + + const static OSStatus as_empty = 'qtem'; + + // Converter callback + static OSStatus converterCallback(AudioConverterRef inAudioConverter, + UInt32 *ioNumberDataPackets, + AudioBufferList *ioData, + AudioStreamPacketDescription **outDataPacketDescription, + void *inUserData); +}; + +class QDarwinAudioSourceDevice : public QIODevice +{ + Q_OBJECT + +public: + QDarwinAudioSourceDevice(QDarwinAudioSourceBuffer *audioBuffer, QObject *parent); + + qint64 readData(char *data, qint64 len); + qint64 writeData(const char *data, qint64 len); + + bool isSequential() const { return true; } + +private: + QDarwinAudioSourceBuffer *m_audioBuffer; +}; + +class QDarwinAudioSource : public QPlatformAudioSource +{ + Q_OBJECT + +public: + QDarwinAudioSource(const QAudioDevice &device, QObject *parent); + ~QDarwinAudioSource(); + + void start(QIODevice *device); + QIODevice *start(); + void stop(); + void reset(); + void suspend(); + void resume(); + qsizetype bytesReady() const; + void setBufferSize(qsizetype value); + qsizetype bufferSize() const; + qint64 processedUSecs() const; + QAudio::Error error() const; + QAudio::State state() const; + void setFormat(const QAudioFormat &format); + QAudioFormat format() const; + + void setVolume(qreal volume); + qreal volume() const; + +private slots: + void deviceStoppped(); + +private: + enum { + Running, + Stopped + }; + + bool open(); + void close(); + + void audioThreadStart(); + void audioThreadStop(); + + void audioDeviceStop(); + void audioDeviceActive(); + void audioDeviceFull(); + void audioDeviceError(); + + void startTimers(); + void stopTimers(); + + // Input callback + static OSStatus inputCallback(void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumberFrames, + AudioBufferList *ioData); + + QAudioDevice m_audioDeviceInfo; + QByteArray m_device; + bool m_isOpen; + int m_periodSizeBytes; + int m_internalBufferSize; + qint64 m_totalFrames; + QAudioFormat m_audioFormat; + QIODevice *m_audioIO; + AudioUnit m_audioUnit; +#if defined(Q_OS_MACOS) + AudioDeviceID m_audioDeviceId; +#endif + Float64 m_clockFrequency; + QAudio::Error m_errorCode; + QAudio::State m_stateCode; + QDarwinAudioSourceBuffer *m_audioBuffer; + QAtomicInt m_audioThreadState; + AudioStreamBasicDescription m_streamFormat; + AudioStreamBasicDescription m_deviceFormat; + qreal m_volume; +}; + +QT_END_NAMESPACE + +#endif // IOSAUDIOINPUT_H diff --git a/src/multimedia/darwin/qdarwinmediadevices.mm b/src/multimedia/darwin/qdarwinmediadevices.mm new file mode 100644 index 000000000..b0a108935 --- /dev/null +++ b/src/multimedia/darwin/qdarwinmediadevices.mm @@ -0,0 +1,269 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdarwinmediadevices_p.h" +#include "qmediadevices.h" +#include "private/qaudiodevice_p.h" +#include "qdarwinaudiodevice_p.h" +#include "qdarwinaudiosource_p.h" +#include "qdarwinaudiosink_p.h" + +#include <qloggingcategory.h> + +#include <qdebug.h> + +#if defined(Q_OS_IOS) +#include "qcoreaudiosessionmanager_p.h" +#import <AVFoundation/AVFoundation.h> +#else +#include "qmacosaudiodatautils_p.h" +#endif + +#if defined(Q_OS_MACOS) +static Q_LOGGING_CATEGORY(qLcDarwinMediaDevices, "qt.multimedia.darwin.mediaDevices") +#endif + +QT_BEGIN_NAMESPACE + +template<typename... Args> +QAudioDevice createAudioDevice(bool isDefault, Args &&...args) +{ + auto *dev = new QCoreAudioDeviceInfo(std::forward<Args>(args)...); + dev->isDefault = isDefault; + return dev->create(); +} + +#if defined(Q_OS_MACOS) + +static AudioDeviceID defaultAudioDevice(QAudioDevice::Mode mode) +{ + const AudioObjectPropertySelector selector = (mode == QAudioDevice::Output) ? kAudioHardwarePropertyDefaultOutputDevice + : kAudioHardwarePropertyDefaultInputDevice; + const AudioObjectPropertyAddress propertyAddress = { + selector, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain, + }; + + if (auto audioDevice = getAudioObject<AudioDeviceID>(kAudioObjectSystemObject, propertyAddress, + "Default Device")) { + return *audioDevice; + } + + return 0; +} + +static QByteArray uniqueId(AudioDeviceID device, QAudioDevice::Mode mode) +{ + const AudioObjectPropertyAddress propertyAddress = + makePropertyAddress(kAudioDevicePropertyDeviceUID, mode); + + if (auto name = getAudioObject<CFStringRef>(device, propertyAddress, "Device UID")) { + QString s = QString::fromCFString(*name); + CFRelease(*name); + return s.toUtf8(); + } + + return QByteArray(); +} + +static QList<QAudioDevice> availableAudioDevices(QAudioDevice::Mode mode) +{ + QList<QAudioDevice> devices; + + AudioDeviceID defaultDevice = defaultAudioDevice(mode); + if (defaultDevice != 0) + devices << createAudioDevice(true, defaultDevice, uniqueId(defaultDevice, mode), mode); + + const AudioObjectPropertyAddress audioDevicesPropertyAddress = { + kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain + }; + + if (auto audioDevices = getAudioData<AudioDeviceID>( + kAudioObjectSystemObject, audioDevicesPropertyAddress, "Audio Devices")) { + const AudioObjectPropertyAddress audioDeviceStreamFormatPropertyAddress = + makePropertyAddress(kAudioDevicePropertyStreamFormat, mode); + + for (const auto &device : *audioDevices) { + if (device == defaultDevice) + continue; + + if (getAudioObject<AudioStreamBasicDescription>(device, + audioDeviceStreamFormatPropertyAddress, + nullptr /*don't print logs*/)) { + devices << createAudioDevice(false, device, uniqueId(device, mode), mode); + } + } + } + + return devices; +} + +static OSStatus audioDeviceChangeListener(AudioObjectID id, UInt32, + const AudioObjectPropertyAddress *address, void *ptr) +{ + Q_ASSERT(address); + Q_ASSERT(ptr); + + QDarwinMediaDevices *instance = static_cast<QDarwinMediaDevices *>(ptr); + + qCDebug(qLcDarwinMediaDevices) + << "audioDeviceChangeListener: id:" << id << "address: " << address->mSelector + << address->mScope << address->mElement; + + switch (address->mSelector) { + case kAudioHardwarePropertyDefaultInputDevice: + instance->onInputsUpdated(); + break; + case kAudioHardwarePropertyDefaultOutputDevice: + instance->onOutputsUpdated(); + break; + default: + instance->onInputsUpdated(); + instance->onOutputsUpdated(); + break; + } + + return 0; +} + +static constexpr AudioObjectPropertyAddress listenerAddresses[] = { + { kAudioHardwarePropertyDefaultInputDevice, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain }, + { kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain }, + { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain } +}; + +static void setAudioListeners(QDarwinMediaDevices &instance) +{ + for (const auto &address : listenerAddresses) { + const auto err = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &address, + audioDeviceChangeListener, &instance); + + if (err) + qWarning() << "Fail to add listener. mSelector:" << address.mSelector + << "mScope:" << address.mScope << "mElement:" << address.mElement + << "err:" << err; + } +} + +static void removeAudioListeners(QDarwinMediaDevices &instance) +{ + for (const auto &address : listenerAddresses) { + const auto err = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &address, + audioDeviceChangeListener, &instance); + + if (err) + qWarning() << "Fail to remove listener. mSelector:" << address.mSelector + << "mScope:" << address.mScope << "mElement:" << address.mElement + << "err:" << err; + } +} + +#elif defined(Q_OS_IOS) + +static QList<QAudioDevice> availableAudioDevices(QAudioDevice::Mode mode) +{ + QList<QAudioDevice> devices; + + if (mode == QAudioDevice::Output) { + devices.append(createAudioDevice(true, "default", QAudioDevice::Output)); + } else { + AVCaptureDevice *defaultDevice = + [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; + + // TODO: Support Bluetooth and USB devices + AVCaptureDeviceDiscoverySession *captureDeviceDiscoverySession = + [AVCaptureDeviceDiscoverySession + discoverySessionWithDeviceTypes:@[ AVCaptureDeviceTypeBuiltInMicrophone ] + mediaType:AVMediaTypeAudio + position:AVCaptureDevicePositionUnspecified]; + + NSArray *captureDevices = [captureDeviceDiscoverySession devices]; + for (AVCaptureDevice *device in captureDevices) { + const bool isDefault = + defaultDevice && [defaultDevice.uniqueID isEqualToString:device.uniqueID]; + devices.append(createAudioDevice(isDefault, + QString::fromNSString(device.uniqueID).toUtf8(), + QAudioDevice::Input)); + } + } + + return devices; +} + +static void setAudioListeners(QDarwinMediaDevices &) +{ + // ### This should use the audio session manager +} + +static void removeAudioListeners(QDarwinMediaDevices &) +{ + // ### This should use the audio session manager +} + +#endif + + +QDarwinMediaDevices::QDarwinMediaDevices() + : QPlatformMediaDevices() +{ +#ifdef Q_OS_MACOS // TODO: implement setAudioListeners, removeAudioListeners for Q_OS_IOS, after + // that - remove or modify the define + m_cachedAudioInputs = availableAudioDevices(QAudioDevice::Input); + m_cachedAudioOutputs = availableAudioDevices(QAudioDevice::Output); +#endif + + setAudioListeners(*this); +} + + +QDarwinMediaDevices::~QDarwinMediaDevices() +{ + removeAudioListeners(*this); +} + +QList<QAudioDevice> QDarwinMediaDevices::audioInputs() const +{ + return availableAudioDevices(QAudioDevice::Input); +} + +QList<QAudioDevice> QDarwinMediaDevices::audioOutputs() const +{ + return availableAudioDevices(QAudioDevice::Output); +} + +void QDarwinMediaDevices::onInputsUpdated() +{ + auto inputs = availableAudioDevices(QAudioDevice::Input); + if (m_cachedAudioInputs != inputs) { + m_cachedAudioInputs = inputs; + emit audioInputsChanged(); + } +} + +void QDarwinMediaDevices::onOutputsUpdated() +{ + auto outputs = availableAudioDevices(QAudioDevice::Output); + if (m_cachedAudioOutputs != outputs) { + m_cachedAudioOutputs = outputs; + emit audioOutputsChanged(); + } +} + +QPlatformAudioSource *QDarwinMediaDevices::createAudioSource(const QAudioDevice &info, + QObject *parent) +{ + return new QDarwinAudioSource(info, parent); +} + +QPlatformAudioSink *QDarwinMediaDevices::createAudioSink(const QAudioDevice &info, + QObject *parent) +{ + return new QDarwinAudioSink(info, parent); +} + +QT_END_NAMESPACE diff --git a/src/multimedia/darwin/qdarwinmediadevices_p.h b/src/multimedia/darwin/qdarwinmediadevices_p.h new file mode 100644 index 000000000..0c7a45433 --- /dev/null +++ b/src/multimedia/darwin/qdarwinmediadevices_p.h @@ -0,0 +1,49 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QDARWINMEDIADEVICES_H +#define QDARWINMEDIADEVICES_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qplatformmediadevices_p.h> +#include <qelapsedtimer.h> +#include <qcameradevice.h> + +QT_BEGIN_NAMESPACE + +class QCameraDevice; + +class QDarwinMediaDevices : public QPlatformMediaDevices +{ +public: + QDarwinMediaDevices(); + ~QDarwinMediaDevices() override; + + QList<QAudioDevice> audioInputs() const override; + QList<QAudioDevice> audioOutputs() const override; + QPlatformAudioSource *createAudioSource(const QAudioDevice &info, + QObject *parent) override; + QPlatformAudioSink *createAudioSink(const QAudioDevice &info, + QObject *parent) override; + + void onInputsUpdated(); + void onOutputsUpdated(); + +private: + QList<QAudioDevice> m_cachedAudioInputs; + QList<QAudioDevice> m_cachedAudioOutputs; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/darwin/qmacosaudiodatautils_p.h b/src/multimedia/darwin/qmacosaudiodatautils_p.h new file mode 100644 index 000000000..8cc2f8440 --- /dev/null +++ b/src/multimedia/darwin/qmacosaudiodatautils_p.h @@ -0,0 +1,112 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QMACOSAUDIODATAUTILS_P_H +#define QMACOSAUDIODATAUTILS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <CoreAudio/AudioHardware.h> + +#include "qaudiodevice.h" +#include "qdebug.h" + +#include <optional> +#include <vector> +#include <algorithm> + +QT_BEGIN_NAMESPACE + +template<typename... Args> +void printUnableToReadWarning(const char *logName, AudioObjectID objectID, const AudioObjectPropertyAddress &address, Args &&...args) +{ + if (!logName) + return; + + char scope[5] = {0}; + memcpy(&scope, &address.mScope, 4); + std::reverse(scope, scope + 4); + + auto warn = qWarning(); + warn << "Unable to read property" << logName << "for object" << objectID << ", scope" << scope << ";"; + (warn << ... << args); + warn << "\n If the warning is unexpected use test_audio_config to get comprehensive audio info and report a bug"; +} + +inline static AudioObjectPropertyAddress +makePropertyAddress(AudioObjectPropertySelector selector, QAudioDevice::Mode mode, + AudioObjectPropertyElement element = kAudioObjectPropertyElementMain) +{ + return { selector, + mode == QAudioDevice::Input ? kAudioDevicePropertyScopeInput + : kAudioDevicePropertyScopeOutput, + element }; +} + +inline static bool getAudioData(AudioObjectID objectID, const AudioObjectPropertyAddress &address, + void *dst, UInt32 dstSize, const char *logName) +{ + UInt32 readBytes = dstSize; + const auto res = AudioObjectGetPropertyData(objectID, &address, 0, nullptr, &readBytes, dst); + + if (res != noErr) + printUnableToReadWarning(logName, objectID, address, "Err:", res); + else if (readBytes != dstSize) + printUnableToReadWarning(logName, objectID, address, "Data size", readBytes, "VS", dstSize, + "expected"); + else + return true; + + return false; +} + +template<typename T> +std::optional<std::vector<T>> getAudioData(AudioObjectID objectID, + const AudioObjectPropertyAddress &address, + const char *logName, size_t minDataSize = 0) +{ + static_assert(std::is_trivial_v<T>, "A trivial type is expected"); + + UInt32 size = 0; + const auto res = AudioObjectGetPropertyDataSize(objectID, &address, 0, nullptr, &size); + + if (res != noErr) { + printUnableToReadWarning(logName, objectID, address, + "AudioObjectGetPropertyDataSize failed, Err:", res); + } else if (size / sizeof(T) < minDataSize) { + printUnableToReadWarning(logName, objectID, address, "Data size is too small:", size, "VS", + minDataSize * sizeof(T), "bytes"); + } else { + std::vector<T> data(size / sizeof(T)); + if (getAudioData(objectID, address, data.data(), data.size() * sizeof(T), logName)) + return { std::move(data) }; + } + + return {}; +} + +template<typename T> +std::optional<T> getAudioObject(AudioObjectID objectID, const AudioObjectPropertyAddress &address, + const char *logName) +{ + static_assert(std::is_trivial_v<T>, "A trivial type is expected"); + + T object{}; + if (getAudioData(objectID, address, &object, sizeof(T), logName)) + return { object }; + + return {}; +} + +QT_END_NAMESPACE + +#endif // QMACOSAUDIODATAUTILS_P_H |