lundi 13 septembre 2010

Tutoriel: 3) Ajout d'un paramètre

Source
Code source (Visual Studio 2008, attention de choisir le profil de compilation "DebugFreeware")

But
Dans ce troisième tutoriel nous allons ajouter un paramètre, et donc activer le mécanisme d'exploration qui permet à un éditeur universel par exemple de récupérer à distance les descriptifs des paramètres. Nous allons également voir comment fonctionne la synchronisation de valeur courante.

Définition du paramètre
La première chose à faire, c'est définir clairement notre paramètre. Nous allons donner la possibilité à notre application de modifier la transposition appliquée aux messages Event. La valeur peut être choisie de manière continue, il ne s'agit pas d'une sélection dans une liste. Le type de message utilisé pour transporter cette valeur est donc un Modifier. Un petit tour sur la section du Wiki qui traite des différents Modifiers nous permet de voir qu'il existe un Modifier standard (0x00A1) prévu exactement pour ça. Afin de faciliter la vie du programmeur, toutes les valeurs standard sont décrites dans des enums, dans le cas présent CPNS::Enums::MOD_Transpose.

Ce Modifier transporte une valeur signée 16 bits que nous allons utiliser pour représenter la transposition sur +/- 2 octaves. Normalement, le modifier est capable de recevoir la valeur exprimée en MKZ16, format 16 bits signé. Cela dit CopperLan exige qu'un modifier supporte au minimum le transport de valeur sous la forme de la position du bouton (de 0x0000 à 0x7FFF) ce qui permet un contrôle à disance à partir de n'importe quelle application. Par mesure de simplification, nous n'allons considérer ici que ce format. La valeur 0x8000 correspond donc à aucune transposition, 0xFFFF à +24 demi-tons, et 0x0001 à -24 demi-tons. La valeur 0x0000 est normalement ignorée ou assimilée à 0x0001.

Réception du Modifier
Nous allons ici ajouter le code nécessaure à OnInput_Message() pour recevoir le modifier CPNS::Enums::MOD_Transpose et donc agir sur le paramètre en question.

    case CPNS::Enums::OT_ModifierMessage:
        {
            // Récupération de l'interface des messages Modifier
            CPNS::IModifierMessage* p = pMsg->GetIModifierMessage();

            // Vérifie si le message est bien celui qu'on attend
            if (p->GetNumber() == CPNS::Enums::MOD_Transpose)
            {
                // Récupère la valeur brute (position du bouton)
                CPT::uint16 wV = pMsg->GetValue().GetRawValue();
                if (wV == 0) wV = 1; // rapporte la valeur 0 à 1
               // Calcul de la valeur signée
                CPT::int16 nV =  wV - 0x8000;
                // Mise à l'échelle et stockage dans la variable du paramètre
                m_semiToneOffset = nV * 24 / 0x7FFF;
            }
        }
        break;

Traitement de la transposition
Nous allons appliquer une transposition sur un flux polyphonique, il est donc nécessaire de tenir une trace des notes qui sont actuellement actives. Utilisons un set tels que celui-ci:

std::set   m_CurrentTones;

Et donc, nous allons modifier le code de réception d'un message Event dans OnInput_Message() pour maintenir ce set de données.

    case CPNS::Enums::OT_EventMessage:
        {
            // Récupération de l'interface des messages Event
           CPNS::IEventMessage* p = pMsg->GetIEventMessage();

            // Vérifie si l'information de hauteur de son est disponible
            if (p->IsToneAvailable())
            {
                // Récupération du ton
                CPT::uint16 wTone = p->GetTone();
                // Vérifie si un déclenchement est disponible
                switch (p->GetProfileGate())
                {
                case CPNS::Enums::EGM_GateOn:
                    // Si le ton n'existe pas encore dans la liste, on l'ajoute
                    if (m_CurrentTones.find(wTone) == m_CurrentTones.end())
                    {
                        m_CurrentTones.insert(wTone);
                    }
                    break;
                case CPNS::Enums::EGM_GateOff:
                    // Si le ton existe, on l'enlève
                    if (m_CurrentTones.find(wTone) != m_CurrentTones.end())
                    {
                        m_CurrentTones.erase(wTone);
                    }
                    break;
                }
                // Appliquer la transposition. La résolution est de 256 valeurs par demi-ton
                p->SetTone(wTone + m_semiToneOffset * 256);
            }
            // Send du message (mis à jour)
            m_pOutput->Send(p);
        }
        break;

Alors, forcément, si on modifie la valeur de la transposition, il faut bien entendu "éteindre" les notes avant et les "rallumer" ensuite.

On peut faire ça aisément en modifiant le code de réception de CPNS::Enums::MOD_Transpose comme suit:

Extinction des notes courantes:

                // Création d'un message Event
                CPNS::IEventMessage* pE = m_pCHAI->CreateEventMessage();
                // Extinction des notes actives
                pE->SetProfileGate(CPNS::Enums::EGM_GateOff);
                for (std::set::iterator it = m_CurrentTones.begin(); it != m_CurrentTones.end(); ++it)
                {
                    pE->SetTone(*it + m_semiToneOffset * 256);
                    // Les messages sont envoyés en multipart.

                    // Ils sont empilés pour être empaquetés dans un minimum de messages de transport CopperLan.
                    m_pOutput->Send(pE, false);
                }
                // Flush
                m_pOutput->Flush();

Et réallumage des notes avec la bonne transposition

                pE->SetProfileGate(CPNS::Enums::EGM_GateOn);
                for (std::set::iterator it = m_CurrentTones.begin(); it != m_CurrentTones.end(); ++it)
                {
                    pE->SetTone(*it + m_semiToneOffset * 256);
                    m_pOutput->Send(pE, false);
                }
                m_pOutput->Flush();

                //Release de l'Event
                pE->Release();

Gestion de l'exploration
L'exploration d'un device pour que celui-ci expose ses paramètres est rendu possible par le fait d'activer sa notification d'exploration:

    m_pDevice->SetExplorationNotificationHandler(this);

Il faut dès lors que l'objet qui doit être notifié implémente CPNS::IBaseLocalDevice_ExplorationNotificationHandler.

La méthode OnBaseLocalDevice_RequestProperty() est appelée lorsqu'un device distant souhaite obtenir des infos sur les propriétés d'un device. Voici un exemple d'implémentation:

CPT::boolean Engine::OnBaseLocalDevice_RequestProperty(
    IN CPNS::IBaseLocalDevice * const pNotifiedObject,
    IN CPT::uint16 const wRequestID,
    IN CPT::uint16 const wPropertyID )
{
    switch (wPropertyID)
    {
    case CPNS::Enums::DP_SerialNumber:
        pNotifiedObject->Reply_RequestProperty(wRequestID, "My Serial Number");
        break;
    case CPNS::Enums::DP_FirmwareVersion:
        pNotifiedObject->Reply_RequestProperty(wRequestID, "My Firmware Version");
        break;
    case CPNS::Enums::DP_Description:
        pNotifiedObject->Reply_RequestProperty(wRequestID, "Stream Modifier sample for CopperLan");
        break;
    case CPNS::Enums::DP_ApplicationVersion:
        pNotifiedObject->Reply_RequestProperty(wRequestID, "1.0");
        break;
    }
    return TRUE;
}

Nous entrons maintenant dans le vif du sujet concernant l'exploration des paramètres. La méthode suivante est appelée durant la phase d'énumération des paramètres. L'argument eri contient une combinaison de flags qui indique ce que l'on cherche, c'est à dire le type de message, relatif à quel type de endpoint, est-ce le premier, le suivant, ...

CPT::boolean Engine::OnBaseLocalDevice_RequestInfo(
    IN CPNS::IBaseLocalDevice * const pNotifiedObject,
    IN CPT::uint16 const wRequestID,
    IN CPNS::Enums::ExplorationRequestInfo const eri,
    IN CPT::uint16 const wInputOutputID,
    IN CPT::uint16 const wNumber )
{
    // Vérification que la requête est bien relative à l'Input
    if (((eri & CPNS::Enums::_EIT_RelatedMask_) ==  CPNS::Enums::_EIT_Input_) &&
        (wInputOutputID == m_pInput->GetInputID()))
    {
        // Vérification du type de requête
        switch (eri)
        {
            // Juste un seul test ici car on a qu'un seul paramètre pour l'instant
        case CPNS::Enums::ERI_FindFirst_InputModifier:
            pNotifiedObject->Reply_RequestInputModifierInfo(
                wRequestID,
                // Numéro du message associé au paramètre
                CPNS::Enums::MOD_Transpose,
                // Index max
                0,
                // Nom
                "Transpose",
                // Position du point médian
                0x8000,
                // Labels
                "-24", "0", "+24",
                // Type de donnée préféré (bien qu'on en tienne pas compte pour l'instant)
                CPNS::Enums::DT_MKZ16,
                // Ordering (pas applicable vu qu'on a qu'un seul paramètre)
                0,
                // Informations de profil complémentaires. Pas applicable ici.
                CPNS::Enums::MP_None);
            return TRUE;
        }
    }
    return FALSE;
}

La prochaine méthode n'est pas strictement nécessaire dans notre exemple car on ne gère pas (encore) le type de donnée MKZ16. Cela dit, dans la description du paramètre on y indique qu'on aime bien ce type de donnée... et donc voici comment répondre à un contrôleur qui souhaite connaître les limites spécifiques à un type de donnée particulier:

CPT::boolean Engine::OnBaseLocalDevice_RequestInputModifierValueRange(
    IN CPNS::IBaseLocalDevice * const pNotifiedObject,
    IN CPT::uint16 const wRequestID,
    IN CPT::uint16 const wInputID,
    IN CPT::uint16 const wModifierNumber,
    IN CPNS::Enums::DataTypes const dataType)
{
    // Vérification de la requête
    if ((wInputID == m_pInput->GetInputID()) &&
        (wModifierNumber == CPNS::Enums::MOD_Transpose) &&
        (dataType == CPNS::Enums::DT_MKZ16))
    {
        pNotifiedObject->Reply_RequestInputModifierValueRange(
            wRequestID,
            // Min
            CPNS::Value(1,TRUE),
            // Mid
            CPNS::Value(0x8000,TRUE),
            // Max
            CPNS::Value(0xFFFF, TRUE));
        return TRUE;
    }
    return FALSE;
}

Maintenant, on doit répondre à une requête d'énumération des items d'un selector. Vu qu'on ne supporte pas de Selector, un simple return FALSE suffit.

CPT::boolean Engine::OnBaseLocalDevice_RequestInputSelectorValueText(
    IN CPNS::IBaseLocalDevice * const pNotifiedObject,
    IN CPT::uint16 const wRequestID,
    IN CPT::uint16 const wInputID,
    IN CPT::uint16 const wSelectorNumber,
    IN CPT::uint16 const wItemIndex)
{
    return FALSE;
}

Un contrôleur peut également souhaiter obtenir la valeur courante d'un paramètre spécifique. Cet exemple fait appel à une méthode _GetTransposeView() qui construit la valeur retournée ainsi que les textes associés. Le fait de délocaliser cette construction dans une méthode a du sens car on en aura aussi besoin un peu plus loin lorsqu'on parlera de la synchro.

CPT::boolean Engine::OnBaseLocalDevice_RequestCurrentValue(
    IN CPNS::IBaseLocalDevice * const pNotifiedObject,
    IN CPT::uint16 const wRequestID,
    IN CPNS::Enums::ExplorationItemTypes const type,
    IN CPT::uint16 const wInputOutputID,
    IN CPT::uint16 const wNumber,
    IN CPT::uint16 const wIndex)
{
    // Check que la requête
    if ((wInputOutputID == m_pInput->GetInputID()) &&
        (type == CPNS::Enums::EIT_InputModifier))
    {
        // Vérification de l'identité du message associé au paramètre
        if ((wNumber == CPNS::Enums::MOD_Transpose) && (wIndex == 0))
        {
            // Création d'une valeur qui représente le paramètre
            CPNS::Value v;
            CPT::UTF8String t;
            CPT::UTF8String u;
            _GetTransposeView(v, t, u);
            // Reply to the request
            pNotifiedObject->Reply_RequestInputModifierCurrentValue(
                wRequestID,
                v, t, u
                );
            return TRUE;
        }
    }
    return FALSE;
}

Et voici la méthode _GetTransposeView():

void Engine::_GetTransposeView(CPNS::Value& outValue, CPT::UTF8String& outText, CPT::UTF8String& outUnit)
{
    // Calcul de la position de bouton qui correspond à la transposition courante
    CPT::uint16 v = 0x8000 + m_semiToneOffset * 0x7FFF / 24;
    // Charge la valeur en mode bipolaire. C'est utile pour insiquer au contrôleur qu'il peut afficher un
    // bouton avec le zéro centré si il en est capable.
    outValue.SetValue(v, TRUE);
    // Création de la représentation textuelle
    outText.Set(CPT::UTF8String::FromInt32(m_semiToneOffset));
    // Unité
    outUnit.Set(" semitone");
}

Et enfin la méthode qui permet d'associer du texte aux index. Etant donné qu'on n'utilise pas d'index, on retourne simplement FALSE.

CPT::boolean Engine::OnBaseLocalDevice_RequestIndexText(
    IN CPNS::IBaseLocalDevice * const pNotifiedObject,
    IN CPT::uint16 const wRequestID,
    IN CPNS::Enums::IndexTextTypes const type,
    IN CPT::uint16 const wInputOutputID,
    IN CPT::uint16 const wNumber,
    IN CPT::uint16 const wIndex)
{
    return FALSE;
}

Et la synchro?
Et bien la synchro, c'est tout simple... Un contrôleur doit recevoir une mise à jour de la valeur courant des paramètres lorsque cette valeur change, ou bien lorsqu'il le demande.

Nous allons donc ajouter l'appel suivant dans OnInput_Message(), juste après avoir modifié m_semiToneOffset:

_RefreshCurrentValue();      

et également dans OnInput_QueryCurrentValues():

void Engine::OnInput_QueryCurrentValues( IN CPNS::IInput * const pNotifiedObject, IN CPT::CEndPoint const & source )
{
    _RefreshCurrentValue();
}

Cette méthode _RefreshCurrentValue() utilise la méthode _GetTransposeView() préalablement définie et envoie les données récoltées vers l'Input qui elle-même transmettra l'info vers toutes les Outputs qui y sont connectées.

void Engine::_RefreshCurrentValue()
{
    // Récupération de la valeur courant de la transposition
    CPNS::Value v;
    CPT::UTF8String t;
    CPT::UTF8String u;
    _GetTransposeView(v, t, u);
    // Mise à jour à travers le mécanisme de synchronisation
    m_pInput->RefreshCurrentModifierValue(
        // Identité du message
        CPNS::Enums::MOD_Transpose, 0,
        // Données
        v, t, u
        );
}

Remarque importante concernant la synchronisation
Attention au fait que si on n'y prend garde on peu charger méchamment le réseau CopperLan, ce qui n'a pas ou peu d'impact lorsque ce réseau est constitué d'ordinateurs, mais qui peut devenir problématique si on s'adresse à du hardware qui n'a pas forcément la capacité de traitement d'un ordi.
Par exemple, un contrôleur hardware qui envoie des Modifiers issus d'une entrée analogique doit s'assurer qu'il n'y a pas de bruit sur cette entrée et n'envoyer des messages que lorsque c'est utile.
De même, le système de synchro s'adressant à priori à des êtres humains, ça ne sert à rien de signaler un changement de valeur courante à chaque fois si cela survient plus de 50 fois par seconde...

Conclusion
Et voilà, vous avez maintenant une application capable d'être éditée à partir du CopperLan Manager.

J'anticipe les remarques: la version actuelle du CopperLan Manager ne supporte pas le flag bipolaire, et donc le bouton de réglage de la transposition a son zéro calé à gauche...Ce ne sera plus le cas avec la prochaine version sur laquelle nous travaillons actuellement.