Projekte > Audio-CD (CDDA) > MP3 speichern

Als MPEG-1 Audio Layer 3 (MP3) speichern

  • Beschreibung
  • Encoder
  • MP3-Header
  • Einstellungen
  • Puffer
  • Encoden
  • Demo
  • Veränderungen
  • Beschreibung

    In der Regel wurden die Tracks der Audio-CD als Windows Media Audio (WMA) oder als MPEG-1 Audio Layer 3 (MP3) gespeichert. Hier soll letzteres beschrieben werden.

    Encoder

    Ein guter und bekannter Encoder ist lame, welcher dort allerdings nur als Quelltext angeboten wird. Kompilate der Lame_enc.DLL erhält man auf verschiedenen Seiten. Die erste Wahl dürfte dabei wohl RareWares sein.

    Seit Mai 2014 wird dort die Version 3.99.5 angeboten. Das Archiv bei RareWares enthält neben der Lame.exe und der Lame_enc.DLL verschiedene HTML-Seiten des lame-Projektes. Um die DLL in ein Delphi-Projekt einbinden zu können, wird eine Headerdatei mit den Definitionen der Funktionen und Typen benötigt.

    MP3-Header

    Die Header sind im Quelltext enthalten. Im Verzeichnis DLL ist bereits eine Unit MP3export.pas enthalten, welche man direkt einbinden kann. Oder man übersetzt sich das Notwendige selbst aus der BladeMP3EncDLL.h.

    So könnte diese Unit aussehen:

    unit uBladeMP3EncDLL;
     
    interface
     
    const
      LAME_DLL = 'lame_enc.dll';
     
    type
      HBE_STREAM = Cardinal;
      BE_ERR     = Integer;
     
    const
      BE_ERR_SUCCESSFUL                = $00000000;
      BE_ERR_INVALID_FORMAT            = $00000001;
      BE_ERR_INVALID_FORMAT_PARAMETERS = $00000002;
      BE_ERR_NO_MORE_HANDLES           = $00000003;
      BE_ERR_INVALID_HANDLE            = $00000004;
      BE_ERR_BUFFER_TOO_SMALL          = $00000005;
     
    const
      BE_CONFIG_MP3  = 0;
      BE_CONFIG_LAME = 256;
     
    const
      TLAME_QUALITY_PRESET = (
        LQP_NOPRESET         = -1,
        LQP_NORMAL_QUALITY   = 0,
        LQP_LOW_QUALITY      = 1,
        LQP_HIGH_QUALITY     = 2,
        LQP_VOICE_QUALITY    = 3,
        LQP_R3MIX            = 4,
        LQP_VERYHIGH_QUALITY = 5,
        LQP_STANDARD         = 6,
        LQP_FAST_STANDARD    = 7,
        LQP_EXTREME          = 8,
        LQP_FAST_EXTREME     = 9,
        LQP_INSANE           = 10,
        LQP_ABR              = 11,
        LQP_CBR              = 12,
        LQP_MEDIUM           = 13,
        LQP_FAST_MEDIUM      = 14,
        LQP_PHONE            = 1000,
        LQP_SW               = 2000,
        LQP_AM               = 3000,
        LQP_FM               = 4000,
        LQP_VOICE            = 5000,
        LQP_RADIO            = 6000,
        LQP_TAPE             = 7000,
        LQP_HIFI             = 8000,
        LQP_CD               = 9000,
        LQP_STUDIO           = 10000
      );
     
    const
      MPEG1 = 1;
      MPEG2 = 0;
     
    type
      TVBRMETHOD = (
        VBR_METHOD_NONE    = -1,
        VBR_METHOD_DEFAULT =  0,
        VBR_METHOD_OLD     =  1,
        VBR_METHOD_NEW     =  2,
        VBR_METHOD_MTRH    =  3,
        VBR_METHOD_ABR     =  4
      );
     
    type
      Tmp3 = packed record
        dwSampleRate : Cardinal;
        byMode       : Byte;
        wBitrate     : Word;
        bPrivate     : LongBool;
        bCRC         : LongBool;
        bCopyright   : LongBool;
        bOriginal    : LongBool;
      end;
     
    type
      TLHV1 = packed record
        dwStructVersion : Cardinal;
        dwStructSize    : Cardinal;
        dwSampleRate    : Cardinal;
        dwReSampleRate  : Cardinal;
        nMode           : Integer;
        dwBitrate       : Cardinal;
        dwMaxBitrate    : Cardinal;
        nPreset         : Integer;
        dwMpegVersion   : Cardinal;
        dwPsyModel      : Cardinal;
        dwEmphasis      : Cardinal;
        bPrivate        : LongBool;
        bCRC            : LongBool;
        bCopyright      : LongBool;
        bOriginal       : LongBool;
        WriteVBRHeader  : LongBool;
        bEnableVBR      : LongBool;
        nVBRQuality     : Integer;
        dwVbrAbr_bps    : Cardinal;
        nVbrMethod      : TVBRMETHOD;
        bNoRes          : LongBool;
        bStrictIso      : LongBool;
        nQuality        : Word;
        btReserved      : Array[0..255 - 4 * SizeOf(Cardinal)] of Byte;
      end;
     
    type
      Tformat = packed record
      case dwConfig : Cardinal of
        BE_CONFIG_MP3  : (mp3  : TMP3);
        BE_CONFIG_LAME : (lhv1 : TLHV1);
      end;
     
    type
      TBE_CONFIG = packed record
        format : Tformat;
      end;
      PBE_CONFIG = ^TBE_CONFIG;
     
    const
      CURRENT_STRUCT_VERSION = 1;
      CURRENT_STRUCT_SIZE    = sizeof(TBE_CONFIG);
     
    function beInitStream  (var pbeConfig: TBE_CONFIG;
                            var dwSamples, dwBufferSize: Cardinal;
                            var phbeStream: HBE_STREAM
                           ): BE_ERR;
                           CDECL; external LAME_DLL name 'beInitStream';
    function beEncodeChunk (hbeStream: HBE_STREAM;
                            nSamples: Cardinal; pSamples: PSmallInt;
                            pOutput: PByte; var pdwOutput: Cardinal
                           ): BE_ERR;
                           CDECL; external LAME_DLL name 'beEncodeChunk';
    function beDeinitStream(hbeStream: HBE_STREAM;
                            pOutput: PByte; var pdwOutput: Cardinal
                           ): BE_ERR;
                           CDECL; external LAME_DLL name 'beDeInitStream';
    function beCloseStream (hbeStream: HBE_STREAM
                           ): BE_ERR;
                           CDECL; external LAME_DLL name 'beCloseStream';
     
    implementation
     
    end.
    

    Die Struktur TBE_CONFIG nimmt die Einstellungen TLHV1 für das Encoding auf. Mit der Funktion beInitStream wird der Encoder initialisiert und ihm die Konfiguration mitgeteilt. Dafür erhält man die Anzahl der Sample, welche dem Encoder jeweils übergeben werden sollen, die Größe für den für die Antwort bereitzustellenden Buffer und das Handle für den Stream. Der Funktion beEncodeChunk wird das Handle, die Anzahl der zu encodenden Sample und der Pointer zu diesen Samples übergeben. Bei älteren Versionen der Lame_enc.DLL musste die Anzahl der Sample exakt stimmen. Nun dürfen es auch mehr sein. Zurück bekommt man den Pointer auf den Output und dessen Größe. Nach dem letzten Aufruf der Funktion beEncodeChunk wird die Funktion beDeInitStream aufgerufen. Ihr wird wieder das Handle übergeben und man erhält den Pointer auf den letzten Output sowie dessen Größe. Mit der Funktion beCloseStream schließt man den Stream.

    Einstellungen

    BE_CONFIG.dwConfig - Gibt an, welcher Header konfiguriert wird.

    BE_CONFIG.LHV1.dwStructVersion - Version der Struktur.

    BE_CONFIG.LHV1.dwStructSize - Größe der Struktur.

    BE_CONFIG.LHV1.dwSampleRate - Samplerate der Quelle, akzeptiert werden 32000, 44100 und 48000 Hz.

    BE_CONFIG.LHV1.dwReSampleRate - Samplerate des Zieles, akzeptiert werden 32000, 44100 und 48000 Hz. Wenn die Samplerate nicht geändert werden soll wird 0 angegeben.

    BE_CONFIG.LHV1.nMode - Mode der Audiokanäle: Stereo (0), Joint Stereo (1), Dualchannel (2) und Mono (3).

    BE_CONFIG.LHV1.dwBitrate - Bei konstanter Bitrate ist es die Bitrate, bei variabler Bitrate ist es die minimale Bitrate.

    BE_CONFIG.LHV1.dwMaxBitrate - Wird nur bei maximaler Bitrate angegeben.

    BE_CONFIG.LHV1.nPreset - Ein Wert aus der Aufzählung von TLAME_QUALITY_PRESET. Dabei ist zu beachten, dass manche Presets eigene Bitrateneinstellungen beinhalten und somit individuelle aussen vor bleiben.

    BE_CONFIG.LHV1.dwMpegVersion - MPEG-1, MPEG-2 oder MPEG-2.5.

    BE_CONFIG.LHV1.dwPsyModel und BE_CONFIG.LHV1.dwEmphasis sind noch reserviert.

    BE_CONFIG.LHV1.bPrivate - Gibt an, ob es ein privater Stream ist.

    BE_CONFIG.LHV1.bCRC - Gibt an, ob die Frames mit einer Fehlerprüfsumme versehen werden soll.

    BE_CONFIG.LHV1.bCopyright - Gibt an, ob der Stream kopiergeschützt ist. Könnte man aus dem CONTROL des Tracks auslesen.

    BE_CONFIG.LHV1.bOriginal - Gibt an, ob der Titel das Original ist.

    BE_CONFIG.LHV1.WriteVBRHeader - Gibt an, ob ein XING Header geschrieben werden soll.

    BE_CONFIG.LHV1.bEnableVBR - gibt an, ob mit variabler Bitrate encodet werden soll.

    BE_CONFIG.LHV1.nVBRQuality - Gibt die Qualität beim Encoding mit variabler Bitrate an. Die Werte reichen von 0 (Höchste Qualität - Langsam) bis 9 (Niedrigste Qulität, Schnell).

    BE_CONFIG.LHV1.dwVbrAbr_bps - Gibt die durchschnittliche Bitrate an.

    BE_CONFIG.LHV1.nVbrMethod - Gibt die Encodingmethode beim Encoding mit variabler Bitrate an. Die Werte sind in TVBRMETHOD aufgezählt.

    BE_CONFIG.LHV1.bNoRes, BE_CONFIG.LHV1.bStrictIso, BE_CONFIG.LHV1.nQuality und BE_CONFIG.LHV1.btReserved sind noch reserviert.

    Das könnte dann so aussehen:

    var
      ...
      pbeConfig : TBE_CONFIG;
     
    begin
      ...
      FillChar(pbeConfig, SizeOf(TBE_CONFIG), 0);
      with pbeConfig.format
      do begin
        dwConfig             := BE_CONFIG_LAME;
        lhv1.dwStructVersion := CURRENT_STRUCT_VERSION;
        lhv1.dwStructSize    := CURRENT_STRUCT_SIZE);
        lhv1.dwSampleRate    := 44100;
        lhv1.dwReSampleRate  := 0;
        lhv1.nMode           := 1;
        lhv1.dwBitrate       := 32;
        lhv1.dwMaxBitrate    := 192;
        lhv1.nPreset         := Integer(LQP_FAST_STANDARD);
        lhv1.dwMpegVersion   := MPEG1;
        lhv1.dwPsyModel      := 0;
        lhv1.dwEmphasis      := 0;
        lhv1.bPrivate        := False;
        lhv1.bCRC            := False;
        lhv1.bCopyright      := False;
        lhv1.bOriginal       := False;
        lhv1.WriteVBRHeader  := False;
        lhv1.bEnableVBR      := True;
        lhv1.nVBRQuality     := 5;
        lhv1.dwVbrAbr_bps    := 0;
        lhv1.nVbrMethod      := TVBRMETHOD(fMP3Settings.VBRMethod - 1);
        lhv1.bNoRes          := False;
        lhv1.bStrictIso      := False;
        lhv1.nQuality        := 0;
      end;
      ...
    end;
    ...
    

    Buffer

    Die Sektoren der CD werden in einen Buffer eingelesen. Der Encoder liest die Chunks aus einen Buffer und gibt die encodeten Frames in einen anderen Buffer aus.

    Jeder Sektor einer CD enthält 98 Frames mit je sechs Samples für beide Kanäle. Das heißt 588 Samples für jeden Kanal bzw. insgesamt 1176 Samples.

    Mit dem Aufruf der Funktion beInitStream erhält man die Anzahl der Samples, welche der Funktion beEncodeChunk übergeben werden sollen. Dies sind 2304. Dabei wird jedes Sample ohne Unterscheidung nach Mono oder Stereo, gezählt.

    Für die Ausgabe der Daten von der CD und die Eingabe zum Encoder sollte ein gemeinsamer Buffer benutzt werden. Dieser sollte in seiner Größe so bemessen sein, dass nicht großartig mit den Daten jongliert werden muss. Dazu wird das kleinste gemeinsame Vielfache ermittelt:

    kgV(1176, 2304) = 112896 (Siehe mathepower.com)

    Dies sind 96 Sektoren, 49 Chunks oder 225792 B bzw. etwa 220,5 kB. In den ersten Demos waren 96 * 6 = 576 Sektoren eingestellt. Das funktionierte mit dem Laufwerk in PC sehr gut. Im aktuellen Laptop jedoch nicht. Da musste die Einstellung bis auf 48 heruntergenommen werden.

    Encoden

    Als Grundlage dient das Programm aus dem vorigen Kapitel. Dieses muss ein wenig geändert und erweitert werden. So werden in die grafische Oberfläche einige Einstellmöglichkeiten für das MP3 eingefügt:

    Die Anzahl der Einstellmöglichkeiten könnte man reduzieren, wenn man bei den Qualitätspreset nur solche einträgt, welche andere beinhalten. Zum Beispiel scheint das Preset STANDARD ein VBR-Encoding mit BR minimal von 32 und BR maximal von 320 zu sein.

    In die Funktion READTrack müssen die Encodingfunktionen eingefügt werden. Hier sind diesmal auch schon ein paar Überprüfungen enthalten:

    function ReadTrackMP3(aTrack: TRipTrack; aDevice: THandle): Boolean;
    const
      cnBR  : Array[0..13] of Integer = (32, 64, 96, 128, 160, 192, 224, 256,
                                         288, 320, 352, 384, 416, 448);
      cnLQP : Array[0..25] of Integer = (-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
                                         10, 11, 12, 13, 14, 1000, 2000, 3000,
                                         4000, 5000, 6000, 7000, 8000, 9000, 10000);
      SamplePerSector = 2 {Channels} * 6 {Samples per Frame} * 98 {Frames per Sector};
      fdwSectors      = 96;
    var
      dwReturned   : Cardinal;
      dwSectors    : Cardinal;
      aFileStream  : TFileStream;
      pbeConfig    : TBE_CONFIG;
      beError      : Cardinal;
      dwSamples    : Cardinal;
      dwBufferSize : Cardinal;
      hbeStream    : HBE_STREAM;
      pBufferPCM   : PSmallInt;
      pBufferMP3   : PByte;
      toWrite      : Cardinal;
    begin
      {
      *  Konfiguration eingeben.
      }
      FillChar(pbeConfig, SizeOf(TBE_CONFIG), 0);
      pbeConfig.format.dwConfig             := BE_CONFIG_LAME;
      pbeConfig.format.lhv1.dwStructVersion := CURRENT_STRUCT_VERSION;
      pbeConfig.format.lhv1.dwStructSize    := CURRENT_STRUCT_SIZE;
      pbeConfig.format.lhv1.dwSampleRate    := 44100;
      pbeConfig.format.lhv1.dwReSampleRate  := 0;
      pbeConfig.format.lhv1.nMode           := fMP3Settings.ChannelMode;
      pbeConfig.format.lhv1.dwBitrate       := cnBR[fMP3Settings.MinimalBR];
      pbeConfig.format.lhv1.nPreset         := cnLQP[fMP3Settings.Qualität];
      pbeConfig.format.lhv1.dwMpegVersion   := MPEG1;
      pbeConfig.format.lhv1.dwPsyModel      := 0;
      pbeConfig.format.lhv1.dwEmphasis      := 0;
      pbeConfig.format.lhv1.bPrivate        := False;
      pbeConfig.format.lhv1.bCRC            := False;
      pbeConfig.format.lhv1.bCopyright      := False;
      pbeConfig.format.lhv1.bOriginal       := False;
      pbeConfig.format.lhv1.WriteVBRHeader  := False;
      pbeConfig.format.lhv1.bEnableVBR      := fMP3Settings.EnabledVBR;
      pbeConfig.format.lhv1.dwVbrAbr_bps    := 0;
      pbeConfig.format.lhv1.bNoRes          := False;
      pbeConfig.format.lhv1.bStrictIso      := False;
      pbeConfig.format.lhv1.nQuality        := 0;
      if fMP3Settings.EnabledVBR
      then begin
        pbeConfig.format.lhv1.dwMaxBitrate := cnBR[fMP3Settings.MaximalBR];
        pbeConfig.format.lhv1.nVBRQuality  := fMP3Settings.VBRQuality;
        pbeConfig.format.lhv1.nVbrMethod   := TVBRMETHOD(fMP3Settings.VBRMethod - 1);
      end;
      {
      *  Stream initialisieren.
      }
      beError := beInitStream(pbeConfig, dwSamples, dwBufferSize, hbeStream);
      if beError = BE_ERR_SUCCESSFUL
      then begin
        {
        *  Speicher reservieren.
        }
        dwBufferSize := CB_AUDIOSECTOR * fdwSectors;
        GetMem(pBufferPCM, dwBufferSize);
        GetMem(pBufferMP3, dwBufferSize);
        try
          aFileStream := TFileStream.Create(aTrack.stFilename, fmCreate or
                                            fmOpenWrite or fmShareExclusive);
          {
          *  Lesen.
          }
          dwReturned := 0;
          Result     := True;
          while (dwReturned < aTrack.dwLength) and Result
          do begin
            {
            *  Auf Rest prüfen.
            }
            if (dwReturned + fdwSectors) < aTrack.dwLength
            then dwSectors := fdwSectors
            else dwSectors := aTrack.dwLength - dwReturned;
            {
            *  Daten lesen.
            }
            Result := ReadCDDA(aTrack.dwOffset + dwReturned, dwSectors, aDevice,
                               pBufferPCM);
            if not(Result)
            then begin
              beCloseStream(hbeStream);
              raise Exception.Create('Beim Lesen von CD ist ein Fehler aufgetreten.');
            end;
            {
            *  Encoden.
            }
            beError := beEncodeChunk(hbeStream, dwSectors * SamplePerSector,
                                     pBufferPCM, pBufferMP3, ToWrite);
            if beError <> BE_ERR_SUCCESSFUL
            then begin
              beCloseStream(hbeStream);
              raise Exception.Create('Beim Encoden ist ein Fehler aufgetreten.');
            end;
            {
            *  Datei schreiben.
            }
            if aFileStream.Write(pBufferMP3^, ToWrite) <> ToWrite
            then begin
              beCloseStream(hbeStream);
              raise Exception.Create('Beim Schreiben ist ein Fehler aufgetreten.');
            end;
            {
            *  Zähler erhöhen.
            }
            inc(dwReturned, dwSectors);
            Application.ProcessMessages;
          end;
          {
          *  Deinitialisieren und Rest schreiben.
          }
          beError := beDeInitStream(hbeStream, pBufferMP3, ToWrite);
          if (beError <> BE_ERR_SUCCESSFUL)
          or (aFileStream.Write(pBufferMP3, ToWrite) <> ToWrite)
          then begin
            beCloseStream(hbeStream);
            raise Exception.Create('Beim Schreiben ist ein Fehler aufgetreten.');
          end;
          {
          *  Datei schließen.
          }
          aFileStream.Destroy;
        finally
          {
          *  Speicher freigeben.
          }
          FreeMem(pBufferPCM);
          FreeMem(pBufferMP3);
        end;
        {
        *  Stream schließen.
        }
        beCloseStream(hbeStream);
      end
      else raise Exception.Create('Beim Initialisieren ist ein Fehler aufgetreten.');
    end;
    

    Die Notation ist nicht konsistent, aber dies ist ja nur eine Demo. Wichtiger ist, dass das Encoden das Auslesen des Laufwerkes ausbremsen kann. Deshalb kann man der Konstante fdwSectors auch einen Faktor verpassen. Die Faktoren 4 bis 10 scheinen bei mir ganz gut zu gehen. Vermutlich sollte man einen Thread zum Encoden verwenden.

    Demo

    Demo, welche sich die Infos von Music Brainz holt und die Tracks ohne Fortschrittsanzeige kopiert und als MP3 speichert:

    CD Copy Step 3 (podCDCopyStep3.7z - 773 kb) inklusive Lame_enc.DLL MD5 (1 kb).
    Compiler: Turbo Delphi
    Stand: 23. Juli 2017

    Änderungen an der Demo

    13.07.2017Änderung: Reservierte Zeichen werden vor dem Speichern aus dem Dateinamen entfernt.
    29.07.2015Änderung: Lesen der CD überarbeitet, Anzahl der auf einmal zu lesenden Sektoren herabgesetzt.
    Änderung: Austausch der Downloadroutine.
    Änderung: Anzeige der DiscID und der ReleaseID.
    Hinzu: Fortschrittsanzeige.
    10.06.2012Fehler: Bei den Helferlein zur Berechnung der Titellänge fehlte eine Klammer.