WinFuture-Forum.de: [Sample Implementation] Einfacher AudioPlayer in PowerShell - WinFuture-Forum.de

Zum Inhalt wechseln

Nachrichten zum Thema: Entwicklung
Seite 1 von 1

[Sample Implementation] Einfacher AudioPlayer in PowerShell Beispielimplementierung mit Windows-Bordmitteln

#1 Mitglied ist offline   RalphS 

  • Gruppe: VIP Mitglieder
  • Beiträge: 7.288
  • Beigetreten: 20. Juli 07
  • Reputation: 796

geschrieben 21. März 2017 - 02:12

Note: Implementierung in PS v5, Plattform: Windows 10. Sollte auch in früheren Windowsversionen funktionieren, wenn eine Codezeile wie dokumentiert geändert wird.
PS v2 (Standard in Win7, wenn nicht auf spätere Version aktualisiert) erfordert ggf. leicht angepaßte Syntax (insbesondere: PS2 unterstützt keine Listen-, sondern nur Elementverarbeitung).

Ziel ist ein flinker Einblick in die Möglichkeiten von PowerShell, die Absetzung von Batch (objektbasiert statt textbasiert) sowie die Möglichkeiten von PowerShell.

Er hat's zweimal gesagt.

Ja, weil es wichtig war. :wink:

Code ist inline kommentiert.
Zusätzlich sei erwähnt, daß strenge Typisierung eine Macke von RalphS ist. PS läßt schwache Typisierung genauso zu; Typisierung ist streng genommen nicht mal erforderlich.
Der TO ist aber der Ansicht, daß mit starker Typisierung Probleme wesentlich schneller auffallen und während der Code dadurch merklich aufgeblasen wird, wird man so nicht erfolgreich Mist bauen. Insbesondere ist der Plus (+) Operator überladen und array[] + array[] tut was anderes als [int] + [int] oder auch nur [string] + [string]; schwach oder gar nicht typisiert verliert man nicht nur den Überblick, es kann auch schon mal passieren, daß einfach nur was Falsches berechnet wird.

Aber das nur nebenher. Typenbezeichnungen (hauptsächlich das, was in [] angegeben ist) zumindest theoretisch (aber mit den genannten Caveats) weggelassen werden.


Hinweis in mehr oder weniger eigener Sache: Weitere Beispielimplementierungen zur Veranschaulichung von sonstwovon sind herzlich willkommen. Muß nicht mal PowerShell sein, sollte aber im Geiste zumindest eine Scriptsprache darstellen. Will heißen: es sollte möglich sein, den Code herzunehmen und auszuführen, ohne weiter was damit machen zu müssen. PHP/Perl & Co gingen durch. C# & Co eher nicht.




Weil es so beliebt ist, außer einem Shutdown-Script einen Mediaplayer in einer Programmiersprache zu implementieren, um diese kennenzulernen...

... soll an dieser Stelle PowerShell als eine vielleicht-zu-mächtige Scriptsprache, vielleicht-zu-schwache Programmiersprache als Plattform herhalten.

Das dargestellte Beispiel versucht insbesondere, auf einzelne PowerShell-Features einzugehen. Der Audioplayer ist so out-ouf-the-box funktionsfähig (note: wegen zusätzlich eingefügter Kommentarzeilen gibt es möglichrweise auch nachträglich eingefügte Syntaxfehler) wenn man ganz einfach den untenstehenden Code copypastet und so in ein PS-Kommandofenster einfügt. Get-Help Play-Item -Full und Get-Command Play-Item -Syntax verraten dann mehr über die Ausführungsoptionen (und obendrein über die Möglichkeiten von simplen Scripts, die nicht mal kompiliert werden müssen, um zu funktionieren).

<#
.SYNOPSIS
 Simpler Mediaplayer in PowerShell

.Description
 Ausführlicher Beschreibungstext für die Funktion.
 Hinweis: Der beinhaltende Kommantarblock darf maximal eine (1) Leerzeile von der 
Funktionsdefinition entfernt sein, damit PowerShell diesen als zur Funktion gehörigen 
Hilfetext anzeigt.

Play-Item spielt Audiodateien und Ordner mit Audiodateien ab. Es unterstützt jedoch keine 
Playlists und auch keine weiteren Kontrollfunktionen; es ist lediglich eine 
Beispielimplementierung ohne besondere Features. 

Der Aufruf von Play-Item mit dem Parameter -filePath X versucht, die Datei X als Audiodatei 
zu öffnen und abzuspielen. Mit dem Parameter -Wait wird synchron, ohne ihn asynchron 
abgespielt; entsprechend wird mit -Wait solange kein Prompt angezeigt, wie die Audiodatei 
noch spielt. 

Mit dem Parameter -Wait wird der Einfachheit halber Ordnerinhalte ausschließlich synchron abgespielt. Asynchrones 
Abspielen wäre per Runspaces (PowerShell Threading) möglich, würde jedoch den Rahmen 
sprengen.

.Example
 Play-Item -folderPath 'C:\Audio\Absurd Minds'
 spielt alle Audiodateien im benannten Ordner.

.Example
 Play-Item -filePath 'C:\Audio\musik.mp3'
 spielt die benannte Datei im Hintergrund ab.
#>

 function Play-Item
 {
 [cmdletbinding(DefaultParameterSetName='byFile')] # Welche Parameterkonfiguration standardmäßig verwendet werden soll
 [outputType([void])] # Rückgabetyp ist leer. Diese Angabe hat lediglich informativen 
Charakter - wird zB für IntelliSense in der ISE verwendet - sollte aber trotzdem mit dem 
tatsächlichen Ausgabetypen übereinstimmen.

 Param( # PowerShell definiert Ein- und Ausgabeparameter nicht in der Funktions-Signatur, 
sondern inline im Funktionskörper.

# Parameter #1 erhält den Namen -filePath. Wird er angegeben, wird Parametersatz "byFile" 
ausgewählt, für welchen er verpflichtend ist. Außerdem kann der blanke Dateiname an erster 
Stelle nach dem Funktionsnamen kommen (Play-Item Dateiname.mp3).
 [Parameter(Mandatory=$true,Position=1, ParameterSetName='byFile')]
 [ValidateNotNullOrEmpty()] # Der übergebene Dateiname darf nicht leer sein
[ValidateScript({($_ | Get-Item).Exists})] # ... und muß existieren
    [System.IO.FileInfo]$filePath, # Hier wird der Parameter schließlich bezeichnet und typisiert (es ist eine Datei). Note: Streng genommen kann auch ein Ordner als Dateiobjekt übergeben werden. Das kann man im ValidateScript-Attribut über ( $_.Attributes -band [System.IO.FileAttributes]::Directory ) -eq 0 gegenprüfen, wenn man das möchte.


# Analog für den Ordnerpfad mit entsprechenden Eigenschaften und Typdefinition für Ordner.
# Note: Einzelne Parameterdefinitionen sind mit ',' voneinander getrennt; auf die letzte Parameterdefinition darf aber kein ',' folgen (Syntaxfehler).

[parameter(Mandatory=$true,Position=1,ParameterSetName='byFolder'
)]
[ValidateNotNullOrEmpty()]
[ValidateScript({($_ | Get-Item).Exists})]
    [System.IO.DirectoryInfo] $FolderPath,
    
    # Der optionale WAIT-Parameter trifft nur fürs Dateiabspielen zu; fürs Ordnerabspielen muß in jedem Fall gewartet werden, bis die einzelne Datei fertig ist (sonst spielen alle gleichzeitig)

[Parameter(Mandatory=$false,ParameterSetName='byFile')]

    [switch] $Wait, # Switch-Parameter sind simple Flags, die als BOOLEsche Werte gesetzt werden. Parameter da = Variable = $True. Sonst $False, soweit nicht -Parameter:$False angegeben wurde (aber Achtung, das kann je nach Implementierung seltsame Folgeerscheinungen haben.) 

[switch] $PassThru # Komplett ungeschmückter Parameter. Ein Typ ist aber erforderlich, insbesondere für SwitchParameter. Ansonsten kann auch dieser weggelassen werden; die zugehörige Variable ist dann ein [object].
       )

       switch($PSCmdlet.ParameterSetName) # Welche Parameterkonfiguration verwenden wir gerade?
       {
       'byFile' { # Der Benutzer hat gar nichts oder -filePath gesagt
# In diesem Fall müssen wir sehen, daß wir die übergebene Objektreferenz an ein Dateisystemobjekt geknippert kriegen. 
# Note: Existiert die referenzierte Datei nicht, würde es hier eine Ausnahme geben. Das kann aber wegen den Validierungsoptionen im PARAM()-Block nicht passieren.

    $filePath = $filepath|get-item; # Note the PIPE. Pipelining ist ein definierendes Feature der PowerShell: da vollständig mit Objekten gearbeitet wird, ist es ein Leichtes, Ausgabeobjekte einer Funktion unbesehen in eine andere Funktion reinzustopfen.

# Damit das funktioniert, wie es soll, benötigen wir die Windows Presentation Foundation.
# Das kann oder kann nicht bereits vorhanden sein. Mit Add-Type... stellen wir sicher, daß es da ist.
    Add-Type -AssemblyName presentationcore;

# Als nächstes benötigen wir eine Referenz auf den MediaPlayer.
# Verwendete Syntax ist PS5-Syntax. Frühere Versionen erfordern hier
# [System.Windows.Media.MediaPlayer] $player = New-Object System.Windows.Media.MediaPlayer, damit das funktioniert.

    [System.Windows.Media.MediaPlayer] $player = [System.Windows.Media.MediaPlayer]::new();
    # WPF ist eher netzwerkorientiert. Entsprechend sind erwartete Übergabe-Parameter URIs und keine Dateisystemobjekte. Also gibt es einen flinken expliziten Cast des vollständigen Dateinamens in einen URI. Im Sinne einer Zeichenfolge wird der Dateiname also einfach als URL dargestellt mit file:// davor.

    [uri]    $playFile = $filePath.FullName -as [uri];

# Öffne bezeichnete Datei als Datenstrom.
    $player.Open($playFile);
    while($player.IsBuffering) # Es bleibt jedem überlassen, wie - oder ob - mit der Pufferzeit umgegangen werden soll. Diese Implementierung wartet, bis fertig gepuffert ist. Das ist aber nicht notwendig.
    {
        Start-Sleep -Seconds 1;
    }
# Und los damit
    $player.Play();
    
    if($Wait) # Wenn -Wait angegeben war, müssen wir warten, bis das fertig ist. Sonst nicht. 
    {
# Dazu hat unser Player eine Eigenschaft NaturalDuration, welche uns verrät, wie lange das dauern wird. Wir ermitteln hier die Millisekunden, um ausreichend genau an der Realität zu bleiben. Minuten und Sekunden wären ebenfalls möglich, aber dann müßte jeweils aufgerundet werden und es würde entsprechend ungenauer gewartet.
 [int] $ItemDuration = $player.NaturalDuration.TimeSpan.TotalMilliseconds;

       # $player.Play() in sich ist asynchron. Wir müssen also explizit selber warten.
       # Dazu gibt es Start-Sleep mit einem zum obigen Ergebnis passenden Parameter (hier: -Milliseconds). Übergibt man den ermittelten Wert aus der Abspielzeit oben an Start-Sleep...

       Start-Sleep -Millisecionds $itemDuration;
      # ... dann wird die Scriptverarbeitung für die Dauer des abgespielten Titels unterbrochen.

      # Protip: Man kann an dieser Stelle eine kleine Helferfunktion Wait-Progress implementieren, die einen Fortschrittsbalken an die verstrichene Zeit koppelt und Start-Sleep dort kapselt. Dieser Fortschrittsbalken ist aber nicht interaktiv, sondern wird nur dargestellt und ist auch nur bei -Wait einfach implementierbar (ohne -Wait müßte ein zusätzlicher Ausführungsthread diese Arbeit übernehmen, damit das Script terminieren, aber den Fortschrittsbalken trotzdem synchron zur Wiedergabe anzeigen kann).

       # Wait-Progress -Milliseconds $ItemDuration -Message ('Playing: {0}' -f $filePath);
    }
    } # case: byFile -- Die spezifische Verarbeitung für Dateien ist hiermit abgeschlossen.
    'byFolder' # Wenden wir uns also dem -FolderPath -Parameter zu. Weil das Parameterkonfigurationen sind, kann -FilePath niemals zusammen mit -FolderPath verwendet werden und wir kommen nicht in die Situation, daß sich das semantisch wie logisch beißt.
     {
        # Die Situation für -FolderPath ist denn auch weniger eine Implementierung als ein logisches  Konstrukt: Ordner können nicht wiedergegeben werden, nur die Dateien darin.

# Ergo beschaffen wir uns zunächst einmal den Ordnerinhalt. 

        [System.IO.FileInfo[]] $folderItems = $FolderPath | Get-ChildItem;

# Dann filtern wir das beispielhaft auf MP3-Dateien...

        $folderItems |
        ? { $_.Extension -eq '.mp3'} |
        % { # ... und spielen die alle der Reihe nach ab. Note: Dieser Aufruf ist *rekursiv*, verwendet aber ein anderes ParameterSet als das gerade aktive, welches auch insbesondere selbst keine rekursiven Bezüge auf "unser" Parameterset beinhaltet. Das könnte in einer verschränkten Rekursion ohne Terminierungsbedingung (=> Endlosschleife) enden.
        # Play-Item -filePath ... -Wait spielt aber nur das eine Element ab und terminiert danach (das ist wichtig, weil ohne -Wait an dieser Stelle einfach für n Dateien auch n Playinerinstanzen geöffnet werden und kurzerhand n Audiodateien gleichzeitig spielen).
        # $_ bezieht sich dabei auf das aktuelle Item in der Pipeline: $folderItems ist eine Auflistung, die mit ? ... gefiltert wird und der Rest mit % ... enumeriert, sodaß $_ jetzt der Reihe nach alle MP3-Dateien aus dem bezeichneten Ordnr abgespielt werden.

# Der Vollständigkeit halber sollte erwähnt werden, daß die übergebenen MP3-Dateien so wie sie im Ordner eingetragen sind abgespielt werden. Üblicherweise  heißt das "alphabetisch aufsteigend nach Dateiname".
# Mit einem eingeschobenen Sort-Object -Property <Eigenschaft> kann eine Sortierfolge explizit festgelegt werden und mit -Descending umgekehrt werden.
# Sinngemäß also FolderItems | Sort -Property Length | ? {"is MP3?"} | % {for each MP3 do this or that}.        
# Was hier eigentlich passiert, ist die Übergabe  des logischen Programmflusses an die Implementierung: Play-Item -folderPath tut selbst nichts und kann selbst auch nichts tun; es beschreibt nur, daß alle MP3-Dateien im bezeichneten Ordner abgespielt werden sollen. 
# Das besorgt der nachstehende Befehl.
    
 Play-Item -filePath $_ -Wait;
        }
     }
    } # switch(Parametersets

# Da war doch noch was? Richtig... $PassThru.
# Die wird in PS per Dekret dafür verwendet, daß eine Funktion mit dem Rückgabetyp [void]
# doch was zurückgibt. Beispielhaft nehmen wir einfach die Playervariable.

if($PassThru -and ($Wait -eq $False) # $passthru ist ein Switch-Pameter, der schon durch die Definition im PARAM-Block Gültigkeit erlangt und zumindest $False ist, wenn nicht sogar $True. IST er $True, machen wir das hier:
{
 $player; # .... Jedenfalls dann, wenn Wait nicht auch noch gesetzt ist (ja, die Implementierung hierfür ist komplett daneben und unsystematisch; soll auch nur in Beispiel sein. Es soll einfach vermieden werden, daß für -FolderPath... pro Audiodatei ein $Player in die Pipeline wandert.)

# PS packt alles, was nicht näher bezeichnet ist, in die Pipeline. Hier muß man ein bissel aufpassen, insbesondre dann, wenn man selber mit Pipelines arbeitet: das vorliegende Beispielscript hat das Problem nicht, aber es gibt genügend Funktionen, die tatsächlich eine Rückgabe haben -- .Add()-Funktionen gehören da gern dazu. Man ruft also $liste.Add($element) auf und bekommt prompt eine 1 in die Pipeline, weil das die neue Listenlänge ist. Das muß man per Zuweisung zB auf $null (ja, das geht mit PS) oder mit ... | Out-Null wegwerfen (oder natürlich verwenden, wenn man es tatsächlich brauchen sollte).

# PS benötigt keine explizite Ausgabe. Jeder einzelne Befehl schreibt in die Pipeline, und bis auf wenige Ausnahmen ist jeder Befehl auch in einer Pipeline verwendbar, solange es nur eine Ausgabe gibt und es eine Funktion gibt, welche diese Ausgabe als Eingabe verwenden kann.

Durch das Statement "$player;" weiter oben wird via Angabe von -PassThru eine Referenz auf den verwendeten Player zurückgegeben. Dieser Rückgabewert kann zugewiesen oder sonst weiter verwendet oder sogar ignoriert werden (nicht sehr sinnvoll, wenn man -PassThru einfach weglassen könnte).
}

# Und damit sind wir fertig. Falls gewünscht kann man die verwendeten Variablen und insbesondere den $Player noch schließen  und entsorgen. Darauf sei hier frecherweise verzichtet. :)
 }



Hinweise, Ergänzungen oder ähnliches, einschließlich evtl drinsteckender Fehler, sind definitiv willkommen. Fragen nach dem Sinn des Lebens nicht ganz so, dafür gibt es die Monty-Python-Filme. :)
"If you give a man a fish he is hungry again in an hour. If you teach him to catch a fish you do him a good turn."-- Anne Isabella Thackeray Ritchie

Eingefügtes Bild
0

Anzeige

#2 Mitglied ist offline   RalphS 

  • Gruppe: VIP Mitglieder
  • Beiträge: 7.288
  • Beigetreten: 20. Juli 07
  • Reputation: 796

geschrieben 21. März 2017 - 20:48

Na, wo ich's schon angeteasert hab, plaudern wir noch ein bissel aus dem Nähkasten.

Wait-Progress macht eigentlich nicht besonders viel. Es ist ein Wrapper um Start-Sleep, welcher letztlich alle relevanten Parameter von Start-Sleep unterstützen könnte (hier nur -Milliseconds). Wait-Progress bekommt eine Nachricht übergeben und einen Zeitraum Milliseconds als long. Dann wird der Zeitraum auf Hunderstel (Prozent) standardisiert und die Anzahl Millisekunden gewartet und das Ganze mit dem Fortschrittsbalken visualisiert.

Wie man unschwer erkennen kann, existieren noch Parameter, die übergeben werden könnten (hier aber nicht werden). Einfach mit Write-Progress ein bissel herumspielen, welcher Parameter was tut.

Wichtig: Write-Progress in sich tut nichts. Das funktioniert nur in einer Schleife.

Der letzte Befehl ganz unten (Write-Progress -Completed) ist an dieser Stelle nicht zwangsläufig erforderlich: Mit -Completed wird der ausgewählte Fortschrittsbalken wieder ausgeblendet (das passiert bei Scriptende ohnehin automatisch). Write-Progress kann aber mehrere Fortschrittsbalken auf einmal anzeigen; dann ist es gut, wenn man in der Lage ist, auf den richtigen zuzugreifen und den richtigen ein- bzw auszublenden, wenn er erforderlich bzw nicht mehr erforderlich ist.

Protip: Wait-Progress, so wie implementiert, ist zeitbasiert. Entsprechend bietet es sich an, statt [long] oder vergleichbaren Skalaren einen [timespan]-Parameter zu übergeben und/oder den -Milliseconds noch gröber unterteilende Optionen mitzugeben (-Seconds, -Minutes etc). [TimeSpan]s können dann direkt übergeben werden. Bei Skalaren ist darauf zu achten, daß wenn man das über [timespan] implementiert, die einzelnen Komponenten 'realitätsnah' funktionieren; eine TimeSpan mit 70 Minuten funktioniert zB nicht so ohne Weiteres, da müßte man vorher in Stunden und Minuten zerlegen (der Rest ist im Beispiel ja 0).

function Wait-Progress
{
[cmdletbinding()]

Param
(
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]

    [string] $Message,

[Parameter(Mandatory=$true)]
 # -Milliseconds wurde kurzerhand Verpflichtend deklariert, um Standardwerte wie 0 zu vermeiden => das gibt sonst weiter unten eine Division durch 0
# Ergänzend sollte erwähnt werden: wenn man das ein bissel aufbohren will und mehr als nur -Milliseconds zulassen will (zB -Minutes oder -Seconds) dann kann -Milliseconds natürlich nicht mehr verpflichtend sein; simpler Workaround wäre dann, die Validierung auf -gt 0 zu lassen, die Verpflichtung wegzunehmen und den Standardwert auf 1 zu setzen. Ist eine Taustendstelsekunde ungenauer, gibt aber keine Division durch 0, wenn man das wegläßt.

[ValidateScript({ $_ -gt 0 })]
    [long]$MilliSeconds
)
[int] $startValue   = 0;
[int] $CurrentPosition = 0; 
[int] $endPosition = $MilliSeconds;
[int] $stepInterval = 500; # ms

while($CurrentPosition -le $endPosition) # Tu nur dann was, wenn wir im spezifizierten Intervall liegen
{
# -PercentComplete positioniert den Balken. Zulässige Werte sind 0..100. Die Klammern sind dafür da, daß der Prozentwert erst berechnet wird und DANACH zugewiesen wird. Ohne die Klammern wäre das Konstrukt schlicht ein Syntaxfehler.
# Außerdem anschauen, wie sich das mit -CurrentOperation und -Status verhält. Die Parameter sind hier fix, das kann man aber ganz problemlos als Parameter <Typ String> übergeben - sagen wir, den Basisnamen (.BaseName) der per Play-Item grad abgespielten Datei. -CurrentOperation könnte dann 'Now Playing' sein und -Status, wenn man das mag, als Anzeige vergangene Zeit / Zielzeit anzeigen lassen.
 
    Write-Progress -Activity $Message -Status 'Status' -PercentComplete ( $CurrentPosition * 100 / $endPosition ) -CurrentOperation 'CurrentOperation' -Id 0;

   Start-Sleep -Milliseconds $stepInterval; # Es wird immer halb-sekundenweise gewartet und dann neu geschaut, ob wir schon fertig sind => ob $CurrentPosition jetzt gleich oder größer der Zielposition ist.
   $CurrentPosition += $stepInterval;
  }
# Wenn wir fertig sind, blenden wir den Fortschrittsbalken explizit aus. Note: Nicht unbedingt erforderlich an dieser Stelle, hält aber ein bissel die Programmstruktur zusammen.

 Write-Progress -Activity $Message -Status 'Done(0)' -Completed -Id 0;
}


Dieser Beitrag wurde von RalphS bearbeitet: 21. März 2017 - 21:04

"If you give a man a fish he is hungry again in an hour. If you teach him to catch a fish you do him a good turn."-- Anne Isabella Thackeray Ritchie

Eingefügtes Bild
0

Thema verteilen:


Seite 1 von 1

1 Besucher lesen dieses Thema
Mitglieder: 0, Gäste: 1, unsichtbare Mitglieder: 0