Benutzer-Werkzeuge

Webseiten-Werkzeuge


themen:themen:directshow_in_.net_verwenden

DirectShow in .NET verwenden

Nachdem ich die Grundlagen von DirectShow schon mal angekratzt habe, hier nun wie versprochen die Beschreibung, wie man von C# zu DirectShow kommt.

Das ist leider nicht ganz einfach, wenn man sich mit Microsoft's COM Architektur - dem Vorläufer der .NET Komponentenarchitektur - nicht gut auskennt. Wer hier nicht alles versteht: nicht schlimm, im Rahmen des NapCap Projekts gibt es auch schon fertige .NET- Assemblies zum Runterladen.

Für .NET braucht man eine DirectShow- „Assembly“, welche die DirectShow-Funktionalität mittels .NET Klassen bzw. Schnittstellen (Interfaces) bereitstellt. DirectShow ist aber in einer „gewöhnlichen“ DLL namens „Quartz.dll“ zuhause und mittels ein paar dutzend COM-Objekten (auch ActiveX-Controls genannt) realisiert.

Seit Windows 2000 besteht wohl der größte Teil des Betriebssystems aus COM-Objekten und Microsoft wäre schön dumm und das ganze .NET wäre auch nicht brauchbarer als eine überall hin portable, aber nirgendwo wirklich hinpassende Sofwareumgebung wie Java, wenn man diese ganzen COM-Objekte nicht auch zu .Net- Objekten verpacken könnte. Genau das macht aber ein Tool namens „tlbimp.exe“, welches mit der .NET SDK geliefert wird.

Leider gibt es für DirectShow aber weder eine TLB-Datei noch ist in der quartz.dll wie in den meisten ActiveX- Objekten eine solche „Typenbibliothek“ drin, wie sie TLBIMP.EXE zur Verarbeitung voraussetzt. TLBs sind für ActiveX- Controls eigentlich Pflicht, denn Skriptsprachen wie Javascript aber auch VisualBasic können ohne TLB solche Komponenten-Bibliotheken gar nicht verwenden.

Schnittstellen-Beschreibung

Was ist denn aber eigentlich eine „Typenbibliothek“ alias TLB und wie erzeugt man sie?

Eine TLB ist ein mit dem Tool MIDL.EXE übersetzte Schnittstellenbeschreibung, die in der Sprache IDL (Interface Description Language) geschrieben wurde. IDL sieht fast so wie C++ aus, ist aber kein ausführbarer Programmcode sondern besteht nur aus Funktionssignaturen, Konstanten und Typendeklarationen, die eine Komponente anderen Komponenten bereitstellt.

IDL beschreibt nicht nur die Schnittstellen von COM- Komponenten, sondern auch anderen „Object Request Brokern“ (ORBs) wie CORBA. Durch XML, SOAP und der darauf basierenden WSDL (Web Services Description Language) ist IDL etwas aus der Mode gekommen, dient aber dem gleichen Zweck und ist (nicht nur) bei Windows noch weit verbreitet.

Lange Rede, kurzer Sinn: Im Include-Verzeichnis der frei herunter ladbaren Platform-SDK (siehe Visual Studio 2005) und vor allem in dessen Unterverzeichnis „DShowIDL“ befindet sich die komplette IDL-Beschreibung für DirectShow. Mit diesen IDLs und dem MIDL.EXE Tool kann man sich nun selbst die fehlende TLB erzeugen und mit TLBIMP.EXE die für .NET nötige Assembly erzeugen. MIDL.EXE auch bei der Platform-SDK dabei, die leider einen weiteren >400MB Download darstellt, auch wenn man sie sonst als C# -Programmierer nicht unbedingt braucht.

Einige der Filter, die ich bei NapCap verwende, erfordern zur Übersetzung leider auch noch die DirectX 9- oder DirectX 10-SDK, die noch weitere ~300 MB Download bedeutet. Mit den NapCap Quellen liefere ich aber alle .NET Assemblies und Filter bereits übersetzt mit, sodaß man weder Platform-SDK noch DirectX- SDK unbedingt braucht.

Für eine Typenbibliothek braucht man natürlich erstmal eine Bibliotheks- Definition. Die muß man für DirectShow erstmal selbst erledigen- in einem eigenem IDL-File, das alle Schnittstellen von DirectShow auflistet, die man in seinem C#- Programm braucht.

Für mein NapCap Projekt habe ich folgende kleine IDL geschrieben:

// zu verwendende IDL- Definitionen
import "devenum.idl";  // Geräte (Device-Treiber) Auflistung 
import "axcore.idl";     // DirectShow Kern
import "axextend.idl";  // DirectShow Erweiterungen 
import "control.odl";    // (Player-) Gerätesteuerung
 
[
    // eindeutige Kennung meiner Bibliothek
    uuid(8E0A9DEA-B025-4e20-944D-D8B25F010823),
    helpstring("Capture type library")
]
library CaptureTypeLib // Bibliotheks/Assembly Name
{
    // COM- Allerweltskram
    importlib("STDOLE2.TLB");
 
    interface IGraphBuilder;   // Filtergraph Management/Container
    interface IMediaControl;   // Gerätesteuerung
    interface IMediaEventEx; // Geräte- Feedback (Zustandsmeldungen)
    interface IMediaSeeking; // Medien- Positionierung 
    interface ICreateDevEnum; // Geräte- Sucher
    interface IAMAudioInputMixer; // Soundkarten- Mixer
    interface IFileSinkFilter;  // Mediendateien- Speicherung
};

Diese IDL ist wohl nur für Audio-Transcoder geeignet, weil andere Schnittstellen- z.B. für Video-Anwendungen fehlen.

Wie ein .NET Programm seine Referenzen importiert, gibt es hier erstmal viele Importe von anderen IDL-Dateien und TLBs. Dann definiere ich meine CaptureTypeLib- Bibliothek und gebe alle Schnittstellen an, die ich in meinem Programm brauche. Der Bibliotheksname ist auch der spätere Assembly- Name, den ich in den Projekt-Referenzen und Namensräumen sehe. Da COM noch aus Zeiten ohne Namensräume stammt, werden hier statt Namen GUIDs (globally unique IDs) zur weltweit eindeutigen Kennzeichnung sämtlicher benamten Objekte wie Interfaces, Klassen, Typen und Konstanten verwendet. Solche GUIDs kann z.B. das Tool GUIDGEN.EXE erzeugen, das wie MIDL.EXE bei der PlatformSDK dabei ist. Bei XML / SOAP / WSDL sollen ja nun URIs, zu denen ja z.B. auch Webseiten-Addressen gehören, den Zweck solch eindeutiger Kennzeichnung erledigen.

Wie auch immer- GUIDs sind bei Windows sehr verbreitet und kennzeichnen so ziemlich alles und machen es in der Registrierung systemweit bekannt. Alle COM-Objekte haben also eine hier CLSID genannte GUID, Interfaces dort eine IID genannte GUID und gerade bei DirectShow werden wir noch viele andere GUIDs für allerlei Typen kennenlernen, von denen die meisten leider nur in einer C-Headerdatei „uuids.h“ auftauchen, die man ebenfalls im Include-Verzeichnis der PlatformSDK findet. Die „globale“ Eintragung von Komponenten und Typen in der Registry ist ein großer Nachteil von COM, denn sie verbietet unterschiedliche Versionsstände der gleichen Komponenten auf einem Computer und führt daher leicht zu inkompatiblen Anwendungen.

Doch zuerst zurück zu unserer TLB. Mit den Kommandos…

midl /I <include> /I <include>/DShowIDL capturetypelib.idl
tlbimp capturetypelib.tlb

.. kommen wir endlich zu der benötigten Assembly, wenn wir statt <include> den korrekten Pfadnamen für das Platform-SDK Include Verzeichnis einsetzen und „capturetypelib.idl“ der Name unserer obigen IDL-Datei ist. MIDL sollte ohne meckern durchlaufen, aber TLBIMP meckert wegen einiger „Funktions-Signaturen“, welche in .NET nicht abbildbar sind. Im Klartext: Ohne „Handcodierung“ sind einige DirectShow Methoden nicht benutzbar, weil sie C- Strukturen verwenden, die nicht automatisch in C# Strukturen umgewandelt (gemarshalled) werden können. Zum Glück brauchen wir in NapCap wahrscheinlich keine solche Funktion- sonst müssten wir uns noch mit einer Menge „unsafe/unmanaged“ Code herumschlagen.

Wenn wir nun endlich eine CaptureTypeLib.dll haben, wie verwenden wir die in C#? Zuerst müssen wir sie natürlich als Referenz ins Projekt aufnehmen.

Vom Mixer in die Wav-Datei

Der folgende Klassen-Quelltext zeigt schon die Implementierung eines auf einer DirectShow Filterkette aufbauenden AudioMixer-Aufnahmeprogramms, wie es in der DirectShow -Einführung im letzten GraphEdt- Beispiel gezeigt wurde:

using System;
using System.Collections;
using System.Text;
using System.Windows.Forms; // Fenster Handles
 
using System.Runtime.InteropServices; // Zugriff auf COM/ActiveX- Objekte
using CaptureTypeLib; // DirectShow Audio Capture
 
namespace audiocap
{
  #region event arguments
 
  /// <summary>
  /// liefert nach Filterkettenaufbau deren Struktur als String
  /// </summary>
  class GraphBuiltEventArgs : EventArgs
  {
    public GraphBuiltEventArgs(string filterList)
    {
	this.filterList = filterList;
    }
 
    public string FilterList
    {
	get
	{
	  return this.filterList;
	}
    }
 
    private string filterList;
  }
 
  #endregion
 
  /// <summary>
	/// Klasse zum Aufnehmen von MP3-Dateien von einem Audio-Mixereingang
	/// </summary>
	public class AudioCap
  {
    #region public members
 
    public const int WM_GRAPHNOTIFY= 0x00008001; // windows message von IMediaEvents
    public event EventHandler OnGraphBuilt; // event wenn Filtergraph aufgebaut
 
    #endregion
 
    #region private members
 
    private IGraphBuilder gb = null; // Filtergraph Management/Container
    private IMediaControl mc = null; // Medien/Player- Steuerung (Run/Pause/Stop)
    private IMediaEventEx me = null; // Playerzustands- Benachrichtigung
    private IBaseFilter audioSource = null; // audio capture filter
    private Hashtable inputPins = null; // alle Mixer-Eingangs-Pins
    private Form notifyWin= null;  // Fenster das Player-Zustandsmeldungen bekommt
    private string recPin; // für Aufnahme gewählter Mixerpin (z.B. Stereomixer)
 
    #endregion
 
    #region properties
 
    /// <summary>
    /// liefert die Namen aller verfügbaren Mixer Eingangspins
    /// </summary>
    public string[] InputPinNames
    {
	get
	{
	  string[] result = new string[inputPins.Keys.Count];
	  IEnumerator pinEnum = inputPins.Keys.GetEnumerator();
	  for (int i=0; pinEnum.MoveNext(); ++i)
	  {
	    result[i] = (string)pinEnum.Current;
	  }
	  return result;
	}
    }
 
    /// <summary>
    /// liefert/setzt den eingestellten/gewünschten Eingangspin des Mixers
    /// </summary>
    public string InputPin
    {
	get
	{
	  return recPin;
	}
 
	set
	{
	  recPin = value;
	  IAMAudioInputMixer mixer;
 
	  for (IEnumerator pinEnu = inputPins.Values.GetEnumerator(); pinEnu.MoveNext(); )
	  {
	    try
	    {
		mixer = (IAMAudioInputMixer)(IPin)pinEnu.Current;
		mixer.put_Enable(0);
	    }
	    catch (Exception) { }
	  }
	  mixer = (IAMAudioInputMixer)(IPin)inputPins[recPin];
	  mixer.put_Enable(1);
	}
    }
 
    /// <summary>
    /// liefert/setzt den Aufnahmepegel 0..100
    /// </summary>
    public int RecLevel
    {
	get
	{
	  double vol;
	  IAMAudioInputMixer mixer = (IAMAudioInputMixer)(IPin)inputPins[recPin];
	  mixer.get_MixLevel(out vol);
	  return (int)(vol * 100.0);
	}
 
	set
	{
	  double vol = (double)value / 100.0;
	  IAMAudioInputMixer mixer = (IAMAudioInputMixer)(IPin)inputPins[recPin];
	  mixer.put_MixLevel(vol);
	}
    }
 
    /// <summary>
    /// schaltet Aufnahme ein/aus
    /// </summary>
    public bool Record
    {
	get
	{
	  _FilterState state;
	  int _state;
	  mc.GetState(0, out _state);
	  state = (_FilterState)_state;
	  return state == _FilterState.State_Running;
	}
 
	set
	{
	  if (value)
	  {
	    mc.Run();
	  }
	  else
	  {
	    mc.Stop();
	  }
	}
    }
 
    #endregion
 
    #region DirectShow GUIDs
 
    //CLSID_FilterGraph (Filtergraph Klasse, aus uuids.h)
    Guid FgGuid = new Guid("e436ebb3-524f-11ce-9f53-0020af0ba770");
 
    //CLSID_SystemDeviceEnum (DirectShow Geräte Enumerator, aus uuids.h)
    Guid DevEnumGuid = new Guid( "62BE5D10-60EB-11d0-BD3B-00A0C911CE86" );
 
    // CLSID WavDest (Multiplexer filter, Quellcode ist bei den DirectX Samples)
    Guid WavDestGuid = new Guid( "3C78B8E2-6C4D-11D1-ADE2-0000F8754B99" );
 
    // CLSID File Writer (Dateiausgabe filter, aus uuids.h)
    Guid FileWriterGuid = new Guid( "8596E5F0-0DA5-11D0-BD21-00A0C911CE86" );
 
    // CLSID Fraunhofer MP3-Encoder filter (in Registry gesucht)
    Guid Mp3EncoderGuid = new Guid("6A08CF80-0E18-11CF-A24D-0020AFD79767");
 
    // CLSID Lame Audio Encoder (in Registry gesucht)
    Guid LameAudioEncoderGuid = new Guid("B8D27088-FF5F-4B7C-98DC-0E91A1696286");
 
    // Basefilter Interface ID- leider nur in strmif.h zu finden
    Guid IID_IBaseFilter = new Guid( "56a86895-0ad4-11ce-b03a-0020af0ba770" );
 
    // CLSID_AudioInputDeviceCategory (Hauptgerätekategorie Audiogerät)
    Guid AudCapGuid = new Guid( "33d9a762-90c8-11d0-bd43-00a0c911ce86" );
 
    // MEDIATYPE_Audio (Medien Haupttyp Audio)
    Guid MEDIATYPE_Audio = new  Guid("73647561-0000-0010-8000-00AA00389B71");
 
    // MEDIATYPE_Stream (Medien Haupttyp Datenstrom)
    Guid MEDIATYPE_Stream = new Guid("e436eb83-524f-11ce-9f53-0020af0ba770");
 
    #endregion
 
    #region constructor
    /// <summary>
    /// Erzeuge Soundrecorder Klasse
    /// </summary>
    /// <param name="notifyWin">Fenster an das Zustandsmeldungen geschickt werden</param>
    public AudioCap(Form notifyWin)
		{
	this.notifyWin= notifyWin;
    }
 
    #endregion
 
    /// <summary>
    /// erzeuge einen Audio-Aufnahme Filtergraph
    /// </summary>
    /// <param name="fileName">Ziel-Dateiname für Aufnahme</param>
    /// <returns></returns>
    public bool BuildGraph(string fileName)
    {
	Cleanup(); // evtl. alten Filtergraph zerstören
 
	// Filter initialisieren
	IBaseFilter encoder= null;
	IBaseFilter wavDest= null;
	IBaseFilter fwriter= null;
 
	// Pins initialisieren
	IPin pin, captureOut = null;
	IPin encoderIn = null, encoderOut = null;
	IPin waveDestIn = null, waveDestOut = null;
	IPin fWriterIn;
 
	// Hilfsvariablen
	IEnumPins pins = null;
	_PinInfo pinInfo;
	uint fetched;
 
	try
	{
	  // erzeuge COM- Objekt für Filtergraph Manager
	  Type t = Type.GetTypeFromCLSID( FgGuid );
	  gb = (IGraphBuilder)Activator.CreateInstance( t );
 
	  // suche Soundkarte für Aufnahme
	  if ( FindDevice( out audioSource ) )
	  {
	    // bei Erfolg trage Audio-Aufnahme Filter ein
	    gb.AddFilter( audioSource , "Audio Capture" );			
	  }
	  else 
	  { 
	    // keine Soundkarte gefunden
	    Cleanup();
	    return false;
	  }
 
	  // erzeuge COM-Objekt für MP3-Encoder und hole Eingang/Ausgang- Pins
	  t = Type.GetTypeFromCLSID(Mp3EncoderGuid);
	  //t = Type.GetTypeFromCLSID(LameAudioEncoderGuid);
	  encoder= (IBaseFilter)Activator.CreateInstance(t);
	  gb.AddFilter(encoder, "Encoder");
	  encoder.FindPin("In", out encoderIn);
	  encoder.FindPin("Out", out encoderOut);
 
	  // erzeuge COM-Objekt für Datenstrom-Multiplexer und hole Eingang/Ausgang- Pins
	  t = Type.GetTypeFromCLSID(WavDestGuid);
	  wavDest= (IBaseFilter)Activator.CreateInstance(t);
	  gb.AddFilter(wavDest, "Wave Dest");
	  wavDest.FindPin("In", out waveDestIn);
	  wavDest.FindPin("Out", out waveDestOut);
 
	  // erzeuge COM-Objekt für Datei-Ausgabefilter
	  t = Type.GetTypeFromCLSID(FileWriterGuid);
	  fwriter= (IBaseFilter)Activator.CreateInstance(t);
 
	  // Datei-Ausgabefilter braucht einen Dateinamen für Ausgabe
	  IFileSinkFilter sink = (IFileSinkFilter)fwriter;
	  _AMMediaType mt = new _AMMediaType();
	  mt.majortype = MEDIATYPE_Stream;
	  sink.SetFileName(fileName, ref mt);
 
	  // trage Datei-Ausgabefilter in Filtergraph ein und hole Eingang/Ausgang- Pins
	  gb.AddFilter(fwriter, "File Writer");
	  fwriter.EnumPins(out pins);
	  pins.Next(1, out fWriterIn, out fetched);
 
	  // Mixer-Pin enumerator
	  audioSource.EnumPins(out pins);
	  inputPins = new Hashtable();
 
	  // iteriere alle Mixerpins
	  do 
	  {
	    pins.Next(1, out pin, out fetched);
	    if (fetched > 0)
	    {
		pin.QueryPinInfo(out pinInfo);
		// merke den Ausgabepin in "captureOut"
		if (pinInfo.dir == _PinDirection.PINDIR_OUTPUT)
		{
		  captureOut = pin;
		}
		else // speichere alle Eingabepins in Hashtable
		{
		  inputPins.Add(AsString(pinInfo.achName), pin);
		}
	    }
	  } 
	  while(fetched>0);
 
	  // ohne Ausgabepin geht nix
	  if (captureOut == null)
	  {
	    return false;
	  }
 
	  /* Verbinde Filterkette mit Encoder (Ziel: Mp3-Datei)
	  gb.Connect(captureOut, encoderIn);
	  gb.Connect(encoderOut, waveDestIn);*/
 
	  // Verbinde Filterkette ohne Encoder (Ziel: Wav-Datei)
	  gb.Connect(captureOut, waveDestIn);
	  gb.Connect(waveDestOut, fWriterIn);
 
	  // signalisiere Filtergraph an Aufrufer
	  PrintGraph();
 
	  // hole Mediensteuerung/Zustandsmeldungs- Interfaces
	  mc = (IMediaControl)gb;
	  me = (IMediaEventEx)gb;
 
	  // setze Fenster, das Zustandsmeldungen empfangen soll
	  me.SetNotifyWindow( (int)notifyWin.Handle, WM_GRAPHNOTIFY, 0 );
 
	}
	catch( Exception e) 
	{
	  MessageBox.Show(e.Message);
	  Cleanup();
	  return false;
	}
 
	return true;
    }
 
    /// <summary>
    /// Filterkette abbauen
    /// </summary>
    public void Cleanup() 
    {
	// Mixer Inputpins löschen
	if (inputPins != null)
	{
	  inputPins.Clear();
	}
	// Mediensteuerung löschen
	if( mc != null ) 
	{
	  mc.Stop();
	  mc = null;
	}
	// Zustandsmeldungen stoppen
	if( me != null ) 
	{
	  me.SetNotifyWindow( 0, WM_GRAPHNOTIFY, 0 );
	  me = null;
	}
	// Filtergraph vernichten
	if (gb != null)
	{
	  Marshal.ReleaseComObject(gb);
	}
    }
 
    /// <summary>
    /// Verarbeitung von Zustandsmeldungen
    /// </summary>
    public void OnGraphNotify()
    {
	int p1, p2;
	int code;
	if( me == null )  return;
	try 
	{
	  do
	  {
	    if( me == null )return;    
	    me.GetEvent( out code, out p1, out p2, 0 );   
	    // hier tut sich noch nicht viel
	    me.FreeEventParams( code, p1, p2 );
	  }
	  while( true );
	} 
	catch( Exception ){}
    }
 
    /// <summary>
    /// schicke Filtergraph-Beschreibung an registrierte clients
    /// </summary>
    public void PrintGraph()
    {
	string filterList;
	PrintGraph(gb, out filterList);
	if (OnGraphBuilt != null)
	{
	  OnGraphBuilt(this, new GraphBuiltEventArgs(filterList));
	}
    }
 
    /// <summary>
    /// liefert eine Beschreibung des erzeugten Filtergraphen
    //  (Filternamen und sämtliche hergestellten Verbindungen)
    /// </summary>
    /// <param name="graph"></param>
    /// <param name="text"></param>
    public void PrintGraph(IFilterGraph graph, out string text)
    {
	IEnumFilters enuFi;
	IEnumPins enuPi;
	_FilterInfo fiInfo;
	_PinInfo piInfo;
	IBaseFilter filter, filter2;
	IPin pin=null, pin2=null;
	uint fetched, fetched2;
	string name, name2, f2name, line;
	StringBuilder filters = new StringBuilder();
 
	// iteriere filter
	graph.EnumFilters(out enuFi);
	do
	{
	  enuFi.Next(1, out filter, out fetched);
	  if (fetched > 0)
	  {
	    // hole und gib filter- Info aus
	    filter.QueryFilterInfo(out fiInfo);
	    name = AsString(fiInfo.achName);
	    filters.AppendLine(name);
 
	    // iteriere pins des Filters
	    filter.EnumPins(out enuPi);
	    do
	    {
		  enuPi.Next(1, out pin, out fetched2);
		  if (fetched2 > 0)
		  {
		    // hole pin info
		    pin.QueryPinInfo(out piInfo);
		    name = AsString(piInfo.achName);
 
		    // nicht verbundene pins liefern exception, die abgefangen werden muss
		    try
		    {
			pin.ConnectedTo(out pin2);
		    }
		    catch (Exception) {}
 
		    if (pin2 != null) // gib Verbindung aus
		    {
			pin2.QueryPinInfo(out piInfo);
			name2 = AsString(piInfo.achName);
			filter2 = piInfo.pFilter;
			filter2.QueryFilterInfo(out fiInfo);
			f2name = AsString(fiInfo.achName);
			line = String.Format("  {0} -> {1}.{2}", name, f2name, name2);
		    }
		    else // gib Name von nicht verbundenem Pin aus
		    {
			line= String.Format("  {0}-> null", name);
		    }
		    filters.AppendLine(line);
		  }
	    }
	    while (fetched2 > 0);
	  }
	}
	while (fetched > 0);
	text = filters.ToString();
    }
 
    /// <summary>
    /// wandelt einen COM BSTR in einen .NET String um
    /// </summary>
    /// <param name="bstr"></param>
    /// <returns></returns>
    private string AsString(ushort[] bstr)
    {
	StringBuilder sb= new StringBuilder();
	for (int i = 0; i < bstr.Length; ++i)
	{
	  if (bstr[i] == 0) break;
	  sb.Append(Char.ConvertFromUtf32(bstr[i]));
	}
	return sb.ToString();
    }
 
    /// <summary>
    /// liefert das erste gefundene Audio-Aufnahmefilter
    /// </summary>
    /// <param name="srcFilter"></param>
    /// <returns></returns>
    private bool FindDevice(out IBaseFilter srcFilter) 
    {
	bool found = true;
	ICreateDevEnum de = null;
	srcFilter = null;
	try
	{
	  // erzeuge Geräte- Enumerator
	  Type t = Type.GetTypeFromCLSID( DevEnumGuid );
	  de = (ICreateDevEnum)Activator.CreateInstance( t );
	  IEnumMoniker em;
 
	  // suche Audio Aufnahmgeräte
	  de.CreateClassEnumerator( ref AudCapGuid, out em, 0 );
	  IMoniker mon;
	  uint result;
 
	  // hole erstes Aufnahmegerät
	  em.RemoteNext( 1, out mon, out result );  
	  object o;
 
	  // hole IBaseFilter Interface für das Aufnahmegerät
	  mon.RemoteBindToObject( null, null, ref IID_IBaseFilter, out o ); 
	  srcFilter = (IBaseFilter)o; 
	}
	catch( Exception ) 
	{
	  found = false;
	}
	finally
	{
	  Marshal.ReleaseComObject( de );	
	}
	return found;
    }
 
  }
}
 

Diese Klasse erledigt also komplett den Aufbau der benötigten Filterkette und das Aufnehmen über den Audiomixer in eine WAV bzw. MP3- Datei. Zur Kontrolle des gebauten Filtergraphen dient optional die PrintGraph() Methode, welche die Filterkette analysiert und die Filter und zwischen Ihnen hergestellten Verbindungen ausgibt.

Das ist hier nicht so wichtig wie bei Verwendung „intelligenter“ Filterverbindungen. DirectShow kann nämlich automatisch benötigte Zwischenfilter einfügen und mit Schnittstellen wie ICaptureGraphBuider2 auch ganze Aufnahme-Filtergraphen automatisch erzeugen. Warum habe ich hier nicht einfach ICaptureGraphBuider2 verwendet? Das klappt leider nur mit wenigen Multiplexerfilteren wie dem AVI-Mux filter. Mit dem von mir zum Schreiben von WAV-Dateien verwendeten WavDest-Multiplexer geht es laut MS-Knowledgebase leider nicht.

Der Filter WavDest

Dieses WavDest- Filter ist bei der PlatformSDK im Verzeichnis <Platform SDK>\Samples\Multimedia\DirectShow\Filters nur in veraltetem C++ Quellcode dabei. Mit dem aktuellem Visual C++ 2005 Compiler läßt es sich erst nach vielen Korrekturen übersetzen, Projektdateien gibt es auch nicht- nur Makefiles für NMAKE.EXE. Wer sich hier stundenlange Frustration ersparen will, lädt dieses und andere Filter besser mit den NapCap Quellen runter, wo es bereits fertig übersetzt vorliegt. Man muß diese Filter jedoch noch mit RegSrv32 registrieren:

regsvr32 wavdest.ax

Verwendung des Audio-Recorders

So kompliziert wie die Klasse aussieht- sie ist ganz einfach anzuwenden.

Der Konstruktor meiner Testklasse baut den Filtergraphen und registriert sich für Zustandsmeldungen:

private AudioCap ac= null;
 
public Form1()
{
  InitializeComponent();
 
   ac= new AudioCap(this);
   ac.OnGraphBuilt += this.OnGraphBuilt;
   ac.BuildGraph(@"c:\test.wav");
}

Wenn die Nachricht kommt, daß der Filtergraph aufgebaut ist, werden die Namen der Mixer-Eingänge in eine Combobox zur Auswahl durch den Benutzer eingetragen.

    public void OnGraphBuilt(object sender, EventArgs e)
    {
	//GraphBuiltEventArgs args= (GraphBuiltEventArgs)e;
 
	pinSelector.Items.Clear();
	foreach (string pin in ac.InputPinNames)
	{
	  pinSelector.Items.Add(pin);
	}
    }
 

Die Fensterfunktion des Hauptfenster muß die Zustandsmeldungen des Filtergraphen an die AudioCapture Klasse zurück reflektieren.

    protected override void WndProc( ref Message m ) 
    {
	if( m.Msg == AudioCap.WM_GRAPHNOTIFY )
	{
	  ac.OnGraphNotify();
	  return;
	}
	base.WndProc( ref m );
    }
 

Ein paar simple Eventhandler erlauben Einstellung von Eingangspin und Aufnahmepegel

    // nimm in Combobox gewählten Eingangspin
    private void pinSelector_SelectedIndexChanged(object sender, EventArgs e)
    {
	string pin = (string)pinSelector.SelectedItem;
	ac.InputPin = pin;
	recLevel.Value = ac.RecLevel;
    }
 
    // stelle Aufnahmepegel ein
    private void recLevel_Changed(object sender, EventArgs e)
    {
	ac.RecLevel = recLevel.Value;
    }
 
    // starte Aufnahme
    private void RecordButton_Click(object sender, EventArgs e)
    {
	ac.Record = true;
    }
 
    // beende Aufnahme
    private void StopButton_Click(object sender, EventArgs e)
    {
	ac.Record = false;
    }
 
    // beende Programm
    private void ExitButton_Click(object sender, EventArgs e)
    {
	this.Close();
    }

So, damit hab ich alle Leser erstmal genug erschreckt und kann aber beruhigen, daß die meisten anderen Teile von NapCap nicht so gruselig „maschinennah“ werden.

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