/* * Copyright (C) 2017 "Audiokinetic Inc" * * 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 _GNU_SOURCE #include <stdio.h> #include <string.h> #include <alsa/asoundlib.h> #include <alsa/pcm.h> #include <json-c/json.h> #include "wrap-json.h" #include "ahl-binding.h" #include "ahl-policy.h" extern AHLCtxT g_AHLCtx; // TODO: Hash from endpoint ID information instead static endpointID_t CreateNewSourceID() { endpointID_t newID = g_AHLCtx.nextSourceEndpointID; g_AHLCtx.nextSourceEndpointID++; return newID; } // TODO: Hash from endpoint ID information instead static endpointID_t CreateNewSinkID() { endpointID_t newID = g_AHLCtx.nextSinkEndpointID; g_AHLCtx.nextSinkEndpointID++; return newID; } // Watchout: This function uses strtok and is destructive on the input string (use a copy) static int SeparateDomainFromDeviceURI( char * in_pDeviceURI, char ** out_pDomain, char ** out_pDevice) { *out_pDomain = strtok(in_pDeviceURI, "."); if (*out_pDomain == NULL) { AFB_ERROR("Error tokenizing device URI -> %s",in_pDeviceURI); return 1; } // TODO: Validate domain is known string (e.g. ALSA,Pulse,GStreamer) *out_pDevice = strtok(NULL, "."); if (*out_pDevice == NULL) { AFB_ERROR("Error tokenizing device URI -> %s",in_pDeviceURI); return 1; } return 0; } static int IsAlsaDomain(const char * in_pDomainStr) { return (int) (strcasecmp(in_pDomainStr,AHL_DOMAIN_ALSA) == 0); } static int IsPulseDomain(const char * in_pDomainStr) { return (int) (strcasecmp(in_pDomainStr,AHL_DOMAIN_PULSE) == 0); } static int IsGStreamerDomain(const char * in_pDomainStr) { return (int) (strcasecmp(in_pDomainStr,AHL_DOMAIN_GSTREAMER) == 0); } static int IsExternalDomain(const char * in_pDomainStr) { return (int) (strcasecmp(in_pDomainStr,AHL_DOMAIN_EXTERNAL) == 0); } static int FillALSAPCMInfo( snd_pcm_t * in_pPcmHandle, EndpointInfoT * out_pEndpointInfo ) { g_assert_nonnull(in_pPcmHandle); g_assert_nonnull(out_pEndpointInfo); snd_pcm_type_t pcmType = 0; snd_pcm_info_t * pPcmInfo = NULL; int iAlsaRet = 0; const char * pCardName = NULL; int retVal = 0; snd_ctl_t * ctlHandle = NULL; snd_ctl_card_info_t * ctlInfo = NULL; snd_pcm_info_alloca(&pPcmInfo); snd_ctl_card_info_alloca(&ctlInfo); // retrieve PCM type pcmType = snd_pcm_type(in_pPcmHandle); switch (pcmType) { case SND_PCM_TYPE_HW: out_pEndpointInfo->deviceURIType = DEVICEURITYPE_ALSA_HW; break; case SND_PCM_TYPE_DMIX: out_pEndpointInfo->deviceURIType = DEVICEURITYPE_ALSA_DMIX; break; case SND_PCM_TYPE_SOFTVOL: out_pEndpointInfo->deviceURIType = DEVICEURITYPE_ALSA_SOFTVOL; break; case SND_PCM_TYPE_PLUG: out_pEndpointInfo->deviceURIType = DEVICEURITYPE_ALSA_PLUG; break; default: out_pEndpointInfo->deviceURIType = DEVICEURITYPE_ALSA_OTHER; break; } iAlsaRet = snd_pcm_info(in_pPcmHandle,pPcmInfo); if (iAlsaRet < 0) { AFB_WARNING("Error retrieving PCM device info"); return 1; } // get card number out_pEndpointInfo->alsaInfo.cardNum = snd_pcm_info_get_card(pPcmInfo); if ( out_pEndpointInfo->alsaInfo.cardNum < 0 ) { AFB_WARNING("No Alsa card number available"); return 1; } // get device number out_pEndpointInfo->alsaInfo.deviceNum = snd_pcm_info_get_device(pPcmInfo); if ( out_pEndpointInfo->alsaInfo.deviceNum < 0 ) { AFB_WARNING("No Alsa device number available"); return 1; } // get sub-device number out_pEndpointInfo->alsaInfo.subDeviceNum = snd_pcm_info_get_subdevice(pPcmInfo); if ( out_pEndpointInfo->alsaInfo.subDeviceNum < 0 ) { AFB_WARNING("No Alsa subdevice number available"); return 1; } char cardName[32]; sprintf(cardName, "hw:%d", out_pEndpointInfo->alsaInfo.cardNum); iAlsaRet = snd_ctl_open(&ctlHandle, cardName, 0); if ( iAlsaRet < 0 ) { AFB_WARNING("Could not open ALSA card control"); return 1; } iAlsaRet = snd_ctl_card_info(ctlHandle, ctlInfo); if ( iAlsaRet < 0 ) { AFB_WARNING("Could not retrieve ALSA card info"); snd_ctl_close(ctlHandle); return 1; } // Populate unique target card name pCardName = snd_ctl_card_info_get_id(ctlInfo); if (pCardName == NULL) { AFB_WARNING("No Alsa card name available"); snd_ctl_close(ctlHandle); return 1; } out_pEndpointInfo->gsDeviceName = g_strdup(pCardName); snd_ctl_close(ctlHandle); return retVal; } EndpointInfoT * InitEndpointInfo() { EndpointInfoT * pEndpointInfo = (EndpointInfoT*) malloc(sizeof(EndpointInfoT)); memset(pEndpointInfo,0,sizeof(EndpointInfoT)); pEndpointInfo->endpointID = AHL_UNDEFINED; pEndpointInfo->type = ENDPOINTTYPE_MAXVALUE; pEndpointInfo->deviceURIType = DEVICEURITYPE_MAXVALUE; pEndpointInfo->alsaInfo.cardNum = AHL_UNDEFINED; pEndpointInfo->alsaInfo.deviceNum = AHL_UNDEFINED; pEndpointInfo->alsaInfo.subDeviceNum = AHL_UNDEFINED; pEndpointInfo->format.sampleRate = AHL_UNDEFINED; pEndpointInfo->format.numChannels = AHL_UNDEFINED; pEndpointInfo->format.sampleType = AHL_FORMAT_UNKNOWN; pEndpointInfo->pPropTable = g_hash_table_new(g_str_hash, g_str_equal); return pEndpointInfo; } void TermEndpointInfo( EndpointInfoT * out_pEndpointInfo ) { #define SAFE_FREE(__ptr__) if(__ptr__) g_free(__ptr__); __ptr__ = NULL; SAFE_FREE(out_pEndpointInfo->gsDeviceName); SAFE_FREE(out_pEndpointInfo->gsDisplayName); SAFE_FREE(out_pEndpointInfo->gsDeviceDomain); SAFE_FREE(out_pEndpointInfo->pRoleName); SAFE_FREE(out_pEndpointInfo->gsDeviceURI); SAFE_FREE(out_pEndpointInfo->gsHALAPIName); if (out_pEndpointInfo->pPropTable) { // Free json_object for all property values GHashTableIter iter; gpointer key, value; g_hash_table_iter_init (&iter, out_pEndpointInfo->pPropTable); while (g_hash_table_iter_next (&iter, &key, &value)) { if (value) json_object_put(value); } g_hash_table_remove_all(out_pEndpointInfo->pPropTable); g_hash_table_destroy(out_pEndpointInfo->pPropTable); out_pEndpointInfo->pPropTable = NULL; } // GLib automatically frees item when removed from the array } // For a given audio role int EnumerateDevices(json_object * in_jDeviceArray, char * in_pAudioRole, EndpointTypeT in_deviceType, GPtrArray * out_pEndpointArray) { g_assert_nonnull(in_jDeviceArray); int iNumberDevices = json_object_array_length(in_jDeviceArray); // Parse and validate list of available devices for (int i = 0; i < iNumberDevices; i++) { char * pDeviceURIDomain = NULL; char * pFullDeviceURI = NULL; char * pDeviceURIPCM = NULL; int err = 0; json_object * jDevice = json_object_array_get_idx(in_jDeviceArray,i); if (jDevice == NULL) { AFB_WARNING("Invalid device array -> %s",json_object_to_json_string(in_jDeviceArray)); continue; } // strip domain name from URI pFullDeviceURI = (char *)json_object_get_string(jDevice); char * pFullDeviceURICopy = g_strdup(pFullDeviceURI); // strtok is destructive err = SeparateDomainFromDeviceURI(pFullDeviceURICopy,&pDeviceURIDomain,&pDeviceURIPCM); if (err) { AFB_WARNING("Invalid device URI string -> %s",pFullDeviceURICopy); continue; } EndpointInfoT * pEndpointInfo = InitEndpointInfo(); g_assert_nonnull(pEndpointInfo); // non ALSA URI are simply passed to application (no validation) at this time // In Non ALSA case devices in config are assumed to be available, if not application can fallback on explicit device selection pEndpointInfo->gsDeviceName = g_strdup(pDeviceURIPCM); pEndpointInfo->gsDeviceDomain = g_strdup(pDeviceURIDomain); pEndpointInfo->gsDeviceURI = g_strdup(pDeviceURIPCM); pEndpointInfo->pRoleName = g_strdup(in_pAudioRole); g_free(pFullDeviceURICopy); pFullDeviceURICopy = NULL; pDeviceURIDomain = NULL; //Derived from above mem pDeviceURIPCM = NULL; //Derived from above mem if (IsAlsaDomain(pEndpointInfo->gsDeviceDomain)) { // TODO: Missing support for loose name matching // This will require using ALSA hints to get PCM names // And would iterate over all available devices matching string (possibly all if no filtering is desired for a certain role) // Get PCM handle snd_pcm_t * pPcmHandle = NULL; snd_pcm_stream_t streamType = in_deviceType == ENDPOINTTYPE_SOURCE ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK; err = snd_pcm_open(&pPcmHandle, pEndpointInfo->gsDeviceURI, streamType, 0); if (err < 0) { AFB_NOTICE("Alsa PCM device was not found -> %s", pEndpointInfo->gsDeviceURI); continue; } err = FillALSAPCMInfo(pPcmHandle,pEndpointInfo); if (err) { AFB_WARNING("Unable to retrieve PCM information for PCM -> %s",pEndpointInfo->gsDeviceURI); snd_pcm_close(pPcmHandle); continue; } snd_pcm_close(pPcmHandle); } else if (IsPulseDomain(pEndpointInfo->gsDeviceDomain)) { // Pulse domain // For now display name is device URI directly, could extrapolated using more heuristics or even usins Pulse API later on pEndpointInfo->deviceURIType = DEVICEURITYPE_NOT_ALSA; } else if (IsGStreamerDomain(pEndpointInfo->gsDeviceDomain)){ // GStreamer domain // For now display name is device URI directly, could extrapolated using more heuristics or even usins GStreamer API later on pEndpointInfo->deviceURIType = DEVICEURITYPE_NOT_ALSA; } else if (IsExternalDomain(pEndpointInfo->gsDeviceDomain)){ // External domain pEndpointInfo->deviceURIType = DEVICEURITYPE_NOT_ALSA; } else { // Unknown domain AFB_WARNING("Unknown domain in device URI string -> %s",pFullDeviceURI); continue; } pEndpointInfo->endpointID = in_deviceType == ENDPOINTTYPE_SOURCE ? CreateNewSourceID() : CreateNewSinkID(); pEndpointInfo->type = in_deviceType; // Assigned by policy initialization pEndpointInfo->gsDisplayName = malloc(AHL_STR_MAX_LENGTH*sizeof(char)); memset(pEndpointInfo->gsDisplayName,0,AHL_STR_MAX_LENGTH*sizeof(char)); pEndpointInfo->gsHALAPIName = malloc(AHL_STR_MAX_LENGTH*sizeof(char)); memset(pEndpointInfo->gsDisplayName,0,AHL_STR_MAX_LENGTH*sizeof(char)); // add to structure to list of available devices g_ptr_array_add(out_pEndpointArray, pEndpointInfo); } // for all devices AFB_DEBUG ("Audio high-level - Enumerate devices done"); return 0; }