From 772f738c47cd285197013fff1ea766233a582065 Mon Sep 17 00:00:00 2001 From: Mike McDonald Date: Wed, 28 Aug 2013 21:02:48 -0400 Subject: [PATCH] Added RDPSND device plugin for iOS --- channels/rdpsnd/client/ios/CMakeLists.txt | 52 +++ channels/rdpsnd/client/ios/TPCircularBuffer.c | 136 ++++++++ channels/rdpsnd/client/ios/TPCircularBuffer.h | 195 ++++++++++++ channels/rdpsnd/client/ios/rdpsnd_ios.c | 300 ++++++++++++++++++ 4 files changed, 683 insertions(+) create mode 100644 channels/rdpsnd/client/ios/CMakeLists.txt create mode 100644 channels/rdpsnd/client/ios/TPCircularBuffer.c create mode 100644 channels/rdpsnd/client/ios/TPCircularBuffer.h create mode 100644 channels/rdpsnd/client/ios/rdpsnd_ios.c diff --git a/channels/rdpsnd/client/ios/CMakeLists.txt b/channels/rdpsnd/client/ios/CMakeLists.txt new file mode 100644 index 000000000..2db67b285 --- /dev/null +++ b/channels/rdpsnd/client/ios/CMakeLists.txt @@ -0,0 +1,52 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Laxmikant Rashinkar +# Copyright 2012 Marc-Andre Moreau +# Copyright 2013 Corey Clayton +# +# 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. + +define_channel_client_subsystem("rdpsnd" "ios" "") + +FIND_LIBRARY(CORE_AUDIO CoreAudio) +FIND_LIBRARY(AUDIO_TOOL AudioToolbox) +FIND_LIBRARY(CORE_FOUNDATION CoreFoundation) + +set(${MODULE_PREFIX}_SRCS + rdpsnd_ios.c + TPCircularBuffer.c) + +include_directories(..) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + +set_target_properties(${MODULE_NAME} PROPERTIES PREFIX "") + +set_complex_link_libraries(VARIABLE ${MODULE_PREFIX}_LIBS + MONOLITHIC ${MONOLITHIC_BUILD} + MODULE freerdp + MODULES freerdp-utils) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} + ${AUDIO_TOOL} + ${CORE_AUDIO} + ${CORE_FOUNDATION}) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +if(NOT STATIC_CHANNELS) + install(TARGETS ${MODULE_NAME} DESTINATION ${FREERDP_ADDIN_PATH}) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client/ios") diff --git a/channels/rdpsnd/client/ios/TPCircularBuffer.c b/channels/rdpsnd/client/ios/TPCircularBuffer.c new file mode 100644 index 000000000..d9e049ad2 --- /dev/null +++ b/channels/rdpsnd/client/ios/TPCircularBuffer.c @@ -0,0 +1,136 @@ +// +// TPCircularBuffer.c +// Circular/Ring buffer implementation +// +// https://github.com/michaeltyson/TPCircularBuffer +// +// Created by Michael Tyson on 10/12/2011. +// +// Copyright (C) 2012-2013 A Tasty Pixel +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#include "TPCircularBuffer.h" +#include +#include + +#define reportResult(result,operation) (_reportResult((result),(operation),strrchr(__FILE__, '/')+1,__LINE__)) +static inline bool _reportResult(kern_return_t result, const char *operation, const char* file, int line) { + if ( result != ERR_SUCCESS ) { + printf("%s:%d: %s: %s\n", file, line, operation, mach_error_string(result)); + return false; + } + return true; +} + +bool TPCircularBufferInit(TPCircularBuffer *buffer, int length) { + + // Keep trying until we get our buffer, needed to handle race conditions + int retries = 3; + while ( true ) { + + buffer->length = round_page(length); // We need whole page sizes + + // Temporarily allocate twice the length, so we have the contiguous address space to + // support a second instance of the buffer directly after + vm_address_t bufferAddress; + kern_return_t result = vm_allocate(mach_task_self(), + &bufferAddress, + buffer->length * 2, + VM_FLAGS_ANYWHERE); // allocate anywhere it'll fit + if ( result != ERR_SUCCESS ) { + if ( retries-- == 0 ) { + reportResult(result, "Buffer allocation"); + return false; + } + // Try again if we fail + continue; + } + + // Now replace the second half of the allocation with a virtual copy of the first half. Deallocate the second half... + result = vm_deallocate(mach_task_self(), + bufferAddress + buffer->length, + buffer->length); + if ( result != ERR_SUCCESS ) { + if ( retries-- == 0 ) { + reportResult(result, "Buffer deallocation"); + return false; + } + // If this fails somehow, deallocate the whole region and try again + vm_deallocate(mach_task_self(), bufferAddress, buffer->length); + continue; + } + + // Re-map the buffer to the address space immediately after the buffer + vm_address_t virtualAddress = bufferAddress + buffer->length; + vm_prot_t cur_prot, max_prot; + result = vm_remap(mach_task_self(), + &virtualAddress, // mirror target + buffer->length, // size of mirror + 0, // auto alignment + 0, // force remapping to virtualAddress + mach_task_self(), // same task + bufferAddress, // mirror source + 0, // MAP READ-WRITE, NOT COPY + &cur_prot, // unused protection struct + &max_prot, // unused protection struct + VM_INHERIT_DEFAULT); + if ( result != ERR_SUCCESS ) { + if ( retries-- == 0 ) { + reportResult(result, "Remap buffer memory"); + return false; + } + // If this remap failed, we hit a race condition, so deallocate and try again + vm_deallocate(mach_task_self(), bufferAddress, buffer->length); + continue; + } + + if ( virtualAddress != bufferAddress+buffer->length ) { + // If the memory is not contiguous, clean up both allocated buffers and try again + if ( retries-- == 0 ) { + printf("Couldn't map buffer memory to end of buffer\n"); + return false; + } + + vm_deallocate(mach_task_self(), virtualAddress, buffer->length); + vm_deallocate(mach_task_self(), bufferAddress, buffer->length); + continue; + } + + buffer->buffer = (void*)bufferAddress; + buffer->fillCount = 0; + buffer->head = buffer->tail = 0; + + return true; + } + return false; +} + +void TPCircularBufferCleanup(TPCircularBuffer *buffer) { + vm_deallocate(mach_task_self(), (vm_address_t)buffer->buffer, buffer->length * 2); + memset(buffer, 0, sizeof(TPCircularBuffer)); +} + +void TPCircularBufferClear(TPCircularBuffer *buffer) { + int32_t fillCount; + if ( TPCircularBufferTail(buffer, &fillCount) ) { + TPCircularBufferConsume(buffer, fillCount); + } +} diff --git a/channels/rdpsnd/client/ios/TPCircularBuffer.h b/channels/rdpsnd/client/ios/TPCircularBuffer.h new file mode 100644 index 000000000..cd2a4d837 --- /dev/null +++ b/channels/rdpsnd/client/ios/TPCircularBuffer.h @@ -0,0 +1,195 @@ +// +// TPCircularBuffer.h +// Circular/Ring buffer implementation +// +// https://github.com/michaeltyson/TPCircularBuffer +// +// Created by Michael Tyson on 10/12/2011. +// +// +// This implementation makes use of a virtual memory mapping technique that inserts a virtual copy +// of the buffer memory directly after the buffer's end, negating the need for any buffer wrap-around +// logic. Clients can simply use the returned memory address as if it were contiguous space. +// +// The implementation is thread-safe in the case of a single producer and single consumer. +// +// Virtual memory technique originally proposed by Philip Howard (http://vrb.slashusr.org/), and +// adapted to Darwin by Kurt Revis (http://www.snoize.com, +// http://www.snoize.com/Code/PlayBufferedSoundFile.tar.gz) +// +// +// Copyright (C) 2012-2013 A Tasty Pixel +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#ifndef TPCircularBuffer_h +#define TPCircularBuffer_h + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + void *buffer; + int32_t length; + int32_t tail; + int32_t head; + volatile int32_t fillCount; +} TPCircularBuffer; + +/*! + * Initialise buffer + * + * Note that the length is advisory only: Because of the way the + * memory mirroring technique works, the true buffer length will + * be multiples of the device page size (e.g. 4096 bytes) + * + * @param buffer Circular buffer + * @param length Length of buffer + */ +bool TPCircularBufferInit(TPCircularBuffer *buffer, int32_t length); + +/*! + * Cleanup buffer + * + * Releases buffer resources. + */ +void TPCircularBufferCleanup(TPCircularBuffer *buffer); + +/*! + * Clear buffer + * + * Resets buffer to original, empty state. + * + * This is safe for use by consumer while producer is accessing + * buffer. + */ +void TPCircularBufferClear(TPCircularBuffer *buffer); + +// Reading (consuming) + +/*! + * Access end of buffer + * + * This gives you a pointer to the end of the buffer, ready + * for reading, and the number of available bytes to read. + * + * @param buffer Circular buffer + * @param availableBytes On output, the number of bytes ready for reading + * @return Pointer to the first bytes ready for reading, or NULL if buffer is empty + */ +static __inline__ __attribute__((always_inline)) void* TPCircularBufferTail(TPCircularBuffer *buffer, int32_t* availableBytes) { + *availableBytes = buffer->fillCount; + if ( *availableBytes == 0 ) return NULL; + return (void*)((char*)buffer->buffer + buffer->tail); +} + +/*! + * Consume bytes in buffer + * + * This frees up the just-read bytes, ready for writing again. + * + * @param buffer Circular buffer + * @param amount Number of bytes to consume + */ +static __inline__ __attribute__((always_inline)) void TPCircularBufferConsume(TPCircularBuffer *buffer, int32_t amount) { + buffer->tail = (buffer->tail + amount) % buffer->length; + OSAtomicAdd32Barrier(-amount, &buffer->fillCount); + assert(buffer->fillCount >= 0); +} + +/*! + * Version of TPCircularBufferConsume without the memory barrier, for more optimal use in single-threaded contexts + */ +static __inline__ __attribute__((always_inline)) void TPCircularBufferConsumeNoBarrier(TPCircularBuffer *buffer, int32_t amount) { + buffer->tail = (buffer->tail + amount) % buffer->length; + buffer->fillCount -= amount; + assert(buffer->fillCount >= 0); +} + +/*! + * Access front of buffer + * + * This gives you a pointer to the front of the buffer, ready + * for writing, and the number of available bytes to write. + * + * @param buffer Circular buffer + * @param availableBytes On output, the number of bytes ready for writing + * @return Pointer to the first bytes ready for writing, or NULL if buffer is full + */ +static __inline__ __attribute__((always_inline)) void* TPCircularBufferHead(TPCircularBuffer *buffer, int32_t* availableBytes) { + *availableBytes = (buffer->length - buffer->fillCount); + if ( *availableBytes == 0 ) return NULL; + return (void*)((char*)buffer->buffer + buffer->head); +} + +// Writing (producing) + +/*! + * Produce bytes in buffer + * + * This marks the given section of the buffer ready for reading. + * + * @param buffer Circular buffer + * @param amount Number of bytes to produce + */ +static __inline__ __attribute__((always_inline)) void TPCircularBufferProduce(TPCircularBuffer *buffer, int amount) { + buffer->head = (buffer->head + amount) % buffer->length; + OSAtomicAdd32Barrier(amount, &buffer->fillCount); + assert(buffer->fillCount <= buffer->length); +} + +/*! + * Version of TPCircularBufferProduce without the memory barrier, for more optimal use in single-threaded contexts + */ +static __inline__ __attribute__((always_inline)) void TPCircularBufferProduceNoBarrier(TPCircularBuffer *buffer, int amount) { + buffer->head = (buffer->head + amount) % buffer->length; + buffer->fillCount += amount; + assert(buffer->fillCount <= buffer->length); +} + +/*! + * Helper routine to copy bytes to buffer + * + * This copies the given bytes to the buffer, and marks them ready for writing. + * + * @param buffer Circular buffer + * @param src Source buffer + * @param len Number of bytes in source buffer + * @return true if bytes copied, false if there was insufficient space + */ +static __inline__ __attribute__((always_inline)) bool TPCircularBufferProduceBytes(TPCircularBuffer *buffer, const void* src, int32_t len) { + int32_t space; + void *ptr = TPCircularBufferHead(buffer, &space); + if ( space < len ) return false; + memcpy(ptr, src, len); + TPCircularBufferProduce(buffer, len); + return true; +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/channels/rdpsnd/client/ios/rdpsnd_ios.c b/channels/rdpsnd/client/ios/rdpsnd_ios.c new file mode 100644 index 000000000..60d6b9655 --- /dev/null +++ b/channels/rdpsnd/client/ios/rdpsnd_ios.c @@ -0,0 +1,300 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2013 Dell Software + * + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include +#include + +#import + +#include "rdpsnd_main.h" +#include "TPCircularBuffer.h" + +#define INPUT_BUFFER_SIZE 32768 +#define CIRCULAR_BUFFER_SIZE (INPUT_BUFFER_SIZE * 4) + +typedef struct rdpsnd_ios_plugin +{ + rdpsndDevicePlugin device; + AudioComponentInstance audio_unit; + TPCircularBuffer buffer; + BOOL is_opened; + BOOL is_playing; +} rdpsndIOSPlugin; + +#define THIS(__ptr) ((rdpsndIOSPlugin*)__ptr) + +static OSStatus rdpsnd_ios_render_cb( + void *inRefCon, + AudioUnitRenderActionFlags __unused *ioActionFlags, + const AudioTimeStamp __unused *inTimeStamp, + UInt32 inBusNumber, + UInt32 __unused inNumberFrames, + AudioBufferList *ioData +) +{ + unsigned int i; + + if (inBusNumber != 0) + { + return noErr; + }; + + rdpsndIOSPlugin *p = THIS(inRefCon); + + for (i = 0; i < ioData->mNumberBuffers; i++) + { + AudioBuffer* target_buffer = &ioData->mBuffers[i]; + + int32_t available_bytes = 0; + const void *buffer = TPCircularBufferTail(&p->buffer, &available_bytes); + if (buffer != NULL && available_bytes > 0) + { + const int bytes_to_copy = MIN((int32_t)target_buffer->mDataByteSize, available_bytes); + + memcpy(target_buffer->mData, buffer, bytes_to_copy); + target_buffer->mDataByteSize = bytes_to_copy; + + TPCircularBufferConsume(&p->buffer, bytes_to_copy); + } + else + { + target_buffer->mDataByteSize = 0; + AudioOutputUnitStop(p->audio_unit); + p->is_playing = 0; + }; + }; + + return noErr; +} + +static BOOL rdpsnd_ios_format_supported(rdpsndDevicePlugin* __unused device, AUDIO_FORMAT* format) +{ + if (format->wFormatTag == WAVE_FORMAT_PCM) + { + return 1; + } + return 0; +} + +static void rdpsnd_ios_set_format(rdpsndDevicePlugin* __unused device, AUDIO_FORMAT* __unused format, int __unused latency) +{ +} + +static void rdpsnd_ios_set_volume(rdpsndDevicePlugin* __unused device, UINT32 __unused value) +{ +} + +static void rdpsnd_ios_start(rdpsndDevicePlugin* device) +{ + rdpsndIOSPlugin *p = THIS(device); + + /* If this device is not playing... */ + if (!p->is_playing) + { + /* Start the device. */ + int32_t available_bytes = 0; + TPCircularBufferTail(&p->buffer, &available_bytes); + if (available_bytes > 0) + { + p->is_playing = 1; + AudioOutputUnitStart(p->audio_unit); + } + } +} + +static void rdpsnd_ios_stop(rdpsndDevicePlugin* __unused device) +{ + rdpsndIOSPlugin *p = THIS(device); + + /* If the device is playing... */ + if (p->is_playing) + { + /* Stop the device. */ + AudioOutputUnitStop(p->audio_unit); + p->is_playing = 0; + + /* Free all buffers. */ + TPCircularBufferClear(&p->buffer); + }; +} + +static void rdpsnd_ios_play(rdpsndDevicePlugin* device, BYTE* data, int size) +{ + rdpsndIOSPlugin *p = THIS(device); + + const BOOL ok = TPCircularBufferProduceBytes(&p->buffer, data, size); + if (!ok) + { + return; + } + + rdpsnd_ios_start(device); + +} + +static void rdpsnd_ios_open(rdpsndDevicePlugin* device, AUDIO_FORMAT* format, int __unused latency) +{ + rdpsndIOSPlugin *p = THIS(device); + + if (p->is_opened) + { + return; + }; + + /* Find the output audio unit. */ + AudioComponentDescription desc; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = kAudioUnitSubType_RemoteIO; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + + AudioComponent audioComponent = AudioComponentFindNext(NULL, &desc); + if (audioComponent == NULL) return; + + /* Open the audio unit. */ + OSStatus status = AudioComponentInstanceNew(audioComponent, &p->audio_unit); + if (status != 0) return; + + /* Set the format for the AudioUnit. */ + AudioStreamBasicDescription audioFormat = {0}; + audioFormat.mSampleRate = format->nSamplesPerSec; + audioFormat.mFormatID = kAudioFormatLinearPCM; + audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + audioFormat.mFramesPerPacket = 1; /* imminent property of the Linear PCM */ + audioFormat.mChannelsPerFrame = format->nChannels; + audioFormat.mBitsPerChannel = format->wBitsPerSample; + audioFormat.mBytesPerFrame = (format->wBitsPerSample * format->nChannels) / 8; + audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame * audioFormat.mFramesPerPacket; + + status = AudioUnitSetProperty( + p->audio_unit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + 0, + &audioFormat, + sizeof(audioFormat)); + if (status != 0) + { + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + return; + } + + /* Set up the AudioUnit callback. */ + AURenderCallbackStruct callbackStruct = {0}; + callbackStruct.inputProc = rdpsnd_ios_render_cb; + callbackStruct.inputProcRefCon = p; + status = AudioUnitSetProperty( + p->audio_unit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + 0, + &callbackStruct, + sizeof(callbackStruct)); + if (status != 0) + { + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + return; + } + + /* Initialize the AudioUnit. */ + status = AudioUnitInitialize(p->audio_unit); + if (status != 0) + { + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + return; + } + + /* Allocate the circular buffer. */ + const BOOL ok = TPCircularBufferInit(&p->buffer, CIRCULAR_BUFFER_SIZE); + if (!ok) + { + AudioUnitUninitialize(p->audio_unit); + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + return; + } + + p->is_opened = 1; +} + +static void rdpsnd_ios_close(rdpsndDevicePlugin* device) +{ + rdpsndIOSPlugin *p = THIS(device); + + /* Make sure the device is stopped. */ + rdpsnd_ios_stop(device); + + /* If the device is open... */ + if (p->is_opened) + { + /* Close the device. */ + AudioUnitUninitialize(p->audio_unit); + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + p->is_opened = 0; + + /* Destroy the circular buffer. */ + TPCircularBufferCleanup(&p->buffer); + } +} + +static void rdpsnd_ios_free(rdpsndDevicePlugin* device) +{ + rdpsndIOSPlugin *p = THIS(device); + + /* Ensure the device is closed. */ + rdpsnd_ios_close(device); + + /* Free memory associated with the device. */ + free(p); +} + +#ifdef STATIC_CHANNELS +#define freerdp_rdpsnd_client_subsystem_entry ios_freerdp_rdpsnd_client_subsystem_entry +#endif + +int freerdp_rdpsnd_client_subsystem_entry(PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints) +{ + rdpsndIOSPlugin *p = (rdpsndIOSPlugin*)malloc(sizeof(rdpsndIOSPlugin)); + memset(p, 0, sizeof(rdpsndIOSPlugin)); + + p->device.Open = rdpsnd_ios_open; + p->device.FormatSupported = rdpsnd_ios_format_supported; + p->device.SetFormat = rdpsnd_ios_set_format; + p->device.SetVolume = rdpsnd_ios_set_volume; + p->device.Play = rdpsnd_ios_play; + p->device.Start = rdpsnd_ios_start; + p->device.Close = rdpsnd_ios_close; + p->device.Free = rdpsnd_ios_free; + + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)p); + + return 0; +} \ No newline at end of file