Il n’arrête pas d’arriver, je sais, mais rassurez-vous, la fin de l’attente se rapproche ! Continuons l’exploration de nos fonctions, et promis, ce coup-ci on va pouvoir entendre de la musique !
Sélectionner un répertoire
Vous l’avez vu sur les captures d’écran, tout en haut à droite du formulaire se trouve un bouton nous permettant de choisir le répertoire à scanner : $button_browse (c’est son petit nom). La première chose à faire est de le faire réagir à un clic de la souris :
$button_browse.Add_Click( { selectdir } )
Ok, c’est tout simple, on clique dessus et hop, ça lance la fonction selectdir. Easy. Et la voici :
function selectdir { $newdir = New-Object System.Windows.Forms.FolderBrowserDialog -Property @{SelectedPath = $textbox0.Text} [void]$newdir.ShowDialog() $textbox0.Text=$newdir.SelectedPath scandir }
Bon, qu’est-ce que ça fait tout ça ?
- On crée un nouvel objet $newdir de type System.Windows.Forms.FolderBrowserDialog, en le faisant pointer vers le répertoire contenu dans la propriété Text de $textbox0.
- On affiche le sélecteur avec la méthode ShowDialog(), qui est bloquante, comme pour notre formulaire (c’est-à-dire que tant qu’on ne l’a pas fermé, le programme est arrêté).
- On récupère le chemin sélectionné – la propriété SelectedPath de $newdir – et on la colle dans $textbox0.Text
- Enfin, on appelle la fonction scandir que nous avons vu dans l’article précédent. Ainsi, on aura une toute nouvelle playlist à rejouer.
Oui, nous aurions très bien pu mettre ces 4 lignes directement entre les accolades de $button_browse.add_Click(), mais personnellement je préfère appeler une fonction, chacun son style de programmation (et je sais que le mien est déplorable, mais il fonctionne).
La lecture (enfin !)
Le moment tant attendu est arrivé ! Nous allons enfin lire des MP3 ! Youhou ! Vous vous rappelez notre bouton Play ? Et bien, on va le faire aussi réagir au clic :
$button_play.Add_Click( { play } )
Si avec ça vous ne savez toujours pas comment faire réagir un bouton au clic, c’est à désespérer. Voyons maintenant notre fonction play :
function play { if($global:status -eq 0) # On est en attente et on va lancer la musique { # Notification $script:balloon = New-Object System.Windows.Forms.NotifyIcon $script:balloon.Icon = $formIcon $script:balloon.BalloonTipText = "Maintenant jouant : `n$($playlist[$global:i].performers) - $($playlist[$global:i].title)" $script:balloon.BalloonTipTitle = "DédéAmp $version" $script:balloon.Visible = $true $script:balloon.ShowBalloonTip(10000) $script:balloon.Visible = $false $script:balloon.Dispose() $textbox1.Text=$playlist[$global:i].title $textbox2.Text=$playlist[$global:i].performers $textbox3.Text=$playlist[$global:i].album $textbox4.Text=$playlist[$global:i].year $textbox5.Text="$($playlist[$global:i].bitrate) kbps" $textbox55.Text="$($playlist[$global:i].track)/$($playlist[$global:i].trackcount)" $textbox6.Text=$playlist[$global:i].genres $button_play.Image=[System.Drawing.Image]::FromFile("$path\pause.png") # On récupère l'image de couverture du MP3 s'il en a une, et on la met en background dans $pbox if($playlist[$global:i].pictures -ne $null) { $tagimage=[Convert]::ToBase64String($playlist[$global:i].pictures) $albumimage=base64toimage($tagimage) } else { $albumimage=$noimgimage } $pbox.BackgroundImage = $albumimage $pbox.BackgroundImageLayout = "Stretch" $j=$global:i+1 $realnbfiles=$global:nbfiles+1 $textbox10.Text="$j/$realnbfiles" $textbox7.Text="Lecture" $form.refresh() [uri]$song = $playlist[$global:i].fullname do { $MediaPlayer.Open($song) $songDuration = $MediaPlayer.NaturalDuration.TimeSpan.TotalMilliseconds } until ($songDuration) # On attend un peu $global:status=1 $global:timer.start() $MediaPlayer.Volume = 1 $MediaPlayer.Play() } elseif($global:status -eq 1) { $button_play.Image=[System.Drawing.Image]::FromFile("$path\play.png") $textbox7.Text="Pause" $form.Refresh() $global:timer.Stop() $MediaPlayer.Pause() $global:status=2 } elseif($global:status -eq 2) { $button_play.Image=[System.Drawing.Image]::FromFile("$path\pause.png") $textbox7.Text="Lecture" $form.Refresh() $global:timer.Start() $MediaPlayer.Play() $global:status=1 } }
Ouhlala, tout ça ?! Et oui… faut ce qu’il faut ! Mais là encore, rien de sorcier ! En fonction de l’état $global:status de mon player (s’il est en attente, en cours de lecture ou en pause), je ne vais pas faire les mêmes choses. Commençons par le début : le player est en attente, on vient de cliquer sur le bouton Play, il faut lancer la lecture…
Tout d’abord, nous allons créer une petite notification bien sympathique en créant un objet de type System.Windows.Forms.NotifyIcon. A quoi ça ressemble ? A ceci :
Nous lui définissons tout d’abord quelques propriétés :
- Icon : l’icône (ma tête) que vous voyez sur la capture d’écran.
- BalloonTipTitle : le titre (écrit en blanc) de la notification
- BalloonTipText : le message de la notification. Ici je récupère dans la playlist les informations (les tags ID3 artiste (performers) et titre (title)) de l’objet représentant le morceau à jouer
- Visible : indique si la bulle de notification est visible ($true) ou non ($false). On commence par la rendre visible.
- Puis on appelle la méthode ShowBalloonTip() en spécifiant qu’on veut la voir pendant 10000 millisecondes, soit 10 secondes (c’est le temps par défaut).
- Ensuite, on ne veut plus qu’elle soit visible, donc on passe la propriété Visible à $false
- Enfin, on supprime la bulle de notification avec la méthode Dispose()
Et on fera ça pour chaque morceau qui est joué. Un petit souci sur lequel hélas nous n’aurons pas la possibilité de faire grand chose : si vous changez de morceau rapidement (en cliquant frénétiquement sur les boutons Next ou Prev), les notifications vont se mettre en file d’attente et s’afficheront toutes pendant 10 secondes les unes après les autres… Donc il y aura un décalage entre ce qui est notifié et ce qui est joué…
Un petit aparté sur la syntaxe que j’ai utilisé qui peut paraître bizarre :
$script:balloon.BalloonTipText = "Maintenant jouant : `n$($playlist[$global:i].performers) - $($playlist[$global:i].title)"
D’abord, mon objet s’appelle $script:balloon. $script est ici le scope de ma variable (tout le script), un peu comme $global. Pourquoi avoir précisé ce scope ? Euh ben en fait, c’est parce que j’ai fait un bête copier/coller depuis un autre script que j’ai écrit il y a quelque temps… On pourrait très bien s’en passer.
Ensuite, cette partie :
$($playlist[$global:i].performers)
Nous sommes dans une chaîne de texte, délimitée par des guillemets « ». Si on met un nom de variable tout bête comme $toto, pas de problème, ça passe. Mais ce que nous voulons ici, c’est la propriété performers d’un objet qui est celui d’index $global:i d’une liste nommée $playlist ! Pour que Powershell ne perde pas les pédales et hurle à l’erreur de syntaxe, on l’aide un peu en délimitant tout ça par des parenthèses précédées d’un signe $. Et le tour est joué !
Ensuite, nous mettons à jour nos textboxes en affichant d’autres propriétés de notre objet, comme le titre, l’artiste etc. Si je n’ai pas de texte à rajouter, pas la peine de jouer du dollar et des parenthèses. Si par contre j’en rajoute ( » kpbs » dans l’exemple ci-dessous), je dois mettre mon petit dollar et ses parenthèses :
$textbox4.Text=$playlist[$global:i].year # pas de texte en plus, donc pas $( ) $textbox5.Text="$($playlist[$global:i].bitrate) kbps" # là par contre j'ai du texte en plus, donc je mets $( ) autour
J’en profite pour mettre aussi l’image de mon bouton Play à jour en lui collant le symbole « pause » (ici, chargé depuis le disque : rien ne vous empêche de l’intégrer au script en base64 comme je l’ai déjà expliqué dans un article précédent).
Dans mon formulaire, j’ai créé une picturebox, destinée comme son nom l’indique à afficher une image, en l’occurence la pochette de mon album. La picturebox est définie comme ceci dans la région Formulaire :
# Picturebox $pbox = New-Object Windows.Forms.PictureBox $pbox.Location = New-Object System.Drawing.Point(20,78) $pbox.Size = New-Object System.Drawing.Size(90,90)
Je me suis contenté de la positionner (Location) et de lui spécifier une taille (Size), de 90×90 pixels. N’oubliez pas : les pochettes de CD (et de vinyles) sont carrées, sauf exception ! Donc les images le sont aussi la plupart du temps !
Il faut donc récupérer l’image depuis la propriété pictures de mon objet custom (qui se trouve dans ma playlist). Pourquoi pictures avec un s ? Parce que les tags ID3 sont prévus pour intégrer plusieurs images : la pochette (front cover), mais aussi l’arrière (back cover), mais aussi éventuellement l’image du CD lui-même, le contenu du livret… Dans la vraie vie, la plupart du temps, soit il n’y aucune image (il faudra prévoir ce cas), soit il y a la front cover… mais rarement autre chose.
# On récupère l'image de couverture du MP3 s'il en a une, et on la met en background dans $pbox if($playlist[$global:i].pictures -ne $null) { $tagimage=[Convert]::ToBase64String($playlist[$global:i].pictures) $albumimage=base64toimage($tagimage) } else { $albumimage=$noimgimage } $pbox.BackgroundImage = $albumimage $pbox.BackgroundImageLayout = "Stretch"
Si ma propriété pictures n’est pas $null (donc qu’il y a une image), je la convertis d’abord en base64, puis je la reconvertis de base64 en image ! Pourquoi ?! Parce que je n’ai pas trouvé d’autre solution pour extraire les données image pour les faire s’afficher dans ma picturebox…
Si par contre je n’ai pas d’image, je remplace la pochette de l’album par une image explicite, stockée dans $noimgimage (qui est intégrée au script en base64) :
Enfin, je mets cette image dans ma picturebox via sa propriété BackgroundImage (et en lui demande de l’étirer avec la valeur « stretch » de BackgroundImageLayout). Lors de mes premières expérimentations avec la pochette de l’album, je l’avais mise en fond du formulaire avec ces deux lignes :
$form.BackgroundImage = $image $form.BackgroundImageLayout = "Stretch"
Mais le résultat… hum… disons qu’esthétiquement, c’est quand même pas top, et si la pochette est sombre, le formulaire devient carrément illisible !
Viennent ensuite les lignes suivantes :
$j=$global:i+1 $realnbfiles=$global:nbfiles+1 $textbox10.Text="$j/$realnbfiles" $textbox7.Text="Lecture" $form.refresh()
Ma variable $j va me servir à indiquer à quel morceau j’en suis dans ma playlist et $realnbfiles le nombre de MP3 qui s’y trouvent. N’oubliez pas que l’index $global:$i de $playlist commence à 0 : or pour un humain, le premier morceau doit avoir le numéro 1. D’où ma variable $j. Et pourquoi rajouter 1 à $global:nbfiles qui était déjà le nombre de morceaux $MusicFiles.Count auquel j’ai retiré 1 ? Comment dire… je programme parfois (souvent, ok ok) salement, et je me rend compte après coup que j’aurai du faire autrement et que j’ai besoin finalement d’une valeur que j’avais avant, voilà… Enfin bon, ce n’est pas bien grave, on met à jour $textbox10 et $textbox7 en conséquence, et c’est tout ce qui compte. Et je rafraîchis mon formulaire par un $form.refresh() en passant (j’ai plusieurs éléments, j’aurai pu les rafraîchir un par un ou par groupbox, mais autant faire simple, là).
Ensuite, je dois récupérer la durée du morceau :
[uri]$song = $playlist[$global:i].fullname do { $MediaPlayer.Open($song) $songDuration = $MediaPlayer.NaturalDuration.TimeSpan.TotalMilliseconds } until ($songDuration) # On attend un peu
Je récupère dans la variable $song (de type URI, Uniform Resource Identifier) le chemin complet et le nom de mon MP3 depuis la propriété fullname de mon objet dans la playlist.
J’appelle la méthode Open() de mon objet $MediaPlayer en lui fournissant $song en paramètre, et je lui demande la durée du morceau en allant lire sa propriété NaturalDuration.TimeSpan.TotalMilliseconds. Comme cela peut prendre un peu de temps (la durée n’est pas inscrite dans un tag, elle doit être calculée par rapport à chaque frame du flux MP3… c’est compliqué à faire et donc à expliquer), j’attends avec une boucle do…until que ma variable $songDuration soit créée et disponible.
Cette durée va m’être utile puisque je vais l’afficher dans une textbox, mais pas dans la fonction Play. Je garde ça pour plus tard.
$global:status=1 $global:timer.start() $MediaPlayer.Volume = 1 $MediaPlayer.Play()
Je mets le statut de mon player à 1 (en lecture), je démarre un timer, je mets le volume à fond, et je lance vraiment la lecture du MP3 avec la méthode Play() ! Enfin !
Mais mais mais… j’ai parlé de timer… Qu’est-ce que c’est encore que ça ?! Comme son nom l’indique, c’est un chronomètre, en réalité un événement qui va se déclencher à une fréquence que j’aurai indiqué. Pourquoi faire ? Et bien comme je suis en lecture, il faudra bien afficher à quel moment du morceau je me trouve ! Après tout, j’ai une textbox faite pour ça, il faudra bien afficher quelque chose dedans ! Et ce sera l’utilité de la fonction updatepos, nous verrons cela plus tard, et je reviendrai donc sur cette histoire de timer.
Reste à gérer deux autres statuts de mon player. Rappelez-vous : nous sommes passés d' »en attente » à « lecture ». Que faire si l’utilisateur appuie de nouveau sur le bouton Play (qui affiche l’icône Pause) ? Et bien il faut se mettre en pause (et afficher l’icône Play) !
elseif($global:status -eq 1) { $button_play.Image=[System.Drawing.Image]::FromFile("$path\play.png") $textbox7.Text="Pause" $form.Refresh() $global:timer.Stop() $MediaPlayer.Pause() $global:status=2 }
Je remets donc le symbole Play à mon bouton (pour repasser en lecture plus tard), j’indique que nous sommes en pause, j’arrête mon timer (il n’a pas besoin de mettre à jour la position dans le morceau vu qu’on ne bouge plus), j’appelle la méthode Pause() de mon objet $MediaPlayer et hop, je change le status de lecture (1) à en pause (2) !
Il ne reste plus qu’à gérer le dernier statut. Je suis en pause, je vais reprendre la lecture :
elseif($global:status -eq 2) { $button_play.Image=[System.Drawing.Image]::FromFile("$path\pause.png") $textbox7.Text="Lecture" $form.Refresh() $global:timer.Start() $MediaPlayer.Play() $global:status=1 }
On remet le symbole Pause, j’affiche Lecture, je relance mon timer et la lecture, et je rechange le statut de mon player. Et voilà, c’est tout. Ma fonction Play est terminée !
Changer de morceau
Vous le savez, il y a un bouton Next (suivant) et un bouton Prev (previous – précédent) qui appellent des fonctions qui portent les mêmes noms (comme c’est pratique). Evidemment, elles ne font pas la même chose, mais presque. Pour changer de morceau, je n’ai qu’à modifier l’index $global:i de ma playlist, en faisant attention toutefois :
- Dans la fonction Prev, si j’atteins la limite basse (index 0), je reste à 0. Dans le cas contraire, je décrémente mon index.
- Dans la fonction Next, si j’atteins la limite haute (le dernier morceau), je reste au dernier index. Dans le cas contraire, j’incrémente mon index.
Ce qui nous donne :
function prev { if($global:i -gt 0) { $global:i-- } else { $global:i=$global:nbfiles } $global:status=0 # on simule que le player est en attente play } function next { if($global:i -lt $global:nbfiles) { $global:i++ } else { $global:i=0 } $global:status=0 # on simule que le player est en attente play }
Dans chaque fonction, je fais comme si le player était en attente ($global:status est mis à 0) avant d’appeler ma fonction play : même si un morceau est déjà en cours de lecture, pas de souci, on peut rappeler la méthode Play() de $MediaPlayer pour un autre morceau : il arrêtera tout seul la lecture du premier avant de lancer la seconde, pas de mélange dissonant comme ça. Il s’agit juste que ma fonction play exécute la bonne partie de son code.
Je vous laisse digérer tout ça (et vu l’heure, je vais aller me faire à manger !). Dans le prochain article (que j’espère être le dernier parce que j’en ai un peu marre et je sens que vous aussi !), nous verrons en détail cette histoire de timer et la fonction updatepos. D’ici là, portez-vous bien et tchüss !