Milestone 5: deliver embedded RDP sessions and lifecycle hardening

This commit is contained in:
Keith Smith
2026-03-03 18:59:26 -07:00
parent 230a401386
commit 36006bd4aa
2941 changed files with 724359 additions and 77 deletions

View File

@@ -0,0 +1,58 @@
# 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" "")

View File

@@ -0,0 +1,30 @@
# 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)
freerdp_client_pc_add_requires_private("alsa")
set(${MODULE_PREFIX}_SRCS rdpsnd_alsa.c)
set(${MODULE_PREFIX}_LIBS winpr freerdp ${ALSA_LIBRARIES})
include_directories(..)
include_directories(SYSTEM ${ALSA_INCLUDE_DIRS})
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")

View File

@@ -0,0 +1,571 @@
/**
* 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 = nullptr;
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, nullptr);
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.
*/
const size_t interrupts_per_sec_near = 50;
const size_t bytes_per_sec =
(1ull * 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,
nullptr);
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 = nullptr;
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, (1ULL * 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 = nullptr;
}
}
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, nullptr, nullptr);
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 = nullptr;
}
}
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(WINPR_ATTR_UNUSED 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;
default:
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;
rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device;
UINT32 dwVolumeLeft = ((50 * 0xFFFF) / 100); /* 50% */
UINT32 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;
}
}
return (dwVolumeLeft << 16) | dwVolumeRight;
}
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;
rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device;
WINPR_ASSERT(alsa);
WINPR_ASSERT(data || (size == 0));
const size_t frame_size = 1ull * 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, (int)status, 0);
if (status < 0)
{
WLog_ERR(TAG, "status: %ld\n", status);
rdpsnd_alsa_close(device);
rdpsnd_alsa_open(device, nullptr, alsa->latency);
break;
}
offset += WINPR_ASSERTING_INT_CAST(size_t, 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) && (available == 0)) /* Get [ms] from number of samples */
latency = (UINT32)MIN(UINT32_MAX, delay * 1000 / alsa->actual_rate);
}
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 = nullptr;
rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device;
COMMAND_LINE_ARGUMENT_A rdpsnd_alsa_args[] = {
{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr, "device" },
{ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
};
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,
nullptr, nullptr);
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)) != nullptr);
return CHANNEL_RC_OK;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
FREERDP_ENTRY_POINT(UINT VCAPITYPE alsa_freerdp_rdpsnd_client_subsystem_entry(
PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints))
{
const ADDIN_ARGV* args = nullptr;
rdpsndAlsaPlugin* alsa = nullptr;
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 = nullptr;
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;
}

View File

@@ -0,0 +1,27 @@
# 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 "")

View File

@@ -0,0 +1,153 @@
/**
* 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(WINPR_ATTR_UNUSED rdpsndDevicePlugin* device,
WINPR_ATTR_UNUSED const AUDIO_FORMAT* format,
WINPR_ATTR_UNUSED UINT32 latency)
{
return TRUE;
}
static void rdpsnd_fake_close(WINPR_ATTR_UNUSED rdpsndDevicePlugin* device)
{
}
static BOOL rdpsnd_fake_set_volume(WINPR_ATTR_UNUSED rdpsndDevicePlugin* device,
WINPR_ATTR_UNUSED 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(WINPR_ATTR_UNUSED rdpsndDevicePlugin* device,
WINPR_ATTR_UNUSED const AUDIO_FORMAT* format)
{
return TRUE;
}
static UINT rdpsnd_fake_play(WINPR_ATTR_UNUSED rdpsndDevicePlugin* device,
WINPR_ATTR_UNUSED const BYTE* data, WINPR_ATTR_UNUSED 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 = nullptr;
COMMAND_LINE_ARGUMENT_A rdpsnd_fake_args[] = { { nullptr, 0, nullptr, nullptr, nullptr, -1,
nullptr, nullptr } };
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,
nullptr, nullptr);
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)) != nullptr);
return CHANNEL_RC_OK;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
FREERDP_ENTRY_POINT(UINT VCAPITYPE fake_freerdp_rdpsnd_client_subsystem_entry(
PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints))
{
const ADDIN_ARGV* args = nullptr;
rdpsndFakePlugin* fake = nullptr;
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;
}

View File

@@ -0,0 +1,32 @@
# 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 "")

View File

@@ -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);
}
}

View File

@@ -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 nullptr if buffer is empty
*/
static inline __attribute__((always_inline)) void*
TPCircularBufferTail(TPCircularBuffer* buffer, int32_t* availableBytes)
{
*availableBytes = buffer->fillCount;
if (*availableBytes == 0)
return nullptr;
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 nullptr 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 nullptr;
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

View File

@@ -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 != nullptr && 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,
UINT32 __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(nullptr, &desc);
if (audioComponent == nullptr)
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 = WINPR_C_ARRAY_INIT;
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 = nullptr;
return FALSE;
}
/* Set up the AudioUnit callback. */
AURenderCallbackStruct callbackStruct = WINPR_C_ARRAY_INIT;
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 = nullptr;
return FALSE;
}
/* Initialize the AudioUnit. */
status = AudioUnitInitialize(p->audio_unit);
if (status != 0)
{
AudioComponentInstanceDispose(p->audio_unit);
p->audio_unit = nullptr;
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 = nullptr;
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 = nullptr;
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 VCAPITYPE 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;
}

View File

@@ -0,0 +1,43 @@
# 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(SYSTEM ${MACAUDIO_INCLUDE_DIRS})
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")

View File

@@ -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 = nullptr;
if (mac->engine)
[mac->engine release];
mac->engine = nullptr;
}
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, nullptr,
&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 VCAPITYPE 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;
}

View File

@@ -0,0 +1,29 @@
# 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(SYSTEM ${OpenSLES_INCLUDE_DIRS})
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")

View File

@@ -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, nullptr, 0, nullptr, nullptr);
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, nullptr };
// 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 != nullptr)
{
(*p->bqPlayerObject)->Destroy(p->bqPlayerObject);
p->bqPlayerObject = nullptr;
p->bqPlayerVolume = nullptr;
p->bqPlayerPlay = nullptr;
p->bqPlayerBufferQueue = nullptr;
p->bqPlayerEffectSend = nullptr;
}
// destroy output mix object, and invalidate all associated interfaces
if (p->outputMixObject != nullptr)
{
(*p->outputMixObject)->Destroy(p->outputMixObject);
p->outputMixObject = nullptr;
}
// destroy engine object, and invalidate all associated interfaces
if (p->engineObject != nullptr)
{
(*p->engineObject)->Destroy(p->engineObject);
p->engineObject = nullptr;
p->engineEngine = nullptr;
}
}
// 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 nullptr;
p->queuesize = bufferframes;
p->outchannels = outchannels;
p->sr = sr;
if (openSLCreateEngine(p) != SL_RESULT_SUCCESS)
{
android_CloseAudioDevice(p);
return nullptr;
}
if (openSLPlayOpen(p) != SL_RESULT_SUCCESS)
{
android_CloseAudioDevice(p);
return nullptr;
}
p->queue = Queue_New(TRUE, -1, -1);
if (!p->queue)
{
android_CloseAudioDevice(p);
return nullptr;
}
return p;
}
// close the android audio device
void android_CloseAudioDevice(OPENSL_STREAM* p)
{
if (p == nullptr)
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;
}

View File

@@ -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 */

View File

@@ -0,0 +1,373 @@
/**
* 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 = nullptr;
}
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, const ADDIN_ARGV* args)
{
rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device;
COMMAND_LINE_ARGUMENT_A rdpsnd_opensles_args[] = {
{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr, "device" },
{ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
};
WINPR_ASSERT(opensles);
WINPR_ASSERT(args);
DEBUG_SND("opensles=%p, args=%p", (void*)opensles, (void*)args);
const DWORD flags =
COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD;
const int status = CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_opensles_args,
flags, opensles, nullptr, nullptr);
if (status < 0)
return status;
const COMMAND_LINE_ARGUMENT_A* 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)) != nullptr);
return status;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
FREERDP_ENTRY_POINT(UINT VCAPITYPE opensles_freerdp_rdpsnd_client_subsystem_entry(
PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints))
{
UINT error = ERROR_INTERNAL_ERROR;
DEBUG_SND("pEntryPoints=%p", (void*)pEntryPoints);
rdpsndopenslesPlugin* 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;
const ADDIN_ARGV* 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;
}

View File

@@ -0,0 +1,31 @@
# 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(${CMAKE_CURRENT_BINARY_DIR})
include_directories(SYSTEM ${OSS_INCLUDE_DIRS})
cleaning_configure_file(${CMAKE_SOURCE_DIR}/cmake/oss-includes.h.in ${CMAKE_CURRENT_BINARY_DIR}/oss-includes.h @ONLY)
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")

View File

@@ -0,0 +1,444 @@
/**
* 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>
#include <oss-includes.h>
#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] = WINPR_C_ARRAY_INIT; \
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;
default:
break;
}
break;
default:
break;
}
return 0;
}
static BOOL rdpsnd_oss_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format)
{
int req_fmt = 0;
rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device;
if (device == nullptr || format == nullptr)
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 == nullptr || oss->pcm_handle == -1 || format == nullptr)
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 = (int)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)
(void)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 == nullptr || oss->pcm_handle != -1)
return TRUE;
if (oss->dev_unit != -1)
(void)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 (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 == nullptr)
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 == nullptr)
return;
rdpsnd_oss_close(device);
free(oss);
}
static UINT32 rdpsnd_oss_get_volume(rdpsndDevicePlugin* device)
{
rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device;
WINPR_ASSERT(oss);
int vol = 0;
/* On error return 50% volume. */
UINT32 dwVolumeLeft = ((50 * 0xFFFF) / 100); /* 50% */
UINT32 dwVolumeRight = ((50 * 0xFFFF) / 100); /* 50% */
UINT32 dwVolume = ((dwVolumeLeft << 16) | dwVolumeRight);
if (device == nullptr || 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)
{
rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device;
WINPR_ASSERT(oss);
if (device == nullptr || oss->mixer_handle == -1)
return FALSE;
unsigned left = (((value & 0xFFFF) * 100) / 0xFFFF);
unsigned right = ((((value >> 16) & 0xFFFF) * 100) / 0xFFFF);
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 == nullptr || 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, nullptr, 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 = nullptr;
char* eptr = nullptr;
DWORD flags = 0;
const COMMAND_LINE_ARGUMENT_A* arg = nullptr;
rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device;
COMMAND_LINE_ARGUMENT_A rdpsnd_oss_args[] = {
{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr, "device" },
{ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
};
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,
nullptr, nullptr);
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 = (int)val;
}
if (oss->dev_unit < 0 || *eptr != '\0')
oss->dev_unit = -1;
free(str_num);
}
CommandLineSwitchEnd(arg)
} while ((arg = CommandLineFindNextArgumentA(arg)) != nullptr);
return status;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
FREERDP_ENTRY_POINT(UINT VCAPITYPE oss_freerdp_rdpsnd_client_subsystem_entry(
PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints))
{
const ADDIN_ARGV* args = nullptr;
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;
}

View File

@@ -0,0 +1,30 @@
# 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)
freerdp_client_pc_add_requires_private("libpulse")
set(${MODULE_PREFIX}_SRCS rdpsnd_pulse.c)
set(${MODULE_PREFIX}_LIBS winpr freerdp ${PULSEAUDIO_LIBRARY} ${PULSEAUDIO_MAINLOOP_LIBRARY})
include_directories(..)
include_directories(SYSTEM ${PULSEAUDIO_INCLUDE_DIR})
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")

View File

@@ -0,0 +1,789 @@
/**
* 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 <freerdp/utils/helpers.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <winpr/crt.h>
#include <winpr/cast.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;
char* client_name;
char* stream_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=nullptr");
rc = FALSE;
}
if (haveStream)
{
if (!pulse->stream)
{
WLog_WARN(TAG, "pulse->stream=%p", WINPR_CXX_COMPAT_CAST(const void*, pulse->stream));
rc = FALSE;
}
}
if (!pulse->mainloop)
{
WLog_WARN(TAG, "pulse->mainloop=%p", WINPR_CXX_COMPAT_CAST(const void*, 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,
WINPR_ATTR_UNUSED 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 = nullptr;
if (pulse->reconnect_delay_seconds >= 0)
pulse->reconnect_time = time(nullptr) + 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 = nullptr;
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, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 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(WINPR_ATTR_UNUSED pa_stream* stream,
WINPR_ATTR_UNUSED 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 = nullptr;
pa_threaded_mainloop_signal(pulse->mainloop, 0);
break;
default:
break;
}
}
static void rdpsnd_pulse_stream_request_callback(pa_stream* stream, WINPR_ATTR_UNUSED 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 = nullptr;
}
pa_threaded_mainloop_unlock(pulse->mainloop);
}
static BOOL rdpsnd_pulse_set_format_spec(rdpsndPulsePlugin* pulse, const AUDIO_FORMAT* format)
{
WINPR_ASSERT(format);
if (!rdpsnd_check_pulse(pulse, FALSE))
return FALSE;
if (!rdpsnd_pulse_format_supported(&pulse->device, format))
return FALSE;
pa_sample_format_t sformat = PA_SAMPLE_INVALID;
switch (format->wFormatTag)
{
case WAVE_FORMAT_PCM:
switch (format->wBitsPerSample)
{
case 8:
sformat = PA_SAMPLE_U8;
break;
case 16:
sformat = PA_SAMPLE_S16LE;
break;
default:
return FALSE;
}
break;
default:
return FALSE;
}
const pa_sample_spec sample_spec = { .format = sformat,
.rate = format->nSamplesPerSec,
.channels =
WINPR_ASSERTING_INT_CAST(uint8_t, format->nChannels) };
pulse->sample_spec = sample_spec;
return TRUE;
}
static BOOL rdpsnd_pulse_context_connect(rdpsndDevicePlugin* device)
{
rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device;
WINPR_ASSERT(pulse);
pulse->context =
pa_context_new(pa_threaded_mainloop_get_api(pulse->mainloop), pulse->client_name);
if (!pulse->context)
return FALSE;
pa_context_set_state_callback(pulse->context, rdpsnd_pulse_context_state_callback, pulse);
return rdpsnd_pulse_connect(&pulse->device);
}
static BOOL rdpsnd_pulse_open_stream(rdpsndDevicePlugin* device)
{
pa_stream_state_t state = PA_STREAM_FAILED;
int flags = PA_STREAM_NOFLAGS;
pa_buffer_attr buffer_attr = WINPR_C_ARRAY_INIT;
char ss[PA_SAMPLE_SPEC_SNPRINT_MAX] = WINPR_C_ARRAY_INIT;
rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device;
WINPR_ASSERT(pulse);
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(nullptr) - 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, pulse->stream_name, &pulse->sample_spec, nullptr);
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)
{
const size_t val = pa_usec_to_bytes(1000ULL * pulse->latency, &pulse->sample_spec);
buffer_attr.maxlength = UINT32_MAX;
buffer_attr.tlength = (val > UINT32_MAX) ? UINT32_MAX : (UINT32)val;
buffer_attr.prebuf = UINT32_MAX;
buffer_attr.minreq = UINT32_MAX;
buffer_attr.fragsize = UINT32_MAX;
flags |= PA_STREAM_ADJUST_LATENCY;
}
// NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)
pa_stream_flags_t eflags = (pa_stream_flags_t)flags;
if (pa_stream_connect_playback(pulse->stream, pulse->device_name,
pulse->latency > 0 ? &buffer_attr : nullptr, eflags, nullptr,
nullptr) < 0)
{
WLog_ERR(TAG, "error connecting playback stream");
pa_stream_unref(pulse->stream);
pulse->stream = nullptr;
pa_threaded_mainloop_unlock(pulse->mainloop);
return FALSE;
}
while (pulse->stream)
{
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 = nullptr;
}
if (pulse->mainloop)
{
pa_threaded_mainloop_free(pulse->mainloop);
pulse->mainloop = nullptr;
}
free(pulse->device_name);
free(pulse->client_name);
free(pulse->stream_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 = nullptr;
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 = nullptr;
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 = WINPR_C_ARRAY_INIT;
pa_volume_t left = 0;
pa_volume_t right = 0;
pa_operation* operation = nullptr;
rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device;
if (!rdpsnd_check_pulse(pulse, TRUE))
{
WLog_WARN(TAG, "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 = nullptr;
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, nullptr, 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);
const pa_usec_t val = latency / 1000;
if (val > UINT32_MAX)
return UINT32_MAX;
return (UINT32)val;
}
static UINT rdpsnd_pulse_parse_addin_args(rdpsndPulsePlugin* pulse, const ADDIN_ARGV* args)
{
COMMAND_LINE_ARGUMENT_A rdpsnd_pulse_args[] = {
{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr, "device" },
{ "reconnect_delay_seconds", COMMAND_LINE_VALUE_REQUIRED, "<reconnect_delay_seconds>",
nullptr, nullptr, -1, nullptr, "reconnect_delay_seconds" },
{ "client_name", COMMAND_LINE_VALUE_REQUIRED, "<client_name>", nullptr, nullptr, -1,
nullptr, "name of pulse client" },
{ "stream_name", COMMAND_LINE_VALUE_REQUIRED, "<stream_name>", nullptr, nullptr, -1,
nullptr, "name of pulse stream" },
{ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
};
const DWORD flags =
COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD;
WINPR_ASSERT(pulse);
WINPR_ASSERT(args);
const int status = CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_pulse_args, flags,
pulse, nullptr, nullptr);
if (status < 0)
return ERROR_INVALID_DATA;
const COMMAND_LINE_ARGUMENT_A* arg = rdpsnd_pulse_args;
const char* client_name = nullptr;
const char* stream_name = nullptr;
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, nullptr, 0);
if ((errno != 0) || (val > INT32_MAX))
return ERROR_INVALID_DATA;
pulse->reconnect_delay_seconds = (time_t)val;
}
CommandLineSwitchCase(arg, "client_name")
{
client_name = arg->Value;
}
CommandLineSwitchCase(arg, "stream_name")
{
stream_name = arg->Value;
}
CommandLineSwitchEnd(arg)
} while ((arg = CommandLineFindNextArgumentA(arg)) != nullptr);
if (!client_name)
client_name = freerdp_getApplicationDetailsString();
if (!stream_name)
stream_name = freerdp_getApplicationDetailsString();
pulse->client_name = _strdup(client_name);
pulse->stream_name = _strdup(stream_name);
if (!pulse->client_name || !pulse->stream_name)
return ERROR_OUTOFMEMORY;
return CHANNEL_RC_OK;
}
FREERDP_ENTRY_POINT(UINT VCAPITYPE pulse_freerdp_rdpsnd_client_subsystem_entry(
PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints))
{
WINPR_ASSERT(pEntryPoints);
rdpsndPulsePlugin* 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;
const ADDIN_ARGV* args = pEntryPoints->args;
UINT ret = rdpsnd_pulse_parse_addin_args(pulse, args);
if (ret != CHANNEL_RC_OK)
{
WLog_ERR(TAG, "error parsing arguments");
goto error;
}
pulse->reconnect_delay_seconds = 5;
pulse->reconnect_time = time(nullptr);
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);
goto error;
}
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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 */

View File

@@ -0,0 +1,30 @@
# 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(SYSTEM ${SNDIO_INCLUDE_DIRS})
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")

View File

@@ -0,0 +1,218 @@
/**
* 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 == nullptr || format == nullptr)
return FALSE;
if (sndio->hdl != nullptr)
return TRUE;
sndio->hdl = sio_open(SIO_DEVANY, SIO_PLAY, 0);
if (sndio->hdl == nullptr)
{
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 == nullptr)
return;
if (sndio->hdl != nullptr)
{
sio_stop(sndio->hdl);
sio_close(sndio->hdl);
sndio->hdl = nullptr;
}
}
static BOOL rdpsnd_sndio_set_volume(rdpsndDevicePlugin* device, UINT32 value)
{
rdpsndSndioPlugin* sndio = (rdpsndSndioPlugin*)device;
if (device == nullptr || sndio->hdl == nullptr)
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 == nullptr)
return;
rdpsnd_sndio_close(device);
free(sndio);
}
static BOOL rdpsnd_sndio_format_supported(rdpsndDevicePlugin* device, AUDIO_FORMAT* format)
{
if (format == nullptr)
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 == nullptr || sndio->hdl == nullptr)
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[] = { { nullptr, 0, nullptr, nullptr, nullptr, -1,
nullptr, nullptr } };
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, nullptr, nullptr);
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)) != nullptr);
return CHANNEL_RC_OK;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
FREERDP_ENTRY_POINT(UINT VCAPITYPE 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 == nullptr)
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;
}

View File

@@ -0,0 +1,26 @@
# FreeRDP: A Remote Desktop Protocol Implementation
# FreeRDP cmake build script
#
# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
define_channel_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 "")

View File

@@ -0,0 +1,348 @@
/**
* 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/wtsapi.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, nullptr, 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(nullptr, 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 = nullptr;
}
if (winmm->hThread)
{
(void)WaitForSingleObject(winmm->hThread, INFINITE);
(void)CloseHandle(winmm->hThread);
winmm->hThread = nullptr;
}
}
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(nullptr, 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 VCAPITYPE 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;
}