diff --git a/README.md b/README.md index 38263c96..98fc0382 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ sudo apt-get install cmake sudo apt-get install libavahi-compat-libdnssd-dev sudo apt-get install libplist-dev sudo apt-get install libssl-dev +sudo apt-get install libasound2-dev mkdir build cd build cmake .. @@ -64,7 +65,7 @@ For building on desktop linux, follow these steps as per your distribution: ```bash sudo apt-get install cmake libavahi-compat-libdnssd-dev libplist-dev libssl-dev \ libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-libav \ - gstreamer1.0-vaapi gstreamer1.0-plugins-bad + gstreamer1.0-vaapi gstreamer1.0-plugins-bad libasound2-dev mkdir build cd build cmake .. @@ -75,7 +76,7 @@ make ```bash sudo dnf install cmake avahi-compat-libdns_sd-devel libplist-devel openssl-devel \ gstreamer1-plugins-base-devel gstreamer1-libav gstreamer1-vaapi \ - gstreamer1-plugins-bad-free + gstreamer1-plugins-bad-free libasound2-dev mkdir build cd build cmake .. @@ -106,11 +107,11 @@ At the moment, these options are implemented: **-l**: Enables low-latency mode. Low-latency mode reduces latency by effectively rendering audio and video frames as soon as they are received, ignoring the associated timestamps. As a side effect, playback will be choppy and audio-video sync will be noticably off. -**-a (hdmi|analog|off)**: Set audio output device +**-a (hdmi|analog|hw:\*|off)**: Set audio output device, hw:* for ALSA renderer **-vr renderer**: Select a video renderer to use (rpi, gstreamer, or dummy) -**-ar renderer**: Select an audio renderer to use (rpi, gstreamer, or dummy) +**-ar renderer**: Select an audio renderer to use (rpi, gstreamer, alsa, or dummy) **-d**: Enables debug logging. Will lead to choppy playback due to heavy console output. diff --git a/renderers/CMakeLists.txt b/renderers/CMakeLists.txt index e5612504..f3d6cb25 100755 --- a/renderers/CMakeLists.txt +++ b/renderers/CMakeLists.txt @@ -16,6 +16,21 @@ set( RENDERER_SOURCES audio_renderer_dummy.c video_renderer_dummy.c ) set( RENDERER_LINK_LIBS "" ) set( RENDERER_INCLUDE_DIRS "" ) +# Flag to compile AAC decoder +set( NEED_AAC_COMPILE false ) + +# Check Alsa lib +find_package( ALSA ) +if( ALSA_FOUND ) + set( RENDERER_FLAGS "${RENDERER_FLAGS} -LASOUND -DHAS_ALSA_RENDERER" ) + set( RENDERER_SOURCES ${RENDERER_SOURCES} audio_renderer_alsa.c ) + set( RENDERER_LINK_LIBS ${RENDERER_LINK_LIBS} ${ALSA_LIBRARIES} ) + set( RENDERER_INCLUDE_DIRS ${RENDERER_INCLUDE_DIRS} ${ALSA_INCLUDE_DIRS} ) + set( NEED_AAC_COMPILE true ) +else() + message( STATUS "ALSA not found, skipping compilation of ALSA" ) +endif() + # Check for availability of OpenMAX libraries on Raspberry Pi find_library( BRCM_GLES_V2 brcmGLESv2 HINTS ${CMAKE_SYSROOT}/opt/vc/lib/ ) find_library( BRCM_EGL brcmEGL HINTS ${CMAKE_SYSROOT}/opt/vc/lib/ ) @@ -32,9 +47,7 @@ if( BRCM_GLES_V2 AND BRCM_EGL AND OPENMAXIL AND BCM_HOST AND VCOS AND VCHIQ_ARM ${CMAKE_SYSROOT}/opt/vc/include/interface/vmcs_host/linux ${CMAKE_SYSROOT}/opt/vc/src/hello_pi/libs/ilclient ) - option(BUILD_SHARED_LIBS "" OFF) - add_subdirectory(fdk-aac EXCLUDE_FROM_ALL) - add_subdirectory( h264-bitstream ) + set( NEED_AAC_COMPILE true ) set( CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_LIBOPENMAX=2 -DOMX -DOMX_SKIP64BIT -ftree-vectorize -pipe -DUSE_EXTERNAL_OMX -DHAVE_LIBBCM_HOST -DUSE_EXTERNAL_LIBBCM_HOST -DUSE_VCHIQ_ARM -Wno-psabi" ) @@ -47,11 +60,20 @@ if( BRCM_GLES_V2 AND BRCM_EGL AND OPENMAXIL AND BCM_HOST AND VCOS AND VCHIQ_ARM set( RENDERER_FLAGS "${RENDERER_FLAGS} -DHAS_RPI_RENDERER" ) set( RENDERER_SOURCES ${RENDERER_SOURCES} audio_renderer_rpi.c video_renderer_rpi.c ) - set( RENDERER_LINK_LIBS ${RENDERER_LINK_LIBS} ilclient airplay fdk-aac h264-bitstream ) + set( RENDERER_LINK_LIBS ${RENDERER_LINK_LIBS} ilclient ) else() message( STATUS "OpenMAX libraries not found, skipping compilation of Raspberry Pi renderer" ) endif() +# Need add AAC decoder lib +if( NEED_AAC_COMPILE ) + option(BUILD_SHARED_LIBS "" OFF) + add_subdirectory(fdk-aac EXCLUDE_FROM_ALL) + add_subdirectory( h264-bitstream ) + + set( RENDERER_LINK_LIBS ${RENDERER_LINK_LIBS} airplay fdk-aac h264-bitstream ) +endif() + # Check for availability of gstreamer find_package( PkgConfig ) if( PKG_CONFIG_FOUND ) diff --git a/renderers/audio_renderer.h b/renderers/audio_renderer.h index 142279a7..1c0c235e 100644 --- a/renderers/audio_renderer.h +++ b/renderers/audio_renderer.h @@ -40,6 +40,7 @@ typedef enum audio_renderer_type_e { typedef struct audio_renderer_config_s { audio_device_t device; + const char *alsaString; bool low_latency; } audio_renderer_config_t; @@ -62,6 +63,7 @@ typedef struct audio_renderer_s { audio_renderer_t *audio_renderer_dummy_init(logger_t *logger, video_renderer_t *video_renderer, audio_renderer_config_t const *config); audio_renderer_t *audio_renderer_rpi_init(logger_t *logger, video_renderer_t *video_renderer, audio_renderer_config_t const *config); audio_renderer_t *audio_renderer_gstreamer_init(logger_t *logger, video_renderer_t *video_renderer, audio_renderer_config_t const *config); +audio_renderer_t *audio_renderer_alsa_init(logger_t *logger, video_renderer_t *video_renderer, audio_renderer_config_t const *config); #ifdef __cplusplus } diff --git a/renderers/audio_renderer_alsa.c b/renderers/audio_renderer_alsa.c new file mode 100644 index 00000000..94b8cb6a --- /dev/null +++ b/renderers/audio_renderer_alsa.c @@ -0,0 +1,282 @@ +/** + * RPiPlay - An open-source AirPlay mirroring server for Raspberry Pi + * Copyright (C) 2019 Florian Draschbacher + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* + * AAC renderer using fdk-aac for decoding and ALSA for rendering +*/ + +#include "audio_renderer.h" + +#include + +#include "fdk-aac/libAACdec/include/aacdecoder_lib.h" +#include "alsa/asoundlib.h" + +// Check ALSA error code and show log +#define CHK_ALSA_ERRNO(err, msg, r) \ + if ( (err) < 0 )\ + logger_log(r, LOGGER_ERR, "%s: %s", msg, snd_strerror(err)) + +static bool mHasCtrlVolume; + + +typedef struct audio_renderer_alsa_s { + audio_renderer_t base; + audio_renderer_config_t const *config; + + HANDLE_AACDECODER audio_decoder; + snd_pcm_t *audio_renderer; + + // For volume control + snd_ctl_t *ctl; + snd_ctl_elem_id_t *ctl_elem_id; + +} audio_renderer_alsa_t; + +static const audio_renderer_funcs_t audio_renderer_alsa_funcs; + +static void audio_renderer_alsa_destroy_decoder(audio_renderer_alsa_t *renderer) { + aacDecoder_Close(renderer->audio_decoder); +} + +static int audio_renderer_alsa_init_decoder(audio_renderer_alsa_t *renderer) { + int ret; + renderer->audio_decoder = aacDecoder_Open(TT_MP4_RAW, 1); + if (renderer->audio_decoder == NULL) { + logger_log(renderer->base.logger, LOGGER_ERR, "aacDecoder open failed!"); + return -1; + } + /* ASC config binary data */ + UCHAR eld_conf[] = { 0xF8, 0xE8, 0x50, 0x00 }; + UCHAR *conf[] = { eld_conf }; + static UINT conf_len = sizeof(eld_conf); + ret = aacDecoder_ConfigRaw(renderer->audio_decoder, conf, &conf_len); + if (ret != AAC_DEC_OK) { + logger_log(renderer->base.logger, LOGGER_ERR, "Unable to set configRaw"); + return -2; + } + CStreamInfo *aac_stream_info = aacDecoder_GetStreamInfo(renderer->audio_decoder); + if (aac_stream_info == NULL) { + logger_log(renderer->base.logger, LOGGER_ERR, "aacDecoder_GetStreamInfo failed!"); + return -3; + } + + logger_log(renderer->base.logger, LOGGER_DEBUG, "> stream info: channel = %d\tsample_rate = %d\tframe_size = %d\taot = %d\tbitrate = %d", \ + aac_stream_info->channelConfig, aac_stream_info->aacSampleRate, + aac_stream_info->aacSamplesPerFrame, aac_stream_info->aot, aac_stream_info->bitRate); + return 1; +} + +static void audio_renderer_rpi_destroy_renderer(audio_renderer_alsa_t *renderer) { + CHK_ALSA_ERRNO( snd_pcm_drain( renderer->audio_renderer ), "ALSA PCM drain", renderer->base.logger); + CHK_ALSA_ERRNO( snd_pcm_close( renderer->audio_renderer ), "ALSA PCM close", renderer->base.logger); + CHK_ALSA_ERRNO( snd_ctl_close(renderer->ctl), "ALSA CTL close", renderer->base.logger); +} + +static int audio_renderer_rpi_init_renderer( audio_renderer_alsa_t *renderer, + __attribute__((unused)) video_renderer_t *video_renderer ) +{ + const char *device; + if ( renderer->config->alsaString ) + device = renderer->config->alsaString; + else + device = "default"; + // Opening a connection to PCM for audio playback + int err = snd_pcm_open(&renderer->audio_renderer, device, SND_PCM_STREAM_PLAYBACK, 0); + if ( err < 0 ) { + logger_log(renderer->base.logger, LOGGER_ERR, snd_strerror(err)); + return -51; + } + + err = snd_pcm_set_params(renderer->audio_renderer, + SND_PCM_FORMAT_S16_LE, + SND_PCM_ACCESS_RW_INTERLEAVED, + 2, + 44100, + 1, + 100000); + + if ( err < 0 ) { + logger_log(renderer->base.logger, LOGGER_ERR, snd_strerror(err)); + return -52; + } + // Opening a connection to the sound card to adjust the volume + CHK_ALSA_ERRNO( snd_ctl_open(&renderer->ctl, device, 0), "Ctrl open", renderer->base.logger ); + CHK_ALSA_ERRNO( snd_ctl_elem_id_malloc(&renderer->ctl_elem_id), "Ctrl mem allocation", renderer->base.logger ); + snd_ctl_elem_id_set_interface(renderer->ctl_elem_id, SND_CTL_ELEM_IFACE_MIXER); + + // Finding the right sound card item to adjust the volume + snd_ctl_elem_list_t* list; + snd_ctl_elem_list_alloca(&list); + CHK_ALSA_ERRNO( snd_ctl_elem_list(renderer->ctl, list), "Get ALSA control element list", renderer->base.logger ); + uint count = snd_ctl_elem_list_get_count(list); + CHK_ALSA_ERRNO( snd_ctl_elem_list_alloc_space(list, count), "Alloc space for ALSA ctrl ID", renderer->base.logger ); + CHK_ALSA_ERRNO( snd_ctl_elem_list(renderer->ctl, list), "Get ALSA control element list", renderer->base.logger ); + + mHasCtrlVolume = true; + for ( ;count != 0; count-- ) { + if ( strstr( snd_ctl_elem_list_get_name(list, count - 1), "Playback Volume" ) ) { + snd_ctl_elem_id_set_numid( renderer->ctl_elem_id, + snd_ctl_elem_list_get_numid(list, count - 1) ); + break; + } + + if (count == 1) { + logger_log(renderer->base.logger, LOGGER_INFO, "Cannot control Volume for selected device"); + mHasCtrlVolume = false; + } + } + + snd_ctl_elem_list_free_space(list); + + return 1; +} + +audio_renderer_t *audio_renderer_alsa_init(logger_t *logger, video_renderer_t *video_renderer, audio_renderer_config_t const *config) { + audio_renderer_alsa_t *renderer; + renderer = calloc(1, sizeof(audio_renderer_alsa_t)); + if (!renderer) { + return NULL; + } + renderer->base.logger = logger; + renderer->base.funcs = &audio_renderer_alsa_funcs; + renderer->base.type = AUDIO_RENDERER_RPI; + // Only refer to an existing video renderer if it's an RPI renderer, + // in which case we have to share resources + if (video_renderer && video_renderer->type != VIDEO_RENDERER_RPI) { + video_renderer = NULL; + } + renderer->config = config; + + if (audio_renderer_alsa_init_decoder(renderer) != 1) { + free(renderer); + renderer = NULL; + return NULL; + } + + if (audio_renderer_rpi_init_renderer(renderer, video_renderer) != 1) { + audio_renderer_alsa_destroy_decoder(renderer); + free(renderer); + renderer = NULL; + return NULL; + } + + return &renderer->base; +} + +static void audio_renderer_alsa_start(__attribute__((unused)) audio_renderer_t *renderer) { + // Nothing to do +} + +#ifdef DUMP_AUDIO +static FILE* file_pcm = NULL; +#endif + +static void audio_renderer_alsa_render_buffer( audio_renderer_t *renderer, + __attribute__((unused)) raop_ntp_t *ntp, + unsigned char *data, + int data_len, + __attribute__((unused)) uint64_t pts ) +{ + if (data_len == 0) return; + + audio_renderer_alsa_t *r = (audio_renderer_alsa_t *)renderer; + + logger_log(renderer->logger, LOGGER_DEBUG, "Got AAC data of %d bytes", data_len); + + // We assume that every buffer contains exactly 1 frame. + + AAC_DECODER_ERROR error; + + UCHAR *p_buffer[1] = {data}; + UINT buffer_size = data_len; + UINT bytes_valid = data_len; + error = aacDecoder_Fill(r->audio_decoder, p_buffer, &buffer_size, &bytes_valid); + if (error != AAC_DEC_OK) { + logger_log(renderer->logger, LOGGER_ERR, "aacDecoder_Fill error : %x", error); + } + + INT time_data_size = 4 * 480; + INT_PCM *p_time_data = malloc(time_data_size); // The buffer for the decoded AAC frames + error = aacDecoder_DecodeFrame(r->audio_decoder, p_time_data, time_data_size, 0); + if (error != AAC_DEC_OK) { + logger_log(renderer->logger, LOGGER_ERR, "aacDecoder_DecodeFrame error : 0x%x", error); + } + +#ifdef DUMP_AUDIO + if (file_pcm == NULL) { + file_pcm = fopen("/home/pi/Airplay.pcm", "wb"); + } + + fwrite(p_time_data, time_data_size, 1, file_pcm); +#endif + + snd_pcm_sframes_t playedFrames, receivedFrames = snd_pcm_bytes_to_frames(r->audio_renderer,time_data_size); + playedFrames = snd_pcm_writei(r->audio_renderer,p_time_data , receivedFrames); + if (playedFrames < 0) + playedFrames = snd_pcm_recover(r->audio_renderer, (int)playedFrames, 0); + if (playedFrames < 0) + logger_log(renderer->logger, LOGGER_ERR, snd_strerror((int) playedFrames)); + + if (playedFrames > 0 && playedFrames < receivedFrames) + logger_log( renderer->logger, + LOGGER_ERR, + "ALSA: Expected write %li, but wrote %li frames)\n", receivedFrames, playedFrames); + + free(p_time_data); +} + +static void audio_renderer_alsa_set_volume(audio_renderer_t *renderer, float volume) { + if ( !mHasCtrlVolume ) return; + + audio_renderer_alsa_t *r = (audio_renderer_alsa_t *)renderer; + long raw; + CHK_ALSA_ERRNO ( + snd_ctl_convert_from_dB(r->ctl, r->ctl_elem_id, (long)volume * 200, &raw, 0), + "Convert", + r->base.logger ); + snd_ctl_elem_value_t *value; + snd_ctl_elem_value_alloca(&value); + snd_ctl_elem_value_set_id(value, r->ctl_elem_id); + snd_ctl_elem_value_set_integer(value, 0, raw); + snd_ctl_elem_value_set_integer(value, 1, raw); + CHK_ALSA_ERRNO( snd_ctl_elem_write(r->ctl, value), "Set volume", r->base.logger ); +} + +static void audio_renderer_alsa_flush(__attribute__((unused)) audio_renderer_t *renderer) { + // Nothing to do +} + +static void audio_renderer_alsa_destroy(audio_renderer_t *renderer) { + if (renderer) { + audio_renderer_alsa_t *r = (audio_renderer_alsa_t *)renderer; + audio_renderer_alsa_flush(renderer); + audio_renderer_alsa_destroy_decoder(r); + audio_renderer_rpi_destroy_renderer(r); + free(renderer); + } +} + +static const audio_renderer_funcs_t audio_renderer_alsa_funcs = { + .start = audio_renderer_alsa_start, + .render_buffer = audio_renderer_alsa_render_buffer, + .set_volume = audio_renderer_alsa_set_volume, + .flush = audio_renderer_alsa_flush, + .destroy = audio_renderer_alsa_destroy, +}; diff --git a/rpiplay.cpp b/rpiplay.cpp index c5a67745..02ff5403 100755 --- a/rpiplay.cpp +++ b/rpiplay.cpp @@ -92,6 +92,9 @@ static const audio_renderer_list_entry_t audio_renderers[] = { #if defined(HAS_GSTREAMER_RENDERER) {"gstreamer", "GStreamer audio renderer", audio_renderer_gstreamer_init}, #endif +#if defined(HAS_ALSA_RENDERER) + {"alsa", "Alsa renderer", audio_renderer_alsa_init}, +#endif #if defined(HAS_DUMMY_RENDERER) {"dummy", "Dummy renderer; does not actually play audio", audio_renderer_dummy_init}, #endif @@ -217,9 +220,15 @@ int main(int argc, char *argv[]) { } else if (arg == "-a") { if (i == argc - 1) continue; std::string audio_device_name(argv[++i]); - audio_config.device = audio_device_name == "hdmi" ? AUDIO_DEVICE_HDMI : - audio_device_name == "analog" ? AUDIO_DEVICE_ANALOG : - AUDIO_DEVICE_NONE; + if ( audio_device_name.substr(0, 2) != "hw" ) { + audio_config.device = audio_device_name == "hdmi" ? AUDIO_DEVICE_HDMI : + audio_device_name == "analog" ? AUDIO_DEVICE_ANALOG : + AUDIO_DEVICE_NONE; + audio_config.alsaString = nullptr; + } else { + audio_config.alsaString = argv[i]; + } + } else if (arg == "-l") { video_config.low_latency = !video_config.low_latency; audio_config.low_latency = !audio_config.low_latency;