summaryrefslogtreecommitdiffstats
path: root/channels/rdpsnd/client
diff options
context:
space:
mode:
Diffstat (limited to 'channels/rdpsnd/client')
-rw-r--r--channels/rdpsnd/client/CMakeLists.txt66
-rw-r--r--channels/rdpsnd/client/alsa/CMakeLists.txt34
-rw-r--r--channels/rdpsnd/client/alsa/rdpsnd_alsa.c574
-rw-r--r--channels/rdpsnd/client/fake/CMakeLists.txt32
-rw-r--r--channels/rdpsnd/client/fake/rdpsnd_fake.c147
-rw-r--r--channels/rdpsnd/client/ios/CMakeLists.txt40
-rw-r--r--channels/rdpsnd/client/ios/TPCircularBuffer.c153
-rw-r--r--channels/rdpsnd/client/ios/TPCircularBuffer.h217
-rw-r--r--channels/rdpsnd/client/ios/rdpsnd_ios.c282
-rw-r--r--channels/rdpsnd/client/mac/CMakeLists.txt44
-rw-r--r--channels/rdpsnd/client/mac/rdpsnd_mac.m402
-rw-r--r--channels/rdpsnd/client/opensles/CMakeLists.txt35
-rw-r--r--channels/rdpsnd/client/opensles/opensl_io.c422
-rw-r--r--channels/rdpsnd/client/opensles/opensl_io.h110
-rw-r--r--channels/rdpsnd/client/opensles/rdpsnd_opensles.c378
-rw-r--r--channels/rdpsnd/client/oss/CMakeLists.txt35
-rw-r--r--channels/rdpsnd/client/oss/rdpsnd_oss.c478
-rw-r--r--channels/rdpsnd/client/pulse/CMakeLists.txt36
-rw-r--r--channels/rdpsnd/client/pulse/rdpsnd_pulse.c768
-rw-r--r--channels/rdpsnd/client/rdpsnd_main.c1850
-rw-r--r--channels/rdpsnd/client/rdpsnd_main.h41
-rw-r--r--channels/rdpsnd/client/sndio/CMakeLists.txt36
-rw-r--r--channels/rdpsnd/client/sndio/rdpsnd_sndio.c217
-rw-r--r--channels/rdpsnd/client/winmm/CMakeLists.txt32
-rw-r--r--channels/rdpsnd/client/winmm/rdpsnd_winmm.c347
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;
+}