EPOOL - Environnement de Programmation Orientée Objet par Irv Kalb

Chapitre précédent

Table des matières

Chapitre suivant

Gestionnaire de son

Gérer les sons nous donne une bonne occasion d'illustrer la programmation orientée objet. Nous verrons comment un gestionnaire de sons peut être un excellent exemple d'encapsulation, puisqu'un seul objet peut être écrit pour gérer tous les aspects du son dans un programme. Lors de la modélisation de la gestion globale du son pour un projet, il y a de nombreux cas à prendre en compte. Un gestionnaire de son peut être utilisé pour s'occuper de tous les détails d'administration des ressources, tandis qu'il fournit une interface claire à ses clients.

Dans ce chapitre nous verrons comment concevoir différents gestionnaires de sons, en fonction de différentes contraintes. Comme nous avancerons dans ce développement, nous verrons comment faire d'un simple objet un objet gestionnaire d'objets - un seul objet qui en administre d'autres.

 

Un gestionnaire de son simple

Imaginons un projet éducatif qui comporte deux types de sons, par exemple les sons de narration et des effets sonores. Les sons de narration sont de longue durée, et joués à l'arrivée à une certaine "page" définie dans le scénario. Les effets sonores sont de courte durée et joués en réponse à des événements - clic sur un boutton, réponse à la question, etc. La création d'un gestionnaire de son serait un moyen idéal pour s'adapter à ces caractéristiques. Le code pour jouer et contrôler les différents types de sons pourrait être encapsulé dans un objet de gestion du son accessible globalement. Voici le code pour un gestionnaire de son simple qui satisfait ces critères:

-- script GestionnaireSonSimple

property pDelim
property pPisteSonNarration
property pPisteSonEffets

on
new me
  pDelim = the
last char of the moviePath
  -- On utilise la piste 1 pour les sons de narration
  -- et la piste 2 pour les effets sonores
  pPisteSonNarration = 1
  pPisteSonEffets = 2
  
  return
me
end

on mLectureSonNarration me, nomFichierSon
  sound
stop pPisteSonNarration -- au cas où un son est déjà en cours de lecture
  cheminCompletSon = the
moviePath & "Sons" & pDelim & nomFichierSon
  sound
playFile pPisteSonNarration, cheminCompletSon
end


on mPauseSonNarration me
  sound(pPisteSonNarration).pause()
end

on mContinueSonNarration me
  sound(pPisteSonNarration).play()
end

on mLectureEffet me, nomSon
  sound
stop pPisteSonEffets -- au cas où un son est déjà en cours de lecture
  puppetSound pPisteSonEffets,
nomSon
end



La démarche est très directe. Pour instancier un objet global de gestion du son nous faisons (par exemple dans un gestionnaire prepareMovie):

global goGestionnaireSonSimple

goGestionnaireSonSimple = new(script "GestionnaireSonSimple")

A l'instanciation, le gestionnaire de son définit une propriété, pDelim, pour identifier le caractère de délimitation des chemins ('\' ou ':') .On le fait une fois ici, puis on l'utilisera plus tard. Ensuite nous définissons pPisteSonNarration et pPisteSonEffets à 1et 2 pour correspondre aux pistes son 1 et 2. En plaçant ces valeurs à l'intérieur de l'objet, le GestionnaireSonSimple masque les détails de l'implémentation au client. Cette technique est appelée masquage d'information. De l'extérieur de l'objet, aucun client ne sait - ni n'a besoin de savoir - quelles pistes sont utilisées. En fait les clients du gestionnaire de son n'ont même pas besoin de connaître le concept de piste sonore.

D'autres langages de programmation ont un type de données appelée constante. L'idée est que vous définissez une valeur qui ne peut varier, mais dont vous pouvez utiliser le nom plutôt que la valeur, puisqu'un nom porte plus d'information qu'une valeur. Dans d'autres langages vous pourriez écrire quelque chose comme:

constant cPiseSonNarration = 1

constant cPisteSonEffets = 2

Et vous pourriez utiliser cette constante partout. Mais Lingo ne possède pas de telle constante. Un moyen de le simuler et de faire comme nous avons fait ici. Si vous êtes à l'intérieur d'un objet, vous utilisez une propriété, définie une fois pour toute dans le gestionnaire "new". Une autre méthode est d'utiliser une globale et de l'initialiser dans le gestionnaire prepareMovie.

Puisque les sons de narration sont longs (ie, sont des fichiers de taille importante), nous avons choisi un modèle où les sons de narration sont des fichiers externes. De plus, pour avoir une structure claire, nous plaçons tous les sons de narration dans un répertoire nommé "Sons" à côté du programme. Lorsque nous voulons lire un son de narration, nous utilisons "sound play file" et construisons à la volée le chemin complet vers le fichier son. C'est ce qui ce passe dans la méthode mLectureSonNarration. La première ligne arrête la lecture éventuelle d'un son sur cette piste. Ensuite, en utilisant le nomFichierSon passé en argument et le délimiteur que nous avons défini et enregistré plus tôt, nous créons le chemin complet du fichier. Enfin nous lançons la lecture du son. De l'extérieur de l'objet, dès que nous voulons jouer un son de narration il nous suffit d'écrire quelque chose comme:

global goGestionnaireSonSimple

goGestionnaireSonSimple.mLectureSonNarration("unNomDeFichierSon")

Nous avons ensuite défini de simples méthodes de contrôle qui permettent de mettre le son en pause et de le relancer si l'utilisateur le souhaite (regardez les méthodes mPauseSonNarration et mContinueSonNarration qui utilisent des nouvelles fonctions de Director 8). Puisque nous avons sauvegardé la piste utilisée pour les sons de narration, ces méthodes sont triviales.

Les effets sonores sont généralement courts (ie des fichiers de petite taille). Nous avons choisi que les effets sonores étaient des acteurs de l'animation. Lorsque le GestionnaireSonSimple veut lire un effet sonore, il se contente d'un puppetSound pour le jouer. Si vous regardez le code de la méthode mLectureEffet method, vous verrez cette implémentation simple. Quand nous voulons jouer un effet sonore, il nous suffit d'écrire une ligne du style:

global goGestionnaireSonSimple

goGestionnaireSonSimple.mLectureEffet("unNomDActeurSon")

 

Un gestionnaire de son plus compliqué

Maintenant attaquons nous à des contraintes plus fortes. Supposons que nous voulons être capable de lire un nombe quelconque de sons (toujours internes ou externes) en simultané, sans avoir à se soucier de la gestion des pistes audio. Nous pouvons écrire un gestionnaire de son qui cherche dynamiquement quelle piste utiliser. Une autre contrainte pourrait être l'ajout de la possibilité de modifier un son pendant sa lecture (stop, fondu in ou out, augmenter ou diminuer le volume, etc.).

Pour implémenter cette variation, le gestionnaire son générera un indentifiant unique pour un son - que nous appellerons soundID - à chaque requête de lecture de son. Lorsque le gestionnaire de son veut lire un son, il trouve une piste non utilisée, génére un soundID unique, lance le son approprié, et stocke le soundID dans une liste les regroupant tous. Par exemple si nous voulons utiliser 4 pistes, nous commencerons par créer une liste (plSounsIDs) de 4 éléments. Nous initialiserons toutes ses valeurs à zéro, chaque zéro indiquant qu'aucun son n'est joué dans la piste correspondante. A l'arrivée de la première requête de lecture d'un son, nous incrémenterons une propriété (pSoundIDCourant) pour trouver un identifiant unique, nous chercherons une piste libre et lancerons la lecture du son sur cette piste. Enfin nous stockerons le nouvel identifiant soundID dans l'enregistrement approprié de la liste plSoundIDs.

Cette version du gestionnaire de son permet aussi n'importe quel nombre de méthodes "d'action". Chaque méthode représente une action qui peut être effectuée sur un son en cours de lecture. Dans le code ci-après, nous n'implémenterons que la méthode mStopSon. L'identifiant soundID créé par le gestionnaire de son à chaque requête de lecture d'un son est unique - l'identifiant permet de retrouver le son de manière certaine. Le concept est le même que celui que nous avons vu avec les références d'objet en Lingo. Lorsque vous créez un objet dans Director, Director vous retourne une référence vers cet objet. Lorsque vous appellez les méthodes d'un objet, vous avez besoin d'identifier grâce à la référence d'objet l'instance sur laquelle vous appliquez ces méthodes. Dans ce gestionnaire de son, à chaque fois que vous voulez effectuer une action sur un son, vous appellez la méthode appropriée du gestionnaire de son en lui passant l'identifiant soundID en paramètre.

Utilisons une analogie. Supposons que vous achetez un article sur internet et que le vendeur décide de vous le livrer par UPS. Lorsque le vendeur donne le colis à UPS, UPS créé un numéro de suivi et le donne au vendeur, qui peut vous le transmettre. Une fois que vous avez ce numéro de suivi, à chaque fois que vous voulez connaître une propriété du colis (sans doute souhaitez vous savoir où il se trouve) vous devez contactez UPS et leur donner votre numéro de suivi. Le numéro de suivi est l'identifiant qui vous est nécessaire pour effectuer n'importe quelle opération sur le colis. Si vous souhaitez modifier l'adresse de livraison ou définir un horaire, vous avez besoin de ce numéro. Ce numéro de suivi est dans un format interne généré par UPS (lors d'un récent achat j'ai eu un numéro qui ressemblait à: 1ZV44V58034409782). Cette valeur a sans doute un sens pour UPS puisqu'elle leur permet d'identifier de manière unique votre colis. Mais en tant que numéro hors du système de suivi d'UPS, il ne contient aucune information. De la même manière, l'identifiant soudID généré par le gestionnaire de son a un sens à l'intérieur de l'objet, mais pour un client (a l'extérieur du gestionnaire) Il ne contient aucune information.

Le code commence à se compliquer, mais accompagné de commentaire et d'explications vous devriez l'assimiler facilement:

-- GestionnaireSonDynamique

property pDelim -- délimiteur de chemin
property pnPistes -- nombre de pistes
property pSoundIDCourant -- identifiant soundID courant
property plSoundIDs -- liste des soundIDs par piste


on
new me, nPistes
  pnPistes =
nPistes
  pDelim = the
last char of the moviePath
  plSoundIDs = []
  repeat
with ceNumeroDePiste = 1 to pnPistes
    append(plSoundIDs, 0)
  end
repeat
  pSoundIDCourant = 0

  return
me
end
new

on mLectureSonExterne me, nomSon
  soundID = me.mLectureSon(
nomSon, #externe)
  return soundID
end mLectureSonExterne


on mLectureSonInterne me,
nomSon
  soundID = me.mLectureSon(
nomSon, #interne)
  return soundID
end mLectureSonInterne

on mLectureSon me,
nomSon, symInterneOuExterne
  numeroPiste = me.mAffectePiste()

  pSoundIDCourant = pSoundIDCourant + 1
-- crée un soundID unique à chaque lecture de son
  plSoundIDs[
numeroPiste] = pSoundIDCourant
  
  if symInterneOuExterne = #externe then
    cheminCompletSon = the
moviePath & "Sons" & pDelim & nomSon
    sound
playFile numeroPiste, cheminCompletSon
  else
    puppetSound
numeroPiste, nomSon
  end
if
  return pSoundIDCourant -- retourne l'identifiant au client
end


on mAffectePiste me

  -- Vérifie si tous les sons en cours de lecture sont toujours en train d'être lus
  -- Si la lecture est terminée on remet le soundID à zéro.

  repeat
with ceNumeroPiste = 1 to pnPistes
    if plSoundIDs[
ceNumeroPiste] > 0 then
      if not(soundBusy(
ceNumeroPiste)) then
        plSoundIDs[
ceNumeroPiste] = 0
      end
if
    end
if
  end
repeat
  
  -- Maintenant on cherche une piste libre
  quellePiste = getOne(plSoundIDs, 0)
  if
quellePiste > 0 then -- on en a trouvé une, on la retourne
    return
quellePiste
  end
if
  
  -- Toutes les pistes sont occupées
  -- On cherche la piste avec le plus vieux son (le plus petit soundID)
  -- On arrête ce son pour libérer la piste
  -- on redéfinit ce soundID et on retourne le numéro de piste
  idDoyen = min(plSoundIDs)
  pisteAvecIdDoyen = getOne(plSoundIDs,
idDoyen)
  sound
stop pisteAvecIdDoyen
  plSoundIDs[
pisteAvecIdDoyen] = 0
  return
pisteAvecIdDoyen
end mAffectePiste

on mArretSon me, soundID
  numeroPiste = me.mTrouvePisteDuSoundID(soundID)
  if
numeroPiste = 0 then -- piste ou soundID non valide
    return
  end
if
  
  sound
stop numeroPiste
  
end mArretSon


on mTrouvePisteDuSoundID me, soundID
  if soundID = 0
then
    return
0
  end
if
  
  -- Cherche l'identifiant. Si aucun trouvé, la valeur retournée sera 0.
  quellePiste = getOne(plSoundIDs, soundID)
  return
quellePiste
end mTrouvePisteDuSoundID


Dans la méthode "new" nous faisons l'initialisation des propriétés. Nous sauvegardons le nombre de pistes dans une propriété, stockons le délimiteur de chemin, et initialisons la liste des soundIDs et la valeur du soundID courant à zéro.

mLectureSonExterne et mLectureSonInterne sont les deux méthodes principales pour le client lorsqu'il veut lire un son. Ces deux méthodes appellent la méthode privée mLectureSon (qui n'est pas sensée être appelée de l'extérieur de l'objet). Après l'exécution de mLectureSon, ces deux méthodes principales retournent au client un soundID unique pour le son en réponse à leur requête. Si le client veut plus tard agir sur ce son, il devra utiliser ce soundID.

La méthode mLectureSon appelle mAffectePiste pour récupérer une piste dans laquelle lire le son. Ensuite elle incrémente pSoundIDCourant pour créer un soundID unique pour le son courant, et le sauvegarde à l'indice approprié dans la liste plSoundIDs. En fonction de l'origine du son (interne ou externe) nous exécutons playFile ou puppetSound pour lire le son. Enfin nous retournons l'identifiant soundID à l'appelant.

La méthode mAffectePiste choisit dynamiquement la piste. Voici la logique utilisée dans ce choix. D'abord nous parcourons la liste plSoundIDs, et pour toutes les pistes en train de lire un son (la valeur est supérieure à zéro) nous vérifions si la piste est encore utilisée. Si la lecture sur cette piste est terminée, nous réinitialisons à zéro cette piste dans la liste plSoundIDs. Ensuite nous cherchons s'il reste une piste libre pour lire le nouveau son - si oui nous retournons son numéro de piste. Si aucune piste libre n'est trouvée, nous devons décider autoritairement laquelle couper. Nous décidons que si toutes les pistes sont occupées, nous coupons le "plus vieux" son en train d'être joué. Pour trouver ce son il suffit de chercher le plus vieux soundID, c'est à dire le plus petit. Une fois trouvé, nous arrêtons la lecture de ce son, marquons la piste comme libre et retournons son numéro. Puisque ce choix est implémenté une seule fois à cet endroit (encapsulé dans une méthode du gestionnaire de son), si pour une raison quelconque nous décidons plus tard de changer cette règle, il nous suffit de modifier cette seule méthode pour propager la mise à jour à l'ensemble du programme. Il n'y a aucun changement à faire dans les appels des clients aux méthodes du gestionnaire de son.

Comme nous l'avons annoncé, nous pouvons ajouter autant de méthodes d'action pour modifier le son en train d'être lu. Pour illustrer ces méthodes par un exemple nous avons déjà codé mArretSon. Cette méthode commence par appeller mTrouvePisteDuSoundID pour connaître le numéro de la piste dans laquelle est joué le son correspondant à l'identifiant soundID. Ensuite il arrête ce son. D'autres méthodes "d'action" telle que pause, continue, restart, augmenter le volume, le diminuer, fondu in ou out, etc. peuvent facilement être ajoutées à cette architecture en ajoutant simplement des méthodes dans le gestionnaire de son. Le client appellerait alors la méthode d'action appropriée en lui passant le soundID en paramètre.

Comme son nom l'indique, mTrouvePisteDuSoundID prend comme paramètre un identifiant soundID et retourne le numéro de la piste dans laquelle est joué le son correspondant à cet identifiant. C'est une méthode privée qui peut être appelée par toutes les méthodes d'action. Il parcoure la liste plSoundIDs à la recherche du soundID passé en paramètre. Si cet identifiant n'est pas trouvé (eg, si la lecture du son est terminée), la méthode retourne zéro.

Cette version du gestionnaire de son fait aussi du masquage d'information. Un client du gestionnaire de son (c'est à dire n'importe quelle ligne du programme qui souhaite jouer un son) se contente de demander pour jouer tel son, sans savoir ni avoir à savoir quelle piste est utilisée. De plus, l'identifiant soundID qui permet de dialoguer avec le gestionnaire de son est dans un format interne à notre objet, et seulement compréhensible par lui. Puisque le soundId est retourné au client, celui-ci peut regarder sa valeur, mais elle n'a aucune signification hors de l'objet gestionnaire de son. Si après s'être adapté à d'autres contraintes, nous décidons de changer l'algorithme de génération de l'identifiant soundID, le code des clients resterait inchangé.

 

Repensons l'architecture du gestionnaire de son

Avec l'ajout de nouvelles fonctions au gestionnaire de son, nous pouvons penser que notre objet se complexifie. Du point de vue de l'exécution, grouper tout ce dont nous avons besoin dans un seul script est peut-être plus efficace. Cependant, d'un point de vue humain, avoir beaucoup de code dans un seul script peut commencer à brouiller notre vision de la chose. Arrivé à ce point, nous pouvons avoir envie de modifier légèrement l'architecture de notre objet pour séparer le code en plusieurs couches. C'est une approche évolutive.

Si nous prêtons attention à ce que fait le gestionnaire de son, nous nous apercevons qu'il exécute deux fonctions distinctes ; 1) attribuer les pistes, 2) gérer les opérations sur chaque piste (eg, lancer la lecture du son, l'arrêter, faire un fondu, etc.). Nous pouvons utiliser cette distinction des fonctions pour séparer les fonctionnalités et déplacer certaines portions de code dans un nouvel objet. Le gestionnaire de son pourrait continuer à attribuer les pistes, mais il pourrait aussi instancier un objet PisteSon pour chaque piste utilisée. Ainsi chaque objet PisteSon gérerait tous les détails du son pour sa propre piste. C'est la même chose que la délégation de responsablilités dans votre environement de travail. Au travail, le rôle du chef d'équipe est de prendre les décisions qui affectent tous les membres de l'équipe. Ici le travail du gestionnaire de son sera d'assigner les pistes, mais il transférera les requêtes pour agir sur les sons à l'objet pisteSon de son choix.

Puisque nous suivons une approche orientée objet, les clients utiliseront toujours les même méthodes que dans notre gestionnaire de son dynamique précédent. Encore une fois, les clients ne connaissent pas et n'ont pas besoin de connaître les détails de l'implémentation de notre gestionnaire de son, ils veulent juste lire des sons et agir dessus. Le fait que notre gestionnaire utilise d'autres objets ne concerne pas le client. Voici encore un exemple du monde réel. Si vous appellez quelqu'un pour réparer votre maison. S'il s'agit d'une très petite compagnie, la personne qui vous répond au téléphone sera peut-ête celle qui viendra effectuer la réparation. Mais lorsque l'enreprise grandira, il est plus probable que la personne qui vous répondra au téléphone attribuera la réparation à quelqu'un d'autre (un technicien spécialisé).

Dans notre nouveau gestionnaire de son éclaté, supposez qu'on veuille lire deux sons A et B, puis arrêter plus tard le son A. Nous appellerons le gestionnaire de son pour démarrer les deux sons, et il nous retournera deux identifiants soundID uniques. Cependant en interne, le gestionnaire de son attribue ces deux sons à deux instances différentes d'objet PisteSon. Puis plus tard nous appellons le gestionnaire pour lui demander de stopper le son A (en lui passant le soundID du son A) et le son s'arrête. A l'intérieur du gestionnaire de son, celui-ci cherche à quelle piste correspond le soundID passé, et transfère la requête à l'objet PisteSon qui gère cette piste. Voici un squelette de ce type de gestionnaire de son. Remarquez que certaines actions et propriétés ont été déplacées de l'objet gestionnaire de son vers l'objet PisteSon.

-- GestionnaireSonEclate

property pnPistes -- nombre de pistes
property pSoundIDCourant -- identifiant soundID courant
property ploPisteSon -- liste des objets PisteSon


on
new me, nPistes
  pnPistes = nPistes
  theDelim = the
last char of the moviePath
  ploPisteSon = []
  repeat
with ceNumeroPiste = 1 to pnPistes
    oPisteSon = new(script
"PisteSon", theDelim, ceNumeroPiste)
    append(ploPisteSon,
oPisteSon)
  end
repeat
  pSoundIDCourant = 0

  return
me
end
new

on mLectureSonExterne me, nomSon
  soundID = me.imLectureSon(nomSon, #externe)
  return soundID
end mLectureSonExterne


on mLectureSonInterne me, nomSon
  soundID = me.imLectureSon(nomSon, #interne)
  return soundID
end mLectureSonInterne

-- Le nom "im" LectureSon précise qu'il s'agit d'une méthode
-- interne de l'objet GestionnaireSonEclate. C'est à dire que
-- cette méthode ne doit être appelée que de l'intérieur de l'objet

on imLectureSon me, nomSon, symInterneOuExterne
  numeroPiste = me.mAffectePiste()
  pSoundIDCourant = pSoundIDCourant + 1
-- crée un identifiant unique soundID pour chaque nouveau son
  
  -- Cherche l'objet PisteSon approprié
  -- et lui demande de démarrer la lecture du son.
  oPisteSon = ploPisteSon[
numeroPiste]
  oPisteSon.mLectureSon(nomSon, symInterneOuExterne, pSoundIDCourant)
  
  return pSoundIDCourant -- retourne l'identifiant au client
end



on mAffectePiste me

  -- Vérifie si tous les sons en cours de lecture sont toujours en train d'être lus
  -- Si la lecture est terminée on remet le soundID à zéro.

  repeat
with ceNumeroPiste = 1 to pnPistes
    oPisteSon = ploPisteSon[ceNumeroPiste]
    ceSoundID = oPisteSon.mGetSoundID()
    if ceSoundID > 0
then
      if not(soundBusy(ceNumeroPiste)) then
        oPisteSon.mLibereSoundID()
      end
if
    end
if
  end
repeat
  
  -- Maintenant on cherche une piste libre
  repeat
with ceNumeroPiste = 1 to pnPistes
    oPisteSon = ploPisteSon[ceNumeroPiste]
    soundIDDeLaPiste = oPisteSon.mGetSoundID()
    if soundIDDeLaPiste = 0
then -- on en a trouvé une, on la retourne
      return ceNumeroPiste
    end
if
  end
repeat
  
  
  -- Toutes les pistes sont occupées
  -- On cherche la piste avec le plus vieux son (le plus petit soundID)
  -- On arrête ce son pour libérer la piste
  -- on redéfinit ce soundID et on retourne le numéro de piste

  idDoyen = the
maxInteger -- initialisation de la variable
  repeat
with ceNumeroPiste = 1 to pnPistes
    oPisteSon = ploPisteSon[ceNumeroPiste]
    soundIDDeLaPiste = oPisteSon.mGetSoundID()
    if soundIDDeLaPiste < idDoyen then
-- on en a trouvé un plus ancien
      idDoyen = soundIDDeLaPiste
      
pisteAvecIdDoyen = ceNumeroPiste
    end
if
  end
repeat
  
  -- on arrête la lecture du son sur la piste "pisteAvecIdDoyen"
  -- pour que le nouveau son puisse être lu
  oPisteSon = ploPisteSon[pisteAvecIdDoyen]
  oPisteSon.mArretSon()
  return pisteAvecIdDoyen
end mAffectePiste

on mArretSon me, soundID
  numeroPiste = me.mTrouvePisteDuSoundID(soundID)
  if numeroPiste = 0
then -- piste non valide
    return
  end
if
  oPisteSon = ploPisteSon[numeroPiste]
  oPisteSon.mArretSon()
end mArretSon


on mTrouvePisteDuSoundID me, soundID
  if soundID = 0
then
    return
0
  end
if
  
  -- demande à chaque piste son soundID courant
  -- si nous trouvons la valeur demandée, on retourne ce numéro de piste
  repeat
with ceNumeroPiste = 1 to pnPistes
    oPisteSon = ploPisteSon[ceNumeroPiste]
    soundIDDeLaPiste = oPisteSon.mGetSoundID
    if soundID = soundIDDeLaPiste then
-- trouvé!
      return ceNumeroPiste
    end
if
  end
repeat
  -- pas trouvé, on retourne 0
  return
0
end mTrouvePisteDuSoundID


on mLibere me
  repeat
with ceNumeroPiste = 1 to pnPistes
    oPisteSon = ploPisteSon[ceNumeroPiste]
    oPisteSon.mLibere()
    ploPisteSon[ceNumeroPiste] = VOID

  end
repeat
end mLibere


Voici le code du script parent PisteSon. Dans sa méthode "new", le gestionnaire instancie un objet PisteSon pour chaque piste son réelle que le programme veut pouvoir gérer.


-- PisteSon

property pDelim -- delimiteur de chemin
property pSoundID -- identifiant sound ID courant
property pNumeroPiste -- numéro de la piste son gérée par cet objet


on
new me, theDelim, numeroPiste
  pDelim = theDelim
  pNumeroPiste = numeroPiste
  pSoundID = 0

  return
me
end
new


on mGetSoundID me
  return pSoundID
end

on mLibereSoundID me
  pSoundID = 0
end

on mLectureSon me, nomSon, symInterneOuExterne, soundID
  pSoundID = soundID
  if symInterneOuExterne = #externe then
    cheminCompletSon = the
moviePath & "Sons" & pDelim & nomSon
    sound
playFile pNumeroPiste, cheminCompletSon
  else
    puppetSound pNumeroPiste, nomSon
  end
if
end

on mArretSon me
  sound
stop pNumeroPiste
  pSoundID = 0
end mArretSon



Mainetant que nous avons créé cette structure, il est facile d'ajouter des fonctionnalités. Un exemple serait de faire retenir facilement à chaque objet PisteSon le nom du son actuellement joué si bien qu'on pourrait ajouter des méthodes pour retrouver le nom du son lu. Un autre exemple serait l'ajout d'un système de file d'attente à chaque objet PisteSon, ainsi lorsqu'un son est terminé, un autre son démarre aussitôt. Juste pour illustrer le cas par du code, supposons que nous voulions ajouter "faire un fondu out du son". Nous utiliserons la méthode mArretSon du gestionnaire de son et de l'objet PisteSon comme prototypes. Dans le gestionnaire de son, nous devrions ajouter la méthode suivante:

on mFonduOut me, soundID
  numeroPiste = me.mTrouvePisteDuSoundID(soundID)
  if numeroPiste = 0
then -- piste non valide
    return
  end
if
  oPisteSon = ploPisteSon[numeroPiste]
  oPisteSon.mFonduOut()
end
mFonduOut

 

Et dans le script de l'objet PisteSon, nous ajoutons:

on mFonduOut me
  sound fadeOut pNumeroPiste

end
mFonduOut


Soyons propres

Dans ce chapitre nous avons vu comment un objet simple peut être transformé en un objet gestionnaie d'objets pour créer et transmettre des traitements à des objets subordonnés. Dans notre introduction aux objets, nous avons vu comment, lors de l'instanciation d'un objet, Director alloue de la mémoire pour les propriétés de l'objet. Nous avons aussi vu que lorsque nous n'avons plus besoin d'un objet, nous devons le libérer (ie, libérer la mémoire utilisée par l'objet et remettre la référence vers l'objet à VOID). Si nous ne le faisons pas, la mémoire allouée à un objet ne peut jamais être récupérée par Director. Et si nous perdons ainsi de la mémoire de façon répétée, nous pouvons épuiser la mémoire disponible et planter le programme.

Mais un objet gestionnaire d'objet est un cas particulier. Nous devons être très prudents lorsque nous voulons libérer un objet gestionnaire d'objet ou arrêter un programme qui contient de tels objets. Si un objet instancie un ou plusieurs objets (se transformant ainsi en objet gestionnaire d'objets), alors lorsqu'il doit être détruit, il a la responsabilité de libérer le ou les objets qu'il a instancié.

En Lingo, lorsque vous créez un objet, vous appellez explicitement la méthode "new" du script parent. Dans d'autres langages de programmation cette méthode est appelée constructeur puisqu'elle vous permet de construire un nouvel objet. Et dans d'autres langages que Lingo il existe une méthode générique pour se débarrasser d'un objet, appelée destructeur.

Pour combler ce défaut de Director, nous pouvons adopter une convention. Cette convention est que dans chaque script parent que vous écrivez vous incluez une méthode de destruction utilisant toujours un nom générique. Si nous respectons cette convention, alors nous avons une structure qui permet aux objets de libérer la mémoire correctement. Pour être plus précis, chaque parent doit inclure une méthode nommée "mLibere" (n'importe quel autre nom irait aussi bien, le plus important est de suivre un consensus). mLibere est un nom simple qui définit bien le fonction de la méthode. Dans le code de mLibere nous écrivons le code nécessaire à la libération de tous les objets que nous avons instancié, et n'importe quelle autre instruction que nous souhaitons exécuter avant la disparition de l'objet.

Voici un exemple basé sur le gestionnaire de son "éclaté". Habituellement nous instancierons un objet accessible globalement comme notre gestionnaire de son dans un gestionnaire prepareMovie ou startMovie comme ceci :

global goGestionnaireSon

on
prepareMovie
  goGestionnaireSon = new(script
"GestionnaireSonEclate", 4)
end

Dans notre gestionnaire stopMovie, nous devons inclure le code pour libérer tous les objets de portée globale comme celui que nous avons créé. Comme nous l'avons déjà dit, se contenter de mettre la variable à VOID libérera la mémoire allouée à cet objet. Mais si par exemple nous mettons goGestionnaireSon à VOID, nous ne donnons aucune chance au gestionnaire de son de "se nettoyer". Le résultat sera la libération effective de l'objet gestionnaire de son, mais tous les objets PisteSon créés par cet objet ne le seront pas et la mémoire qu'ils occupent sera perdue. Donc juste avant d'attribuer VOID à goGestionnaireSon, nous appellons la méthode mLibere de cet objet gestionnaire de son. Voici le code:

on stopMovie
  goGestionnaireSon.mLibere()
  goGestionnaireSon = VOID
end

Ainsi la méthode mLibere du gestionnaire de son lui donne l'opportunité de nettoyer son monde. Son monde est constitué d'un certain nombre d'instances d'objets PisteSon. Dans le code de la méthode mLibere du gestionnaire de son, nous appellons la méthode mLibere de chaque objet PisteSon puis définissons la variable appropriée àVOID. Voici comment nous faisons:

on mLibere me
  repeat with ceNumeroPiste = 1 to pnPistes
    oPisteSon = ploPisteSon[ceNumeroPiste]
    oPisteSon.mLibere()
    ploPisteSon
[ceNumeroPiste] = VOID
  end
repeat
end mLibere



En respectant cette convention, nous devons aussi ajouter la méthode mLibere à l'objet PisteSon. Si un script parent n'a pas de nettoyage particulier à faire dans sa méthode mLibere, nous pouvons nous limiter à la méthode "dégénérée" qui n'exécute rien. Ou pour être plus explicite, nous pouvons y mettre une commande "nothing" comme ci dessous:

on mLibere me
  nothing

end mLibere

Si l'objet PisteSon avait aussi instancié d'autres objets, il devrait appeller mLibere sur ces instances puis mettre leurs références à VOID.

Dans le cas de notre gestionnaire d'objets, voici la séquence complète des évenements. Dans le gestionnaire stopMovie nous appellons la méthode mLibere du gestionnaire de son. Celui-ci sait qu'il a créé des instances d'objets PisteSon, et qu'il doit donc appeller la méthode mLibere sur chacune de ces instances. la méthode de chaque objet PisteSon est executée mais ne fait rien. Au retour des fonctions mLibere des objets PisteSon, le gestionnaire de son définit la référence d'objet pour chacune de ces instances à VOID, libérant ainsi l'objet et la mémoire. Lorsque mLibere du gestionnaire de son a fini son exécution, nous retournons dans notre gestionnaire stopMovie pour définir la variable globale goGestionnaireSon à VOID, libérant ainsi l'objet gestionnaire de son.

Les clés du processus de libération sont ; 1) une convention qui définit un nom commun pour le méthode de destruction, et 2) dans cette méthode de destruction, chaque objet appelle la méthode de destruction des objets qu'il a créé, puis définit ses variables de référence d'objet à VOID. Un appel à la méthode mLibere d'un objet gestionnaire d'objets lance une séquence pseud-récursive d'appels à la méthode mLibere pour s'assurer que chaque objet peut nettoyer son monde. Dans l'exemple du gestionnaire de son, la hiérarchie n'est profonde que de deux niveaux. Cependant à tous les niveaux chaque objet gestionnaire d'objets étant responsable de la libération des objets qu'il a instanciés, une profondeur quelconque de la hiérarchie peut être nettoyée en suivant cette approche.

De plus, puisque vous savez que la méthode mLibere est un destructeur, vous savez qu'elle sera la dernière méthode appelée et exécutée de votre objet avant sa disparition, si vous développez un objet de suivi du parcours de l'utilisateur à travers un programme, vous pouvez avoir envie de placer le code qui sauvegarde l'état de progression de l'utilisateur dans un fichier à cet endroit. Un autre exemple peut être un objet de communication. Sachant lors de l'exécution de sa méthode mLibere qu'il va disparaître, nous pouvons y inclure du code pour fermer le canal de communication.

Chapitre précédent

Table des matières

Chapitre suivant