/******************************************************************************\
 * Copyright (c) 2004-2020
 *
 * Author(s):
 *  Volker Fischer
 *
 ******************************************************************************
 *
 * 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 2 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
 *
\******************************************************************************/

#include "sound.h"


/* Implementation *************************************************************/
CSound::CSound ( void           (*fpNewProcessCallback) ( CVector<short>& psData, void* arg ),
                 void*          arg,
                 const int      iCtrlMIDIChannel,
                 const bool     ,
                 const QString& ) :
    CSoundBase ( "CoreAudio", true, fpNewProcessCallback, arg, iCtrlMIDIChannel ),
    midiInPortRef ( static_cast<MIDIPortRef> ( NULL ) )
{
    // Apple Mailing Lists: Subject: GUI Apps should set kAudioHardwarePropertyRunLoop
    // in the HAL, From: Jeff Moore, Date: Fri, 6 Dec 2002
    // Most GUI applciations have several threads on which they receive
    // notifications already, so the having the HAL's thread around is wasteful.
    // Here is what you should do: On the thread you want the HAL to use for
    // notifications (for most apps, this will be the main thread), add the
    // following lines of code:
    // tell the HAL to use the current thread as it's run loop
    CFRunLoopRef               theRunLoop = CFRunLoopGetCurrent();
    AudioObjectPropertyAddress property   = { kAudioHardwarePropertyRunLoop,
                                              kAudioObjectPropertyScopeGlobal,
                                              kAudioObjectPropertyElementMaster };

    AudioObjectSetPropertyData ( kAudioObjectSystemObject,
                                 &property,
                                 0,
                                 NULL,
                                 sizeof ( CFRunLoopRef ),
                                 &theRunLoop );


    // Get available input/output devices --------------------------------------
    UInt32                     iPropertySize = 0;
    AudioObjectPropertyAddress stPropertyAddress;

    stPropertyAddress.mScope   = kAudioObjectPropertyScopeGlobal;
    stPropertyAddress.mElement = kAudioObjectPropertyElementMaster;

    // first get property size of devices array and allocate memory
    stPropertyAddress.mSelector = kAudioHardwarePropertyDevices;

    AudioObjectGetPropertyDataSize ( kAudioObjectSystemObject,
                                     &stPropertyAddress,
                                     0,
                                     NULL,
                                     &iPropertySize );

    CVector<AudioDeviceID> vAudioDevices ( iPropertySize );

    // now actually query all devices present in the system
    AudioObjectGetPropertyData ( kAudioObjectSystemObject,
                                 &stPropertyAddress,
                                 0,
                                 NULL,
                                 &iPropertySize,
                                 &vAudioDevices[0] );

    // calculate device count based on size of returned data array
    const UInt32 iDeviceCount = iPropertySize / sizeof ( AudioDeviceID );

    // always add system default devices for input and output as first entry
    lNumDevs                 = 0;
    strDriverNames[lNumDevs] = "System Default In/Out Devices";

    iPropertySize               = sizeof ( AudioDeviceID );
    stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice;

    if ( AudioObjectGetPropertyData ( kAudioObjectSystemObject,
                                      &stPropertyAddress,
                                      0,
                                      NULL,
                                      &iPropertySize,
                                      &audioInputDevice[lNumDevs] ) )
    {
        throw CGenErr ( tr ( "CoreAudio input AudioHardwareGetProperty call failed. "
                             "It seems that no sound card is available in the system." ) );
    }

    iPropertySize               = sizeof ( AudioDeviceID );
    stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice;

    if ( AudioObjectGetPropertyData ( kAudioObjectSystemObject,
                                      &stPropertyAddress,
                                      0,
                                      NULL,
                                      &iPropertySize,
                                      &audioOutputDevice[lNumDevs] ) )
    {
        throw CGenErr ( tr ( "CoreAudio output AudioHardwareGetProperty call failed. "
                             "It seems that no sound card is available in the system." ) );
    }

    lNumDevs++; // next device

    // add detected devices
    //
    // we add combined entries for input and output for each device so that we
    // do not need two combo boxes in the GUI for input and output (therefore
    // all possible combinations are required which can be a large number)
    for ( UInt32 i = 0; i < iDeviceCount; i++ )
    {
        for ( UInt32 j = 0; j < iDeviceCount; j++ )
        {
            // get device infos for both current devices
            QString strDeviceName_i;
            QString strDeviceName_j;
            bool    bIsInput_i;
            bool    bIsInput_j;
            bool    bIsOutput_i;
            bool    bIsOutput_j;

            GetAudioDeviceInfos ( vAudioDevices[i],
                                  strDeviceName_i,
                                  bIsInput_i,
                                  bIsOutput_i );

            GetAudioDeviceInfos ( vAudioDevices[j],
                                  strDeviceName_j,
                                  bIsInput_j,
                                  bIsOutput_j );

            // check if i device is input and j device is output and that we are
            // in range
            if ( bIsInput_i && bIsOutput_j && ( lNumDevs < MAX_NUMBER_SOUND_CARDS ) )
            {
                strDriverNames[lNumDevs] = "in: " +
                    strDeviceName_i + "/out: " +
                    strDeviceName_j;

                // store audio device IDs
                audioInputDevice[lNumDevs]  = vAudioDevices[i];
                audioOutputDevice[lNumDevs] = vAudioDevices[j];

                lNumDevs++; // next device
            }
        }
    }

    // init device index as not initialized (invalid)
    lCurDev                    = INVALID_INDEX;
    CurrentAudioInputDeviceID  = 0;
    CurrentAudioOutputDeviceID = 0;
    iNumInChan                 = 0;
    iNumInChanPlusAddChan      = 0;
    iNumOutChan                = 0;
    iSelInputLeftChannel       = 0;
    iSelInputRightChannel      = 0;
    iSelOutputLeftChannel      = 0;
    iSelOutputRightChannel     = 0;


    // Optional MIDI initialization --------------------------------------------
    if ( iCtrlMIDIChannel != INVALID_MIDI_CH )
    {
        // create client and ports
        MIDIClientRef midiClient = static_cast<MIDIClientRef> ( NULL );
        MIDIClientCreate ( CFSTR ( APP_NAME ), NULL, NULL, &midiClient );
        MIDIInputPortCreate ( midiClient, CFSTR ( "Input port" ), callbackMIDI, this, &midiInPortRef );

        // open connections from all sources
        const int iNMIDISources = MIDIGetNumberOfSources();

        for ( int i = 0; i < iNMIDISources; i++ )
        {
            MIDIEndpointRef src = MIDIGetSource ( i );
            MIDIPortConnectSource ( midiInPortRef, src, NULL ) ;
        }
    }
}

void CSound::GetAudioDeviceInfos ( const AudioDeviceID DeviceID,
                                   QString&            strDeviceName,
                                   bool&               bIsInput,
                                   bool&               bIsOutput )
{
    UInt32                     iPropertySize;
    AudioObjectPropertyAddress stPropertyAddress;

    // init return values
    bIsInput  = false;
    bIsOutput = false;

    // check if device is input or output or both (is that possible?)
    stPropertyAddress.mSelector = kAudioDevicePropertyStreams;
    stPropertyAddress.mElement  = kAudioObjectPropertyElementMaster;

    // input check
    iPropertySize            = 0;
    stPropertyAddress.mScope = kAudioDevicePropertyScopeInput;

    AudioObjectGetPropertyDataSize ( DeviceID,
                                     &stPropertyAddress,
                                     0,
                                     NULL,
                                     &iPropertySize );

    bIsInput = ( iPropertySize > 0 ); // check if any input streams are available

    // output check
    iPropertySize            = 0;
    stPropertyAddress.mScope = kAudioDevicePropertyScopeOutput;

    AudioObjectGetPropertyDataSize ( DeviceID,
                                     &stPropertyAddress,
                                     0,
                                     NULL,
                                     &iPropertySize );

    bIsOutput = ( iPropertySize > 0 ); // check if any output streams are available

    // get property name
    CFStringRef sPropertyStringValue = NULL;

    stPropertyAddress.mSelector = kAudioObjectPropertyName;
    stPropertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;
    iPropertySize               = sizeof ( CFStringRef );

    AudioObjectGetPropertyData ( DeviceID,
                                 &stPropertyAddress,
                                 0,
                                 NULL,
                                 &iPropertySize,
                                 &sPropertyStringValue );

    // convert string
    if ( !ConvertCFStringToQString ( sPropertyStringValue, strDeviceName ) )
    {
        // use a default name in case the conversion did not succeed
        strDeviceName = "UNKNOWN";
    }
}

int CSound::CountChannels ( AudioDeviceID devID,
                            bool          isInput )
{
    OSStatus err;
    UInt32   propSize;
    int      result = 0;

    if ( isInput )
    {
        vecNumInBufChan.Init ( 0 );
    }
    else
    {
        vecNumOutBufChan.Init ( 0 );
    }

    // it seems we have multiple buffers where each buffer has only one channel,
    // in that case we assume that each input channel has its own buffer
    AudioObjectPropertyScope theScope = isInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput;

    AudioObjectPropertyAddress theAddress = { kAudioDevicePropertyStreamConfiguration,
                                              theScope,
                                              0 };

    AudioObjectGetPropertyDataSize ( devID, &theAddress, 0, NULL, &propSize );

    AudioBufferList *buflist = (AudioBufferList*) malloc ( propSize );

    err = AudioObjectGetPropertyData ( devID, &theAddress, 0, NULL, &propSize, buflist );

    if ( !err )
    {
        for ( UInt32 i = 0; i < buflist->mNumberBuffers; ++i )
        {
            // The correct value mNumberChannels for an AudioBuffer can be derived from the mChannelsPerFrame
            // and the interleaved flag. For non interleaved formats, mNumberChannels is always 1.
            // For interleaved formats, mNumberChannels is equal to mChannelsPerFrame.
            result += buflist->mBuffers[i].mNumberChannels;

            if ( isInput )
            {
                vecNumInBufChan.Add ( buflist->mBuffers[i].mNumberChannels );
            }
            else
            {
                vecNumOutBufChan.Add ( buflist->mBuffers[i].mNumberChannels );
            }
        }
    }
    free ( buflist );

    return result;
}

QString CSound::LoadAndInitializeDriver ( int iDriverIdx, bool )
{
    // check device capabilities if it fulfills our requirements
    const QString strStat = CheckDeviceCapabilities ( iDriverIdx );

    // check if device is capable
    if ( strStat.isEmpty() )
    {
        // store ID of selected driver if initialization was successful
        lCurDev                    = iDriverIdx;
        CurrentAudioInputDeviceID  = audioInputDevice[iDriverIdx];
        CurrentAudioOutputDeviceID = audioOutputDevice[iDriverIdx];

        // the device has changed, per definition we reset the channel
        // mapping to the defaults (first two available channels)
        SetLeftInputChannel   ( 0 );
        SetRightInputChannel  ( 1 );
        SetLeftOutputChannel  ( 0 );
        SetRightOutputChannel ( 1 );
    }

    return strStat;
}

QString CSound::CheckDeviceCapabilities ( const int iDriverIdx )
{
    UInt32                      iPropertySize;
    AudioStreamBasicDescription	CurDevStreamFormat;
    Float64                     inputSampleRate   = 0;
    Float64                     outputSampleRate  = 0;
    const Float64               fSystemSampleRate = static_cast<Float64> ( SYSTEM_SAMPLE_RATE_HZ );
    AudioObjectPropertyAddress  stPropertyAddress;

    stPropertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;
    stPropertyAddress.mElement  = kAudioObjectPropertyElementMaster;

    // check input device sample rate
    stPropertyAddress.mSelector = kAudioDevicePropertyNominalSampleRate;
    iPropertySize               = sizeof ( Float64 );

    AudioObjectGetPropertyData ( audioInputDevice[iDriverIdx],
                                 &stPropertyAddress,
                                 0,
                                 NULL,
                                 &iPropertySize,
                                 &inputSampleRate );

    if ( inputSampleRate != fSystemSampleRate )
    {
        // try to change the sample rate
        if ( AudioObjectSetPropertyData ( audioInputDevice[iDriverIdx],
                                          &stPropertyAddress,
                                          0,
                                          NULL,
                                          sizeof ( Float64 ),
                                          &fSystemSampleRate ) != noErr )
        {
            return QString ( tr ( "Current system audio input device sample "
                "rate of %1 Hz is not supported. Please open the Audio-MIDI-Setup in "
                "Applications->Utilities and try to set a sample rate of %2 Hz." ) ).arg (
                static_cast<int> ( inputSampleRate ) ).arg ( SYSTEM_SAMPLE_RATE_HZ );
        }
    }

    // check output device sample rate
    iPropertySize = sizeof ( Float64 );

    AudioObjectGetPropertyData ( audioOutputDevice[iDriverIdx],
                                 &stPropertyAddress,
                                 0,
                                 NULL,
                                 &iPropertySize,
                                 &outputSampleRate );

    if ( outputSampleRate != fSystemSampleRate )
    {
        // try to change the sample rate
        if ( AudioObjectSetPropertyData ( audioOutputDevice[iDriverIdx],
                                          &stPropertyAddress,
                                          0,
                                          NULL,
                                          sizeof ( Float64 ),
                                          &fSystemSampleRate ) != noErr )
        {
            return QString ( tr ( "Current system audio output device sample "
                "rate of %1 Hz is not supported. Please open the Audio-MIDI-Setup in "
                "Applications->Utilities and try to set a sample rate of %2 Hz." ) ).arg (
                static_cast<int> ( outputSampleRate ) ).arg ( SYSTEM_SAMPLE_RATE_HZ );
        }
    }

    // get the stream ID of the input device (at least one stream must always exist)
    iPropertySize               = 0;
    stPropertyAddress.mSelector = kAudioDevicePropertyStreams;
    stPropertyAddress.mScope    = kAudioObjectPropertyScopeInput;

    AudioObjectGetPropertyDataSize ( audioInputDevice[iDriverIdx],
                                     &stPropertyAddress,
                                     0,
                                     NULL,
                                     &iPropertySize );

    CVector<AudioStreamID> vInputStreamIDList ( iPropertySize );

    AudioObjectGetPropertyData ( audioInputDevice[iDriverIdx],
                                 &stPropertyAddress,
                                 0,
                                 NULL,
                                 &iPropertySize,
                                 &vInputStreamIDList[0] );

    const AudioStreamID inputStreamID = vInputStreamIDList[0];

    // get the stream ID of the output device (at least one stream must always exist)
    iPropertySize               = 0;
    stPropertyAddress.mSelector = kAudioDevicePropertyStreams;
    stPropertyAddress.mScope    = kAudioObjectPropertyScopeOutput;

    AudioObjectGetPropertyDataSize ( audioOutputDevice[iDriverIdx],
                                     &stPropertyAddress,
                                     0,
                                     NULL,
                                     &iPropertySize );

    CVector<AudioStreamID> vOutputStreamIDList ( iPropertySize );

    AudioObjectGetPropertyData ( audioOutputDevice[iDriverIdx],
                                 &stPropertyAddress,
                                 0,
                                 NULL,
                                 &iPropertySize,
                                 &vOutputStreamIDList[0] );

    const AudioStreamID outputStreamID = vOutputStreamIDList[0];

    // According to the AudioHardware documentation: "If the format is a linear PCM
    // format, the data will always be presented as 32 bit, native endian floating
    // point. All conversions to and from the true physical format of the hardware
    // is handled by the devices driver.".
    // check the input
    iPropertySize               = sizeof ( AudioStreamBasicDescription );
    stPropertyAddress.mSelector = kAudioStreamPropertyVirtualFormat;
    stPropertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;

    AudioObjectGetPropertyData ( inputStreamID,
                                 &stPropertyAddress,
                                 0,
                                 NULL,
                                 &iPropertySize,
                                 &CurDevStreamFormat );

    if ( ( CurDevStreamFormat.mFormatID        != kAudioFormatLinearPCM ) ||
         ( CurDevStreamFormat.mFramesPerPacket != 1 ) ||
         ( CurDevStreamFormat.mBitsPerChannel  != 32 ) ||
         ( !( CurDevStreamFormat.mFormatFlags & kAudioFormatFlagIsFloat ) ) ||
         ( !( CurDevStreamFormat.mFormatFlags & kAudioFormatFlagIsPacked ) ) )
    {
        return tr ( "The audio input stream format for this audio device is "
                    "not compatible with this software." );
    }

    // check the output
    AudioObjectGetPropertyData ( outputStreamID,
                                 &stPropertyAddress,
                                 0,
                                 NULL,
                                 &iPropertySize,
                                 &CurDevStreamFormat );

    if ( ( CurDevStreamFormat.mFormatID        != kAudioFormatLinearPCM ) ||
         ( CurDevStreamFormat.mFramesPerPacket != 1 ) ||
         ( CurDevStreamFormat.mBitsPerChannel  != 32 ) ||
         ( !( CurDevStreamFormat.mFormatFlags & kAudioFormatFlagIsFloat ) ) ||
         ( !( CurDevStreamFormat.mFormatFlags & kAudioFormatFlagIsPacked ) ) )
    {
        return tr ( "The audio output stream format for this audio device is "
                    "not compatible with this software." );
    }

    // store the input and out number of channels for this device
    iNumInChan  = CountChannels ( audioInputDevice[iDriverIdx], true );
    iNumOutChan = CountChannels ( audioOutputDevice[iDriverIdx], false );

    // clip the number of input/output channels to our allowed maximum
    if ( iNumInChan > MAX_NUM_IN_OUT_CHANNELS )
    {
        iNumInChan = MAX_NUM_IN_OUT_CHANNELS;
    }
    if ( iNumOutChan > MAX_NUM_IN_OUT_CHANNELS )
    {
        iNumOutChan = MAX_NUM_IN_OUT_CHANNELS;
    }

    // get the channel names of the input device
    for ( int iCurInCH = 0; iCurInCH < iNumInChan; iCurInCH++ )
    {
        CFStringRef sPropertyStringValue = NULL;

        stPropertyAddress.mSelector = kAudioObjectPropertyElementName;
        stPropertyAddress.mElement  = iCurInCH + 1;
        stPropertyAddress.mScope    = kAudioObjectPropertyScopeInput;
        iPropertySize               = sizeof ( CFStringRef );

        AudioObjectGetPropertyData ( audioInputDevice[iDriverIdx],
                                     &stPropertyAddress,
                                     0,
                                     NULL,
                                     &iPropertySize,
                                     &sPropertyStringValue );

        // convert string
        const bool bConvOK = ConvertCFStringToQString ( sPropertyStringValue,
                                                        sChannelNamesInput[iCurInCH] );

        // add the "[n]:" at the beginning as is in the Audio-Midi-Setup
        if ( !bConvOK || ( iPropertySize == 0 ) )
        {
            // use a default name in case there was an error or the name is empty
            sChannelNamesInput[iCurInCH] =
                QString ( "%1: Channel %1" ).arg ( iCurInCH + 1 );
        }
        else
        {
            sChannelNamesInput[iCurInCH].prepend ( QString ( "%1: " ).arg ( iCurInCH + 1 ) );
        }
    }

    // get the channel names of the output device
    for ( int iCurOutCH = 0; iCurOutCH < iNumOutChan; iCurOutCH++ )
    {
        CFStringRef sPropertyStringValue = NULL;

        stPropertyAddress.mSelector = kAudioObjectPropertyElementName;
        stPropertyAddress.mElement  = iCurOutCH + 1;
        stPropertyAddress.mScope    = kAudioObjectPropertyScopeOutput;
        iPropertySize               = sizeof ( CFStringRef );

        AudioObjectGetPropertyData ( audioOutputDevice[iDriverIdx],
                                     &stPropertyAddress,
                                     0,
                                     NULL,
                                     &iPropertySize,
                                     &sPropertyStringValue );

        // convert string
        const bool bConvOK = ConvertCFStringToQString ( sPropertyStringValue,
                                                        sChannelNamesOutput[iCurOutCH] );

        // add the "[n]:" at the beginning as is in the Audio-Midi-Setup
        if ( !bConvOK || ( iPropertySize == 0 ) )
        {
            // use a default name in case there was an error or the name is empty
            sChannelNamesOutput[iCurOutCH] =
                QString ( "%1: Channel %1" ).arg ( iCurOutCH + 1 );
        }
        else
        {
            sChannelNamesOutput[iCurOutCH].prepend ( QString ( "%1: " ).arg ( iCurOutCH + 1 ) );
        }
    }

    // special case with 4 input channels: support adding channels
    if ( iNumInChan == 4 )
    {
        // add four mixed channels (i.e. 4 normal, 4 mixed channels)
        iNumInChanPlusAddChan = 8;

        for ( int iCh = 0; iCh < iNumInChanPlusAddChan; iCh++ )
        {
            int iSelCH, iSelAddCH;

            GetSelCHAndAddCH ( iCh, iNumInChan, iSelCH, iSelAddCH );

            if ( iSelAddCH >= 0 )
            {
                // for mixed channels, show both audio channel names to be mixed
                sChannelNamesInput[iCh] =
                    sChannelNamesInput[iSelCH] + " + " + sChannelNamesInput[iSelAddCH];
            }
        }
    }
    else
    {
        // regular case: no mixing input channels used
        iNumInChanPlusAddChan = iNumInChan;
    }

    // everything is ok, return empty string for "no error" case
    return "";
}

void CSound::UpdateChSelection()
{
    // calculate the selected input/output buffer and the selected interleaved
    // channel index in the buffer, note that each buffer can have a different
    // number of interleaved channels
    int iChCnt;
    int iSelCHLeft,  iSelAddCHLeft;
    int iSelCHRight, iSelAddCHRight;

    // initialize all buffer indexes with an invalid value
    iSelInBufferLeft     = INVALID_INDEX;
    iSelInBufferRight    = INVALID_INDEX;
    iSelAddInBufferLeft  = INVALID_INDEX; // if no additional channel used, this will stay on the invalid value
    iSelAddInBufferRight = INVALID_INDEX; // if no additional channel used, this will stay on the invalid value
    iSelOutBufferLeft    = INVALID_INDEX;
    iSelOutBufferRight   = INVALID_INDEX;

    // input
    GetSelCHAndAddCH ( iSelInputLeftChannel,  iNumInChan, iSelCHLeft,  iSelAddCHLeft );
    GetSelCHAndAddCH ( iSelInputRightChannel, iNumInChan, iSelCHRight, iSelAddCHRight );

    iChCnt = 0;

    for ( int iBuf = 0; iBuf < vecNumInBufChan.Size(); iBuf++ )
    {
        iChCnt += vecNumInBufChan[iBuf];

        if ( ( iSelInBufferLeft < 0 ) && ( iChCnt > iSelCHLeft ) )
        {
            iSelInBufferLeft   = iBuf;
            iSelInInterlChLeft = iSelCHLeft - iChCnt + vecNumInBufChan[iBuf];
        }

        if ( ( iSelInBufferRight < 0 ) && ( iChCnt > iSelCHRight ) )
        {
            iSelInBufferRight   = iBuf;
            iSelInInterlChRight = iSelCHRight - iChCnt + vecNumInBufChan[iBuf];
        }

        if ( ( iSelAddCHLeft >= 0 ) && ( iSelAddInBufferLeft < 0 ) && ( iChCnt > iSelAddCHLeft ) )
        {
            iSelAddInBufferLeft   = iBuf;
            iSelAddInInterlChLeft = iSelAddCHLeft - iChCnt + vecNumInBufChan[iBuf];
        }

        if ( ( iSelAddCHRight >= 0 ) && ( iSelAddInBufferRight < 0 ) && ( iChCnt > iSelAddCHRight ) )
        {
            iSelAddInBufferRight   = iBuf;
            iSelAddInInterlChRight = iSelAddCHRight - iChCnt + vecNumInBufChan[iBuf];
        }
    }

    // output
    GetSelCHAndAddCH ( iSelOutputLeftChannel,  iNumOutChan, iSelCHLeft,  iSelAddCHLeft );
    GetSelCHAndAddCH ( iSelOutputRightChannel, iNumOutChan, iSelCHRight, iSelAddCHRight );

    iChCnt = 0;

    for ( int iBuf = 0; iBuf < vecNumOutBufChan.Size(); iBuf++ )
    {
        iChCnt += vecNumOutBufChan[iBuf];

        if ( ( iSelOutBufferLeft < 0 ) && ( iChCnt > iSelCHLeft ) )
        {
            iSelOutBufferLeft   = iBuf;
            iSelOutInterlChLeft = iSelCHLeft - iChCnt + vecNumOutBufChan[iBuf];
        }

        if ( ( iSelOutBufferRight < 0 ) && ( iChCnt > iSelCHRight ) )
        {
            iSelOutBufferRight   = iBuf;
            iSelOutInterlChRight = iSelCHRight - iChCnt + vecNumOutBufChan[iBuf];
        }
    }
}

void CSound::SetLeftInputChannel  ( const int iNewChan )
{
    // apply parameter after input parameter check
    if ( ( iNewChan >= 0 ) && ( iNewChan < iNumInChanPlusAddChan ) )
    {
        iSelInputLeftChannel = iNewChan;
        UpdateChSelection();
    }
}

void CSound::SetRightInputChannel ( const int iNewChan )
{
    // apply parameter after input parameter check
    if ( ( iNewChan >= 0 ) && ( iNewChan < iNumInChanPlusAddChan ) )
    {
        iSelInputRightChannel = iNewChan;
        UpdateChSelection();
    }
}

void CSound::SetLeftOutputChannel  ( const int iNewChan )
{
    // apply parameter after input parameter check
    if ( ( iNewChan >= 0 ) && ( iNewChan < iNumOutChan ) )
    {
        iSelOutputLeftChannel = iNewChan;
        UpdateChSelection();
    }
}

void CSound::SetRightOutputChannel ( const int iNewChan )
{
    // apply parameter after input parameter check
    if ( ( iNewChan >= 0 ) && ( iNewChan < iNumOutChan ) )
    {
        iSelOutputRightChannel = iNewChan;
        UpdateChSelection();
    }
}

void CSound::Start()
{
    AudioObjectPropertyAddress stPropertyAddress;

    stPropertyAddress.mElement  = kAudioObjectPropertyElementMaster;
    stPropertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;

    // setup callback for xruns (only for input is enough)
    stPropertyAddress.mSelector = kAudioDeviceProcessorOverload;

    AudioObjectAddPropertyListener ( audioInputDevice[lCurDev],
                                     &stPropertyAddress,
                                     deviceNotification,
                                     this );

    // setup callbacks for device property changes
    stPropertyAddress.mSelector = kAudioDevicePropertyDeviceHasChanged;

    AudioObjectAddPropertyListener ( audioInputDevice[lCurDev],
                                     &stPropertyAddress,
                                     deviceNotification,
                                     this );

    AudioObjectAddPropertyListener ( audioOutputDevice[lCurDev],
                                     &stPropertyAddress,
                                     deviceNotification,
                                     this );

    // register the callback function for input and output
    AudioDeviceCreateIOProcID ( audioInputDevice[lCurDev],
                                callbackIO,
                                this,
                                &audioInputProcID );

    AudioDeviceCreateIOProcID ( audioOutputDevice[lCurDev],
                                callbackIO,
                                this,
                                &audioOutputProcID );

    // start the audio stream
    AudioDeviceStart ( audioInputDevice[lCurDev], audioInputProcID );
    AudioDeviceStart ( audioOutputDevice[lCurDev], audioOutputProcID );

    // call base class
    CSoundBase::Start();
}

void CSound::Stop()
{
    // stop the audio stream
    AudioDeviceStop ( audioInputDevice[lCurDev], audioInputProcID );
    AudioDeviceStop ( audioOutputDevice[lCurDev], audioOutputProcID );

    // unregister the callback function for input and output
    AudioDeviceDestroyIOProcID ( audioInputDevice[lCurDev], audioInputProcID );
    AudioDeviceDestroyIOProcID ( audioOutputDevice[lCurDev], audioOutputProcID );

    AudioObjectPropertyAddress stPropertyAddress;

    stPropertyAddress.mElement  = kAudioObjectPropertyElementMaster;
    stPropertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;

    // unregister callback functions for device property changes
    stPropertyAddress.mSelector = kAudioDevicePropertyDeviceHasChanged;

    AudioObjectRemovePropertyListener( audioOutputDevice[lCurDev],
                                       &stPropertyAddress,
                                       deviceNotification,
                                       this );

    AudioObjectRemovePropertyListener( audioInputDevice[lCurDev],
                                       &stPropertyAddress,
                                       deviceNotification,
                                       this );

    // unregister the callback function for xruns
    stPropertyAddress.mSelector = kAudioDeviceProcessorOverload;

    AudioObjectRemovePropertyListener( audioInputDevice[lCurDev],
                                       &stPropertyAddress,
                                       deviceNotification,
                                       this );

    // call base class
    CSoundBase::Stop();
}

int CSound::Init ( const int iNewPrefMonoBufferSize )
{
    UInt32 iActualMonoBufferSize;

    // Error message string: in case buffer sizes on input and output cannot be
    // set to the same value
    const QString strErrBufSize = tr ( "The buffer sizes of the current "
        "input and output audio device cannot be set to a common value. Please "
        "choose other input/output audio devices in your system settings." );

    // try to set input buffer size
    iActualMonoBufferSize =
        SetBufferSize ( audioInputDevice[lCurDev], true, iNewPrefMonoBufferSize );

    if ( iActualMonoBufferSize != static_cast<UInt32> ( iNewPrefMonoBufferSize ) )
    {
        // try to set the input buffer size to the output so that we
        // have a matching pair
        if ( SetBufferSize ( audioOutputDevice[lCurDev], false, iActualMonoBufferSize ) !=
             iActualMonoBufferSize )
        {
            throw CGenErr ( strErrBufSize );
        }
    }
    else
    {
        // try to set output buffer size
        if ( SetBufferSize ( audioOutputDevice[lCurDev], false, iNewPrefMonoBufferSize ) !=
             static_cast<UInt32> ( iNewPrefMonoBufferSize ) )
        {
            throw CGenErr ( strErrBufSize );
        }
    }

    // store buffer size
    iCoreAudioBufferSizeMono = iActualMonoBufferSize;

    // init base class
    CSoundBase::Init ( iCoreAudioBufferSizeMono );

    // set internal buffer size value and calculate stereo buffer size
    iCoreAudioBufferSizeStereo = 2 * iCoreAudioBufferSizeMono;

    // create memory for intermediate audio buffer
    vecsTmpAudioSndCrdStereo.Init ( iCoreAudioBufferSizeStereo );

    return iCoreAudioBufferSizeMono;
}

UInt32 CSound::SetBufferSize ( AudioDeviceID& audioDeviceID,
                               const bool     bIsInput,
                               UInt32         iPrefBufferSize )
{
    AudioObjectPropertyAddress stPropertyAddress;
    stPropertyAddress.mSelector = kAudioDevicePropertyBufferFrameSize;

    if ( bIsInput )
    {
        stPropertyAddress.mScope = kAudioDevicePropertyScopeInput;
    }
    else
    {
        stPropertyAddress.mScope = kAudioDevicePropertyScopeOutput;
    }

    stPropertyAddress.mElement = kAudioObjectPropertyElementMaster;

    // first set the value
    UInt32 iSizeBufValue = sizeof ( UInt32 );

    AudioObjectSetPropertyData ( audioDeviceID,
                                 &stPropertyAddress,
                                 0,
                                 NULL,
                                 iSizeBufValue,
                                 &iPrefBufferSize );

    // read back which value is actually used
    UInt32 iActualMonoBufferSize = 0;

    AudioObjectGetPropertyData ( audioDeviceID,
                                 &stPropertyAddress,
                                 0,
                                 NULL,
                                 &iSizeBufValue,
                                 &iActualMonoBufferSize );

    return iActualMonoBufferSize;
}

OSStatus CSound::deviceNotification ( AudioDeviceID,
                                      UInt32,
                                      const AudioObjectPropertyAddress* inAddresses,
                                      void*                             inRefCon )
{
    CSound* pSound = static_cast<CSound*> ( inRefCon );

    if ( inAddresses->mSelector == kAudioDevicePropertyDeviceHasChanged )
    {
        // if any property of the device has changed, do a full reload
        pSound->EmitReinitRequestSignal ( RS_RELOAD_RESTART_AND_INIT );
    }

/* NOTE that this code does not solve the crackling sound issue
    if ( inAddresses->mSelector == kAudioDeviceProcessorOverload )
    {
        // xrun handling (it is important to act on xruns under CoreAudio
        // since it seems that the xrun situation stays stable for a
        // while and would give you a long time bad audio)
        pSound->EmitReinitRequestSignal ( RS_ONLY_RESTART );
    }
*/

    return noErr;
}

OSStatus CSound::callbackIO ( AudioDeviceID          inDevice,
                              const AudioTimeStamp*,
                              const AudioBufferList* inInputData,
                              const AudioTimeStamp*,
                              AudioBufferList*       outOutputData,
                              const AudioTimeStamp*,
                              void*                  inRefCon )
{
    CSound* pSound = static_cast<CSound*> ( inRefCon );

    // both, the input and output device use the same callback function
    QMutexLocker locker ( &pSound->Mutex );

    const int           iCoreAudioBufferSizeMono = pSound->iCoreAudioBufferSizeMono;
    const int           iSelInBufferLeft         = pSound->iSelInBufferLeft;
    const int           iSelInBufferRight        = pSound->iSelInBufferRight;
    const int           iSelInInterlChLeft       = pSound->iSelInInterlChLeft;
    const int           iSelInInterlChRight      = pSound->iSelInInterlChRight;
    const int           iSelAddInBufferLeft      = pSound->iSelAddInBufferLeft;
    const int           iSelAddInBufferRight     = pSound->iSelAddInBufferRight;
    const int           iSelAddInInterlChLeft    = pSound->iSelAddInInterlChLeft;
    const int           iSelAddInInterlChRight   = pSound->iSelAddInInterlChRight;
    const int           iSelOutBufferLeft        = pSound->iSelOutBufferLeft;
    const int           iSelOutBufferRight       = pSound->iSelOutBufferRight;
    const int           iSelOutInterlChLeft      = pSound->iSelOutInterlChLeft;
    const int           iSelOutInterlChRight     = pSound->iSelOutInterlChRight;
    const CVector<int>& vecNumInBufChan          = pSound->vecNumInBufChan;
    const CVector<int>& vecNumOutBufChan         = pSound->vecNumOutBufChan;

    if ( ( inDevice == pSound->CurrentAudioInputDeviceID ) && inInputData )
    {
        // check sizes (note that float32 has four bytes)
        if ( ( iSelInBufferLeft >= 0 ) &&
             ( iSelInBufferLeft < static_cast<int> ( inInputData->mNumberBuffers ) ) &&
             ( iSelInBufferRight >= 0 ) &&
             ( iSelInBufferRight < static_cast<int> ( inInputData->mNumberBuffers ) ) &&
             ( iSelAddInBufferLeft < static_cast<int> ( inInputData->mNumberBuffers ) ) &&
             ( iSelAddInBufferRight < static_cast<int> ( inInputData->mNumberBuffers ) ) &&
             ( inInputData->mBuffers[iSelInBufferLeft].mDataByteSize  == static_cast<UInt32> ( vecNumInBufChan[iSelInBufferLeft] *  iCoreAudioBufferSizeMono * 4 ) ) &&
             ( inInputData->mBuffers[iSelInBufferRight].mDataByteSize == static_cast<UInt32> ( vecNumInBufChan[iSelInBufferRight] * iCoreAudioBufferSizeMono * 4 ) ) )
        {
            Float32* pLeftData             = static_cast<Float32*> ( inInputData->mBuffers[iSelInBufferLeft].mData );
            Float32* pRightData            = static_cast<Float32*> ( inInputData->mBuffers[iSelInBufferRight].mData );
            int      iNumChanPerFrameLeft  = vecNumInBufChan[iSelInBufferLeft];
            int      iNumChanPerFrameRight = vecNumInBufChan[iSelInBufferRight];

            // copy input data
            for ( int i = 0; i < iCoreAudioBufferSizeMono; i++ )
            {
                // copy left and right channels separately
                pSound->vecsTmpAudioSndCrdStereo[2 * i]     = (short) ( pLeftData[iNumChanPerFrameLeft * i + iSelInInterlChLeft] * _MAXSHORT );
                pSound->vecsTmpAudioSndCrdStereo[2 * i + 1] = (short) ( pRightData[iNumChanPerFrameRight * i + iSelInInterlChRight] * _MAXSHORT );
            }

            // add an additional optional channel
            if ( iSelAddInBufferLeft >= 0 )
            {
                pLeftData            = static_cast<Float32*> ( inInputData->mBuffers[iSelAddInBufferLeft].mData );
                iNumChanPerFrameLeft = vecNumInBufChan[iSelAddInBufferLeft];

                for ( int i = 0; i < iCoreAudioBufferSizeMono; i++ )
                {
                    pSound->vecsTmpAudioSndCrdStereo[2 * i] = Double2Short (
                        pSound->vecsTmpAudioSndCrdStereo[2 * i] + pLeftData[iNumChanPerFrameLeft * i + iSelAddInInterlChLeft] * _MAXSHORT );
                }
            }

            if ( iSelAddInBufferRight >= 0 )
            {
                pRightData            = static_cast<Float32*> ( inInputData->mBuffers[iSelAddInBufferRight].mData );
                iNumChanPerFrameRight = vecNumInBufChan[iSelAddInBufferRight];

                for ( int i = 0; i < iCoreAudioBufferSizeMono; i++ )
                {
                    pSound->vecsTmpAudioSndCrdStereo[2 * i + 1] = Double2Short (
                        pSound->vecsTmpAudioSndCrdStereo[2 * i + 1] + pRightData[iNumChanPerFrameRight * i + iSelAddInInterlChRight] * _MAXSHORT );
                }
            }
        }
        else
        {
            // incompatible sizes, clear work buffer
            pSound->vecsTmpAudioSndCrdStereo.Reset ( 0 );
        }

        // call processing callback function
        pSound->ProcessCallback ( pSound->vecsTmpAudioSndCrdStereo );
    }

    if ( ( inDevice == pSound->CurrentAudioOutputDeviceID ) && outOutputData )
    {
       // check sizes (note that float32 has four bytes)
       if ( ( iSelOutBufferLeft >= 0 ) &&
            ( iSelOutBufferLeft < static_cast<int> ( outOutputData->mNumberBuffers ) ) &&
            ( iSelOutBufferRight >= 0 ) &&
            ( iSelOutBufferRight < static_cast<int> ( outOutputData->mNumberBuffers ) ) &&
            ( outOutputData->mBuffers[iSelOutBufferLeft].mDataByteSize  == static_cast<UInt32> ( vecNumOutBufChan[iSelOutBufferLeft] *  iCoreAudioBufferSizeMono * 4 ) ) &&
            ( outOutputData->mBuffers[iSelOutBufferRight].mDataByteSize == static_cast<UInt32> ( vecNumOutBufChan[iSelOutBufferRight] * iCoreAudioBufferSizeMono * 4 ) ) )
       {
           Float32* pLeftData             = static_cast<Float32*> ( outOutputData->mBuffers[iSelOutBufferLeft].mData );
           Float32* pRightData            = static_cast<Float32*> ( outOutputData->mBuffers[iSelOutBufferRight].mData );
           int      iNumChanPerFrameLeft  = vecNumOutBufChan[iSelOutBufferLeft];
           int      iNumChanPerFrameRight = vecNumOutBufChan[iSelOutBufferRight];

           // copy output data
           for ( int i = 0; i < iCoreAudioBufferSizeMono; i++ )
           {
               // copy left and right channels separately
               pLeftData[iNumChanPerFrameLeft * i + iSelOutInterlChLeft]    = (Float32) pSound->vecsTmpAudioSndCrdStereo[2 * i] / _MAXSHORT;
               pRightData[iNumChanPerFrameRight * i + iSelOutInterlChRight] = (Float32) pSound->vecsTmpAudioSndCrdStereo[2 * i + 1] / _MAXSHORT;
           }
        }
    }

    return kAudioHardwareNoError;
}

void CSound::callbackMIDI ( const MIDIPacketList* pktlist,
                            void*                 refCon,
                            void* )
{
    CSound* pSound = static_cast<CSound*> ( refCon );

    if ( pSound->midiInPortRef != static_cast<MIDIPortRef> ( NULL ) )
    {
        MIDIPacket* midiPacket = const_cast<MIDIPacket*> ( pktlist->packet );

        for ( unsigned int j = 0; j < pktlist->numPackets; j++ )
        {
            // copy packet and send it to the MIDI parser
            CVector<uint8_t> vMIDIPaketBytes ( midiPacket->length );
            for ( int i = 0; i < midiPacket->length; i++ )
            {
                vMIDIPaketBytes[i] = static_cast<uint8_t> ( midiPacket->data[i] );
            }
            pSound->ParseMIDIMessage ( vMIDIPaketBytes );

            midiPacket = MIDIPacketNext ( midiPacket );
        }
    }
}

bool CSound::ConvertCFStringToQString ( const CFStringRef stringRef,
                                        QString&          sOut )
{
    // check if the string reference is a valid pointer
    if ( stringRef != NULL )
    {
        // first check if the string is not empty
        if ( CFStringGetLength ( stringRef ) > 0 )
        {
            // convert CFString in c-string (quick hack!) and then in QString
            char* sC_strPropValue =
                (char*) malloc ( CFStringGetLength ( stringRef ) * 3 + 1 );

            if ( CFStringGetCString ( stringRef,
                                      sC_strPropValue,
                                      CFStringGetLength ( stringRef ) * 3 + 1,
                                      kCFStringEncodingUTF8 ) )
            {
                sOut = sC_strPropValue;
                free ( sC_strPropValue );

                return true; // OK
            }
        }

        // release the string reference because it is not needed anymore
        CFRelease ( stringRef );
    }

    return false; // not OK
}