diff options
Diffstat (limited to 'channels/audin')
25 files changed, 6493 insertions, 0 deletions
diff --git a/channels/audin/CMakeLists.txt b/channels/audin/CMakeLists.txt new file mode 100644 index 0000000..d72b102 --- /dev/null +++ b/channels/audin/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# +# 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("audin") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/audin/ChannelOptions.cmake b/channels/audin/ChannelOptions.cmake new file mode 100644 index 0000000..39ca402 --- /dev/null +++ b/channels/audin/ChannelOptions.cmake @@ -0,0 +1,17 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT ON) + +if(ANDROID) + set(OPTION_SERVER_DEFAULT OFF) +endif() + +define_channel_options(NAME "audin" TYPE "dynamic" + DESCRIPTION "Audio Input Redirection Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEAI]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/audin/client/CMakeLists.txt b/channels/audin/client/CMakeLists.txt new file mode 100644 index 0000000..043e1b5 --- /dev/null +++ b/channels/audin/client/CMakeLists.txt @@ -0,0 +1,63 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# +# 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("audin") + +set(${MODULE_PREFIX}_SRCS + audin_main.c + audin_main.h +) + +set(${MODULE_PREFIX}_LIBS + freerdp winpr +) + +include_directories(..) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + +if(WITH_OSS) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "oss" "") +endif() + +if(WITH_ALSA) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "alsa" "") +endif() + +if(WITH_PULSE) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "pulse" "") +endif() + +if(WITH_OPENSLES) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "opensles" "") +endif() + +if(WITH_WINMM) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "winmm" "") +endif() + +if(WITH_MACAUDIO) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "mac" "") +endif() + +if(WITH_SNDIO) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "sndio" "") +endif() + +if(WITH_IOSAUDIO) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "ios" "") + endif() diff --git a/channels/audin/client/alsa/CMakeLists.txt b/channels/audin/client/alsa/CMakeLists.txt new file mode 100644 index 0000000..1213f74 --- /dev/null +++ b/channels/audin/client/alsa/CMakeLists.txt @@ -0,0 +1,35 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# +# 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("audin" "alsa" "") + +find_package(ALSA REQUIRED) + +set(${MODULE_PREFIX}_SRCS + audin_alsa.c +) + +set(${MODULE_PREFIX}_LIBS + winpr + freerdp + ${ALSA_LIBRARIES} +) + +include_directories(..) +include_directories(${ALSA_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") diff --git a/channels/audin/client/alsa/audin_alsa.c b/channels/audin/client/alsa/audin_alsa.c new file mode 100644 index 0000000..be3e52a --- /dev/null +++ b/channels/audin/client/alsa/audin_alsa.c @@ -0,0 +1,453 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - ALSA implementation + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * + * 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 <freerdp/config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <winpr/crt.h> +#include <winpr/synch.h> +#include <winpr/thread.h> +#include <winpr/cmdline.h> +#include <winpr/wlog.h> + +#include <alsa/asoundlib.h> + +#include <freerdp/freerdp.h> +#include <freerdp/addin.h> +#include <freerdp/channels/rdpsnd.h> + +#include "audin_main.h" + +typedef struct +{ + IAudinDevice iface; + + char* device_name; + UINT32 frames_per_packet; + AUDIO_FORMAT aformat; + + HANDLE thread; + HANDLE stopEvent; + + AudinReceive receive; + void* user_data; + + rdpContext* rdpcontext; + wLog* log; + int bytes_per_frame; +} AudinALSADevice; + +static snd_pcm_format_t audin_alsa_format(UINT32 wFormatTag, UINT32 bitPerChannel) +{ + switch (wFormatTag) + { + case WAVE_FORMAT_PCM: + switch (bitPerChannel) + { + case 16: + return SND_PCM_FORMAT_S16_LE; + + case 8: + return SND_PCM_FORMAT_S8; + + default: + return SND_PCM_FORMAT_UNKNOWN; + } + + case WAVE_FORMAT_ALAW: + return SND_PCM_FORMAT_A_LAW; + + case WAVE_FORMAT_MULAW: + return SND_PCM_FORMAT_MU_LAW; + + default: + return SND_PCM_FORMAT_UNKNOWN; + } +} + +static BOOL audin_alsa_set_params(AudinALSADevice* alsa, snd_pcm_t* capture_handle) +{ + int error = 0; + SSIZE_T s = 0; + UINT32 channels = alsa->aformat.nChannels; + snd_pcm_hw_params_t* hw_params = NULL; + snd_pcm_format_t format = + audin_alsa_format(alsa->aformat.wFormatTag, alsa->aformat.wBitsPerSample); + + if ((error = snd_pcm_hw_params_malloc(&hw_params)) < 0) + { + WLog_Print(alsa->log, WLOG_ERROR, "snd_pcm_hw_params_malloc (%s)", snd_strerror(error)); + return FALSE; + } + + snd_pcm_hw_params_any(capture_handle, hw_params); + snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); + snd_pcm_hw_params_set_format(capture_handle, hw_params, format); + snd_pcm_hw_params_set_rate_near(capture_handle, hw_params, &alsa->aformat.nSamplesPerSec, NULL); + snd_pcm_hw_params_set_channels_near(capture_handle, hw_params, &channels); + snd_pcm_hw_params(capture_handle, hw_params); + snd_pcm_hw_params_free(hw_params); + snd_pcm_prepare(capture_handle); + if (channels > UINT16_MAX) + return FALSE; + s = snd_pcm_format_size(format, 1); + if ((s < 0) || (s > UINT16_MAX)) + return FALSE; + alsa->aformat.nChannels = (UINT16)channels; + alsa->bytes_per_frame = (size_t)s * channels; + return TRUE; +} + +static DWORD WINAPI audin_alsa_thread_func(LPVOID arg) +{ + int error = 0; + BYTE* buffer = NULL; + snd_pcm_t* capture_handle = NULL; + AudinALSADevice* alsa = (AudinALSADevice*)arg; + DWORD status = 0; + WLog_Print(alsa->log, WLOG_DEBUG, "in"); + + if ((error = snd_pcm_open(&capture_handle, alsa->device_name, SND_PCM_STREAM_CAPTURE, 0)) < 0) + { + WLog_Print(alsa->log, WLOG_ERROR, "snd_pcm_open (%s)", snd_strerror(error)); + error = CHANNEL_RC_INITIALIZATION_ERROR; + goto out; + } + + if (!audin_alsa_set_params(alsa, capture_handle)) + { + WLog_Print(alsa->log, WLOG_ERROR, "audin_alsa_set_params failed"); + goto out; + } + + buffer = + (BYTE*)calloc(alsa->frames_per_packet + alsa->aformat.nBlockAlign, alsa->bytes_per_frame); + + if (!buffer) + { + WLog_Print(alsa->log, WLOG_ERROR, "calloc failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + while (1) + { + size_t frames = alsa->frames_per_packet; + status = WaitForSingleObject(alsa->stopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_Print(alsa->log, WLOG_ERROR, "WaitForSingleObject failed with error %ld!", error); + break; + } + + if (status == WAIT_OBJECT_0) + break; + + error = snd_pcm_readi(capture_handle, buffer, frames); + + if (error == 0) + continue; + + if (error == -EPIPE) + { + snd_pcm_recover(capture_handle, error, 0); + continue; + } + else if (error < 0) + { + WLog_Print(alsa->log, WLOG_ERROR, "snd_pcm_readi (%s)", snd_strerror(error)); + break; + } + + error = alsa->receive(&alsa->aformat, buffer, (long)error * alsa->bytes_per_frame, + alsa->user_data); + + if (error) + { + WLog_Print(alsa->log, WLOG_ERROR, "audin_alsa_thread_receive failed with error %ld", + error); + break; + } + } + + free(buffer); + + if (capture_handle) + snd_pcm_close(capture_handle); + +out: + WLog_Print(alsa->log, WLOG_DEBUG, "out"); + + if (error && alsa->rdpcontext) + setChannelError(alsa->rdpcontext, error, "audin_alsa_thread_func reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_alsa_free(IAudinDevice* device) +{ + AudinALSADevice* alsa = (AudinALSADevice*)device; + + if (alsa) + free(alsa->device_name); + + free(alsa); + return CHANNEL_RC_OK; +} + +static BOOL audin_alsa_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format) +{ + if (!device || !format) + return FALSE; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + if (format->cbSize == 0 && (format->nSamplesPerSec <= 48000) && + (format->wBitsPerSample == 8 || format->wBitsPerSample == 16) && + (format->nChannels == 1 || format->nChannels == 2)) + { + return TRUE; + } + + break; + + default: + return FALSE; + } + + return FALSE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_alsa_set_format(IAudinDevice* device, const AUDIO_FORMAT* format, + UINT32 FramesPerPacket) +{ + AudinALSADevice* alsa = (AudinALSADevice*)device; + + if (!alsa || !format) + return ERROR_INVALID_PARAMETER; + + alsa->aformat = *format; + alsa->frames_per_packet = FramesPerPacket; + + if (audin_alsa_format(format->wFormatTag, format->wBitsPerSample) == SND_PCM_FORMAT_UNKNOWN) + return ERROR_INTERNAL_ERROR; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_alsa_open(IAudinDevice* device, AudinReceive receive, void* user_data) +{ + AudinALSADevice* alsa = (AudinALSADevice*)device; + + if (!device || !receive || !user_data) + return ERROR_INVALID_PARAMETER; + + alsa->receive = receive; + alsa->user_data = user_data; + + if (!(alsa->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_Print(alsa->log, WLOG_ERROR, "CreateEvent failed!"); + goto error_out; + } + + if (!(alsa->thread = CreateThread(NULL, 0, audin_alsa_thread_func, alsa, 0, NULL))) + { + WLog_Print(alsa->log, WLOG_ERROR, "CreateThread failed!"); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + CloseHandle(alsa->stopEvent); + alsa->stopEvent = NULL; + return ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_alsa_close(IAudinDevice* device) +{ + UINT error = CHANNEL_RC_OK; + AudinALSADevice* alsa = (AudinALSADevice*)device; + + if (!alsa) + return ERROR_INVALID_PARAMETER; + + if (alsa->stopEvent) + { + SetEvent(alsa->stopEvent); + + if (WaitForSingleObject(alsa->thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_Print(alsa->log, WLOG_ERROR, "WaitForSingleObject failed with error %" PRIu32 "", + error); + return error; + } + + CloseHandle(alsa->stopEvent); + alsa->stopEvent = NULL; + CloseHandle(alsa->thread); + alsa->thread = NULL; + } + + alsa->receive = NULL; + alsa->user_data = NULL; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_alsa_parse_addin_args(AudinALSADevice* device, const ADDIN_ARGV* args) +{ + int status = 0; + DWORD flags = 0; + const COMMAND_LINE_ARGUMENT_A* arg = NULL; + AudinALSADevice* alsa = (AudinALSADevice*)device; + COMMAND_LINE_ARGUMENT_A audin_alsa_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", + NULL, NULL, -1, NULL, "audio device name" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, audin_alsa_args, flags, alsa, NULL, + NULL); + + if (status < 0) + return ERROR_INVALID_PARAMETER; + + arg = audin_alsa_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + alsa->device_name = _strdup(arg->Value); + + if (!alsa->device_name) + { + WLog_Print(alsa->log, WLOG_ERROR, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT( + UINT alsa_freerdp_audin_client_subsystem_entry(PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + const ADDIN_ARGV* args = NULL; + AudinALSADevice* alsa = NULL; + UINT error = 0; + alsa = (AudinALSADevice*)calloc(1, sizeof(AudinALSADevice)); + + if (!alsa) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + alsa->log = WLog_Get(TAG); + alsa->iface.Open = audin_alsa_open; + alsa->iface.FormatSupported = audin_alsa_format_supported; + alsa->iface.SetFormat = audin_alsa_set_format; + alsa->iface.Close = audin_alsa_close; + alsa->iface.Free = audin_alsa_free; + alsa->rdpcontext = pEntryPoints->rdpcontext; + args = pEntryPoints->args; + + if ((error = audin_alsa_parse_addin_args(alsa, args))) + { + WLog_Print(alsa->log, WLOG_ERROR, + "audin_alsa_parse_addin_args failed with errorcode %" PRIu32 "!", error); + goto error_out; + } + + if (!alsa->device_name) + { + alsa->device_name = _strdup("default"); + + if (!alsa->device_name) + { + WLog_Print(alsa->log, WLOG_ERROR, "_strdup failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + } + + alsa->frames_per_packet = 128; + alsa->aformat.nChannels = 2; + alsa->aformat.wBitsPerSample = 16; + alsa->aformat.wFormatTag = WAVE_FORMAT_PCM; + alsa->aformat.nSamplesPerSec = 44100; + + if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice*)alsa))) + { + WLog_Print(alsa->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!", + error); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + free(alsa->device_name); + free(alsa); + return error; +} diff --git a/channels/audin/client/audin_main.c b/channels/audin/client/audin_main.c new file mode 100644 index 0000000..1578d26 --- /dev/null +++ b/channels/audin/client/audin_main.c @@ -0,0 +1,1109 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * Copyright 2015 Armin Novak <armin.novak@thincast.com> + * + * 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 <freerdp/config.h> + +#include <errno.h> +#include <winpr/assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <winpr/crt.h> +#include <winpr/cmdline.h> +#include <winpr/wlog.h> + +#include <freerdp/addin.h> + +#include <winpr/stream.h> +#include <freerdp/freerdp.h> +#include <freerdp/codec/dsp.h> +#include <freerdp/client/channels.h> +#include <freerdp/channels/audin.h> + +#include "audin_main.h" + +#define SNDIN_VERSION 0x02 + +typedef enum +{ + MSG_SNDIN_VERSION = 0x01, + MSG_SNDIN_FORMATS = 0x02, + MSG_SNDIN_OPEN = 0x03, + MSG_SNDIN_OPEN_REPLY = 0x04, + MSG_SNDIN_DATA_INCOMING = 0x05, + MSG_SNDIN_DATA = 0x06, + MSG_SNDIN_FORMATCHANGE = 0x07, +} MSG_SNDIN; + +typedef struct +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; + + /** + * The supported format list sent back to the server, which needs to + * be stored as reference when the server sends the format index in + * Open PDU and Format Change PDU + */ + AUDIO_FORMAT* formats; + UINT32 formats_count; +} AUDIN_CHANNEL_CALLBACK; + +typedef struct +{ + IWTSPlugin iface; + + GENERIC_LISTENER_CALLBACK* listener_callback; + + /* Parsed plugin data */ + AUDIO_FORMAT* fixed_format; + char* subsystem; + char* device_name; + + /* Device interface */ + IAudinDevice* device; + + rdpContext* rdpcontext; + BOOL attached; + wStream* data; + AUDIO_FORMAT* format; + UINT32 FramesPerPacket; + + FREERDP_DSP_CONTEXT* dsp_context; + wLog* log; + + IWTSListener* listener; + + BOOL initialized; + UINT32 version; +} AUDIN_PLUGIN; + +static BOOL audin_process_addin_args(AUDIN_PLUGIN* audin, const ADDIN_ARGV* args); + +static UINT audin_channel_write_and_free(AUDIN_CHANNEL_CALLBACK* callback, wStream* out, + BOOL freeStream) +{ + if (!callback || !out) + return ERROR_INVALID_PARAMETER; + + if (!callback->channel || !callback->channel->Write) + return ERROR_INTERNAL_ERROR; + + Stream_SealLength(out); + WINPR_ASSERT(Stream_Length(out) <= ULONG_MAX); + const UINT error = callback->channel->Write(callback->channel, (ULONG)Stream_Length(out), + Stream_Buffer(out), NULL); + + if (freeStream) + Stream_Free(out, TRUE); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_process_version(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback, wStream* s) +{ + const UINT32 ClientVersion = SNDIN_VERSION; + UINT32 ServerVersion = 0; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 4)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, ServerVersion); + WLog_Print(audin->log, WLOG_DEBUG, "ServerVersion=%" PRIu32 ", ClientVersion=%" PRIu32, + ServerVersion, ClientVersion); + + /* Do not answer server packet, we do not support the channel version. */ + if (ServerVersion > ClientVersion) + { + WLog_Print(audin->log, WLOG_WARN, + "Incompatible channel version server=%" PRIu32 + ", client supports version=%" PRIu32, + ServerVersion, ClientVersion); + return CHANNEL_RC_OK; + } + audin->version = ServerVersion; + + wStream* out = Stream_New(NULL, 5); + + if (!out) + { + WLog_Print(audin->log, WLOG_ERROR, "Stream_New failed!"); + return ERROR_OUTOFMEMORY; + } + + Stream_Write_UINT8(out, MSG_SNDIN_VERSION); + Stream_Write_UINT32(out, ClientVersion); + return audin_channel_write_and_free(callback, out, TRUE); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_send_incoming_data_pdu(AUDIN_CHANNEL_CALLBACK* callback) +{ + BYTE out_data[1] = { MSG_SNDIN_DATA_INCOMING }; + + if (!callback || !callback->channel || !callback->channel->Write) + return ERROR_INTERNAL_ERROR; + + return callback->channel->Write(callback->channel, 1, out_data, NULL); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_process_formats(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT error = ERROR_INTERNAL_ERROR; + UINT32 NumFormats = 0; + UINT32 cbSizeFormatsPacket = 0; + + WINPR_ASSERT(audin); + WINPR_ASSERT(callback); + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 8)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, NumFormats); + WLog_Print(audin->log, WLOG_DEBUG, "NumFormats %" PRIu32 "", NumFormats); + + if ((NumFormats < 1) || (NumFormats > 1000)) + { + WLog_Print(audin->log, WLOG_ERROR, "bad NumFormats %" PRIu32 "", NumFormats); + return ERROR_INVALID_DATA; + } + + Stream_Seek_UINT32(s); /* cbSizeFormatsPacket */ + callback->formats = audio_formats_new(NumFormats); + + if (!callback->formats) + { + WLog_Print(audin->log, WLOG_ERROR, "calloc failed!"); + return ERROR_INVALID_DATA; + } + + wStream* out = Stream_New(NULL, 9); + + if (!out) + { + error = CHANNEL_RC_NO_MEMORY; + WLog_Print(audin->log, WLOG_ERROR, "Stream_New failed!"); + goto out; + } + + Stream_Seek(out, 9); + + /* SoundFormats (variable) */ + for (UINT32 i = 0; i < NumFormats; i++) + { + AUDIO_FORMAT format = { 0 }; + + if (!audio_format_read(s, &format)) + { + error = ERROR_INVALID_DATA; + goto out; + } + + audio_format_print(audin->log, WLOG_DEBUG, &format); + + if (!audio_format_compatible(audin->fixed_format, &format)) + { + audio_format_free(&format); + continue; + } + + if (freerdp_dsp_supports_format(&format, TRUE) || + audin->device->FormatSupported(audin->device, &format)) + { + /* Store the agreed format in the corresponding index */ + callback->formats[callback->formats_count++] = format; + + if (!audio_format_write(out, &format)) + { + error = CHANNEL_RC_NO_MEMORY; + WLog_Print(audin->log, WLOG_ERROR, "Stream_EnsureRemainingCapacity failed!"); + goto out; + } + } + else + { + audio_format_free(&format); + } + } + + if ((error = audin_send_incoming_data_pdu(callback))) + { + WLog_Print(audin->log, WLOG_ERROR, "audin_send_incoming_data_pdu failed!"); + goto out; + } + + cbSizeFormatsPacket = (UINT32)Stream_GetPosition(out); + Stream_SetPosition(out, 0); + Stream_Write_UINT8(out, MSG_SNDIN_FORMATS); /* Header (1 byte) */ + Stream_Write_UINT32(out, callback->formats_count); /* NumFormats (4 bytes) */ + Stream_Write_UINT32(out, cbSizeFormatsPacket); /* cbSizeFormatsPacket (4 bytes) */ + Stream_SetPosition(out, cbSizeFormatsPacket); + error = audin_channel_write_and_free(callback, out, FALSE); +out: + + if (error != CHANNEL_RC_OK) + { + audio_formats_free(callback->formats, NumFormats); + callback->formats = NULL; + } + + Stream_Free(out, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_send_format_change_pdu(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback, + UINT32 NewFormat) +{ + WINPR_ASSERT(audin); + WINPR_ASSERT(callback); + + wStream* out = Stream_New(NULL, 5); + + if (!out) + { + WLog_Print(audin->log, WLOG_ERROR, "Stream_New failed!"); + return CHANNEL_RC_OK; + } + + Stream_Write_UINT8(out, MSG_SNDIN_FORMATCHANGE); + Stream_Write_UINT32(out, NewFormat); + return audin_channel_write_and_free(callback, out, TRUE); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_send_open_reply_pdu(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback, + UINT32 Result) +{ + WINPR_ASSERT(audin); + WINPR_ASSERT(callback); + + wStream* out = Stream_New(NULL, 5); + + if (!out) + { + WLog_Print(audin->log, WLOG_ERROR, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT8(out, MSG_SNDIN_OPEN_REPLY); + Stream_Write_UINT32(out, Result); + return audin_channel_write_and_free(callback, out, TRUE); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_receive_wave_data(const AUDIO_FORMAT* format, const BYTE* data, size_t size, + void* user_data) +{ + WINPR_ASSERT(format); + + UINT error = ERROR_INTERNAL_ERROR; + AUDIN_CHANNEL_CALLBACK* callback = (AUDIN_CHANNEL_CALLBACK*)user_data; + + if (!callback) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)callback->plugin; + + if (!audin) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + if (!audin->attached) + return CHANNEL_RC_OK; + + Stream_SetPosition(audin->data, 0); + + if (!Stream_EnsureRemainingCapacity(audin->data, 1)) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write_UINT8(audin->data, MSG_SNDIN_DATA); + + const BOOL compatible = audio_format_compatible(format, audin->format); + if (compatible && audin->device->FormatSupported(audin->device, audin->format)) + { + if (!Stream_EnsureRemainingCapacity(audin->data, size)) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write(audin->data, data, size); + } + else + { + if (!freerdp_dsp_encode(audin->dsp_context, format, data, size, audin->data)) + return ERROR_INTERNAL_ERROR; + } + + /* Did not encode anything, skip this, the codec is not ready for output. */ + if (Stream_GetPosition(audin->data) <= 1) + return CHANNEL_RC_OK; + + audio_format_print(audin->log, WLOG_TRACE, audin->format); + WLog_Print(audin->log, WLOG_TRACE, "[%" PRIdz "/%" PRIdz "]", size, + Stream_GetPosition(audin->data) - 1); + + if ((error = audin_send_incoming_data_pdu(callback))) + { + WLog_Print(audin->log, WLOG_ERROR, "audin_send_incoming_data_pdu failed!"); + return error; + } + + return audin_channel_write_and_free(callback, audin->data, FALSE); +} + +static BOOL audin_open_device(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback) +{ + UINT error = ERROR_INTERNAL_ERROR; + AUDIO_FORMAT format = { 0 }; + + if (!audin || !audin->device) + return FALSE; + + format = *audin->format; + const BOOL supported = + IFCALLRESULT(FALSE, audin->device->FormatSupported, audin->device, &format); + WLog_Print(audin->log, WLOG_DEBUG, "microphone uses %s codec", + audio_format_get_tag_string(format.wFormatTag)); + + if (!supported) + { + /* Default sample rates supported by most backends. */ + const UINT32 samplerates[] = { format.nSamplesPerSec, 96000, 48000, 44100, 22050 }; + BOOL test = FALSE; + + format.wFormatTag = WAVE_FORMAT_PCM; + format.wBitsPerSample = 16; + format.cbSize = 0; + for (size_t x = 0; x < ARRAYSIZE(samplerates); x++) + { + format.nSamplesPerSec = samplerates[x]; + for (UINT16 y = audin->format->nChannels; y > 0; y--) + { + format.nChannels = y; + format.nBlockAlign = 2 * format.nChannels; + test = IFCALLRESULT(FALSE, audin->device->FormatSupported, audin->device, &format); + if (test) + break; + } + if (test) + break; + } + if (!test) + return FALSE; + } + + IFCALLRET(audin->device->SetFormat, error, audin->device, &format, audin->FramesPerPacket); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "SetFormat failed with errorcode %" PRIu32 "", error); + return FALSE; + } + + if (!freerdp_dsp_context_reset(audin->dsp_context, audin->format, audin->FramesPerPacket)) + return FALSE; + + IFCALLRET(audin->device->Open, error, audin->device, audin_receive_wave_data, callback); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "Open failed with errorcode %" PRIu32 "", error); + return FALSE; + } + + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_process_open(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT32 initialFormat = 0; + UINT32 FramesPerPacket = 0; + UINT error = CHANNEL_RC_OK; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 8)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, FramesPerPacket); + Stream_Read_UINT32(s, initialFormat); + WLog_Print(audin->log, WLOG_DEBUG, "FramesPerPacket=%" PRIu32 " initialFormat=%" PRIu32 "", + FramesPerPacket, initialFormat); + audin->FramesPerPacket = FramesPerPacket; + + if (initialFormat >= callback->formats_count) + { + WLog_Print(audin->log, WLOG_ERROR, "invalid format index %" PRIu32 " (total %" PRIu32 ")", + initialFormat, callback->formats_count); + return ERROR_INVALID_DATA; + } + + audin->format = &callback->formats[initialFormat]; + + if (!audin_open_device(audin, callback)) + return ERROR_INTERNAL_ERROR; + + if ((error = audin_send_format_change_pdu(audin, callback, initialFormat))) + { + WLog_Print(audin->log, WLOG_ERROR, "audin_send_format_change_pdu failed!"); + return error; + } + + if ((error = audin_send_open_reply_pdu(audin, callback, 0))) + WLog_Print(audin->log, WLOG_ERROR, "audin_send_open_reply_pdu failed!"); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_process_format_change(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback, + wStream* s) +{ + UINT32 NewFormat = 0; + UINT error = CHANNEL_RC_OK; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 4)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, NewFormat); + WLog_Print(audin->log, WLOG_DEBUG, "NewFormat=%" PRIu32 "", NewFormat); + + if (NewFormat >= callback->formats_count) + { + WLog_Print(audin->log, WLOG_ERROR, "invalid format index %" PRIu32 " (total %" PRIu32 ")", + NewFormat, callback->formats_count); + return ERROR_INVALID_DATA; + } + + audin->format = &callback->formats[NewFormat]; + + if (audin->device) + { + IFCALLRET(audin->device->Close, error, audin->device); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "Close failed with errorcode %" PRIu32 "", error); + return error; + } + } + + if (!audin_open_device(audin, callback)) + return ERROR_INTERNAL_ERROR; + + if ((error = audin_send_format_change_pdu(audin, callback, NewFormat))) + WLog_ERR(TAG, "audin_send_format_change_pdu failed!"); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + UINT error = 0; + BYTE MessageId = 0; + AUDIN_CHANNEL_CALLBACK* callback = (AUDIN_CHANNEL_CALLBACK*)pChannelCallback; + + if (!callback || !data) + return ERROR_INVALID_PARAMETER; + + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)callback->plugin; + + if (!audin) + return ERROR_INTERNAL_ERROR; + + if (!Stream_CheckAndLogRequiredCapacity(TAG, data, 1)) + return ERROR_NO_DATA; + + Stream_Read_UINT8(data, MessageId); + WLog_Print(audin->log, WLOG_DEBUG, "MessageId=0x%02" PRIx8 "", MessageId); + + switch (MessageId) + { + case MSG_SNDIN_VERSION: + error = audin_process_version(audin, callback, data); + break; + + case MSG_SNDIN_FORMATS: + error = audin_process_formats(audin, callback, data); + break; + + case MSG_SNDIN_OPEN: + error = audin_process_open(audin, callback, data); + break; + + case MSG_SNDIN_FORMATCHANGE: + error = audin_process_format_change(audin, callback, data); + break; + + default: + WLog_Print(audin->log, WLOG_ERROR, "unknown MessageId=0x%02" PRIx8 "", MessageId); + error = ERROR_INVALID_DATA; + break; + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + AUDIN_CHANNEL_CALLBACK* callback = (AUDIN_CHANNEL_CALLBACK*)pChannelCallback; + WINPR_ASSERT(callback); + + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)callback->plugin; + WINPR_ASSERT(audin); + + UINT error = CHANNEL_RC_OK; + WLog_Print(audin->log, WLOG_TRACE, "..."); + + if (audin->device) + { + IFCALLRET(audin->device->Close, error, audin->device); + + if (error != CHANNEL_RC_OK) + WLog_Print(audin->log, WLOG_ERROR, "Close failed with errorcode %" PRIu32 "", error); + } + + audin->format = NULL; + audio_formats_free(callback->formats, callback->formats_count); + free(callback); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, + BOOL* pbAccept, IWTSVirtualChannelCallback** ppCallback) +{ + GENERIC_LISTENER_CALLBACK* listener_callback = (GENERIC_LISTENER_CALLBACK*)pListenerCallback; + + if (!listener_callback || !listener_callback->plugin) + return ERROR_INTERNAL_ERROR; + + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)listener_callback->plugin; + WLog_Print(audin->log, WLOG_TRACE, "..."); + AUDIN_CHANNEL_CALLBACK* callback = + (AUDIN_CHANNEL_CALLBACK*)calloc(1, sizeof(AUDIN_CHANNEL_CALLBACK)); + + if (!callback) + { + WLog_Print(audin->log, WLOG_ERROR, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnDataReceived = audin_on_data_received; + callback->iface.OnClose = audin_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + *ppCallback = (IWTSVirtualChannelCallback*)callback; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)pPlugin; + + if (!audin) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + if (!pChannelMgr) + return ERROR_INVALID_PARAMETER; + + if (audin->initialized) + { + WLog_ERR(TAG, "[%s] channel initialized twice, aborting", AUDIN_DVC_CHANNEL_NAME); + return ERROR_INVALID_DATA; + } + + WLog_Print(audin->log, WLOG_TRACE, "..."); + audin->listener_callback = + (GENERIC_LISTENER_CALLBACK*)calloc(1, sizeof(GENERIC_LISTENER_CALLBACK)); + + if (!audin->listener_callback) + { + WLog_Print(audin->log, WLOG_ERROR, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + audin->listener_callback->iface.OnNewChannelConnection = audin_on_new_channel_connection; + audin->listener_callback->plugin = pPlugin; + audin->listener_callback->channel_mgr = pChannelMgr; + const UINT rc = pChannelMgr->CreateListener(pChannelMgr, AUDIN_DVC_CHANNEL_NAME, 0, + &audin->listener_callback->iface, &audin->listener); + + audin->initialized = rc == CHANNEL_RC_OK; + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_plugin_terminated(IWTSPlugin* pPlugin) +{ + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)pPlugin; + UINT error = CHANNEL_RC_OK; + + if (!audin) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + WLog_Print(audin->log, WLOG_TRACE, "..."); + + if (audin->listener_callback) + { + IWTSVirtualChannelManager* mgr = audin->listener_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, audin->listener); + } + audio_formats_free(audin->fixed_format, 1); + + if (audin->device) + { + IFCALLRET(audin->device->Free, error, audin->device); + + if (error != CHANNEL_RC_OK) + { + WLog_Print(audin->log, WLOG_ERROR, "Free failed with errorcode %" PRIu32 "", error); + // dont stop on error + } + + audin->device = NULL; + } + + freerdp_dsp_context_free(audin->dsp_context); + Stream_Free(audin->data, TRUE); + free(audin->subsystem); + free(audin->device_name); + free(audin->listener_callback); + free(audin); + return CHANNEL_RC_OK; +} + +static UINT audin_plugin_attached(IWTSPlugin* pPlugin) +{ + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)pPlugin; + UINT error = CHANNEL_RC_OK; + + if (!audin) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + audin->attached = TRUE; + return error; +} + +static UINT audin_plugin_detached(IWTSPlugin* pPlugin) +{ + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)pPlugin; + UINT error = CHANNEL_RC_OK; + + if (!audin) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + audin->attached = FALSE; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_register_device_plugin(IWTSPlugin* pPlugin, IAudinDevice* device) +{ + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)pPlugin; + + WINPR_ASSERT(audin); + + if (audin->device) + { + WLog_Print(audin->log, WLOG_ERROR, "existing device, abort."); + return ERROR_ALREADY_EXISTS; + } + + WLog_Print(audin->log, WLOG_DEBUG, "device registered."); + audin->device = device; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_load_device_plugin(AUDIN_PLUGIN* audin, const char* name, const ADDIN_ARGV* args) +{ + WINPR_ASSERT(audin); + + FREERDP_AUDIN_DEVICE_ENTRY_POINTS entryPoints = { 0 }; + UINT error = ERROR_INTERNAL_ERROR; + const PFREERDP_AUDIN_DEVICE_ENTRY entry = + (const PFREERDP_AUDIN_DEVICE_ENTRY)freerdp_load_channel_addin_entry(AUDIN_CHANNEL_NAME, + name, NULL, 0); + + if (entry == NULL) + { + WLog_Print(audin->log, WLOG_ERROR, + "freerdp_load_channel_addin_entry did not return any function pointers for %s ", + name); + return ERROR_INVALID_FUNCTION; + } + + entryPoints.plugin = &audin->iface; + entryPoints.pRegisterAudinDevice = audin_register_device_plugin; + entryPoints.args = args; + entryPoints.rdpcontext = audin->rdpcontext; + + if ((error = entry(&entryPoints))) + { + WLog_Print(audin->log, WLOG_ERROR, "%s entry returned error %" PRIu32 ".", name, error); + return error; + } + + WLog_Print(audin->log, WLOG_INFO, "Loaded %s backend for audin", name); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_set_subsystem(AUDIN_PLUGIN* audin, const char* subsystem) +{ + WINPR_ASSERT(audin); + + free(audin->subsystem); + audin->subsystem = _strdup(subsystem); + + if (!audin->subsystem) + { + WLog_Print(audin->log, WLOG_ERROR, "_strdup failed!"); + return ERROR_NOT_ENOUGH_MEMORY; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_set_device_name(AUDIN_PLUGIN* audin, const char* device_name) +{ + WINPR_ASSERT(audin); + + free(audin->device_name); + audin->device_name = _strdup(device_name); + + if (!audin->device_name) + { + WLog_Print(audin->log, WLOG_ERROR, "_strdup failed!"); + return ERROR_NOT_ENOUGH_MEMORY; + } + + return CHANNEL_RC_OK; +} + +BOOL audin_process_addin_args(AUDIN_PLUGIN* audin, const ADDIN_ARGV* args) +{ + COMMAND_LINE_ARGUMENT_A audin_args[] = { + { "sys", COMMAND_LINE_VALUE_REQUIRED, "<subsystem>", NULL, NULL, -1, NULL, "subsystem" }, + { "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", NULL, NULL, -1, NULL, "device" }, + { "format", COMMAND_LINE_VALUE_REQUIRED, "<format>", NULL, NULL, -1, NULL, "format" }, + { "rate", COMMAND_LINE_VALUE_REQUIRED, "<rate>", NULL, NULL, -1, NULL, "rate" }, + { "channel", COMMAND_LINE_VALUE_REQUIRED, "<channel>", NULL, NULL, -1, NULL, "channel" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } + }; + + if (!args || args->argc == 1) + return TRUE; + + const DWORD flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + const int status = + CommandLineParseArgumentsA(args->argc, args->argv, audin_args, flags, audin, NULL, NULL); + + if (status != 0) + return FALSE; + + const COMMAND_LINE_ARGUMENT_A* arg = audin_args; + errno = 0; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "sys") + { + const UINT error = audin_set_subsystem(audin, arg->Value); + if (error != CHANNEL_RC_OK) + { + WLog_Print(audin->log, WLOG_ERROR, + "audin_set_subsystem failed with error %" PRIu32 "!", error); + return FALSE; + } + } + CommandLineSwitchCase(arg, "dev") + { + const UINT error = audin_set_device_name(audin, arg->Value); + if (error != CHANNEL_RC_OK) + { + WLog_Print(audin->log, WLOG_ERROR, + "audin_set_device_name failed with error %" PRIu32 "!", error); + return FALSE; + } + } + CommandLineSwitchCase(arg, "format") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val > UINT16_MAX)) + return FALSE; + + audin->fixed_format->wFormatTag = (UINT16)val; + } + CommandLineSwitchCase(arg, "rate") + { + long val = strtol(arg->Value, NULL, 0); + + if ((errno != 0) || (val < INT32_MIN) || (val > INT32_MAX)) + return FALSE; + + audin->fixed_format->nSamplesPerSec = val; + } + CommandLineSwitchCase(arg, "channel") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val <= UINT16_MAX)) + audin->fixed_format->nChannels = (UINT16)val; + } + CommandLineSwitchDefault(arg) + { + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT(UINT audin_DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints)) +{ + struct SubsystemEntry + { + char* subsystem; + char* device; + }; + UINT error = CHANNEL_RC_INITIALIZATION_ERROR; + struct SubsystemEntry entries[] = + { +#if defined(WITH_PULSE) + { "pulse", "" }, +#endif +#if defined(WITH_OSS) + { "oss", "default" }, +#endif +#if defined(WITH_ALSA) + { "alsa", "default" }, +#endif +#if defined(WITH_OPENSLES) + { "opensles", "default" }, +#endif +#if defined(WITH_WINMM) + { "winmm", "default" }, +#endif +#if defined(WITH_MACAUDIO) + { "mac", "default" }, +#endif +#if defined(WITH_IOSAUDIO) + { "ios", "default" }, +#endif +#if defined(WITH_SNDIO) + { "sndio", "default" }, +#endif + { NULL, NULL } + }; + struct SubsystemEntry* entry = &entries[0]; + WINPR_ASSERT(pEntryPoints); + WINPR_ASSERT(pEntryPoints->GetPlugin); + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, AUDIN_CHANNEL_NAME); + + if (audin != NULL) + return CHANNEL_RC_ALREADY_INITIALIZED; + + audin = (AUDIN_PLUGIN*)calloc(1, sizeof(AUDIN_PLUGIN)); + + if (!audin) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + audin->log = WLog_Get(TAG); + audin->data = Stream_New(NULL, 4096); + audin->fixed_format = audio_format_new(); + + if (!audin->fixed_format) + goto out; + + if (!audin->data) + goto out; + + audin->dsp_context = freerdp_dsp_context_new(TRUE); + + if (!audin->dsp_context) + goto out; + + audin->attached = TRUE; + audin->iface.Initialize = audin_plugin_initialize; + audin->iface.Connected = NULL; + audin->iface.Disconnected = NULL; + audin->iface.Terminated = audin_plugin_terminated; + audin->iface.Attached = audin_plugin_attached; + audin->iface.Detached = audin_plugin_detached; + + const ADDIN_ARGV* args = pEntryPoints->GetPluginData(pEntryPoints); + audin->rdpcontext = pEntryPoints->GetRdpContext(pEntryPoints); + + if (args) + { + if (!audin_process_addin_args(audin, args)) + goto out; + } + + if (audin->subsystem) + { + if ((error = audin_load_device_plugin(audin, audin->subsystem, args))) + { + WLog_Print( + audin->log, WLOG_ERROR, + "Unable to load microphone redirection subsystem %s because of error %" PRIu32 "", + audin->subsystem, error); + goto out; + } + } + else + { + while (entry && entry->subsystem && !audin->device) + { + if ((error = audin_set_subsystem(audin, entry->subsystem))) + { + WLog_Print(audin->log, WLOG_ERROR, + "audin_set_subsystem for %s failed with error %" PRIu32 "!", + entry->subsystem, error); + } + else if ((error = audin_set_device_name(audin, entry->device))) + { + WLog_Print(audin->log, WLOG_ERROR, + "audin_set_device_name for %s failed with error %" PRIu32 "!", + entry->subsystem, error); + } + else if ((error = audin_load_device_plugin(audin, audin->subsystem, args))) + { + WLog_Print(audin->log, WLOG_ERROR, + "audin_load_device_plugin %s failed with error %" PRIu32 "!", + entry->subsystem, error); + } + + entry++; + } + } + + if (audin->device == NULL) + { + /* If we have no audin device do not register plugin but still return OK or the client will + * just disconnect due to a missing microphone. */ + WLog_Print(audin->log, WLOG_ERROR, "No microphone device could be found."); + error = CHANNEL_RC_OK; + goto out; + } + + error = pEntryPoints->RegisterPlugin(pEntryPoints, AUDIN_CHANNEL_NAME, &audin->iface); + if (error == CHANNEL_RC_OK) + return error; + +out: + audin_plugin_terminated(&audin->iface); + return error; +} diff --git a/channels/audin/client/audin_main.h b/channels/audin/client/audin_main.h new file mode 100644 index 0000000..1e6a498 --- /dev/null +++ b/channels/audin/client/audin_main.h @@ -0,0 +1,33 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * + * 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. + */ + +#ifndef FREERDP_CHANNEL_AUDIN_CLIENT_MAIN_H +#define FREERDP_CHANNEL_AUDIN_CLIENT_MAIN_H + +#include <freerdp/config.h> + +#include <freerdp/dvc.h> +#include <freerdp/types.h> +#include <freerdp/addin.h> +#include <freerdp/channels/log.h> +#include <freerdp/client/audin.h> + +#define TAG CHANNELS_TAG("audin.client") + +#endif /* FREERDP_CHANNEL_AUDIN_CLIENT_MAIN_H */ diff --git a/channels/audin/client/ios/CMakeLists.txt b/channels/audin/client/ios/CMakeLists.txt new file mode 100644 index 0000000..e6ec3ad --- /dev/null +++ b/channels/audin/client/ios/CMakeLists.txt @@ -0,0 +1,39 @@ +# FreeRDP: A Remote Desktop Protocol Implementation + # FreeRDP cmake build script + # + # Copyright (c) 2015 Armin Novak <armin.novak@thincast.com> + # Copyright (c) 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. + + define_channel_client_subsystem("audin" "ios" "") + FIND_LIBRARY(CORE_AUDIO CoreAudio) + FIND_LIBRARY(AVFOUNDATION AVFoundation) + FIND_LIBRARY(AUDIO_TOOL AudioToolbox) + + set(${MODULE_PREFIX}_SRCS + audin_ios.m +) + +set(${MODULE_PREFIX}_LIBS + winpr + freerdp + ${AVFOUNDATION} + ${CORE_AUDIO} + ${AUDIO_TOOL} +) + +include_directories(..) +include_directories(${MAC_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") diff --git a/channels/audin/client/ios/audin_ios.m b/channels/audin/client/ios/audin_ios.m new file mode 100644 index 0000000..ae30aee --- /dev/null +++ b/channels/audin/client/ios/audin_ios.m @@ -0,0 +1,335 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - iOS implementation + * + * Copyright (c) 2015 Armin Novak <armin.novak@thincast.com> + * 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 <freerdp/config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <winpr/crt.h> +#include <winpr/synch.h> +#include <winpr/string.h> +#include <winpr/thread.h> +#include <winpr/debug.h> +#include <winpr/cmdline.h> + +#import <AVFoundation/AVFoundation.h> + +#define __COREFOUNDATION_CFPLUGINCOM__ 1 +#define IUNKNOWN_C_GUTS \ + void *_reserved; \ + void *QueryInterface; \ + void *AddRef; \ + void *Release + +#include <CoreAudio/CoreAudioTypes.h> +#include <AudioToolbox/AudioToolbox.h> +#include <AudioToolbox/AudioQueue.h> + +#include <freerdp/addin.h> +#include <freerdp/channels/rdpsnd.h> + +#include "audin_main.h" + +#define IOS_AUDIO_QUEUE_NUM_BUFFERS 100 + +typedef struct +{ + IAudinDevice iface; + + AUDIO_FORMAT format; + UINT32 FramesPerPacket; + int dev_unit; + + AudinReceive receive; + void *user_data; + + rdpContext *rdpcontext; + + bool isOpen; + AudioQueueRef audioQueue; + AudioStreamBasicDescription audioFormat; + AudioQueueBufferRef audioBuffers[IOS_AUDIO_QUEUE_NUM_BUFFERS]; +} AudinIosDevice; + +static AudioFormatID audin_ios_get_format(const AUDIO_FORMAT *format) +{ + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + return kAudioFormatLinearPCM; + + default: + return 0; + } +} + +static AudioFormatFlags audin_ios_get_flags_for_format(const AUDIO_FORMAT *format) +{ + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + return kAudioFormatFlagIsSignedInteger; + + default: + return 0; + } +} + +static BOOL audin_ios_format_supported(IAudinDevice *device, const AUDIO_FORMAT *format) +{ + AudinIosDevice *ios = (AudinIosDevice *)device; + AudioFormatID req_fmt = 0; + + if (device == NULL || format == NULL) + return FALSE; + + req_fmt = audin_ios_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_ios_set_format(IAudinDevice *device, const AUDIO_FORMAT *format, + UINT32 FramesPerPacket) +{ + AudinIosDevice *ios = (AudinIosDevice *)device; + + if (device == NULL || format == NULL) + return ERROR_INVALID_PARAMETER; + + ios->FramesPerPacket = FramesPerPacket; + ios->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); + ios->audioFormat.mBitsPerChannel = format->wBitsPerSample; + + if (format->wBitsPerSample == 0) + ios->audioFormat.mBitsPerChannel = 16; + + ios->audioFormat.mChannelsPerFrame = ios->format.nChannels; + ios->audioFormat.mFramesPerPacket = 1; + + ios->audioFormat.mBytesPerFrame = + ios->audioFormat.mChannelsPerFrame * (ios->audioFormat.mBitsPerChannel / 8); + ios->audioFormat.mBytesPerPacket = + ios->audioFormat.mBytesPerFrame * ios->audioFormat.mFramesPerPacket; + + ios->audioFormat.mFormatFlags = audin_ios_get_flags_for_format(format); + ios->audioFormat.mFormatID = audin_ios_get_format(format); + ios->audioFormat.mReserved = 0; + ios->audioFormat.mSampleRate = ios->format.nSamplesPerSec; + return CHANNEL_RC_OK; +} + +static void ios_audio_queue_input_cb(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, + const AudioTimeStamp *inStartTime, UInt32 inNumPackets, + const AudioStreamPacketDescription *inPacketDesc) +{ + AudinIosDevice *ios = (AudinIosDevice *)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 = ios->receive(&ios->format, buffer, buffer_size, ios->user_data); + + AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL); + + if (error) + { + WLog_ERR(TAG, "ios->receive failed with error %" PRIu32 "", error); + SetLastError(ERROR_INTERNAL_ERROR); + } +} + +static UINT audin_ios_close(IAudinDevice *device) +{ + UINT errCode = CHANNEL_RC_OK; + char errString[1024]; + OSStatus devStat; + AudinIosDevice *ios = (AudinIosDevice *)device; + + if (device == NULL) + return ERROR_INVALID_PARAMETER; + + if (ios->isOpen) + { + devStat = AudioQueueStop(ios->audioQueue, true); + + if (devStat != 0) + { + errCode = GetLastError(); + WLog_ERR(TAG, "AudioQueueStop failed with %s [%" PRIu32 "]", + winpr_strerror(errCode, errString, sizeof(errString)), errCode); + } + + ios->isOpen = false; + } + + if (ios->audioQueue) + { + devStat = AudioQueueDispose(ios->audioQueue, true); + + if (devStat != 0) + { + errCode = GetLastError(); + WLog_ERR(TAG, "AudioQueueDispose failed with %s [%" PRIu32 "]", + winpr_strerror(errCode, errString, sizeof(errString)), errCode); + } + + ios->audioQueue = NULL; + } + + ios->receive = NULL; + ios->user_data = NULL; + return errCode; +} + +static UINT audin_ios_open(IAudinDevice *device, AudinReceive receive, void *user_data) +{ + AudinIosDevice *ios = (AudinIosDevice *)device; + DWORD errCode; + char errString[1024]; + OSStatus devStat; + + ios->receive = receive; + ios->user_data = user_data; + devStat = AudioQueueNewInput(&(ios->audioFormat), ios_audio_queue_input_cb, ios, NULL, + kCFRunLoopCommonModes, 0, &(ios->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 (size_t index = 0; index < IOS_AUDIO_QUEUE_NUM_BUFFERS; index++) + { + devStat = AudioQueueAllocateBuffer(ios->audioQueue, + ios->FramesPerPacket * 2 * ios->format.nChannels, + &ios->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(ios->audioQueue, ios->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(ios->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; + } + + ios->isOpen = true; + return CHANNEL_RC_OK; +err_out: + audin_ios_close(device); + return CHANNEL_RC_INITIALIZATION_ERROR; +} + +static UINT audin_ios_free(IAudinDevice *device) +{ + AudinIosDevice *ios = (AudinIosDevice *)device; + int error; + + if (device == NULL) + return ERROR_INVALID_PARAMETER; + + if ((error = audin_ios_close(device))) + { + WLog_ERR(TAG, "audin_oss_close failed with error code %d!", error); + } + + free(ios); + return CHANNEL_RC_OK; +} + +FREERDP_ENTRY_POINT( + UINT ios_freerdp_audin_client_subsystem_entry(PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + DWORD errCode; + char errString[1024]; + const ADDIN_ARGV *args; + AudinIosDevice *ios; + UINT error; + ios = (AudinIosDevice *)calloc(1, sizeof(AudinIosDevice)); + + if (!ios) + { + errCode = GetLastError(); + WLog_ERR(TAG, "calloc failed with %s [%" PRIu32 "]", + winpr_strerror(errCode, errString, sizeof(errString)), errCode); + return CHANNEL_RC_NO_MEMORY; + } + + ios->iface.Open = audin_ios_open; + ios->iface.FormatSupported = audin_ios_format_supported; + ios->iface.SetFormat = audin_ios_set_format; + ios->iface.Close = audin_ios_close; + ios->iface.Free = audin_ios_free; + ios->rdpcontext = pEntryPoints->rdpcontext; + ios->dev_unit = -1; + args = pEntryPoints->args; + + if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice *)ios))) + { + WLog_ERR(TAG, "RegisterAudinDevice failed with error %" PRIu32 "!", error); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + free(ios); + return error; +} diff --git a/channels/audin/client/mac/CMakeLists.txt b/channels/audin/client/mac/CMakeLists.txt new file mode 100644 index 0000000..6b3e792 --- /dev/null +++ b/channels/audin/client/mac/CMakeLists.txt @@ -0,0 +1,41 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright (c) 2015 Armin Novak <armin.novak@thincast.com> +# Copyright (c) 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. + +define_channel_client_subsystem("audin" "mac" "") +FIND_LIBRARY(CORE_AUDIO CoreAudio) +FIND_LIBRARY(AVFOUNDATION AVFoundation) +FIND_LIBRARY(AUDIO_TOOL AudioToolbox) +FIND_LIBRARY(APP_SERVICES ApplicationServices) + +set(${MODULE_PREFIX}_SRCS + audin_mac.m +) + +set(${MODULE_PREFIX}_LIBS + winpr + freerdp + ${AVFOUNDATION} + ${CORE_AUDIO} + ${AUDIO_TOOL} + ${APP_SERVICES} +) + +include_directories(..) +include_directories(${MAC_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") diff --git a/channels/audin/client/mac/audin_mac.m b/channels/audin/client/mac/audin_mac.m new file mode 100644 index 0000000..19749a9 --- /dev/null +++ b/channels/audin/client/mac/audin_mac.m @@ -0,0 +1,463 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - Mac OS X implementation + * + * Copyright (c) 2015 Armin Novak <armin.novak@thincast.com> + * 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 <freerdp/config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <winpr/crt.h> +#include <winpr/synch.h> +#include <winpr/string.h> +#include <winpr/thread.h> +#include <winpr/debug.h> +#include <winpr/cmdline.h> + +#import <AVFoundation/AVFoundation.h> + +#define __COREFOUNDATION_CFPLUGINCOM__ 1 +#define IUNKNOWN_C_GUTS \ + void *_reserved; \ + void *QueryInterface; \ + void *AddRef; \ + void *Release + +#include <CoreAudio/CoreAudioTypes.h> +#include <CoreAudio/CoreAudio.h> +#include <AudioToolbox/AudioToolbox.h> +#include <AudioToolbox/AudioQueue.h> + +#include <freerdp/addin.h> +#include <freerdp/channels/rdpsnd.h> + +#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; + + 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 (size_t 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, "<device>", + 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; +} + +FREERDP_ENTRY_POINT( + 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; +} diff --git a/channels/audin/client/opensles/CMakeLists.txt b/channels/audin/client/opensles/CMakeLists.txt new file mode 100644 index 0000000..9b2a287 --- /dev/null +++ b/channels/audin/client/opensles/CMakeLists.txt @@ -0,0 +1,36 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2013 Armin Novak <armin.novak@gmail.com> +# +# 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("audin" "opensles" "") + +find_package(OpenSLES REQUIRED) + +set(${MODULE_PREFIX}_SRCS + opensl_io.c + audin_opensl_es.c +) + +set(${MODULE_PREFIX}_LIBS + winpr + freerdp + ${OpenSLES_LIBRARIES} +) + +include_directories(..) +include_directories(${OpenSLES_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") diff --git a/channels/audin/client/opensles/audin_opensl_es.c b/channels/audin/client/opensles/audin_opensl_es.c new file mode 100644 index 0000000..a59fe05 --- /dev/null +++ b/channels/audin/client/opensles/audin_opensl_es.c @@ -0,0 +1,336 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - OpenSL ES implementation + * + * Copyright 2013 Armin Novak <armin.novak@gmail.com> + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * + * 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 <freerdp/config.h> + +#include <winpr/assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <winpr/crt.h> +#include <winpr/cmdline.h> + +#include <freerdp/freerdp.h> +#include <freerdp/addin.h> +#include <freerdp/channels/rdpsnd.h> + +#include <SLES/OpenSLES.h> +#include <freerdp/client/audin.h> + +#include "audin_main.h" +#include "opensl_io.h" + +typedef struct +{ + IAudinDevice iface; + + char* device_name; + OPENSL_STREAM* stream; + + AUDIO_FORMAT format; + UINT32 frames_per_packet; + + UINT32 bytes_per_channel; + + AudinReceive receive; + + void* user_data; + + rdpContext* rdpcontext; + wLog* log; +} AudinOpenSLESDevice; + +static UINT audin_opensles_close(IAudinDevice* device); + +static void audin_receive(void* context, const void* data, size_t size) +{ + UINT error; + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)context; + + if (!opensles || !data) + { + WLog_ERR(TAG, "Invalid arguments context=%p, data=%p", opensles, data); + return; + } + + error = opensles->receive(&opensles->format, data, size, opensles->user_data); + + if (error && opensles->rdpcontext) + setChannelError(opensles->rdpcontext, error, "audin_receive reported an error"); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_opensles_free(IAudinDevice* device) +{ + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device; + + if (!opensles) + return ERROR_INVALID_PARAMETER; + + WLog_Print(opensles->log, WLOG_DEBUG, "device=%p", (void*)device); + + free(opensles->device_name); + free(opensles); + return CHANNEL_RC_OK; +} + +static BOOL audin_opensles_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format) +{ + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device; + + if (!opensles || !format) + return FALSE; + + WLog_Print(opensles->log, WLOG_DEBUG, "device=%p, format=%p", (void*)opensles, (void*)format); + WINPR_ASSERT(format); + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: /* PCM */ + if (format->cbSize == 0 && (format->nSamplesPerSec <= 48000) && + (format->wBitsPerSample == 8 || format->wBitsPerSample == 16) && + (format->nChannels >= 1 && format->nChannels <= 2)) + { + return TRUE; + } + + break; + + default: + WLog_Print(opensles->log, WLOG_DEBUG, "Encoding '%s' [0x%04" PRIX16 "] not supported", + audio_format_get_tag_string(format->wFormatTag), format->wFormatTag); + break; + } + + return FALSE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_opensles_set_format(IAudinDevice* device, const AUDIO_FORMAT* format, + UINT32 FramesPerPacket) +{ + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device; + + if (!opensles || !format) + return ERROR_INVALID_PARAMETER; + + WLog_Print(opensles->log, WLOG_DEBUG, "device=%p, format=%p, FramesPerPacket=%" PRIu32 "", + (void*)device, (void*)format, FramesPerPacket); + WINPR_ASSERT(format); + + opensles->format = *format; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + opensles->frames_per_packet = FramesPerPacket; + + switch (format->wBitsPerSample) + { + case 4: + opensles->bytes_per_channel = 1; + break; + + case 8: + opensles->bytes_per_channel = 1; + break; + + case 16: + opensles->bytes_per_channel = 2; + break; + + default: + return ERROR_UNSUPPORTED_TYPE; + } + + break; + + default: + WLog_Print(opensles->log, WLOG_ERROR, + "Encoding '%" PRIu16 "' [%04" PRIX16 "] not supported", format->wFormatTag, + format->wFormatTag); + return ERROR_UNSUPPORTED_TYPE; + } + + WLog_Print(opensles->log, WLOG_DEBUG, "frames_per_packet=%" PRIu32, + opensles->frames_per_packet); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_opensles_open(IAudinDevice* device, AudinReceive receive, void* user_data) +{ + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device; + + if (!opensles || !receive || !user_data) + return ERROR_INVALID_PARAMETER; + + WLog_Print(opensles->log, WLOG_DEBUG, "device=%p, receive=%p, user_data=%p", (void*)device, + (void*)receive, (void*)user_data); + + if (opensles->stream) + goto error_out; + + if (!(opensles->stream = android_OpenRecDevice( + opensles, audin_receive, opensles->format.nSamplesPerSec, opensles->format.nChannels, + opensles->frames_per_packet, opensles->format.wBitsPerSample))) + { + WLog_Print(opensles->log, WLOG_ERROR, "android_OpenRecDevice failed!"); + goto error_out; + } + + opensles->receive = receive; + opensles->user_data = user_data; + return CHANNEL_RC_OK; +error_out: + audin_opensles_close(device); + return ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT audin_opensles_close(IAudinDevice* device) +{ + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device; + + if (!opensles) + return ERROR_INVALID_PARAMETER; + + WLog_Print(opensles->log, WLOG_DEBUG, "device=%p", (void*)device); + android_CloseRecDevice(opensles->stream); + opensles->receive = NULL; + opensles->user_data = NULL; + opensles->stream = NULL; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_opensles_parse_addin_args(AudinOpenSLESDevice* device, const ADDIN_ARGV* args) +{ + UINT status; + DWORD flags; + const COMMAND_LINE_ARGUMENT_A* arg; + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device; + COMMAND_LINE_ARGUMENT_A audin_opensles_args[] = { + { "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", NULL, NULL, -1, NULL, + "audio device name" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } + }; + + WLog_Print(opensles->log, WLOG_DEBUG, "device=%p, args=%p", (void*)device, (void*)args); + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, audin_opensles_args, flags, + opensles, NULL, NULL); + + if (status < 0) + return status; + + arg = audin_opensles_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + opensles->device_name = _strdup(arg->Value); + + if (!opensles->device_name) + { + WLog_Print(opensles->log, WLOG_ERROR, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT(UINT opensles_freerdp_audin_client_subsystem_entry( + PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + const ADDIN_ARGV* args; + AudinOpenSLESDevice* opensles; + UINT error; + opensles = (AudinOpenSLESDevice*)calloc(1, sizeof(AudinOpenSLESDevice)); + + if (!opensles) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + opensles->log = WLog_Get(TAG); + opensles->iface.Open = audin_opensles_open; + opensles->iface.FormatSupported = audin_opensles_format_supported; + opensles->iface.SetFormat = audin_opensles_set_format; + opensles->iface.Close = audin_opensles_close; + opensles->iface.Free = audin_opensles_free; + opensles->rdpcontext = pEntryPoints->rdpcontext; + args = pEntryPoints->args; + + if ((error = audin_opensles_parse_addin_args(opensles, args))) + { + WLog_Print(opensles->log, WLOG_ERROR, + "audin_opensles_parse_addin_args failed with errorcode %" PRIu32 "!", error); + goto error_out; + } + + if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice*)opensles))) + { + WLog_Print(opensles->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!", + error); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + free(opensles); + return error; +} diff --git a/channels/audin/client/opensles/opensl_io.c b/channels/audin/client/opensles/opensl_io.c new file mode 100644 index 0000000..39b3dc8 --- /dev/null +++ b/channels/audin/client/opensles/opensl_io.c @@ -0,0 +1,388 @@ +/* +opensl_io.c: +Android OpenSL input/output module +Copyright (c) 2012, Victor Lazzarini +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the <organization> nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include <winpr/assert.h> + +#include "audin_main.h" +#include "opensl_io.h" +#define CONV16BIT 32768 +#define CONVMYFLT (1. / 32768.) + +typedef struct +{ + size_t size; + void* data; +} queue_element; + +struct opensl_stream +{ + // engine interfaces + SLObjectItf engineObject; + SLEngineItf engineEngine; + + // device interfaces + SLDeviceVolumeItf deviceVolume; + + // recorder interfaces + SLObjectItf recorderObject; + SLRecordItf recorderRecord; + SLAndroidSimpleBufferQueueItf recorderBufferQueue; + + unsigned int inchannels; + unsigned int sr; + unsigned int buffersize; + unsigned int bits_per_sample; + + queue_element* prep; + queue_element* next; + + void* context; + opensl_receive_t receive; +}; + +static void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void* context); + +// creates the OpenSL ES audio engine +static SLresult openSLCreateEngine(OPENSL_STREAM* p) +{ + SLresult result; + // create engine + result = slCreateEngine(&(p->engineObject), 0, NULL, 0, NULL, NULL); + + if (result != SL_RESULT_SUCCESS) + goto engine_end; + + // realize the engine + result = (*p->engineObject)->Realize(p->engineObject, SL_BOOLEAN_FALSE); + + if (result != SL_RESULT_SUCCESS) + goto engine_end; + + // get the engine interface, which is needed in order to create other objects + result = (*p->engineObject)->GetInterface(p->engineObject, SL_IID_ENGINE, &(p->engineEngine)); + + if (result != SL_RESULT_SUCCESS) + goto engine_end; + + // get the volume interface - important, this is optional! + result = + (*p->engineObject)->GetInterface(p->engineObject, SL_IID_DEVICEVOLUME, &(p->deviceVolume)); + + if (result != SL_RESULT_SUCCESS) + { + p->deviceVolume = NULL; + result = SL_RESULT_SUCCESS; + } + +engine_end: + WINPR_ASSERT(SL_RESULT_SUCCESS == result); + return result; +} + +// Open the OpenSL ES device for input +static SLresult openSLRecOpen(OPENSL_STREAM* p) +{ + SLresult result; + SLuint32 sr = p->sr; + SLuint32 channels = p->inchannels; + WINPR_ASSERT(!p->recorderObject); + + if (channels) + { + switch (sr) + { + case 8000: + sr = SL_SAMPLINGRATE_8; + break; + + case 11025: + sr = SL_SAMPLINGRATE_11_025; + break; + + case 16000: + sr = SL_SAMPLINGRATE_16; + break; + + case 22050: + sr = SL_SAMPLINGRATE_22_05; + break; + + case 24000: + sr = SL_SAMPLINGRATE_24; + break; + + case 32000: + sr = SL_SAMPLINGRATE_32; + break; + + case 44100: + sr = SL_SAMPLINGRATE_44_1; + break; + + case 48000: + sr = SL_SAMPLINGRATE_48; + break; + + case 64000: + sr = SL_SAMPLINGRATE_64; + break; + + case 88200: + sr = SL_SAMPLINGRATE_88_2; + break; + + case 96000: + sr = SL_SAMPLINGRATE_96; + break; + + case 192000: + sr = SL_SAMPLINGRATE_192; + break; + + default: + return -1; + } + + // configure audio source + SLDataLocator_IODevice loc_dev = { SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, + SL_DEFAULTDEVICEID_AUDIOINPUT, NULL }; + SLDataSource audioSrc = { &loc_dev, NULL }; + // configure audio sink + int speakers; + + if (channels > 1) + speakers = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT; + else + speakers = SL_SPEAKER_FRONT_CENTER; + + SLDataLocator_AndroidSimpleBufferQueue loc_bq = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, + 2 }; + SLDataFormat_PCM format_pcm; + format_pcm.formatType = SL_DATAFORMAT_PCM; + format_pcm.numChannels = channels; + format_pcm.samplesPerSec = sr; + format_pcm.channelMask = speakers; + format_pcm.endianness = SL_BYTEORDER_LITTLEENDIAN; + + if (16 == p->bits_per_sample) + { + format_pcm.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16; + format_pcm.containerSize = 16; + } + else if (8 == p->bits_per_sample) + { + format_pcm.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_8; + format_pcm.containerSize = 8; + } + else + WINPR_ASSERT(0); + + SLDataSink audioSnk = { &loc_bq, &format_pcm }; + // create audio recorder + // (requires the RECORD_AUDIO permission) + const SLInterfaceID id[] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE }; + const SLboolean req[] = { SL_BOOLEAN_TRUE }; + result = (*p->engineEngine) + ->CreateAudioRecorder(p->engineEngine, &(p->recorderObject), &audioSrc, + &audioSnk, 1, id, req); + WINPR_ASSERT(!result); + + if (SL_RESULT_SUCCESS != result) + goto end_recopen; + + // realize the audio recorder + result = (*p->recorderObject)->Realize(p->recorderObject, SL_BOOLEAN_FALSE); + WINPR_ASSERT(!result); + + if (SL_RESULT_SUCCESS != result) + goto end_recopen; + + // get the record interface + result = (*p->recorderObject) + ->GetInterface(p->recorderObject, SL_IID_RECORD, &(p->recorderRecord)); + WINPR_ASSERT(!result); + + if (SL_RESULT_SUCCESS != result) + goto end_recopen; + + // get the buffer queue interface + result = (*p->recorderObject) + ->GetInterface(p->recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, + &(p->recorderBufferQueue)); + WINPR_ASSERT(!result); + + if (SL_RESULT_SUCCESS != result) + goto end_recopen; + + // register callback on the buffer queue + result = (*p->recorderBufferQueue) + ->RegisterCallback(p->recorderBufferQueue, bqRecorderCallback, p); + WINPR_ASSERT(!result); + + if (SL_RESULT_SUCCESS != result) + goto end_recopen; + + end_recopen: + return result; + } + else + return SL_RESULT_SUCCESS; +} + +// close the OpenSL IO and destroy the audio engine +static void openSLDestroyEngine(OPENSL_STREAM* p) +{ + // destroy audio recorder object, and invalidate all associated interfaces + if (p->recorderObject != NULL) + { + (*p->recorderObject)->Destroy(p->recorderObject); + p->recorderObject = NULL; + p->recorderRecord = NULL; + p->recorderBufferQueue = NULL; + } + + // destroy engine object, and invalidate all associated interfaces + if (p->engineObject != NULL) + { + (*p->engineObject)->Destroy(p->engineObject); + p->engineObject = NULL; + p->engineEngine = NULL; + } +} + +static queue_element* opensles_queue_element_new(size_t size) +{ + queue_element* q = calloc(1, sizeof(queue_element)); + + if (!q) + goto fail; + + q->size = size; + q->data = malloc(size); + + if (!q->data) + goto fail; + + return q; +fail: + free(q); + return NULL; +} + +static void opensles_queue_element_free(void* obj) +{ + queue_element* e = (queue_element*)obj; + + if (e) + free(e->data); + + free(e); +} + +// open the android audio device for input +OPENSL_STREAM* android_OpenRecDevice(void* context, opensl_receive_t receive, int sr, + int inchannels, int bufferframes, int bits_per_sample) +{ + OPENSL_STREAM* p; + + if (!context || !receive) + return NULL; + + p = (OPENSL_STREAM*)calloc(1, sizeof(OPENSL_STREAM)); + + if (!p) + return NULL; + + p->context = context; + p->receive = receive; + p->inchannels = inchannels; + p->sr = sr; + p->buffersize = bufferframes; + p->bits_per_sample = bits_per_sample; + + if ((p->bits_per_sample != 8) && (p->bits_per_sample != 16)) + goto fail; + + if (openSLCreateEngine(p) != SL_RESULT_SUCCESS) + goto fail; + + if (openSLRecOpen(p) != SL_RESULT_SUCCESS) + goto fail; + + /* Create receive buffers, prepare them and start recording */ + p->prep = opensles_queue_element_new(p->buffersize * p->bits_per_sample / 8); + p->next = opensles_queue_element_new(p->buffersize * p->bits_per_sample / 8); + + if (!p->prep || !p->next) + goto fail; + + (*p->recorderBufferQueue)->Enqueue(p->recorderBufferQueue, p->next->data, p->next->size); + (*p->recorderBufferQueue)->Enqueue(p->recorderBufferQueue, p->prep->data, p->prep->size); + (*p->recorderRecord)->SetRecordState(p->recorderRecord, SL_RECORDSTATE_RECORDING); + return p; +fail: + android_CloseRecDevice(p); + return NULL; +} + +// close the android audio device +void android_CloseRecDevice(OPENSL_STREAM* p) +{ + if (p == NULL) + return; + + opensles_queue_element_free(p->next); + opensles_queue_element_free(p->prep); + openSLDestroyEngine(p); + free(p); +} + +// this callback handler is called every time a buffer finishes recording +static void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void* context) +{ + OPENSL_STREAM* p = (OPENSL_STREAM*)context; + queue_element* e; + + if (!p) + return; + + e = p->next; + + if (!e) + return; + + if (!p->context || !p->receive) + WLog_WARN(TAG, "Missing receive callback=%p, context=%p", p->receive, p->context); + else + p->receive(p->context, e->data, e->size); + + p->next = p->prep; + p->prep = e; + (*p->recorderBufferQueue)->Enqueue(p->recorderBufferQueue, e->data, e->size); +} diff --git a/channels/audin/client/opensles/opensl_io.h b/channels/audin/client/opensles/opensl_io.h new file mode 100644 index 0000000..e99522c --- /dev/null +++ b/channels/audin/client/opensles/opensl_io.h @@ -0,0 +1,65 @@ +/* +opensl_io.c: +Android OpenSL input/output module header +Copyright (c) 2012, Victor Lazzarini +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the <organization> nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef FREERDP_CHANNEL_AUDIN_CLIENT_OPENSL_IO_H +#define FREERDP_CHANNEL_AUDIN_CLIENT_OPENSL_IO_H + +#include <SLES/OpenSLES.h> +#include <SLES/OpenSLES_Android.h> + +#include <freerdp/api.h> + +#include <stdlib.h> + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef struct opensl_stream OPENSL_STREAM; + + typedef void (*opensl_receive_t)(void* context, const void* data, size_t size); + + /* + Open the audio device with a given sampling rate (sr), input and output channels and IO buffer + size in frames. Returns a handle to the OpenSL stream + */ + FREERDP_LOCAL OPENSL_STREAM* android_OpenRecDevice(void* context, opensl_receive_t receive, + int sr, int inchannels, int bufferframes, + int bits_per_sample); + /* + Close the audio device + */ + FREERDP_LOCAL void android_CloseRecDevice(OPENSL_STREAM* p); + +#ifdef __cplusplus +}; +#endif + +#endif /* FREERDP_CHANNEL_AUDIN_CLIENT_OPENSL_IO_H */ diff --git a/channels/audin/client/oss/CMakeLists.txt b/channels/audin/client/oss/CMakeLists.txt new file mode 100644 index 0000000..6b747e4 --- /dev/null +++ b/channels/audin/client/oss/CMakeLists.txt @@ -0,0 +1,36 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright (c) 2015 Rozhuk Ivan <rozhuk.im@gmail.com> +# +# 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("audin" "oss" "") + +find_package(OSS REQUIRED) + +set(${MODULE_PREFIX}_SRCS + audin_oss.c +) + +set(${MODULE_PREFIX}_LIBS + winpr + freerdp + ${OSS_LIBRARIES} +) + +include_directories(..) +include_directories(${OSS_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + diff --git a/channels/audin/client/oss/audin_oss.c b/channels/audin/client/oss/audin_oss.c new file mode 100644 index 0000000..979a800 --- /dev/null +++ b/channels/audin/client/oss/audin_oss.c @@ -0,0 +1,491 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - OSS implementation + * + * Copyright (c) 2015 Rozhuk Ivan <rozhuk.im@gmail.com> + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * + * 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 <freerdp/config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <winpr/crt.h> +#include <winpr/synch.h> +#include <winpr/string.h> +#include <winpr/thread.h> +#include <winpr/cmdline.h> + +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <libgen.h> +#include <limits.h> +#include <unistd.h> +#if defined(__OpenBSD__) +#include <soundcard.h> +#else +#include <sys/soundcard.h> +#endif +#include <sys/ioctl.h> + +#include <freerdp/freerdp.h> +#include <freerdp/addin.h> +#include <freerdp/channels/rdpsnd.h> + +#include "audin_main.h" + +typedef struct +{ + IAudinDevice iface; + + HANDLE thread; + HANDLE stopEvent; + + AUDIO_FORMAT format; + UINT32 FramesPerPacket; + int dev_unit; + + AudinReceive receive; + void* user_data; + + rdpContext* rdpcontext; +} AudinOSSDevice; + +#define OSS_LOG_ERR(_text, _error) \ + do \ + { \ + if (_error != 0) \ + { \ + char buffer[256] = { 0 }; \ + WLog_ERR(TAG, "%s: %i - %s\n", _text, _error, \ + winpr_strerror(_error, buffer, sizeof(buffer))); \ + } \ + } while (0) + +static UINT32 audin_oss_get_format(const AUDIO_FORMAT* format) +{ + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + switch (format->wBitsPerSample) + { + case 8: + return AFMT_S8; + + case 16: + return AFMT_S16_LE; + } + + break; + + case WAVE_FORMAT_ALAW: + return AFMT_A_LAW; + + case WAVE_FORMAT_MULAW: + return AFMT_MU_LAW; + } + + return 0; +} + +static BOOL audin_oss_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format) +{ + if (device == NULL || format == NULL) + return FALSE; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + if (format->cbSize != 0 || format->nSamplesPerSec > 48000 || + (format->wBitsPerSample != 8 && format->wBitsPerSample != 16) || + (format->nChannels != 1 && format->nChannels != 2)) + return FALSE; + + break; + + default: + return FALSE; + } + + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_oss_set_format(IAudinDevice* device, const AUDIO_FORMAT* format, + UINT32 FramesPerPacket) +{ + AudinOSSDevice* oss = (AudinOSSDevice*)device; + + if (device == NULL || format == NULL) + return ERROR_INVALID_PARAMETER; + + oss->FramesPerPacket = FramesPerPacket; + oss->format = *format; + return CHANNEL_RC_OK; +} + +static DWORD WINAPI audin_oss_thread_func(LPVOID arg) +{ + char dev_name[PATH_MAX] = "/dev/dsp"; + char mixer_name[PATH_MAX] = "/dev/mixer"; + int pcm_handle = -1; + int mixer_handle = 0; + BYTE* buffer = NULL; + unsigned long tmp = 0; + size_t buffer_size = 0; + AudinOSSDevice* oss = (AudinOSSDevice*)arg; + UINT error = 0; + DWORD status = 0; + + if (oss == NULL) + { + error = ERROR_INVALID_PARAMETER; + goto err_out; + } + + if (oss->dev_unit != -1) + { + sprintf_s(dev_name, (PATH_MAX - 1), "/dev/dsp%i", oss->dev_unit); + sprintf_s(mixer_name, PATH_MAX - 1, "/dev/mixer%i", oss->dev_unit); + } + + WLog_INFO(TAG, "open: %s", dev_name); + + if ((pcm_handle = open(dev_name, O_RDONLY)) < 0) + { + OSS_LOG_ERR("sound dev open failed", errno); + error = ERROR_INTERNAL_ERROR; + goto err_out; + } + + /* Set rec volume to 100%. */ + if ((mixer_handle = open(mixer_name, O_RDWR)) < 0) + { + OSS_LOG_ERR("mixer open failed, not critical", errno); + } + else + { + tmp = (100 | (100 << 8)); + + if (ioctl(mixer_handle, MIXER_WRITE(SOUND_MIXER_MIC), &tmp) == -1) + OSS_LOG_ERR("WRITE_MIXER - SOUND_MIXER_MIC, not critical", errno); + + tmp = (100 | (100 << 8)); + + if (ioctl(mixer_handle, MIXER_WRITE(SOUND_MIXER_RECLEV), &tmp) == -1) + OSS_LOG_ERR("WRITE_MIXER - SOUND_MIXER_RECLEV, not critical", errno); + + close(mixer_handle); + } + +#if 0 /* FreeBSD OSS implementation at this moment (2015.03) does not set PCM_CAP_INPUT flag. */ + tmp = 0; + + if (ioctl(pcm_handle, SNDCTL_DSP_GETCAPS, &tmp) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_GETCAPS failed, try ignory", errno); + } + else if ((tmp & PCM_CAP_INPUT) == 0) + { + OSS_LOG_ERR("Device does not supports playback", EOPNOTSUPP); + goto err_out; + } + +#endif + /* Set format. */ + tmp = audin_oss_get_format(&oss->format); + + if (ioctl(pcm_handle, SNDCTL_DSP_SETFMT, &tmp) == -1) + OSS_LOG_ERR("SNDCTL_DSP_SETFMT failed", errno); + + tmp = oss->format.nChannels; + + if (ioctl(pcm_handle, SNDCTL_DSP_CHANNELS, &tmp) == -1) + OSS_LOG_ERR("SNDCTL_DSP_CHANNELS failed", errno); + + tmp = oss->format.nSamplesPerSec; + + if (ioctl(pcm_handle, SNDCTL_DSP_SPEED, &tmp) == -1) + OSS_LOG_ERR("SNDCTL_DSP_SPEED failed", errno); + + tmp = oss->format.nBlockAlign; + + if (ioctl(pcm_handle, SNDCTL_DSP_SETFRAGMENT, &tmp) == -1) + OSS_LOG_ERR("SNDCTL_DSP_SETFRAGMENT failed", errno); + + buffer_size = (oss->FramesPerPacket * oss->format.nChannels * (oss->format.wBitsPerSample / 8)); + buffer = (BYTE*)calloc((buffer_size + sizeof(void*)), sizeof(BYTE)); + + if (NULL == buffer) + { + OSS_LOG_ERR("malloc() fail", errno); + error = ERROR_NOT_ENOUGH_MEMORY; + goto err_out; + } + + while (1) + { + SSIZE_T stmp = -1; + status = WaitForSingleObject(oss->stopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + goto err_out; + } + + if (status == WAIT_OBJECT_0) + break; + + stmp = read(pcm_handle, buffer, buffer_size); + + /* Error happen. */ + if (stmp < 0) + { + OSS_LOG_ERR("read() error", errno); + continue; + } + + if ((size_t)stmp < buffer_size) /* Not enouth data. */ + continue; + + if ((error = oss->receive(&oss->format, buffer, buffer_size, oss->user_data))) + { + WLog_ERR(TAG, "oss->receive failed with error %" PRIu32 "", error); + break; + } + } + +err_out: + + if (error && oss && oss->rdpcontext) + setChannelError(oss->rdpcontext, error, "audin_oss_thread_func reported an error"); + + if (pcm_handle != -1) + { + WLog_INFO(TAG, "close: %s", dev_name); + close(pcm_handle); + } + + free(buffer); + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_oss_open(IAudinDevice* device, AudinReceive receive, void* user_data) +{ + AudinOSSDevice* oss = (AudinOSSDevice*)device; + oss->receive = receive; + oss->user_data = user_data; + + if (!(oss->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (!(oss->thread = CreateThread(NULL, 0, audin_oss_thread_func, oss, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + CloseHandle(oss->stopEvent); + oss->stopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_oss_close(IAudinDevice* device) +{ + UINT error = 0; + AudinOSSDevice* oss = (AudinOSSDevice*)device; + + if (device == NULL) + return ERROR_INVALID_PARAMETER; + + if (oss->stopEvent != NULL) + { + SetEvent(oss->stopEvent); + + if (WaitForSingleObject(oss->thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + CloseHandle(oss->stopEvent); + oss->stopEvent = NULL; + CloseHandle(oss->thread); + oss->thread = NULL; + } + + oss->receive = NULL; + oss->user_data = NULL; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_oss_free(IAudinDevice* device) +{ + AudinOSSDevice* oss = (AudinOSSDevice*)device; + UINT error = 0; + + if (device == NULL) + return ERROR_INVALID_PARAMETER; + + if ((error = audin_oss_close(device))) + { + WLog_ERR(TAG, "audin_oss_close failed with error code %" PRIu32 "!", error); + } + + free(oss); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_oss_parse_addin_args(AudinOSSDevice* device, const ADDIN_ARGV* args) +{ + int status = 0; + char* str_num = NULL; + char* eptr = NULL; + DWORD flags = 0; + const COMMAND_LINE_ARGUMENT_A* arg = NULL; + AudinOSSDevice* oss = (AudinOSSDevice*)device; + COMMAND_LINE_ARGUMENT_A audin_oss_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", + NULL, NULL, -1, NULL, "audio device name" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = + CommandLineParseArgumentsA(args->argc, args->argv, audin_oss_args, flags, oss, NULL, NULL); + + if (status < 0) + return ERROR_INVALID_PARAMETER; + + arg = audin_oss_args; + errno = 0; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + str_num = _strdup(arg->Value); + + if (!str_num) + { + WLog_ERR(TAG, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + { + long val = strtol(str_num, &eptr, 10); + + if ((errno != 0) || (val < INT32_MIN) || (val > INT32_MAX)) + { + free(str_num); + return CHANNEL_RC_NULL_DATA; + } + + oss->dev_unit = (INT32)val; + } + + if (oss->dev_unit < 0 || *eptr != '\0') + oss->dev_unit = -1; + + free(str_num); + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT( + UINT oss_freerdp_audin_client_subsystem_entry(PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + const ADDIN_ARGV* args = NULL; + AudinOSSDevice* oss = NULL; + UINT error = 0; + oss = (AudinOSSDevice*)calloc(1, sizeof(AudinOSSDevice)); + + if (!oss) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + oss->iface.Open = audin_oss_open; + oss->iface.FormatSupported = audin_oss_format_supported; + oss->iface.SetFormat = audin_oss_set_format; + oss->iface.Close = audin_oss_close; + oss->iface.Free = audin_oss_free; + oss->rdpcontext = pEntryPoints->rdpcontext; + oss->dev_unit = -1; + args = pEntryPoints->args; + + if ((error = audin_oss_parse_addin_args(oss, args))) + { + WLog_ERR(TAG, "audin_oss_parse_addin_args failed with errorcode %" PRIu32 "!", error); + goto error_out; + } + + if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice*)oss))) + { + WLog_ERR(TAG, "RegisterAudinDevice failed with error %" PRIu32 "!", error); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + free(oss); + return error; +} diff --git a/channels/audin/client/pulse/CMakeLists.txt b/channels/audin/client/pulse/CMakeLists.txt new file mode 100644 index 0000000..5bf0fcd --- /dev/null +++ b/channels/audin/client/pulse/CMakeLists.txt @@ -0,0 +1,35 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# +# 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("audin" "pulse" "") + +find_package(PulseAudio REQUIRED) + +set(${MODULE_PREFIX}_SRCS + audin_pulse.c) + +set(${MODULE_PREFIX}_LIBS + winpr + freerdp + ${PULSEAUDIO_LIBRARY} + ${PULSEAUDIO_MAINLOOP_LIBRARY} +) + +include_directories(..) +include_directories(${PULSEAUDIO_INCLUDE_DIR}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") diff --git a/channels/audin/client/pulse/audin_pulse.c b/channels/audin/client/pulse/audin_pulse.c new file mode 100644 index 0000000..0e330ad --- /dev/null +++ b/channels/audin/client/pulse/audin_pulse.c @@ -0,0 +1,566 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - PulseAudio implementation + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * + * 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 <freerdp/config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <winpr/crt.h> +#include <winpr/cmdline.h> +#include <winpr/wlog.h> + +#include <pulse/pulseaudio.h> + +#include <freerdp/types.h> +#include <freerdp/addin.h> +#include <freerdp/freerdp.h> +#include <freerdp/codec/audio.h> +#include <freerdp/client/audin.h> + +#include "audin_main.h" + +typedef struct +{ + IAudinDevice iface; + + char* device_name; + UINT32 frames_per_packet; + pa_threaded_mainloop* mainloop; + pa_context* context; + pa_sample_spec sample_spec; + pa_stream* stream; + AUDIO_FORMAT format; + + size_t bytes_per_frame; + size_t buffer_frames; + + AudinReceive receive; + void* user_data; + + rdpContext* rdpcontext; + wLog* log; +} AudinPulseDevice; + +static const char* pulse_context_state_string(pa_context_state_t state) +{ + switch (state) + { + case PA_CONTEXT_UNCONNECTED: + return "PA_CONTEXT_UNCONNECTED"; + case PA_CONTEXT_CONNECTING: + return "PA_CONTEXT_CONNECTING"; + case PA_CONTEXT_AUTHORIZING: + return "PA_CONTEXT_AUTHORIZING"; + case PA_CONTEXT_SETTING_NAME: + return "PA_CONTEXT_SETTING_NAME"; + case PA_CONTEXT_READY: + return "PA_CONTEXT_READY"; + case PA_CONTEXT_FAILED: + return "PA_CONTEXT_FAILED"; + case PA_CONTEXT_TERMINATED: + return "PA_CONTEXT_TERMINATED"; + default: + return "UNKNOWN"; + } +} + +static const char* pulse_stream_state_string(pa_stream_state_t state) +{ + switch (state) + { + case PA_STREAM_UNCONNECTED: + return "PA_STREAM_UNCONNECTED"; + case PA_STREAM_CREATING: + return "PA_STREAM_CREATING"; + case PA_STREAM_READY: + return "PA_STREAM_READY"; + case PA_STREAM_FAILED: + return "PA_STREAM_FAILED"; + case PA_STREAM_TERMINATED: + return "PA_STREAM_TERMINATED"; + default: + return "UNKNOWN"; + } +} + +static void audin_pulse_context_state_callback(pa_context* context, void* userdata) +{ + AudinPulseDevice* pulse = (AudinPulseDevice*)userdata; + pa_context_state_t state = pa_context_get_state(context); + + WLog_Print(pulse->log, WLOG_DEBUG, "context state %s", pulse_context_state_string(state)); + switch (state) + { + case PA_CONTEXT_READY: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + default: + break; + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_pulse_connect(IAudinDevice* device) +{ + pa_context_state_t state = PA_CONTEXT_FAILED; + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + + if (!pulse->context) + return ERROR_INVALID_PARAMETER; + + if (pa_context_connect(pulse->context, NULL, 0, NULL)) + { + WLog_Print(pulse->log, WLOG_ERROR, "pa_context_connect failed (%d)", + pa_context_errno(pulse->context)); + return ERROR_INTERNAL_ERROR; + } + + pa_threaded_mainloop_lock(pulse->mainloop); + + if (pa_threaded_mainloop_start(pulse->mainloop) < 0) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + WLog_Print(pulse->log, WLOG_ERROR, "pa_threaded_mainloop_start failed (%d)", + pa_context_errno(pulse->context)); + return ERROR_INTERNAL_ERROR; + } + + for (;;) + { + state = pa_context_get_state(pulse->context); + + if (state == PA_CONTEXT_READY) + break; + + if (!PA_CONTEXT_IS_GOOD(state)) + { + WLog_Print(pulse->log, WLOG_ERROR, "bad context state (%s: %d)", + pulse_context_state_string(state), pa_context_errno(pulse->context)); + pa_context_disconnect(pulse->context); + return ERROR_INVALID_STATE; + } + + pa_threaded_mainloop_wait(pulse->mainloop); + } + + pa_threaded_mainloop_unlock(pulse->mainloop); + WLog_Print(pulse->log, WLOG_DEBUG, "connected"); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_pulse_free(IAudinDevice* device) +{ + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + + if (!pulse) + return ERROR_INVALID_PARAMETER; + + if (pulse->mainloop) + { + pa_threaded_mainloop_stop(pulse->mainloop); + } + + if (pulse->context) + { + pa_context_disconnect(pulse->context); + pa_context_unref(pulse->context); + pulse->context = NULL; + } + + if (pulse->mainloop) + { + pa_threaded_mainloop_free(pulse->mainloop); + pulse->mainloop = NULL; + } + + free(pulse); + return CHANNEL_RC_OK; +} + +static BOOL audin_pulse_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format) +{ + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + + if (!pulse || !format) + return FALSE; + + if (!pulse->context) + return 0; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + if (format->cbSize == 0 && (format->nSamplesPerSec <= PA_RATE_MAX) && + (format->wBitsPerSample == 8 || format->wBitsPerSample == 16) && + (format->nChannels >= 1 && format->nChannels <= PA_CHANNELS_MAX)) + { + return TRUE; + } + + break; + + default: + return FALSE; + } + + return FALSE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_pulse_set_format(IAudinDevice* device, const AUDIO_FORMAT* format, + UINT32 FramesPerPacket) +{ + pa_sample_spec sample_spec = { 0 }; + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + + if (!pulse || !format) + return ERROR_INVALID_PARAMETER; + + if (!pulse->context) + return ERROR_INVALID_PARAMETER; + + if (FramesPerPacket > 0) + pulse->frames_per_packet = FramesPerPacket; + + sample_spec.rate = format->nSamplesPerSec; + sample_spec.channels = format->nChannels; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: /* PCM */ + switch (format->wBitsPerSample) + { + case 8: + sample_spec.format = PA_SAMPLE_U8; + break; + + case 16: + sample_spec.format = PA_SAMPLE_S16LE; + break; + + default: + return ERROR_INTERNAL_ERROR; + } + + break; + + case WAVE_FORMAT_ALAW: /* A-LAW */ + sample_spec.format = PA_SAMPLE_ALAW; + break; + + case WAVE_FORMAT_MULAW: /* U-LAW */ + sample_spec.format = PA_SAMPLE_ULAW; + break; + + default: + return ERROR_INTERNAL_ERROR; + } + + pulse->sample_spec = sample_spec; + pulse->format = *format; + return CHANNEL_RC_OK; +} + +static void audin_pulse_stream_state_callback(pa_stream* stream, void* userdata) +{ + AudinPulseDevice* pulse = (AudinPulseDevice*)userdata; + pa_stream_state_t state = pa_stream_get_state(stream); + + WLog_Print(pulse->log, WLOG_DEBUG, "stream state %s", pulse_stream_state_string(state)); + switch (state) + { + case PA_STREAM_READY: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + case PA_STREAM_UNCONNECTED: + case PA_STREAM_CREATING: + default: + break; + } +} + +static void audin_pulse_stream_request_callback(pa_stream* stream, size_t length, void* userdata) +{ + const void* data = NULL; + AudinPulseDevice* pulse = (AudinPulseDevice*)userdata; + UINT error = CHANNEL_RC_OK; + pa_stream_peek(stream, &data, &length); + error = + IFCALLRESULT(CHANNEL_RC_OK, pulse->receive, &pulse->format, data, length, pulse->user_data); + pa_stream_drop(stream); + + if (error && pulse->rdpcontext) + setChannelError(pulse->rdpcontext, error, "audin_pulse_thread_func reported an error"); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_pulse_close(IAudinDevice* device) +{ + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + + if (!pulse) + return ERROR_INVALID_PARAMETER; + + if (pulse->stream) + { + pa_threaded_mainloop_lock(pulse->mainloop); + pa_stream_disconnect(pulse->stream); + pa_stream_unref(pulse->stream); + pulse->stream = NULL; + pa_threaded_mainloop_unlock(pulse->mainloop); + } + + pulse->receive = NULL; + pulse->user_data = NULL; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_pulse_open(IAudinDevice* device, AudinReceive receive, void* user_data) +{ + pa_stream_state_t state = PA_STREAM_FAILED; + pa_buffer_attr buffer_attr = { 0 }; + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + + if (!pulse || !receive || !user_data) + return ERROR_INVALID_PARAMETER; + + if (!pulse->context) + return ERROR_INVALID_PARAMETER; + + if (!pulse->sample_spec.rate || pulse->stream) + return ERROR_INVALID_PARAMETER; + + pulse->receive = receive; + pulse->user_data = user_data; + pa_threaded_mainloop_lock(pulse->mainloop); + pulse->stream = pa_stream_new(pulse->context, "freerdp_audin", &pulse->sample_spec, NULL); + + if (!pulse->stream) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + WLog_Print(pulse->log, WLOG_DEBUG, "pa_stream_new failed (%d)", + pa_context_errno(pulse->context)); + return pa_context_errno(pulse->context); + } + + pulse->bytes_per_frame = pa_frame_size(&pulse->sample_spec); + pa_stream_set_state_callback(pulse->stream, audin_pulse_stream_state_callback, pulse); + pa_stream_set_read_callback(pulse->stream, audin_pulse_stream_request_callback, pulse); + buffer_attr.maxlength = (UINT32)-1; + buffer_attr.tlength = (UINT32)-1; + buffer_attr.prebuf = (UINT32)-1; + buffer_attr.minreq = (UINT32)-1; + /* 500ms latency */ + buffer_attr.fragsize = pulse->bytes_per_frame * pulse->frames_per_packet; + + if (buffer_attr.fragsize % pulse->format.nBlockAlign) + buffer_attr.fragsize += + pulse->format.nBlockAlign - buffer_attr.fragsize % pulse->format.nBlockAlign; + + if (pa_stream_connect_record(pulse->stream, pulse->device_name, &buffer_attr, + PA_STREAM_ADJUST_LATENCY) < 0) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + WLog_Print(pulse->log, WLOG_ERROR, "pa_stream_connect_playback failed (%d)", + pa_context_errno(pulse->context)); + return pa_context_errno(pulse->context); + } + + for (;;) + { + state = pa_stream_get_state(pulse->stream); + + if (state == PA_STREAM_READY) + break; + + if (!PA_STREAM_IS_GOOD(state)) + { + audin_pulse_close(device); + WLog_Print(pulse->log, WLOG_ERROR, "bad stream state (%s: %d)", + pulse_stream_state_string(state), pa_context_errno(pulse->context)); + pa_threaded_mainloop_unlock(pulse->mainloop); + return pa_context_errno(pulse->context); + } + + pa_threaded_mainloop_wait(pulse->mainloop); + } + + pa_threaded_mainloop_unlock(pulse->mainloop); + pulse->buffer_frames = 0; + WLog_Print(pulse->log, WLOG_DEBUG, "connected"); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_pulse_parse_addin_args(AudinPulseDevice* device, const ADDIN_ARGV* args) +{ + int status = 0; + DWORD flags = 0; + const COMMAND_LINE_ARGUMENT_A* arg = NULL; + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + COMMAND_LINE_ARGUMENT_A audin_pulse_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", + NULL, NULL, -1, NULL, "audio device name" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, audin_pulse_args, flags, pulse, + NULL, NULL); + + if (status < 0) + return ERROR_INVALID_PARAMETER; + + arg = audin_pulse_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + pulse->device_name = _strdup(arg->Value); + + if (!pulse->device_name) + { + WLog_Print(pulse->log, WLOG_ERROR, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT(UINT pulse_freerdp_audin_client_subsystem_entry( + PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + const ADDIN_ARGV* args = NULL; + AudinPulseDevice* pulse = NULL; + UINT error = 0; + pulse = (AudinPulseDevice*)calloc(1, sizeof(AudinPulseDevice)); + + if (!pulse) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + pulse->log = WLog_Get(TAG); + pulse->iface.Open = audin_pulse_open; + pulse->iface.FormatSupported = audin_pulse_format_supported; + pulse->iface.SetFormat = audin_pulse_set_format; + pulse->iface.Close = audin_pulse_close; + pulse->iface.Free = audin_pulse_free; + pulse->rdpcontext = pEntryPoints->rdpcontext; + args = pEntryPoints->args; + + if ((error = audin_pulse_parse_addin_args(pulse, args))) + { + WLog_Print(pulse->log, WLOG_ERROR, + "audin_pulse_parse_addin_args failed with error %" PRIu32 "!", error); + goto error_out; + } + + pulse->mainloop = pa_threaded_mainloop_new(); + + if (!pulse->mainloop) + { + WLog_Print(pulse->log, WLOG_ERROR, "pa_threaded_mainloop_new failed"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + pulse->context = pa_context_new(pa_threaded_mainloop_get_api(pulse->mainloop), "freerdp"); + + if (!pulse->context) + { + WLog_Print(pulse->log, WLOG_ERROR, "pa_context_new failed"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + pa_context_set_state_callback(pulse->context, audin_pulse_context_state_callback, pulse); + + if ((error = audin_pulse_connect(&pulse->iface))) + { + WLog_Print(pulse->log, WLOG_ERROR, "audin_pulse_connect failed"); + goto error_out; + } + + if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, &pulse->iface))) + { + WLog_Print(pulse->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!", + error); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + audin_pulse_free(&pulse->iface); + return error; +} diff --git a/channels/audin/client/sndio/CMakeLists.txt b/channels/audin/client/sndio/CMakeLists.txt new file mode 100644 index 0000000..ef68228 --- /dev/null +++ b/channels/audin/client/sndio/CMakeLists.txt @@ -0,0 +1,36 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright (c) 2015 Rozhuk Ivan <rozhuk.im@gmail.com> +# Copyright (c) 2020 Ingo Feinerer <feinerer@logic.at> +# +# 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("audin" "sndio" "") + +find_package(SNDIO REQUIRED) + +set(${MODULE_PREFIX}_SRCS + audin_sndio.c) + +set(${MODULE_PREFIX}_LIBS + winpr + freerdp + ${SNDIO_LIBRARIES} +) + +include_directories(..) +include_directories(${SNDIO_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + diff --git a/channels/audin/client/sndio/audin_sndio.c b/channels/audin/client/sndio/audin_sndio.c new file mode 100644 index 0000000..92dac13 --- /dev/null +++ b/channels/audin/client/sndio/audin_sndio.c @@ -0,0 +1,352 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - sndio implementation + * + * Copyright (c) 2015 Rozhuk Ivan <rozhuk.im@gmail.com> + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * Copyright 2020 Ingo Feinerer <feinerer@logic.at> + * + * 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 <freerdp/config.h> + +#include <sndio.h> + +#include <winpr/cmdline.h> + +#include <freerdp/freerdp.h> +#include <freerdp/channels/rdpsnd.h> + +#include "audin_main.h" + +typedef struct +{ + IAudinDevice device; + + HANDLE thread; + HANDLE stopEvent; + + AUDIO_FORMAT format; + UINT32 FramesPerPacket; + + AudinReceive receive; + void* user_data; + + rdpContext* rdpcontext; +} AudinSndioDevice; + +static BOOL audin_sndio_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format) +{ + if (device == NULL || format == NULL) + return FALSE; + + return (format->wFormatTag == WAVE_FORMAT_PCM); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_sndio_set_format(IAudinDevice* device, AUDIO_FORMAT* format, + UINT32 FramesPerPacket) +{ + AudinSndioDevice* sndio = (AudinSndioDevice*)device; + + if (device == NULL || format == NULL) + return ERROR_INVALID_PARAMETER; + + if (format->wFormatTag != WAVE_FORMAT_PCM) + return ERROR_INTERNAL_ERROR; + + sndio->format = *format; + sndio->FramesPerPacket = FramesPerPacket; + + return CHANNEL_RC_OK; +} + +static void* audin_sndio_thread_func(void* arg) +{ + struct sio_hdl* hdl; + struct sio_par par; + BYTE* buffer = NULL; + size_t n, nbytes; + AudinSndioDevice* sndio = (AudinSndioDevice*)arg; + UINT error = 0; + DWORD status; + + if (arg == NULL) + { + error = ERROR_INVALID_PARAMETER; + goto err_out; + } + + hdl = sio_open(SIO_DEVANY, SIO_REC, 0); + if (hdl == NULL) + { + WLog_ERR(TAG, "could not open audio device"); + error = ERROR_INTERNAL_ERROR; + goto err_out; + } + + sio_initpar(&par); + par.bits = sndio->format.wBitsPerSample; + par.rchan = sndio->format.nChannels; + par.rate = sndio->format.nSamplesPerSec; + if (!sio_setpar(hdl, &par)) + { + WLog_ERR(TAG, "could not set audio parameters"); + error = ERROR_INTERNAL_ERROR; + goto err_out; + } + if (!sio_getpar(hdl, &par)) + { + WLog_ERR(TAG, "could not get audio parameters"); + error = ERROR_INTERNAL_ERROR; + goto err_out; + } + + if (!sio_start(hdl)) + { + WLog_ERR(TAG, "could not start audio device"); + error = ERROR_INTERNAL_ERROR; + goto err_out; + } + + nbytes = + (sndio->FramesPerPacket * sndio->format.nChannels * (sndio->format.wBitsPerSample / 8)); + buffer = (BYTE*)calloc((nbytes + sizeof(void*)), sizeof(BYTE)); + + if (buffer == NULL) + { + error = ERROR_NOT_ENOUGH_MEMORY; + goto err_out; + } + + while (1) + { + status = WaitForSingleObject(sndio->stopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + goto err_out; + } + + if (status == WAIT_OBJECT_0) + break; + + n = sio_read(hdl, buffer, nbytes); + + if (n == 0) + { + WLog_ERR(TAG, "could not read"); + continue; + } + + if (n < nbytes) + continue; + + if ((error = sndio->receive(&sndio->format, buffer, nbytes, sndio->user_data))) + { + WLog_ERR(TAG, "sndio->receive failed with error %" PRIu32 "", error); + break; + } + } + +err_out: + if (error && sndio->rdpcontext) + setChannelError(sndio->rdpcontext, error, "audin_sndio_thread_func reported an error"); + + if (hdl != NULL) + { + WLog_INFO(TAG, "sio_close"); + sio_stop(hdl); + sio_close(hdl); + } + + free(buffer); + ExitThread(0); + return NULL; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_sndio_open(IAudinDevice* device, AudinReceive receive, void* user_data) +{ + AudinSndioDevice* sndio = (AudinSndioDevice*)device; + sndio->receive = receive; + sndio->user_data = user_data; + + if (!(sndio->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed"); + return ERROR_INTERNAL_ERROR; + } + + if (!(sndio->thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)audin_sndio_thread_func, + sndio, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed"); + CloseHandle(sndio->stopEvent); + sndio->stopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_sndio_close(IAudinDevice* device) +{ + UINT error; + AudinSndioDevice* sndio = (AudinSndioDevice*)device; + + if (device == NULL) + return ERROR_INVALID_PARAMETER; + + if (sndio->stopEvent != NULL) + { + SetEvent(sndio->stopEvent); + + if (WaitForSingleObject(sndio->thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + CloseHandle(sndio->stopEvent); + sndio->stopEvent = NULL; + CloseHandle(sndio->thread); + sndio->thread = NULL; + } + + sndio->receive = NULL; + sndio->user_data = NULL; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_sndio_free(IAudinDevice* device) +{ + AudinSndioDevice* sndio = (AudinSndioDevice*)device; + int error; + + if (device == NULL) + return ERROR_INVALID_PARAMETER; + + if ((error = audin_sndio_close(device))) + { + WLog_ERR(TAG, "audin_sndio_close failed with error code %d", error); + } + + free(sndio); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_sndio_parse_addin_args(AudinSndioDevice* device, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + AudinSndioDevice* sndio = (AudinSndioDevice*)device; + COMMAND_LINE_ARGUMENT_A audin_sndio_args[] = { { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, (const char**)args->argv, audin_sndio_args, + flags, sndio, NULL, NULL); + + if (status < 0) + return ERROR_INVALID_PARAMETER; + + arg = audin_sndio_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT(UINT sndio_freerdp_audin_client_subsystem_entry( + PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + ADDIN_ARGV* args; + AudinSndioDevice* sndio; + UINT ret = CHANNEL_RC_OK; + sndio = (AudinSndioDevice*)calloc(1, sizeof(AudinSndioDevice)); + + if (sndio == NULL) + return CHANNEL_RC_NO_MEMORY; + + sndio->device.Open = audin_sndio_open; + sndio->device.FormatSupported = audin_sndio_format_supported; + sndio->device.SetFormat = audin_sndio_set_format; + sndio->device.Close = audin_sndio_close; + sndio->device.Free = audin_sndio_free; + sndio->rdpcontext = pEntryPoints->rdpcontext; + args = pEntryPoints->args; + + if (args->argc > 1) + { + ret = audin_sndio_parse_addin_args(sndio, args); + + if (ret != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "error parsing arguments"); + goto error; + } + } + + if ((ret = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice*)sndio))) + { + WLog_ERR(TAG, "RegisterAudinDevice failed with error %" PRIu32 "", ret); + goto error; + } + + return ret; +error: + audin_sndio_free(&sndio->device); + return ret; +} diff --git a/channels/audin/client/winmm/CMakeLists.txt b/channels/audin/client/winmm/CMakeLists.txt new file mode 100644 index 0000000..8657942 --- /dev/null +++ b/channels/audin/client/winmm/CMakeLists.txt @@ -0,0 +1,31 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# +# 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("audin" "winmm" "") + +set(${MODULE_PREFIX}_SRCS + audin_winmm.c) + +set(${MODULE_PREFIX}_LIBS + winpr + freerdp + winmm.lib +) + +include_directories(..) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") diff --git a/channels/audin/client/winmm/audin_winmm.c b/channels/audin/client/winmm/audin_winmm.c new file mode 100644 index 0000000..b4172a5 --- /dev/null +++ b/channels/audin/client/winmm/audin_winmm.c @@ -0,0 +1,566 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - WinMM implementation + * + * Copyright 2013 Zhang Zhaolong <zhangzl2013@126.com> + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * + * 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 <freerdp/config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <windows.h> +#include <mmsystem.h> + +#include <winpr/crt.h> +#include <winpr/cmdline.h> +#include <freerdp/freerdp.h> +#include <freerdp/addin.h> +#include <freerdp/client/audin.h> + +#include "audin_main.h" + +/* fix missing definitions in mingw */ +#ifndef WAVE_MAPPED_DEFAULT_COMMUNICATION_DEVICE +#define WAVE_MAPPED_DEFAULT_COMMUNICATION_DEVICE 0x0010 +#endif + +typedef struct +{ + IAudinDevice iface; + + char* device_name; + AudinReceive receive; + void* user_data; + HANDLE thread; + HANDLE stopEvent; + HWAVEIN hWaveIn; + PWAVEFORMATEX* ppwfx; + PWAVEFORMATEX pwfx_cur; + UINT32 ppwfx_size; + UINT32 cFormats; + UINT32 frames_per_packet; + rdpContext* rdpcontext; + wLog* log; +} AudinWinmmDevice; + +static void CALLBACK waveInProc(HWAVEIN hWaveIn, UINT uMsg, DWORD_PTR dwInstance, + DWORD_PTR dwParam1, DWORD_PTR dwParam2) +{ + AudinWinmmDevice* winmm = (AudinWinmmDevice*)dwInstance; + PWAVEHDR pWaveHdr; + UINT error = CHANNEL_RC_OK; + MMRESULT mmResult; + + switch (uMsg) + { + case WIM_CLOSE: + break; + + case WIM_DATA: + pWaveHdr = (WAVEHDR*)dwParam1; + + if (WHDR_DONE == (WHDR_DONE & pWaveHdr->dwFlags)) + { + if (pWaveHdr->dwBytesRecorded && + !(WaitForSingleObject(winmm->stopEvent, 0) == WAIT_OBJECT_0)) + { + AUDIO_FORMAT format; + format.cbSize = winmm->pwfx_cur->cbSize; + format.nBlockAlign = winmm->pwfx_cur->nBlockAlign; + format.nAvgBytesPerSec = winmm->pwfx_cur->nAvgBytesPerSec; + format.nChannels = winmm->pwfx_cur->nChannels; + format.nSamplesPerSec = winmm->pwfx_cur->nSamplesPerSec; + format.wBitsPerSample = winmm->pwfx_cur->wBitsPerSample; + format.wFormatTag = winmm->pwfx_cur->wFormatTag; + + if ((error = winmm->receive(&format, pWaveHdr->lpData, + pWaveHdr->dwBytesRecorded, winmm->user_data))) + break; + + mmResult = waveInAddBuffer(hWaveIn, pWaveHdr, sizeof(WAVEHDR)); + + if (mmResult != MMSYSERR_NOERROR) + error = ERROR_INTERNAL_ERROR; + } + } + + break; + + case WIM_OPEN: + break; + + default: + break; + } + + if (error && winmm->rdpcontext) + setChannelError(winmm->rdpcontext, error, "waveInProc reported an error"); +} + +static BOOL log_mmresult(AudinWinmmDevice* winmm, const char* what, MMRESULT result) +{ + if (result != MMSYSERR_NOERROR) + { + CHAR buffer[8192] = { 0 }; + CHAR msg[8192] = { 0 }; + CHAR cmsg[8192] = { 0 }; + waveInGetErrorTextA(result, buffer, sizeof(buffer)); + + _snprintf(msg, sizeof(msg) - 1, "%s failed. %" PRIu32 " [%s]", what, result, buffer); + _snprintf(cmsg, sizeof(cmsg) - 1, "audin_winmm_thread_func reported an error '%s'", msg); + WLog_Print(winmm->log, WLOG_DEBUG, "%s", msg); + if (winmm->rdpcontext) + setChannelError(winmm->rdpcontext, ERROR_INTERNAL_ERROR, cmsg); + return FALSE; + } + return TRUE; +} + +static BOOL test_format_supported(const PWAVEFORMATEX pwfx) +{ + MMRESULT rc; + WAVEINCAPSA caps = { 0 }; + + rc = waveInGetDevCapsA(WAVE_MAPPER, &caps, sizeof(caps)); + if (rc != MMSYSERR_NOERROR) + return FALSE; + + switch (pwfx->nChannels) + { + case 1: + if ((caps.dwFormats & + (WAVE_FORMAT_1M08 | WAVE_FORMAT_2M08 | WAVE_FORMAT_4M08 | WAVE_FORMAT_96M08 | + WAVE_FORMAT_1M16 | WAVE_FORMAT_2M16 | WAVE_FORMAT_4M16 | WAVE_FORMAT_96M16)) == 0) + return FALSE; + break; + case 2: + if ((caps.dwFormats & + (WAVE_FORMAT_1S08 | WAVE_FORMAT_2S08 | WAVE_FORMAT_4S08 | WAVE_FORMAT_96S08 | + WAVE_FORMAT_1S16 | WAVE_FORMAT_2S16 | WAVE_FORMAT_4S16 | WAVE_FORMAT_96S16)) == 0) + return FALSE; + break; + default: + return FALSE; + } + + rc = waveInOpen(NULL, WAVE_MAPPER, pwfx, 0, 0, + WAVE_FORMAT_QUERY | WAVE_MAPPED_DEFAULT_COMMUNICATION_DEVICE); + return (rc == MMSYSERR_NOERROR); +} + +static DWORD WINAPI audin_winmm_thread_func(LPVOID arg) +{ + AudinWinmmDevice* winmm = (AudinWinmmDevice*)arg; + char* buffer = NULL; + int size = 0; + WAVEHDR waveHdr[4] = { 0 }; + DWORD status = 0; + MMRESULT rc = 0; + + if (!winmm->hWaveIn) + { + MMRESULT rc; + rc = waveInOpen(&winmm->hWaveIn, WAVE_MAPPER, winmm->pwfx_cur, (DWORD_PTR)waveInProc, + (DWORD_PTR)winmm, + CALLBACK_FUNCTION | WAVE_MAPPED_DEFAULT_COMMUNICATION_DEVICE); + if (!log_mmresult(winmm, "waveInOpen", rc)) + return ERROR_INTERNAL_ERROR; + } + + size = + (winmm->pwfx_cur->wBitsPerSample * winmm->pwfx_cur->nChannels * winmm->frames_per_packet + + 7) / + 8; + + for (int i = 0; i < 4; i++) + { + buffer = (char*)malloc(size); + + if (!buffer) + return CHANNEL_RC_NO_MEMORY; + + waveHdr[i].dwBufferLength = size; + waveHdr[i].dwFlags = 0; + waveHdr[i].lpData = buffer; + rc = waveInPrepareHeader(winmm->hWaveIn, &waveHdr[i], sizeof(waveHdr[i])); + + if (!log_mmresult(winmm, "waveInPrepareHeader", rc)) + { + } + + rc = waveInAddBuffer(winmm->hWaveIn, &waveHdr[i], sizeof(waveHdr[i])); + + if (!log_mmresult(winmm, "waveInAddBuffer", rc)) + { + } + } + + rc = waveInStart(winmm->hWaveIn); + + if (!log_mmresult(winmm, "waveInStart", rc)) + { + } + + status = WaitForSingleObject(winmm->stopEvent, INFINITE); + + if (status == WAIT_FAILED) + { + WLog_Print(winmm->log, WLOG_DEBUG, "WaitForSingleObject failed."); + + if (winmm->rdpcontext) + setChannelError(winmm->rdpcontext, ERROR_INTERNAL_ERROR, + "audin_winmm_thread_func reported an error"); + } + + rc = waveInReset(winmm->hWaveIn); + + if (!log_mmresult(winmm, "waveInReset", rc)) + { + } + + for (int i = 0; i < 4; i++) + { + rc = waveInUnprepareHeader(winmm->hWaveIn, &waveHdr[i], sizeof(waveHdr[i])); + + if (!log_mmresult(winmm, "waveInUnprepareHeader", rc)) + { + } + + free(waveHdr[i].lpData); + } + + rc = waveInClose(winmm->hWaveIn); + + if (!log_mmresult(winmm, "waveInClose", rc)) + { + } + + winmm->hWaveIn = NULL; + return 0; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_winmm_free(IAudinDevice* device) +{ + AudinWinmmDevice* winmm = (AudinWinmmDevice*)device; + + if (!winmm) + return ERROR_INVALID_PARAMETER; + + for (UINT32 i = 0; i < winmm->cFormats; i++) + { + free(winmm->ppwfx[i]); + } + + free(winmm->ppwfx); + free(winmm->device_name); + free(winmm); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_winmm_close(IAudinDevice* device) +{ + DWORD status; + UINT error = CHANNEL_RC_OK; + AudinWinmmDevice* winmm = (AudinWinmmDevice*)device; + + if (!winmm) + return ERROR_INVALID_PARAMETER; + + SetEvent(winmm->stopEvent); + status = WaitForSingleObject(winmm->thread, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_Print(winmm->log, WLOG_ERROR, "WaitForSingleObject failed with error %" PRIu32 "!", + error); + return error; + } + + CloseHandle(winmm->thread); + CloseHandle(winmm->stopEvent); + winmm->thread = NULL; + winmm->stopEvent = NULL; + winmm->receive = NULL; + winmm->user_data = NULL; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_winmm_set_format(IAudinDevice* device, const AUDIO_FORMAT* format, + UINT32 FramesPerPacket) +{ + AudinWinmmDevice* winmm = (AudinWinmmDevice*)device; + + if (!winmm || !format) + return ERROR_INVALID_PARAMETER; + + winmm->frames_per_packet = FramesPerPacket; + + for (UINT32 i = 0; i < winmm->cFormats; i++) + { + const PWAVEFORMATEX ppwfx = winmm->ppwfx[i]; + if ((ppwfx->wFormatTag == format->wFormatTag) && (ppwfx->nChannels == format->nChannels) && + (ppwfx->wBitsPerSample == format->wBitsPerSample) && + (ppwfx->nSamplesPerSec == format->nSamplesPerSec)) + { + /* BUG: Many devices report to support stereo recording but fail here. + * Ensure we always use mono. */ + if (ppwfx->nChannels > 1) + { + ppwfx->nChannels = 1; + } + + if (ppwfx->nBlockAlign != 2) + { + ppwfx->nBlockAlign = 2; + ppwfx->nAvgBytesPerSec = ppwfx->nSamplesPerSec * ppwfx->nBlockAlign; + } + + if (!test_format_supported(ppwfx)) + return ERROR_INVALID_PARAMETER; + winmm->pwfx_cur = ppwfx; + return CHANNEL_RC_OK; + } + } + + return ERROR_INVALID_PARAMETER; +} + +static BOOL audin_winmm_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format) +{ + AudinWinmmDevice* winmm = (AudinWinmmDevice*)device; + PWAVEFORMATEX pwfx; + BYTE* data; + + if (!winmm || !format) + return FALSE; + + if (format->wFormatTag != WAVE_FORMAT_PCM) + return FALSE; + + if (format->nChannels != 1) + return FALSE; + + pwfx = (PWAVEFORMATEX)malloc(sizeof(WAVEFORMATEX) + format->cbSize); + + if (!pwfx) + return FALSE; + + pwfx->cbSize = format->cbSize; + pwfx->wFormatTag = format->wFormatTag; + pwfx->nChannels = format->nChannels; + pwfx->nSamplesPerSec = format->nSamplesPerSec; + pwfx->nBlockAlign = format->nBlockAlign; + pwfx->wBitsPerSample = format->wBitsPerSample; + data = (BYTE*)pwfx + sizeof(WAVEFORMATEX); + memcpy(data, format->data, format->cbSize); + + pwfx->nAvgBytesPerSec = pwfx->nSamplesPerSec * pwfx->nBlockAlign; + + if (!test_format_supported(pwfx)) + goto fail; + + if (winmm->cFormats >= winmm->ppwfx_size) + { + PWAVEFORMATEX* tmp_ppwfx; + tmp_ppwfx = realloc(winmm->ppwfx, sizeof(PWAVEFORMATEX) * winmm->ppwfx_size * 2); + + if (!tmp_ppwfx) + goto fail; + + winmm->ppwfx_size *= 2; + winmm->ppwfx = tmp_ppwfx; + } + + winmm->ppwfx[winmm->cFormats++] = pwfx; + return TRUE; + +fail: + free(pwfx); + return FALSE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_winmm_open(IAudinDevice* device, AudinReceive receive, void* user_data) +{ + AudinWinmmDevice* winmm = (AudinWinmmDevice*)device; + + if (!winmm || !receive || !user_data) + return ERROR_INVALID_PARAMETER; + + winmm->receive = receive; + winmm->user_data = user_data; + + if (!(winmm->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_Print(winmm->log, WLOG_ERROR, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (!(winmm->thread = CreateThread(NULL, 0, audin_winmm_thread_func, winmm, 0, NULL))) + { + WLog_Print(winmm->log, WLOG_ERROR, "CreateThread failed!"); + CloseHandle(winmm->stopEvent); + winmm->stopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_winmm_parse_addin_args(AudinWinmmDevice* device, const ADDIN_ARGV* args) +{ + int status; + DWORD flags; + const COMMAND_LINE_ARGUMENT_A* arg; + AudinWinmmDevice* winmm = (AudinWinmmDevice*)device; + COMMAND_LINE_ARGUMENT_A audin_winmm_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", + NULL, NULL, -1, NULL, "audio device name" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, audin_winmm_args, flags, winmm, + NULL, NULL); + arg = audin_winmm_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + winmm->device_name = _strdup(arg->Value); + + if (!winmm->device_name) + { + WLog_Print(winmm->log, WLOG_ERROR, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT(UINT winmm_freerdp_audin_client_subsystem_entry( + PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + const ADDIN_ARGV* args; + AudinWinmmDevice* winmm; + UINT error; + + if (waveInGetNumDevs() == 0) + { + WLog_Print(WLog_Get(TAG), WLOG_ERROR, "No microphone available!"); + return ERROR_DEVICE_NOT_AVAILABLE; + } + + winmm = (AudinWinmmDevice*)calloc(1, sizeof(AudinWinmmDevice)); + + if (!winmm) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + winmm->log = WLog_Get(TAG); + winmm->iface.Open = audin_winmm_open; + winmm->iface.FormatSupported = audin_winmm_format_supported; + winmm->iface.SetFormat = audin_winmm_set_format; + winmm->iface.Close = audin_winmm_close; + winmm->iface.Free = audin_winmm_free; + winmm->rdpcontext = pEntryPoints->rdpcontext; + args = pEntryPoints->args; + + if ((error = audin_winmm_parse_addin_args(winmm, args))) + { + WLog_Print(winmm->log, WLOG_ERROR, + "audin_winmm_parse_addin_args failed with error %" PRIu32 "!", error); + goto error_out; + } + + if (!winmm->device_name) + { + winmm->device_name = _strdup("default"); + + if (!winmm->device_name) + { + WLog_Print(winmm->log, WLOG_ERROR, "_strdup failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + } + + winmm->ppwfx_size = 10; + winmm->ppwfx = calloc(winmm->ppwfx_size, sizeof(PWAVEFORMATEX)); + + if (!winmm->ppwfx) + { + WLog_Print(winmm->log, WLOG_ERROR, "malloc failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, &winmm->iface))) + { + WLog_Print(winmm->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!", + error); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + free(winmm->ppwfx); + free(winmm->device_name); + free(winmm); + return error; +} diff --git a/channels/audin/server/CMakeLists.txt b/channels/audin/server/CMakeLists.txt new file mode 100644 index 0000000..454d2a6 --- /dev/null +++ b/channels/audin/server/CMakeLists.txt @@ -0,0 +1,27 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# +# 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_server("audin") + +set(${MODULE_PREFIX}_SRCS + audin.c +) + +set(${MODULE_PREFIX}_LIBS + freerdp +) +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "DVCPluginEntry") diff --git a/channels/audin/server/audin.c b/channels/audin/server/audin.c new file mode 100644 index 0000000..d67937a --- /dev/null +++ b/channels/audin/server/audin.c @@ -0,0 +1,914 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Server Audio Input Virtual Channel + * + * Copyright 2012 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * Copyright 2022 Pascal Nowack <Pascal.Nowack@gmx.de> + * + * 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 <freerdp/config.h> + +#include <winpr/crt.h> +#include <winpr/assert.h> +#include <winpr/synch.h> +#include <winpr/thread.h> +#include <winpr/stream.h> + +#include <freerdp/freerdp.h> +#include <freerdp/server/server-common.h> +#include <freerdp/server/audin.h> +#include <freerdp/channels/log.h> + +#define AUDIN_TAG CHANNELS_TAG("audin.server") + +#define SNDIN_HEADER_SIZE 1 + +typedef enum +{ + MSG_SNDIN_VERSION = 0x01, + MSG_SNDIN_FORMATS = 0x02, + MSG_SNDIN_OPEN = 0x03, + MSG_SNDIN_OPEN_REPLY = 0x04, + MSG_SNDIN_DATA_INCOMING = 0x05, + MSG_SNDIN_DATA = 0x06, + MSG_SNDIN_FORMATCHANGE = 0x07, +} MSG_SNDIN; + +typedef struct +{ + audin_server_context context; + + HANDLE stopEvent; + + HANDLE thread; + void* audin_channel; + + DWORD SessionId; + + AUDIO_FORMAT* audin_server_formats; + UINT32 audin_n_server_formats; + AUDIO_FORMAT* audin_negotiated_format; + UINT32 audin_client_format_idx; + wLog* log; +} audin_server; + +static UINT audin_server_recv_version(audin_server_context* context, wStream* s, + const SNDIN_PDU* header) +{ + audin_server* audin = (audin_server*)context; + SNDIN_VERSION pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + if (!Stream_CheckAndLogRequiredLengthWLog(audin->log, s, 4)) + return ERROR_NO_DATA; + + Stream_Read_UINT32(s, pdu.Version); + + IFCALLRET(context->ReceiveVersion, error, context, &pdu); + if (error) + WLog_Print(audin->log, WLOG_ERROR, "context->ReceiveVersion failed with error %" PRIu32 "", + error); + + return error; +} + +static UINT audin_server_recv_formats(audin_server_context* context, wStream* s, + const SNDIN_PDU* header) +{ + audin_server* audin = (audin_server*)context; + SNDIN_FORMATS pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + /* Implementations MUST, at a minimum, support WAVE_FORMAT_PCM (0x0001) */ + if (!Stream_CheckAndLogRequiredLengthWLog(audin->log, s, 4 + 4 + 18)) + return ERROR_NO_DATA; + + Stream_Read_UINT32(s, pdu.NumFormats); + Stream_Read_UINT32(s, pdu.cbSizeFormatsPacket); + + if (pdu.NumFormats == 0) + { + WLog_Print(audin->log, WLOG_ERROR, "Sound Formats PDU contains no formats"); + return ERROR_INVALID_DATA; + } + + pdu.SoundFormats = audio_formats_new(pdu.NumFormats); + if (!pdu.SoundFormats) + { + WLog_Print(audin->log, WLOG_ERROR, "Failed to allocate %u SoundFormats", pdu.NumFormats); + return ERROR_NOT_ENOUGH_MEMORY; + } + + for (UINT32 i = 0; i < pdu.NumFormats; ++i) + { + AUDIO_FORMAT* format = &pdu.SoundFormats[i]; + + if (!audio_format_read(s, format)) + { + WLog_Print(audin->log, WLOG_ERROR, "Failed to read audio format"); + audio_formats_free(pdu.SoundFormats, i + i); + return ERROR_INVALID_DATA; + } + + audio_format_print(audin->log, WLOG_DEBUG, format); + } + + if (pdu.cbSizeFormatsPacket != Stream_GetPosition(s)) + { + WLog_Print(audin->log, WLOG_WARN, + "cbSizeFormatsPacket is invalid! Expected: %u Got: %zu. Fixing size", + pdu.cbSizeFormatsPacket, Stream_GetPosition(s)); + const size_t pos = Stream_GetPosition(s); + if (pos > UINT32_MAX) + { + WLog_Print(audin->log, WLOG_ERROR, "Stream too long, %" PRIuz " exceeds UINT32_MAX", + pos); + error = ERROR_INVALID_PARAMETER; + goto fail; + } + pdu.cbSizeFormatsPacket = (UINT32)pos; + } + + pdu.ExtraDataSize = Stream_GetRemainingLength(s); + + IFCALLRET(context->ReceiveFormats, error, context, &pdu); + if (error) + WLog_Print(audin->log, WLOG_ERROR, "context->ReceiveFormats failed with error %" PRIu32 "", + error); + +fail: + audio_formats_free(pdu.SoundFormats, pdu.NumFormats); + + return error; +} + +static UINT audin_server_recv_open_reply(audin_server_context* context, wStream* s, + const SNDIN_PDU* header) +{ + audin_server* audin = (audin_server*)context; + SNDIN_OPEN_REPLY pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + if (!Stream_CheckAndLogRequiredLengthWLog(audin->log, s, 4)) + return ERROR_NO_DATA; + + Stream_Read_UINT32(s, pdu.Result); + + IFCALLRET(context->OpenReply, error, context, &pdu); + if (error) + WLog_Print(audin->log, WLOG_ERROR, "context->OpenReply failed with error %" PRIu32 "", + error); + + return error; +} + +static UINT audin_server_recv_data_incoming(audin_server_context* context, wStream* s, + const SNDIN_PDU* header) +{ + audin_server* audin = (audin_server*)context; + SNDIN_DATA_INCOMING pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + IFCALLRET(context->IncomingData, error, context, &pdu); + if (error) + WLog_Print(audin->log, WLOG_ERROR, "context->IncomingData failed with error %" PRIu32 "", + error); + + return error; +} + +static UINT audin_server_recv_data(audin_server_context* context, wStream* s, + const SNDIN_PDU* header) +{ + audin_server* audin = (audin_server*)context; + SNDIN_DATA pdu = { 0 }; + wStream dataBuffer = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + pdu.Data = Stream_StaticInit(&dataBuffer, Stream_Pointer(s), Stream_GetRemainingLength(s)); + + IFCALLRET(context->Data, error, context, &pdu); + if (error) + WLog_Print(audin->log, WLOG_ERROR, "context->Data failed with error %" PRIu32 "", error); + + return error; +} + +static UINT audin_server_recv_format_change(audin_server_context* context, wStream* s, + const SNDIN_PDU* header) +{ + audin_server* audin = (audin_server*)context; + SNDIN_FORMATCHANGE pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + if (!Stream_CheckAndLogRequiredLengthWLog(audin->log, s, 4)) + return ERROR_NO_DATA; + + Stream_Read_UINT32(s, pdu.NewFormat); + + IFCALLRET(context->ReceiveFormatChange, error, context, &pdu); + if (error) + WLog_Print(audin->log, WLOG_ERROR, + "context->ReceiveFormatChange failed with error %" PRIu32 "", error); + + return error; +} + +static DWORD WINAPI audin_server_thread_func(LPVOID arg) +{ + wStream* s = NULL; + void* buffer = NULL; + DWORD nCount = 0; + HANDLE events[8] = { 0 }; + BOOL ready = FALSE; + HANDLE ChannelEvent = NULL; + DWORD BytesReturned = 0; + audin_server* audin = (audin_server*)arg; + UINT error = CHANNEL_RC_OK; + DWORD status = ERROR_INTERNAL_ERROR; + + WINPR_ASSERT(audin); + + if (WTSVirtualChannelQuery(audin->audin_channel, WTSVirtualEventHandle, &buffer, + &BytesReturned) == TRUE) + { + if (BytesReturned == sizeof(HANDLE)) + CopyMemory(&ChannelEvent, buffer, sizeof(HANDLE)); + + WTSFreeMemory(buffer); + } + else + { + WLog_Print(audin->log, WLOG_ERROR, "WTSVirtualChannelQuery failed"); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + nCount = 0; + events[nCount++] = audin->stopEvent; + events[nCount++] = ChannelEvent; + + /* Wait for the client to confirm that the Audio Input dynamic channel is ready */ + + while (1) + { + if ((status = WaitForMultipleObjects(nCount, events, FALSE, 100)) == WAIT_OBJECT_0) + goto out; + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_Print(audin->log, WLOG_ERROR, + "WaitForMultipleObjects failed with error %" PRIu32 "", error); + goto out; + } + if (status == WAIT_OBJECT_0) + goto out; + + if (WTSVirtualChannelQuery(audin->audin_channel, WTSVirtualChannelReady, &buffer, + &BytesReturned) == FALSE) + { + WLog_Print(audin->log, WLOG_ERROR, "WTSVirtualChannelQuery failed"); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + ready = *((BOOL*)buffer); + WTSFreeMemory(buffer); + + if (ready) + break; + } + + s = Stream_New(NULL, 4096); + + if (!s) + { + WLog_Print(audin->log, WLOG_ERROR, "Stream_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + if (ready) + { + SNDIN_VERSION version = { 0 }; + + version.Version = audin->context.serverVersion; + + if ((error = audin->context.SendVersion(&audin->context, &version))) + { + WLog_Print(audin->log, WLOG_ERROR, "SendVersion failed with error %" PRIu32 "!", error); + goto out_capacity; + } + } + + while (ready) + { + SNDIN_PDU header = { 0 }; + + if ((status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE)) == WAIT_OBJECT_0) + break; + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_Print(audin->log, WLOG_ERROR, + "WaitForMultipleObjects failed with error %" PRIu32 "", error); + break; + } + if (status == WAIT_OBJECT_0) + break; + + Stream_SetPosition(s, 0); + + if (!WTSVirtualChannelRead(audin->audin_channel, 0, NULL, 0, &BytesReturned)) + { + WLog_Print(audin->log, WLOG_ERROR, "WTSVirtualChannelRead failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (BytesReturned < 1) + continue; + + if (!Stream_EnsureRemainingCapacity(s, BytesReturned)) + break; + + WINPR_ASSERT(Stream_Capacity(s) <= ULONG_MAX); + if (WTSVirtualChannelRead(audin->audin_channel, 0, (PCHAR)Stream_Buffer(s), + (ULONG)Stream_Capacity(s), &BytesReturned) == FALSE) + { + WLog_Print(audin->log, WLOG_ERROR, "WTSVirtualChannelRead failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + Stream_SetLength(s, BytesReturned); + if (!Stream_CheckAndLogRequiredLengthWLog(audin->log, s, SNDIN_HEADER_SIZE)) + { + error = ERROR_INTERNAL_ERROR; + break; + } + + Stream_Read_UINT8(s, header.MessageId); + + switch (header.MessageId) + { + case MSG_SNDIN_VERSION: + error = audin_server_recv_version(&audin->context, s, &header); + break; + case MSG_SNDIN_FORMATS: + error = audin_server_recv_formats(&audin->context, s, &header); + break; + case MSG_SNDIN_OPEN_REPLY: + error = audin_server_recv_open_reply(&audin->context, s, &header); + break; + case MSG_SNDIN_DATA_INCOMING: + error = audin_server_recv_data_incoming(&audin->context, s, &header); + break; + case MSG_SNDIN_DATA: + error = audin_server_recv_data(&audin->context, s, &header); + break; + case MSG_SNDIN_FORMATCHANGE: + error = audin_server_recv_format_change(&audin->context, s, &header); + break; + default: + WLog_Print(audin->log, WLOG_ERROR, + "audin_server_thread_func: unknown or invalid MessageId %" PRIu8 "", + header.MessageId); + error = ERROR_INVALID_DATA; + break; + } + if (error) + break; + } + +out_capacity: + Stream_Free(s, TRUE); +out: + WTSVirtualChannelClose(audin->audin_channel); + audin->audin_channel = NULL; + + if (error && audin->context.rdpcontext) + setChannelError(audin->context.rdpcontext, error, + "audin_server_thread_func reported an error"); + + ExitThread(error); + return error; +} + +static BOOL audin_server_open(audin_server_context* context) +{ + audin_server* audin = (audin_server*)context; + + WINPR_ASSERT(audin); + if (!audin->thread) + { + PULONG pSessionId = NULL; + DWORD BytesReturned = 0; + audin->SessionId = WTS_CURRENT_SESSION; + UINT32 channelId = 0; + BOOL status = TRUE; + + if (WTSQuerySessionInformationA(context->vcm, WTS_CURRENT_SESSION, WTSSessionId, + (LPSTR*)&pSessionId, &BytesReturned)) + { + audin->SessionId = (DWORD)*pSessionId; + WTSFreeMemory(pSessionId); + } + + audin->audin_channel = WTSVirtualChannelOpenEx(audin->SessionId, AUDIN_DVC_CHANNEL_NAME, + WTS_CHANNEL_OPTION_DYNAMIC); + + if (!audin->audin_channel) + { + WLog_Print(audin->log, WLOG_ERROR, "WTSVirtualChannelOpenEx failed!"); + return FALSE; + } + + channelId = WTSChannelGetIdByHandle(audin->audin_channel); + + IFCALLRET(context->ChannelIdAssigned, status, context, channelId); + if (!status) + { + WLog_Print(audin->log, WLOG_ERROR, "context->ChannelIdAssigned failed!"); + return FALSE; + } + + if (!(audin->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_Print(audin->log, WLOG_ERROR, "CreateEvent failed!"); + return FALSE; + } + + if (!(audin->thread = + CreateThread(NULL, 0, audin_server_thread_func, (void*)audin, 0, NULL))) + { + WLog_Print(audin->log, WLOG_ERROR, "CreateThread failed!"); + CloseHandle(audin->stopEvent); + audin->stopEvent = NULL; + return FALSE; + } + + return TRUE; + } + + WLog_Print(audin->log, WLOG_ERROR, "thread already running!"); + return FALSE; +} + +static BOOL audin_server_is_open(audin_server_context* context) +{ + audin_server* audin = (audin_server*)context; + + WINPR_ASSERT(audin); + return audin->thread != NULL; +} + +static BOOL audin_server_close(audin_server_context* context) +{ + audin_server* audin = (audin_server*)context; + WINPR_ASSERT(audin); + + if (audin->thread) + { + SetEvent(audin->stopEvent); + + if (WaitForSingleObject(audin->thread, INFINITE) == WAIT_FAILED) + { + WLog_Print(audin->log, WLOG_ERROR, "WaitForSingleObject failed with error %" PRIu32 "", + GetLastError()); + return FALSE; + } + + CloseHandle(audin->thread); + CloseHandle(audin->stopEvent); + audin->thread = NULL; + audin->stopEvent = NULL; + } + + if (audin->audin_channel) + { + WTSVirtualChannelClose(audin->audin_channel); + audin->audin_channel = NULL; + } + + audin->audin_negotiated_format = NULL; + + return TRUE; +} + +static wStream* audin_server_packet_new(wLog* log, size_t size, BYTE MessageId) +{ + WINPR_ASSERT(log); + + /* Allocate what we need plus header bytes */ + wStream* s = Stream_New(NULL, size + SNDIN_HEADER_SIZE); + if (!s) + { + WLog_Print(log, WLOG_ERROR, "Stream_New failed!"); + return NULL; + } + + Stream_Write_UINT8(s, MessageId); + + return s; +} + +static UINT audin_server_packet_send(audin_server_context* context, wStream* s) +{ + audin_server* audin = (audin_server*)context; + UINT error = CHANNEL_RC_OK; + ULONG written = 0; + + WINPR_ASSERT(context); + WINPR_ASSERT(s); + + const size_t pos = Stream_GetPosition(s); + if (pos > UINT32_MAX) + return ERROR_INVALID_PARAMETER; + + if (!WTSVirtualChannelWrite(audin->audin_channel, (PCHAR)Stream_Buffer(s), (UINT32)pos, + &written)) + { + WLog_Print(audin->log, WLOG_ERROR, "WTSVirtualChannelWrite failed!"); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + if (written < Stream_GetPosition(s)) + { + WLog_Print(audin->log, WLOG_WARN, "Unexpected bytes written: %" PRIu32 "/%" PRIuz "", + written, Stream_GetPosition(s)); + } + +out: + Stream_Free(s, TRUE); + return error; +} + +static UINT audin_server_send_version(audin_server_context* context, const SNDIN_VERSION* version) +{ + audin_server* audin = (audin_server*)context; + + WINPR_ASSERT(context); + WINPR_ASSERT(version); + + wStream* s = audin_server_packet_new(audin->log, 4, MSG_SNDIN_VERSION); + if (!s) + return ERROR_NOT_ENOUGH_MEMORY; + + Stream_Write_UINT32(s, version->Version); + + return audin_server_packet_send(context, s); +} + +static UINT audin_server_send_formats(audin_server_context* context, const SNDIN_FORMATS* formats) +{ + audin_server* audin = (audin_server*)context; + + WINPR_ASSERT(audin); + WINPR_ASSERT(formats); + + wStream* s = audin_server_packet_new(audin->log, 4 + 4 + 18, MSG_SNDIN_FORMATS); + if (!s) + return ERROR_NOT_ENOUGH_MEMORY; + + Stream_Write_UINT32(s, formats->NumFormats); + Stream_Write_UINT32(s, formats->cbSizeFormatsPacket); + + for (UINT32 i = 0; i < formats->NumFormats; ++i) + { + AUDIO_FORMAT* format = &formats->SoundFormats[i]; + + if (!audio_format_write(s, format)) + { + WLog_Print(audin->log, WLOG_ERROR, "Failed to write audio format"); + Stream_Free(s, TRUE); + return CHANNEL_RC_NO_MEMORY; + } + } + + return audin_server_packet_send(context, s); +} + +static UINT audin_server_send_open(audin_server_context* context, const SNDIN_OPEN* open) +{ + audin_server* audin = (audin_server*)context; + WINPR_ASSERT(audin); + WINPR_ASSERT(open); + + wStream* s = audin_server_packet_new(audin->log, 4 + 4 + 18 + 22, MSG_SNDIN_OPEN); + if (!s) + return ERROR_NOT_ENOUGH_MEMORY; + + Stream_Write_UINT32(s, open->FramesPerPacket); + Stream_Write_UINT32(s, open->initialFormat); + + Stream_Write_UINT16(s, open->captureFormat.wFormatTag); + Stream_Write_UINT16(s, open->captureFormat.nChannels); + Stream_Write_UINT32(s, open->captureFormat.nSamplesPerSec); + Stream_Write_UINT32(s, open->captureFormat.nAvgBytesPerSec); + Stream_Write_UINT16(s, open->captureFormat.nBlockAlign); + Stream_Write_UINT16(s, open->captureFormat.wBitsPerSample); + + if (open->ExtraFormatData) + { + Stream_Write_UINT16(s, 22); /* cbSize */ + + Stream_Write_UINT16(s, open->ExtraFormatData->Samples.wReserved); + Stream_Write_UINT32(s, open->ExtraFormatData->dwChannelMask); + + Stream_Write_UINT32(s, open->ExtraFormatData->SubFormat.Data1); + Stream_Write_UINT16(s, open->ExtraFormatData->SubFormat.Data2); + Stream_Write_UINT16(s, open->ExtraFormatData->SubFormat.Data3); + Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[0]); + Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[1]); + Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[2]); + Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[3]); + Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[4]); + Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[5]); + Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[6]); + Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[7]); + } + else + { + WINPR_ASSERT(open->captureFormat.wFormatTag != WAVE_FORMAT_EXTENSIBLE); + + Stream_Write_UINT16(s, 0); /* cbSize */ + } + + return audin_server_packet_send(context, s); +} + +static UINT audin_server_send_format_change(audin_server_context* context, + const SNDIN_FORMATCHANGE* format_change) +{ + audin_server* audin = (audin_server*)context; + + WINPR_ASSERT(context); + WINPR_ASSERT(format_change); + + wStream* s = audin_server_packet_new(audin->log, 4, MSG_SNDIN_FORMATCHANGE); + if (!s) + return ERROR_NOT_ENOUGH_MEMORY; + + Stream_Write_UINT32(s, format_change->NewFormat); + + return audin_server_packet_send(context, s); +} + +static UINT audin_server_receive_version_default(audin_server_context* audin_ctx, + const SNDIN_VERSION* version) +{ + audin_server* audin = (audin_server*)audin_ctx; + SNDIN_FORMATS formats = { 0 }; + + WINPR_ASSERT(audin); + WINPR_ASSERT(version); + + if (version->Version == 0) + { + WLog_Print(audin->log, WLOG_ERROR, "Received invalid AUDIO_INPUT version from client"); + return ERROR_INVALID_DATA; + } + + WLog_Print(audin->log, WLOG_DEBUG, "AUDIO_INPUT version of client: %u", version->Version); + + formats.NumFormats = audin->audin_n_server_formats; + formats.SoundFormats = audin->audin_server_formats; + + return audin->context.SendFormats(&audin->context, &formats); +} + +static UINT send_open(audin_server* audin) +{ + SNDIN_OPEN open = { 0 }; + + WINPR_ASSERT(audin); + + open.FramesPerPacket = 441; + open.initialFormat = audin->audin_client_format_idx; + open.captureFormat.wFormatTag = WAVE_FORMAT_PCM; + open.captureFormat.nChannels = 2; + open.captureFormat.nSamplesPerSec = 44100; + open.captureFormat.nAvgBytesPerSec = 44100 * 2 * 2; + open.captureFormat.nBlockAlign = 4; + open.captureFormat.wBitsPerSample = 16; + + WINPR_ASSERT(audin->context.SendOpen); + return audin->context.SendOpen(&audin->context, &open); +} + +static UINT audin_server_receive_formats_default(audin_server_context* context, + const SNDIN_FORMATS* formats) +{ + audin_server* audin = (audin_server*)context; + WINPR_ASSERT(audin); + WINPR_ASSERT(formats); + + if (audin->audin_negotiated_format) + { + WLog_Print(audin->log, WLOG_ERROR, + "Received client formats, but negotiation was already done"); + return ERROR_INVALID_DATA; + } + + for (UINT32 i = 0; i < audin->audin_n_server_formats; ++i) + { + for (UINT32 j = 0; j < formats->NumFormats; ++j) + { + if (audio_format_compatible(&audin->audin_server_formats[i], &formats->SoundFormats[j])) + { + audin->audin_negotiated_format = &audin->audin_server_formats[i]; + audin->audin_client_format_idx = i; + return send_open(audin); + } + } + } + + WLog_Print(audin->log, WLOG_ERROR, "Could not agree on a audio format with the server"); + + return ERROR_INVALID_DATA; +} + +static UINT audin_server_receive_format_change_default(audin_server_context* context, + const SNDIN_FORMATCHANGE* format_change) +{ + audin_server* audin = (audin_server*)context; + + WINPR_ASSERT(audin); + WINPR_ASSERT(format_change); + + if (format_change->NewFormat != audin->audin_client_format_idx) + { + WLog_Print(audin->log, WLOG_ERROR, + "NewFormat in FormatChange differs from requested format"); + return ERROR_INVALID_DATA; + } + + WLog_Print(audin->log, WLOG_DEBUG, "Received Format Change PDU: %u", format_change->NewFormat); + + return CHANNEL_RC_OK; +} + +static UINT audin_server_incoming_data_default(audin_server_context* context, + const SNDIN_DATA_INCOMING* data_incoming) +{ + audin_server* audin = (audin_server*)context; + WINPR_ASSERT(audin); + WINPR_ASSERT(data_incoming); + + /* TODO: Implement bandwidth measure of clients uplink */ + WLog_Print(audin->log, WLOG_DEBUG, "Received Incoming Data PDU"); + return CHANNEL_RC_OK; +} + +static UINT audin_server_open_reply_default(audin_server_context* context, + const SNDIN_OPEN_REPLY* open_reply) +{ + audin_server* audin = (audin_server*)context; + WINPR_ASSERT(audin); + WINPR_ASSERT(open_reply); + + /* TODO: Implement failure handling */ + WLog_Print(audin->log, WLOG_DEBUG, "Open Reply PDU: Result: %i", open_reply->Result); + return CHANNEL_RC_OK; +} + +audin_server_context* audin_server_context_new(HANDLE vcm) +{ + audin_server* audin = (audin_server*)calloc(1, sizeof(audin_server)); + + if (!audin) + { + WLog_ERR(AUDIN_TAG, "calloc failed!"); + return NULL; + } + audin->log = WLog_Get(AUDIN_TAG); + audin->context.vcm = vcm; + audin->context.Open = audin_server_open; + audin->context.IsOpen = audin_server_is_open; + audin->context.Close = audin_server_close; + + audin->context.SendVersion = audin_server_send_version; + audin->context.SendFormats = audin_server_send_formats; + audin->context.SendOpen = audin_server_send_open; + audin->context.SendFormatChange = audin_server_send_format_change; + + /* Default values */ + audin->context.serverVersion = SNDIN_VERSION_Version_2; + audin->context.ReceiveVersion = audin_server_receive_version_default; + audin->context.ReceiveFormats = audin_server_receive_formats_default; + audin->context.ReceiveFormatChange = audin_server_receive_format_change_default; + audin->context.IncomingData = audin_server_incoming_data_default; + audin->context.OpenReply = audin_server_open_reply_default; + + return &audin->context; +} + +void audin_server_context_free(audin_server_context* context) +{ + audin_server* audin = (audin_server*)context; + + if (!audin) + return; + + audin_server_close(context); + audio_formats_free(audin->audin_server_formats, audin->audin_n_server_formats); + audin->audin_server_formats = NULL; + free(audin); +} + +BOOL audin_server_set_formats(audin_server_context* context, SSIZE_T count, + const AUDIO_FORMAT* formats) +{ + audin_server* audin = (audin_server*)context; + WINPR_ASSERT(audin); + + audio_formats_free(audin->audin_server_formats, audin->audin_n_server_formats); + audin->audin_n_server_formats = 0; + audin->audin_server_formats = NULL; + audin->audin_negotiated_format = NULL; + + if (count < 0) + { + const size_t audin_n_server_formats = + server_audin_get_formats(&audin->audin_server_formats); + WINPR_ASSERT(audin_n_server_formats <= UINT32_MAX); + + audin->audin_n_server_formats = (UINT32)audin_n_server_formats; + } + else + { + AUDIO_FORMAT* audin_server_formats = audio_formats_new(count); + if (!audin_server_formats) + return count == 0; + + for (SSIZE_T x = 0; x < count; x++) + { + if (!audio_format_copy(&formats[x], &audin_server_formats[x])) + { + audio_formats_free(audin_server_formats, count); + return FALSE; + } + } + + WINPR_ASSERT(count <= UINT32_MAX); + audin->audin_server_formats = audin_server_formats; + audin->audin_n_server_formats = (UINT32)count; + } + return audin->audin_n_server_formats > 0; +} + +const AUDIO_FORMAT* audin_server_get_negotiated_format(const audin_server_context* context) +{ + const audin_server* audin = (const audin_server*)context; + WINPR_ASSERT(audin); + + return audin->audin_negotiated_format; +} |