mirror of
https://github.com/mollyim/webrtc.git
synced 2025-05-13 05:40:42 +01:00

This reverts commit c89fdd716c
.
Reason for revert: Causes rare compilation error on win-libfuzzer-asan trybot.
See https://ci.chromium.org/p/chromium/builders/try/win-libfuzzer-asan-rel/713745?
Original change's description:
> Refactor the PlatformThread API.
>
> PlatformThread's API is using old style function pointers, causes
> casting, is unintuitive and forces artificial call sequences, and
> is additionally possible to misuse in release mode.
>
> Fix this by an API face lift:
> 1. The class is turned into a handle, which can be empty.
> 2. The only way of getting a non-empty PlatformThread is by calling
> SpawnJoinable or SpawnDetached, clearly conveying the semantics to the
> code reader.
> 3. Handles can be Finalized, which works differently for joinable and
> detached threads:
> a) Handles for detached threads are simply closed where applicable.
> b) Joinable threads are joined before handles are closed.
> 4. The destructor finalizes handles. No explicit call is needed.
>
> Fixed: webrtc:12727
> Change-Id: Id00a0464edf4fc9e552b6a1fbb5d2e1280e88811
> Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/215075
> Commit-Queue: Markus Handell <handellm@webrtc.org>
> Reviewed-by: Harald Alvestrand <hta@webrtc.org>
> Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
> Reviewed-by: Tommi <tommi@webrtc.org>
> Cr-Commit-Position: refs/heads/master@{#33923}
# Not skipping CQ checks because original CL landed > 1 day ago.
TBR=handellm@webrtc.org
Bug: webrtc:12727
Change-Id: Ic0146be8866f6dd3ad9c364fb8646650b8e07419
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/217583
Reviewed-by: Guido Urdaneta <guidou@webrtc.org>
Reviewed-by: Markus Handell <handellm@webrtc.org>
Commit-Queue: Guido Urdaneta <guidou@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#33936}
2524 lines
80 KiB
C++
2524 lines
80 KiB
C++
/*
|
|
* Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree. An additional intellectual property rights grant can be found
|
|
* in the file PATENTS. All contributing project authors may
|
|
* be found in the AUTHORS file in the root of the source tree.
|
|
*/
|
|
|
|
#include "modules/audio_device/mac/audio_device_mac.h"
|
|
|
|
#include <ApplicationServices/ApplicationServices.h>
|
|
#include <libkern/OSAtomic.h> // OSAtomicCompareAndSwap()
|
|
#include <mach/mach.h> // mach_task_self()
|
|
#include <sys/sysctl.h> // sysctlbyname()
|
|
|
|
#include <memory>
|
|
|
|
#include "modules/audio_device/audio_device_config.h"
|
|
#include "modules/third_party/portaudio/pa_ringbuffer.h"
|
|
#include "rtc_base/arraysize.h"
|
|
#include "rtc_base/checks.h"
|
|
#include "rtc_base/platform_thread.h"
|
|
#include "rtc_base/system/arch.h"
|
|
|
|
namespace webrtc {
|
|
|
|
#define WEBRTC_CA_RETURN_ON_ERR(expr) \
|
|
do { \
|
|
err = expr; \
|
|
if (err != noErr) { \
|
|
logCAMsg(rtc::LS_ERROR, "Error in " #expr, (const char*)&err); \
|
|
return -1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define WEBRTC_CA_LOG_ERR(expr) \
|
|
do { \
|
|
err = expr; \
|
|
if (err != noErr) { \
|
|
logCAMsg(rtc::LS_ERROR, "Error in " #expr, (const char*)&err); \
|
|
} \
|
|
} while (0)
|
|
|
|
#define WEBRTC_CA_LOG_WARN(expr) \
|
|
do { \
|
|
err = expr; \
|
|
if (err != noErr) { \
|
|
logCAMsg(rtc::LS_WARNING, "Error in " #expr, (const char*)&err); \
|
|
} \
|
|
} while (0)
|
|
|
|
enum { MaxNumberDevices = 64 };
|
|
|
|
void AudioDeviceMac::AtomicSet32(int32_t* theValue, int32_t newValue) {
|
|
while (1) {
|
|
int32_t oldValue = *theValue;
|
|
if (OSAtomicCompareAndSwap32Barrier(oldValue, newValue, theValue) == true) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
int32_t AudioDeviceMac::AtomicGet32(int32_t* theValue) {
|
|
while (1) {
|
|
int32_t value = *theValue;
|
|
if (OSAtomicCompareAndSwap32Barrier(value, value, theValue) == true) {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// CoreAudio errors are best interpreted as four character strings.
|
|
void AudioDeviceMac::logCAMsg(const rtc::LoggingSeverity sev,
|
|
const char* msg,
|
|
const char* err) {
|
|
RTC_DCHECK(msg != NULL);
|
|
RTC_DCHECK(err != NULL);
|
|
|
|
#ifdef WEBRTC_ARCH_BIG_ENDIAN
|
|
switch (sev) {
|
|
case rtc::LS_ERROR:
|
|
RTC_LOG(LS_ERROR) << msg << ": " << err[0] << err[1] << err[2] << err[3];
|
|
break;
|
|
case rtc::LS_WARNING:
|
|
RTC_LOG(LS_WARNING) << msg << ": " << err[0] << err[1] << err[2]
|
|
<< err[3];
|
|
break;
|
|
case rtc::LS_VERBOSE:
|
|
RTC_LOG(LS_VERBOSE) << msg << ": " << err[0] << err[1] << err[2]
|
|
<< err[3];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
#else
|
|
// We need to flip the characters in this case.
|
|
switch (sev) {
|
|
case rtc::LS_ERROR:
|
|
RTC_LOG(LS_ERROR) << msg << ": " << err[3] << err[2] << err[1] << err[0];
|
|
break;
|
|
case rtc::LS_WARNING:
|
|
RTC_LOG(LS_WARNING) << msg << ": " << err[3] << err[2] << err[1]
|
|
<< err[0];
|
|
break;
|
|
case rtc::LS_VERBOSE:
|
|
RTC_LOG(LS_VERBOSE) << msg << ": " << err[3] << err[2] << err[1]
|
|
<< err[0];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
AudioDeviceMac::AudioDeviceMac()
|
|
: _ptrAudioBuffer(NULL),
|
|
_mixerManager(),
|
|
_inputDeviceIndex(0),
|
|
_outputDeviceIndex(0),
|
|
_inputDeviceID(kAudioObjectUnknown),
|
|
_outputDeviceID(kAudioObjectUnknown),
|
|
_inputDeviceIsSpecified(false),
|
|
_outputDeviceIsSpecified(false),
|
|
_recChannels(N_REC_CHANNELS),
|
|
_playChannels(N_PLAY_CHANNELS),
|
|
_captureBufData(NULL),
|
|
_renderBufData(NULL),
|
|
_initialized(false),
|
|
_isShutDown(false),
|
|
_recording(false),
|
|
_playing(false),
|
|
_recIsInitialized(false),
|
|
_playIsInitialized(false),
|
|
_renderDeviceIsAlive(1),
|
|
_captureDeviceIsAlive(1),
|
|
_twoDevices(true),
|
|
_doStop(false),
|
|
_doStopRec(false),
|
|
_macBookPro(false),
|
|
_macBookProPanRight(false),
|
|
_captureLatencyUs(0),
|
|
_renderLatencyUs(0),
|
|
_captureDelayUs(0),
|
|
_renderDelayUs(0),
|
|
_renderDelayOffsetSamples(0),
|
|
_paCaptureBuffer(NULL),
|
|
_paRenderBuffer(NULL),
|
|
_captureBufSizeSamples(0),
|
|
_renderBufSizeSamples(0),
|
|
prev_key_state_() {
|
|
RTC_DLOG(LS_INFO) << __FUNCTION__ << " created";
|
|
|
|
memset(_renderConvertData, 0, sizeof(_renderConvertData));
|
|
memset(&_outStreamFormat, 0, sizeof(AudioStreamBasicDescription));
|
|
memset(&_outDesiredFormat, 0, sizeof(AudioStreamBasicDescription));
|
|
memset(&_inStreamFormat, 0, sizeof(AudioStreamBasicDescription));
|
|
memset(&_inDesiredFormat, 0, sizeof(AudioStreamBasicDescription));
|
|
}
|
|
|
|
AudioDeviceMac::~AudioDeviceMac() {
|
|
RTC_DLOG(LS_INFO) << __FUNCTION__ << " destroyed";
|
|
|
|
if (!_isShutDown) {
|
|
Terminate();
|
|
}
|
|
|
|
RTC_DCHECK(!capture_worker_thread_.get());
|
|
RTC_DCHECK(!render_worker_thread_.get());
|
|
|
|
if (_paRenderBuffer) {
|
|
delete _paRenderBuffer;
|
|
_paRenderBuffer = NULL;
|
|
}
|
|
|
|
if (_paCaptureBuffer) {
|
|
delete _paCaptureBuffer;
|
|
_paCaptureBuffer = NULL;
|
|
}
|
|
|
|
if (_renderBufData) {
|
|
delete[] _renderBufData;
|
|
_renderBufData = NULL;
|
|
}
|
|
|
|
if (_captureBufData) {
|
|
delete[] _captureBufData;
|
|
_captureBufData = NULL;
|
|
}
|
|
|
|
kern_return_t kernErr = KERN_SUCCESS;
|
|
kernErr = semaphore_destroy(mach_task_self(), _renderSemaphore);
|
|
if (kernErr != KERN_SUCCESS) {
|
|
RTC_LOG(LS_ERROR) << "semaphore_destroy() error: " << kernErr;
|
|
}
|
|
|
|
kernErr = semaphore_destroy(mach_task_self(), _captureSemaphore);
|
|
if (kernErr != KERN_SUCCESS) {
|
|
RTC_LOG(LS_ERROR) << "semaphore_destroy() error: " << kernErr;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// API
|
|
// ============================================================================
|
|
|
|
void AudioDeviceMac::AttachAudioBuffer(AudioDeviceBuffer* audioBuffer) {
|
|
MutexLock lock(&mutex_);
|
|
|
|
_ptrAudioBuffer = audioBuffer;
|
|
|
|
// inform the AudioBuffer about default settings for this implementation
|
|
_ptrAudioBuffer->SetRecordingSampleRate(N_REC_SAMPLES_PER_SEC);
|
|
_ptrAudioBuffer->SetPlayoutSampleRate(N_PLAY_SAMPLES_PER_SEC);
|
|
_ptrAudioBuffer->SetRecordingChannels(N_REC_CHANNELS);
|
|
_ptrAudioBuffer->SetPlayoutChannels(N_PLAY_CHANNELS);
|
|
}
|
|
|
|
int32_t AudioDeviceMac::ActiveAudioLayer(
|
|
AudioDeviceModule::AudioLayer& audioLayer) const {
|
|
audioLayer = AudioDeviceModule::kPlatformDefaultAudio;
|
|
return 0;
|
|
}
|
|
|
|
AudioDeviceGeneric::InitStatus AudioDeviceMac::Init() {
|
|
MutexLock lock(&mutex_);
|
|
|
|
if (_initialized) {
|
|
return InitStatus::OK;
|
|
}
|
|
|
|
OSStatus err = noErr;
|
|
|
|
_isShutDown = false;
|
|
|
|
// PortAudio ring buffers require an elementCount which is a power of two.
|
|
if (_renderBufData == NULL) {
|
|
UInt32 powerOfTwo = 1;
|
|
while (powerOfTwo < PLAY_BUF_SIZE_IN_SAMPLES) {
|
|
powerOfTwo <<= 1;
|
|
}
|
|
_renderBufSizeSamples = powerOfTwo;
|
|
_renderBufData = new SInt16[_renderBufSizeSamples];
|
|
}
|
|
|
|
if (_paRenderBuffer == NULL) {
|
|
_paRenderBuffer = new PaUtilRingBuffer;
|
|
PaRingBufferSize bufSize = -1;
|
|
bufSize = PaUtil_InitializeRingBuffer(
|
|
_paRenderBuffer, sizeof(SInt16), _renderBufSizeSamples, _renderBufData);
|
|
if (bufSize == -1) {
|
|
RTC_LOG(LS_ERROR) << "PaUtil_InitializeRingBuffer() error";
|
|
return InitStatus::PLAYOUT_ERROR;
|
|
}
|
|
}
|
|
|
|
if (_captureBufData == NULL) {
|
|
UInt32 powerOfTwo = 1;
|
|
while (powerOfTwo < REC_BUF_SIZE_IN_SAMPLES) {
|
|
powerOfTwo <<= 1;
|
|
}
|
|
_captureBufSizeSamples = powerOfTwo;
|
|
_captureBufData = new Float32[_captureBufSizeSamples];
|
|
}
|
|
|
|
if (_paCaptureBuffer == NULL) {
|
|
_paCaptureBuffer = new PaUtilRingBuffer;
|
|
PaRingBufferSize bufSize = -1;
|
|
bufSize =
|
|
PaUtil_InitializeRingBuffer(_paCaptureBuffer, sizeof(Float32),
|
|
_captureBufSizeSamples, _captureBufData);
|
|
if (bufSize == -1) {
|
|
RTC_LOG(LS_ERROR) << "PaUtil_InitializeRingBuffer() error";
|
|
return InitStatus::RECORDING_ERROR;
|
|
}
|
|
}
|
|
|
|
kern_return_t kernErr = KERN_SUCCESS;
|
|
kernErr = semaphore_create(mach_task_self(), &_renderSemaphore,
|
|
SYNC_POLICY_FIFO, 0);
|
|
if (kernErr != KERN_SUCCESS) {
|
|
RTC_LOG(LS_ERROR) << "semaphore_create() error: " << kernErr;
|
|
return InitStatus::OTHER_ERROR;
|
|
}
|
|
|
|
kernErr = semaphore_create(mach_task_self(), &_captureSemaphore,
|
|
SYNC_POLICY_FIFO, 0);
|
|
if (kernErr != KERN_SUCCESS) {
|
|
RTC_LOG(LS_ERROR) << "semaphore_create() error: " << kernErr;
|
|
return InitStatus::OTHER_ERROR;
|
|
}
|
|
|
|
// Setting RunLoop to NULL here instructs HAL to manage its own thread for
|
|
// notifications. This was the default behaviour on OS X 10.5 and earlier,
|
|
// but now must be explicitly specified. HAL would otherwise try to use the
|
|
// main thread to issue notifications.
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
kAudioHardwarePropertyRunLoop, kAudioObjectPropertyScopeGlobal,
|
|
kAudioObjectPropertyElementMaster};
|
|
CFRunLoopRef runLoop = NULL;
|
|
UInt32 size = sizeof(CFRunLoopRef);
|
|
int aoerr = AudioObjectSetPropertyData(
|
|
kAudioObjectSystemObject, &propertyAddress, 0, NULL, size, &runLoop);
|
|
if (aoerr != noErr) {
|
|
RTC_LOG(LS_ERROR) << "Error in AudioObjectSetPropertyData: "
|
|
<< (const char*)&aoerr;
|
|
return InitStatus::OTHER_ERROR;
|
|
}
|
|
|
|
// Listen for any device changes.
|
|
propertyAddress.mSelector = kAudioHardwarePropertyDevices;
|
|
WEBRTC_CA_LOG_ERR(AudioObjectAddPropertyListener(
|
|
kAudioObjectSystemObject, &propertyAddress, &objectListenerProc, this));
|
|
|
|
// Determine if this is a MacBook Pro
|
|
_macBookPro = false;
|
|
_macBookProPanRight = false;
|
|
char buf[128];
|
|
size_t length = sizeof(buf);
|
|
memset(buf, 0, length);
|
|
|
|
int intErr = sysctlbyname("hw.model", buf, &length, NULL, 0);
|
|
if (intErr != 0) {
|
|
RTC_LOG(LS_ERROR) << "Error in sysctlbyname(): " << err;
|
|
} else {
|
|
RTC_LOG(LS_VERBOSE) << "Hardware model: " << buf;
|
|
if (strncmp(buf, "MacBookPro", 10) == 0) {
|
|
_macBookPro = true;
|
|
}
|
|
}
|
|
|
|
_initialized = true;
|
|
|
|
return InitStatus::OK;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::Terminate() {
|
|
if (!_initialized) {
|
|
return 0;
|
|
}
|
|
|
|
if (_recording) {
|
|
RTC_LOG(LS_ERROR) << "Recording must be stopped";
|
|
return -1;
|
|
}
|
|
|
|
if (_playing) {
|
|
RTC_LOG(LS_ERROR) << "Playback must be stopped";
|
|
return -1;
|
|
}
|
|
|
|
MutexLock lock(&mutex_);
|
|
_mixerManager.Close();
|
|
|
|
OSStatus err = noErr;
|
|
int retVal = 0;
|
|
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal,
|
|
kAudioObjectPropertyElementMaster};
|
|
WEBRTC_CA_LOG_WARN(AudioObjectRemovePropertyListener(
|
|
kAudioObjectSystemObject, &propertyAddress, &objectListenerProc, this));
|
|
|
|
err = AudioHardwareUnload();
|
|
if (err != noErr) {
|
|
logCAMsg(rtc::LS_ERROR, "Error in AudioHardwareUnload()",
|
|
(const char*)&err);
|
|
retVal = -1;
|
|
}
|
|
|
|
_isShutDown = true;
|
|
_initialized = false;
|
|
_outputDeviceIsSpecified = false;
|
|
_inputDeviceIsSpecified = false;
|
|
|
|
return retVal;
|
|
}
|
|
|
|
bool AudioDeviceMac::Initialized() const {
|
|
return (_initialized);
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SpeakerIsAvailable(bool& available) {
|
|
MutexLock lock(&mutex_);
|
|
return SpeakerIsAvailableLocked(available);
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SpeakerIsAvailableLocked(bool& available) {
|
|
bool wasInitialized = _mixerManager.SpeakerIsInitialized();
|
|
|
|
// Make an attempt to open up the
|
|
// output mixer corresponding to the currently selected output device.
|
|
//
|
|
if (!wasInitialized && InitSpeakerLocked() == -1) {
|
|
available = false;
|
|
return 0;
|
|
}
|
|
|
|
// Given that InitSpeaker was successful, we know that a valid speaker
|
|
// exists.
|
|
available = true;
|
|
|
|
// Close the initialized output mixer
|
|
//
|
|
if (!wasInitialized) {
|
|
_mixerManager.CloseSpeaker();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::InitSpeaker() {
|
|
MutexLock lock(&mutex_);
|
|
return InitSpeakerLocked();
|
|
}
|
|
|
|
int32_t AudioDeviceMac::InitSpeakerLocked() {
|
|
if (_playing) {
|
|
return -1;
|
|
}
|
|
|
|
if (InitDevice(_outputDeviceIndex, _outputDeviceID, false) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
if (_inputDeviceID == _outputDeviceID) {
|
|
_twoDevices = false;
|
|
} else {
|
|
_twoDevices = true;
|
|
}
|
|
|
|
if (_mixerManager.OpenSpeaker(_outputDeviceID) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::MicrophoneIsAvailable(bool& available) {
|
|
MutexLock lock(&mutex_);
|
|
return MicrophoneIsAvailableLocked(available);
|
|
}
|
|
|
|
int32_t AudioDeviceMac::MicrophoneIsAvailableLocked(bool& available) {
|
|
bool wasInitialized = _mixerManager.MicrophoneIsInitialized();
|
|
|
|
// Make an attempt to open up the
|
|
// input mixer corresponding to the currently selected output device.
|
|
//
|
|
if (!wasInitialized && InitMicrophoneLocked() == -1) {
|
|
available = false;
|
|
return 0;
|
|
}
|
|
|
|
// Given that InitMicrophone was successful, we know that a valid microphone
|
|
// exists.
|
|
available = true;
|
|
|
|
// Close the initialized input mixer
|
|
//
|
|
if (!wasInitialized) {
|
|
_mixerManager.CloseMicrophone();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::InitMicrophone() {
|
|
MutexLock lock(&mutex_);
|
|
return InitMicrophoneLocked();
|
|
}
|
|
|
|
int32_t AudioDeviceMac::InitMicrophoneLocked() {
|
|
if (_recording) {
|
|
return -1;
|
|
}
|
|
|
|
if (InitDevice(_inputDeviceIndex, _inputDeviceID, true) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
if (_inputDeviceID == _outputDeviceID) {
|
|
_twoDevices = false;
|
|
} else {
|
|
_twoDevices = true;
|
|
}
|
|
|
|
if (_mixerManager.OpenMicrophone(_inputDeviceID) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool AudioDeviceMac::SpeakerIsInitialized() const {
|
|
return (_mixerManager.SpeakerIsInitialized());
|
|
}
|
|
|
|
bool AudioDeviceMac::MicrophoneIsInitialized() const {
|
|
return (_mixerManager.MicrophoneIsInitialized());
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SpeakerVolumeIsAvailable(bool& available) {
|
|
bool wasInitialized = _mixerManager.SpeakerIsInitialized();
|
|
|
|
// Make an attempt to open up the
|
|
// output mixer corresponding to the currently selected output device.
|
|
//
|
|
if (!wasInitialized && InitSpeaker() == -1) {
|
|
// If we end up here it means that the selected speaker has no volume
|
|
// control.
|
|
available = false;
|
|
return 0;
|
|
}
|
|
|
|
// Given that InitSpeaker was successful, we know that a volume control exists
|
|
//
|
|
available = true;
|
|
|
|
// Close the initialized output mixer
|
|
//
|
|
if (!wasInitialized) {
|
|
_mixerManager.CloseSpeaker();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SetSpeakerVolume(uint32_t volume) {
|
|
return (_mixerManager.SetSpeakerVolume(volume));
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SpeakerVolume(uint32_t& volume) const {
|
|
uint32_t level(0);
|
|
|
|
if (_mixerManager.SpeakerVolume(level) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
volume = level;
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::MaxSpeakerVolume(uint32_t& maxVolume) const {
|
|
uint32_t maxVol(0);
|
|
|
|
if (_mixerManager.MaxSpeakerVolume(maxVol) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
maxVolume = maxVol;
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::MinSpeakerVolume(uint32_t& minVolume) const {
|
|
uint32_t minVol(0);
|
|
|
|
if (_mixerManager.MinSpeakerVolume(minVol) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
minVolume = minVol;
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SpeakerMuteIsAvailable(bool& available) {
|
|
bool isAvailable(false);
|
|
bool wasInitialized = _mixerManager.SpeakerIsInitialized();
|
|
|
|
// Make an attempt to open up the
|
|
// output mixer corresponding to the currently selected output device.
|
|
//
|
|
if (!wasInitialized && InitSpeaker() == -1) {
|
|
// If we end up here it means that the selected speaker has no volume
|
|
// control, hence it is safe to state that there is no mute control
|
|
// already at this stage.
|
|
available = false;
|
|
return 0;
|
|
}
|
|
|
|
// Check if the selected speaker has a mute control
|
|
//
|
|
_mixerManager.SpeakerMuteIsAvailable(isAvailable);
|
|
|
|
available = isAvailable;
|
|
|
|
// Close the initialized output mixer
|
|
//
|
|
if (!wasInitialized) {
|
|
_mixerManager.CloseSpeaker();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SetSpeakerMute(bool enable) {
|
|
return (_mixerManager.SetSpeakerMute(enable));
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SpeakerMute(bool& enabled) const {
|
|
bool muted(0);
|
|
|
|
if (_mixerManager.SpeakerMute(muted) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
enabled = muted;
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::MicrophoneMuteIsAvailable(bool& available) {
|
|
bool isAvailable(false);
|
|
bool wasInitialized = _mixerManager.MicrophoneIsInitialized();
|
|
|
|
// Make an attempt to open up the
|
|
// input mixer corresponding to the currently selected input device.
|
|
//
|
|
if (!wasInitialized && InitMicrophone() == -1) {
|
|
// If we end up here it means that the selected microphone has no volume
|
|
// control, hence it is safe to state that there is no boost control
|
|
// already at this stage.
|
|
available = false;
|
|
return 0;
|
|
}
|
|
|
|
// Check if the selected microphone has a mute control
|
|
//
|
|
_mixerManager.MicrophoneMuteIsAvailable(isAvailable);
|
|
available = isAvailable;
|
|
|
|
// Close the initialized input mixer
|
|
//
|
|
if (!wasInitialized) {
|
|
_mixerManager.CloseMicrophone();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SetMicrophoneMute(bool enable) {
|
|
return (_mixerManager.SetMicrophoneMute(enable));
|
|
}
|
|
|
|
int32_t AudioDeviceMac::MicrophoneMute(bool& enabled) const {
|
|
bool muted(0);
|
|
|
|
if (_mixerManager.MicrophoneMute(muted) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
enabled = muted;
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::StereoRecordingIsAvailable(bool& available) {
|
|
bool isAvailable(false);
|
|
bool wasInitialized = _mixerManager.MicrophoneIsInitialized();
|
|
|
|
if (!wasInitialized && InitMicrophone() == -1) {
|
|
// Cannot open the specified device
|
|
available = false;
|
|
return 0;
|
|
}
|
|
|
|
// Check if the selected microphone can record stereo
|
|
//
|
|
_mixerManager.StereoRecordingIsAvailable(isAvailable);
|
|
available = isAvailable;
|
|
|
|
// Close the initialized input mixer
|
|
//
|
|
if (!wasInitialized) {
|
|
_mixerManager.CloseMicrophone();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SetStereoRecording(bool enable) {
|
|
if (enable)
|
|
_recChannels = 2;
|
|
else
|
|
_recChannels = 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::StereoRecording(bool& enabled) const {
|
|
if (_recChannels == 2)
|
|
enabled = true;
|
|
else
|
|
enabled = false;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::StereoPlayoutIsAvailable(bool& available) {
|
|
bool isAvailable(false);
|
|
bool wasInitialized = _mixerManager.SpeakerIsInitialized();
|
|
|
|
if (!wasInitialized && InitSpeaker() == -1) {
|
|
// Cannot open the specified device
|
|
available = false;
|
|
return 0;
|
|
}
|
|
|
|
// Check if the selected microphone can record stereo
|
|
//
|
|
_mixerManager.StereoPlayoutIsAvailable(isAvailable);
|
|
available = isAvailable;
|
|
|
|
// Close the initialized input mixer
|
|
//
|
|
if (!wasInitialized) {
|
|
_mixerManager.CloseSpeaker();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SetStereoPlayout(bool enable) {
|
|
if (enable)
|
|
_playChannels = 2;
|
|
else
|
|
_playChannels = 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::StereoPlayout(bool& enabled) const {
|
|
if (_playChannels == 2)
|
|
enabled = true;
|
|
else
|
|
enabled = false;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::MicrophoneVolumeIsAvailable(bool& available) {
|
|
bool wasInitialized = _mixerManager.MicrophoneIsInitialized();
|
|
|
|
// Make an attempt to open up the
|
|
// input mixer corresponding to the currently selected output device.
|
|
//
|
|
if (!wasInitialized && InitMicrophone() == -1) {
|
|
// If we end up here it means that the selected microphone has no volume
|
|
// control.
|
|
available = false;
|
|
return 0;
|
|
}
|
|
|
|
// Given that InitMicrophone was successful, we know that a volume control
|
|
// exists
|
|
//
|
|
available = true;
|
|
|
|
// Close the initialized input mixer
|
|
//
|
|
if (!wasInitialized) {
|
|
_mixerManager.CloseMicrophone();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SetMicrophoneVolume(uint32_t volume) {
|
|
return (_mixerManager.SetMicrophoneVolume(volume));
|
|
}
|
|
|
|
int32_t AudioDeviceMac::MicrophoneVolume(uint32_t& volume) const {
|
|
uint32_t level(0);
|
|
|
|
if (_mixerManager.MicrophoneVolume(level) == -1) {
|
|
RTC_LOG(LS_WARNING) << "failed to retrieve current microphone level";
|
|
return -1;
|
|
}
|
|
|
|
volume = level;
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::MaxMicrophoneVolume(uint32_t& maxVolume) const {
|
|
uint32_t maxVol(0);
|
|
|
|
if (_mixerManager.MaxMicrophoneVolume(maxVol) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
maxVolume = maxVol;
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::MinMicrophoneVolume(uint32_t& minVolume) const {
|
|
uint32_t minVol(0);
|
|
|
|
if (_mixerManager.MinMicrophoneVolume(minVol) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
minVolume = minVol;
|
|
return 0;
|
|
}
|
|
|
|
int16_t AudioDeviceMac::PlayoutDevices() {
|
|
AudioDeviceID playDevices[MaxNumberDevices];
|
|
return GetNumberDevices(kAudioDevicePropertyScopeOutput, playDevices,
|
|
MaxNumberDevices);
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SetPlayoutDevice(uint16_t index) {
|
|
MutexLock lock(&mutex_);
|
|
|
|
if (_playIsInitialized) {
|
|
return -1;
|
|
}
|
|
|
|
AudioDeviceID playDevices[MaxNumberDevices];
|
|
uint32_t nDevices = GetNumberDevices(kAudioDevicePropertyScopeOutput,
|
|
playDevices, MaxNumberDevices);
|
|
RTC_LOG(LS_VERBOSE) << "number of available waveform-audio output devices is "
|
|
<< nDevices;
|
|
|
|
if (index > (nDevices - 1)) {
|
|
RTC_LOG(LS_ERROR) << "device index is out of range [0," << (nDevices - 1)
|
|
<< "]";
|
|
return -1;
|
|
}
|
|
|
|
_outputDeviceIndex = index;
|
|
_outputDeviceIsSpecified = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SetPlayoutDevice(
|
|
AudioDeviceModule::WindowsDeviceType /*device*/) {
|
|
RTC_LOG(LS_ERROR) << "WindowsDeviceType not supported";
|
|
return -1;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::PlayoutDeviceName(uint16_t index,
|
|
char name[kAdmMaxDeviceNameSize],
|
|
char guid[kAdmMaxGuidSize]) {
|
|
const uint16_t nDevices(PlayoutDevices());
|
|
|
|
if ((index > (nDevices - 1)) || (name == NULL)) {
|
|
return -1;
|
|
}
|
|
|
|
memset(name, 0, kAdmMaxDeviceNameSize);
|
|
|
|
if (guid != NULL) {
|
|
memset(guid, 0, kAdmMaxGuidSize);
|
|
}
|
|
|
|
return GetDeviceName(kAudioDevicePropertyScopeOutput, index, name);
|
|
}
|
|
|
|
int32_t AudioDeviceMac::RecordingDeviceName(uint16_t index,
|
|
char name[kAdmMaxDeviceNameSize],
|
|
char guid[kAdmMaxGuidSize]) {
|
|
const uint16_t nDevices(RecordingDevices());
|
|
|
|
if ((index > (nDevices - 1)) || (name == NULL)) {
|
|
return -1;
|
|
}
|
|
|
|
memset(name, 0, kAdmMaxDeviceNameSize);
|
|
|
|
if (guid != NULL) {
|
|
memset(guid, 0, kAdmMaxGuidSize);
|
|
}
|
|
|
|
return GetDeviceName(kAudioDevicePropertyScopeInput, index, name);
|
|
}
|
|
|
|
int16_t AudioDeviceMac::RecordingDevices() {
|
|
AudioDeviceID recDevices[MaxNumberDevices];
|
|
return GetNumberDevices(kAudioDevicePropertyScopeInput, recDevices,
|
|
MaxNumberDevices);
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SetRecordingDevice(uint16_t index) {
|
|
if (_recIsInitialized) {
|
|
return -1;
|
|
}
|
|
|
|
AudioDeviceID recDevices[MaxNumberDevices];
|
|
uint32_t nDevices = GetNumberDevices(kAudioDevicePropertyScopeInput,
|
|
recDevices, MaxNumberDevices);
|
|
RTC_LOG(LS_VERBOSE) << "number of available waveform-audio input devices is "
|
|
<< nDevices;
|
|
|
|
if (index > (nDevices - 1)) {
|
|
RTC_LOG(LS_ERROR) << "device index is out of range [0," << (nDevices - 1)
|
|
<< "]";
|
|
return -1;
|
|
}
|
|
|
|
_inputDeviceIndex = index;
|
|
_inputDeviceIsSpecified = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::SetRecordingDevice(
|
|
AudioDeviceModule::WindowsDeviceType /*device*/) {
|
|
RTC_LOG(LS_ERROR) << "WindowsDeviceType not supported";
|
|
return -1;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::PlayoutIsAvailable(bool& available) {
|
|
available = true;
|
|
|
|
// Try to initialize the playout side
|
|
if (InitPlayout() == -1) {
|
|
available = false;
|
|
}
|
|
|
|
// We destroy the IOProc created by InitPlayout() in implDeviceIOProc().
|
|
// We must actually start playout here in order to have the IOProc
|
|
// deleted by calling StopPlayout().
|
|
if (StartPlayout() == -1) {
|
|
available = false;
|
|
}
|
|
|
|
// Cancel effect of initialization
|
|
if (StopPlayout() == -1) {
|
|
available = false;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::RecordingIsAvailable(bool& available) {
|
|
available = true;
|
|
|
|
// Try to initialize the recording side
|
|
if (InitRecording() == -1) {
|
|
available = false;
|
|
}
|
|
|
|
// We destroy the IOProc created by InitRecording() in implInDeviceIOProc().
|
|
// We must actually start recording here in order to have the IOProc
|
|
// deleted by calling StopRecording().
|
|
if (StartRecording() == -1) {
|
|
available = false;
|
|
}
|
|
|
|
// Cancel effect of initialization
|
|
if (StopRecording() == -1) {
|
|
available = false;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::InitPlayout() {
|
|
RTC_LOG(LS_INFO) << "InitPlayout";
|
|
MutexLock lock(&mutex_);
|
|
|
|
if (_playing) {
|
|
return -1;
|
|
}
|
|
|
|
if (!_outputDeviceIsSpecified) {
|
|
return -1;
|
|
}
|
|
|
|
if (_playIsInitialized) {
|
|
return 0;
|
|
}
|
|
|
|
// Initialize the speaker (devices might have been added or removed)
|
|
if (InitSpeakerLocked() == -1) {
|
|
RTC_LOG(LS_WARNING) << "InitSpeaker() failed";
|
|
}
|
|
|
|
if (!MicrophoneIsInitialized()) {
|
|
// Make this call to check if we are using
|
|
// one or two devices (_twoDevices)
|
|
bool available = false;
|
|
if (MicrophoneIsAvailableLocked(available) == -1) {
|
|
RTC_LOG(LS_WARNING) << "MicrophoneIsAvailable() failed";
|
|
}
|
|
}
|
|
|
|
PaUtil_FlushRingBuffer(_paRenderBuffer);
|
|
|
|
OSStatus err = noErr;
|
|
UInt32 size = 0;
|
|
_renderDelayOffsetSamples = 0;
|
|
_renderDelayUs = 0;
|
|
_renderLatencyUs = 0;
|
|
_renderDeviceIsAlive = 1;
|
|
_doStop = false;
|
|
|
|
// The internal microphone of a MacBook Pro is located under the left speaker
|
|
// grille. When the internal speakers are in use, we want to fully stereo
|
|
// pan to the right.
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
kAudioDevicePropertyDataSource, kAudioDevicePropertyScopeOutput, 0};
|
|
if (_macBookPro) {
|
|
_macBookProPanRight = false;
|
|
Boolean hasProperty =
|
|
AudioObjectHasProperty(_outputDeviceID, &propertyAddress);
|
|
if (hasProperty) {
|
|
UInt32 dataSource = 0;
|
|
size = sizeof(dataSource);
|
|
WEBRTC_CA_LOG_WARN(AudioObjectGetPropertyData(
|
|
_outputDeviceID, &propertyAddress, 0, NULL, &size, &dataSource));
|
|
|
|
if (dataSource == 'ispk') {
|
|
_macBookProPanRight = true;
|
|
RTC_LOG(LS_VERBOSE)
|
|
<< "MacBook Pro using internal speakers; stereo panning right";
|
|
} else {
|
|
RTC_LOG(LS_VERBOSE) << "MacBook Pro not using internal speakers";
|
|
}
|
|
|
|
// Add a listener to determine if the status changes.
|
|
WEBRTC_CA_LOG_WARN(AudioObjectAddPropertyListener(
|
|
_outputDeviceID, &propertyAddress, &objectListenerProc, this));
|
|
}
|
|
}
|
|
|
|
// Get current stream description
|
|
propertyAddress.mSelector = kAudioDevicePropertyStreamFormat;
|
|
memset(&_outStreamFormat, 0, sizeof(_outStreamFormat));
|
|
size = sizeof(_outStreamFormat);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
_outputDeviceID, &propertyAddress, 0, NULL, &size, &_outStreamFormat));
|
|
|
|
if (_outStreamFormat.mFormatID != kAudioFormatLinearPCM) {
|
|
logCAMsg(rtc::LS_ERROR, "Unacceptable output stream format -> mFormatID",
|
|
(const char*)&_outStreamFormat.mFormatID);
|
|
return -1;
|
|
}
|
|
|
|
if (_outStreamFormat.mChannelsPerFrame > N_DEVICE_CHANNELS) {
|
|
RTC_LOG(LS_ERROR)
|
|
<< "Too many channels on output device (mChannelsPerFrame = "
|
|
<< _outStreamFormat.mChannelsPerFrame << ")";
|
|
return -1;
|
|
}
|
|
|
|
if (_outStreamFormat.mFormatFlags & kAudioFormatFlagIsNonInterleaved) {
|
|
RTC_LOG(LS_ERROR) << "Non-interleaved audio data is not supported."
|
|
"AudioHardware streams should not have this format.";
|
|
return -1;
|
|
}
|
|
|
|
RTC_LOG(LS_VERBOSE) << "Ouput stream format:";
|
|
RTC_LOG(LS_VERBOSE) << "mSampleRate = " << _outStreamFormat.mSampleRate
|
|
<< ", mChannelsPerFrame = "
|
|
<< _outStreamFormat.mChannelsPerFrame;
|
|
RTC_LOG(LS_VERBOSE) << "mBytesPerPacket = "
|
|
<< _outStreamFormat.mBytesPerPacket
|
|
<< ", mFramesPerPacket = "
|
|
<< _outStreamFormat.mFramesPerPacket;
|
|
RTC_LOG(LS_VERBOSE) << "mBytesPerFrame = " << _outStreamFormat.mBytesPerFrame
|
|
<< ", mBitsPerChannel = "
|
|
<< _outStreamFormat.mBitsPerChannel;
|
|
RTC_LOG(LS_VERBOSE) << "mFormatFlags = " << _outStreamFormat.mFormatFlags;
|
|
logCAMsg(rtc::LS_VERBOSE, "mFormatID",
|
|
(const char*)&_outStreamFormat.mFormatID);
|
|
|
|
// Our preferred format to work with.
|
|
if (_outStreamFormat.mChannelsPerFrame < 2) {
|
|
// Disable stereo playout when we only have one channel on the device.
|
|
_playChannels = 1;
|
|
RTC_LOG(LS_VERBOSE) << "Stereo playout unavailable on this device";
|
|
}
|
|
WEBRTC_CA_RETURN_ON_ERR(SetDesiredPlayoutFormat());
|
|
|
|
// Listen for format changes.
|
|
propertyAddress.mSelector = kAudioDevicePropertyStreamFormat;
|
|
WEBRTC_CA_LOG_WARN(AudioObjectAddPropertyListener(
|
|
_outputDeviceID, &propertyAddress, &objectListenerProc, this));
|
|
|
|
// Listen for processor overloads.
|
|
propertyAddress.mSelector = kAudioDeviceProcessorOverload;
|
|
WEBRTC_CA_LOG_WARN(AudioObjectAddPropertyListener(
|
|
_outputDeviceID, &propertyAddress, &objectListenerProc, this));
|
|
|
|
if (_twoDevices || !_recIsInitialized) {
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioDeviceCreateIOProcID(
|
|
_outputDeviceID, deviceIOProc, this, &_deviceIOProcID));
|
|
}
|
|
|
|
_playIsInitialized = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::InitRecording() {
|
|
RTC_LOG(LS_INFO) << "InitRecording";
|
|
MutexLock lock(&mutex_);
|
|
|
|
if (_recording) {
|
|
return -1;
|
|
}
|
|
|
|
if (!_inputDeviceIsSpecified) {
|
|
return -1;
|
|
}
|
|
|
|
if (_recIsInitialized) {
|
|
return 0;
|
|
}
|
|
|
|
// Initialize the microphone (devices might have been added or removed)
|
|
if (InitMicrophoneLocked() == -1) {
|
|
RTC_LOG(LS_WARNING) << "InitMicrophone() failed";
|
|
}
|
|
|
|
if (!SpeakerIsInitialized()) {
|
|
// Make this call to check if we are using
|
|
// one or two devices (_twoDevices)
|
|
bool available = false;
|
|
if (SpeakerIsAvailableLocked(available) == -1) {
|
|
RTC_LOG(LS_WARNING) << "SpeakerIsAvailable() failed";
|
|
}
|
|
}
|
|
|
|
OSStatus err = noErr;
|
|
UInt32 size = 0;
|
|
|
|
PaUtil_FlushRingBuffer(_paCaptureBuffer);
|
|
|
|
_captureDelayUs = 0;
|
|
_captureLatencyUs = 0;
|
|
_captureDeviceIsAlive = 1;
|
|
_doStopRec = false;
|
|
|
|
// Get current stream description
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
kAudioDevicePropertyStreamFormat, kAudioDevicePropertyScopeInput, 0};
|
|
memset(&_inStreamFormat, 0, sizeof(_inStreamFormat));
|
|
size = sizeof(_inStreamFormat);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
_inputDeviceID, &propertyAddress, 0, NULL, &size, &_inStreamFormat));
|
|
|
|
if (_inStreamFormat.mFormatID != kAudioFormatLinearPCM) {
|
|
logCAMsg(rtc::LS_ERROR, "Unacceptable input stream format -> mFormatID",
|
|
(const char*)&_inStreamFormat.mFormatID);
|
|
return -1;
|
|
}
|
|
|
|
if (_inStreamFormat.mChannelsPerFrame > N_DEVICE_CHANNELS) {
|
|
RTC_LOG(LS_ERROR)
|
|
<< "Too many channels on input device (mChannelsPerFrame = "
|
|
<< _inStreamFormat.mChannelsPerFrame << ")";
|
|
return -1;
|
|
}
|
|
|
|
const int io_block_size_samples = _inStreamFormat.mChannelsPerFrame *
|
|
_inStreamFormat.mSampleRate / 100 *
|
|
N_BLOCKS_IO;
|
|
if (io_block_size_samples > _captureBufSizeSamples) {
|
|
RTC_LOG(LS_ERROR) << "Input IO block size (" << io_block_size_samples
|
|
<< ") is larger than ring buffer ("
|
|
<< _captureBufSizeSamples << ")";
|
|
return -1;
|
|
}
|
|
|
|
RTC_LOG(LS_VERBOSE) << "Input stream format:";
|
|
RTC_LOG(LS_VERBOSE) << "mSampleRate = " << _inStreamFormat.mSampleRate
|
|
<< ", mChannelsPerFrame = "
|
|
<< _inStreamFormat.mChannelsPerFrame;
|
|
RTC_LOG(LS_VERBOSE) << "mBytesPerPacket = " << _inStreamFormat.mBytesPerPacket
|
|
<< ", mFramesPerPacket = "
|
|
<< _inStreamFormat.mFramesPerPacket;
|
|
RTC_LOG(LS_VERBOSE) << "mBytesPerFrame = " << _inStreamFormat.mBytesPerFrame
|
|
<< ", mBitsPerChannel = "
|
|
<< _inStreamFormat.mBitsPerChannel;
|
|
RTC_LOG(LS_VERBOSE) << "mFormatFlags = " << _inStreamFormat.mFormatFlags;
|
|
logCAMsg(rtc::LS_VERBOSE, "mFormatID",
|
|
(const char*)&_inStreamFormat.mFormatID);
|
|
|
|
// Our preferred format to work with
|
|
if (_inStreamFormat.mChannelsPerFrame >= 2 && (_recChannels == 2)) {
|
|
_inDesiredFormat.mChannelsPerFrame = 2;
|
|
} else {
|
|
// Disable stereo recording when we only have one channel on the device.
|
|
_inDesiredFormat.mChannelsPerFrame = 1;
|
|
_recChannels = 1;
|
|
RTC_LOG(LS_VERBOSE) << "Stereo recording unavailable on this device";
|
|
}
|
|
|
|
if (_ptrAudioBuffer) {
|
|
// Update audio buffer with the selected parameters
|
|
_ptrAudioBuffer->SetRecordingSampleRate(N_REC_SAMPLES_PER_SEC);
|
|
_ptrAudioBuffer->SetRecordingChannels((uint8_t)_recChannels);
|
|
}
|
|
|
|
_inDesiredFormat.mSampleRate = N_REC_SAMPLES_PER_SEC;
|
|
_inDesiredFormat.mBytesPerPacket =
|
|
_inDesiredFormat.mChannelsPerFrame * sizeof(SInt16);
|
|
_inDesiredFormat.mFramesPerPacket = 1;
|
|
_inDesiredFormat.mBytesPerFrame =
|
|
_inDesiredFormat.mChannelsPerFrame * sizeof(SInt16);
|
|
_inDesiredFormat.mBitsPerChannel = sizeof(SInt16) * 8;
|
|
|
|
_inDesiredFormat.mFormatFlags =
|
|
kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
|
|
#ifdef WEBRTC_ARCH_BIG_ENDIAN
|
|
_inDesiredFormat.mFormatFlags |= kLinearPCMFormatFlagIsBigEndian;
|
|
#endif
|
|
_inDesiredFormat.mFormatID = kAudioFormatLinearPCM;
|
|
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioConverterNew(&_inStreamFormat, &_inDesiredFormat,
|
|
&_captureConverter));
|
|
|
|
// First try to set buffer size to desired value (10 ms * N_BLOCKS_IO)
|
|
// TODO(xians): investigate this block.
|
|
UInt32 bufByteCount =
|
|
(UInt32)((_inStreamFormat.mSampleRate / 1000.0) * 10.0 * N_BLOCKS_IO *
|
|
_inStreamFormat.mChannelsPerFrame * sizeof(Float32));
|
|
if (_inStreamFormat.mFramesPerPacket != 0) {
|
|
if (bufByteCount % _inStreamFormat.mFramesPerPacket != 0) {
|
|
bufByteCount =
|
|
((UInt32)(bufByteCount / _inStreamFormat.mFramesPerPacket) + 1) *
|
|
_inStreamFormat.mFramesPerPacket;
|
|
}
|
|
}
|
|
|
|
// Ensure the buffer size is within the acceptable range provided by the
|
|
// device.
|
|
propertyAddress.mSelector = kAudioDevicePropertyBufferSizeRange;
|
|
AudioValueRange range;
|
|
size = sizeof(range);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
_inputDeviceID, &propertyAddress, 0, NULL, &size, &range));
|
|
if (range.mMinimum > bufByteCount) {
|
|
bufByteCount = range.mMinimum;
|
|
} else if (range.mMaximum < bufByteCount) {
|
|
bufByteCount = range.mMaximum;
|
|
}
|
|
|
|
propertyAddress.mSelector = kAudioDevicePropertyBufferSize;
|
|
size = sizeof(bufByteCount);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectSetPropertyData(
|
|
_inputDeviceID, &propertyAddress, 0, NULL, size, &bufByteCount));
|
|
|
|
// Get capture device latency
|
|
propertyAddress.mSelector = kAudioDevicePropertyLatency;
|
|
UInt32 latency = 0;
|
|
size = sizeof(UInt32);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
_inputDeviceID, &propertyAddress, 0, NULL, &size, &latency));
|
|
_captureLatencyUs = (UInt32)((1.0e6 * latency) / _inStreamFormat.mSampleRate);
|
|
|
|
// Get capture stream latency
|
|
propertyAddress.mSelector = kAudioDevicePropertyStreams;
|
|
AudioStreamID stream = 0;
|
|
size = sizeof(AudioStreamID);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
_inputDeviceID, &propertyAddress, 0, NULL, &size, &stream));
|
|
propertyAddress.mSelector = kAudioStreamPropertyLatency;
|
|
size = sizeof(UInt32);
|
|
latency = 0;
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
_inputDeviceID, &propertyAddress, 0, NULL, &size, &latency));
|
|
_captureLatencyUs +=
|
|
(UInt32)((1.0e6 * latency) / _inStreamFormat.mSampleRate);
|
|
|
|
// Listen for format changes
|
|
// TODO(xians): should we be using kAudioDevicePropertyDeviceHasChanged?
|
|
propertyAddress.mSelector = kAudioDevicePropertyStreamFormat;
|
|
WEBRTC_CA_LOG_WARN(AudioObjectAddPropertyListener(
|
|
_inputDeviceID, &propertyAddress, &objectListenerProc, this));
|
|
|
|
// Listen for processor overloads
|
|
propertyAddress.mSelector = kAudioDeviceProcessorOverload;
|
|
WEBRTC_CA_LOG_WARN(AudioObjectAddPropertyListener(
|
|
_inputDeviceID, &propertyAddress, &objectListenerProc, this));
|
|
|
|
if (_twoDevices) {
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioDeviceCreateIOProcID(
|
|
_inputDeviceID, inDeviceIOProc, this, &_inDeviceIOProcID));
|
|
} else if (!_playIsInitialized) {
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioDeviceCreateIOProcID(
|
|
_inputDeviceID, deviceIOProc, this, &_deviceIOProcID));
|
|
}
|
|
|
|
// Mark recording side as initialized
|
|
_recIsInitialized = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::StartRecording() {
|
|
RTC_LOG(LS_INFO) << "StartRecording";
|
|
MutexLock lock(&mutex_);
|
|
|
|
if (!_recIsInitialized) {
|
|
return -1;
|
|
}
|
|
|
|
if (_recording) {
|
|
return 0;
|
|
}
|
|
|
|
if (!_initialized) {
|
|
RTC_LOG(LS_ERROR) << "Recording worker thread has not been started";
|
|
return -1;
|
|
}
|
|
|
|
RTC_DCHECK(!capture_worker_thread_.get());
|
|
capture_worker_thread_.reset(new rtc::PlatformThread(
|
|
RunCapture, this, "CaptureWorkerThread",
|
|
rtc::ThreadAttributes().SetPriority(rtc::kRealtimePriority)));
|
|
RTC_DCHECK(capture_worker_thread_.get());
|
|
capture_worker_thread_->Start();
|
|
|
|
OSStatus err = noErr;
|
|
if (_twoDevices) {
|
|
WEBRTC_CA_RETURN_ON_ERR(
|
|
AudioDeviceStart(_inputDeviceID, _inDeviceIOProcID));
|
|
} else if (!_playing) {
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioDeviceStart(_inputDeviceID, _deviceIOProcID));
|
|
}
|
|
|
|
_recording = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::StopRecording() {
|
|
RTC_LOG(LS_INFO) << "StopRecording";
|
|
MutexLock lock(&mutex_);
|
|
|
|
if (!_recIsInitialized) {
|
|
return 0;
|
|
}
|
|
|
|
OSStatus err = noErr;
|
|
int32_t captureDeviceIsAlive = AtomicGet32(&_captureDeviceIsAlive);
|
|
if (_twoDevices && captureDeviceIsAlive == 1) {
|
|
// Recording side uses its own dedicated device and IOProc.
|
|
if (_recording) {
|
|
_recording = false;
|
|
_doStopRec = true; // Signal to io proc to stop audio device
|
|
mutex_.Unlock(); // Cannot be under lock, risk of deadlock
|
|
if (!_stopEventRec.Wait(2000)) {
|
|
MutexLock lockScoped(&mutex_);
|
|
RTC_LOG(LS_WARNING) << "Timed out stopping the capture IOProc."
|
|
"We may have failed to detect a device removal.";
|
|
WEBRTC_CA_LOG_WARN(AudioDeviceStop(_inputDeviceID, _inDeviceIOProcID));
|
|
WEBRTC_CA_LOG_WARN(
|
|
AudioDeviceDestroyIOProcID(_inputDeviceID, _inDeviceIOProcID));
|
|
}
|
|
mutex_.Lock();
|
|
_doStopRec = false;
|
|
RTC_LOG(LS_INFO) << "Recording stopped (input device)";
|
|
} else if (_recIsInitialized) {
|
|
WEBRTC_CA_LOG_WARN(
|
|
AudioDeviceDestroyIOProcID(_inputDeviceID, _inDeviceIOProcID));
|
|
RTC_LOG(LS_INFO) << "Recording uninitialized (input device)";
|
|
}
|
|
} else {
|
|
// We signal a stop for a shared device even when rendering has
|
|
// not yet ended. This is to ensure the IOProc will return early as
|
|
// intended (by checking |_recording|) before accessing
|
|
// resources we free below (e.g. the capture converter).
|
|
//
|
|
// In the case of a shared devcie, the IOProc will verify
|
|
// rendering has ended before stopping itself.
|
|
if (_recording && captureDeviceIsAlive == 1) {
|
|
_recording = false;
|
|
_doStop = true; // Signal to io proc to stop audio device
|
|
mutex_.Unlock(); // Cannot be under lock, risk of deadlock
|
|
if (!_stopEvent.Wait(2000)) {
|
|
MutexLock lockScoped(&mutex_);
|
|
RTC_LOG(LS_WARNING) << "Timed out stopping the shared IOProc."
|
|
"We may have failed to detect a device removal.";
|
|
// We assume rendering on a shared device has stopped as well if
|
|
// the IOProc times out.
|
|
WEBRTC_CA_LOG_WARN(AudioDeviceStop(_outputDeviceID, _deviceIOProcID));
|
|
WEBRTC_CA_LOG_WARN(
|
|
AudioDeviceDestroyIOProcID(_outputDeviceID, _deviceIOProcID));
|
|
}
|
|
mutex_.Lock();
|
|
_doStop = false;
|
|
RTC_LOG(LS_INFO) << "Recording stopped (shared device)";
|
|
} else if (_recIsInitialized && !_playing && !_playIsInitialized) {
|
|
WEBRTC_CA_LOG_WARN(
|
|
AudioDeviceDestroyIOProcID(_outputDeviceID, _deviceIOProcID));
|
|
RTC_LOG(LS_INFO) << "Recording uninitialized (shared device)";
|
|
}
|
|
}
|
|
|
|
// Setting this signal will allow the worker thread to be stopped.
|
|
AtomicSet32(&_captureDeviceIsAlive, 0);
|
|
|
|
if (capture_worker_thread_.get()) {
|
|
mutex_.Unlock();
|
|
capture_worker_thread_->Stop();
|
|
capture_worker_thread_.reset();
|
|
mutex_.Lock();
|
|
}
|
|
|
|
WEBRTC_CA_LOG_WARN(AudioConverterDispose(_captureConverter));
|
|
|
|
// Remove listeners.
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
kAudioDevicePropertyStreamFormat, kAudioDevicePropertyScopeInput, 0};
|
|
WEBRTC_CA_LOG_WARN(AudioObjectRemovePropertyListener(
|
|
_inputDeviceID, &propertyAddress, &objectListenerProc, this));
|
|
|
|
propertyAddress.mSelector = kAudioDeviceProcessorOverload;
|
|
WEBRTC_CA_LOG_WARN(AudioObjectRemovePropertyListener(
|
|
_inputDeviceID, &propertyAddress, &objectListenerProc, this));
|
|
|
|
_recIsInitialized = false;
|
|
_recording = false;
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool AudioDeviceMac::RecordingIsInitialized() const {
|
|
return (_recIsInitialized);
|
|
}
|
|
|
|
bool AudioDeviceMac::Recording() const {
|
|
return (_recording);
|
|
}
|
|
|
|
bool AudioDeviceMac::PlayoutIsInitialized() const {
|
|
return (_playIsInitialized);
|
|
}
|
|
|
|
int32_t AudioDeviceMac::StartPlayout() {
|
|
RTC_LOG(LS_INFO) << "StartPlayout";
|
|
MutexLock lock(&mutex_);
|
|
|
|
if (!_playIsInitialized) {
|
|
return -1;
|
|
}
|
|
|
|
if (_playing) {
|
|
return 0;
|
|
}
|
|
|
|
RTC_DCHECK(!render_worker_thread_.get());
|
|
render_worker_thread_.reset(new rtc::PlatformThread(
|
|
RunRender, this, "RenderWorkerThread",
|
|
rtc::ThreadAttributes().SetPriority(rtc::kRealtimePriority)));
|
|
render_worker_thread_->Start();
|
|
|
|
if (_twoDevices || !_recording) {
|
|
OSStatus err = noErr;
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioDeviceStart(_outputDeviceID, _deviceIOProcID));
|
|
}
|
|
_playing = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::StopPlayout() {
|
|
RTC_LOG(LS_INFO) << "StopPlayout";
|
|
MutexLock lock(&mutex_);
|
|
|
|
if (!_playIsInitialized) {
|
|
return 0;
|
|
}
|
|
|
|
OSStatus err = noErr;
|
|
int32_t renderDeviceIsAlive = AtomicGet32(&_renderDeviceIsAlive);
|
|
if (_playing && renderDeviceIsAlive == 1) {
|
|
// We signal a stop for a shared device even when capturing has not
|
|
// yet ended. This is to ensure the IOProc will return early as
|
|
// intended (by checking |_playing|) before accessing resources we
|
|
// free below (e.g. the render converter).
|
|
//
|
|
// In the case of a shared device, the IOProc will verify capturing
|
|
// has ended before stopping itself.
|
|
_playing = false;
|
|
_doStop = true; // Signal to io proc to stop audio device
|
|
mutex_.Unlock(); // Cannot be under lock, risk of deadlock
|
|
if (!_stopEvent.Wait(2000)) {
|
|
MutexLock lockScoped(&mutex_);
|
|
RTC_LOG(LS_WARNING) << "Timed out stopping the render IOProc."
|
|
"We may have failed to detect a device removal.";
|
|
|
|
// We assume capturing on a shared device has stopped as well if the
|
|
// IOProc times out.
|
|
WEBRTC_CA_LOG_WARN(AudioDeviceStop(_outputDeviceID, _deviceIOProcID));
|
|
WEBRTC_CA_LOG_WARN(
|
|
AudioDeviceDestroyIOProcID(_outputDeviceID, _deviceIOProcID));
|
|
}
|
|
mutex_.Lock();
|
|
_doStop = false;
|
|
RTC_LOG(LS_INFO) << "Playout stopped";
|
|
} else if (_twoDevices && _playIsInitialized) {
|
|
WEBRTC_CA_LOG_WARN(
|
|
AudioDeviceDestroyIOProcID(_outputDeviceID, _deviceIOProcID));
|
|
RTC_LOG(LS_INFO) << "Playout uninitialized (output device)";
|
|
} else if (!_twoDevices && _playIsInitialized && !_recIsInitialized) {
|
|
WEBRTC_CA_LOG_WARN(
|
|
AudioDeviceDestroyIOProcID(_outputDeviceID, _deviceIOProcID));
|
|
RTC_LOG(LS_INFO) << "Playout uninitialized (shared device)";
|
|
}
|
|
|
|
// Setting this signal will allow the worker thread to be stopped.
|
|
AtomicSet32(&_renderDeviceIsAlive, 0);
|
|
if (render_worker_thread_.get()) {
|
|
mutex_.Unlock();
|
|
render_worker_thread_->Stop();
|
|
render_worker_thread_.reset();
|
|
mutex_.Lock();
|
|
}
|
|
|
|
WEBRTC_CA_LOG_WARN(AudioConverterDispose(_renderConverter));
|
|
|
|
// Remove listeners.
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
kAudioDevicePropertyStreamFormat, kAudioDevicePropertyScopeOutput, 0};
|
|
WEBRTC_CA_LOG_WARN(AudioObjectRemovePropertyListener(
|
|
_outputDeviceID, &propertyAddress, &objectListenerProc, this));
|
|
|
|
propertyAddress.mSelector = kAudioDeviceProcessorOverload;
|
|
WEBRTC_CA_LOG_WARN(AudioObjectRemovePropertyListener(
|
|
_outputDeviceID, &propertyAddress, &objectListenerProc, this));
|
|
|
|
if (_macBookPro) {
|
|
Boolean hasProperty =
|
|
AudioObjectHasProperty(_outputDeviceID, &propertyAddress);
|
|
if (hasProperty) {
|
|
propertyAddress.mSelector = kAudioDevicePropertyDataSource;
|
|
WEBRTC_CA_LOG_WARN(AudioObjectRemovePropertyListener(
|
|
_outputDeviceID, &propertyAddress, &objectListenerProc, this));
|
|
}
|
|
}
|
|
|
|
_playIsInitialized = false;
|
|
_playing = false;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::PlayoutDelay(uint16_t& delayMS) const {
|
|
int32_t renderDelayUs = AtomicGet32(&_renderDelayUs);
|
|
delayMS =
|
|
static_cast<uint16_t>(1e-3 * (renderDelayUs + _renderLatencyUs) + 0.5);
|
|
return 0;
|
|
}
|
|
|
|
bool AudioDeviceMac::Playing() const {
|
|
return (_playing);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Private Methods
|
|
// ============================================================================
|
|
|
|
int32_t AudioDeviceMac::GetNumberDevices(const AudioObjectPropertyScope scope,
|
|
AudioDeviceID scopedDeviceIds[],
|
|
const uint32_t deviceListLength) {
|
|
OSStatus err = noErr;
|
|
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal,
|
|
kAudioObjectPropertyElementMaster};
|
|
UInt32 size = 0;
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyDataSize(
|
|
kAudioObjectSystemObject, &propertyAddress, 0, NULL, &size));
|
|
if (size == 0) {
|
|
RTC_LOG(LS_WARNING) << "No devices";
|
|
return 0;
|
|
}
|
|
|
|
UInt32 numberDevices = size / sizeof(AudioDeviceID);
|
|
const auto deviceIds = std::make_unique<AudioDeviceID[]>(numberDevices);
|
|
AudioBufferList* bufferList = NULL;
|
|
UInt32 numberScopedDevices = 0;
|
|
|
|
// First check if there is a default device and list it
|
|
UInt32 hardwareProperty = 0;
|
|
if (scope == kAudioDevicePropertyScopeOutput) {
|
|
hardwareProperty = kAudioHardwarePropertyDefaultOutputDevice;
|
|
} else {
|
|
hardwareProperty = kAudioHardwarePropertyDefaultInputDevice;
|
|
}
|
|
|
|
AudioObjectPropertyAddress propertyAddressDefault = {
|
|
hardwareProperty, kAudioObjectPropertyScopeGlobal,
|
|
kAudioObjectPropertyElementMaster};
|
|
|
|
AudioDeviceID usedID;
|
|
UInt32 uintSize = sizeof(UInt32);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(kAudioObjectSystemObject,
|
|
&propertyAddressDefault, 0,
|
|
NULL, &uintSize, &usedID));
|
|
if (usedID != kAudioDeviceUnknown) {
|
|
scopedDeviceIds[numberScopedDevices] = usedID;
|
|
numberScopedDevices++;
|
|
} else {
|
|
RTC_LOG(LS_WARNING) << "GetNumberDevices(): Default device unknown";
|
|
}
|
|
|
|
// Then list the rest of the devices
|
|
bool listOK = true;
|
|
|
|
WEBRTC_CA_LOG_ERR(AudioObjectGetPropertyData(kAudioObjectSystemObject,
|
|
&propertyAddress, 0, NULL, &size,
|
|
deviceIds.get()));
|
|
if (err != noErr) {
|
|
listOK = false;
|
|
} else {
|
|
propertyAddress.mSelector = kAudioDevicePropertyStreamConfiguration;
|
|
propertyAddress.mScope = scope;
|
|
propertyAddress.mElement = 0;
|
|
for (UInt32 i = 0; i < numberDevices; i++) {
|
|
// Check for input channels
|
|
WEBRTC_CA_LOG_ERR(AudioObjectGetPropertyDataSize(
|
|
deviceIds[i], &propertyAddress, 0, NULL, &size));
|
|
if (err == kAudioHardwareBadDeviceError) {
|
|
// This device doesn't actually exist; continue iterating.
|
|
continue;
|
|
} else if (err != noErr) {
|
|
listOK = false;
|
|
break;
|
|
}
|
|
|
|
bufferList = (AudioBufferList*)malloc(size);
|
|
WEBRTC_CA_LOG_ERR(AudioObjectGetPropertyData(
|
|
deviceIds[i], &propertyAddress, 0, NULL, &size, bufferList));
|
|
if (err != noErr) {
|
|
listOK = false;
|
|
break;
|
|
}
|
|
|
|
if (bufferList->mNumberBuffers > 0) {
|
|
if (numberScopedDevices >= deviceListLength) {
|
|
RTC_LOG(LS_ERROR) << "Device list is not long enough";
|
|
listOK = false;
|
|
break;
|
|
}
|
|
|
|
scopedDeviceIds[numberScopedDevices] = deviceIds[i];
|
|
numberScopedDevices++;
|
|
}
|
|
|
|
free(bufferList);
|
|
bufferList = NULL;
|
|
} // for
|
|
}
|
|
|
|
if (!listOK) {
|
|
if (bufferList) {
|
|
free(bufferList);
|
|
bufferList = NULL;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
return numberScopedDevices;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::GetDeviceName(const AudioObjectPropertyScope scope,
|
|
const uint16_t index,
|
|
char* name) {
|
|
OSStatus err = noErr;
|
|
UInt32 len = kAdmMaxDeviceNameSize;
|
|
AudioDeviceID deviceIds[MaxNumberDevices];
|
|
|
|
int numberDevices = GetNumberDevices(scope, deviceIds, MaxNumberDevices);
|
|
if (numberDevices < 0) {
|
|
return -1;
|
|
} else if (numberDevices == 0) {
|
|
RTC_LOG(LS_ERROR) << "No devices";
|
|
return -1;
|
|
}
|
|
|
|
// If the number is below the number of devices, assume it's "WEBRTC ID"
|
|
// otherwise assume it's a CoreAudio ID
|
|
AudioDeviceID usedID;
|
|
|
|
// Check if there is a default device
|
|
bool isDefaultDevice = false;
|
|
if (index == 0) {
|
|
UInt32 hardwareProperty = 0;
|
|
if (scope == kAudioDevicePropertyScopeOutput) {
|
|
hardwareProperty = kAudioHardwarePropertyDefaultOutputDevice;
|
|
} else {
|
|
hardwareProperty = kAudioHardwarePropertyDefaultInputDevice;
|
|
}
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
hardwareProperty, kAudioObjectPropertyScopeGlobal,
|
|
kAudioObjectPropertyElementMaster};
|
|
UInt32 size = sizeof(UInt32);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
kAudioObjectSystemObject, &propertyAddress, 0, NULL, &size, &usedID));
|
|
if (usedID == kAudioDeviceUnknown) {
|
|
RTC_LOG(LS_WARNING) << "GetDeviceName(): Default device unknown";
|
|
} else {
|
|
isDefaultDevice = true;
|
|
}
|
|
}
|
|
|
|
AudioObjectPropertyAddress propertyAddress = {kAudioDevicePropertyDeviceName,
|
|
scope, 0};
|
|
|
|
if (isDefaultDevice) {
|
|
char devName[len];
|
|
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(usedID, &propertyAddress,
|
|
0, NULL, &len, devName));
|
|
|
|
sprintf(name, "default (%s)", devName);
|
|
} else {
|
|
if (index < numberDevices) {
|
|
usedID = deviceIds[index];
|
|
} else {
|
|
usedID = index;
|
|
}
|
|
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(usedID, &propertyAddress,
|
|
0, NULL, &len, name));
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::InitDevice(const uint16_t userDeviceIndex,
|
|
AudioDeviceID& deviceId,
|
|
const bool isInput) {
|
|
OSStatus err = noErr;
|
|
UInt32 size = 0;
|
|
AudioObjectPropertyScope deviceScope;
|
|
AudioObjectPropertySelector defaultDeviceSelector;
|
|
AudioDeviceID deviceIds[MaxNumberDevices];
|
|
|
|
if (isInput) {
|
|
deviceScope = kAudioDevicePropertyScopeInput;
|
|
defaultDeviceSelector = kAudioHardwarePropertyDefaultInputDevice;
|
|
} else {
|
|
deviceScope = kAudioDevicePropertyScopeOutput;
|
|
defaultDeviceSelector = kAudioHardwarePropertyDefaultOutputDevice;
|
|
}
|
|
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
defaultDeviceSelector, kAudioObjectPropertyScopeGlobal,
|
|
kAudioObjectPropertyElementMaster};
|
|
|
|
// Get the actual device IDs
|
|
int numberDevices =
|
|
GetNumberDevices(deviceScope, deviceIds, MaxNumberDevices);
|
|
if (numberDevices < 0) {
|
|
return -1;
|
|
} else if (numberDevices == 0) {
|
|
RTC_LOG(LS_ERROR) << "InitDevice(): No devices";
|
|
return -1;
|
|
}
|
|
|
|
bool isDefaultDevice = false;
|
|
deviceId = kAudioDeviceUnknown;
|
|
if (userDeviceIndex == 0) {
|
|
// Try to use default system device
|
|
size = sizeof(AudioDeviceID);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
kAudioObjectSystemObject, &propertyAddress, 0, NULL, &size, &deviceId));
|
|
if (deviceId == kAudioDeviceUnknown) {
|
|
RTC_LOG(LS_WARNING) << "No default device exists";
|
|
} else {
|
|
isDefaultDevice = true;
|
|
}
|
|
}
|
|
|
|
if (!isDefaultDevice) {
|
|
deviceId = deviceIds[userDeviceIndex];
|
|
}
|
|
|
|
// Obtain device name and manufacturer for logging.
|
|
// Also use this as a test to ensure a user-set device ID is valid.
|
|
char devName[128];
|
|
char devManf[128];
|
|
memset(devName, 0, sizeof(devName));
|
|
memset(devManf, 0, sizeof(devManf));
|
|
|
|
propertyAddress.mSelector = kAudioDevicePropertyDeviceName;
|
|
propertyAddress.mScope = deviceScope;
|
|
propertyAddress.mElement = 0;
|
|
size = sizeof(devName);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(deviceId, &propertyAddress,
|
|
0, NULL, &size, devName));
|
|
|
|
propertyAddress.mSelector = kAudioDevicePropertyDeviceManufacturer;
|
|
size = sizeof(devManf);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(deviceId, &propertyAddress,
|
|
0, NULL, &size, devManf));
|
|
|
|
if (isInput) {
|
|
RTC_LOG(LS_INFO) << "Input device: " << devManf << " " << devName;
|
|
} else {
|
|
RTC_LOG(LS_INFO) << "Output device: " << devManf << " " << devName;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
OSStatus AudioDeviceMac::SetDesiredPlayoutFormat() {
|
|
// Our preferred format to work with.
|
|
_outDesiredFormat.mSampleRate = N_PLAY_SAMPLES_PER_SEC;
|
|
_outDesiredFormat.mChannelsPerFrame = _playChannels;
|
|
|
|
if (_ptrAudioBuffer) {
|
|
// Update audio buffer with the selected parameters.
|
|
_ptrAudioBuffer->SetPlayoutSampleRate(N_PLAY_SAMPLES_PER_SEC);
|
|
_ptrAudioBuffer->SetPlayoutChannels((uint8_t)_playChannels);
|
|
}
|
|
|
|
_renderDelayOffsetSamples =
|
|
_renderBufSizeSamples - N_BUFFERS_OUT * ENGINE_PLAY_BUF_SIZE_IN_SAMPLES *
|
|
_outDesiredFormat.mChannelsPerFrame;
|
|
|
|
_outDesiredFormat.mBytesPerPacket =
|
|
_outDesiredFormat.mChannelsPerFrame * sizeof(SInt16);
|
|
// In uncompressed audio, a packet is one frame.
|
|
_outDesiredFormat.mFramesPerPacket = 1;
|
|
_outDesiredFormat.mBytesPerFrame =
|
|
_outDesiredFormat.mChannelsPerFrame * sizeof(SInt16);
|
|
_outDesiredFormat.mBitsPerChannel = sizeof(SInt16) * 8;
|
|
|
|
_outDesiredFormat.mFormatFlags =
|
|
kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
|
|
#ifdef WEBRTC_ARCH_BIG_ENDIAN
|
|
_outDesiredFormat.mFormatFlags |= kLinearPCMFormatFlagIsBigEndian;
|
|
#endif
|
|
_outDesiredFormat.mFormatID = kAudioFormatLinearPCM;
|
|
|
|
OSStatus err = noErr;
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioConverterNew(
|
|
&_outDesiredFormat, &_outStreamFormat, &_renderConverter));
|
|
|
|
// Try to set buffer size to desired value set to 20ms.
|
|
const uint16_t kPlayBufDelayFixed = 20;
|
|
UInt32 bufByteCount = static_cast<UInt32>(
|
|
(_outStreamFormat.mSampleRate / 1000.0) * kPlayBufDelayFixed *
|
|
_outStreamFormat.mChannelsPerFrame * sizeof(Float32));
|
|
if (_outStreamFormat.mFramesPerPacket != 0) {
|
|
if (bufByteCount % _outStreamFormat.mFramesPerPacket != 0) {
|
|
bufByteCount = (static_cast<UInt32>(bufByteCount /
|
|
_outStreamFormat.mFramesPerPacket) +
|
|
1) *
|
|
_outStreamFormat.mFramesPerPacket;
|
|
}
|
|
}
|
|
|
|
// Ensure the buffer size is within the range provided by the device.
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
kAudioDevicePropertyDataSource, kAudioDevicePropertyScopeOutput, 0};
|
|
propertyAddress.mSelector = kAudioDevicePropertyBufferSizeRange;
|
|
AudioValueRange range;
|
|
UInt32 size = sizeof(range);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
_outputDeviceID, &propertyAddress, 0, NULL, &size, &range));
|
|
if (range.mMinimum > bufByteCount) {
|
|
bufByteCount = range.mMinimum;
|
|
} else if (range.mMaximum < bufByteCount) {
|
|
bufByteCount = range.mMaximum;
|
|
}
|
|
|
|
propertyAddress.mSelector = kAudioDevicePropertyBufferSize;
|
|
size = sizeof(bufByteCount);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectSetPropertyData(
|
|
_outputDeviceID, &propertyAddress, 0, NULL, size, &bufByteCount));
|
|
|
|
// Get render device latency.
|
|
propertyAddress.mSelector = kAudioDevicePropertyLatency;
|
|
UInt32 latency = 0;
|
|
size = sizeof(UInt32);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
_outputDeviceID, &propertyAddress, 0, NULL, &size, &latency));
|
|
_renderLatencyUs =
|
|
static_cast<uint32_t>((1.0e6 * latency) / _outStreamFormat.mSampleRate);
|
|
|
|
// Get render stream latency.
|
|
propertyAddress.mSelector = kAudioDevicePropertyStreams;
|
|
AudioStreamID stream = 0;
|
|
size = sizeof(AudioStreamID);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
_outputDeviceID, &propertyAddress, 0, NULL, &size, &stream));
|
|
propertyAddress.mSelector = kAudioStreamPropertyLatency;
|
|
size = sizeof(UInt32);
|
|
latency = 0;
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
_outputDeviceID, &propertyAddress, 0, NULL, &size, &latency));
|
|
_renderLatencyUs +=
|
|
static_cast<uint32_t>((1.0e6 * latency) / _outStreamFormat.mSampleRate);
|
|
|
|
RTC_LOG(LS_VERBOSE) << "initial playout status: _renderDelayOffsetSamples="
|
|
<< _renderDelayOffsetSamples
|
|
<< ", _renderDelayUs=" << _renderDelayUs
|
|
<< ", _renderLatencyUs=" << _renderLatencyUs;
|
|
return 0;
|
|
}
|
|
|
|
OSStatus AudioDeviceMac::objectListenerProc(
|
|
AudioObjectID objectId,
|
|
UInt32 numberAddresses,
|
|
const AudioObjectPropertyAddress addresses[],
|
|
void* clientData) {
|
|
AudioDeviceMac* ptrThis = (AudioDeviceMac*)clientData;
|
|
RTC_DCHECK(ptrThis != NULL);
|
|
|
|
ptrThis->implObjectListenerProc(objectId, numberAddresses, addresses);
|
|
|
|
// AudioObjectPropertyListenerProc functions are supposed to return 0
|
|
return 0;
|
|
}
|
|
|
|
OSStatus AudioDeviceMac::implObjectListenerProc(
|
|
const AudioObjectID objectId,
|
|
const UInt32 numberAddresses,
|
|
const AudioObjectPropertyAddress addresses[]) {
|
|
RTC_LOG(LS_VERBOSE) << "AudioDeviceMac::implObjectListenerProc()";
|
|
|
|
for (UInt32 i = 0; i < numberAddresses; i++) {
|
|
if (addresses[i].mSelector == kAudioHardwarePropertyDevices) {
|
|
HandleDeviceChange();
|
|
} else if (addresses[i].mSelector == kAudioDevicePropertyStreamFormat) {
|
|
HandleStreamFormatChange(objectId, addresses[i]);
|
|
} else if (addresses[i].mSelector == kAudioDevicePropertyDataSource) {
|
|
HandleDataSourceChange(objectId, addresses[i]);
|
|
} else if (addresses[i].mSelector == kAudioDeviceProcessorOverload) {
|
|
HandleProcessorOverload(addresses[i]);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::HandleDeviceChange() {
|
|
OSStatus err = noErr;
|
|
|
|
RTC_LOG(LS_VERBOSE) << "kAudioHardwarePropertyDevices";
|
|
|
|
// A device has changed. Check if our registered devices have been removed.
|
|
// Ensure the devices have been initialized, meaning the IDs are valid.
|
|
if (MicrophoneIsInitialized()) {
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
kAudioDevicePropertyDeviceIsAlive, kAudioDevicePropertyScopeInput, 0};
|
|
UInt32 deviceIsAlive = 1;
|
|
UInt32 size = sizeof(UInt32);
|
|
err = AudioObjectGetPropertyData(_inputDeviceID, &propertyAddress, 0, NULL,
|
|
&size, &deviceIsAlive);
|
|
|
|
if (err == kAudioHardwareBadDeviceError || deviceIsAlive == 0) {
|
|
RTC_LOG(LS_WARNING) << "Capture device is not alive (probably removed)";
|
|
AtomicSet32(&_captureDeviceIsAlive, 0);
|
|
_mixerManager.CloseMicrophone();
|
|
} else if (err != noErr) {
|
|
logCAMsg(rtc::LS_ERROR, "Error in AudioDeviceGetProperty()",
|
|
(const char*)&err);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (SpeakerIsInitialized()) {
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
kAudioDevicePropertyDeviceIsAlive, kAudioDevicePropertyScopeOutput, 0};
|
|
UInt32 deviceIsAlive = 1;
|
|
UInt32 size = sizeof(UInt32);
|
|
err = AudioObjectGetPropertyData(_outputDeviceID, &propertyAddress, 0, NULL,
|
|
&size, &deviceIsAlive);
|
|
|
|
if (err == kAudioHardwareBadDeviceError || deviceIsAlive == 0) {
|
|
RTC_LOG(LS_WARNING) << "Render device is not alive (probably removed)";
|
|
AtomicSet32(&_renderDeviceIsAlive, 0);
|
|
_mixerManager.CloseSpeaker();
|
|
} else if (err != noErr) {
|
|
logCAMsg(rtc::LS_ERROR, "Error in AudioDeviceGetProperty()",
|
|
(const char*)&err);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::HandleStreamFormatChange(
|
|
const AudioObjectID objectId,
|
|
const AudioObjectPropertyAddress propertyAddress) {
|
|
OSStatus err = noErr;
|
|
|
|
RTC_LOG(LS_VERBOSE) << "Stream format changed";
|
|
|
|
if (objectId != _inputDeviceID && objectId != _outputDeviceID) {
|
|
return 0;
|
|
}
|
|
|
|
// Get the new device format
|
|
AudioStreamBasicDescription streamFormat;
|
|
UInt32 size = sizeof(streamFormat);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
objectId, &propertyAddress, 0, NULL, &size, &streamFormat));
|
|
|
|
if (streamFormat.mFormatID != kAudioFormatLinearPCM) {
|
|
logCAMsg(rtc::LS_ERROR, "Unacceptable input stream format -> mFormatID",
|
|
(const char*)&streamFormat.mFormatID);
|
|
return -1;
|
|
}
|
|
|
|
if (streamFormat.mChannelsPerFrame > N_DEVICE_CHANNELS) {
|
|
RTC_LOG(LS_ERROR) << "Too many channels on device (mChannelsPerFrame = "
|
|
<< streamFormat.mChannelsPerFrame << ")";
|
|
return -1;
|
|
}
|
|
|
|
if (_ptrAudioBuffer && streamFormat.mChannelsPerFrame != _recChannels) {
|
|
RTC_LOG(LS_ERROR) << "Changing channels not supported (mChannelsPerFrame = "
|
|
<< streamFormat.mChannelsPerFrame << ")";
|
|
return -1;
|
|
}
|
|
|
|
RTC_LOG(LS_VERBOSE) << "Stream format:";
|
|
RTC_LOG(LS_VERBOSE) << "mSampleRate = " << streamFormat.mSampleRate
|
|
<< ", mChannelsPerFrame = "
|
|
<< streamFormat.mChannelsPerFrame;
|
|
RTC_LOG(LS_VERBOSE) << "mBytesPerPacket = " << streamFormat.mBytesPerPacket
|
|
<< ", mFramesPerPacket = "
|
|
<< streamFormat.mFramesPerPacket;
|
|
RTC_LOG(LS_VERBOSE) << "mBytesPerFrame = " << streamFormat.mBytesPerFrame
|
|
<< ", mBitsPerChannel = " << streamFormat.mBitsPerChannel;
|
|
RTC_LOG(LS_VERBOSE) << "mFormatFlags = " << streamFormat.mFormatFlags;
|
|
logCAMsg(rtc::LS_VERBOSE, "mFormatID", (const char*)&streamFormat.mFormatID);
|
|
|
|
if (propertyAddress.mScope == kAudioDevicePropertyScopeInput) {
|
|
const int io_block_size_samples = streamFormat.mChannelsPerFrame *
|
|
streamFormat.mSampleRate / 100 *
|
|
N_BLOCKS_IO;
|
|
if (io_block_size_samples > _captureBufSizeSamples) {
|
|
RTC_LOG(LS_ERROR) << "Input IO block size (" << io_block_size_samples
|
|
<< ") is larger than ring buffer ("
|
|
<< _captureBufSizeSamples << ")";
|
|
return -1;
|
|
}
|
|
|
|
memcpy(&_inStreamFormat, &streamFormat, sizeof(streamFormat));
|
|
|
|
if (_inStreamFormat.mChannelsPerFrame >= 2 && (_recChannels == 2)) {
|
|
_inDesiredFormat.mChannelsPerFrame = 2;
|
|
} else {
|
|
// Disable stereo recording when we only have one channel on the device.
|
|
_inDesiredFormat.mChannelsPerFrame = 1;
|
|
_recChannels = 1;
|
|
RTC_LOG(LS_VERBOSE) << "Stereo recording unavailable on this device";
|
|
}
|
|
|
|
// Recreate the converter with the new format
|
|
// TODO(xians): make this thread safe
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioConverterDispose(_captureConverter));
|
|
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioConverterNew(&streamFormat, &_inDesiredFormat,
|
|
&_captureConverter));
|
|
} else {
|
|
memcpy(&_outStreamFormat, &streamFormat, sizeof(streamFormat));
|
|
|
|
// Our preferred format to work with
|
|
if (_outStreamFormat.mChannelsPerFrame < 2) {
|
|
_playChannels = 1;
|
|
RTC_LOG(LS_VERBOSE) << "Stereo playout unavailable on this device";
|
|
}
|
|
WEBRTC_CA_RETURN_ON_ERR(SetDesiredPlayoutFormat());
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int32_t AudioDeviceMac::HandleDataSourceChange(
|
|
const AudioObjectID objectId,
|
|
const AudioObjectPropertyAddress propertyAddress) {
|
|
OSStatus err = noErr;
|
|
|
|
if (_macBookPro &&
|
|
propertyAddress.mScope == kAudioDevicePropertyScopeOutput) {
|
|
RTC_LOG(LS_VERBOSE) << "Data source changed";
|
|
|
|
_macBookProPanRight = false;
|
|
UInt32 dataSource = 0;
|
|
UInt32 size = sizeof(UInt32);
|
|
WEBRTC_CA_RETURN_ON_ERR(AudioObjectGetPropertyData(
|
|
objectId, &propertyAddress, 0, NULL, &size, &dataSource));
|
|
if (dataSource == 'ispk') {
|
|
_macBookProPanRight = true;
|
|
RTC_LOG(LS_VERBOSE)
|
|
<< "MacBook Pro using internal speakers; stereo panning right";
|
|
} else {
|
|
RTC_LOG(LS_VERBOSE) << "MacBook Pro not using internal speakers";
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
int32_t AudioDeviceMac::HandleProcessorOverload(
|
|
const AudioObjectPropertyAddress propertyAddress) {
|
|
// TODO(xians): we probably want to notify the user in some way of the
|
|
// overload. However, the Windows interpretations of these errors seem to
|
|
// be more severe than what ProcessorOverload is thrown for.
|
|
//
|
|
// We don't log the notification, as it's sent from the HAL's IO thread. We
|
|
// don't want to slow it down even further.
|
|
if (propertyAddress.mScope == kAudioDevicePropertyScopeInput) {
|
|
// RTC_LOG(LS_WARNING) << "Capture processor // overload";
|
|
//_callback->ProblemIsReported(
|
|
// SndCardStreamObserver::ERecordingProblem);
|
|
} else {
|
|
// RTC_LOG(LS_WARNING) << "Render processor overload";
|
|
//_callback->ProblemIsReported(
|
|
// SndCardStreamObserver::EPlaybackProblem);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Thread Methods
|
|
// ============================================================================
|
|
|
|
OSStatus AudioDeviceMac::deviceIOProc(AudioDeviceID,
|
|
const AudioTimeStamp*,
|
|
const AudioBufferList* inputData,
|
|
const AudioTimeStamp* inputTime,
|
|
AudioBufferList* outputData,
|
|
const AudioTimeStamp* outputTime,
|
|
void* clientData) {
|
|
AudioDeviceMac* ptrThis = (AudioDeviceMac*)clientData;
|
|
RTC_DCHECK(ptrThis != NULL);
|
|
|
|
ptrThis->implDeviceIOProc(inputData, inputTime, outputData, outputTime);
|
|
|
|
// AudioDeviceIOProc functions are supposed to return 0
|
|
return 0;
|
|
}
|
|
|
|
OSStatus AudioDeviceMac::outConverterProc(AudioConverterRef,
|
|
UInt32* numberDataPackets,
|
|
AudioBufferList* data,
|
|
AudioStreamPacketDescription**,
|
|
void* userData) {
|
|
AudioDeviceMac* ptrThis = (AudioDeviceMac*)userData;
|
|
RTC_DCHECK(ptrThis != NULL);
|
|
|
|
return ptrThis->implOutConverterProc(numberDataPackets, data);
|
|
}
|
|
|
|
OSStatus AudioDeviceMac::inDeviceIOProc(AudioDeviceID,
|
|
const AudioTimeStamp*,
|
|
const AudioBufferList* inputData,
|
|
const AudioTimeStamp* inputTime,
|
|
AudioBufferList*,
|
|
const AudioTimeStamp*,
|
|
void* clientData) {
|
|
AudioDeviceMac* ptrThis = (AudioDeviceMac*)clientData;
|
|
RTC_DCHECK(ptrThis != NULL);
|
|
|
|
ptrThis->implInDeviceIOProc(inputData, inputTime);
|
|
|
|
// AudioDeviceIOProc functions are supposed to return 0
|
|
return 0;
|
|
}
|
|
|
|
OSStatus AudioDeviceMac::inConverterProc(
|
|
AudioConverterRef,
|
|
UInt32* numberDataPackets,
|
|
AudioBufferList* data,
|
|
AudioStreamPacketDescription** /*dataPacketDescription*/,
|
|
void* userData) {
|
|
AudioDeviceMac* ptrThis = static_cast<AudioDeviceMac*>(userData);
|
|
RTC_DCHECK(ptrThis != NULL);
|
|
|
|
return ptrThis->implInConverterProc(numberDataPackets, data);
|
|
}
|
|
|
|
OSStatus AudioDeviceMac::implDeviceIOProc(const AudioBufferList* inputData,
|
|
const AudioTimeStamp* inputTime,
|
|
AudioBufferList* outputData,
|
|
const AudioTimeStamp* outputTime) {
|
|
OSStatus err = noErr;
|
|
UInt64 outputTimeNs = AudioConvertHostTimeToNanos(outputTime->mHostTime);
|
|
UInt64 nowNs = AudioConvertHostTimeToNanos(AudioGetCurrentHostTime());
|
|
|
|
if (!_twoDevices && _recording) {
|
|
implInDeviceIOProc(inputData, inputTime);
|
|
}
|
|
|
|
// Check if we should close down audio device
|
|
// Double-checked locking optimization to remove locking overhead
|
|
if (_doStop) {
|
|
MutexLock lock(&mutex_);
|
|
if (_doStop) {
|
|
if (_twoDevices || (!_recording && !_playing)) {
|
|
// In the case of a shared device, the single driving ioProc
|
|
// is stopped here
|
|
WEBRTC_CA_LOG_ERR(AudioDeviceStop(_outputDeviceID, _deviceIOProcID));
|
|
WEBRTC_CA_LOG_WARN(
|
|
AudioDeviceDestroyIOProcID(_outputDeviceID, _deviceIOProcID));
|
|
if (err == noErr) {
|
|
RTC_LOG(LS_VERBOSE) << "Playout or shared device stopped";
|
|
}
|
|
}
|
|
|
|
_doStop = false;
|
|
_stopEvent.Set();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (!_playing) {
|
|
// This can be the case when a shared device is capturing but not
|
|
// rendering. We allow the checks above before returning to avoid a
|
|
// timeout when capturing is stopped.
|
|
return 0;
|
|
}
|
|
|
|
RTC_DCHECK(_outStreamFormat.mBytesPerFrame != 0);
|
|
UInt32 size =
|
|
outputData->mBuffers->mDataByteSize / _outStreamFormat.mBytesPerFrame;
|
|
|
|
// TODO(xians): signal an error somehow?
|
|
err = AudioConverterFillComplexBuffer(_renderConverter, outConverterProc,
|
|
this, &size, outputData, NULL);
|
|
if (err != noErr) {
|
|
if (err == 1) {
|
|
// This is our own error.
|
|
RTC_LOG(LS_ERROR) << "Error in AudioConverterFillComplexBuffer()";
|
|
return 1;
|
|
} else {
|
|
logCAMsg(rtc::LS_ERROR, "Error in AudioConverterFillComplexBuffer()",
|
|
(const char*)&err);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
PaRingBufferSize bufSizeSamples =
|
|
PaUtil_GetRingBufferReadAvailable(_paRenderBuffer);
|
|
|
|
int32_t renderDelayUs =
|
|
static_cast<int32_t>(1e-3 * (outputTimeNs - nowNs) + 0.5);
|
|
renderDelayUs += static_cast<int32_t>(
|
|
(1.0e6 * bufSizeSamples) / _outDesiredFormat.mChannelsPerFrame /
|
|
_outDesiredFormat.mSampleRate +
|
|
0.5);
|
|
|
|
AtomicSet32(&_renderDelayUs, renderDelayUs);
|
|
|
|
return 0;
|
|
}
|
|
|
|
OSStatus AudioDeviceMac::implOutConverterProc(UInt32* numberDataPackets,
|
|
AudioBufferList* data) {
|
|
RTC_DCHECK(data->mNumberBuffers == 1);
|
|
PaRingBufferSize numSamples =
|
|
*numberDataPackets * _outDesiredFormat.mChannelsPerFrame;
|
|
|
|
data->mBuffers->mNumberChannels = _outDesiredFormat.mChannelsPerFrame;
|
|
// Always give the converter as much as it wants, zero padding as required.
|
|
data->mBuffers->mDataByteSize =
|
|
*numberDataPackets * _outDesiredFormat.mBytesPerPacket;
|
|
data->mBuffers->mData = _renderConvertData;
|
|
memset(_renderConvertData, 0, sizeof(_renderConvertData));
|
|
|
|
PaUtil_ReadRingBuffer(_paRenderBuffer, _renderConvertData, numSamples);
|
|
|
|
kern_return_t kernErr = semaphore_signal_all(_renderSemaphore);
|
|
if (kernErr != KERN_SUCCESS) {
|
|
RTC_LOG(LS_ERROR) << "semaphore_signal_all() error: " << kernErr;
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
OSStatus AudioDeviceMac::implInDeviceIOProc(const AudioBufferList* inputData,
|
|
const AudioTimeStamp* inputTime) {
|
|
OSStatus err = noErr;
|
|
UInt64 inputTimeNs = AudioConvertHostTimeToNanos(inputTime->mHostTime);
|
|
UInt64 nowNs = AudioConvertHostTimeToNanos(AudioGetCurrentHostTime());
|
|
|
|
// Check if we should close down audio device
|
|
// Double-checked locking optimization to remove locking overhead
|
|
if (_doStopRec) {
|
|
MutexLock lock(&mutex_);
|
|
if (_doStopRec) {
|
|
// This will be signalled only when a shared device is not in use.
|
|
WEBRTC_CA_LOG_ERR(AudioDeviceStop(_inputDeviceID, _inDeviceIOProcID));
|
|
WEBRTC_CA_LOG_WARN(
|
|
AudioDeviceDestroyIOProcID(_inputDeviceID, _inDeviceIOProcID));
|
|
if (err == noErr) {
|
|
RTC_LOG(LS_VERBOSE) << "Recording device stopped";
|
|
}
|
|
|
|
_doStopRec = false;
|
|
_stopEventRec.Set();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (!_recording) {
|
|
// Allow above checks to avoid a timeout on stopping capture.
|
|
return 0;
|
|
}
|
|
|
|
PaRingBufferSize bufSizeSamples =
|
|
PaUtil_GetRingBufferReadAvailable(_paCaptureBuffer);
|
|
|
|
int32_t captureDelayUs =
|
|
static_cast<int32_t>(1e-3 * (nowNs - inputTimeNs) + 0.5);
|
|
captureDelayUs += static_cast<int32_t>((1.0e6 * bufSizeSamples) /
|
|
_inStreamFormat.mChannelsPerFrame /
|
|
_inStreamFormat.mSampleRate +
|
|
0.5);
|
|
|
|
AtomicSet32(&_captureDelayUs, captureDelayUs);
|
|
|
|
RTC_DCHECK(inputData->mNumberBuffers == 1);
|
|
PaRingBufferSize numSamples = inputData->mBuffers->mDataByteSize *
|
|
_inStreamFormat.mChannelsPerFrame /
|
|
_inStreamFormat.mBytesPerPacket;
|
|
PaUtil_WriteRingBuffer(_paCaptureBuffer, inputData->mBuffers->mData,
|
|
numSamples);
|
|
|
|
kern_return_t kernErr = semaphore_signal_all(_captureSemaphore);
|
|
if (kernErr != KERN_SUCCESS) {
|
|
RTC_LOG(LS_ERROR) << "semaphore_signal_all() error: " << kernErr;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
OSStatus AudioDeviceMac::implInConverterProc(UInt32* numberDataPackets,
|
|
AudioBufferList* data) {
|
|
RTC_DCHECK(data->mNumberBuffers == 1);
|
|
PaRingBufferSize numSamples =
|
|
*numberDataPackets * _inStreamFormat.mChannelsPerFrame;
|
|
|
|
while (PaUtil_GetRingBufferReadAvailable(_paCaptureBuffer) < numSamples) {
|
|
mach_timespec_t timeout;
|
|
timeout.tv_sec = 0;
|
|
timeout.tv_nsec = TIMER_PERIOD_MS;
|
|
|
|
kern_return_t kernErr = semaphore_timedwait(_captureSemaphore, timeout);
|
|
if (kernErr == KERN_OPERATION_TIMED_OUT) {
|
|
int32_t signal = AtomicGet32(&_captureDeviceIsAlive);
|
|
if (signal == 0) {
|
|
// The capture device is no longer alive; stop the worker thread.
|
|
*numberDataPackets = 0;
|
|
return 1;
|
|
}
|
|
} else if (kernErr != KERN_SUCCESS) {
|
|
RTC_LOG(LS_ERROR) << "semaphore_wait() error: " << kernErr;
|
|
}
|
|
}
|
|
|
|
// Pass the read pointer directly to the converter to avoid a memcpy.
|
|
void* dummyPtr;
|
|
PaRingBufferSize dummySize;
|
|
PaUtil_GetRingBufferReadRegions(_paCaptureBuffer, numSamples,
|
|
&data->mBuffers->mData, &numSamples,
|
|
&dummyPtr, &dummySize);
|
|
PaUtil_AdvanceRingBufferReadIndex(_paCaptureBuffer, numSamples);
|
|
|
|
data->mBuffers->mNumberChannels = _inStreamFormat.mChannelsPerFrame;
|
|
*numberDataPackets = numSamples / _inStreamFormat.mChannelsPerFrame;
|
|
data->mBuffers->mDataByteSize =
|
|
*numberDataPackets * _inStreamFormat.mBytesPerPacket;
|
|
|
|
return 0;
|
|
}
|
|
|
|
void AudioDeviceMac::RunRender(void* ptrThis) {
|
|
AudioDeviceMac* device = static_cast<AudioDeviceMac*>(ptrThis);
|
|
while (device->RenderWorkerThread()) {
|
|
}
|
|
}
|
|
|
|
bool AudioDeviceMac::RenderWorkerThread() {
|
|
PaRingBufferSize numSamples =
|
|
ENGINE_PLAY_BUF_SIZE_IN_SAMPLES * _outDesiredFormat.mChannelsPerFrame;
|
|
while (PaUtil_GetRingBufferWriteAvailable(_paRenderBuffer) -
|
|
_renderDelayOffsetSamples <
|
|
numSamples) {
|
|
mach_timespec_t timeout;
|
|
timeout.tv_sec = 0;
|
|
timeout.tv_nsec = TIMER_PERIOD_MS;
|
|
|
|
kern_return_t kernErr = semaphore_timedwait(_renderSemaphore, timeout);
|
|
if (kernErr == KERN_OPERATION_TIMED_OUT) {
|
|
int32_t signal = AtomicGet32(&_renderDeviceIsAlive);
|
|
if (signal == 0) {
|
|
// The render device is no longer alive; stop the worker thread.
|
|
return false;
|
|
}
|
|
} else if (kernErr != KERN_SUCCESS) {
|
|
RTC_LOG(LS_ERROR) << "semaphore_timedwait() error: " << kernErr;
|
|
}
|
|
}
|
|
|
|
int8_t playBuffer[4 * ENGINE_PLAY_BUF_SIZE_IN_SAMPLES];
|
|
|
|
if (!_ptrAudioBuffer) {
|
|
RTC_LOG(LS_ERROR) << "capture AudioBuffer is invalid";
|
|
return false;
|
|
}
|
|
|
|
// Ask for new PCM data to be played out using the AudioDeviceBuffer.
|
|
uint32_t nSamples =
|
|
_ptrAudioBuffer->RequestPlayoutData(ENGINE_PLAY_BUF_SIZE_IN_SAMPLES);
|
|
|
|
nSamples = _ptrAudioBuffer->GetPlayoutData(playBuffer);
|
|
if (nSamples != ENGINE_PLAY_BUF_SIZE_IN_SAMPLES) {
|
|
RTC_LOG(LS_ERROR) << "invalid number of output samples(" << nSamples << ")";
|
|
}
|
|
|
|
uint32_t nOutSamples = nSamples * _outDesiredFormat.mChannelsPerFrame;
|
|
|
|
SInt16* pPlayBuffer = (SInt16*)&playBuffer;
|
|
if (_macBookProPanRight && (_playChannels == 2)) {
|
|
// Mix entirely into the right channel and zero the left channel.
|
|
SInt32 sampleInt32 = 0;
|
|
for (uint32_t sampleIdx = 0; sampleIdx < nOutSamples; sampleIdx += 2) {
|
|
sampleInt32 = pPlayBuffer[sampleIdx];
|
|
sampleInt32 += pPlayBuffer[sampleIdx + 1];
|
|
sampleInt32 /= 2;
|
|
|
|
if (sampleInt32 > 32767) {
|
|
sampleInt32 = 32767;
|
|
} else if (sampleInt32 < -32768) {
|
|
sampleInt32 = -32768;
|
|
}
|
|
|
|
pPlayBuffer[sampleIdx] = 0;
|
|
pPlayBuffer[sampleIdx + 1] = static_cast<SInt16>(sampleInt32);
|
|
}
|
|
}
|
|
|
|
PaUtil_WriteRingBuffer(_paRenderBuffer, pPlayBuffer, nOutSamples);
|
|
|
|
return true;
|
|
}
|
|
|
|
void AudioDeviceMac::RunCapture(void* ptrThis) {
|
|
AudioDeviceMac* device = static_cast<AudioDeviceMac*>(ptrThis);
|
|
while (device->CaptureWorkerThread()) {
|
|
}
|
|
}
|
|
|
|
bool AudioDeviceMac::CaptureWorkerThread() {
|
|
OSStatus err = noErr;
|
|
UInt32 noRecSamples =
|
|
ENGINE_REC_BUF_SIZE_IN_SAMPLES * _inDesiredFormat.mChannelsPerFrame;
|
|
SInt16 recordBuffer[noRecSamples];
|
|
UInt32 size = ENGINE_REC_BUF_SIZE_IN_SAMPLES;
|
|
|
|
AudioBufferList engineBuffer;
|
|
engineBuffer.mNumberBuffers = 1; // Interleaved channels.
|
|
engineBuffer.mBuffers->mNumberChannels = _inDesiredFormat.mChannelsPerFrame;
|
|
engineBuffer.mBuffers->mDataByteSize =
|
|
_inDesiredFormat.mBytesPerPacket * noRecSamples;
|
|
engineBuffer.mBuffers->mData = recordBuffer;
|
|
|
|
err = AudioConverterFillComplexBuffer(_captureConverter, inConverterProc,
|
|
this, &size, &engineBuffer, NULL);
|
|
if (err != noErr) {
|
|
if (err == 1) {
|
|
// This is our own error.
|
|
return false;
|
|
} else {
|
|
logCAMsg(rtc::LS_ERROR, "Error in AudioConverterFillComplexBuffer()",
|
|
(const char*)&err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// TODO(xians): what if the returned size is incorrect?
|
|
if (size == ENGINE_REC_BUF_SIZE_IN_SAMPLES) {
|
|
int32_t msecOnPlaySide;
|
|
int32_t msecOnRecordSide;
|
|
|
|
int32_t captureDelayUs = AtomicGet32(&_captureDelayUs);
|
|
int32_t renderDelayUs = AtomicGet32(&_renderDelayUs);
|
|
|
|
msecOnPlaySide =
|
|
static_cast<int32_t>(1e-3 * (renderDelayUs + _renderLatencyUs) + 0.5);
|
|
msecOnRecordSide =
|
|
static_cast<int32_t>(1e-3 * (captureDelayUs + _captureLatencyUs) + 0.5);
|
|
|
|
if (!_ptrAudioBuffer) {
|
|
RTC_LOG(LS_ERROR) << "capture AudioBuffer is invalid";
|
|
return false;
|
|
}
|
|
|
|
// store the recorded buffer (no action will be taken if the
|
|
// #recorded samples is not a full buffer)
|
|
_ptrAudioBuffer->SetRecordedBuffer((int8_t*)&recordBuffer, (uint32_t)size);
|
|
_ptrAudioBuffer->SetVQEData(msecOnPlaySide, msecOnRecordSide);
|
|
_ptrAudioBuffer->SetTypingStatus(KeyPressed());
|
|
|
|
// deliver recorded samples at specified sample rate, mic level etc.
|
|
// to the observer using callback
|
|
_ptrAudioBuffer->DeliverRecordedData();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AudioDeviceMac::KeyPressed() {
|
|
bool key_down = false;
|
|
// Loop through all Mac virtual key constant values.
|
|
for (unsigned int key_index = 0; key_index < arraysize(prev_key_state_);
|
|
++key_index) {
|
|
bool keyState =
|
|
CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, key_index);
|
|
// A false -> true change in keymap means a key is pressed.
|
|
key_down |= (keyState && !prev_key_state_[key_index]);
|
|
// Save current state.
|
|
prev_key_state_[key_index] = keyState;
|
|
}
|
|
return key_down;
|
|
}
|
|
} // namespace webrtc
|