diff options
Diffstat (limited to 'channels/rdpsnd/client')
25 files changed, 6776 insertions, 0 deletions
diff --git a/channels/rdpsnd/client/CMakeLists.txt b/channels/rdpsnd/client/CMakeLists.txt new file mode 100644 index 0000000..fc8cae2 --- /dev/null +++ b/channels/rdpsnd/client/CMakeLists.txt @@ -0,0 +1,66 @@ +# 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("rdpsnd") + +set(${MODULE_PREFIX}_SRCS + rdpsnd_main.c + rdpsnd_main.h +) + +set(${MODULE_PREFIX}_LIBS + winpr + freerdp + ${CMAKE_THREAD_LIBS_INIT} + rdpsnd-common +) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntryEx;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_IOSAUDIO) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "ios" "") +endif() + +if(WITH_PULSE) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "pulse" "") +endif() + +if(WITH_MACAUDIO) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "mac" "") +endif() + +if(WITH_WINMM) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "winmm" "") +endif() + +if(WITH_OPENSLES) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "opensles" "") +endif() + +if(WITH_SNDIO) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "sndio" "") +endif() + +add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "fake" "") diff --git a/channels/rdpsnd/client/alsa/CMakeLists.txt b/channels/rdpsnd/client/alsa/CMakeLists.txt new file mode 100644 index 0000000..3041b95 --- /dev/null +++ b/channels/rdpsnd/client/alsa/CMakeLists.txt @@ -0,0 +1,34 @@ +# 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("rdpsnd" "alsa" "") + +find_package(ALSA REQUIRED) + +set(${MODULE_PREFIX}_SRCS + rdpsnd_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/rdpsnd/client/alsa/rdpsnd_alsa.c b/channels/rdpsnd/client/alsa/rdpsnd_alsa.c new file mode 100644 index 0000000..97f0ba0 --- /dev/null +++ b/channels/rdpsnd/client/alsa/rdpsnd_alsa.c @@ -0,0 +1,574 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2009-2011 Jay Sorg + * 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/sysinfo.h> +#include <winpr/collections.h> + +#include <alsa/asoundlib.h> + +#include <freerdp/types.h> +#include <freerdp/codec/dsp.h> +#include <freerdp/channels/log.h> + +#include "rdpsnd_main.h" + +typedef struct +{ + rdpsndDevicePlugin device; + + UINT32 latency; + AUDIO_FORMAT aformat; + char* device_name; + snd_pcm_t* pcm_handle; + snd_mixer_t* mixer_handle; + + UINT32 actual_rate; + snd_pcm_format_t format; + UINT32 actual_channels; + + snd_pcm_uframes_t buffer_size; + snd_pcm_uframes_t period_size; +} rdpsndAlsaPlugin; + +#define SND_PCM_CHECK(_func, _status) \ + do \ + { \ + if (_status < 0) \ + { \ + WLog_ERR(TAG, "%s: %d\n", _func, _status); \ + return -1; \ + } \ + } while (0) + +static int rdpsnd_alsa_set_hw_params(rdpsndAlsaPlugin* alsa) +{ + int status = 0; + snd_pcm_hw_params_t* hw_params = NULL; + snd_pcm_uframes_t buffer_size_max = 0; + status = snd_pcm_hw_params_malloc(&hw_params); + SND_PCM_CHECK("snd_pcm_hw_params_malloc", status); + status = snd_pcm_hw_params_any(alsa->pcm_handle, hw_params); + SND_PCM_CHECK("snd_pcm_hw_params_any", status); + /* Set interleaved read/write access */ + status = + snd_pcm_hw_params_set_access(alsa->pcm_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); + SND_PCM_CHECK("snd_pcm_hw_params_set_access", status); + /* Set sample format */ + status = snd_pcm_hw_params_set_format(alsa->pcm_handle, hw_params, alsa->format); + SND_PCM_CHECK("snd_pcm_hw_params_set_format", status); + /* Set sample rate */ + status = snd_pcm_hw_params_set_rate_near(alsa->pcm_handle, hw_params, &alsa->actual_rate, NULL); + SND_PCM_CHECK("snd_pcm_hw_params_set_rate_near", status); + /* Set number of channels */ + status = snd_pcm_hw_params_set_channels(alsa->pcm_handle, hw_params, alsa->actual_channels); + SND_PCM_CHECK("snd_pcm_hw_params_set_channels", status); + /* Get maximum buffer size */ + status = snd_pcm_hw_params_get_buffer_size_max(hw_params, &buffer_size_max); + SND_PCM_CHECK("snd_pcm_hw_params_get_buffer_size_max", status); + /** + * ALSA Parameters + * + * http://www.alsa-project.org/main/index.php/FramesPeriods + * + * buffer_size = period_size * periods + * period_bytes = period_size * bytes_per_frame + * bytes_per_frame = channels * bytes_per_sample + * + * A frame is equivalent of one sample being played, + * irrespective of the number of channels or the number of bits + * + * A period is the number of frames in between each hardware interrupt. + * + * The buffer size always has to be greater than one period size. + * Commonly this is (2 * period_size), but some hardware can do 8 periods per buffer. + * It is also possible for the buffer size to not be an integer multiple of the period size. + */ + int interrupts_per_sec_near = 50; + int bytes_per_sec = + (alsa->actual_rate * alsa->aformat.wBitsPerSample / 8 * alsa->actual_channels); + alsa->buffer_size = buffer_size_max; + alsa->period_size = (bytes_per_sec / interrupts_per_sec_near); + + if (alsa->period_size > buffer_size_max) + { + WLog_ERR(TAG, "Warning: requested sound buffer size %lu, got %lu instead\n", + alsa->buffer_size, buffer_size_max); + alsa->period_size = (buffer_size_max / 8); + } + + /* Set buffer size */ + status = + snd_pcm_hw_params_set_buffer_size_near(alsa->pcm_handle, hw_params, &alsa->buffer_size); + SND_PCM_CHECK("snd_pcm_hw_params_set_buffer_size_near", status); + /* Set period size */ + status = snd_pcm_hw_params_set_period_size_near(alsa->pcm_handle, hw_params, &alsa->period_size, + NULL); + SND_PCM_CHECK("snd_pcm_hw_params_set_period_size_near", status); + status = snd_pcm_hw_params(alsa->pcm_handle, hw_params); + SND_PCM_CHECK("snd_pcm_hw_params", status); + snd_pcm_hw_params_free(hw_params); + return 0; +} + +static int rdpsnd_alsa_set_sw_params(rdpsndAlsaPlugin* alsa) +{ + int status = 0; + snd_pcm_sw_params_t* sw_params = NULL; + status = snd_pcm_sw_params_malloc(&sw_params); + SND_PCM_CHECK("snd_pcm_sw_params_malloc", status); + status = snd_pcm_sw_params_current(alsa->pcm_handle, sw_params); + SND_PCM_CHECK("snd_pcm_sw_params_current", status); + status = snd_pcm_sw_params_set_avail_min(alsa->pcm_handle, sw_params, + (alsa->aformat.nChannels * alsa->actual_channels)); + SND_PCM_CHECK("snd_pcm_sw_params_set_avail_min", status); + status = snd_pcm_sw_params_set_start_threshold(alsa->pcm_handle, sw_params, + alsa->aformat.nBlockAlign); + SND_PCM_CHECK("snd_pcm_sw_params_set_start_threshold", status); + status = snd_pcm_sw_params(alsa->pcm_handle, sw_params); + SND_PCM_CHECK("snd_pcm_sw_params", status); + snd_pcm_sw_params_free(sw_params); + status = snd_pcm_prepare(alsa->pcm_handle); + SND_PCM_CHECK("snd_pcm_prepare", status); + return 0; +} + +static int rdpsnd_alsa_validate_params(rdpsndAlsaPlugin* alsa) +{ + int status = 0; + snd_pcm_uframes_t buffer_size = 0; + snd_pcm_uframes_t period_size = 0; + status = snd_pcm_get_params(alsa->pcm_handle, &buffer_size, &period_size); + SND_PCM_CHECK("snd_pcm_get_params", status); + return 0; +} + +static int rdpsnd_alsa_set_params(rdpsndAlsaPlugin* alsa) +{ + snd_pcm_drop(alsa->pcm_handle); + + if (rdpsnd_alsa_set_hw_params(alsa) < 0) + return -1; + + if (rdpsnd_alsa_set_sw_params(alsa) < 0) + return -1; + + return rdpsnd_alsa_validate_params(alsa); +} + +static BOOL rdpsnd_alsa_set_format(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + + if (format) + { + alsa->aformat = *format; + alsa->actual_rate = format->nSamplesPerSec; + alsa->actual_channels = format->nChannels; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + switch (format->wBitsPerSample) + { + case 8: + alsa->format = SND_PCM_FORMAT_S8; + break; + + case 16: + alsa->format = SND_PCM_FORMAT_S16_LE; + break; + + default: + return FALSE; + } + + break; + + default: + return FALSE; + } + } + + alsa->latency = latency; + return (rdpsnd_alsa_set_params(alsa) == 0); +} + +static void rdpsnd_alsa_close_mixer(rdpsndAlsaPlugin* alsa) +{ + if (alsa && alsa->mixer_handle) + { + snd_mixer_close(alsa->mixer_handle); + alsa->mixer_handle = NULL; + } +} + +static BOOL rdpsnd_alsa_open_mixer(rdpsndAlsaPlugin* alsa) +{ + int status = 0; + + if (alsa->mixer_handle) + return TRUE; + + status = snd_mixer_open(&alsa->mixer_handle, 0); + + if (status < 0) + { + WLog_ERR(TAG, "snd_mixer_open failed"); + goto fail; + } + + status = snd_mixer_attach(alsa->mixer_handle, alsa->device_name); + + if (status < 0) + { + WLog_ERR(TAG, "snd_mixer_attach failed"); + goto fail; + } + + status = snd_mixer_selem_register(alsa->mixer_handle, NULL, NULL); + + if (status < 0) + { + WLog_ERR(TAG, "snd_mixer_selem_register failed"); + goto fail; + } + + status = snd_mixer_load(alsa->mixer_handle); + + if (status < 0) + { + WLog_ERR(TAG, "snd_mixer_load failed"); + goto fail; + } + + return TRUE; +fail: + rdpsnd_alsa_close_mixer(alsa); + return FALSE; +} + +static void rdpsnd_alsa_pcm_close(rdpsndAlsaPlugin* alsa) +{ + if (alsa && alsa->pcm_handle) + { + snd_pcm_drain(alsa->pcm_handle); + snd_pcm_close(alsa->pcm_handle); + alsa->pcm_handle = 0; + } +} + +static BOOL rdpsnd_alsa_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, UINT32 latency) +{ + int mode = 0; + int status = 0; + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + + if (alsa->pcm_handle) + return TRUE; + + mode = 0; + /*mode |= SND_PCM_NONBLOCK;*/ + status = snd_pcm_open(&alsa->pcm_handle, alsa->device_name, SND_PCM_STREAM_PLAYBACK, mode); + + if (status < 0) + { + WLog_ERR(TAG, "snd_pcm_open failed"); + return FALSE; + } + + return rdpsnd_alsa_set_format(device, format, latency) && rdpsnd_alsa_open_mixer(alsa); +} + +static void rdpsnd_alsa_close(rdpsndDevicePlugin* device) +{ + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + + if (!alsa) + return; + + rdpsnd_alsa_close_mixer(alsa); +} + +static void rdpsnd_alsa_free(rdpsndDevicePlugin* device) +{ + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + rdpsnd_alsa_pcm_close(alsa); + rdpsnd_alsa_close_mixer(alsa); + free(alsa->device_name); + free(alsa); +} + +static BOOL rdpsnd_alsa_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + 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; + } + + return FALSE; +} + +static UINT32 rdpsnd_alsa_get_volume(rdpsndDevicePlugin* device) +{ + long volume_min = 0; + long volume_max = 0; + long volume_left = 0; + long volume_right = 0; + UINT32 dwVolume = 0; + UINT16 dwVolumeLeft = 0; + UINT16 dwVolumeRight = 0; + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + dwVolumeLeft = ((50 * 0xFFFF) / 100); /* 50% */ + dwVolumeRight = ((50 * 0xFFFF) / 100); /* 50% */ + + if (!rdpsnd_alsa_open_mixer(alsa)) + return 0; + + for (snd_mixer_elem_t* elem = snd_mixer_first_elem(alsa->mixer_handle); elem; + elem = snd_mixer_elem_next(elem)) + { + if (snd_mixer_selem_has_playback_volume(elem)) + { + snd_mixer_selem_get_playback_volume_range(elem, &volume_min, &volume_max); + snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, &volume_left); + snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_RIGHT, &volume_right); + dwVolumeLeft = + (UINT16)(((volume_left * 0xFFFF) - volume_min) / (volume_max - volume_min)); + dwVolumeRight = + (UINT16)(((volume_right * 0xFFFF) - volume_min) / (volume_max - volume_min)); + break; + } + } + + dwVolume = (dwVolumeLeft << 16) | dwVolumeRight; + return dwVolume; +} + +static BOOL rdpsnd_alsa_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + long left = 0; + long right = 0; + long volume_min = 0; + long volume_max = 0; + long volume_left = 0; + long volume_right = 0; + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + + if (!rdpsnd_alsa_open_mixer(alsa)) + return FALSE; + + left = (value & 0xFFFF); + right = ((value >> 16) & 0xFFFF); + + for (snd_mixer_elem_t* elem = snd_mixer_first_elem(alsa->mixer_handle); elem; + elem = snd_mixer_elem_next(elem)) + { + if (snd_mixer_selem_has_playback_volume(elem)) + { + snd_mixer_selem_get_playback_volume_range(elem, &volume_min, &volume_max); + volume_left = volume_min + (left * (volume_max - volume_min)) / 0xFFFF; + volume_right = volume_min + (right * (volume_max - volume_min)) / 0xFFFF; + + if ((snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, volume_left) < + 0) || + (snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_RIGHT, + volume_right) < 0)) + { + WLog_ERR(TAG, "error setting the volume\n"); + return FALSE; + } + } + } + + return TRUE; +} + +static UINT rdpsnd_alsa_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + UINT latency = 0; + size_t offset = 0; + int frame_size = 0; + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + WINPR_ASSERT(alsa); + WINPR_ASSERT(data || (size == 0)); + frame_size = alsa->actual_channels * alsa->aformat.wBitsPerSample / 8; + if (frame_size <= 0) + return 0; + + while (offset < size) + { + snd_pcm_sframes_t status = + snd_pcm_writei(alsa->pcm_handle, &data[offset], (size - offset) / frame_size); + + if (status < 0) + status = snd_pcm_recover(alsa->pcm_handle, status, 0); + + if (status < 0) + { + WLog_ERR(TAG, "status: %d\n", status); + rdpsnd_alsa_close(device); + rdpsnd_alsa_open(device, NULL, alsa->latency); + break; + } + + offset += status * frame_size; + } + + { + snd_pcm_sframes_t available = 0; + snd_pcm_sframes_t delay = 0; + int rc = snd_pcm_avail_delay(alsa->pcm_handle, &available, &delay); + + if (rc != 0) + latency = 0; + else if (available == 0) /* Get [ms] from number of samples */ + latency = delay * 1000 / alsa->actual_rate; + else + latency = 0; + } + + return latency + alsa->latency; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_alsa_parse_addin_args(rdpsndDevicePlugin* device, const ADDIN_ARGV* args) +{ + int status = 0; + DWORD flags = 0; + const COMMAND_LINE_ARGUMENT_A* arg = NULL; + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + COMMAND_LINE_ARGUMENT_A rdpsnd_alsa_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", + NULL, NULL, -1, NULL, "device" }, + { 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, rdpsnd_alsa_args, flags, alsa, NULL, + NULL); + + if (status < 0) + { + WLog_ERR(TAG, "CommandLineParseArgumentsA failed!"); + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + arg = rdpsnd_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) + 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_rdpsnd_client_subsystem_entry( + PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + const ADDIN_ARGV* args = NULL; + rdpsndAlsaPlugin* alsa = NULL; + UINT error = 0; + alsa = (rdpsndAlsaPlugin*)calloc(1, sizeof(rdpsndAlsaPlugin)); + + if (!alsa) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + alsa->device.Open = rdpsnd_alsa_open; + alsa->device.FormatSupported = rdpsnd_alsa_format_supported; + alsa->device.GetVolume = rdpsnd_alsa_get_volume; + alsa->device.SetVolume = rdpsnd_alsa_set_volume; + alsa->device.Play = rdpsnd_alsa_play; + alsa->device.Close = rdpsnd_alsa_close; + alsa->device.Free = rdpsnd_alsa_free; + args = pEntryPoints->args; + + if (args->argc > 1) + { + if ((error = rdpsnd_alsa_parse_addin_args(&alsa->device, args))) + { + WLog_ERR(TAG, "rdpsnd_alsa_parse_addin_args failed with error %" PRIu32 "", error); + goto error_parse_args; + } + } + + if (!alsa->device_name) + { + alsa->device_name = _strdup("default"); + + if (!alsa->device_name) + { + WLog_ERR(TAG, "_strdup failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_strdup; + } + } + + alsa->pcm_handle = 0; + alsa->actual_rate = 22050; + alsa->format = SND_PCM_FORMAT_S16_LE; + alsa->actual_channels = 2; + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)alsa); + return CHANNEL_RC_OK; +error_strdup: + free(alsa->device_name); +error_parse_args: + free(alsa); + return error; +} diff --git a/channels/rdpsnd/client/fake/CMakeLists.txt b/channels/rdpsnd/client/fake/CMakeLists.txt new file mode 100644 index 0000000..a41b41f --- /dev/null +++ b/channels/rdpsnd/client/fake/CMakeLists.txt @@ -0,0 +1,32 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2019 Armin Novak <armin.novak@thincast.com> +# Copyright 2019 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("rdpsnd" "fake" "") + +set(${MODULE_PREFIX}_SRCS + rdpsnd_fake.c +) + +set(${MODULE_PREFIX}_LIBS + winpr + freerdp +) + +include_directories(..) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") diff --git a/channels/rdpsnd/client/fake/rdpsnd_fake.c b/channels/rdpsnd/client/fake/rdpsnd_fake.c new file mode 100644 index 0000000..35251d2 --- /dev/null +++ b/channels/rdpsnd/client/fake/rdpsnd_fake.c @@ -0,0 +1,147 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2019 Armin Novak <armin.novak@thincast.com> + * Copyright 2019 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/stream.h> +#include <winpr/cmdline.h> + +#include <freerdp/types.h> +#include <freerdp/settings.h> + +#include "rdpsnd_main.h" + +typedef struct +{ + rdpsndDevicePlugin device; +} rdpsndFakePlugin; + +static BOOL rdpsnd_fake_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, UINT32 latency) +{ + return TRUE; +} + +static void rdpsnd_fake_close(rdpsndDevicePlugin* device) +{ +} + +static BOOL rdpsnd_fake_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + return TRUE; +} + +static void rdpsnd_fake_free(rdpsndDevicePlugin* device) +{ + rdpsndFakePlugin* fake = (rdpsndFakePlugin*)device; + + if (!fake) + return; + + free(fake); +} + +static BOOL rdpsnd_fake_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + return TRUE; +} + +static UINT rdpsnd_fake_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_fake_parse_addin_args(rdpsndFakePlugin* fake, const ADDIN_ARGV* args) +{ + int status = 0; + DWORD flags = 0; + const COMMAND_LINE_ARGUMENT_A* arg = NULL; + COMMAND_LINE_ARGUMENT_A rdpsnd_fake_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, args->argv, rdpsnd_fake_args, flags, fake, NULL, + NULL); + + if (status < 0) + return ERROR_INVALID_DATA; + + arg = rdpsnd_fake_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 fake_freerdp_rdpsnd_client_subsystem_entry( + PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + const ADDIN_ARGV* args = NULL; + rdpsndFakePlugin* fake = NULL; + UINT ret = CHANNEL_RC_OK; + fake = (rdpsndFakePlugin*)calloc(1, sizeof(rdpsndFakePlugin)); + + if (!fake) + return CHANNEL_RC_NO_MEMORY; + + fake->device.Open = rdpsnd_fake_open; + fake->device.FormatSupported = rdpsnd_fake_format_supported; + fake->device.SetVolume = rdpsnd_fake_set_volume; + fake->device.Play = rdpsnd_fake_play; + fake->device.Close = rdpsnd_fake_close; + fake->device.Free = rdpsnd_fake_free; + args = pEntryPoints->args; + + if (args->argc > 1) + { + ret = rdpsnd_fake_parse_addin_args(fake, args); + + if (ret != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "error parsing arguments"); + goto error; + } + } + + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, &fake->device); + return ret; +error: + rdpsnd_fake_free(&fake->device); + return ret; +} diff --git a/channels/rdpsnd/client/ios/CMakeLists.txt b/channels/rdpsnd/client/ios/CMakeLists.txt new file mode 100644 index 0000000..bfb3903 --- /dev/null +++ b/channels/rdpsnd/client/ios/CMakeLists.txt @@ -0,0 +1,40 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Laxmikant Rashinkar <LK.Rashinkar@gmail.com> +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# Copyright 2013 Corey Clayton <can.of.tuna@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("rdpsnd" "ios" "") + +FIND_LIBRARY(CORE_AUDIO CoreAudio) +FIND_LIBRARY(AUDIO_TOOL AudioToolbox) +FIND_LIBRARY(CORE_FOUNDATION CoreFoundation) + +set(${MODULE_PREFIX}_SRCS + rdpsnd_ios.c + TPCircularBuffer.c) + +set(${MODULE_PREFIX}_LIBS + winpr + freerdp + ${AUDIO_TOOL} + ${CORE_AUDIO} + ${CORE_FOUNDATION} +) + +include_directories(..) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") diff --git a/channels/rdpsnd/client/ios/TPCircularBuffer.c b/channels/rdpsnd/client/ios/TPCircularBuffer.c new file mode 100644 index 0000000..b29f611 --- /dev/null +++ b/channels/rdpsnd/client/ios/TPCircularBuffer.c @@ -0,0 +1,153 @@ +// +// TPCircularBuffer.c +// Circular/Ring buffer implementation +// +// https://github.com/michaeltyson/TPCircularBuffer +// +// Created by Michael Tyson on 10/12/2011. +// +// Copyright (C) 2012-2013 A Tasty Pixel +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#include <winpr/wlog.h> + +#include "TPCircularBuffer.h" +#include "rdpsnd_main.h" + +#include <mach/mach.h> +#include <stdio.h> + +#define reportResult(result, operation) (_reportResult((result), (operation), __FILE__, __LINE__)) +static inline bool _reportResult(kern_return_t result, const char* operation, const char* file, + int line) +{ + if (result != ERR_SUCCESS) + { + WLog_DBG(TAG, "%s:%d: %s: %s\n", file, line, operation, mach_error_string(result)); + return false; + } + return true; +} + +bool TPCircularBufferInit(TPCircularBuffer* buffer, int length) +{ + + // Keep trying until we get our buffer, needed to handle race conditions + int retries = 3; + while (true) + { + + buffer->length = round_page(length); // We need whole page sizes + + // Temporarily allocate twice the length, so we have the contiguous address space to + // support a second instance of the buffer directly after + vm_address_t bufferAddress; + kern_return_t result = vm_allocate(mach_task_self(), &bufferAddress, buffer->length * 2, + VM_FLAGS_ANYWHERE); // allocate anywhere it'll fit + if (result != ERR_SUCCESS) + { + if (retries-- == 0) + { + reportResult(result, "Buffer allocation"); + return false; + } + // Try again if we fail + continue; + } + + // Now replace the second half of the allocation with a virtual copy of the first half. + // Deallocate the second half... + result = vm_deallocate(mach_task_self(), bufferAddress + buffer->length, buffer->length); + if (result != ERR_SUCCESS) + { + if (retries-- == 0) + { + reportResult(result, "Buffer deallocation"); + return false; + } + // If this fails somehow, deallocate the whole region and try again + vm_deallocate(mach_task_self(), bufferAddress, buffer->length); + continue; + } + + // Re-map the buffer to the address space immediately after the buffer + vm_address_t virtualAddress = bufferAddress + buffer->length; + vm_prot_t cur_prot, max_prot; + result = vm_remap(mach_task_self(), + &virtualAddress, // mirror target + buffer->length, // size of mirror + 0, // auto alignment + 0, // force remapping to virtualAddress + mach_task_self(), // same task + bufferAddress, // mirror source + 0, // MAP READ-WRITE, NOT COPY + &cur_prot, // unused protection struct + &max_prot, // unused protection struct + VM_INHERIT_DEFAULT); + if (result != ERR_SUCCESS) + { + if (retries-- == 0) + { + reportResult(result, "Remap buffer memory"); + return false; + } + // If this remap failed, we hit a race condition, so deallocate and try again + vm_deallocate(mach_task_self(), bufferAddress, buffer->length); + continue; + } + + if (virtualAddress != bufferAddress + buffer->length) + { + // If the memory is not contiguous, clean up both allocated buffers and try again + if (retries-- == 0) + { + WLog_DBG(TAG, "Couldn't map buffer memory to end of buffer"); + return false; + } + + vm_deallocate(mach_task_self(), virtualAddress, buffer->length); + vm_deallocate(mach_task_self(), bufferAddress, buffer->length); + continue; + } + + buffer->buffer = (void*)bufferAddress; + buffer->fillCount = 0; + buffer->head = buffer->tail = 0; + + return true; + } + return false; +} + +void TPCircularBufferCleanup(TPCircularBuffer* buffer) +{ + vm_deallocate(mach_task_self(), (vm_address_t)buffer->buffer, buffer->length * 2); + memset(buffer, 0, sizeof(TPCircularBuffer)); +} + +void TPCircularBufferClear(TPCircularBuffer* buffer) +{ + int32_t fillCount; + if (TPCircularBufferTail(buffer, &fillCount)) + { + TPCircularBufferConsume(buffer, fillCount); + } +} diff --git a/channels/rdpsnd/client/ios/TPCircularBuffer.h b/channels/rdpsnd/client/ios/TPCircularBuffer.h new file mode 100644 index 0000000..97e1095 --- /dev/null +++ b/channels/rdpsnd/client/ios/TPCircularBuffer.h @@ -0,0 +1,217 @@ +// +// TPCircularBuffer.h +// Circular/Ring buffer implementation +// +// https://github.com/michaeltyson/TPCircularBuffer +// +// Created by Michael Tyson on 10/12/2011. +// +// +// This implementation makes use of a virtual memory mapping technique that inserts a virtual copy +// of the buffer memory directly after the buffer's end, negating the need for any buffer +// wrap-around logic. Clients can simply use the returned memory address as if it were contiguous +// space. +// +// The implementation is thread-safe in the case of a single producer and single consumer. +// +// Virtual memory technique originally proposed by Philip Howard (http://vrb.slashusr.org/), and +// adapted to Darwin by Kurt Revis (http://www.snoize.com, +// http://www.snoize.com/Code/PlayBufferedSoundFile.tar.gz) +// +// +// Copyright (C) 2012-2013 A Tasty Pixel +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#ifndef TPCircularBuffer_h +#define TPCircularBuffer_h + +#include <libkern/OSAtomic.h> +#include <string.h> +#include <winpr/assert.h> + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef struct + { + void* buffer; + int32_t length; + int32_t tail; + int32_t head; + volatile int32_t fillCount; + } TPCircularBuffer; + + /*! + * Initialise buffer + * + * Note that the length is advisory only: Because of the way the + * memory mirroring technique works, the true buffer length will + * be multiples of the device page size (e.g. 4096 bytes) + * + * @param buffer Circular buffer + * @param length Length of buffer + */ + bool TPCircularBufferInit(TPCircularBuffer* buffer, int32_t length); + + /*! + * Cleanup buffer + * + * Releases buffer resources. + */ + void TPCircularBufferCleanup(TPCircularBuffer* buffer); + + /*! + * Clear buffer + * + * Resets buffer to original, empty state. + * + * This is safe for use by consumer while producer is accessing + * buffer. + */ + void TPCircularBufferClear(TPCircularBuffer* buffer); + + // Reading (consuming) + + /*! + * Access end of buffer + * + * This gives you a pointer to the end of the buffer, ready + * for reading, and the number of available bytes to read. + * + * @param buffer Circular buffer + * @param availableBytes On output, the number of bytes ready for reading + * @return Pointer to the first bytes ready for reading, or NULL if buffer is empty + */ + static __inline__ __attribute__((always_inline)) void* + TPCircularBufferTail(TPCircularBuffer* buffer, int32_t* availableBytes) + { + *availableBytes = buffer->fillCount; + if (*availableBytes == 0) + return NULL; + return (void*)((char*)buffer->buffer + buffer->tail); + } + + /*! + * Consume bytes in buffer + * + * This frees up the just-read bytes, ready for writing again. + * + * @param buffer Circular buffer + * @param amount Number of bytes to consume + */ + static __inline__ __attribute__((always_inline)) void + TPCircularBufferConsume(TPCircularBuffer* buffer, int32_t amount) + { + buffer->tail = (buffer->tail + amount) % buffer->length; + OSAtomicAdd32Barrier(-amount, &buffer->fillCount); + WINPR_ASSERT(buffer->fillCount >= 0); + } + + /*! + * Version of TPCircularBufferConsume without the memory barrier, for more optimal use in + * single-threaded contexts + */ + static __inline__ __attribute__((always_inline)) void + TPCircularBufferConsumeNoBarrier(TPCircularBuffer* buffer, int32_t amount) + { + buffer->tail = (buffer->tail + amount) % buffer->length; + buffer->fillCount -= amount; + WINPR_ASSERT(buffer->fillCount >= 0); + } + + /*! + * Access front of buffer + * + * This gives you a pointer to the front of the buffer, ready + * for writing, and the number of available bytes to write. + * + * @param buffer Circular buffer + * @param availableBytes On output, the number of bytes ready for writing + * @return Pointer to the first bytes ready for writing, or NULL if buffer is full + */ + static __inline__ __attribute__((always_inline)) void* + TPCircularBufferHead(TPCircularBuffer* buffer, int32_t* availableBytes) + { + *availableBytes = (buffer->length - buffer->fillCount); + if (*availableBytes == 0) + return NULL; + return (void*)((char*)buffer->buffer + buffer->head); + } + + // Writing (producing) + + /*! + * Produce bytes in buffer + * + * This marks the given section of the buffer ready for reading. + * + * @param buffer Circular buffer + * @param amount Number of bytes to produce + */ + static __inline__ __attribute__((always_inline)) void + TPCircularBufferProduce(TPCircularBuffer* buffer, int amount) + { + buffer->head = (buffer->head + amount) % buffer->length; + OSAtomicAdd32Barrier(amount, &buffer->fillCount); + WINPR_ASSERT(buffer->fillCount <= buffer->length); + } + + /*! + * Version of TPCircularBufferProduce without the memory barrier, for more optimal use in + * single-threaded contexts + */ + static __inline__ __attribute__((always_inline)) void + TPCircularBufferProduceNoBarrier(TPCircularBuffer* buffer, int amount) + { + buffer->head = (buffer->head + amount) % buffer->length; + buffer->fillCount += amount; + WINPR_ASSERT(buffer->fillCount <= buffer->length); + } + + /*! + * Helper routine to copy bytes to buffer + * + * This copies the given bytes to the buffer, and marks them ready for writing. + * + * @param buffer Circular buffer + * @param src Source buffer + * @param len Number of bytes in source buffer + * @return true if bytes copied, false if there was insufficient space + */ + static __inline__ __attribute__((always_inline)) bool + TPCircularBufferProduceBytes(TPCircularBuffer* buffer, const void* src, int32_t len) + { + int32_t space; + void* ptr = TPCircularBufferHead(buffer, &space); + if (space < len) + return false; + memcpy(ptr, src, len); + TPCircularBufferProduce(buffer, len); + return true; + } + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/channels/rdpsnd/client/ios/rdpsnd_ios.c b/channels/rdpsnd/client/ios/rdpsnd_ios.c new file mode 100644 index 0000000..b8f004f --- /dev/null +++ b/channels/rdpsnd/client/ios/rdpsnd_ios.c @@ -0,0 +1,282 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2013 Dell Software <Mike.McDonald@software.dell.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/wtypes.h> + +#include <freerdp/types.h> +#include <freerdp/codec/dsp.h> + +#import <AudioToolbox/AudioToolbox.h> + +#include "rdpsnd_main.h" +#include "TPCircularBuffer.h" + +#define INPUT_BUFFER_SIZE 32768 +#define CIRCULAR_BUFFER_SIZE (INPUT_BUFFER_SIZE * 4) + +typedef struct +{ + rdpsndDevicePlugin device; + AudioComponentInstance audio_unit; + TPCircularBuffer buffer; + BOOL is_opened; + BOOL is_playing; +} rdpsndIOSPlugin; + +#define THIS(__ptr) ((rdpsndIOSPlugin*)__ptr) + +static OSStatus rdpsnd_ios_render_cb(void* inRefCon, + AudioUnitRenderActionFlags __unused* ioActionFlags, + const AudioTimeStamp __unused* inTimeStamp, UInt32 inBusNumber, + UInt32 __unused inNumberFrames, AudioBufferList* ioData) +{ + if (inBusNumber != 0) + { + return noErr; + } + + rdpsndIOSPlugin* p = THIS(inRefCon); + + for (unsigned int i = 0; i < ioData->mNumberBuffers; i++) + { + AudioBuffer* target_buffer = &ioData->mBuffers[i]; + int32_t available_bytes = 0; + const void* buffer = TPCircularBufferTail(&p->buffer, &available_bytes); + + if (buffer != NULL && available_bytes > 0) + { + const int bytes_to_copy = MIN((int32_t)target_buffer->mDataByteSize, available_bytes); + memcpy(target_buffer->mData, buffer, bytes_to_copy); + target_buffer->mDataByteSize = bytes_to_copy; + TPCircularBufferConsume(&p->buffer, bytes_to_copy); + } + else + { + target_buffer->mDataByteSize = 0; + AudioOutputUnitStop(p->audio_unit); + p->is_playing = 0; + } + } + + return noErr; +} + +static BOOL rdpsnd_ios_format_supported(rdpsndDevicePlugin* __unused device, + const AUDIO_FORMAT* format) +{ + if (format->wFormatTag == WAVE_FORMAT_PCM) + { + return 1; + } + + return 0; +} + +static BOOL rdpsnd_ios_set_volume(rdpsndDevicePlugin* __unused device, UINT32 __unused value) +{ + return TRUE; +} + +static void rdpsnd_ios_start(rdpsndDevicePlugin* device) +{ + rdpsndIOSPlugin* p = THIS(device); + + /* If this device is not playing... */ + if (!p->is_playing) + { + /* Start the device. */ + int32_t available_bytes = 0; + TPCircularBufferTail(&p->buffer, &available_bytes); + + if (available_bytes > 0) + { + p->is_playing = 1; + AudioOutputUnitStart(p->audio_unit); + } + } +} + +static void rdpsnd_ios_stop(rdpsndDevicePlugin* __unused device) +{ + rdpsndIOSPlugin* p = THIS(device); + + /* If the device is playing... */ + if (p->is_playing) + { + /* Stop the device. */ + AudioOutputUnitStop(p->audio_unit); + p->is_playing = 0; + /* Free all buffers. */ + TPCircularBufferClear(&p->buffer); + } +} + +static UINT rdpsnd_ios_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + rdpsndIOSPlugin* p = THIS(device); + const BOOL ok = TPCircularBufferProduceBytes(&p->buffer, data, size); + + if (!ok) + return 0; + + rdpsnd_ios_start(device); + return 10; /* TODO: Get real latencry in [ms] */ +} + +static BOOL rdpsnd_ios_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + int __unused latency) +{ + rdpsndIOSPlugin* p = THIS(device); + + if (p->is_opened) + return TRUE; + + /* Find the output audio unit. */ + AudioComponentDescription desc; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = kAudioUnitSubType_RemoteIO; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + AudioComponent audioComponent = AudioComponentFindNext(NULL, &desc); + + if (audioComponent == NULL) + return FALSE; + + /* Open the audio unit. */ + OSStatus status = AudioComponentInstanceNew(audioComponent, &p->audio_unit); + + if (status != 0) + return FALSE; + + /* Set the format for the AudioUnit. */ + AudioStreamBasicDescription audioFormat = { 0 }; + audioFormat.mSampleRate = format->nSamplesPerSec; + audioFormat.mFormatID = kAudioFormatLinearPCM; + audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + audioFormat.mFramesPerPacket = 1; /* imminent property of the Linear PCM */ + audioFormat.mChannelsPerFrame = format->nChannels; + audioFormat.mBitsPerChannel = format->wBitsPerSample; + audioFormat.mBytesPerFrame = (format->wBitsPerSample * format->nChannels) / 8; + audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame * audioFormat.mFramesPerPacket; + status = AudioUnitSetProperty(p->audio_unit, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, 0, &audioFormat, sizeof(audioFormat)); + + if (status != 0) + { + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + return FALSE; + } + + /* Set up the AudioUnit callback. */ + AURenderCallbackStruct callbackStruct = { 0 }; + callbackStruct.inputProc = rdpsnd_ios_render_cb; + callbackStruct.inputProcRefCon = p; + status = + AudioUnitSetProperty(p->audio_unit, kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, 0, &callbackStruct, sizeof(callbackStruct)); + + if (status != 0) + { + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + return FALSE; + } + + /* Initialize the AudioUnit. */ + status = AudioUnitInitialize(p->audio_unit); + + if (status != 0) + { + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + return FALSE; + } + + /* Allocate the circular buffer. */ + const BOOL ok = TPCircularBufferInit(&p->buffer, CIRCULAR_BUFFER_SIZE); + + if (!ok) + { + AudioUnitUninitialize(p->audio_unit); + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + return FALSE; + } + + p->is_opened = 1; + return TRUE; +} + +static void rdpsnd_ios_close(rdpsndDevicePlugin* device) +{ + rdpsndIOSPlugin* p = THIS(device); + /* Make sure the device is stopped. */ + rdpsnd_ios_stop(device); + + /* If the device is open... */ + if (p->is_opened) + { + /* Close the device. */ + AudioUnitUninitialize(p->audio_unit); + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + p->is_opened = 0; + /* Destroy the circular buffer. */ + TPCircularBufferCleanup(&p->buffer); + } +} + +static void rdpsnd_ios_free(rdpsndDevicePlugin* device) +{ + rdpsndIOSPlugin* p = THIS(device); + /* Ensure the device is closed. */ + rdpsnd_ios_close(device); + /* Free memory associated with the device. */ + free(p); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT(UINT ios_freerdp_rdpsnd_client_subsystem_entry( + PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + rdpsndIOSPlugin* p = (rdpsndIOSPlugin*)calloc(1, sizeof(rdpsndIOSPlugin)); + + if (!p) + return CHANNEL_RC_NO_MEMORY; + + p->device.Open = rdpsnd_ios_open; + p->device.FormatSupported = rdpsnd_ios_format_supported; + p->device.SetVolume = rdpsnd_ios_set_volume; + p->device.Play = rdpsnd_ios_play; + p->device.Start = rdpsnd_ios_start; + p->device.Close = rdpsnd_ios_close; + p->device.Free = rdpsnd_ios_free; + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)p); + return CHANNEL_RC_OK; +} diff --git a/channels/rdpsnd/client/mac/CMakeLists.txt b/channels/rdpsnd/client/mac/CMakeLists.txt new file mode 100644 index 0000000..4bcf1bc --- /dev/null +++ b/channels/rdpsnd/client/mac/CMakeLists.txt @@ -0,0 +1,44 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Laxmikant Rashinkar <LK.Rashinkar@gmail.com> +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# Copyright 2013 Corey Clayton <can.of.tuna@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("rdpsnd" "mac" "") + +find_library(COCOA_LIBRARY Cocoa REQUIRED) +FIND_LIBRARY(CORE_FOUNDATION CoreFoundation) +FIND_LIBRARY(CORE_AUDIO CoreAudio REQUIRED) +FIND_LIBRARY(AUDIO_TOOL AudioToolbox REQUIRED) +FIND_LIBRARY(AV_FOUNDATION AVFoundation REQUIRED) + +set(${MODULE_PREFIX}_SRCS + rdpsnd_mac.m) + +set(${MODULE_PREFIX}_LIBS + winpr + freerdp + ${AUDIO_TOOL} + ${AV_FOUNDATION} + ${CORE_AUDIO} + ${COCOA_LIBRARY} + ${CORE_FOUNDATION} +) + +include_directories(..) +include_directories(${MACAUDIO_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") diff --git a/channels/rdpsnd/client/mac/rdpsnd_mac.m b/channels/rdpsnd/client/mac/rdpsnd_mac.m new file mode 100644 index 0000000..648ded4 --- /dev/null +++ b/channels/rdpsnd/client/mac/rdpsnd_mac.m @@ -0,0 +1,402 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2012 Laxmikant Rashinkar <LK.Rashinkar@gmail.com> + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * Copyright 2016 Inuvika Inc. + * Copyright 2016 David PHAM-VAN <d.phamvan@inuvika.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/sysinfo.h> + +#include <freerdp/types.h> + +#include <AVFoundation/AVAudioBuffer.h> +#include <AVFoundation/AVFoundation.h> + +#include "rdpsnd_main.h" + +typedef struct +{ + rdpsndDevicePlugin device; + + BOOL isOpen; + BOOL isPlaying; + + UINT32 latency; + AUDIO_FORMAT format; + + AVAudioEngine *engine; + AVAudioPlayerNode *player; + UINT64 diff; +} rdpsndMacPlugin; + +static BOOL rdpsnd_mac_set_format(rdpsndDevicePlugin *device, const AUDIO_FORMAT *format, + UINT32 latency) +{ + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + if (!mac || !format) + return FALSE; + + mac->latency = latency; + mac->format = *format; + + audio_format_print(WLog_Get(TAG), WLOG_DEBUG, format); + return TRUE; +} + +static char *FormatError(OSStatus st) +{ + switch (st) + { + case kAudioFileUnspecifiedError: + return "kAudioFileUnspecifiedError"; + + case kAudioFileUnsupportedFileTypeError: + return "kAudioFileUnsupportedFileTypeError"; + + case kAudioFileUnsupportedDataFormatError: + return "kAudioFileUnsupportedDataFormatError"; + + case kAudioFileUnsupportedPropertyError: + return "kAudioFileUnsupportedPropertyError"; + + case kAudioFileBadPropertySizeError: + return "kAudioFileBadPropertySizeError"; + + case kAudioFilePermissionsError: + return "kAudioFilePermissionsError"; + + case kAudioFileNotOptimizedError: + return "kAudioFileNotOptimizedError"; + + case kAudioFileInvalidChunkError: + return "kAudioFileInvalidChunkError"; + + case kAudioFileDoesNotAllow64BitDataSizeError: + return "kAudioFileDoesNotAllow64BitDataSizeError"; + + case kAudioFileInvalidPacketOffsetError: + return "kAudioFileInvalidPacketOffsetError"; + + case kAudioFileInvalidFileError: + return "kAudioFileInvalidFileError"; + + case kAudioFileOperationNotSupportedError: + return "kAudioFileOperationNotSupportedError"; + + case kAudioFileNotOpenError: + return "kAudioFileNotOpenError"; + + case kAudioFileEndOfFileError: + return "kAudioFileEndOfFileError"; + + case kAudioFilePositionError: + return "kAudioFilePositionError"; + + case kAudioFileFileNotFoundError: + return "kAudioFileFileNotFoundError"; + + default: + return "unknown error"; + } +} + +static void rdpsnd_mac_release(rdpsndMacPlugin *mac) +{ + if (mac->player) + [mac->player release]; + mac->player = NULL; + + if (mac->engine) + [mac->engine release]; + mac->engine = NULL; +} + +static BOOL rdpsnd_mac_open(rdpsndDevicePlugin *device, const AUDIO_FORMAT *format, UINT32 latency) +{ + @autoreleasepool + { + AudioDeviceID outputDeviceID; + UInt32 propertySize; + OSStatus err; + NSError *error; + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + AudioObjectPropertyAddress propertyAddress = { + kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyScopeGlobal, +#if defined(MAC_OS_VERSION_12_0) + kAudioObjectPropertyElementMain +#else + kAudioObjectPropertyElementMaster +#endif + }; + + if (mac->isOpen) + return TRUE; + + if (!rdpsnd_mac_set_format(device, format, latency)) + return FALSE; + + propertySize = sizeof(outputDeviceID); + err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, + &propertySize, &outputDeviceID); + if (err) + { + WLog_ERR(TAG, "AudioHardwareGetProperty: %s", FormatError(err)); + return FALSE; + } + + mac->engine = [[AVAudioEngine alloc] init]; + if (!mac->engine) + return FALSE; + + err = AudioUnitSetProperty(mac->engine.outputNode.audioUnit, + kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, + 0, &outputDeviceID, sizeof(outputDeviceID)); + if (err) + { + rdpsnd_mac_release(mac); + WLog_ERR(TAG, "AudioUnitSetProperty: %s", FormatError(err)); + return FALSE; + } + + mac->player = [[AVAudioPlayerNode alloc] init]; + if (!mac->player) + { + rdpsnd_mac_release(mac); + WLog_ERR(TAG, "AVAudioPlayerNode::init() failed"); + return FALSE; + } + + [mac->engine attachNode:mac->player]; + + [mac->engine connect:mac->player to:mac->engine.mainMixerNode format:nil]; + + [mac->engine prepare]; + + if (![mac->engine startAndReturnError:&error]) + { + device->Close(device); + WLog_ERR(TAG, "Failed to start audio player %s", + [error.localizedDescription UTF8String]); + return FALSE; + } + + mac->isOpen = TRUE; + return TRUE; + } +} + +static void rdpsnd_mac_close(rdpsndDevicePlugin *device) +{ + @autoreleasepool + { + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + + if (mac->isPlaying) + { + [mac->player stop]; + mac->isPlaying = FALSE; + } + + if (mac->isOpen) + { + [mac->engine stop]; + mac->isOpen = FALSE; + } + + rdpsnd_mac_release(mac); + } +} + +static void rdpsnd_mac_free(rdpsndDevicePlugin *device) +{ + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + device->Close(device); + free(mac); +} + +static BOOL rdpsnd_mac_format_supported(rdpsndDevicePlugin *device, const AUDIO_FORMAT *format) +{ + WINPR_UNUSED(device); + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + if (format->wBitsPerSample != 16) + return FALSE; + + if (format->nChannels != 2) + return FALSE; + return TRUE; + + default: + return FALSE; + } +} + +static BOOL rdpsnd_mac_set_volume(rdpsndDevicePlugin *device, UINT32 value) +{ + @autoreleasepool + { + Float32 fVolume; + UINT16 volumeLeft; + UINT16 volumeRight; + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + + if (!mac->player) + return FALSE; + + volumeLeft = (value & 0xFFFF); + volumeRight = ((value >> 16) & 0xFFFF); + fVolume = ((float)volumeLeft) / 65535.0f; + + mac->player.volume = fVolume; + + return TRUE; + } +} + +static void rdpsnd_mac_start(rdpsndDevicePlugin *device) +{ + @autoreleasepool + { + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + + if (!mac->isPlaying) + { + if (!mac->engine.isRunning) + { + NSError *error; + + if (![mac->engine startAndReturnError:&error]) + { + device->Close(device); + WLog_ERR(TAG, "Failed to start audio player %s", + [error.localizedDescription UTF8String]); + return; + } + } + + [mac->player play]; + + mac->isPlaying = TRUE; + mac->diff = 100; /* Initial latency, corrected after first sample is played. */ + } + } +} + +static UINT rdpsnd_mac_play(rdpsndDevicePlugin *device, const BYTE *data, size_t size) +{ + @autoreleasepool + { + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + AVAudioPCMBuffer *buffer; + AVAudioFormat *format; + float *const *db; + size_t step; + AVAudioFrameCount count; + UINT64 start = GetTickCount64(); + + if (!mac->isOpen) + return 0; + + step = 2 * mac->format.nChannels; + + count = size / step; + format = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32 + sampleRate:mac->format.nSamplesPerSec + channels:mac->format.nChannels + interleaved:NO]; + + if (!format) + { + WLog_WARN(TAG, "AVAudioFormat::init() failed"); + return 0; + } + + buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:format frameCapacity:count]; + [format release]; + + if (!buffer) + { + WLog_WARN(TAG, "AVAudioPCMBuffer::init() failed"); + return 0; + } + + buffer.frameLength = buffer.frameCapacity; + db = buffer.floatChannelData; + + for (size_t pos = 0; pos < count; pos++) + { + const BYTE *d = &data[pos * step]; + for (size_t x = 0; x < mac->format.nChannels; x++) + { + const float val = (int16_t)((uint16_t)d[0] | ((uint16_t)d[1] << 8)) / 32768.0f; + db[x][pos] = val; + d += sizeof(int16_t); + } + } + + rdpsnd_mac_start(device); + + [mac->player scheduleBuffer:buffer + completionHandler:^{ + UINT64 stop = GetTickCount64(); + if (start > stop) + mac->diff = 0; + else + mac->diff = stop - start; + }]; + + [buffer release]; + + return mac->diff > UINT_MAX ? UINT_MAX : mac->diff; + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT(UINT mac_freerdp_rdpsnd_client_subsystem_entry( + PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + rdpsndMacPlugin *mac; + mac = (rdpsndMacPlugin *)calloc(1, sizeof(rdpsndMacPlugin)); + + if (!mac) + return CHANNEL_RC_NO_MEMORY; + + mac->device.Open = rdpsnd_mac_open; + mac->device.FormatSupported = rdpsnd_mac_format_supported; + mac->device.SetVolume = rdpsnd_mac_set_volume; + mac->device.Play = rdpsnd_mac_play; + mac->device.Close = rdpsnd_mac_close; + mac->device.Free = rdpsnd_mac_free; + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin *)mac); + return CHANNEL_RC_OK; +} diff --git a/channels/rdpsnd/client/opensles/CMakeLists.txt b/channels/rdpsnd/client/opensles/CMakeLists.txt new file mode 100644 index 0000000..1df65e9 --- /dev/null +++ b/channels/rdpsnd/client/opensles/CMakeLists.txt @@ -0,0 +1,35 @@ +# 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("rdpsnd" "opensles" "") + +find_package(OpenSLES REQUIRED) + +set(${MODULE_PREFIX}_SRCS + opensl_io.c + rdpsnd_opensles.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/rdpsnd/client/opensles/opensl_io.c b/channels/rdpsnd/client/opensles/opensl_io.c new file mode 100644 index 0000000..70da341 --- /dev/null +++ b/channels/rdpsnd/client/opensles/opensl_io.c @@ -0,0 +1,422 @@ +/* +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 "rdpsnd_main.h" +#include "opensl_io.h" +#define CONV16BIT 32768 +#define CONVMYFLT (1. / 32768.) + +static void bqPlayerCallback(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); + DEBUG_SND("engineObject=%p", (void*)p->engineObject); + + if (result != SL_RESULT_SUCCESS) + goto engine_end; + + // realize the engine + result = (*p->engineObject)->Realize(p->engineObject, SL_BOOLEAN_FALSE); + DEBUG_SND("Realize=%" PRIu32 "", result); + + 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)); + DEBUG_SND("engineEngine=%p", (void*)p->engineEngine); + + if (result != SL_RESULT_SUCCESS) + goto engine_end; + +engine_end: + return result; +} + +// opens the OpenSL ES device for output +static SLresult openSLPlayOpen(OPENSL_STREAM* p) +{ + SLresult result; + SLuint32 sr = p->sr; + SLuint32 channels = p->outchannels; + WINPR_ASSERT(p->engineObject); + WINPR_ASSERT(p->engineEngine); + + if (channels) + { + // configure audio source + SLDataLocator_AndroidSimpleBufferQueue loc_bufq = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, + p->queuesize }; + + 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; + } + + const SLInterfaceID ids[] = { SL_IID_VOLUME }; + const SLboolean req[] = { SL_BOOLEAN_FALSE }; + result = (*p->engineEngine) + ->CreateOutputMix(p->engineEngine, &(p->outputMixObject), 1, ids, req); + DEBUG_SND("engineEngine=%p", (void*)p->engineEngine); + WINPR_ASSERT(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // realize the output mix + result = (*p->outputMixObject)->Realize(p->outputMixObject, SL_BOOLEAN_FALSE); + DEBUG_SND("Realize=%" PRIu32 "", result); + WINPR_ASSERT(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + int speakers; + + if (channels > 1) + speakers = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT; + else + speakers = SL_SPEAKER_FRONT_CENTER; + + SLDataFormat_PCM format_pcm = { SL_DATAFORMAT_PCM, + channels, + sr, + SL_PCMSAMPLEFORMAT_FIXED_16, + SL_PCMSAMPLEFORMAT_FIXED_16, + speakers, + SL_BYTEORDER_LITTLEENDIAN }; + SLDataSource audioSrc = { &loc_bufq, &format_pcm }; + // configure audio sink + SLDataLocator_OutputMix loc_outmix = { SL_DATALOCATOR_OUTPUTMIX, p->outputMixObject }; + SLDataSink audioSnk = { &loc_outmix, NULL }; + // create audio player + const SLInterfaceID ids1[] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_VOLUME }; + const SLboolean req1[] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE }; + result = (*p->engineEngine) + ->CreateAudioPlayer(p->engineEngine, &(p->bqPlayerObject), &audioSrc, + &audioSnk, 2, ids1, req1); + DEBUG_SND("bqPlayerObject=%p", (void*)p->bqPlayerObject); + WINPR_ASSERT(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // realize the player + result = (*p->bqPlayerObject)->Realize(p->bqPlayerObject, SL_BOOLEAN_FALSE); + DEBUG_SND("Realize=%" PRIu32 "", result); + WINPR_ASSERT(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // get the play interface + result = + (*p->bqPlayerObject)->GetInterface(p->bqPlayerObject, SL_IID_PLAY, &(p->bqPlayerPlay)); + DEBUG_SND("bqPlayerPlay=%p", (void*)p->bqPlayerPlay); + WINPR_ASSERT(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // get the volume interface + result = (*p->bqPlayerObject) + ->GetInterface(p->bqPlayerObject, SL_IID_VOLUME, &(p->bqPlayerVolume)); + DEBUG_SND("bqPlayerVolume=%p", (void*)p->bqPlayerVolume); + WINPR_ASSERT(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // get the buffer queue interface + result = (*p->bqPlayerObject) + ->GetInterface(p->bqPlayerObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, + &(p->bqPlayerBufferQueue)); + DEBUG_SND("bqPlayerBufferQueue=%p", (void*)p->bqPlayerBufferQueue); + WINPR_ASSERT(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // register callback on the buffer queue + result = (*p->bqPlayerBufferQueue) + ->RegisterCallback(p->bqPlayerBufferQueue, bqPlayerCallback, p); + DEBUG_SND("bqPlayerCallback=%p", (void*)p->bqPlayerCallback); + WINPR_ASSERT(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // set the player's state to playing + result = (*p->bqPlayerPlay)->SetPlayState(p->bqPlayerPlay, SL_PLAYSTATE_PLAYING); + DEBUG_SND("SetPlayState=%" PRIu32 "", result); + WINPR_ASSERT(!result); + end_openaudio: + WINPR_ASSERT(!result); + return result; + } + + return SL_RESULT_SUCCESS; +} + +// close the OpenSL IO and destroy the audio engine +static void openSLDestroyEngine(OPENSL_STREAM* p) +{ + // destroy buffer queue audio player object, and invalidate all associated interfaces + if (p->bqPlayerObject != NULL) + { + (*p->bqPlayerObject)->Destroy(p->bqPlayerObject); + p->bqPlayerObject = NULL; + p->bqPlayerVolume = NULL; + p->bqPlayerPlay = NULL; + p->bqPlayerBufferQueue = NULL; + p->bqPlayerEffectSend = NULL; + } + + // destroy output mix object, and invalidate all associated interfaces + if (p->outputMixObject != NULL) + { + (*p->outputMixObject)->Destroy(p->outputMixObject); + p->outputMixObject = NULL; + } + + // destroy engine object, and invalidate all associated interfaces + if (p->engineObject != NULL) + { + (*p->engineObject)->Destroy(p->engineObject); + p->engineObject = NULL; + p->engineEngine = NULL; + } +} + +// open the android audio device for and/or output +OPENSL_STREAM* android_OpenAudioDevice(int sr, int outchannels, int bufferframes) +{ + OPENSL_STREAM* p; + p = (OPENSL_STREAM*)calloc(1, sizeof(OPENSL_STREAM)); + + if (!p) + return NULL; + + p->queuesize = bufferframes; + p->outchannels = outchannels; + p->sr = sr; + + if (openSLCreateEngine(p) != SL_RESULT_SUCCESS) + { + android_CloseAudioDevice(p); + return NULL; + } + + if (openSLPlayOpen(p) != SL_RESULT_SUCCESS) + { + android_CloseAudioDevice(p); + return NULL; + } + + p->queue = Queue_New(TRUE, -1, -1); + + if (!p->queue) + { + android_CloseAudioDevice(p); + return NULL; + } + + return p; +} + +// close the android audio device +void android_CloseAudioDevice(OPENSL_STREAM* p) +{ + if (p == NULL) + return; + + openSLDestroyEngine(p); + + if (p->queue) + Queue_Free(p->queue); + + free(p); +} + +// this callback handler is called every time a buffer finishes playing +static void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void* context) +{ + OPENSL_STREAM* p = (OPENSL_STREAM*)context; + WINPR_ASSERT(p); + WINPR_ASSERT(p->queue); + void* data = Queue_Dequeue(p->queue); + free(data); +} + +// puts a buffer of size samples to the device +int android_AudioOut(OPENSL_STREAM* p, const short* buffer, int size) +{ + HANDLE ev; + WINPR_ASSERT(p); + WINPR_ASSERT(buffer); + WINPR_ASSERT(size > 0); + + ev = Queue_Event(p->queue); + /* Assure, that the queue is not full. */ + if (p->queuesize <= Queue_Count(p->queue) && WaitForSingleObject(ev, INFINITE) == WAIT_FAILED) + { + DEBUG_SND("WaitForSingleObject failed!"); + return -1; + } + + void* data = calloc(size, sizeof(short)); + + if (!data) + { + DEBUG_SND("unable to allocate a buffer"); + return -1; + } + + memcpy(data, buffer, size * sizeof(short)); + Queue_Enqueue(p->queue, data); + (*p->bqPlayerBufferQueue)->Enqueue(p->bqPlayerBufferQueue, data, sizeof(short) * size); + return size; +} + +int android_GetOutputMute(OPENSL_STREAM* p) +{ + SLboolean mute; + WINPR_ASSERT(p); + WINPR_ASSERT(p->bqPlayerVolume); + SLresult rc = (*p->bqPlayerVolume)->GetMute(p->bqPlayerVolume, &mute); + + if (SL_RESULT_SUCCESS != rc) + return SL_BOOLEAN_FALSE; + + return mute; +} + +BOOL android_SetOutputMute(OPENSL_STREAM* p, BOOL _mute) +{ + SLboolean mute = _mute; + WINPR_ASSERT(p); + WINPR_ASSERT(p->bqPlayerVolume); + SLresult rc = (*p->bqPlayerVolume)->SetMute(p->bqPlayerVolume, mute); + + if (SL_RESULT_SUCCESS != rc) + return FALSE; + + return TRUE; +} + +int android_GetOutputVolume(OPENSL_STREAM* p) +{ + SLmillibel level; + WINPR_ASSERT(p); + WINPR_ASSERT(p->bqPlayerVolume); + SLresult rc = (*p->bqPlayerVolume)->GetVolumeLevel(p->bqPlayerVolume, &level); + + if (SL_RESULT_SUCCESS != rc) + return 0; + + return level; +} + +int android_GetOutputVolumeMax(OPENSL_STREAM* p) +{ + SLmillibel level; + WINPR_ASSERT(p); + WINPR_ASSERT(p->bqPlayerVolume); + SLresult rc = (*p->bqPlayerVolume)->GetMaxVolumeLevel(p->bqPlayerVolume, &level); + + if (SL_RESULT_SUCCESS != rc) + return 0; + + return level; +} + +BOOL android_SetOutputVolume(OPENSL_STREAM* p, int level) +{ + SLresult rc = (*p->bqPlayerVolume)->SetVolumeLevel(p->bqPlayerVolume, level); + + if (SL_RESULT_SUCCESS != rc) + return FALSE; + + return TRUE; +} diff --git a/channels/rdpsnd/client/opensles/opensl_io.h b/channels/rdpsnd/client/opensles/opensl_io.h new file mode 100644 index 0000000..f303d21 --- /dev/null +++ b/channels/rdpsnd/client/opensles/opensl_io.h @@ -0,0 +1,110 @@ +/* +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_RDPSND_CLIENT_OPENSL_IO_H +#define FREERDP_CHANNEL_RDPSND_CLIENT_OPENSL_IO_H + +#include <SLES/OpenSLES.h> +#include <SLES/OpenSLES_Android.h> +#include <stdlib.h> +#include <winpr/synch.h> + +#include <freerdp/api.h> + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef struct + { + // engine interfaces + SLObjectItf engineObject; + SLEngineItf engineEngine; + + // output mix interfaces + SLObjectItf outputMixObject; + + // buffer queue player interfaces + SLObjectItf bqPlayerObject; + SLPlayItf bqPlayerPlay; + SLVolumeItf bqPlayerVolume; + SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue; + SLEffectSendItf bqPlayerEffectSend; + + unsigned int outchannels; + unsigned int sr; + + unsigned int queuesize; + wQueue* queue; + } OPENSL_STREAM; + + /* + Open the audio device with a given sampling rate (sr), output channels and IO buffer size + in frames. Returns a handle to the OpenSL stream + */ + FREERDP_LOCAL OPENSL_STREAM* android_OpenAudioDevice(int sr, int outchannels, int bufferframes); + /* + Close the audio device + */ + FREERDP_LOCAL void android_CloseAudioDevice(OPENSL_STREAM* p); + /* + Write a buffer to the OpenSL stream *p, of size samples. Returns the number of samples written. + */ + FREERDP_LOCAL int android_AudioOut(OPENSL_STREAM* p, const short* buffer, int size); + /* + * Set the volume input level. + */ + FREERDP_LOCAL void android_SetInputVolume(OPENSL_STREAM* p, int level); + /* + * Get the current output mute setting. + */ + FREERDP_LOCAL int android_GetOutputMute(OPENSL_STREAM* p); + /* + * Change the current output mute setting. + */ + FREERDP_LOCAL BOOL android_SetOutputMute(OPENSL_STREAM* p, BOOL mute); + /* + * Get the current output volume level. + */ + FREERDP_LOCAL int android_GetOutputVolume(OPENSL_STREAM* p); + /* + * Get the maximum output volume level. + */ + FREERDP_LOCAL int android_GetOutputVolumeMax(OPENSL_STREAM* p); + + /* + * Set the volume output level. + */ + FREERDP_LOCAL BOOL android_SetOutputVolume(OPENSL_STREAM* p, int level); +#ifdef __cplusplus +}; +#endif + +#endif /* FREERDP_CHANNEL_RDPSND_CLIENT_OPENSL_IO_H */ diff --git a/channels/rdpsnd/client/opensles/rdpsnd_opensles.c b/channels/rdpsnd/client/opensles/rdpsnd_opensles.c new file mode 100644 index 0000000..c5679b6 --- /dev/null +++ b/channels/rdpsnd/client/opensles/rdpsnd_opensles.c @@ -0,0 +1,378 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * 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 <stdbool.h> + +#include <winpr/crt.h> +#include <winpr/cmdline.h> +#include <winpr/sysinfo.h> +#include <winpr/collections.h> + +#include <freerdp/types.h> +#include <freerdp/channels/log.h> + +#include "opensl_io.h" +#include "rdpsnd_main.h" + +typedef struct +{ + rdpsndDevicePlugin device; + + UINT32 latency; + int wformat; + int block_size; + char* device_name; + + OPENSL_STREAM* stream; + + UINT32 volume; + + UINT32 rate; + UINT32 channels; + int format; +} rdpsndopenslesPlugin; + +static int rdpsnd_opensles_volume_to_millibel(unsigned short level, int max) +{ + const int min = SL_MILLIBEL_MIN; + const int step = max - min; + const int rc = (level * step / 0xFFFF) + min; + DEBUG_SND("level=%hu, min=%d, max=%d, step=%d, result=%d", level, min, max, step, rc); + return rc; +} + +static unsigned short rdpsnd_opensles_millibel_to_volume(int millibel, int max) +{ + const int min = SL_MILLIBEL_MIN; + const int range = max - min; + const int rc = ((millibel - min) * 0xFFFF + range / 2 + 1) / range; + DEBUG_SND("millibel=%d, min=%d, max=%d, range=%d, result=%d", millibel, min, max, range, rc); + return rc; +} + +static bool rdpsnd_opensles_check_handle(const rdpsndopenslesPlugin* hdl) +{ + bool rc = true; + + if (!hdl) + rc = false; + else + { + if (!hdl->stream) + rc = false; + } + + return rc; +} + +static BOOL rdpsnd_opensles_set_volume(rdpsndDevicePlugin* device, UINT32 volume); + +static int rdpsnd_opensles_set_params(rdpsndopenslesPlugin* opensles) +{ + DEBUG_SND("opensles=%p", (void*)opensles); + + if (!rdpsnd_opensles_check_handle(opensles)) + return 0; + + if (opensles->stream) + android_CloseAudioDevice(opensles->stream); + + opensles->stream = android_OpenAudioDevice(opensles->rate, opensles->channels, 20); + return 0; +} + +static BOOL rdpsnd_opensles_set_format(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + rdpsnd_opensles_check_handle(opensles); + DEBUG_SND("opensles=%p format=%p, latency=%" PRIu32, (void*)opensles, (void*)format, latency); + + if (format) + { + DEBUG_SND("format=%" PRIu16 ", cbsize=%" PRIu16 ", samples=%" PRIu32 ", bits=%" PRIu16 + ", channels=%" PRIu16 ", align=%" PRIu16 "", + format->wFormatTag, format->cbSize, format->nSamplesPerSec, + format->wBitsPerSample, format->nChannels, format->nBlockAlign); + opensles->rate = format->nSamplesPerSec; + opensles->channels = format->nChannels; + opensles->format = format->wFormatTag; + opensles->wformat = format->wFormatTag; + opensles->block_size = format->nBlockAlign; + } + + opensles->latency = latency; + return (rdpsnd_opensles_set_params(opensles) == 0); +} + +static BOOL rdpsnd_opensles_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + DEBUG_SND("opensles=%p format=%p, latency=%" PRIu32 ", rate=%" PRIu32 "", (void*)opensles, + (void*)format, latency, opensles->rate); + + if (rdpsnd_opensles_check_handle(opensles)) + return TRUE; + + opensles->stream = android_OpenAudioDevice(opensles->rate, opensles->channels, 20); + WINPR_ASSERT(opensles->stream); + + if (!opensles->stream) + WLog_ERR(TAG, "android_OpenAudioDevice failed"); + else + rdpsnd_opensles_set_volume(device, opensles->volume); + + return rdpsnd_opensles_set_format(device, format, latency); +} + +static void rdpsnd_opensles_close(rdpsndDevicePlugin* device) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + DEBUG_SND("opensles=%p", (void*)opensles); + + if (!rdpsnd_opensles_check_handle(opensles)) + return; + + android_CloseAudioDevice(opensles->stream); + opensles->stream = NULL; +} + +static void rdpsnd_opensles_free(rdpsndDevicePlugin* device) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + DEBUG_SND("opensles=%p", (void*)opensles); + WINPR_ASSERT(opensles); + WINPR_ASSERT(opensles->device_name); + free(opensles->device_name); + free(opensles); +} + +static BOOL rdpsnd_opensles_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + DEBUG_SND("format=%" PRIu16 ", cbsize=%" PRIu16 ", samples=%" PRIu32 ", bits=%" PRIu16 + ", channels=%" PRIu16 ", align=%" PRIu16 "", + format->wFormatTag, format->cbSize, format->nSamplesPerSec, format->wBitsPerSample, + format->nChannels, format->nBlockAlign); + WINPR_ASSERT(device); + WINPR_ASSERT(format); + + 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: + break; + } + + return FALSE; +} + +static UINT32 rdpsnd_opensles_get_volume(rdpsndDevicePlugin* device) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + DEBUG_SND("opensles=%p", (void*)opensles); + WINPR_ASSERT(opensles); + + if (opensles->stream) + { + const int max = android_GetOutputVolumeMax(opensles->stream); + const int rc = android_GetOutputVolume(opensles->stream); + + if (android_GetOutputMute(opensles->stream)) + opensles->volume = 0; + else + { + const unsigned short vol = rdpsnd_opensles_millibel_to_volume(rc, max); + opensles->volume = (vol << 16) | (vol & 0xFFFF); + } + } + + return opensles->volume; +} + +static BOOL rdpsnd_opensles_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + DEBUG_SND("opensles=%p, value=%" PRIu32 "", (void*)opensles, value); + WINPR_ASSERT(opensles); + opensles->volume = value; + + if (opensles->stream) + { + if (0 == opensles->volume) + return android_SetOutputMute(opensles->stream, true); + else + { + const int max = android_GetOutputVolumeMax(opensles->stream); + const int vol = rdpsnd_opensles_volume_to_millibel(value & 0xFFFF, max); + + if (!android_SetOutputMute(opensles->stream, false)) + return FALSE; + + if (!android_SetOutputVolume(opensles->stream, vol)) + return FALSE; + } + } + + return TRUE; +} + +static UINT rdpsnd_opensles_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + union + { + const BYTE* b; + const short* s; + } src; + int ret; + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + DEBUG_SND("opensles=%p, data=%p, size=%d", (void*)opensles, (void*)data, size); + + if (!rdpsnd_opensles_check_handle(opensles)) + return 0; + + src.b = data; + DEBUG_SND("size=%d, src=%p", size, (void*)src.b); + WINPR_ASSERT(0 == size % 2); + WINPR_ASSERT(size > 0); + WINPR_ASSERT(src.b); + ret = android_AudioOut(opensles->stream, src.s, size / 2); + + if (ret < 0) + WLog_ERR(TAG, "android_AudioOut failed (%d)", ret); + + return 10; /* TODO: Get real latencry in [ms] */ +} + +static void rdpsnd_opensles_start(rdpsndDevicePlugin* device) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + rdpsnd_opensles_check_handle(opensles); + DEBUG_SND("opensles=%p", (void*)opensles); +} + +static int rdpsnd_opensles_parse_addin_args(rdpsndDevicePlugin* device, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + const COMMAND_LINE_ARGUMENT_A* arg; + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + COMMAND_LINE_ARGUMENT_A rdpsnd_opensles_args[] = { + { "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", NULL, NULL, -1, NULL, "device" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } + }; + + WINPR_ASSERT(opensles); + WINPR_ASSERT(args); + DEBUG_SND("opensles=%p, args=%p", (void*)opensles, (void*)args); + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_opensles_args, flags, + opensles, NULL, NULL); + + if (status < 0) + return status; + + arg = rdpsnd_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) + return ERROR_OUTOFMEMORY; + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT(UINT opensles_freerdp_rdpsnd_client_subsystem_entry( + PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + ADDIN_ARGV* args; + rdpsndopenslesPlugin* opensles; + UINT error; + DEBUG_SND("pEntryPoints=%p", (void*)pEntryPoints); + opensles = (rdpsndopenslesPlugin*)calloc(1, sizeof(rdpsndopenslesPlugin)); + + if (!opensles) + return CHANNEL_RC_NO_MEMORY; + + opensles->device.Open = rdpsnd_opensles_open; + opensles->device.FormatSupported = rdpsnd_opensles_format_supported; + opensles->device.GetVolume = rdpsnd_opensles_get_volume; + opensles->device.SetVolume = rdpsnd_opensles_set_volume; + opensles->device.Start = rdpsnd_opensles_start; + opensles->device.Play = rdpsnd_opensles_play; + opensles->device.Close = rdpsnd_opensles_close; + opensles->device.Free = rdpsnd_opensles_free; + args = pEntryPoints->args; + rdpsnd_opensles_parse_addin_args((rdpsndDevicePlugin*)opensles, args); + + if (!opensles->device_name) + { + opensles->device_name = _strdup("default"); + + if (!opensles->device_name) + { + error = CHANNEL_RC_NO_MEMORY; + goto outstrdup; + } + } + + opensles->rate = 44100; + opensles->channels = 2; + opensles->format = WAVE_FORMAT_ADPCM; + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)opensles); + DEBUG_SND("success"); + return CHANNEL_RC_OK; +outstrdup: + free(opensles); + return error; +} diff --git a/channels/rdpsnd/client/oss/CMakeLists.txt b/channels/rdpsnd/client/oss/CMakeLists.txt new file mode 100644 index 0000000..83bd59f --- /dev/null +++ b/channels/rdpsnd/client/oss/CMakeLists.txt @@ -0,0 +1,35 @@ +# 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("rdpsnd" "oss" "") + +find_package(OSS REQUIRED) + +set(${MODULE_PREFIX}_SRCS + rdpsnd_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/rdpsnd/client/oss/rdpsnd_oss.c b/channels/rdpsnd/client/oss/rdpsnd_oss.c new file mode 100644 index 0000000..54869ab --- /dev/null +++ b/channels/rdpsnd/client/oss/rdpsnd_oss.c @@ -0,0 +1,478 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * 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/string.h> +#include <winpr/cmdline.h> +#include <winpr/sysinfo.h> +#include <winpr/collections.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/types.h> +#include <freerdp/settings.h> +#include <freerdp/channels/log.h> + +#include "rdpsnd_main.h" + +typedef struct +{ + rdpsndDevicePlugin device; + + int pcm_handle; + int mixer_handle; + int dev_unit; + + int supported_formats; + + UINT32 latency; + AUDIO_FORMAT format; +} rdpsndOssPlugin; + +#define OSS_LOG_ERR(_text, _error) \ + do \ + { \ + if (_error != 0) \ + { \ + char ebuffer[256] = { 0 }; \ + WLog_ERR(TAG, "%s: %i - %s", _text, _error, \ + winpr_strerror(_error, ebuffer, sizeof(ebuffer))); \ + } \ + } while (0) + +static int rdpsnd_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 rdpsnd_oss_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + int req_fmt = 0; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + 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; + } + + req_fmt = rdpsnd_oss_get_format(format); + + /* Check really supported formats by dev. */ + if (oss->pcm_handle != -1) + { + if ((req_fmt & oss->supported_formats) == 0) + return FALSE; + } + else + { + if (req_fmt == 0) + return FALSE; + } + + return TRUE; +} + +static BOOL rdpsnd_oss_set_format(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + int tmp = 0; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL || oss->pcm_handle == -1 || format == NULL) + return FALSE; + + oss->latency = latency; + CopyMemory(&(oss->format), format, sizeof(AUDIO_FORMAT)); + tmp = rdpsnd_oss_get_format(format); + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_SETFMT, &tmp) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_SETFMT failed", errno); + return FALSE; + } + + tmp = format->nChannels; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_CHANNELS, &tmp) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_CHANNELS failed", errno); + return FALSE; + } + + tmp = format->nSamplesPerSec; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_SPEED, &tmp) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_SPEED failed", errno); + return FALSE; + } + + tmp = format->nBlockAlign; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_SETFRAGMENT, &tmp) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_SETFRAGMENT failed", errno); + return FALSE; + } + + return TRUE; +} + +static void rdpsnd_oss_open_mixer(rdpsndOssPlugin* oss) +{ + int devmask = 0; + char mixer_name[PATH_MAX] = "/dev/mixer"; + + if (oss->mixer_handle != -1) + return; + + if (oss->dev_unit != -1) + sprintf_s(mixer_name, PATH_MAX - 1, "/dev/mixer%i", oss->dev_unit); + + if ((oss->mixer_handle = open(mixer_name, O_RDWR)) < 0) + { + OSS_LOG_ERR("mixer open failed", errno); + oss->mixer_handle = -1; + return; + } + + if (ioctl(oss->mixer_handle, SOUND_MIXER_READ_DEVMASK, &devmask) == -1) + { + OSS_LOG_ERR("SOUND_MIXER_READ_DEVMASK failed", errno); + close(oss->mixer_handle); + oss->mixer_handle = -1; + return; + } +} + +static BOOL rdpsnd_oss_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, UINT32 latency) +{ + char dev_name[PATH_MAX] = "/dev/dsp"; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL || oss->pcm_handle != -1) + return TRUE; + + if (oss->dev_unit != -1) + sprintf_s(dev_name, PATH_MAX - 1, "/dev/dsp%i", oss->dev_unit); + + WLog_INFO(TAG, "open: %s", dev_name); + + if ((oss->pcm_handle = open(dev_name, O_WRONLY)) < 0) + { + OSS_LOG_ERR("sound dev open failed", errno); + oss->pcm_handle = -1; + return FALSE; + } + +#if 0 /* FreeBSD OSS implementation at this moment (2015.03) does not set PCM_CAP_OUTPUT flag. */ + int mask = 0; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_GETCAPS, &mask) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_GETCAPS failed, try ignory", errno); + } + else if ((mask & PCM_CAP_OUTPUT) == 0) + { + OSS_LOG_ERR("Device does not supports playback", EOPNOTSUPP); + close(oss->pcm_handle); + oss->pcm_handle = -1; + return; + } + +#endif + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_GETFMTS, &oss->supported_formats) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_GETFMTS failed", errno); + close(oss->pcm_handle); + oss->pcm_handle = -1; + return FALSE; + } + + rdpsnd_oss_set_format(device, format, latency); + rdpsnd_oss_open_mixer(oss); + return TRUE; +} + +static void rdpsnd_oss_close(rdpsndDevicePlugin* device) +{ + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL) + return; + + if (oss->pcm_handle != -1) + { + WLog_INFO(TAG, "close: dsp"); + close(oss->pcm_handle); + oss->pcm_handle = -1; + } + + if (oss->mixer_handle != -1) + { + WLog_INFO(TAG, "close: mixer"); + close(oss->mixer_handle); + oss->mixer_handle = -1; + } +} + +static void rdpsnd_oss_free(rdpsndDevicePlugin* device) +{ + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL) + return; + + rdpsnd_oss_close(device); + free(oss); +} + +static UINT32 rdpsnd_oss_get_volume(rdpsndDevicePlugin* device) +{ + int vol = 0; + UINT32 dwVolume = 0; + UINT16 dwVolumeLeft = 0; + UINT16 dwVolumeRight = 0; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + /* On error return 50% volume. */ + dwVolumeLeft = ((50 * 0xFFFF) / 100); /* 50% */ + dwVolumeRight = ((50 * 0xFFFF) / 100); /* 50% */ + dwVolume = ((dwVolumeLeft << 16) | dwVolumeRight); + + if (device == NULL || oss->mixer_handle == -1) + return dwVolume; + + if (ioctl(oss->mixer_handle, MIXER_READ(SOUND_MIXER_VOLUME), &vol) == -1) + { + OSS_LOG_ERR("MIXER_READ", errno); + return dwVolume; + } + + dwVolumeLeft = (((vol & 0x7f) * 0xFFFF) / 100); + dwVolumeRight = ((((vol >> 8) & 0x7f) * 0xFFFF) / 100); + dwVolume = ((dwVolumeLeft << 16) | dwVolumeRight); + return dwVolume; +} + +static BOOL rdpsnd_oss_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + int left = 0; + int right = 0; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL || oss->mixer_handle == -1) + return FALSE; + + left = (((value & 0xFFFF) * 100) / 0xFFFF); + right = ((((value >> 16) & 0xFFFF) * 100) / 0xFFFF); + + if (left < 0) + left = 0; + else if (left > 100) + left = 100; + + if (right < 0) + right = 0; + else if (right > 100) + right = 100; + + left |= (right << 8); + + if (ioctl(oss->mixer_handle, MIXER_WRITE(SOUND_MIXER_VOLUME), &left) == -1) + { + OSS_LOG_ERR("WRITE_MIXER", errno); + return FALSE; + } + + return TRUE; +} + +static UINT rdpsnd_oss_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL || oss->mixer_handle == -1) + return 0; + + while (size > 0) + { + ssize_t status = write(oss->pcm_handle, data, size); + + if (status < 0) + { + OSS_LOG_ERR("write fail", errno); + rdpsnd_oss_close(device); + rdpsnd_oss_open(device, NULL, oss->latency); + break; + } + + data += status; + + if ((size_t)status <= size) + size -= (size_t)status; + else + size = 0; + } + + return 10; /* TODO: Get real latency in [ms] */ +} + +static int rdpsnd_oss_parse_addin_args(rdpsndDevicePlugin* 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; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + COMMAND_LINE_ARGUMENT_A rdpsnd_oss_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", + NULL, NULL, -1, NULL, "device" }, + { 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, rdpsnd_oss_args, flags, oss, NULL, NULL); + + if (status < 0) + return status; + + arg = rdpsnd_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) + return ERROR_OUTOFMEMORY; + + { + 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 = val; + } + + if (oss->dev_unit < 0 || *eptr != '\0') + oss->dev_unit = -1; + + free(str_num); + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT(UINT oss_freerdp_rdpsnd_client_subsystem_entry( + PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + const ADDIN_ARGV* args = NULL; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)calloc(1, sizeof(rdpsndOssPlugin)); + + if (!oss) + return CHANNEL_RC_NO_MEMORY; + + oss->device.Open = rdpsnd_oss_open; + oss->device.FormatSupported = rdpsnd_oss_format_supported; + oss->device.GetVolume = rdpsnd_oss_get_volume; + oss->device.SetVolume = rdpsnd_oss_set_volume; + oss->device.Play = rdpsnd_oss_play; + oss->device.Close = rdpsnd_oss_close; + oss->device.Free = rdpsnd_oss_free; + oss->pcm_handle = -1; + oss->mixer_handle = -1; + oss->dev_unit = -1; + args = pEntryPoints->args; + if (rdpsnd_oss_parse_addin_args(&oss->device, args) < 0) + { + free(oss); + return ERROR_INVALID_PARAMETER; + } + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)oss); + return CHANNEL_RC_OK; +} diff --git a/channels/rdpsnd/client/pulse/CMakeLists.txt b/channels/rdpsnd/client/pulse/CMakeLists.txt new file mode 100644 index 0000000..d42576c --- /dev/null +++ b/channels/rdpsnd/client/pulse/CMakeLists.txt @@ -0,0 +1,36 @@ +# 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("rdpsnd" "pulse" "") + +find_package(PulseAudio REQUIRED) + +set(${MODULE_PREFIX}_SRCS + rdpsnd_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/rdpsnd/client/pulse/rdpsnd_pulse.c b/channels/rdpsnd/client/pulse/rdpsnd_pulse.c new file mode 100644 index 0000000..59b2076 --- /dev/null +++ b/channels/rdpsnd/client/pulse/rdpsnd_pulse.c @@ -0,0 +1,768 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 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 <errno.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#include <winpr/crt.h> +#include <winpr/assert.h> +#include <winpr/stream.h> +#include <winpr/cmdline.h> + +#include <pulse/pulseaudio.h> + +#include <freerdp/types.h> +#include <freerdp/codec/dsp.h> + +#include "rdpsnd_main.h" + +typedef struct +{ + rdpsndDevicePlugin device; + + char* device_name; + pa_threaded_mainloop* mainloop; + pa_context* context; + pa_sample_spec sample_spec; + pa_stream* stream; + UINT32 latency; + UINT32 volume; + time_t reconnect_delay_seconds; + time_t reconnect_time; +} rdpsndPulsePlugin; + +static BOOL rdpsnd_check_pulse(rdpsndPulsePlugin* pulse, BOOL haveStream) +{ + BOOL rc = TRUE; + WINPR_ASSERT(pulse); + + if (!pulse->context) + { + WLog_WARN(TAG, "pulse->context=%p", pulse->context); + rc = FALSE; + } + + if (haveStream) + { + if (!pulse->stream) + { + WLog_WARN(TAG, "pulse->stream=%p", pulse->stream); + rc = FALSE; + } + } + + if (!pulse->mainloop) + { + WLog_WARN(TAG, "pulse->mainloop=%p", pulse->mainloop); + rc = FALSE; + } + + return rc; +} + +static BOOL rdpsnd_pulse_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format); + +static void rdpsnd_pulse_get_sink_info(pa_context* c, const pa_sink_info* i, int eol, + void* userdata) +{ + UINT16 dwVolumeLeft = ((50 * 0xFFFF) / 100); /* 50% */ + UINT16 dwVolumeRight = ((50 * 0xFFFF) / 100); /* 50% */ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)userdata; + + WINPR_ASSERT(c); + if (!rdpsnd_check_pulse(pulse, FALSE) || !i) + return; + + for (uint8_t x = 0; x < i->volume.channels; x++) + { + pa_volume_t volume = i->volume.values[x]; + + if (volume >= PA_VOLUME_NORM) + volume = PA_VOLUME_NORM - 1; + + switch (x) + { + case 0: + dwVolumeLeft = (UINT16)volume; + break; + + case 1: + dwVolumeRight = (UINT16)volume; + break; + + default: + break; + } + } + + pulse->volume = ((UINT32)dwVolumeLeft << 16U) | dwVolumeRight; +} + +static void rdpsnd_pulse_context_state_callback(pa_context* context, void* userdata) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)userdata; + + WINPR_ASSERT(context); + WINPR_ASSERT(pulse); + + pa_context_state_t state = pa_context_get_state(context); + + switch (state) + { + case PA_CONTEXT_READY: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + case PA_CONTEXT_FAILED: + // Destroy context now, create new one for next connection attempt + pa_context_unref(pulse->context); + pulse->context = NULL; + if (pulse->reconnect_delay_seconds >= 0) + pulse->reconnect_time = time(NULL) + pulse->reconnect_delay_seconds; + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + case PA_CONTEXT_TERMINATED: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + default: + break; + } +} + +static BOOL rdpsnd_pulse_connect(rdpsndDevicePlugin* device) +{ + BOOL rc = 0; + pa_operation* o = NULL; + pa_context_state_t state = PA_CONTEXT_FAILED; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!rdpsnd_check_pulse(pulse, FALSE)) + return FALSE; + + pa_threaded_mainloop_lock(pulse->mainloop); + + if (pa_context_connect(pulse->context, NULL, 0, NULL) < 0) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + return FALSE; + } + + for (;;) + { + state = pa_context_get_state(pulse->context); + + if (state == PA_CONTEXT_READY) + break; + + if (!PA_CONTEXT_IS_GOOD(state)) + { + break; + } + + pa_threaded_mainloop_wait(pulse->mainloop); + } + + o = pa_context_get_sink_info_by_index(pulse->context, 0, rdpsnd_pulse_get_sink_info, pulse); + + if (o) + pa_operation_unref(o); + + if (state == PA_CONTEXT_READY) + { + rc = TRUE; + } + else + { + pa_context_disconnect(pulse->context); + rc = FALSE; + } + + pa_threaded_mainloop_unlock(pulse->mainloop); + return rc; +} + +static void rdpsnd_pulse_stream_success_callback(pa_stream* stream, int success, void* userdata) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)userdata; + + if (!rdpsnd_check_pulse(pulse, TRUE)) + return; + + pa_threaded_mainloop_signal(pulse->mainloop, 0); +} + +static void rdpsnd_pulse_wait_for_operation(rdpsndPulsePlugin* pulse, pa_operation* operation) +{ + if (!rdpsnd_check_pulse(pulse, TRUE)) + return; + + if (!operation) + return; + + while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING) + { + pa_threaded_mainloop_wait(pulse->mainloop); + } + + pa_operation_unref(operation); +} + +static void rdpsnd_pulse_stream_state_callback(pa_stream* stream, void* userdata) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)userdata; + + WINPR_ASSERT(stream); + if (!rdpsnd_check_pulse(pulse, TRUE)) + return; + + pa_stream_state_t state = pa_stream_get_state(stream); + + switch (state) + { + case PA_STREAM_READY: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + // Stream object is about to be destroyed, clean up our pointer + pulse->stream = NULL; + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + default: + break; + } +} + +static void rdpsnd_pulse_stream_request_callback(pa_stream* stream, size_t length, void* userdata) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)userdata; + + WINPR_ASSERT(stream); + if (!rdpsnd_check_pulse(pulse, TRUE)) + return; + + pa_threaded_mainloop_signal(pulse->mainloop, 0); +} + +static void rdpsnd_pulse_close(rdpsndDevicePlugin* device) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + WINPR_ASSERT(pulse); + + if (!rdpsnd_check_pulse(pulse, FALSE)) + return; + + pa_threaded_mainloop_lock(pulse->mainloop); + if (pulse->stream) + { + rdpsnd_pulse_wait_for_operation( + pulse, pa_stream_drain(pulse->stream, rdpsnd_pulse_stream_success_callback, pulse)); + pa_stream_disconnect(pulse->stream); + pa_stream_unref(pulse->stream); + pulse->stream = NULL; + } + pa_threaded_mainloop_unlock(pulse->mainloop); +} + +static BOOL rdpsnd_pulse_set_format_spec(rdpsndPulsePlugin* pulse, const AUDIO_FORMAT* format) +{ + pa_sample_spec sample_spec = { 0 }; + + WINPR_ASSERT(format); + + if (!rdpsnd_check_pulse(pulse, FALSE)) + return FALSE; + + if (!rdpsnd_pulse_format_supported(&pulse->device, format)) + return FALSE; + + sample_spec.rate = format->nSamplesPerSec; + sample_spec.channels = format->nChannels; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + switch (format->wBitsPerSample) + { + case 8: + sample_spec.format = PA_SAMPLE_U8; + break; + + case 16: + sample_spec.format = PA_SAMPLE_S16LE; + break; + + default: + return FALSE; + } + + break; + + case WAVE_FORMAT_ALAW: + sample_spec.format = PA_SAMPLE_ALAW; + break; + + case WAVE_FORMAT_MULAW: + sample_spec.format = PA_SAMPLE_ULAW; + break; + + default: + return FALSE; + } + + pulse->sample_spec = sample_spec; + return TRUE; +} + +static BOOL rdpsnd_pulse_context_connect(rdpsndDevicePlugin* device) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + pulse->context = pa_context_new(pa_threaded_mainloop_get_api(pulse->mainloop), "freerdp"); + + if (!pulse->context) + return FALSE; + + pa_context_set_state_callback(pulse->context, rdpsnd_pulse_context_state_callback, pulse); + + if (!rdpsnd_pulse_connect((rdpsndDevicePlugin*)pulse)) + return FALSE; + + return TRUE; +} + +static BOOL rdpsnd_pulse_open_stream(rdpsndDevicePlugin* device) +{ + pa_stream_state_t state = PA_STREAM_FAILED; + pa_stream_flags_t flags = PA_STREAM_NOFLAGS; + pa_buffer_attr buffer_attr = { 0 }; + char ss[PA_SAMPLE_SPEC_SNPRINT_MAX] = { 0 }; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (pa_sample_spec_valid(&pulse->sample_spec) == 0) + { + pa_sample_spec_snprint(ss, sizeof(ss), &pulse->sample_spec); + return FALSE; + } + + pa_threaded_mainloop_lock(pulse->mainloop); + if (!pulse->context) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + if (pulse->reconnect_delay_seconds >= 0 && time(NULL) - pulse->reconnect_time >= 0) + rdpsnd_pulse_context_connect(device); + pa_threaded_mainloop_lock(pulse->mainloop); + } + + if (!rdpsnd_check_pulse(pulse, FALSE)) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + return FALSE; + } + + pulse->stream = pa_stream_new(pulse->context, "freerdp", &pulse->sample_spec, NULL); + + if (!pulse->stream) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + return FALSE; + } + + /* register essential callbacks */ + pa_stream_set_state_callback(pulse->stream, rdpsnd_pulse_stream_state_callback, pulse); + pa_stream_set_write_callback(pulse->stream, rdpsnd_pulse_stream_request_callback, pulse); + flags = PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE; + + if (pulse->latency > 0) + { + buffer_attr.maxlength = UINT32_MAX; + buffer_attr.tlength = pa_usec_to_bytes(pulse->latency * 1000, &pulse->sample_spec); + buffer_attr.prebuf = UINT32_MAX; + buffer_attr.minreq = UINT32_MAX; + buffer_attr.fragsize = UINT32_MAX; + flags |= PA_STREAM_ADJUST_LATENCY; + } + + if (pa_stream_connect_playback(pulse->stream, pulse->device_name, + pulse->latency > 0 ? &buffer_attr : NULL, flags, NULL, NULL) < 0) + { + WLog_ERR(TAG, "error connecting playback stream"); + pa_stream_unref(pulse->stream); + pulse->stream = NULL; + pa_threaded_mainloop_unlock(pulse->mainloop); + return FALSE; + } + + for (;;) + { + state = pa_stream_get_state(pulse->stream); + + if (state == PA_STREAM_READY) + break; + + if (!PA_STREAM_IS_GOOD(state)) + { + break; + } + + pa_threaded_mainloop_wait(pulse->mainloop); + } + + pa_threaded_mainloop_unlock(pulse->mainloop); + + if (state == PA_STREAM_READY) + return TRUE; + + rdpsnd_pulse_close(device); + return FALSE; +} + +static BOOL rdpsnd_pulse_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + WINPR_ASSERT(format); + + if (!rdpsnd_check_pulse(pulse, FALSE)) + return TRUE; + + if (!rdpsnd_pulse_set_format_spec(pulse, format)) + return FALSE; + + pulse->latency = latency; + + return rdpsnd_pulse_open_stream(device); +} + +static void rdpsnd_pulse_free(rdpsndDevicePlugin* device) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!pulse) + return; + + rdpsnd_pulse_close(device); + + 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->device_name); + free(pulse); +} + +static BOOL rdpsnd_pulse_default_format(rdpsndDevicePlugin* device, const AUDIO_FORMAT* desired, + AUDIO_FORMAT* defaultFormat) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!pulse || !defaultFormat) + return FALSE; + + *defaultFormat = *desired; + defaultFormat->data = NULL; + defaultFormat->cbSize = 0; + defaultFormat->wFormatTag = WAVE_FORMAT_PCM; + if ((defaultFormat->nChannels < 1) || (defaultFormat->nChannels > PA_CHANNELS_MAX)) + defaultFormat->nChannels = 2; + if ((defaultFormat->nSamplesPerSec < 1) || (defaultFormat->nSamplesPerSec > PA_RATE_MAX)) + defaultFormat->nSamplesPerSec = 44100; + if ((defaultFormat->wBitsPerSample != 8) && (defaultFormat->wBitsPerSample != 16)) + defaultFormat->wBitsPerSample = 16; + + defaultFormat->nBlockAlign = defaultFormat->nChannels * defaultFormat->wBitsPerSample / 8; + defaultFormat->nAvgBytesPerSec = defaultFormat->nBlockAlign * defaultFormat->nSamplesPerSec; + return TRUE; +} + +BOOL rdpsnd_pulse_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + WINPR_ASSERT(device); + WINPR_ASSERT(format); + + 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: + break; + } + + return FALSE; +} + +static UINT32 rdpsnd_pulse_get_volume(rdpsndDevicePlugin* device) +{ + pa_operation* o = NULL; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!rdpsnd_check_pulse(pulse, FALSE)) + return 0; + + pa_threaded_mainloop_lock(pulse->mainloop); + o = pa_context_get_sink_info_by_index(pulse->context, 0, rdpsnd_pulse_get_sink_info, pulse); + if (o) + pa_operation_unref(o); + pa_threaded_mainloop_unlock(pulse->mainloop); + return pulse->volume; +} + +static void rdpsnd_set_volume_success_cb(pa_context* c, int success, void* userdata) +{ + rdpsndPulsePlugin* pulse = userdata; + + if (!rdpsnd_check_pulse(pulse, TRUE)) + return; + WINPR_ASSERT(c); + + WLog_INFO(TAG, "%d", success); +} + +static BOOL rdpsnd_pulse_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + pa_cvolume cv = { 0 }; + pa_volume_t left = 0; + pa_volume_t right = 0; + pa_operation* operation = NULL; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!rdpsnd_check_pulse(pulse, TRUE)) + { + WLog_WARN(TAG, "%s called before pulse backend was initialized"); + return FALSE; + } + + left = (pa_volume_t)(value & 0xFFFF); + right = (pa_volume_t)((value >> 16) & 0xFFFF); + pa_cvolume_init(&cv); + cv.channels = 2; + cv.values[0] = PA_VOLUME_MUTED + (left * (PA_VOLUME_NORM - PA_VOLUME_MUTED)) / PA_VOLUME_NORM; + cv.values[1] = PA_VOLUME_MUTED + (right * (PA_VOLUME_NORM - PA_VOLUME_MUTED)) / PA_VOLUME_NORM; + pa_threaded_mainloop_lock(pulse->mainloop); + operation = pa_context_set_sink_input_volume(pulse->context, pa_stream_get_index(pulse->stream), + &cv, rdpsnd_set_volume_success_cb, pulse); + + if (operation) + pa_operation_unref(operation); + + pa_threaded_mainloop_unlock(pulse->mainloop); + return TRUE; +} + +static UINT rdpsnd_pulse_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + size_t length = 0; + void* pa_data = NULL; + int status = 0; + pa_usec_t latency = 0; + int negative = 0; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!data) + return 0; + + pa_threaded_mainloop_lock(pulse->mainloop); + + if (!rdpsnd_check_pulse(pulse, TRUE)) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + // Discard this playback request and just attempt to reconnect the stream + WLog_DBG(TAG, "reconnecting playback stream"); + rdpsnd_pulse_open_stream(device); + return 0; + } + + while (size > 0) + { + length = size; + + status = pa_stream_begin_write(pulse->stream, &pa_data, &length); + + if (status < 0) + break; + + memcpy(pa_data, data, length); + + status = pa_stream_write(pulse->stream, pa_data, length, NULL, 0LL, PA_SEEK_RELATIVE); + + if (status < 0) + { + break; + } + + data += length; + size -= length; + } + + if (pa_stream_get_latency(pulse->stream, &latency, &negative) != 0) + latency = 0; + + pa_threaded_mainloop_unlock(pulse->mainloop); + return latency / 1000; +} + +static UINT rdpsnd_pulse_parse_addin_args(rdpsndDevicePlugin* device, const ADDIN_ARGV* args) +{ + int status = 0; + DWORD flags = 0; + const COMMAND_LINE_ARGUMENT_A* arg = NULL; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + COMMAND_LINE_ARGUMENT_A rdpsnd_pulse_args[] = { + { "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", NULL, NULL, -1, NULL, "device" }, + { "reconnect_delay_seconds", COMMAND_LINE_VALUE_REQUIRED, "<reconnect_delay_seconds>", NULL, + NULL, -1, NULL, "reconnect_delay_seconds" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } + }; + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + + WINPR_ASSERT(pulse); + WINPR_ASSERT(args); + + status = CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_pulse_args, flags, pulse, + NULL, NULL); + + if (status < 0) + return ERROR_INVALID_DATA; + + arg = rdpsnd_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) + return ERROR_OUTOFMEMORY; + } + CommandLineSwitchCase(arg, "reconnect_delay_seconds") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val > INT32_MAX)) + return ERROR_INVALID_DATA; + + pulse->reconnect_delay_seconds = val; + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +FREERDP_ENTRY_POINT(UINT pulse_freerdp_rdpsnd_client_subsystem_entry( + PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + const ADDIN_ARGV* args = NULL; + rdpsndPulsePlugin* pulse = NULL; + UINT ret = 0; + + WINPR_ASSERT(pEntryPoints); + + pulse = (rdpsndPulsePlugin*)calloc(1, sizeof(rdpsndPulsePlugin)); + + if (!pulse) + return CHANNEL_RC_NO_MEMORY; + + pulse->device.Open = rdpsnd_pulse_open; + pulse->device.FormatSupported = rdpsnd_pulse_format_supported; + pulse->device.GetVolume = rdpsnd_pulse_get_volume; + pulse->device.SetVolume = rdpsnd_pulse_set_volume; + pulse->device.Play = rdpsnd_pulse_play; + pulse->device.Close = rdpsnd_pulse_close; + pulse->device.Free = rdpsnd_pulse_free; + pulse->device.DefaultFormat = rdpsnd_pulse_default_format; + args = pEntryPoints->args; + + if (args->argc > 1) + { + ret = rdpsnd_pulse_parse_addin_args(&pulse->device, args); + + if (ret != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "error parsing arguments"); + goto error; + } + } + pulse->reconnect_delay_seconds = 5; + pulse->reconnect_time = time(NULL); + + ret = CHANNEL_RC_NO_MEMORY; + pulse->mainloop = pa_threaded_mainloop_new(); + + if (!pulse->mainloop) + goto error; + + pa_threaded_mainloop_lock(pulse->mainloop); + + if (pa_threaded_mainloop_start(pulse->mainloop) < 0) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + return FALSE; + } + + pa_threaded_mainloop_unlock(pulse->mainloop); + + if (!rdpsnd_pulse_context_connect((rdpsndDevicePlugin*)pulse)) + goto error; + + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)pulse); + return CHANNEL_RC_OK; +error: + rdpsnd_pulse_free((rdpsndDevicePlugin*)pulse); + return ret; +} diff --git a/channels/rdpsnd/client/rdpsnd_main.c b/channels/rdpsnd/client/rdpsnd_main.c new file mode 100644 index 0000000..6c66d33 --- /dev/null +++ b/channels/rdpsnd/client/rdpsnd_main.c @@ -0,0 +1,1850 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2012-2013 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * Copyright 2016 David PHAM-VAN <d.phamvan@inuvika.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> + +#ifndef _WIN32 +#include <sys/time.h> +#include <signal.h> +#endif + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> + +#include <winpr/crt.h> +#include <winpr/assert.h> +#include <winpr/wlog.h> +#include <winpr/stream.h> +#include <winpr/cmdline.h> +#include <winpr/sysinfo.h> +#include <winpr/collections.h> + +#include <freerdp/types.h> +#include <freerdp/addin.h> +#include <freerdp/freerdp.h> +#include <freerdp/codec/dsp.h> +#include <freerdp/client/channels.h> + +#include "rdpsnd_common.h" +#include "rdpsnd_main.h" + +struct rdpsnd_plugin +{ + IWTSPlugin iface; + IWTSListener* listener; + GENERIC_LISTENER_CALLBACK* listener_callback; + + CHANNEL_DEF channelDef; + CHANNEL_ENTRY_POINTS_FREERDP_EX channelEntryPoints; + + wStreamPool* pool; + wStream* data_in; + + void* InitHandle; + DWORD OpenHandle; + + wLog* log; + + BYTE cBlockNo; + UINT16 wQualityMode; + UINT16 wCurrentFormatNo; + + AUDIO_FORMAT* ServerFormats; + UINT16 NumberOfServerFormats; + + AUDIO_FORMAT* ClientFormats; + UINT16 NumberOfClientFormats; + + BOOL attached; + BOOL connected; + BOOL dynamic; + + BOOL expectingWave; + BYTE waveData[4]; + UINT16 waveDataSize; + UINT16 wTimeStamp; + UINT64 wArrivalTime; + + UINT32 latency; + BOOL isOpen; + AUDIO_FORMAT* fixed_format; + + UINT32 startPlayTime; + size_t totalPlaySize; + + char* subsystem; + char* device_name; + + /* Device plugin */ + rdpsndDevicePlugin* device; + rdpContext* rdpcontext; + + FREERDP_DSP_CONTEXT* dsp_context; + + HANDLE thread; + wMessageQueue* queue; + BOOL initialized; + + UINT16 wVersion; + UINT32 volume; + BOOL applyVolume; + + size_t references; + BOOL OnOpenCalled; + BOOL async; +}; + +static const char* rdpsnd_is_dyn_str(BOOL dynamic) +{ + if (dynamic) + return "[dynamic]"; + return "[static]"; +} + +static void rdpsnd_virtual_channel_event_terminated(rdpsndPlugin* rdpsnd); + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_virtual_channel_write(rdpsndPlugin* rdpsnd, wStream* s); + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_send_quality_mode_pdu(rdpsndPlugin* rdpsnd) +{ + wStream* pdu = NULL; + WINPR_ASSERT(rdpsnd); + pdu = Stream_New(NULL, 8); + + if (!pdu) + { + WLog_ERR(TAG, "%s Stream_New failed!", rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT8(pdu, SNDC_QUALITYMODE); /* msgType */ + Stream_Write_UINT8(pdu, 0); /* bPad */ + Stream_Write_UINT16(pdu, 4); /* BodySize */ + Stream_Write_UINT16(pdu, rdpsnd->wQualityMode); /* wQualityMode */ + Stream_Write_UINT16(pdu, 0); /* Reserved */ + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s QualityMode: %" PRIu16 "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->wQualityMode); + return rdpsnd_virtual_channel_write(rdpsnd, pdu); +} + +static void rdpsnd_select_supported_audio_formats(rdpsndPlugin* rdpsnd) +{ + WINPR_ASSERT(rdpsnd); + audio_formats_free(rdpsnd->ClientFormats, rdpsnd->NumberOfClientFormats); + rdpsnd->NumberOfClientFormats = 0; + rdpsnd->ClientFormats = NULL; + + if (!rdpsnd->NumberOfServerFormats) + return; + + rdpsnd->ClientFormats = audio_formats_new(rdpsnd->NumberOfServerFormats); + + if (!rdpsnd->ClientFormats || !rdpsnd->device) + return; + + for (UINT16 index = 0; index < rdpsnd->NumberOfServerFormats; index++) + { + const AUDIO_FORMAT* serverFormat = &rdpsnd->ServerFormats[index]; + + if (!audio_format_compatible(rdpsnd->fixed_format, serverFormat)) + continue; + + WINPR_ASSERT(rdpsnd->device->FormatSupported); + if (freerdp_dsp_supports_format(serverFormat, FALSE) || + rdpsnd->device->FormatSupported(rdpsnd->device, serverFormat)) + { + AUDIO_FORMAT* clientFormat = &rdpsnd->ClientFormats[rdpsnd->NumberOfClientFormats++]; + audio_format_copy(serverFormat, clientFormat); + } + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_send_client_audio_formats(rdpsndPlugin* rdpsnd) +{ + wStream* pdu = NULL; + UINT16 length = 0; + UINT32 dwVolume = 0; + UINT16 wNumberOfFormats = 0; + WINPR_ASSERT(rdpsnd); + + if (!rdpsnd->device || (!rdpsnd->dynamic && (rdpsnd->OpenHandle == 0))) + return CHANNEL_RC_INITIALIZATION_ERROR; + + dwVolume = IFCALLRESULT(0, rdpsnd->device->GetVolume, rdpsnd->device); + wNumberOfFormats = rdpsnd->NumberOfClientFormats; + length = 4 + 20; + + for (UINT16 index = 0; index < wNumberOfFormats; index++) + length += (18 + rdpsnd->ClientFormats[index].cbSize); + + pdu = Stream_New(NULL, length); + + if (!pdu) + { + WLog_ERR(TAG, "%s Stream_New failed!", rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT8(pdu, SNDC_FORMATS); /* msgType */ + Stream_Write_UINT8(pdu, 0); /* bPad */ + Stream_Write_UINT16(pdu, length - 4); /* BodySize */ + Stream_Write_UINT32(pdu, TSSNDCAPS_ALIVE | TSSNDCAPS_VOLUME); /* dwFlags */ + Stream_Write_UINT32(pdu, dwVolume); /* dwVolume */ + Stream_Write_UINT32(pdu, 0); /* dwPitch */ + Stream_Write_UINT16(pdu, 0); /* wDGramPort */ + Stream_Write_UINT16(pdu, wNumberOfFormats); /* wNumberOfFormats */ + Stream_Write_UINT8(pdu, 0); /* cLastBlockConfirmed */ + Stream_Write_UINT16(pdu, CHANNEL_VERSION_WIN_MAX); /* wVersion */ + Stream_Write_UINT8(pdu, 0); /* bPad */ + + for (UINT16 index = 0; index < wNumberOfFormats; index++) + { + const AUDIO_FORMAT* clientFormat = &rdpsnd->ClientFormats[index]; + + if (!audio_format_write(pdu, clientFormat)) + { + Stream_Free(pdu, TRUE); + return ERROR_INTERNAL_ERROR; + } + } + + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Client Audio Formats", + rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return rdpsnd_virtual_channel_write(rdpsnd, pdu); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_recv_server_audio_formats_pdu(rdpsndPlugin* rdpsnd, wStream* s) +{ + UINT16 wNumberOfFormats = 0; + UINT ret = ERROR_BAD_LENGTH; + + WINPR_ASSERT(rdpsnd); + audio_formats_free(rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats); + rdpsnd->NumberOfServerFormats = 0; + rdpsnd->ServerFormats = NULL; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 30)) + return ERROR_BAD_LENGTH; + + /* http://msdn.microsoft.com/en-us/library/cc240956.aspx */ + Stream_Seek_UINT32(s); /* dwFlags */ + Stream_Seek_UINT32(s); /* dwVolume */ + Stream_Seek_UINT32(s); /* dwPitch */ + Stream_Seek_UINT16(s); /* wDGramPort */ + Stream_Read_UINT16(s, wNumberOfFormats); + Stream_Read_UINT8(s, rdpsnd->cBlockNo); /* cLastBlockConfirmed */ + Stream_Read_UINT16(s, rdpsnd->wVersion); /* wVersion */ + Stream_Seek_UINT8(s); /* bPad */ + rdpsnd->NumberOfServerFormats = wNumberOfFormats; + + if (!Stream_CheckAndLogRequiredLengthOfSize(TAG, s, wNumberOfFormats, 14ull)) + return ERROR_BAD_LENGTH; + + if (rdpsnd->NumberOfServerFormats > 0) + { + rdpsnd->ServerFormats = audio_formats_new(wNumberOfFormats); + + if (!rdpsnd->ServerFormats) + return CHANNEL_RC_NO_MEMORY; + + for (UINT16 index = 0; index < wNumberOfFormats; index++) + { + AUDIO_FORMAT* format = &rdpsnd->ServerFormats[index]; + + if (!audio_format_read(s, format)) + goto out_fail; + } + } + + WINPR_ASSERT(rdpsnd->device); + ret = IFCALLRESULT(CHANNEL_RC_OK, rdpsnd->device->ServerFormatAnnounce, rdpsnd->device, + rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats); + + rdpsnd_select_supported_audio_formats(rdpsnd); + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Server Audio Formats", + rdpsnd_is_dyn_str(rdpsnd->dynamic)); + ret = rdpsnd_send_client_audio_formats(rdpsnd); + + if (ret == CHANNEL_RC_OK) + { + if (rdpsnd->wVersion >= CHANNEL_VERSION_WIN_7) + ret = rdpsnd_send_quality_mode_pdu(rdpsnd); + } + + return ret; +out_fail: + audio_formats_free(rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats); + rdpsnd->ServerFormats = NULL; + rdpsnd->NumberOfServerFormats = 0; + return ret; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_send_training_confirm_pdu(rdpsndPlugin* rdpsnd, UINT16 wTimeStamp, + UINT16 wPackSize) +{ + wStream* pdu = NULL; + WINPR_ASSERT(rdpsnd); + pdu = Stream_New(NULL, 8); + + if (!pdu) + { + WLog_ERR(TAG, "%s Stream_New failed!", rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT8(pdu, SNDC_TRAINING); /* msgType */ + Stream_Write_UINT8(pdu, 0); /* bPad */ + Stream_Write_UINT16(pdu, 4); /* BodySize */ + Stream_Write_UINT16(pdu, wTimeStamp); + Stream_Write_UINT16(pdu, wPackSize); + WLog_Print(rdpsnd->log, WLOG_DEBUG, + "%s Training Response: wTimeStamp: %" PRIu16 " wPackSize: %" PRIu16 "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), wTimeStamp, wPackSize); + return rdpsnd_virtual_channel_write(rdpsnd, pdu); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_recv_training_pdu(rdpsndPlugin* rdpsnd, wStream* s) +{ + UINT16 wTimeStamp = 0; + UINT16 wPackSize = 0; + WINPR_ASSERT(rdpsnd); + WINPR_ASSERT(s); + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 4)) + return ERROR_BAD_LENGTH; + + Stream_Read_UINT16(s, wTimeStamp); + Stream_Read_UINT16(s, wPackSize); + WLog_Print(rdpsnd->log, WLOG_DEBUG, + "%s Training Request: wTimeStamp: %" PRIu16 " wPackSize: %" PRIu16 "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), wTimeStamp, wPackSize); + return rdpsnd_send_training_confirm_pdu(rdpsnd, wTimeStamp, wPackSize); +} + +static BOOL rdpsnd_apply_volume(rdpsndPlugin* rdpsnd) +{ + WINPR_ASSERT(rdpsnd); + + if (rdpsnd->isOpen && rdpsnd->applyVolume && rdpsnd->device) + { + BOOL rc = IFCALLRESULT(TRUE, rdpsnd->device->SetVolume, rdpsnd->device, rdpsnd->volume); + if (!rc) + return FALSE; + rdpsnd->applyVolume = FALSE; + } + return TRUE; +} + +static BOOL rdpsnd_ensure_device_is_open(rdpsndPlugin* rdpsnd, UINT32 wFormatNo, + const AUDIO_FORMAT* format) +{ + if (!rdpsnd) + return FALSE; + WINPR_ASSERT(format); + + if (!rdpsnd->isOpen || (wFormatNo != rdpsnd->wCurrentFormatNo)) + { + BOOL rc = 0; + BOOL supported = 0; + AUDIO_FORMAT deviceFormat = *format; + + IFCALL(rdpsnd->device->Close, rdpsnd->device); + supported = IFCALLRESULT(FALSE, rdpsnd->device->FormatSupported, rdpsnd->device, format); + + if (!supported) + { + if (!IFCALLRESULT(FALSE, rdpsnd->device->DefaultFormat, rdpsnd->device, format, + &deviceFormat)) + { + deviceFormat.wFormatTag = WAVE_FORMAT_PCM; + deviceFormat.wBitsPerSample = 16; + deviceFormat.cbSize = 0; + } + } + + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Opening device with format %s [backend %s]", + rdpsnd_is_dyn_str(rdpsnd->dynamic), + audio_format_get_tag_string(format->wFormatTag), + audio_format_get_tag_string(deviceFormat.wFormatTag)); + rc = IFCALLRESULT(FALSE, rdpsnd->device->Open, rdpsnd->device, &deviceFormat, + rdpsnd->latency); + + if (!rc) + return FALSE; + + if (!supported) + { + if (!freerdp_dsp_context_reset(rdpsnd->dsp_context, format, 0u)) + return FALSE; + } + + rdpsnd->isOpen = TRUE; + rdpsnd->wCurrentFormatNo = wFormatNo; + rdpsnd->startPlayTime = 0; + rdpsnd->totalPlaySize = 0; + } + + return rdpsnd_apply_volume(rdpsnd); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_recv_wave_info_pdu(rdpsndPlugin* rdpsnd, wStream* s, UINT16 BodySize) +{ + UINT16 wFormatNo = 0; + const AUDIO_FORMAT* format = NULL; + WINPR_ASSERT(rdpsnd); + WINPR_ASSERT(s); + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 12)) + return ERROR_BAD_LENGTH; + + rdpsnd->wArrivalTime = GetTickCount64(); + Stream_Read_UINT16(s, rdpsnd->wTimeStamp); + Stream_Read_UINT16(s, wFormatNo); + + if (wFormatNo >= rdpsnd->NumberOfClientFormats) + return ERROR_INVALID_DATA; + + Stream_Read_UINT8(s, rdpsnd->cBlockNo); + Stream_Seek(s, 3); /* bPad */ + Stream_Read(s, rdpsnd->waveData, 4); + rdpsnd->waveDataSize = BodySize - 8; + format = &rdpsnd->ClientFormats[wFormatNo]; + WLog_Print(rdpsnd->log, WLOG_DEBUG, + "%s WaveInfo: cBlockNo: %" PRIu8 " wFormatNo: %" PRIu16 " [%s]", + rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->cBlockNo, wFormatNo, + audio_format_get_tag_string(format->wFormatTag)); + + if (!rdpsnd_ensure_device_is_open(rdpsnd, wFormatNo, format)) + return ERROR_INTERNAL_ERROR; + + rdpsnd->expectingWave = TRUE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_send_wave_confirm_pdu(rdpsndPlugin* rdpsnd, UINT16 wTimeStamp, + BYTE cConfirmedBlockNo) +{ + wStream* pdu = NULL; + WINPR_ASSERT(rdpsnd); + pdu = Stream_New(NULL, 8); + + if (!pdu) + { + WLog_ERR(TAG, "%s Stream_New failed!", rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT8(pdu, SNDC_WAVECONFIRM); + Stream_Write_UINT8(pdu, 0); + Stream_Write_UINT16(pdu, 4); + Stream_Write_UINT16(pdu, wTimeStamp); + Stream_Write_UINT8(pdu, cConfirmedBlockNo); /* cConfirmedBlockNo */ + Stream_Write_UINT8(pdu, 0); /* bPad */ + return rdpsnd_virtual_channel_write(rdpsnd, pdu); +} + +static BOOL rdpsnd_detect_overrun(rdpsndPlugin* rdpsnd, const AUDIO_FORMAT* format, size_t size) +{ + UINT32 bpf = 0; + UINT32 now = 0; + UINT32 duration = 0; + UINT32 totalDuration = 0; + UINT32 remainingDuration = 0; + UINT32 maxDuration = 0; + + if (!rdpsnd || !format) + return FALSE; + + /* Older windows RDP servers do not limit the send buffer, which can + * cause quite a large amount of sound data buffered client side. + * If e.g. sound is paused server side the client will keep playing + * for a long time instead of pausing playback. + * + * To avoid this we check: + * + * 1. Is the sound sample received from a known format these servers + * support + * 2. If it is calculate the size of the client side sound buffer + * 3. If the buffer is too large silently drop the sample which will + * trigger a retransmit later on. + * + * This check must only be applied to these known formats, because + * with newer and other formats the sample size can not be calculated + * without decompressing the sample first. + */ + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + case WAVE_FORMAT_DVI_ADPCM: + case WAVE_FORMAT_ADPCM: + case WAVE_FORMAT_ALAW: + case WAVE_FORMAT_MULAW: + break; + case WAVE_FORMAT_MSG723: + case WAVE_FORMAT_GSM610: + case WAVE_FORMAT_AAC_MS: + default: + return FALSE; + } + + audio_format_print(WLog_Get(TAG), WLOG_DEBUG, format); + bpf = format->nChannels * format->wBitsPerSample * format->nSamplesPerSec / 8; + if (bpf == 0) + return FALSE; + + duration = (UINT32)(1000 * size / bpf); + totalDuration = (UINT32)(1000 * rdpsnd->totalPlaySize / bpf); + now = GetTickCountPrecise(); + if (rdpsnd->startPlayTime == 0) + { + rdpsnd->startPlayTime = now; + rdpsnd->totalPlaySize = size; + return FALSE; + } + else if (now - rdpsnd->startPlayTime > totalDuration + 10) + { + /* Buffer underrun */ + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Buffer underrun by %u ms", + rdpsnd_is_dyn_str(rdpsnd->dynamic), + (UINT)(now - rdpsnd->startPlayTime - totalDuration)); + rdpsnd->startPlayTime = now; + rdpsnd->totalPlaySize = size; + return FALSE; + } + else + { + /* Calculate remaining duration to be played */ + remainingDuration = totalDuration - (now - rdpsnd->startPlayTime); + + /* Maximum allow duration calculation */ + maxDuration = duration * 2 + rdpsnd->latency; + + if (remainingDuration + duration > maxDuration) + { + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Buffer overrun pending %u ms dropping %u ms", + rdpsnd_is_dyn_str(rdpsnd->dynamic), remainingDuration, duration); + return TRUE; + } + + rdpsnd->totalPlaySize += size; + return FALSE; + } +} + +static UINT rdpsnd_treat_wave(rdpsndPlugin* rdpsnd, wStream* s, size_t size) +{ + AUDIO_FORMAT* format = NULL; + UINT64 end = 0; + UINT64 diffMS = 0; + UINT64 ts = 0; + UINT latency = 0; + UINT error = 0; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, size)) + return ERROR_BAD_LENGTH; + + if (rdpsnd->wCurrentFormatNo >= rdpsnd->NumberOfClientFormats) + return ERROR_INTERNAL_ERROR; + + /* + * Send the first WaveConfirm PDU. The server side uses this to determine the + * network latency. + * See also [MS-RDPEA] 2.2.3.8 Wave Confirm PDU + */ + error = rdpsnd_send_wave_confirm_pdu(rdpsnd, rdpsnd->wTimeStamp, rdpsnd->cBlockNo); + if (error) + return error; + + const BYTE* data = Stream_ConstPointer(s); + format = &rdpsnd->ClientFormats[rdpsnd->wCurrentFormatNo]; + WLog_Print(rdpsnd->log, WLOG_DEBUG, + "%s Wave: cBlockNo: %" PRIu8 " wTimeStamp: %" PRIu16 ", size: %" PRIdz, + rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->cBlockNo, rdpsnd->wTimeStamp, size); + + if (rdpsnd->device && rdpsnd->attached && !rdpsnd_detect_overrun(rdpsnd, format, size)) + { + UINT status = CHANNEL_RC_OK; + wStream* pcmData = StreamPool_Take(rdpsnd->pool, 4096); + + if (rdpsnd->device->FormatSupported(rdpsnd->device, format)) + { + if (rdpsnd->device->PlayEx) + latency = rdpsnd->device->PlayEx(rdpsnd->device, format, data, size); + else + latency = IFCALLRESULT(0, rdpsnd->device->Play, rdpsnd->device, data, size); + } + else if (freerdp_dsp_decode(rdpsnd->dsp_context, format, data, size, pcmData)) + { + Stream_SealLength(pcmData); + + if (rdpsnd->device->PlayEx) + latency = rdpsnd->device->PlayEx(rdpsnd->device, format, Stream_Buffer(pcmData), + Stream_Length(pcmData)); + else + latency = IFCALLRESULT(0, rdpsnd->device->Play, rdpsnd->device, + Stream_Buffer(pcmData), Stream_Length(pcmData)); + } + else + status = ERROR_INTERNAL_ERROR; + + Stream_Release(pcmData); + + if (status != CHANNEL_RC_OK) + return status; + } + + end = GetTickCount64(); + diffMS = end - rdpsnd->wArrivalTime + latency; + ts = (rdpsnd->wTimeStamp + diffMS) % UINT16_MAX; + + /* + * Send the second WaveConfirm PDU. With the first WaveConfirm PDU, + * the server side uses this second WaveConfirm PDU to determine the actual + * render latency. + */ + return rdpsnd_send_wave_confirm_pdu(rdpsnd, (UINT16)ts, rdpsnd->cBlockNo); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_recv_wave_pdu(rdpsndPlugin* rdpsnd, wStream* s) +{ + rdpsnd->expectingWave = FALSE; + + /** + * The Wave PDU is a special case: it is always sent after a Wave Info PDU, + * and we do not process its header. Instead, the header is pad that needs + * to be filled with the first four bytes of the audio sample data sent as + * part of the preceding Wave Info PDU. + */ + if (!Stream_CheckAndLogRequiredLength(TAG, s, 4)) + return ERROR_INVALID_DATA; + + CopyMemory(Stream_Buffer(s), rdpsnd->waveData, 4); + return rdpsnd_treat_wave(rdpsnd, s, rdpsnd->waveDataSize); +} + +static UINT rdpsnd_recv_wave2_pdu(rdpsndPlugin* rdpsnd, wStream* s, UINT16 BodySize) +{ + UINT16 wFormatNo = 0; + AUDIO_FORMAT* format = NULL; + UINT32 dwAudioTimeStamp = 0; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 12)) + return ERROR_BAD_LENGTH; + + Stream_Read_UINT16(s, rdpsnd->wTimeStamp); + Stream_Read_UINT16(s, wFormatNo); + Stream_Read_UINT8(s, rdpsnd->cBlockNo); + Stream_Seek(s, 3); /* bPad */ + Stream_Read_UINT32(s, dwAudioTimeStamp); + if (wFormatNo >= rdpsnd->NumberOfClientFormats) + return ERROR_INVALID_DATA; + format = &rdpsnd->ClientFormats[wFormatNo]; + rdpsnd->waveDataSize = BodySize - 12; + rdpsnd->wArrivalTime = GetTickCount64(); + WLog_Print(rdpsnd->log, WLOG_DEBUG, + "%s Wave2PDU: cBlockNo: %" PRIu8 " wFormatNo: %" PRIu16 " [%s] , align=%hu", + rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->cBlockNo, wFormatNo, + audio_format_get_tag_string(format->wFormatTag), format->nBlockAlign); + + if (!rdpsnd_ensure_device_is_open(rdpsnd, wFormatNo, format)) + return ERROR_INTERNAL_ERROR; + + return rdpsnd_treat_wave(rdpsnd, s, rdpsnd->waveDataSize); +} + +static void rdpsnd_recv_close_pdu(rdpsndPlugin* rdpsnd) +{ + if (rdpsnd->isOpen) + { + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Closing device", + rdpsnd_is_dyn_str(rdpsnd->dynamic)); + } + else + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Device already closed", + rdpsnd_is_dyn_str(rdpsnd->dynamic)); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_recv_volume_pdu(rdpsndPlugin* rdpsnd, wStream* s) +{ + BOOL rc = TRUE; + UINT32 dwVolume = 0; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 4)) + return ERROR_BAD_LENGTH; + + Stream_Read_UINT32(s, dwVolume); + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Volume: 0x%08" PRIX32 "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), dwVolume); + + rdpsnd->volume = dwVolume; + rdpsnd->applyVolume = TRUE; + rc = rdpsnd_apply_volume(rdpsnd); + + if (!rc) + { + WLog_ERR(TAG, "%s error setting volume", rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_recv_pdu(rdpsndPlugin* rdpsnd, wStream* s) +{ + BYTE msgType = 0; + UINT16 BodySize = 0; + UINT status = CHANNEL_RC_OK; + + if (rdpsnd->expectingWave) + { + status = rdpsnd_recv_wave_pdu(rdpsnd, s); + goto out; + } + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 4)) + { + status = ERROR_BAD_LENGTH; + goto out; + } + + Stream_Read_UINT8(s, msgType); /* msgType */ + Stream_Seek_UINT8(s); /* bPad */ + Stream_Read_UINT16(s, BodySize); + + switch (msgType) + { + case SNDC_FORMATS: + status = rdpsnd_recv_server_audio_formats_pdu(rdpsnd, s); + break; + + case SNDC_TRAINING: + status = rdpsnd_recv_training_pdu(rdpsnd, s); + break; + + case SNDC_WAVE: + status = rdpsnd_recv_wave_info_pdu(rdpsnd, s, BodySize); + break; + + case SNDC_CLOSE: + rdpsnd_recv_close_pdu(rdpsnd); + break; + + case SNDC_SETVOLUME: + status = rdpsnd_recv_volume_pdu(rdpsnd, s); + break; + + case SNDC_WAVE2: + status = rdpsnd_recv_wave2_pdu(rdpsnd, s, BodySize); + break; + + default: + WLog_ERR(TAG, "%s unknown msgType %" PRIu8 "", rdpsnd_is_dyn_str(rdpsnd->dynamic), + msgType); + break; + } + +out: + Stream_Release(s); + return status; +} + +static void rdpsnd_register_device_plugin(rdpsndPlugin* rdpsnd, rdpsndDevicePlugin* device) +{ + if (rdpsnd->device) + { + WLog_ERR(TAG, "%s existing device, abort.", rdpsnd_is_dyn_str(FALSE)); + return; + } + + rdpsnd->device = device; + device->rdpsnd = rdpsnd; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_load_device_plugin(rdpsndPlugin* rdpsnd, const char* name, + const ADDIN_ARGV* args) +{ + PFREERDP_RDPSND_DEVICE_ENTRY entry = NULL; + FREERDP_RDPSND_DEVICE_ENTRY_POINTS entryPoints; + UINT error = 0; + DWORD flags = FREERDP_ADDIN_CHANNEL_STATIC | FREERDP_ADDIN_CHANNEL_ENTRYEX; + if (rdpsnd->dynamic) + flags = FREERDP_ADDIN_CHANNEL_DYNAMIC; + entry = (PFREERDP_RDPSND_DEVICE_ENTRY)freerdp_load_channel_addin_entry(RDPSND_CHANNEL_NAME, + name, NULL, flags); + + if (!entry) + return ERROR_INTERNAL_ERROR; + + entryPoints.rdpsnd = rdpsnd; + entryPoints.pRegisterRdpsndDevice = rdpsnd_register_device_plugin; + entryPoints.args = args; + + if ((error = entry(&entryPoints))) + WLog_ERR(TAG, "%s %s entry returns error %" PRIu32 "", rdpsnd_is_dyn_str(rdpsnd->dynamic), + name, error); + + WLog_INFO(TAG, "%s Loaded %s backend for rdpsnd", rdpsnd_is_dyn_str(rdpsnd->dynamic), name); + return error; +} + +static BOOL rdpsnd_set_subsystem(rdpsndPlugin* rdpsnd, const char* subsystem) +{ + free(rdpsnd->subsystem); + rdpsnd->subsystem = _strdup(subsystem); + return (rdpsnd->subsystem != NULL); +} + +static BOOL rdpsnd_set_device_name(rdpsndPlugin* rdpsnd, const char* device_name) +{ + free(rdpsnd->device_name); + rdpsnd->device_name = _strdup(device_name); + return (rdpsnd->device_name != NULL); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_process_addin_args(rdpsndPlugin* rdpsnd, const ADDIN_ARGV* args) +{ + int status = 0; + DWORD flags = 0; + const COMMAND_LINE_ARGUMENT_A* arg = NULL; + COMMAND_LINE_ARGUMENT_A rdpsnd_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" }, + { "latency", COMMAND_LINE_VALUE_REQUIRED, "<latency>", NULL, NULL, -1, NULL, "latency" }, + { "quality", COMMAND_LINE_VALUE_REQUIRED, "<quality mode>", NULL, NULL, -1, NULL, + "quality mode" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } + }; + rdpsnd->wQualityMode = HIGH_QUALITY; /* default quality mode */ + + if (args->argc > 1) + { + flags = COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON; + status = CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_args, flags, rdpsnd, + NULL, NULL); + + if (status < 0) + return CHANNEL_RC_INITIALIZATION_ERROR; + + arg = rdpsnd_args; + errno = 0; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "sys") + { + if (!rdpsnd_set_subsystem(rdpsnd, arg->Value)) + return CHANNEL_RC_NO_MEMORY; + } + CommandLineSwitchCase(arg, "dev") + { + if (!rdpsnd_set_device_name(rdpsnd, arg->Value)) + return CHANNEL_RC_NO_MEMORY; + } + CommandLineSwitchCase(arg, "format") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val > UINT16_MAX)) + return CHANNEL_RC_INITIALIZATION_ERROR; + + rdpsnd->fixed_format->wFormatTag = (UINT16)val; + } + CommandLineSwitchCase(arg, "rate") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val > UINT32_MAX)) + return CHANNEL_RC_INITIALIZATION_ERROR; + + rdpsnd->fixed_format->nSamplesPerSec = val; + } + CommandLineSwitchCase(arg, "channel") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val > UINT16_MAX)) + return CHANNEL_RC_INITIALIZATION_ERROR; + + rdpsnd->fixed_format->nChannels = (UINT16)val; + } + CommandLineSwitchCase(arg, "latency") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val > INT32_MAX)) + return CHANNEL_RC_INITIALIZATION_ERROR; + + rdpsnd->latency = val; + } + CommandLineSwitchCase(arg, "quality") + { + long wQualityMode = DYNAMIC_QUALITY; + + if (_stricmp(arg->Value, "dynamic") == 0) + wQualityMode = DYNAMIC_QUALITY; + else if (_stricmp(arg->Value, "medium") == 0) + wQualityMode = MEDIUM_QUALITY; + else if (_stricmp(arg->Value, "high") == 0) + wQualityMode = HIGH_QUALITY; + else + { + wQualityMode = strtol(arg->Value, NULL, 0); + + if (errno != 0) + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + if ((wQualityMode < 0) || (wQualityMode > 2)) + wQualityMode = DYNAMIC_QUALITY; + + rdpsnd->wQualityMode = (UINT16)wQualityMode; + } + CommandLineSwitchDefault(arg) + { + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_process_connect(rdpsndPlugin* rdpsnd) +{ + const struct + { + const char* subsystem; + const char* device; + } backends[] = { +#if defined(WITH_IOSAUDIO) + { "ios", "" }, +#endif +#if defined(WITH_OPENSLES) + { "opensles", "" }, +#endif +#if defined(WITH_PULSE) + { "pulse", "" }, +#endif +#if defined(WITH_ALSA) + { "alsa", "default" }, +#endif +#if defined(WITH_OSS) + { "oss", "" }, +#endif +#if defined(WITH_MACAUDIO) + { "mac", "default" }, +#endif +#if defined(WITH_WINMM) + { "winmm", "" }, +#endif +#if defined(WITH_SNDIO) + { "sndio", "" }, +#endif + { "fake", "" } + }; + const ADDIN_ARGV* args = NULL; + UINT status = ERROR_INTERNAL_ERROR; + WINPR_ASSERT(rdpsnd); + rdpsnd->latency = 0; + args = (const ADDIN_ARGV*)rdpsnd->channelEntryPoints.pExtendedData; + + if (args) + { + status = rdpsnd_process_addin_args(rdpsnd, args); + + if (status != CHANNEL_RC_OK) + return status; + } + + if (rdpsnd->subsystem) + { + if ((status = rdpsnd_load_device_plugin(rdpsnd, rdpsnd->subsystem, args))) + { + WLog_ERR(TAG, + "%s Unable to load sound playback subsystem %s because of error %" PRIu32 "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->subsystem, status); + return status; + } + } + else + { + for (size_t x = 0; x < ARRAYSIZE(backends); x++) + { + const char* subsystem_name = backends[x].subsystem; + const char* device_name = backends[x].device; + + if ((status = rdpsnd_load_device_plugin(rdpsnd, subsystem_name, args))) + WLog_ERR(TAG, + "%s Unable to load sound playback subsystem %s because of error %" PRIu32 + "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), subsystem_name, status); + + if (!rdpsnd->device) + continue; + + if (!rdpsnd_set_subsystem(rdpsnd, subsystem_name) || + !rdpsnd_set_device_name(rdpsnd, device_name)) + return CHANNEL_RC_NO_MEMORY; + + break; + } + + if (!rdpsnd->device || status) + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpsnd_virtual_channel_write(rdpsndPlugin* rdpsnd, wStream* s) +{ + UINT status = CHANNEL_RC_BAD_INIT_HANDLE; + + if (rdpsnd) + { + if (rdpsnd->dynamic) + { + IWTSVirtualChannel* channel = NULL; + if (rdpsnd->listener_callback) + { + channel = rdpsnd->listener_callback->channel_callback->channel; + status = channel->Write(channel, (UINT32)Stream_Length(s), Stream_Buffer(s), NULL); + } + Stream_Free(s, TRUE); + } + else + { + status = rdpsnd->channelEntryPoints.pVirtualChannelWriteEx( + rdpsnd->InitHandle, rdpsnd->OpenHandle, Stream_Buffer(s), + (UINT32)Stream_GetPosition(s), s); + + if (status != CHANNEL_RC_OK) + { + Stream_Free(s, TRUE); + WLog_ERR(TAG, "%s pVirtualChannelWriteEx failed with %s [%08" PRIX32 "]", + rdpsnd_is_dyn_str(FALSE), WTSErrorToString(status), status); + } + } + } + + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_virtual_channel_event_data_received(rdpsndPlugin* plugin, void* pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + if ((dataFlags & CHANNEL_FLAG_SUSPEND) || (dataFlags & CHANNEL_FLAG_RESUME)) + return CHANNEL_RC_OK; + + if (dataFlags & CHANNEL_FLAG_FIRST) + { + if (!plugin->data_in) + plugin->data_in = StreamPool_Take(plugin->pool, totalLength); + + Stream_SetPosition(plugin->data_in, 0); + } + + if (!Stream_EnsureRemainingCapacity(plugin->data_in, dataLength)) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write(plugin->data_in, pData, dataLength); + + if (dataFlags & CHANNEL_FLAG_LAST) + { + Stream_SealLength(plugin->data_in); + Stream_SetPosition(plugin->data_in, 0); + + if (plugin->async) + { + if (!MessageQueue_Post(plugin->queue, NULL, 0, plugin->data_in, NULL)) + return ERROR_INTERNAL_ERROR; + plugin->data_in = NULL; + } + else + { + UINT error = rdpsnd_recv_pdu(plugin, plugin->data_in); + plugin->data_in = NULL; + if (error) + return error; + } + } + + return CHANNEL_RC_OK; +} + +static VOID VCAPITYPE rdpsnd_virtual_channel_open_event_ex(LPVOID lpUserParam, DWORD openHandle, + UINT event, LPVOID pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + UINT error = CHANNEL_RC_OK; + rdpsndPlugin* rdpsnd = (rdpsndPlugin*)lpUserParam; + WINPR_ASSERT(rdpsnd); + WINPR_ASSERT(!rdpsnd->dynamic); + + switch (event) + { + case CHANNEL_EVENT_DATA_RECEIVED: + if (!rdpsnd) + return; + + if (rdpsnd->OpenHandle != openHandle) + { + WLog_ERR(TAG, "%s error no match", rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return; + } + if ((error = rdpsnd_virtual_channel_event_data_received(rdpsnd, pData, dataLength, + totalLength, dataFlags))) + WLog_ERR(TAG, + "%s rdpsnd_virtual_channel_event_data_received failed with error %" PRIu32 + "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), error); + + break; + + case CHANNEL_EVENT_WRITE_CANCELLED: + case CHANNEL_EVENT_WRITE_COMPLETE: + { + wStream* s = (wStream*)pData; + Stream_Free(s, TRUE); + } + break; + + case CHANNEL_EVENT_USER: + break; + } + + if (error && rdpsnd && rdpsnd->rdpcontext) + { + char buffer[8192]; + _snprintf(buffer, sizeof(buffer), + "%s rdpsnd_virtual_channel_open_event_ex reported an error", + rdpsnd_is_dyn_str(rdpsnd->dynamic)); + setChannelError(rdpsnd->rdpcontext, error, buffer); + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_virtual_channel_event_connected(rdpsndPlugin* rdpsnd, LPVOID pData, + UINT32 dataLength) +{ + UINT32 status = 0; + DWORD opened = 0; + WINPR_UNUSED(pData); + WINPR_UNUSED(dataLength); + + WINPR_ASSERT(rdpsnd); + WINPR_ASSERT(!rdpsnd->dynamic); + + status = rdpsnd->channelEntryPoints.pVirtualChannelOpenEx( + rdpsnd->InitHandle, &opened, rdpsnd->channelDef.name, rdpsnd_virtual_channel_open_event_ex); + + if (status != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "%s pVirtualChannelOpenEx failed with %s [%08" PRIX32 "]", + rdpsnd_is_dyn_str(rdpsnd->dynamic), WTSErrorToString(status), status); + goto fail; + } + + if (rdpsnd_process_connect(rdpsnd) != CHANNEL_RC_OK) + goto fail; + + rdpsnd->OpenHandle = opened; + return CHANNEL_RC_OK; +fail: + if (opened != 0) + rdpsnd->channelEntryPoints.pVirtualChannelCloseEx(rdpsnd->InitHandle, opened); + return CHANNEL_RC_NO_MEMORY; +} + +static void cleanup_internals(rdpsndPlugin* rdpsnd) +{ + if (!rdpsnd) + return; + + if (rdpsnd->pool) + StreamPool_Return(rdpsnd->pool, rdpsnd->data_in); + + audio_formats_free(rdpsnd->ClientFormats, rdpsnd->NumberOfClientFormats); + audio_formats_free(rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats); + + rdpsnd->NumberOfClientFormats = 0; + rdpsnd->ClientFormats = NULL; + rdpsnd->NumberOfServerFormats = 0; + rdpsnd->ServerFormats = NULL; + + rdpsnd->data_in = NULL; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_virtual_channel_event_disconnected(rdpsndPlugin* rdpsnd) +{ + UINT error = 0; + + WINPR_ASSERT(rdpsnd); + WINPR_ASSERT(!rdpsnd->dynamic); + if (rdpsnd->OpenHandle != 0) + { + DWORD opened = rdpsnd->OpenHandle; + rdpsnd->OpenHandle = 0; + if (rdpsnd->device) + IFCALL(rdpsnd->device->Close, rdpsnd->device); + + error = rdpsnd->channelEntryPoints.pVirtualChannelCloseEx(rdpsnd->InitHandle, opened); + + if (CHANNEL_RC_OK != error) + { + WLog_ERR(TAG, "%s pVirtualChannelCloseEx failed with %s [%08" PRIX32 "]", + rdpsnd_is_dyn_str(rdpsnd->dynamic), WTSErrorToString(error), error); + return error; + } + } + + cleanup_internals(rdpsnd); + + if (rdpsnd->device) + { + IFCALL(rdpsnd->device->Free, rdpsnd->device); + rdpsnd->device = NULL; + } + + return CHANNEL_RC_OK; +} + +static void _queue_free(void* obj) +{ + wMessage* msg = obj; + if (!msg) + return; + if (msg->id != 0) + return; + wStream* s = msg->wParam; + Stream_Release(s); +} + +static void free_internals(rdpsndPlugin* rdpsnd) +{ + if (!rdpsnd) + return; + + if (rdpsnd->references > 0) + rdpsnd->references--; + + if (rdpsnd->references > 0) + return; + + freerdp_dsp_context_free(rdpsnd->dsp_context); + StreamPool_Free(rdpsnd->pool); + rdpsnd->pool = NULL; + rdpsnd->dsp_context = NULL; +} + +static BOOL allocate_internals(rdpsndPlugin* rdpsnd) +{ + WINPR_ASSERT(rdpsnd); + + if (!rdpsnd->pool) + { + rdpsnd->pool = StreamPool_New(TRUE, 4096); + if (!rdpsnd->pool) + return FALSE; + } + + if (!rdpsnd->dsp_context) + { + rdpsnd->dsp_context = freerdp_dsp_context_new(FALSE); + if (!rdpsnd->dsp_context) + return FALSE; + } + rdpsnd->references++; + + return TRUE; +} + +static DWORD WINAPI play_thread(LPVOID arg) +{ + UINT error = CHANNEL_RC_OK; + rdpsndPlugin* rdpsnd = arg; + + if (!rdpsnd || !rdpsnd->queue) + return ERROR_INVALID_PARAMETER; + + while (TRUE) + { + int rc = -1; + wMessage message = { 0 }; + wStream* s = NULL; + DWORD status = 0; + DWORD nCount = 0; + HANDLE handles[MAXIMUM_WAIT_OBJECTS] = { 0 }; + + handles[nCount++] = MessageQueue_Event(rdpsnd->queue); + handles[nCount++] = freerdp_abort_event(rdpsnd->rdpcontext); + status = WaitForMultipleObjects(nCount, handles, FALSE, INFINITE); + switch (status) + { + case WAIT_OBJECT_0: + break; + default: + return ERROR_TIMEOUT; + } + + rc = MessageQueue_Peek(rdpsnd->queue, &message, TRUE); + if (rc < 1) + continue; + + if (message.id == WMQ_QUIT) + break; + + s = message.wParam; + error = rdpsnd_recv_pdu(rdpsnd, s); + + if (error) + return error; + } + + return CHANNEL_RC_OK; +} + +static UINT rdpsnd_virtual_channel_event_initialized(rdpsndPlugin* rdpsnd) +{ + if (!rdpsnd) + return ERROR_INVALID_PARAMETER; + + if (rdpsnd->async) + { + wObject obj = { 0 }; + + obj.fnObjectFree = _queue_free; + rdpsnd->queue = MessageQueue_New(&obj); + if (!rdpsnd->queue) + return CHANNEL_RC_NO_MEMORY; + + rdpsnd->thread = CreateThread(NULL, 0, play_thread, rdpsnd, 0, NULL); + if (!rdpsnd->thread) + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + if (!allocate_internals(rdpsnd)) + return CHANNEL_RC_NO_MEMORY; + + return CHANNEL_RC_OK; +} + +void rdpsnd_virtual_channel_event_terminated(rdpsndPlugin* rdpsnd) +{ + if (rdpsnd) + { + if (rdpsnd->queue) + MessageQueue_PostQuit(rdpsnd->queue, 0); + + if (rdpsnd->thread) + { + WaitForSingleObject(rdpsnd->thread, INFINITE); + CloseHandle(rdpsnd->thread); + } + MessageQueue_Free(rdpsnd->queue); + + free_internals(rdpsnd); + audio_formats_free(rdpsnd->fixed_format, 1); + free(rdpsnd->subsystem); + free(rdpsnd->device_name); + rdpsnd->InitHandle = 0; + } + + free(rdpsnd); +} + +static VOID VCAPITYPE rdpsnd_virtual_channel_init_event_ex(LPVOID lpUserParam, LPVOID pInitHandle, + UINT event, LPVOID pData, + UINT dataLength) +{ + UINT error = CHANNEL_RC_OK; + rdpsndPlugin* plugin = (rdpsndPlugin*)lpUserParam; + + if (!plugin) + return; + + if (plugin->InitHandle != pInitHandle) + { + WLog_ERR(TAG, "%s error no match", rdpsnd_is_dyn_str(plugin->dynamic)); + return; + } + + switch (event) + { + case CHANNEL_EVENT_INITIALIZED: + error = rdpsnd_virtual_channel_event_initialized(plugin); + break; + + case CHANNEL_EVENT_CONNECTED: + error = rdpsnd_virtual_channel_event_connected(plugin, pData, dataLength); + break; + + case CHANNEL_EVENT_DISCONNECTED: + error = rdpsnd_virtual_channel_event_disconnected(plugin); + break; + + case CHANNEL_EVENT_TERMINATED: + rdpsnd_virtual_channel_event_terminated(plugin); + plugin = NULL; + break; + + case CHANNEL_EVENT_ATTACHED: + plugin->attached = TRUE; + break; + + case CHANNEL_EVENT_DETACHED: + plugin->attached = FALSE; + break; + + default: + break; + } + + if (error && plugin && plugin->rdpcontext) + { + char buffer[8192]; + _snprintf(buffer, sizeof(buffer), "%s reported an error", + rdpsnd_is_dyn_str(plugin->dynamic)); + setChannelError(plugin->rdpcontext, error, buffer); + } +} + +rdpContext* freerdp_rdpsnd_get_context(rdpsndPlugin* plugin) +{ + if (!plugin) + return NULL; + + return plugin->rdpcontext; +} + +static rdpsndPlugin* allocatePlugin(void) +{ + rdpsndPlugin* rdpsnd = (rdpsndPlugin*)calloc(1, sizeof(rdpsndPlugin)); + if (!rdpsnd) + goto fail; + + rdpsnd->fixed_format = audio_format_new(); + if (!rdpsnd->fixed_format) + goto fail; + rdpsnd->log = WLog_Get("com.freerdp.channels.rdpsnd.client"); + if (!rdpsnd->log) + goto fail; + + rdpsnd->attached = TRUE; + return rdpsnd; + +fail: + if (rdpsnd) + audio_format_free(rdpsnd->fixed_format); + free(rdpsnd); + return NULL; +} +/* rdpsnd is always built-in */ +FREERDP_ENTRY_POINT(BOOL VCAPITYPE rdpsnd_VirtualChannelEntryEx(PCHANNEL_ENTRY_POINTS pEntryPoints, + PVOID pInitHandle)) +{ + UINT rc = 0; + rdpsndPlugin* rdpsnd = NULL; + CHANNEL_ENTRY_POINTS_FREERDP_EX* pEntryPointsEx = NULL; + + if (!pEntryPoints) + return FALSE; + + rdpsnd = allocatePlugin(); + + if (!rdpsnd) + return FALSE; + + rdpsnd->channelDef.options = CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP; + sprintf_s(rdpsnd->channelDef.name, ARRAYSIZE(rdpsnd->channelDef.name), RDPSND_CHANNEL_NAME); + pEntryPointsEx = (CHANNEL_ENTRY_POINTS_FREERDP_EX*)pEntryPoints; + + if ((pEntryPointsEx->cbSize >= sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)) && + (pEntryPointsEx->MagicNumber == FREERDP_CHANNEL_MAGIC_NUMBER)) + { + rdpsnd->rdpcontext = pEntryPointsEx->context; + if (!freerdp_settings_get_bool(rdpsnd->rdpcontext->settings, + FreeRDP_SynchronousStaticChannels)) + rdpsnd->async = TRUE; + } + + CopyMemory(&(rdpsnd->channelEntryPoints), pEntryPoints, + sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)); + rdpsnd->InitHandle = pInitHandle; + + WINPR_ASSERT(rdpsnd->channelEntryPoints.pVirtualChannelInitEx); + rc = rdpsnd->channelEntryPoints.pVirtualChannelInitEx( + rdpsnd, NULL, pInitHandle, &rdpsnd->channelDef, 1, VIRTUAL_CHANNEL_VERSION_WIN2000, + rdpsnd_virtual_channel_init_event_ex); + + if (CHANNEL_RC_OK != rc) + { + WLog_ERR(TAG, "%s pVirtualChannelInitEx failed with %s [%08" PRIX32 "]", + rdpsnd_is_dyn_str(FALSE), WTSErrorToString(rc), rc); + rdpsnd_virtual_channel_event_terminated(rdpsnd); + return FALSE; + } + + return TRUE; +} + +static UINT rdpsnd_on_open(IWTSVirtualChannelCallback* pChannelCallback) +{ + GENERIC_CHANNEL_CALLBACK* callback = (GENERIC_CHANNEL_CALLBACK*)pChannelCallback; + rdpsndPlugin* rdpsnd = NULL; + + WINPR_ASSERT(callback); + + rdpsnd = (rdpsndPlugin*)callback->plugin; + WINPR_ASSERT(rdpsnd); + + if (rdpsnd->OnOpenCalled) + return CHANNEL_RC_OK; + rdpsnd->OnOpenCalled = TRUE; + + if (!allocate_internals(rdpsnd)) + return ERROR_OUTOFMEMORY; + + return rdpsnd_process_connect(rdpsnd); +} + +static UINT rdpsnd_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + GENERIC_CHANNEL_CALLBACK* callback = (GENERIC_CHANNEL_CALLBACK*)pChannelCallback; + rdpsndPlugin* plugin = NULL; + wStream* copy = NULL; + size_t len = 0; + + len = Stream_GetRemainingLength(data); + + if (!callback || !callback->plugin) + return ERROR_INVALID_PARAMETER; + plugin = (rdpsndPlugin*)callback->plugin; + WINPR_ASSERT(plugin); + + copy = StreamPool_Take(plugin->pool, len); + if (!copy) + return ERROR_OUTOFMEMORY; + Stream_Copy(data, copy, len); + Stream_SealLength(copy); + Stream_SetPosition(copy, 0); + + if (plugin->async) + { + if (!MessageQueue_Post(plugin->queue, NULL, 0, copy, NULL)) + { + Stream_Release(copy); + return ERROR_INTERNAL_ERROR; + } + } + else + { + UINT error = rdpsnd_recv_pdu(plugin, copy); + if (error) + return error; + } + + return CHANNEL_RC_OK; +} + +static UINT rdpsnd_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + GENERIC_CHANNEL_CALLBACK* callback = (GENERIC_CHANNEL_CALLBACK*)pChannelCallback; + rdpsndPlugin* rdpsnd = NULL; + + WINPR_ASSERT(callback); + + rdpsnd = (rdpsndPlugin*)callback->plugin; + WINPR_ASSERT(rdpsnd); + + rdpsnd->OnOpenCalled = FALSE; + if (rdpsnd->device) + IFCALL(rdpsnd->device->Close, rdpsnd->device); + + cleanup_internals(rdpsnd); + + if (rdpsnd->device) + { + IFCALL(rdpsnd->device->Free, rdpsnd->device); + rdpsnd->device = NULL; + } + + free_internals(rdpsnd); + free(pChannelCallback); + return CHANNEL_RC_OK; +} + +static UINT rdpsnd_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, + BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + GENERIC_CHANNEL_CALLBACK* callback = NULL; + GENERIC_LISTENER_CALLBACK* listener_callback = (GENERIC_LISTENER_CALLBACK*)pListenerCallback; + WINPR_ASSERT(listener_callback); + WINPR_ASSERT(pChannel); + WINPR_ASSERT(ppCallback); + callback = (GENERIC_CHANNEL_CALLBACK*)calloc(1, sizeof(GENERIC_CHANNEL_CALLBACK)); + + WINPR_UNUSED(Data); + WINPR_UNUSED(pbAccept); + + if (!callback) + { + WLog_ERR(TAG, "%s calloc failed!", rdpsnd_is_dyn_str(TRUE)); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnOpen = rdpsnd_on_open; + callback->iface.OnDataReceived = rdpsnd_on_data_received; + callback->iface.OnClose = rdpsnd_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + listener_callback->channel_callback = callback; + *ppCallback = &callback->iface; + return CHANNEL_RC_OK; +} + +static UINT rdpsnd_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + UINT status = 0; + rdpsndPlugin* rdpsnd = (rdpsndPlugin*)pPlugin; + WINPR_ASSERT(rdpsnd); + WINPR_ASSERT(pChannelMgr); + if (rdpsnd->initialized) + { + WLog_ERR(TAG, "[%s] channel initialized twice, aborting", RDPSND_DVC_CHANNEL_NAME); + return ERROR_INVALID_DATA; + } + rdpsnd->listener_callback = + (GENERIC_LISTENER_CALLBACK*)calloc(1, sizeof(GENERIC_LISTENER_CALLBACK)); + + if (!rdpsnd->listener_callback) + { + WLog_ERR(TAG, "%s calloc failed!", rdpsnd_is_dyn_str(TRUE)); + return CHANNEL_RC_NO_MEMORY; + } + + rdpsnd->listener_callback->iface.OnNewChannelConnection = rdpsnd_on_new_channel_connection; + rdpsnd->listener_callback->plugin = pPlugin; + rdpsnd->listener_callback->channel_mgr = pChannelMgr; + status = pChannelMgr->CreateListener(pChannelMgr, RDPSND_DVC_CHANNEL_NAME, 0, + &rdpsnd->listener_callback->iface, &(rdpsnd->listener)); + if (status != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "%s CreateListener failed!", rdpsnd_is_dyn_str(TRUE)); + return status; + } + + rdpsnd->listener->pInterface = rdpsnd->iface.pInterface; + status = rdpsnd_virtual_channel_event_initialized(rdpsnd); + + rdpsnd->initialized = status == CHANNEL_RC_OK; + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_plugin_terminated(IWTSPlugin* pPlugin) +{ + rdpsndPlugin* rdpsnd = (rdpsndPlugin*)pPlugin; + if (rdpsnd) + { + if (rdpsnd->listener_callback) + { + IWTSVirtualChannelManager* mgr = rdpsnd->listener_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, rdpsnd->listener); + } + free(rdpsnd->listener_callback); + free(rdpsnd->iface.pInterface); + } + rdpsnd_virtual_channel_event_terminated(rdpsnd); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT(UINT rdpsnd_DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints)) +{ + UINT error = CHANNEL_RC_OK; + rdpsndPlugin* rdpsnd = NULL; + + WINPR_ASSERT(pEntryPoints); + WINPR_ASSERT(pEntryPoints->GetPlugin); + + rdpsnd = (rdpsndPlugin*)pEntryPoints->GetPlugin(pEntryPoints, RDPSND_CHANNEL_NAME); + + if (!rdpsnd) + { + IWTSPlugin* iface = NULL; + union + { + const void* cev; + void* ev; + } cnv; + + rdpsnd = allocatePlugin(); + if (!rdpsnd) + { + WLog_ERR(TAG, "%s calloc failed!", rdpsnd_is_dyn_str(TRUE)); + return CHANNEL_RC_NO_MEMORY; + } + + iface = &rdpsnd->iface; + iface->Initialize = rdpsnd_plugin_initialize; + iface->Connected = NULL; + iface->Disconnected = NULL; + iface->Terminated = rdpsnd_plugin_terminated; + + rdpsnd->dynamic = TRUE; + + WINPR_ASSERT(pEntryPoints->GetRdpContext); + rdpsnd->rdpcontext = pEntryPoints->GetRdpContext(pEntryPoints); + + if (!freerdp_settings_get_bool(rdpsnd->rdpcontext->settings, + FreeRDP_SynchronousDynamicChannels)) + rdpsnd->async = TRUE; + + /* user data pointer is not const, cast to avoid warning. */ + cnv.cev = pEntryPoints->GetPluginData(pEntryPoints); + WINPR_ASSERT(pEntryPoints->GetPluginData); + rdpsnd->channelEntryPoints.pExtendedData = cnv.ev; + + error = pEntryPoints->RegisterPlugin(pEntryPoints, RDPSND_CHANNEL_NAME, iface); + } + else + { + WLog_ERR(TAG, "%s could not get rdpsnd Plugin.", rdpsnd_is_dyn_str(TRUE)); + return CHANNEL_RC_BAD_CHANNEL; + } + + return error; +} diff --git a/channels/rdpsnd/client/rdpsnd_main.h b/channels/rdpsnd/client/rdpsnd_main.h new file mode 100644 index 0000000..33adfcd --- /dev/null +++ b/channels/rdpsnd/client/rdpsnd_main.h @@ -0,0 +1,41 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2012-2013 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. + */ + +#ifndef FREERDP_CHANNEL_RDPSND_CLIENT_MAIN_H +#define FREERDP_CHANNEL_RDPSND_CLIENT_MAIN_H + +#include <freerdp/api.h> +#include <freerdp/svc.h> +#include <freerdp/addin.h> +#include <freerdp/client/rdpsnd.h> +#include <freerdp/channels/log.h> + +#define TAG CHANNELS_TAG("rdpsnd.client") + +#if defined(WITH_DEBUG_SND) +#define DEBUG_SND(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_SND(...) \ + do \ + { \ + } while (0) +#endif + +#endif /* FREERDP_CHANNEL_RDPSND_CLIENT_MAIN_H */ diff --git a/channels/rdpsnd/client/sndio/CMakeLists.txt b/channels/rdpsnd/client/sndio/CMakeLists.txt new file mode 100644 index 0000000..78b9c06 --- /dev/null +++ b/channels/rdpsnd/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("rdpsnd" "sndio" "") + +find_package(SNDIO REQUIRED) + +set(${MODULE_PREFIX}_SRCS + rdpsnd_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/rdpsnd/client/sndio/rdpsnd_sndio.c b/channels/rdpsnd/client/sndio/rdpsnd_sndio.c new file mode 100644 index 0000000..4414653 --- /dev/null +++ b/channels/rdpsnd/client/sndio/rdpsnd_sndio.c @@ -0,0 +1,217 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2019 Armin Novak <armin.novak@thincast.com> + * Copyright 2019 Thincast Technologies GmbH + * 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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <winpr/crt.h> +#include <winpr/stream.h> +#include <winpr/cmdline.h> + +#include <freerdp/types.h> + +#include "rdpsnd_main.h" + +typedef struct +{ + rdpsndDevicePlugin device; + + struct sio_hdl* hdl; + struct sio_par par; +} rdpsndSndioPlugin; + +static BOOL rdpsnd_sndio_open(rdpsndDevicePlugin* device, AUDIO_FORMAT* format, int latency) +{ + rdpsndSndioPlugin* sndio = (rdpsndSndioPlugin*)device; + + if (device == NULL || format == NULL) + return FALSE; + + if (sndio->hdl != NULL) + return TRUE; + + sndio->hdl = sio_open(SIO_DEVANY, SIO_PLAY, 0); + if (sndio->hdl == NULL) + { + WLog_ERR(TAG, "could not open audio device"); + return FALSE; + } + + sio_initpar(&sndio->par); + sndio->par.bits = format->wBitsPerSample; + sndio->par.pchan = format->nChannels; + sndio->par.rate = format->nSamplesPerSec; + if (!sio_setpar(sndio->hdl, &sndio->par)) + { + WLog_ERR(TAG, "could not set audio parameters"); + return FALSE; + } + if (!sio_getpar(sndio->hdl, &sndio->par)) + { + WLog_ERR(TAG, "could not get audio parameters"); + return FALSE; + } + + if (!sio_start(sndio->hdl)) + { + WLog_ERR(TAG, "could not start audio device"); + return FALSE; + } + + return TRUE; +} + +static void rdpsnd_sndio_close(rdpsndDevicePlugin* device) +{ + rdpsndSndioPlugin* sndio = (rdpsndSndioPlugin*)device; + + if (device == NULL) + return; + + if (sndio->hdl != NULL) + { + sio_stop(sndio->hdl); + sio_close(sndio->hdl); + sndio->hdl = NULL; + } +} + +static BOOL rdpsnd_sndio_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + rdpsndSndioPlugin* sndio = (rdpsndSndioPlugin*)device; + + if (device == NULL || sndio->hdl == NULL) + return FALSE; + + /* + * Low-order word contains the left-channel volume setting. + * We ignore the right-channel volume setting in the high-order word. + */ + return sio_setvol(sndio->hdl, ((value & 0xFFFF) * SIO_MAXVOL) / 0xFFFF); +} + +static void rdpsnd_sndio_free(rdpsndDevicePlugin* device) +{ + rdpsndSndioPlugin* sndio = (rdpsndSndioPlugin*)device; + + if (device == NULL) + return; + + rdpsnd_sndio_close(device); + free(sndio); +} + +static BOOL rdpsnd_sndio_format_supported(rdpsndDevicePlugin* device, AUDIO_FORMAT* format) +{ + if (format == NULL) + return FALSE; + + return (format->wFormatTag == WAVE_FORMAT_PCM); +} + +static void rdpsnd_sndio_play(rdpsndDevicePlugin* device, BYTE* data, int size) +{ + rdpsndSndioPlugin* sndio = (rdpsndSndioPlugin*)device; + + if (device == NULL || sndio->hdl == NULL) + return; + + sio_write(sndio->hdl, data, size); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_sndio_parse_addin_args(rdpsndDevicePlugin* device, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + rdpsndSndioPlugin* sndio = (rdpsndSndioPlugin*)device; + COMMAND_LINE_ARGUMENT_A rdpsnd_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, rdpsnd_sndio_args, + flags, sndio, NULL, NULL); + + if (status < 0) + return ERROR_INVALID_DATA; + + arg = rdpsnd_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_rdpsnd_client_subsystem_entry( + PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + ADDIN_ARGV* args; + rdpsndSndioPlugin* sndio; + UINT ret = CHANNEL_RC_OK; + sndio = (rdpsndSndioPlugin*)calloc(1, sizeof(rdpsndSndioPlugin)); + + if (sndio == NULL) + return CHANNEL_RC_NO_MEMORY; + + sndio->device.Open = rdpsnd_sndio_open; + sndio->device.FormatSupported = rdpsnd_sndio_format_supported; + sndio->device.SetVolume = rdpsnd_sndio_set_volume; + sndio->device.Play = rdpsnd_sndio_play; + sndio->device.Close = rdpsnd_sndio_close; + sndio->device.Free = rdpsnd_sndio_free; + args = pEntryPoints->args; + + if (args->argc > 1) + { + ret = rdpsnd_sndio_parse_addin_args((rdpsndDevicePlugin*)sndio, args); + + if (ret != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "error parsing arguments"); + goto error; + } + } + + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, &sndio->device); + return ret; +error: + rdpsnd_sndio_free(&sndio->device); + return ret; +} diff --git a/channels/rdpsnd/client/winmm/CMakeLists.txt b/channels/rdpsnd/client/winmm/CMakeLists.txt new file mode 100644 index 0000000..6e9d8d1 --- /dev/null +++ b/channels/rdpsnd/client/winmm/CMakeLists.txt @@ -0,0 +1,32 @@ +# 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("rdpsnd" "winmm" "") + +set(${MODULE_PREFIX}_SRCS + rdpsnd_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/rdpsnd/client/winmm/rdpsnd_winmm.c b/channels/rdpsnd/client/winmm/rdpsnd_winmm.c new file mode 100644 index 0000000..2ba0654 --- /dev/null +++ b/channels/rdpsnd/client/winmm/rdpsnd_winmm.c @@ -0,0 +1,347 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2009-2012 Jay Sorg + * Copyright 2010-2012 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * Copyright 2016 David PHAM-VAN <d.phamvan@inuvika.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 <winpr/sysinfo.h> + +#include <freerdp/types.h> +#include <freerdp/channels/log.h> + +#include "rdpsnd_main.h" + +typedef struct +{ + rdpsndDevicePlugin device; + + HWAVEOUT hWaveOut; + WAVEFORMATEX format; + UINT32 volume; + wLog* log; + UINT32 latency; + HANDLE hThread; + DWORD threadId; + CRITICAL_SECTION cs; +} rdpsndWinmmPlugin; + +static BOOL rdpsnd_winmm_convert_format(const AUDIO_FORMAT* in, WAVEFORMATEX* out) +{ + if (!in || !out) + return FALSE; + + ZeroMemory(out, sizeof(WAVEFORMATEX)); + out->wFormatTag = WAVE_FORMAT_PCM; + out->nChannels = in->nChannels; + out->nSamplesPerSec = in->nSamplesPerSec; + + switch (in->wFormatTag) + { + case WAVE_FORMAT_PCM: + out->wBitsPerSample = in->wBitsPerSample; + break; + + default: + return FALSE; + } + + out->nBlockAlign = out->nChannels * out->wBitsPerSample / 8; + out->nAvgBytesPerSec = out->nSamplesPerSec * out->nBlockAlign; + return TRUE; +} + +static BOOL rdpsnd_winmm_set_format(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + + winmm->latency = latency; + if (!rdpsnd_winmm_convert_format(format, &winmm->format)) + return FALSE; + + return TRUE; +} + +static DWORD WINAPI waveOutProc(LPVOID lpParameter) +{ + MSG msg; + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)lpParameter; + while (GetMessage(&msg, NULL, 0, 0)) + { + if (msg.message == MM_WOM_CLOSE) + { + /* device was closed - exit thread */ + break; + } + else if (msg.message == MM_WOM_DONE) + { + /* free buffer */ + LPWAVEHDR waveHdr = (LPWAVEHDR)msg.lParam; + EnterCriticalSection(&winmm->cs); + waveOutUnprepareHeader((HWAVEOUT)msg.wParam, waveHdr, sizeof(WAVEHDR)); + LeaveCriticalSection(&winmm->cs); + free(waveHdr->lpData); + free(waveHdr); + } + } + + return 0; +} + +static BOOL rdpsnd_winmm_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + MMRESULT mmResult; + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + + if (winmm->hWaveOut) + return TRUE; + + if (!rdpsnd_winmm_set_format(device, format, latency)) + return FALSE; + + winmm->hThread = CreateThread(NULL, 0, waveOutProc, winmm, 0, &winmm->threadId); + if (!winmm->hThread) + { + WLog_Print(winmm->log, WLOG_ERROR, "CreateThread failed: %" PRIu32 "", GetLastError()); + return FALSE; + } + + mmResult = waveOutOpen(&winmm->hWaveOut, WAVE_MAPPER, &winmm->format, + (DWORD_PTR)winmm->threadId, 0, CALLBACK_THREAD); + + if (mmResult != MMSYSERR_NOERROR) + { + WLog_Print(winmm->log, WLOG_ERROR, "waveOutOpen failed: %" PRIu32 "", mmResult); + return FALSE; + } + + mmResult = waveOutSetVolume(winmm->hWaveOut, winmm->volume); + + if (mmResult != MMSYSERR_NOERROR) + { + WLog_Print(winmm->log, WLOG_ERROR, "waveOutSetVolume failed: %" PRIu32 "", mmResult); + return FALSE; + } + + return TRUE; +} + +static void rdpsnd_winmm_close(rdpsndDevicePlugin* device) +{ + MMRESULT mmResult; + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + + if (winmm->hWaveOut) + { + EnterCriticalSection(&winmm->cs); + + mmResult = waveOutReset(winmm->hWaveOut); + if (mmResult != MMSYSERR_NOERROR) + WLog_Print(winmm->log, WLOG_ERROR, "waveOutReset failure: %" PRIu32 "", mmResult); + + mmResult = waveOutClose(winmm->hWaveOut); + if (mmResult != MMSYSERR_NOERROR) + WLog_Print(winmm->log, WLOG_ERROR, "waveOutClose failure: %" PRIu32 "", mmResult); + + LeaveCriticalSection(&winmm->cs); + + winmm->hWaveOut = NULL; + } + + if (winmm->hThread) + { + WaitForSingleObject(winmm->hThread, INFINITE); + CloseHandle(winmm->hThread); + winmm->hThread = NULL; + } +} + +static void rdpsnd_winmm_free(rdpsndDevicePlugin* device) +{ + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + + if (winmm) + { + rdpsnd_winmm_close(device); + DeleteCriticalSection(&winmm->cs); + free(winmm); + } +} + +static BOOL rdpsnd_winmm_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + MMRESULT result; + WAVEFORMATEX out; + + WINPR_UNUSED(device); + if (rdpsnd_winmm_convert_format(format, &out)) + { + result = waveOutOpen(NULL, WAVE_MAPPER, &out, 0, 0, WAVE_FORMAT_QUERY); + + if (result == MMSYSERR_NOERROR) + return TRUE; + } + + return FALSE; +} + +static UINT32 rdpsnd_winmm_get_volume(rdpsndDevicePlugin* device) +{ + MMRESULT mmResult; + DWORD dwVolume = UINT32_MAX; + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + + if (!winmm->hWaveOut) + return dwVolume; + + mmResult = waveOutGetVolume(winmm->hWaveOut, &dwVolume); + if (mmResult != MMSYSERR_NOERROR) + { + WLog_Print(winmm->log, WLOG_ERROR, "waveOutGetVolume failure: %" PRIu32 "", mmResult); + dwVolume = UINT32_MAX; + } + return dwVolume; +} + +static BOOL rdpsnd_winmm_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + MMRESULT mmResult; + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + winmm->volume = value; + + if (!winmm->hWaveOut) + return TRUE; + + mmResult = waveOutSetVolume(winmm->hWaveOut, value); + if (mmResult != MMSYSERR_NOERROR) + { + WLog_Print(winmm->log, WLOG_ERROR, "waveOutGetVolume failure: %" PRIu32 "", mmResult); + return FALSE; + } + return TRUE; +} + +static UINT rdpsnd_winmm_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + MMRESULT mmResult; + LPWAVEHDR lpWaveHdr; + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + + if (!winmm->hWaveOut) + return 0; + + if (size > UINT32_MAX) + return 0; + + lpWaveHdr = (LPWAVEHDR)calloc(1, sizeof(WAVEHDR)); + if (!lpWaveHdr) + return 0; + + lpWaveHdr->dwFlags = 0; + lpWaveHdr->dwLoops = 0; + lpWaveHdr->lpData = malloc(size); + if (!lpWaveHdr->lpData) + goto fail; + memcpy(lpWaveHdr->lpData, data, size); + lpWaveHdr->dwBufferLength = (DWORD)size; + + EnterCriticalSection(&winmm->cs); + + mmResult = waveOutPrepareHeader(winmm->hWaveOut, lpWaveHdr, sizeof(WAVEHDR)); + if (mmResult != MMSYSERR_NOERROR) + { + WLog_Print(winmm->log, WLOG_ERROR, "waveOutPrepareHeader failure: %" PRIu32 "", mmResult); + goto failCS; + } + + mmResult = waveOutWrite(winmm->hWaveOut, lpWaveHdr, sizeof(WAVEHDR)); + if (mmResult != MMSYSERR_NOERROR) + { + WLog_Print(winmm->log, WLOG_ERROR, "waveOutWrite failure: %" PRIu32 "", mmResult); + waveOutUnprepareHeader(winmm->hWaveOut, lpWaveHdr, sizeof(WAVEHDR)); + goto failCS; + } + + LeaveCriticalSection(&winmm->cs); + return winmm->latency; +failCS: + LeaveCriticalSection(&winmm->cs); +fail: + if (lpWaveHdr) + free(lpWaveHdr->lpData); + free(lpWaveHdr); + return 0; +} + +static void rdpsnd_winmm_parse_addin_args(rdpsndDevicePlugin* device, const ADDIN_ARGV* args) +{ + WINPR_UNUSED(device); + WINPR_UNUSED(args); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +FREERDP_ENTRY_POINT(UINT winmm_freerdp_rdpsnd_client_subsystem_entry( + PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints)) +{ + const ADDIN_ARGV* args; + rdpsndWinmmPlugin* winmm; + + if (waveOutGetNumDevs() == 0) + { + WLog_Print(WLog_Get(TAG), WLOG_ERROR, "No sound playback device available!"); + return ERROR_DEVICE_NOT_AVAILABLE; + } + + winmm = (rdpsndWinmmPlugin*)calloc(1, sizeof(rdpsndWinmmPlugin)); + if (!winmm) + return CHANNEL_RC_NO_MEMORY; + + winmm->device.Open = rdpsnd_winmm_open; + winmm->device.FormatSupported = rdpsnd_winmm_format_supported; + winmm->device.GetVolume = rdpsnd_winmm_get_volume; + winmm->device.SetVolume = rdpsnd_winmm_set_volume; + winmm->device.Play = rdpsnd_winmm_play; + winmm->device.Close = rdpsnd_winmm_close; + winmm->device.Free = rdpsnd_winmm_free; + winmm->log = WLog_Get(TAG); + InitializeCriticalSection(&winmm->cs); + + args = pEntryPoints->args; + rdpsnd_winmm_parse_addin_args(&winmm->device, args); + winmm->volume = 0xFFFFFFFF; + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)winmm); + return CHANNEL_RC_OK; +} |