Laden...

CD-Text einer AudioCD auslesen - WMPLib.dll

Erstellt von Disane vor 13 Jahren Letzter Beitrag vor 13 Jahren 11.576 Views
D
Disane Themenstarter:in
10 Beiträge seit 2011
vor 13 Jahren
CD-Text einer AudioCD auslesen - WMPLib.dll

Hallo zusammen, aktuell werkel ich an einer Anwendung um DJs eine Möglichkeit zu geben, schnell und effizient ihre CDs zu erfassen.

Über das COM-Objekt hole ich mir aktuell alle Informationen zu einer CD. Dazu habe ich mir eine Klasse geschrieben:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WMPLib;

namespace TrackIT.Classes
{
    class AudioCD
    {
        public string CDTitle;
        public int driveIndex;
        public WMPLib.IWMPPlaylist songs;

        private WindowsMediaPlayer player = new WindowsMediaPlayer();

        public AudioCD(int driveIndex)
        {
            this.driveIndex = driveIndex;
            this.songs = GetTracksFromCD(this.driveIndex);
            this.CDTitle = GetCDTitle();
        }

        private string GetCDTitle()
        {
            return this.songs.name;
        }

        private WMPLib.IWMPPlaylist GetTracksFromCD(int driveIndex)
        {
            return this.player.cdromCollection.Item(driveIndex).Playlist;

        }
        
    }
}

Zum Testen hatte ich eine normale Audio-CD verwendet. Diese zeigt mir auch alle Tracks mit den entsprechenden Infos an.
Jedoch habe ich mal eine andere CD ausprobiert, diese zeigte mir keine Infos an.

Was mich an der geschichte verwundert ist folgendes:

Meine Pioneer DJ-Player und sämtliche Autoradios finden für die Titel Informationen.
Dabei werden diese vermutlich nicht auf Internetdienste à la FreeDB oder CC irgendwas zurückgreifen.

Meine Frage ist nun, gibt es eine Möglichkeit zuverlässig alle Titel- und CD-Informationen einer selbst erstellten CD zu bekommen?

Ich vermute das Problem liegt in der WMPLib-DLL.

Ich hoffe Ihr könnt mir weiter helfen.

4.942 Beiträge seit 2008
vor 13 Jahren

Hallo,

zeigt der Windows Media Player denn die Track-Titel für diese CD an?

Ich habe für meinen CD-Ripper (s. Audio-CDs schnell und qualitativ hochwertig in mp3 umwandeln ) folgenden Code benutzt: http://khason.net/dev/audio-cd-operation-including-cd-text-reading-in-pure-c/ (du mußt nur alles in eine C#-Datei kopieren)

D
Disane Themenstarter:in
10 Beiträge seit 2011
vor 13 Jahren

Also es werden Track-Informationen angezeigt, aber nach welchem Schema die angezeigt oder nicht angezeigt werden kann ich nicht sagen.

Manchmal werden Sie angezeigt, manchmal nicht. CD-Text ist aber definitiv vorhanden.

Ja, den Link hatte ich des öfteren gefunden.
Also einfach neue Class anlegen, Code rein, glücklich sein?

  • Edit:
    Hab den Code mal von der Seite in eine Klasse kopiert und den Namespace "using System.Runtime.InteropServices;" deklaiert, damit man die Win32-libs verwenden kann.

Jedoch bekomme ich nun folgende Fehlermledungen:> Fehlermeldung:

Error 2 The name 'MINIMUM_CDROM_READ_TOC_EX_SIZE' does not exist in the current context G:\Coding\TrackIT\TrackIT\classes\AudioCDs.cs 31 77 TrackIT
Error 3 The name 'MAXIMUM_NUMBER_TRACKS' does not exist in the current context G:\Coding\TrackIT\TrackIT\classes\AudioCDs.cs 31 110 TrackIT
Error 4 The name 'MINIMUM_CDROM_READ_TOC_EX_SIZE' does not exist in the current context G:\Coding\TrackIT\TrackIT\classes\AudioCDs.cs 33 62 TrackIT
Error 5 The name 'MAXIMUM_NUMBER_TRACKS' does not exist in the current context G:\Coding\TrackIT\TrackIT\classes\AudioCDs.cs 33 95 TrackIT
Error 6 The name 'MINIMUM_CDROM_READ_TOC_EX_SIZE' does not exist in the current context G:\Coding\TrackIT\TrackIT\classes\AudioCDs.cs 43 45 TrackIT
Error 7 The name 'MAXIMUM_NUMBER_TRACKS' does not exist in the current context G:\Coding\TrackIT\TrackIT\classes\AudioCDs.cs 43 78 TrackIT
Error 8 The type or namespace name 'CDROM_CD_TEXT_PACK' could not be found (are you missing a using directive or an assembly reference?) G:\Coding\TrackIT\TrackIT\classes\AudioCDs.cs 79 20 TrackIT

Ich muss ehrlich gesagt sagen, ich habe noch nie mit Unmanaged-Code gearbeitet und kenne mich da wenig aus.

Könntest du mir etwas auf die Sprünge helfen?

Hier mein Code:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace TrackIT.classes
{
    class AudioCDs
    {
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]

        public class CDROM_TOC_CD_TEXT_DATA
        {

            public ushort Length;

            public byte Reserved1;

            public byte Reserved2;

            public CDROM_TOC_CD_TEXT_DATA_BLOCK_ARRAY Descriptors;

        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]

        internal sealed class CDROM_TOC_CD_TEXT_DATA_BLOCK_ARRAY
        {

            internal CDROM_TOC_CD_TEXT_DATA_BLOCK_ARRAY() { data = new byte[MINIMUM_CDROM_READ_TOC_EX_SIZE * MAXIMUM_NUMBER_TRACKS * Marshal.SizeOf(typeof(CDROM_TOC_CD_TEXT_DATA_BLOCK))]; }

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = MINIMUM_CDROM_READ_TOC_EX_SIZE * MAXIMUM_NUMBER_TRACKS * 18)]

            private byte[] data;

            public CDROM_TOC_CD_TEXT_DATA_BLOCK this[int idx]
            {

                get
                {

                    if ((idx < 0) | (idx >= MINIMUM_CDROM_READ_TOC_EX_SIZE * MAXIMUM_NUMBER_TRACKS)) throw new IndexOutOfRangeException();

                    CDROM_TOC_CD_TEXT_DATA_BLOCK res;

                    var hData = GCHandle.Alloc(data, GCHandleType.Pinned);

                    try
                    {

                        var buffer = hData.AddrOfPinnedObject();

                        buffer = (IntPtr)(buffer.ToInt32() + (idx * Marshal.SizeOf(typeof(CDROM_TOC_CD_TEXT_DATA_BLOCK))));

                        res = (CDROM_TOC_CD_TEXT_DATA_BLOCK)Marshal.PtrToStructure(buffer, typeof(CDROM_TOC_CD_TEXT_DATA_BLOCK));

                    }
                    finally
                    {

                        hData.Free();

                    }

                    return res;

                }

            }

        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]

        public struct CDROM_TOC_CD_TEXT_DATA_BLOCK
        {

            public CDROM_CD_TEXT_PACK PackType;

            public byte bitVector1;

            public byte TrackNumber
            {

                get { return ((byte)((this.bitVector1 & 127u))); }

                set { this.bitVector1 = ((byte)((value | this.bitVector1))); }

            }

            public byte ExtensionFlag
            {

                get { return ((byte)(((this.bitVector1 & 128u) / 128))); }

                set { this.bitVector1 = ((byte)(((value * 128) | this.bitVector1))); }

            }

            public byte SequenceNumber;

            public byte bitVector2;

            public byte CharacterPosition
            {

                get { return ((byte)((this.bitVector2 & 15u))); }

                set { this.bitVector2 = ((byte)((value | this.bitVector2))); }

            }

            public byte BlockNumber
            {

                get { return ((byte)(((this.bitVector2 & 112u) / 16))); }

                set { this.bitVector2 = ((byte)(((value * 16) | this.bitVector2))); }

            }

            public byte Unicode
            {

                get { return ((byte)(((this.bitVector2 & 128u) / 128))); }

                set { this.bitVector2 = ((byte)(((value * 128) | this.bitVector2))); }

            }

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12, ArraySubType = UnmanagedType.I1)]

            public byte[] TextBuffer;

            public string Text
            {

                get { return (Unicode == 1) ? ASCIIEncoding.ASCII.GetString(TextBuffer) : UTF32Encoding.UTF8.GetString(TextBuffer); }

            }

            public ushort CRC;

        }

    }
}


4.942 Beiträge seit 2008
vor 13 Jahren

Hallo Disane,

da das CD-Ripper Projekt schon ca. drei Jahre her ist, weiß ich auch nicht mehr genau, ob die Sourcen dort komplett waren oder ob ich noch einiges ergänzen mußte.
Ich habe aber nun die Sourcen bei mir wiedergefunden und im Anhang hochgeladen.

Da du schreibst, daß du noch nie vorher mit Unmanaged-Code gearbeitet hast, ist das evtl. doch etwas zu "hart" für dich, denn auch der Aufruf und die Verwendung der DeviceIoControl-Funktion ist nicht ganz einfach.

Bevor das in zuviel Arbeit ausartet, habe ich mal meinen Ripper (als Exe) unter http://www.bitel.net/dghm1164/downloads/CD%20Ripper.zip abgelegt.
Probiere mal aus, ob mein "CD Ripper" überhaupt die CDTEXT-Daten deiner CD lesen kann?

Und wenn ja, dann kann ich dir den kompletten Source geben...

D
Disane Themenstarter:in
10 Beiträge seit 2011
vor 13 Jahren

Hallo Th69,

vielen Dank für deine schnelle Rückmeldung!
Sobald ich dazu komme schaue ich mir das mal an und werde mal ein paar CDs mit deinem Ripper durchtesten.

Sollte dies klappen wäre es echt super zu sehen, wie du das gemacht hast.
Werde dir aber da noch gesondert zu schreiben.

Ich bin aber guter Dinge, dass dies klappen wird.

D
Disane Themenstarter:in
10 Beiträge seit 2011
vor 13 Jahren

Hab deinen Ripper gerade ausprobiert mit 5 CDs. Immer wieder wurde der CD-Text ausgegeben.
Vermutlich scheint des WMPControl das nicht sauber lesen zu können.
Könntest du mir deinen Code als Beispiel zur Verfügung stellen?

Wäre echt super, danke!

4.942 Beiträge seit 2008
vor 13 Jahren

Hallo,

hätte ich ja jetzt nicht gedacht, daß das so gut klappt 😉

Ich habe nun die entsprechenden Teile aus meinem Gesamtprojekt extrahiert und unter http://www.bitel.net/dghm1164/downloads/CD%20Text-Sources.zip abgelegt.
Hauptklasse ist Ripper (der Name paßt jetzt natürlich nicht mehr so, da ich die anderen Methoden gelöscht habe), welche als Parameter ein Objekt der Klasse CDDrive benötigt. Diese Klasse befindet sich im separaten Projekt "Ripper", welche du dann zu deiner Solution hinzufügen mußt.
Der Aufruf sollte dann in etwas so sein:


using Ripper;

// in Methode
char cDrive = "D"; // Laufwerksbuchstabe deines CDDrives!!!
CDDrive drive = new CDDrive();
if(drive.Open(cDrive))
{
    drive.LoadCD();

    if(drive.IsCDReady())   
    {
        Ripper ripper = new Ripper(drive);
        List<string> TrackTitles;

        bool bCDText = ripper.ReadCDText(out TrackTitles);
        if(bCDText)
        {
            // in TrackTitels stehen nun die einzelnen Titel,
            // wobei an Position 0 der Albumtitel steht!!!
        }
    }
}

Falls du eine Liste der CDDrives benötigst, so kannst du auch


char[] Drives = CDDrive.GetCDDriveLetters();

dafür aufrufen und den User wählen lassen.

Melde dich, falls noch irgendetwas fehlt oder unklar ist.

Ansonsten viel Erfolg noch bei deinem DJ-Projekt!

P.S: Es wäre schön, wenn du mich (Th69) in deinem Projekt im Info-Dialog o.ä. erwähnst (bzw. verlinkst).

D
Disane Themenstarter:in
10 Beiträge seit 2011
vor 13 Jahren

Also ich konnte das erfolgreich einbauen 😃
Meine Frage ist nun, ich bekomme alle Track-Infos, wie würde ich es schaffen, Trackinterpret und Dauer rauszubekommen?

Edit: Natürlich werde ich dich in der Info erwähnen, da die Kernlogik von dir stammt. 😃

4.942 Beiträge seit 2008
vor 13 Jahren

Hi,

über drive.GetNumTracks() und drive.TrackSize(tracknumber) kriegst du die Trackzeiten in Sekunden.
Bei einigen CDs ist ja auch noch eine DataTrack drauf, diesen kannst du mittels !Drive.IsAudioTrack(tracknumber) rausfiltern.
Es gibt noch eine Reihe von weiteren nützlichen Methoden in der CDDrive-Klasse (einfach mal stöbern... die Klasse habe ich aber ja auch nur von CodeProject kopiert).

Und für den Interpreten mußt du mal schauen, ob entsprechende "Performer"-Blöcke vorhanden sind (s. Kommentar in der Ripper.CreateCDText()-Methode.
Dann kannst du die Methode ja so abändern, daß sie eine List<Track> rausliefert, so daß Track dann eine Klasse mit entsprechenden Eigenschaften für Titel, Interpret etc. ist.

D
Disane Themenstarter:in
10 Beiträge seit 2011
vor 13 Jahren

Also ich hab mir nun eine Klasse geschrieben für die <list>track:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TrackIT
{
    class Track
    {
        public List<string> lArtist;
        public List<string> lTrackname;
    }
}


Deine Methode habe ich auch entsprechend umgebaut, dass sie mir diesen Typ zurückgibt:


private List<TrackIT.Track> CreateCDText(Win32Functions.CDROM_TOC_CD_TEXT_DATA Data)
        {
            List<TrackIT.Track> ret = new List<TrackIT.Track>();
            List<string> listTrack = new List<string>();
            List<string> listArtist = new List<string>();
            string sTrack = String.Empty;
            string sArtist = String.Empty;

            try
            {
                for(int i = 0; i < Data.Descriptors.MaxIndex; i++)
                {
                    Win32Functions.CDROM_TOC_CD_TEXT_DATA_BLOCK block = Data.Descriptors[i];

                    // todo: Performer ???
                    if(block.PackType == Win32Functions.CDROM_CD_TEXT_PACK.ALBUM_NAME)
                    {
                        foreach(char c in block.Text)
                        {
                            if(c != 0)
                                sTrack += c;
                            else
                            {
                                // Wenn leer, dann mach nichts
                                listTrack.Add(sTrack);
                                sTrack = String.Empty;
                            }
                        }
                    }
                    else if (block.PackType == Win32Functions.CDROM_CD_TEXT_PACK.PERFORMER)
                    {
                        foreach (char c in block.Text)
                        {
                            if (c != 0)
                                sArtist += c;
                            else
                            {
                                // Wenn leer, dann mach nichts
                                listArtist.Add(sArtist);
                                sArtist = String.Empty;
                            }
                        }
                    }
                }
            }            
            catch(IndexOutOfRangeException)
            {
                // end of list
            }
            finally
            {
                if(sTrack != String.Empty) listTrack.Add(sTrack);
                if(sArtist != String.Empty) listArtist.Add(sArtist);
            }

            // Wertzuweisung
            return ret;
        }

Jedoch scheitere ich gerade daran, dass ich nun die beiden Listen (Artist und Track) der list<Track> hinzufügen muss und nun steh ich da wie ein ochs vorm Berg 😄

Was wäre das beste Verfahren dafür?

Edit: Habs nun hinbekommen, aber der sagt mir beim Auslesen der Track-Dauer, dass der TOC notValid ist. Spricht TovValid ist = false.

4.942 Beiträge seit 2008
vor 13 Jahren

Hallo,

bzgtl. der TOC: fragst du die Trackdauer denn auch erst nach drive.LoadCD() sowie drive.IsCDReady() ab?

(aufgrund des Aprilscherzes 😉 habe ich heute morgen nicht geantwortet...)

D
Disane Themenstarter:in
10 Beiträge seit 2011
vor 13 Jahren

Ja, die Trackdauer lese ich lange nach dem drive.LoadCD(), siehe hier:


CDDrive drive = new CDDrive();
                
                if (drive.Open(cDrive))
                {
                    drive.LoadCD();
                    drive.GetNumAudioTracks();
                    if (drive.IsCDReady())
                    {
                        CD_Ripper.Ripper ripper = new CD_Ripper.Ripper(drive);
                        TrackIT.Track TrackTitles;

                        bool bCDText = ripper.ReadCDText(out TrackTitles);
                        if (bCDText)
                        {
                            txtCdname.Text = TrackTitles.lTrackname[0].ToString();
                            for (int i = 1; i <= TrackTitles.lArtist.Count(); i++)
                            {
                                if (TrackTitles.lArtist[i].ToString() != "" || TrackTitles.lTrackname[i].ToString() != "")
                                    dataGridView1.Rows.Add(i, TrackTitles.lArtist[i].ToString(), TrackTitles.lTrackname[i].ToString(), "", drive.TrackSize(i));
                            }
                        }
                        btnSaveMarked.Enabled = true;
                        btnGetAll.Enabled = true;
                    }
                }

4.942 Beiträge seit 2008
vor 13 Jahren

Hallo,

wo genau kriegst du denn die TOC-Fehlermeldung?

Pack' auch mal das 'drive.GetNumAudioTracks()' nach dem 'drive.IsCDReady()'.
Und werte den Rückgabewert aus!
Besser wäre es sogar, wenn du zwei getrennte Schleifen hintereinander durchläufst, also erst die CD-Texte lesen und anschließend die Trackzeiten.

A propos Trackzeiten:
da habe ich mich vertan, die Methode drive.TrackSize(i) gibt die Anzahl der Bytes (des Tracks) wieder, d.h. die Zeiten muß man sich daraus noch errechnen.
Packe in die Ripper-Klasse noch folgende Zeilen:


public static readonly int Rate = 44100;
public static readonly int Bits = 16;
public static readonly int Channels = 2;

public static int BytesPerSec
{
    get { return Rate * (Bits / 8) * Channels; }
}

Zum Berechnen der Zeit (in Min. und Sek.) dann folgende Methode aufrufen:


uint nTrackLen = TrackLength(drive.TrackSize(i));
uint nTrackMin = (nTrackLen / 60);
uint nTrackSec = (nTrackLen % 60);

wobei die Methode so definiert ist:


uint TrackLength(uint nSize)
{
    uint DIV = (uint)Ripper.BytesPerSec;
    return (nSize + DIV / 2) / DIV;
}

Nun sollte es aber klappen...

D
Disane Themenstarter:in
10 Beiträge seit 2011
vor 13 Jahren

Also es funktioniert.
Die TOC war nicht valid, weil diese nicht mal ausgelesen wurde.
Sobald ich ein Drive.Refresh() mache geht es und die TOC ist valid.

Daraufhin kann ich dann auch die Trackzeiten auslesen.
Werde gleich mal deine Methoden implementieren und dann mal die Info-Form bauen.

//Edit:
Super funzt alles soweit, danke! 😃

Meine Frage ist jedoch auch folgende:
Wenn ich das Datagrid direkt mit den Tracks fülle (ohne vorher auf leere Einträge zu prüfen) wird mir das Datagrig mit vielen leeren Einträgen gefüllt.

Hast du bei dir auch eine solche Prüfung implementiert oder bekommst du bei 12 tTracks auch nur 12 Tracks in der List?

4.942 Beiträge seit 2008
vor 13 Jahren

Guten Morgen Disane,

sorry, daß ich die wichtigste Methode drive.Refresh() vergessen hatte (aber ich wollte dich ja nur testen 😉

Und nein, leere Einträge hatte ich keine bei mir.
Aber ich denke, daß liegt an deiner Performer-Ergänzung in CreateCDText().
Dein unten geposteter Code erscheint mir auch nicht korrekt.
Ein einzelner Track sollte ja auch nur aus genau einem Namen und Titel bestehen (nicht jeweils aus Listen):


class Track
{
    public string Artist { get; set; };
    public string TrackName { get; set; }
}

(ich bin mir nur nicht sicher, wie die Zuordnung dann von ALBUM_NAME und PERFORMER innerhalb der Descriptor-Liste ist, also ob diese abwechselnd kommen oder aber nacheinander - das müßtest du also dann anhand der 'block.TrackNumber' zuordnen. Ich bin bei meinem vorherigen Code immer davon ausgegangen, daß die Reihenfolge stimmt und ich hatte ja auch nur den Titel ausgelesen.)

D
Disane Themenstarter:in
10 Beiträge seit 2011
vor 13 Jahren

Vielen Dank für deine Info. Ja ich bin ja eher so der Mensch der einfach wahllos Methoden durchsucht und durch den Code stepped und dem dann sowas auffällt 😉

Hier mal der Info-Dialog, hoffe er ist in ordnung so (siehe Anhang).

Zum Thema Tracknamen und Artisten:
Die Artisten sind immer als erstes angegeben.
Sprich erst der CD-Name und dann die (z.B.) 14 Artisten. Danach kommen einige Leerzeilen und dann kommen erst die 14 Tracktitel.

Kann dir heute Abend mal zeigen was ich meine, hab gerade keine Möglichkeit eine CD abzurufen.

D
Disane Themenstarter:in
10 Beiträge seit 2011
vor 13 Jahren

Ich habe nochmal nachgesehen, es war ein Fehler meinerseits.

Meine Frage ist jedoch, gibt es die Möglichkeit, dass mein Programm erkennen kann, dass eine neue CD eingelegt wurde und daraufhin z.B. eine Combox mit den Drives automatisch aktualisiert?

Habe leider in der Klasse nichts derartiges gefunden.

49.485 Beiträge seit 2005
vor 13 Jahren

Hallo Disane,

das Thema autorun wurde schon einige Male besprochen. Bitte benutze die Forumssuche und poste die besten Treffer hier. Vielen Dank!

herbivore

4.942 Beiträge seit 2008
vor 13 Jahren

Hallo Disane,

sorry, ich hatte deinen Beitrag bzgl. des Info-Dialogs vor zwei Wochen schon gelesen, aber vergessen zu antworten. Sieht ja schon recht "stylish" aus, also es gefällt mir und der Link reicht mir auch 😉

Und in der CDDrive-Klasse gibt es die beiden Ereignisse CDInserted und CDRemoved, auf die du reagieren kannst. Diese gelten jedoch nur für das aktuelle Laufwerk (s.a. CDDrive.NotWnd_DeviceChange). Bei mehreren Laufwerken müßtest du entweder pro Laufwerk eine CDDrive-Instanz erzeugen oder aber die Ereignisse abändern, so daß du dann noch den Laufwerksbuchstaben übermittelt bekommst.

@herbivore: direkt mit "Autorun" hat dies weniger zu tun, sondern es geht ja um die Aktualisierung der AudioCD-Infos.

49.485 Beiträge seit 2005
vor 13 Jahren

Hallo Th69,

... es geht ja um die Aktualisierung der AudioCD-Infos ...

... aufgrund des Wechsels der CD, was üblicherweise ein autorun auslöst. Zumindest wird man aber über das Stichwort autorun an die gewünschte Information kommen.

herbivore