Benutzer-Werkzeuge

Webseiten-Werkzeuge


start:themen:napcap_audio_batch_player

NapCap Audio Batch Player

Dies ist nach DirectShow in .Net verwenden das Tutorial des zweiten großen Bausteins der NapCap Anwendung.

Hier wird nicht der gesamte Quellcode der Anwendung „abgedruckt“, sondern es wird nur die Funktion der wichtigsten Elemente beschrieben. Der komplette Quellcode, VisualStudio 2005 Projektdateien und natürlich auch das lauffähige Programm sind bei NapCap Quellen verfügbar.

Dieses Tutorial behandelt wirklich nur die Player-Komponente, das Programm nimmt also noch nicht auf. Das Kapitel Aufnahme! bringt Player und Soundrecorder zusammen und führt damit zur ersten vollständig funktionierenden NapCap- Version.

Windows Mediaplayer eingepackt

In Mediaplayer wurde bereits gezeigt, wie man den Windows Mediaplayer quasi ohne eigene Programmierung in eigenen C# Player- Programmen verwenden kann. Dort wurde freilich nur der Original- Mediaplayer in ein Anwendungsfenster gepackt.

Spannender und für NapCap wichtiger ist aber nicht die Visualisierung, sondern Steuerung des Mediaplayers, etwa um ganze Verzeichnisbäume nach WMA- Dateien zu durchsuchen und sie automatisch in „überall brauchbare“ Formate wie MP3 umzuwandeln. Aber auch das geht mittels „COM-Automation“ recht einfach und dieser Artikel zeigt wie.

Voraussetzung ist eine .Net Assembly, welche die benötigten Schnittstellen des MediaPlayers bereitstellt. Wie wir bereits in DirectShow in .Net verwenden gesehen haben, braucht man dazu eine Typenbibliothek, die glücklicherweise bereits im Mediaplayer-Control WMP.DLL enthalten ist- wir müssen also nicht erst selbst eine IDL-Datei schreiben, wie es für DirectShow nötig war. Es reicht das simple Kommando:

Tlbimp c:\windows\system32\wmp.dll

Dadurch entsteht eine Datei WMPLib.dll die wir nur noch als Referenz in unser Projekt aufnehmen müssen. Ganz Faule kommen zu dieser DLL (aber unter dem Namen Interop.WmpLib.dll) auch mit der in Mediaplayer beschriebenen Methode durch Integration des Mediaplayers in die VisualStudio Toolbar.

Vom Umfang her ist die Windows Media Player 10 SDK gigantisch, was man bei der noch relativ handlichen Assembly nicht vermutet. Sich in dieser Masse von COM-Objekten und Interfaces zurecht zu finden, ist nicht ganz einfach, weshalb wir eine einfache Klasse schreiben, welche nur die Schnittstellen für unsere Zwecke vereinfacht adaptiert und bereitstellt. Solche „Decorator“, „Adapter“ oder „Wrapper“ genannten Objekte erfreuen sich gerade bei so komplexen und nicht immer ganz einfach - um nicht zu sagen umständlich - handhabbaren SDKs großer Beliebtheit.

Wir brauchen also nur eine Klasse WMPlayCore, welche uns folgende Funktionen bzw Eigenschaften bietet:

Methode/Property Beschreibung
void Play(dateiName) spielt WMA-Datei „dateiName“ ab
void Stop() beendet das Abspielen
void Start() startet das Abspielen wieder
PlayerStatus Status antwortet den aktuellen Player-Zustand
bool IsPlaying ist true, wenn gerade was abspielt
string LastStatus ist eine Zustandsbeschreibung für z.B. eine Statuszeile
MetaData Tags liefert die Metadaten des aktuellen Titels (für ID3 Tags)
double Position antwortet die aktuelle Abspielposition
int Volume erlaubt ein Abfragen und Einstellen der Abspiel-Lautstärke

In dieser Tabelle befinden sich zwei benutzerdefinierte Typen.

PlayerStatus ist eine in folgender Tabelle beschriebene Zustands-Enumeration:

Zustandsname Beschreibung des Zustands
NotReady Player bzw. Windows-Mediaplayer nicht bereit
Ready Player bereit zum Abspielen
Playing Player spielt gerade ein Stück ab
Ended Das abgespielte Stück ist zuende
Stopped Der Player ist jetzt angehalten
Busy Der Player meldet „Bin (beim) Laden“

Der Mediaplayer selbst kennt noch sehr viel mehr Zustände, die aber in unserem Programm nicht auftreten, weil wir die zugehörigen Funktionen wie „Pause“ oder „Seek“ nicht nutzen oder nur aus Datei lesen und nicht über Internet-Verbindungen. So zeigt uns unser „Adapter“ ein vereinfachtes Bild dieses „Monstrums“.

MetaData ist eine Struktur mit den wichtigsten Informationen über das abgespielte Stück. Diese können später als ID3-Tag in die konvertierte Datei eingetragen werden:

Feldtyp Feldname Bedeutung
string artist Name des Interpreten
string album Name des CD-Albums
string title Titel des Musikstücks
string genre Musik- Genre (z.B. Rock)
string filePath Pfadname der Datei
int track Tracknummer auf der CD
int fileSize Dateigröße in Bytes
double duration Abspieldauer in Minuten.Sekunden

Auch hier speichert der Mediaplayer wieder sehr viel mehr, als wir für ID3-Tags brauchen können, es gibt dort nicht weniger als 76 Datenfelder !

Die Zustandswechsel und evtl. auch Fehler während des Abspielens werden von der Klasse WMPlayCore asynchron gemeldet, es werden also die folgenden beiden Events getriggert:

Eventname Beschreibung
StateNotification EventHandler für Player-Zustandsänderungen
ErrorNotification EventHandler für Medienfehler

Es handelt sich um einfache EventHandler Objekte (keine speziellen Parameter), da die benötigten Informationen ja bequem über die Properties abgefragt werden können.

Eine Anwendung, die WMPlayCore nutzt, muß also nur wie folgt initialisiert werden, um über Abspielstatus und Medienfehler per Event informiert zu werden:

  // erzeuge Player und registriere für Abspielstatus- und Medienfehler-Meldungen
  m_player = new WMPlayCore();
  m_player.StateNotification+= new EventHandler(OnPlayerNotification);
  m_player.ErrorNotification += new EventHandler(OnMediaError);

Die so registrierten Eventhandler- Methoden sind dann wie etwa bei GUI-Events üblich aufgebaut:

  private void OnMediaError(object sender, EventArgs e)
  {
    // zeige Fehler in Statusanzeige und Log an
    wmpStatus.Text = m_player.LastStatus;
    Debug.Print(m_player.LastStatus);
    // setze Fortschrittsanzeige zurück
    timer.Enabled = false;
    progressBar.Value = 0;
  }

WMPlayCore Quelltext

Nach diesen Codeschnippseln folgt der komplette Quellcode der Klasse WMPlayCore hier. Detaillierte Informationen kann man aus den Kommentaren ersehen:

using System; // für EventHandler
using WMPLib; // für Windows MediaPlayer Control
 
namespace wmabatch
{
  /// <summary>
  /// Diese Zustände des WMPlayCore - Players werden
  /// per Event an Aufrufer geschickt
  /// </summary>
  enum PlayerStatus
  {
    NotReady, // Player bzw. Windows-Mediaplayer nicht bereit
    Ready,    // Player bereit zum Abspielen
    Playing,  // Player spielt gerade ein Stück ab
    Ended,    // Das abgespielte Stück ist zuende
    Stopped,  // Der Player ist jetzt angehalten
    Busy      // Der Player meldet "Bin (am) Laden"
  }
 
  /// <summary>
  /// Die Klasse WMPlayCore ist ein "Decorator" (Wrapper) um den Windows MediaPlayer.
  /// Sie bietet nur die zum "Batch-Abspielen" benötigte Funktionalität
  /// wie Play und Stop, meldet aber Fehler und Status per Event an den Aufrufer,
  /// welche so das Abspielen "automatisieren" kann.
  /// </summary>
  class WMPlayCore
  {
    /// <summary>
    /// Alle für ID3-Tagging benötigten Metadaten des abgespielten Stücks
    /// </summary>
    public struct MetaData
    {
      public string artist;
      public string album;
      public string title;
      public string genre;
      public string filePath;
      public int track;
      public int fileSize;
      public double duration;
    }
 
    #region members
 
    private WindowsMediaPlayer m_player = null; // das Mediaplayer- Control
    private string m_lastStatus; // Zustandsmeldung im "Klartext"
    private PlayerStatus m_status = PlayerStatus.NotReady; // aktueller Zustand
    private MetaData m_md; // Metadaten des aktuellen Stücks
 
    #endregion
 
    #region events triggered
 
    public event EventHandler StateNotification; // event für Zustandsmeldung
    public event EventHandler ErrorNotification; // event für Fehlermeldung
 
    #endregion
 
    #region properties
 
    /// <summary>
    /// Zustand abfragen
    /// </summary>
    public PlayerStatus Status
    {
      get
      {
        return m_status;
      }
    }
 
    /// <summary>
    /// spielt Mediaplayer gerade ab?
    /// </summary>
    public bool IsPlaying
    {
      get
      {
        return m_player.playState == WMPPlayState.wmppsPlaying;
      }
    }
 
    /// <summary>
    /// Zustandsmeldung abfragen
    /// </summary>
    public string LastStatus
    {
      get
      {
        return m_lastStatus;
      }
    }
 
    /// <summary>
    /// Metadaten Abfrage
    /// </summary>
    public MetaData Tags
    {
      get
      {
        return m_md;
      }
    }
 
    /// <summary>
    /// Abspielposition abfragen (in Minuten)
    /// </summary>
    public double Position
    {
      get
      {
        if (m_player.controls != null)
        {
          return m_player.controls.currentPosition;
        }
        else
        {
          return 0.0;
        }
      }
    }
 
    /// <summary>
    /// abfragen oder verändern der Abspiel-Lautstärke (0..100)
    /// </summary>
    public int Volume
    {
      get
      {
        int result = 0;
        if ((m_player != null) && (m_player.settings != null))
          result = m_player.settings.volume;
 
        return result;
      }
      set
      {
        if ((m_player != null) && (m_player.settings != null))
        {
          m_player.settings.volume = value;
        }
      }
    }
 
    #endregion
 
    #region constructor
 
    public WMPlayCore()
    {
      m_status = PlayerStatus.NotReady;
 
      try
      {
        // lader Mediaplayer-Control und registriere für Abspielstatus- und Medienfehler-Callbacks
        m_player = new WindowsMediaPlayer();
        m_player.PlayStateChange += new _WMPOCXEvents_PlayStateChangeEventHandler(Player_PlayStateChange);
        m_player.MediaError += new _WMPOCXEvents_MediaErrorEventHandler(Player_MediaError);
        m_player.settings.autoStart = false;
      }
      catch (Exception e)
      {
        m_lastStatus = e.Message;
        NotifyError();
      }
    }
 
    #endregion
 
    #region public methods
 
    /// <summary>
    /// spiele Stück mit Mediaplayer ab
    /// </summary>
    /// <param name="file">Datei- Pfadname des Stücks</param>
    public void Play(string file)
    {
      m_player.URL = file;
      Start();
    }
 
    /// <summary>
    /// beende das Abspielen sofort
    /// </summary>
    public void Stop()
    {
      try
      {
        m_player.controls.stop();
      }
      catch (Exception e)
      {
        m_status = PlayerStatus.NotReady;
        m_lastStatus = e.Message;
        NotifyError();
      }
    }
 
    /// <summary>
    /// beginne Abspielen des Stücks
    /// </summary>
    public void Start()
    {
      try
      {
        m_player.controls.play();
      }
      catch (Exception e)
      {
        m_status = PlayerStatus.NotReady;
        m_lastStatus = e.Message;
        NotifyError();
      }
    }
 
    #endregion
 
    #region player notifications
 
    /// <summary>
    /// melde Abspielstatus an registrierte Aufrufer
    /// </summary>
    private void NotifyState()
    {
      if (StateNotification != null)
      {
        StateNotification(this, new EventArgs());
      }
    }
 
    /// <summary>
    /// melde Medienfehler an registrierte Aufrufer
    /// </summary>
    private void NotifyError()
    {
      if (ErrorNotification != null)
      {
        ErrorNotification(this, new EventArgs());
      }
    }
 
    /// <summary>
    /// delegiere Callback vom Mediaplayer an Aufrufer
    /// </summary>
    /// <param name="NewState">Mediaplayer Abspielsstatus</param>
    private void Player_PlayStateChange(int NewState)
    {
      if( ( WMPLib.WMPPlayState)NewState == WMPLib.WMPPlayState.wmppsReady )
      {
        // Mediaplayer ist bereit, Stücke abzuspielen
        m_lastStatus= "Player bereit";
        m_status = PlayerStatus.Ready;
        NotifyState();
      }
      else if ((WMPLib.WMPPlayState)NewState == WMPLib.WMPPlayState.wmppsStopped)
      {
        // Mediaplayer ist jetzt angehalten
        m_lastStatus = "Player gestoppt";
        m_status = PlayerStatus.Stopped;
        NotifyState();
      }
      else if ((WMPLib.WMPPlayState)NewState == WMPLib.WMPPlayState.wmppsMediaEnded)
      {
        // das abgespielte Stück ist zuende
        m_lastStatus = String.Format("Titel {0} fertig abgespielt", m_md.title);
        m_status = PlayerStatus.Ended;
        NotifyState();
      }
      else if ((WMPLib.WMPPlayState)NewState == WMPLib.WMPPlayState.wmppsTransitioning)
      {
        // Mediaplayer ist beschäftigt
        m_lastStatus = "Bereite abspielen vor";
        m_status = PlayerStatus.Busy;
        NotifyState();
      }
      else if ((WMPLib.WMPPlayState)NewState == WMPLib.WMPPlayState.wmppsPlaying)
      {
        // Mediaplayer fängt an, ein Stück abzuspielen.
        // Wir können uns nun dessen Mediendaten holen
        IWMPMedia media = m_player.currentMedia;
        m_md.album = media.getItemInfo("AlbumID");
        m_md.artist = media.getItemInfo("Author");
        m_md.duration = media.duration;
        m_md.filePath = media.sourceURL;
        m_md.fileSize = Int32.Parse(media.getItemInfo("FileSize"));
        m_md.genre = media.getItemInfo("WM/Genre");
        m_md.title = media.getItemInfo("Title");
        m_md.track = Int32.Parse(media.getItemInfo("WM/Tracknumber"));
 
        // melde Statusmeldung und Status an Aufrufer
        m_lastStatus = String.Format("Titel {0} wird abgespielt", m_md.title); ;
        m_status = PlayerStatus.Playing;
        NotifyState();
      }
    }
 
    /// <summary>
    /// delegiere Medienfehler-Meldung an Aufrufer
    /// </summary>
    /// <param name="pMediaObject"></param>
    private void Player_MediaError(object pMediaObject)
    {
      // lies Fehlerstatus von Mediaplayer
      WMPLib.IWMPMedia2 errSource = pMediaObject as WMPLib.IWMPMedia2;
      WMPLib.IWMPErrorItem errorItem = errSource.Error;
      string errorDesc = errorItem.errorDescription;
 
      // melde Fehler an Aufrufer
      m_lastStatus = String.Format(errorDesc, errSource.name);
      NotifyError();
    }
 
    #endregion
  }
}

Bedienoberfläche und Playersteuerung

Mit der Klasse WMPlayCore ist es nun recht einfach, den NapCap-Player zu bauen, der die Aufgabe hat, eine Liste aus WMA-Dateien aus einem Verzeichnisbaum zu sammeln, um diese dann nacheinander abzuspielen, wobei die Reihenfolge im Gegensatz zu einer wohldurchdachten „Playlist“ sekundär ist, denn wir wollen die Musik ja erst mal konvertieren, um sie später als MP3 auf einem Gerät unserer Wahl anhören zu können.

So eine Liste läßt sich bequem in einem ListView- Control speichern, daß die Bedienoberfläche dann auch noch anzeigen kann und uns darüber hinaus die Möglichkeit gibt, Dateien zum Abspielen direkt anzuklicken. Es gilt also, einen Verzeichnisbaum zu iterieren und alle Dateinamen mit WMA-Extension in die Listview einzutragen. Das kann diese einfache rekursive Methode erledigen:

    /// <summary>
    /// iteriert die Verzeichnisse, sucht die Eingabedateien und
    /// listet sie in einer ListView auf
    /// </summary>
    /// <param name="path">Wurzelverzeichnis der Musikdateien</param>
    /// <param name="recurseLevel">Rekursionstiefe, mit 0 aufrufen</param>
    private void CreateBatchList(string path, int recurseLevel)
    {
      // initialisiere für Wurzelverzeichnis
      if (recurseLevel == 0)
      {
        lvBatchList.Items.Clear();
      }
 
      // erst suche Mediendateien in diesem Verzeichnis
      foreach (string file in Directory.GetFiles(path, SourceFileType))
      {
        FileInfo fi = new FileInfo(file);
        if (fi.Exists)
        {
          // trage alle gefundenen Dateien in die ListView ein
          ListViewItem it= new ListViewItem(fi.Name);
          it.SubItems.Add(fi.DirectoryName);
          lvBatchList.Items.Add(it);
        }
      }
 
      // für alle Unterverzeichnisse rekursiver Aufruf
      ++recurseLevel;
      foreach (string folder in Directory.GetDirectories(path))
      {
        CreateBatchList(folder, recurseLevel);
      }
    }

Abspielen ohne Abstürze

Etwas kniffliger ist das fortlaufende Abspielen. Wenn uns der Player mit PlayerState.Ended meldet, daß er mit einer Datei fertig ist, soll ja das Abspielen der nächsten Datei angestoßen werden. Ein beliebter (Nicht nur-)„Anfängerfehler“ ist nun, aus dem EventHandler wieder direkt „Play“ aufzurufen. Aus dem Kontext des Eventhandlers heraus darf man das aber nicht, sonst wirft der Mediaplayer die Exception: 0xC00d1054, die bedeutet, daß der Abspielgraph, mit welcher die alte Datei abgespielt wurde noch nicht entfernt wurde und ein Abspielgraph für die neue Datei noch nicht existiert. Wer (immer noch?) nicht weiß, was ein „Abspielgraph“ ist, sollte erst einmal die Grundlagen in DirectShow lesen.

Jedenfalls muß man den Play-Befehl in einer neuen Aufrufkette auslösen. Als erfahrener Windows-Programmierer würde man sich nun selbst per PostMessage() eine Windows- Message schicken, in NapCap tuts aber auch ein Timer, den wir ohnehin brauchen, um die Fortschrittsanzeige zu realisieren.

Diese Methode bestimmt den Pfadnamen für die als nächstes abzuspielende Datei aus der aktuellen ListView-Zeile und setzt ein Flag, welches die Timerfunktion verwendet. Hier wird auch gleich der Timer eingeschaltet, der in NapCap eine Laufzeit von 1 Sekunde hat:

    /// <summary>
    /// setze Pfadname für nächste abzuspielende Datei und triggere
    /// Timer, damit dieser das Abspielen startet
    /// </summary>
    /// <param name="it">Listview-Zeile mit abzuspielender Datei</param>
    private void TriggerFile(ListViewItem it)
    {
      m_currentFile = String.Format(@"{0}\{1}", it.SubItems[1].Text, it.Text);
      m_playNext = true;
      timer.Enabled = true;
    }

Die Timerfunktion startet nun beim ersten Aufruf den Player, während des Abspielens aktualisiert sie dann die Fortschrittsanzeige:

    private void timer1_Tick(object sender, EventArgs e)
    {
      if (m_playNext)
      {
        // starte player
        m_player.Play(m_currentFile);
        m_playNext = false;
      }
      else if (m_player.IsPlaying)
      {
        // zeige Fortschritt / Restanzeige
        progressBar.Value = (Int32)(1000.0 * m_player.Position / m_duration);
        int rem = (Int32)(m_duration - m_player.Position);
        remaining.Text = String.Format("Rest: {0:00}:{1:00}", rem / 60, rem % 60);
      }
    }

Zum fortlaufenden Abspielen spielen wir mit der ListView. Die folgende Methode schiebt die selektierte Zeile durch die Liste, stellt den abgespielten Titel in der obersten Listenzeile dar und nutzt das oben beschriebene „TriggerFile“ um über den Timer das Abspielen auszulösen:

    private void NextFile()
    {
      m_currentFile = "";
 
      // keine Dateien vorhanden/gefunden
      if (lvBatchList.Items.Count == 0) return;
 
      // wenn schon eine Datei abgespielt, nimm die nächste
      if (lvBatchList.Items[m_fileIndex].Selected)
      {
        ++m_fileIndex;
        // Abbruch am Ende der Liste
        if (m_fileIndex == lvBatchList.Items.Count)
        {
          return;
        }
      }
 
      // wähle neue Datei an und zeige sie am Listenkopf an
      ListViewItem it = lvBatchList.Items[m_fileIndex];
      it.Selected = true;
      lvBatchList.Select();
      lvBatchList.TopItem = it;
 
      // triggere Abspielen des Stücks
      TriggerFile(it);
    }

Automaten - Automatik

Die letzte Methode, auf die ich noch eingehen möchte, ist der PlayerStatus- Eventhandler. Über den wird nämlich der ganze fortlaufende Abspielvorgang gesteuert, d.h. dieser ruft das oben beschriebene „NextFile“ auf, wenn ein Stück fertig abgespielt ist. Auch übernimmt er vom Player die Metadaten des Stücks und zeigt sie an. Dies ist ein ganz primitiver „endlicher Automat“, bei dem Zustandsänderungen des Mediaplayers letztlich den Programmablauf steuern:

    private void OnPlayerNotification(object sender, EventArgs e)
    {
      // Statuszeilen-Anzeige und Zustandslogging
      wmpStatus.Text = m_player.LastStatus;
      Debug.Print(m_player.Status.ToString());
 
      switch (m_player.Status)
      {
        case PlayerStatus.Playing:
          // Abspiel-Beginn: aktualisiere Metadaten-Anzeige im Player
          WMPlayCore.MetaData tags = m_player.Tags;
          lblArtist.Text = "Interpret: " + tags.artist;
          lblAlbum.Text = "Album: " + tags.album;
          lblTitle.Text = "Titel: " + tags.title;
          lblGenre.Text = "Genre: " + tags.genre;
          lblTrackNr.Text = String.Format("Track: {0}", tags.track);
 
          // aktualisiere Fortschrittsbalken und Restanzeige
          progressBar.Value = 0;
          m_duration = tags.duration;
 
          // aktualisiere Lautstärkeregler
          trackVolume.Value = m_player.Volume;
 
          // schalte Start/Stop- Button auf Stop um
          btnStart.Text = "Stop";
          btnStart.Enabled = true;
          break;
 
        case PlayerStatus.Ended:
          // Abspiel-Ende: Mediadaten-Anzeigen zurücksetzen
          lblArtist.Text = "Interpret:";
          lblAlbum.Text = "Album:";
          lblTitle.Text = "Titel:";
          lblGenre.Text = "Genre:";
          lblTrackNr.Text = "Track:";
 
          // Restzeit-Anzeige und Fortschrittsbalken zurücksetzen
          timer.Enabled = false;
          progressBar.Value = 0;
          break;
 
        case PlayerStatus.Stopped:
          // TODO: stop capturing, convert to MP3 and tag
 
          // triggere Abspielen der nächsten Datei
          NextFile();
          btnStart.Text = "Start";
          break;
      }
    }

Der übrige Code ist zu trivial, um hier abgehandelt zu werden. Wer sich dafür interessiert (oder statt dem Quellcode ein funktionierendes Programm haben will), kann das ganze Projekt bei NapCap Quellen herunterladen.

start/themen/napcap_audio_batch_player.txt · Zuletzt geändert: 2018/09/21 16:45 (Externe Bearbeitung)