/*H********************************************************************************/
/*!
    \File voippc.c

    \Description
        Voip library interface.

    \Copyright
        Copyright (c) Electronic Arts 2004. ALL RIGHTS RESERVED.

    \Version 07/27/2004 (jbrookes) First Version
*/
/********************************************************************************H*/

/*** Include files ****************************************************************/

#pragma warning(push,0)
#include <windows.h>
#pragma warning(pop)

// dirtysock includes
#include "DirtySDK/platform.h"
#include "DirtySDK/dirtysock.h"
#include "DirtySDK/dirtysock/dirtythread.h"
#include "DirtySDK/dirtysock/netconn.h"

// voip includes
#include "DirtySDK/voip/voipdef.h"
#include "voippriv.h"
#include "voipcommon.h"
#include "DirtySDK/voip/voipcodec.h"
#include "DirtySDK/voip/voip.h"

/*** Include files ****************************************************************/

/*** Defines **********************************************************************/

//! transmission interval in milliseconds
#define VOIP_THREAD_SLEEP_DURATION        (20)

/*** Macros ***********************************************************************/

/*** Type Definitions *************************************************************/

//! VoIP module state
struct VoipRefT
{
    VoipCommonRefT          Common;             //!< cross-platform voip data (must come first!)
    uint8_t                 aIsReadyToParticipate[VOIP_MAXLOCALUSERS_EXTENDED]; //!< TRUE if user wanted to participate but was unable (on PC there can only be 1 participant)
    DWORD                   dwThreadId;         //!< thread ID
    volatile int32_t        iThreadState;       //!< control variable used during thread creation and exit (0=waiting for start, 1=running, 2=shutdown)

    uint32_t                bSpeakerOutput;     //!< whether we are outputting through speakers or not
};

/*** Function Prototypes **********************************************************/

/*** Variables ********************************************************************/

// Private Variables

//! affinity of the voip thread, if unset uses the global DirtySDK thread affinity
static int32_t _Voip_iThreadAffinity = -1;

// Public Variables

/*** Private Functions ************************************************************/

/*F*************************************************************************************************/
/*!
    \Function    _VoipStatusCb

    \Description
        Callback to handle change of headset status.

    \Input iLocalUserIndex - headset that has changed (currently ignored)
    \Input bStatus         - if TRUE the headset was inserted, else if FALSE it was removed
    \Input eUpdate         - functionality of the headset (input, output or both)
    \Input *pUserData      - pointer to callback user data

    \Version    1.0       04/01/04 (JLB) First Version
*/
/*************************************************************************************************F*/
static void _VoipStatusCb(int32_t iLocalUserIndex, uint32_t bStatus, VoipHeadsetStatusUpdateE eUpdate, void *pUserData)
{
    VoipRefT *pRef = (VoipRefT *)pUserData;

    /* we don't support each user having their own device, we will treat all users 
    as they have the same device status, since it is likely shared */
    for (iLocalUserIndex = 0; iLocalUserIndex < VOIP_MAXLOCALUSERS; iLocalUserIndex++)
    {
        if (bStatus > 0)
        {
            // Set the appropriate flags
            if (eUpdate == VOIP_HEADSET_STATUS_INPUT || eUpdate == VOIP_HEADSET_STATUS_INOUT)
            {
                pRef->Common.Connectionlist.uLocalUserStatus[iLocalUserIndex] |= VOIP_LOCAL_USER_INPUTDEVICEOK;
            }

            if (eUpdate == VOIP_HEADSET_STATUS_OUTPUT || eUpdate == VOIP_HEADSET_STATUS_INOUT)
            {
                pRef->Common.Connectionlist.uLocalUserStatus[iLocalUserIndex] |= VOIP_LOCAL_USER_OUTPUTDEVICEOK;
            }
        }
        else
        {
            // Reset the appropriate flags
            if (eUpdate == VOIP_HEADSET_STATUS_INPUT || eUpdate == VOIP_HEADSET_STATUS_INOUT)
            {
                pRef->Common.Connectionlist.uLocalUserStatus[iLocalUserIndex] &= ~VOIP_LOCAL_USER_INPUTDEVICEOK;
            }

            if (eUpdate == VOIP_HEADSET_STATUS_OUTPUT || eUpdate == VOIP_HEADSET_STATUS_INOUT)
            {
                pRef->Common.Connectionlist.uLocalUserStatus[iLocalUserIndex] &= ~VOIP_LOCAL_USER_OUTPUTDEVICEOK;
            }
        }

        if ((pRef->Common.Connectionlist.uLocalUserStatus[iLocalUserIndex] & VOIP_LOCAL_USER_INPUTDEVICEOK) &&
            (pRef->Common.Connectionlist.uLocalUserStatus[iLocalUserIndex] & VOIP_LOCAL_USER_OUTPUTDEVICEOK))
        {
            NetPrintf(("voippc: headset active (audio in: on, audio out: on\n)"));
            pRef->Common.Connectionlist.uLocalUserStatus[iLocalUserIndex] |= VOIP_LOCAL_USER_HEADSETOK;
        }
        else
        {
            NetPrintf(("voippc: headset inactive (audio in: %s, audio out: %s\n)", 
                ((pRef->Common.Connectionlist.uLocalUserStatus[iLocalUserIndex] & VOIP_LOCAL_USER_INPUTDEVICEOK) ? "on" : "off"), 
                ((pRef->Common.Connectionlist.uLocalUserStatus[iLocalUserIndex] & VOIP_LOCAL_USER_OUTPUTDEVICEOK) ? "on" : "off")));
            pRef->Common.Connectionlist.uLocalUserStatus[iLocalUserIndex] &= ~VOIP_LOCAL_USER_HEADSETOK;
        }
    }
}

/*F********************************************************************************/
/*!
    \Function   _VoipActivateLocalTalker

    \Description
        Register the participating talker on the given port.

    \Input *pVoipCommon     - voip common state
    \Input iLocalUserIndex  - local user index associated with the user
    \Input *pVoipUser       - VoipUserT to register
    \Input bActivate        - TRUE to activate, FALSE to deactivate

    \Output
        int32_t             - negative if failed, zero if successful

    \Version 04/25/2013 (tcho)
*/
/********************************************************************************F*/
static int32_t _VoipActivateLocalTalker(VoipCommonRefT *pVoipCommon, int32_t iLocalUserIndex, VoipUserT *pVoipUser, uint8_t bActivate)
{
    int32_t iResult = 0;

    if (pVoipUser != NULL)
    {
        VoipHeadsetControl(pVoipCommon->pHeadset, 'aloc', iLocalUserIndex, bActivate, pVoipUser);
        
        // mark local user as participating, or not
        pVoipCommon->Connectionlist.aIsParticipating[iLocalUserIndex] = bActivate;

        // reapply playback muting config based on the channel configuration always (a user who was blocking may be gone now)
        pVoipCommon->bApplyChannelConfig = TRUE;

        // announce jip add or remove event on connection that are already active
        VoipConnectionReliableBroadcastUser(&pVoipCommon->Connectionlist, iLocalUserIndex, bActivate);
    }
    else
    {
        NetPrintf(("voippc: cannot %s a null user %s participating state (local user index %d)\n",
            (bActivate ? "promote" : "demote"), (bActivate ? "to" : "from"), iLocalUserIndex));

        iResult = -1;
    }
    
    return(iResult);
}

/*F********************************************************************************/
/*!
    \Function   _VoipThread

    \Description
        Main Voip thread that updates all active connection and calls the
        XHV Engine's DoWork() function.

    \Input *pParam      - pointer to voip module state

    \Version 1.0 03/19/2004 (jbrookes) First Version
*/
/********************************************************************************F*/
static void _VoipThread(void *pParam)
{
    VoipRefT *pVoip = (VoipRefT *)pParam;
    uint32_t uProcessCt = 0;
    uint32_t uTime0, uTime1;
    int32_t iSleep;

    // wait until we're started up
    while(pVoip->iThreadState == 0)
    {
        NetConnSleep(VOIP_THREAD_SLEEP_DURATION);
    }

    // execute until we're killed
    while(pVoip->iThreadState == 1)
    {
        uTime0 = NetTick();

        NetCritEnter(&pVoip->Common.ThreadCrit);
        NetCritEnter(&pVoip->Common.Connectionlist.NetCrit);
        
        if (pVoip->Common.bApplyChannelConfig)
        {
            pVoip->Common.bApplyChannelConfig = FALSE;
            VoipCommonApplyChannelConfig((VoipCommonRefT *)pVoip);
        }     
        
        // update status of remote users
        VoipCommonUpdateRemoteStatus(&pVoip->Common);
        NetCritLeave(&pVoip->Common.Connectionlist.NetCrit);

        // update connections
        VoipConnectionUpdate(&pVoip->Common.Connectionlist);

        // set headset mute status based on connectionlist send mask
        VoipHeadsetControl(pVoip->Common.pHeadset, 'mute', 0, pVoip->Common.Connectionlist.uSendMask == 0, NULL);

        /* the connectionlist netcrit is entered before calling VoipHeadsetProcess() to protect access to the
           codec used between the socket recv thread (decode) and the voip thread (reset/encode) */
        NetCritEnter(&pVoip->Common.Connectionlist.NetCrit);

        // update headset
        VoipHeadsetProcess(pVoip->Common.pHeadset, uProcessCt++);

        NetCritLeave(&pVoip->Common.Connectionlist.NetCrit);

        // update ttos status
        pVoip->Common.uTtsStatus = VoipHeadsetStatus(pVoip->Common.pHeadset, 'ttos', 0, NULL, 0);

        NetCritLeave(&pVoip->Common.ThreadCrit);

        uTime1 = NetTick();

        iSleep = VOIP_THREAD_SLEEP_DURATION - NetTickDiff(uTime1, uTime0);

        // wait for next tick
        if (iSleep >= 0)
        {
            NetConnSleep(iSleep);
        }
        else
        {
            NetPrintf(("voippc: [WARNING] Overall voip thread update took %d ms\n", NetTickDiff(uTime1, uTime0)));
        }
    }

    // cleanup voice and give voipheadset process time to destroy SAPI related components
    VoipHeadsetControl(pVoip->Common.pHeadset, 'uvoc', 0, 0, NULL);
    VoipHeadsetProcess(pVoip->Common.pHeadset, uProcessCt++);

    // indicate we're exiting
    pVoip->iThreadState = 0;
}

/*F********************************************************************************/
/*!
    \Function   _VoipGetActiveParticipantIndex

    \Description
        Gets the user index of the current active user (there can only be one)

    \Input *pVoip   - module state

    \Output
        int32_t     - User index 0-4 or VOIP_INVALID_LOCAL_USER_INDEX if none.

    \Version 03/14/2019 (cvienneau)
*/
/********************************************************************************F*/
static int32_t _VoipGetActiveParticipantIndex(VoipRefT *pVoip)
{
    uint32_t uIndex;
    for (uIndex = 0; uIndex < VOIP_MAXLOCALUSERS; uIndex++)
    {
        if (pVoip->Common.Connectionlist.aIsParticipating[uIndex] == TRUE)
        {
            return(uIndex);
        }
    }
    return(VOIP_INVALID_LOCAL_USER_INDEX);
}

/*F********************************************************************************/
/*!
    \Function   _VoipGetFirstReadyParticipantIndex

    \Description
        Gets the user index of the first user who would like to be the active user

    \Input *pVoip   - module state

    \Output
        int32_t     - User index 0-4 or VOIP_INVALID_LOCAL_USER_INDEX if none.

    \Version 03/14/2019 (cvienneau)
*/
/********************************************************************************F*/
static int32_t _VoipGetFirstReadyParticipantIndex(VoipRefT *pVoip)
{
    uint32_t uIndex;
    for (uIndex = 0; uIndex < VOIP_MAXLOCALUSERS; uIndex++)
    {
        if (pVoip->aIsReadyToParticipate[uIndex] == TRUE)
        {
            return(uIndex);
        }
    }
    return(VOIP_INVALID_LOCAL_USER_INDEX);
}

/*** Public Functions *************************************************************/


/*F********************************************************************************/
/*!
    \Function   VoipStartup

    \Description
        Prepare VoIP for use.

    \Input iMaxPeers    - maximum number of peers supported (up to VOIP_MAXCONNECT)
    \Input iData        - platform-specific - unused for PC

    \Output
        VoipRefT        - state reference that is passed to all other functions

    \Version 1.0 03/02/2004 (jbrookes) First Version
*/
/********************************************************************************F*/
VoipRefT *VoipStartup(int32_t iMaxPeers, int32_t iData)
{
    VoipRefT *pVoip;
    VoipCommonRefT *pVoipCommon;
    DirtyThreadConfigT ThreadConfig;

    // common startup
    if ((pVoip = VoipCommonStartup(iMaxPeers, sizeof(*pVoip), _VoipStatusCb, iData)) == NULL)
    {
        return(NULL);
    }
    pVoipCommon = (VoipCommonRefT *)pVoip;

    // bPrivileged is always TRUE on PC
    pVoipCommon->bPrivileged = TRUE;

    // configure thread parameters
    ds_memclr(&ThreadConfig, sizeof(ThreadConfig));
    ThreadConfig.pName = "VoIP";
    ThreadConfig.iAffinity = (_Voip_iThreadAffinity == -1) ? NetConnStatus('affn', 0, NULL, 0) : _Voip_iThreadAffinity;
    ThreadConfig.iPriority = THREAD_PRIORITY_HIGHEST;

    // create worker thread
    if (DirtyThreadCreate(_VoipThread, pVoip, &ThreadConfig) == 0)
    {
        // tell thread to start executing
        pVoip->iThreadState = 1;
    }
    else
    {
        pVoip->iThreadState = 0;
        VoipShutdown(pVoip, 0);
        return(NULL);
    }

    // return to caller
    return(pVoip);
}

/*F********************************************************************************/
/*!
    \Function   VoipShutdown

    \Description
        Release all VoIP resources.

    \Input *pVoip           - module state from VoipStartup
    \Input uShutdownFlags   - NET_SHUTDOWN_* flags

    \Version 03/02/2004 (jbrookes) First Version
*/
/********************************************************************************F*/
void VoipShutdown(VoipRefT *pVoip, uint32_t uShutdownFlags)
{
    // make sure we're really started up
    if (VoipGetRef() == NULL)
    {
        NetPrintf(("voippc: module shutdown called when not in a started state\n"));
        return;
    }

    // tell thread to shut down and wait for shutdown confirmation (if running)
    if (pVoip->iThreadState == 1)
    {
        for (pVoip->iThreadState = 2; pVoip->iThreadState != 0; )
        {
            NetPrintf(("voippc: waiting for shutdown\n"));
            NetConnSleep(VOIP_THREAD_SLEEP_DURATION);
        }
    }

    // unregister local talker

    // common shutdown (must come last as this frees the memory)
    VoipCommonShutdown(&pVoip->Common);
}

/*F********************************************************************************/
/*!
    \Function   VoipSetLocalUser

    \Description
        Register/unregister specified local user with the voip sub-system.

    \Input *pVoip           - module state from VoipStartup
    \Input iLocalUserIndex  - local user index
    \Input bRegister        - true to register user, false to unregister user

    \Version 04/15/2004 (jbrookes)
*/
/********************************************************************************F*/
void VoipSetLocalUser(VoipRefT *pVoip, int32_t iLocalUserIndex, uint32_t bRegister)
{
    if ((iLocalUserIndex < VOIP_MAXLOCALUSERS) && (iLocalUserIndex >= 0))
    {
        NetCritEnter(&pVoip->Common.ThreadCrit);

        if (bRegister)
        {
            if (VOIP_NullUser(&pVoip->Common.Connectionlist.LocalUsers[iLocalUserIndex]))
            {
                VoipUserT voipUser;
                ds_memclr(&voipUser, sizeof(voipUser));

                voipUser.ePlatform = VOIP_PLATFORM_PC;
                NetConnStatus('acid', iLocalUserIndex, &voipUser.AccountInfo.iAccountId, sizeof(voipUser.AccountInfo.iAccountId));
                NetConnStatus('peid', iLocalUserIndex, &voipUser.AccountInfo.iPersonaId, sizeof(voipUser.AccountInfo.iPersonaId));

                // register the local talker
                NetPrintf(("voippc: registering a local user %lld at index %d\n", voipUser.AccountInfo.iPersonaId, iLocalUserIndex));
                VOIP_CopyUser(&pVoip->Common.Connectionlist.LocalUsers[iLocalUserIndex], &voipUser);
            }
            else
            {
                NetPrintf(("voippc: warning -- ignoring attempt to register user index %d because user %lld is already registered at that index\n",
                    iLocalUserIndex, pVoip->Common.Connectionlist.LocalUsers[iLocalUserIndex].AccountInfo.iPersonaId));
            }
        }
        else
        {
            if (!VOIP_NullUser(&pVoip->Common.Connectionlist.LocalUsers[iLocalUserIndex]))
            {
                // if a participating user demote him first 
                if (pVoip->Common.Connectionlist.aIsParticipating[iLocalUserIndex] == TRUE)
                {
                    VoipCommonActivateLocalUser(&pVoip->Common, iLocalUserIndex, FALSE);
                }

                NetPrintf(("voippc: unregistering local user %lld at index %d\n", pVoip->Common.Connectionlist.LocalUsers[iLocalUserIndex].AccountInfo.iPersonaId, iLocalUserIndex));
                ds_memclr(&pVoip->Common.Connectionlist.LocalUsers[iLocalUserIndex], sizeof(pVoip->Common.Connectionlist.LocalUsers[iLocalUserIndex]));
            }
            else
            {
                NetPrintf(("voippc: warning -- ignoring attempt to unregister local user because it is not yet registered\n"));
            }
        }

        NetCritLeave(&pVoip->Common.ThreadCrit);
    }
    else
    {
        NetPrintf(("voippc: VoipSetLocalUser() invoked with an invalid user index %d\n", iLocalUserIndex));
    }
}

/*F********************************************************************************/
/*!
    \Function   VoipCommonActivateLocalUser

    \Description
        Promote/demote specified local user to/from "participating" state

    \Input *pVoipCommon     - voip common state
    \Input iLocalUserIndex  - local user index
    \Input bActivate        - TRUE to activate, FALSE to deactivate

    \Version 04/25/2013 (tcho)
*/
/********************************************************************************F*/
void VoipCommonActivateLocalUser(VoipCommonRefT *pVoipCommon, int32_t iLocalUserIndex, uint8_t bActivate)
{
    VoipRefT* pVoip = VoipGetRef();
    if ((iLocalUserIndex < NETCONN_MAXLOCALUSERS) && (iLocalUserIndex >= 0))
    {
        NetCritEnter(&pVoipCommon->ThreadCrit);

        if (!VOIP_NullUser(&pVoipCommon->Connectionlist.LocalUsers[iLocalUserIndex]))
        {
            if (bActivate == TRUE)
            {
                if (_VoipGetActiveParticipantIndex(pVoip) == VOIP_INVALID_LOCAL_USER_INDEX)
                {
                    NetPrintf(("voippc: promoting local user %lld to participating state.\n", &pVoipCommon->Connectionlist.LocalUsers[iLocalUserIndex].AccountInfo.iPersonaId));
                    _VoipActivateLocalTalker(pVoipCommon, iLocalUserIndex, &pVoipCommon->Connectionlist.LocalUsers[iLocalUserIndex], TRUE);
                }
                else
                {
                    NetPrintf(("voippc: a user is already active, %lld to ready state.\n", &pVoipCommon->Connectionlist.LocalUsers[iLocalUserIndex].AccountInfo.iPersonaId));
                }
                pVoip->aIsReadyToParticipate[iLocalUserIndex] = TRUE;
            }
            else
            {
                NetPrintf(("voippc: demoting local user %lld from participating/ready state\n", &pVoipCommon->Connectionlist.LocalUsers[iLocalUserIndex].AccountInfo.iPersonaId));
                
                // were we the active user, if so deactivate
                if (_VoipGetActiveParticipantIndex(pVoip) == iLocalUserIndex)
                {
                    _VoipActivateLocalTalker(pVoipCommon, iLocalUserIndex, &pVoipCommon->Connectionlist.LocalUsers[iLocalUserIndex], FALSE);
                }
                pVoip->aIsReadyToParticipate[iLocalUserIndex] = FALSE;

                // do we still have an active user?
                if (_VoipGetActiveParticipantIndex(pVoip) == VOIP_INVALID_LOCAL_USER_INDEX)
                {
                    // no, but do we have someone who still wants to be the active user? if so make them the active user
                    int32_t iNewParticpantIndex = _VoipGetFirstReadyParticipantIndex(pVoip);
                    if (iNewParticpantIndex != VOIP_INVALID_LOCAL_USER_INDEX)
                    {
                        NetPrintf(("voippc: promoting local user %lld to participating state\n", &pVoipCommon->Connectionlist.LocalUsers[iNewParticpantIndex].AccountInfo.iPersonaId));
                        _VoipActivateLocalTalker(pVoipCommon, iNewParticpantIndex, &pVoipCommon->Connectionlist.LocalUsers[iNewParticpantIndex], TRUE);
                    }
                }
            }
        }
        else
        {
            NetPrintf(("voippc: VoipActivateLocalUser() failed because no local user registered at index %d\n", iLocalUserIndex));
        }

        NetCritLeave(&pVoipCommon->ThreadCrit);
    }
    else
    {
        NetPrintf(("voippc: VoipActivateLocalUser() invoked with an invalid user index %d\n", iLocalUserIndex));
    }
}

/*F********************************************************************************/
/*!
    \Function   VoipStatus

    \Description
        Return status.

    \Input *pVoip   - module state from VoipStartup
    \Input iSelect  - status selector
    \Input iValue   - selector-specific
    \Input *pBuf    - [out] storage for selector-specific output
    \Input iBufSize - size of output buffer

    \Output
        int32_t     - status-specific data

    \Notes
        iSelect can be one of the following:

        \verbatim
            No PC-specific VoipStatus() selectors
        \endverbatim

        Unhandled selectors are passed through to VoipCommonStatus(), and
        further to VoipHeadsetStatus() if unhandled there.

    \Version 1.0 03/02/2004 (jbrookes)
*/
/********************************************************************************F*/
int32_t VoipStatus(VoipRefT *pVoip, int32_t iSelect, int32_t iValue, void *pBuf, int32_t iBufSize)
{
    return(VoipCommonStatus(&pVoip->Common, iSelect, iValue, pBuf, iBufSize));
}

/*F********************************************************************************/
/*!
    \Function   VoipControl

    \Description
        Set control options.

    \Input *pVoip   - module state from VoipStartup
    \Input iControl - status selector
    \Input iValue   - selector-specific
    \Input *pValue  - register value

    \Output
        int32_t     - selector-specific data

    \Notes
        iControl can be one of the following:

        \verbatim
            'affn' - set the voip thread cpu affinity, please call this before voip startup is called with pVoip being null
            '+pbk' - enable voice playback for a given remote user (pValue is the remote user VoipUserT).
            '-pbk' - disable voice playback for a given remote user (pValue is the remote user VoipUserT).
            'cdec' - switch to new codec; passes through to VoipHeadsetControl()
            'creg' - register codec
            'plvl' - set the output power level
        \endverbatim

        Unhandled selectors are passed through to VoipCommonControl(), and
        further to VoipHeadsetControl() if unhandled there.

    \Version 1.0 04/08/2007 (cadam)
*/
/********************************************************************************F*/
int32_t VoipControl(VoipRefT *pVoip, int32_t iControl, int32_t iValue, void *pValue)
{
    if ('affn' == iControl)
    {
        _Voip_iThreadAffinity = iValue;
        return(0);
    }
    if (iControl == 'cdec')
    {
        int32_t iResult;
        // if switching codecs we require sole access to thread critical section
        NetCritEnter(&pVoip->Common.ThreadCrit);
        iResult = VoipHeadsetControl(pVoip->Common.pHeadset, iControl, iValue, 0, pValue);
        NetCritLeave(&pVoip->Common.ThreadCrit);
        return(iResult);
    }
    if (iControl == 'creg')
    {
        VoipCodecRegister(iValue, pValue);
        return(0);
    }
    if (iControl == '+pbk')
    {
        return(VoipHeadsetControl(pVoip->Common.pHeadset, '+pbk', iValue, 0, (VoipUserT *)pValue));
    }
    if (iControl == '-pbk')
    {
        return(VoipHeadsetControl(pVoip->Common.pHeadset, '-pbk', iValue, 0, (VoipUserT *)pValue));
    }
    if (iControl == 'plvl')
    {
        int32_t iFractal;
        float fValue = *(float *)pValue;
        if ((fValue >= 20.0f) || (fValue < 0.0f))
        {
            NetPrintf(("voippc: setting power level failed; value must be less than 20 and greater than or equal to 0."));
            return(-1);
        }
        iFractal = (int32_t)(fValue * (1 << VOIP_CODEC_OUTPUT_FRACTIONAL));
        VoipCodecControl(-1, iControl, iFractal, 0, NULL);
        return(0);
    }

    // let VoipCommonControl take a shot at it
    return(VoipCommonControl(&pVoip->Common, iControl, iValue, pValue));
}
