/** * FreeRDP: A Remote Desktop Protocol Implementation * Audio Input Redirection Virtual Channel - Mac OS X implementation * * Copyright (c) 2015 Armin Novak * Copyright 2015 Thincast Technologies GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include #include #include #include #import #define __COREFOUNDATION_CFPLUGINCOM__ 1 #define IUNKNOWN_C_GUTS \ void *_reserved; \ void *QueryInterface; \ void *AddRef; \ void *Release #include #include #include #include #include #include #include "audin_main.h" #define MAC_AUDIO_QUEUE_NUM_BUFFERS 100 /* Fix for #4462: Provide type alias if not declared (Mac OS < 10.10) * https://developer.apple.com/documentation/coreaudio/audioformatid */ #ifndef AudioFormatID typedef UInt32 AudioFormatID; #endif #ifndef AudioFormatFlags typedef UInt32 AudioFormatFlags; #endif typedef struct { IAudinDevice iface; AUDIO_FORMAT format; UINT32 FramesPerPacket; int dev_unit; AudinReceive receive; void *user_data; rdpContext *rdpcontext; bool isAuthorized; bool isOpen; AudioQueueRef audioQueue; AudioStreamBasicDescription audioFormat; AudioQueueBufferRef audioBuffers[MAC_AUDIO_QUEUE_NUM_BUFFERS]; } AudinMacDevice; static AudioFormatID audin_mac_get_format(const AUDIO_FORMAT *format) { switch (format->wFormatTag) { case WAVE_FORMAT_PCM: return kAudioFormatLinearPCM; default: return 0; } } static AudioFormatFlags audin_mac_get_flags_for_format(const AUDIO_FORMAT *format) { switch (format->wFormatTag) { case WAVE_FORMAT_PCM: return kAudioFormatFlagIsSignedInteger; default: return 0; } } static BOOL audin_mac_format_supported(IAudinDevice *device, const AUDIO_FORMAT *format) { AudinMacDevice *mac = (AudinMacDevice *)device; AudioFormatID req_fmt = 0; if (!mac->isAuthorized) return FALSE; if (device == NULL || format == NULL) return FALSE; if (format->nChannels != 2) return FALSE; req_fmt = audin_mac_get_format(format); if (req_fmt == 0) return FALSE; return TRUE; } /** * Function description * * @return 0 on success, otherwise a Win32 error code */ static UINT audin_mac_set_format(IAudinDevice *device, const AUDIO_FORMAT *format, UINT32 FramesPerPacket) { AudinMacDevice *mac = (AudinMacDevice *)device; if (!mac->isAuthorized) return ERROR_INTERNAL_ERROR; if (device == NULL || format == NULL) return ERROR_INVALID_PARAMETER; mac->FramesPerPacket = FramesPerPacket; mac->format = *format; WLog_INFO(TAG, "Audio Format %s [channels=%d, samples=%d, bits=%d]", audio_format_get_tag_string(format->wFormatTag), format->nChannels, format->nSamplesPerSec, format->wBitsPerSample); mac->audioFormat.mBitsPerChannel = format->wBitsPerSample; if (format->wBitsPerSample == 0) mac->audioFormat.mBitsPerChannel = 16; mac->audioFormat.mChannelsPerFrame = mac->format.nChannels; mac->audioFormat.mFramesPerPacket = 1; mac->audioFormat.mBytesPerFrame = mac->audioFormat.mChannelsPerFrame * (mac->audioFormat.mBitsPerChannel / 8); mac->audioFormat.mBytesPerPacket = mac->audioFormat.mBytesPerFrame * mac->audioFormat.mFramesPerPacket; mac->audioFormat.mFormatFlags = audin_mac_get_flags_for_format(format); mac->audioFormat.mFormatID = audin_mac_get_format(format); mac->audioFormat.mReserved = 0; mac->audioFormat.mSampleRate = mac->format.nSamplesPerSec; return CHANNEL_RC_OK; } static void mac_audio_queue_input_cb(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *inStartTime, UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc) { AudinMacDevice *mac = (AudinMacDevice *)aqData; UINT error = CHANNEL_RC_OK; const BYTE *buffer = inBuffer->mAudioData; int buffer_size = inBuffer->mAudioDataByteSize; (void)inAQ; (void)inStartTime; (void)inNumPackets; (void)inPacketDesc; if (buffer_size > 0) error = mac->receive(&mac->format, buffer, buffer_size, mac->user_data); AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL); if (error) { WLog_ERR(TAG, "mac->receive failed with error %" PRIu32 "", error); SetLastError(ERROR_INTERNAL_ERROR); } } static UINT audin_mac_close(IAudinDevice *device) { UINT errCode = CHANNEL_RC_OK; char errString[1024]; OSStatus devStat; AudinMacDevice *mac = (AudinMacDevice *)device; if (!mac->isAuthorized) return ERROR_INTERNAL_ERROR; if (device == NULL) return ERROR_INVALID_PARAMETER; if (mac->isOpen) { devStat = AudioQueueStop(mac->audioQueue, true); if (devStat != 0) { errCode = GetLastError(); WLog_ERR(TAG, "AudioQueueStop failed with %s [%" PRIu32 "]", winpr_strerror(errCode, errString, sizeof(errString)), errCode); } mac->isOpen = false; } if (mac->audioQueue) { devStat = AudioQueueDispose(mac->audioQueue, true); if (devStat != 0) { errCode = GetLastError(); WLog_ERR(TAG, "AudioQueueDispose failed with %s [%" PRIu32 "]", winpr_strerror(errCode, errString, sizeof(errString)), errCode); } mac->audioQueue = NULL; } mac->receive = NULL; mac->user_data = NULL; return errCode; } static UINT audin_mac_open(IAudinDevice *device, AudinReceive receive, void *user_data) { AudinMacDevice *mac = (AudinMacDevice *)device; DWORD errCode; char errString[1024]; OSStatus devStat; size_t index; if (!mac->isAuthorized) return ERROR_INTERNAL_ERROR; mac->receive = receive; mac->user_data = user_data; devStat = AudioQueueNewInput(&(mac->audioFormat), mac_audio_queue_input_cb, mac, NULL, kCFRunLoopCommonModes, 0, &(mac->audioQueue)); if (devStat != 0) { errCode = GetLastError(); WLog_ERR(TAG, "AudioQueueNewInput failed with %s [%" PRIu32 "]", winpr_strerror(errCode, errString, sizeof(errString)), errCode); goto err_out; } for (index = 0; index < MAC_AUDIO_QUEUE_NUM_BUFFERS; index++) { devStat = AudioQueueAllocateBuffer(mac->audioQueue, mac->FramesPerPacket * 2 * mac->format.nChannels, &mac->audioBuffers[index]); if (devStat != 0) { errCode = GetLastError(); WLog_ERR(TAG, "AudioQueueAllocateBuffer failed with %s [%" PRIu32 "]", winpr_strerror(errCode, errString, sizeof(errString)), errCode); goto err_out; } devStat = AudioQueueEnqueueBuffer(mac->audioQueue, mac->audioBuffers[index], 0, NULL); if (devStat != 0) { errCode = GetLastError(); WLog_ERR(TAG, "AudioQueueEnqueueBuffer failed with %s [%" PRIu32 "]", winpr_strerror(errCode, errString, sizeof(errString)), errCode); goto err_out; } } devStat = AudioQueueStart(mac->audioQueue, NULL); if (devStat != 0) { errCode = GetLastError(); WLog_ERR(TAG, "AudioQueueStart failed with %s [%" PRIu32 "]", winpr_strerror(errCode, errString, sizeof(errString)), errCode); goto err_out; } mac->isOpen = true; return CHANNEL_RC_OK; err_out: audin_mac_close(device); return CHANNEL_RC_INITIALIZATION_ERROR; } static UINT audin_mac_free(IAudinDevice *device) { AudinMacDevice *mac = (AudinMacDevice *)device; int error; if (device == NULL) return ERROR_INVALID_PARAMETER; if ((error = audin_mac_close(device))) { WLog_ERR(TAG, "audin_oss_close failed with error code %d!", error); } free(mac); return CHANNEL_RC_OK; } static UINT audin_mac_parse_addin_args(AudinMacDevice *device, const ADDIN_ARGV *args) { DWORD errCode; char errString[1024]; int status; char *str_num, *eptr; DWORD flags; const COMMAND_LINE_ARGUMENT_A *arg; COMMAND_LINE_ARGUMENT_A audin_mac_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "audio device name" }, { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; AudinMacDevice *mac = (AudinMacDevice *)device; if (args->argc == 1) return CHANNEL_RC_OK; flags = COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; status = CommandLineParseArgumentsA(args->argc, args->argv, audin_mac_args, flags, mac, NULL, NULL); if (status < 0) return ERROR_INVALID_PARAMETER; arg = audin_mac_args; do { if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) continue; CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") { str_num = _strdup(arg->Value); if (!str_num) { errCode = GetLastError(); WLog_ERR(TAG, "_strdup failed with %s [%d]", winpr_strerror(errCode, errString, sizeof(errString)), errCode); return CHANNEL_RC_NO_MEMORY; } mac->dev_unit = strtol(str_num, &eptr, 10); if (mac->dev_unit < 0 || *eptr != '\0') mac->dev_unit = -1; free(str_num); } CommandLineSwitchEnd(arg) } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); return CHANNEL_RC_OK; } UINT mac_freerdp_audin_client_subsystem_entry(PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints) { DWORD errCode; char errString[1024]; const ADDIN_ARGV *args; AudinMacDevice *mac; UINT error; mac = (AudinMacDevice *)calloc(1, sizeof(AudinMacDevice)); if (!mac) { errCode = GetLastError(); WLog_ERR(TAG, "calloc failed with %s [%" PRIu32 "]", winpr_strerror(errCode, errString, sizeof(errString)), errCode); return CHANNEL_RC_NO_MEMORY; } mac->iface.Open = audin_mac_open; mac->iface.FormatSupported = audin_mac_format_supported; mac->iface.SetFormat = audin_mac_set_format; mac->iface.Close = audin_mac_close; mac->iface.Free = audin_mac_free; mac->rdpcontext = pEntryPoints->rdpcontext; mac->dev_unit = -1; args = pEntryPoints->args; if ((error = audin_mac_parse_addin_args(mac, args))) { WLog_ERR(TAG, "audin_mac_parse_addin_args failed with %" PRIu32 "!", error); goto error_out; } if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice *)mac))) { WLog_ERR(TAG, "RegisterAudinDevice failed with error %" PRIu32 "!", error); goto error_out; } #if defined(MAC_OS_X_VERSION_10_14) if (@available(macOS 10.14, *)) { @autoreleasepool { AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; switch (status) { case AVAuthorizationStatusAuthorized: mac->isAuthorized = TRUE; break; case AVAuthorizationStatusNotDetermined: [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) { if (granted == YES) { mac->isAuthorized = TRUE; } else WLog_WARN(TAG, "Microphone access denied by user"); }]; break; case AVAuthorizationStatusRestricted: WLog_WARN(TAG, "Microphone access restricted by policy"); break; case AVAuthorizationStatusDenied: WLog_WARN(TAG, "Microphone access denied by policy"); break; default: break; } } } #endif return CHANNEL_RC_OK; error_out: free(mac); return error; }