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
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
{
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
{
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.