MeMP - Mein einfacher Mp3-Player

Kapitel 4. Abspielen mit der bass.dll

Grundlage für unseren Player bildet die bass.dll. Wer diese noch nicht runtergeladen hat, sollte das jetzt nachholen. Auch wenn diese Dll recht einfach zu benutzen ist, wird das bei komplexeren Playern recht schnell etwas unübersichtlich. Wir packen daher den ganzen Kram rund um die bass in eine eigene Klasse: TMeMPPlayer. Diese Klasse wird uns auf bequeme Art und Weise Methoden und Eigenschaften bereitstellen, um unseren Player steuern zu können.

Hinweis: Ich weiß, dass es dafür schon fertige Komponenten gibt. Mit denen habe ich mich noch nicht wirklich beschäftigt, und wir werden sehen, dass das auch nicht unbedingt nötig ist. So furchtbar kompliziert ist die bass.dll dann auch nicht.
Außerdem: Wir können so unseren Player ganz unseren Bedürfnissen anpassen, und sind nicht auf die Fähigkeiten einer externen Komponente angewiesen.

Die Player-Klasse

Das wird jetzt etwas umfangreicher. Daher springen wir direkt ins kalte Wasser und schauen uns mal die Deklaration dieser Klasse im Ganzen an.

TMeMPPlayer = class
  private
    fMainStream: DWord;
    fMainVolume: single;
    fMainAudioFile: TAudioFile;
 
    function MeMP_CreateStream(aFilename: String): DWord;
    procedure SetVolume(Value: single);
    function GetTime: Double;
    function GetProgress: Double;
    procedure SetProgress(Value: Double);
    function GetBassStatus: DWord;
  public
    property Volume:     single read fMainVolume write SetVolume;
    property Time:       Double read GetTime;
    property Progress:   Double read GetProgress write SetProgress;
    property BassStatus: DWord  read GetBassStatus;
 
    constructor Create;
    destructor Destroy; override;
    procedure InitBassEngine(HND: HWND);
    procedure Play(aAudioFile: TAudioFile);
    procedure Pause;
    procedure Stop;
    procedure Resume;
    procedure StopAndFree;
end;

Die Bedeutung der einzelnen Variablen, Methoden und Properties wird im folgenden nach und nach erläutert. Ein Teil sollte aber selbst erklärend sein.

Initialisierung der bass.dll

Neben dem obligatorischen Konstruktor und Destruktor (in denen z.B. fMainAudioFile erzeugt bzw. freigegeben werden sollte) ist zunächst die Methode InitBassEngine wichtig. Als Parameter erhält sie ein Fenster-Handle. An dieses Handle werden von Seiten der bass.dll auf Wunsch diverse Messages verschickt, was wir hier aber nicht brauchen. Ohne diese Initialisierung würde unser Player später keinen Mucks von sich geben. Sie schlägt fehl, wenn die bass.dll nicht gefunden werden kann, oder wenn sie in einer anderen Version vorliegt.

procedure TMeMPPlayer.InitBassEngine(HND: HWND);
begin
  if (HIWORD(BASS_GetVersion) <> BASSVERSION) then
    MessageDLG('Bass 2.4 nicht gefunden', mtError, [MBOK], 0);
  BASS_Init(-1, 44100, 0, HND, nil);
end;

Play

Eine der wichtigsten Methoden ist natürlich Play. Diese erhält ein AudioFile als Parameter – eben die Datei, die wir abspielen wollen.

procedure TMeMPPlayer.Play(aAudioFile: TAudioFile);
begin
  if aAudioFile <> NIL then
  begin
    fMainAudioFile.Assign(aAudioFile);
    StopAndFree;
    fMainstream := MeMP_CreateStream(fMainAudioFile.Pfad);
    BASS_ChannelSetAttribute(fMainStream, BASS_ATTRIB_VOL, fMainVolume);
    BASS_ChannelPlay(fMainStream , True);
  end;
end;

Hier wird zunächst das übergebene AudioFile-Objekt kopiert (diese Methode müssen wir unserer AudioFile-Klasse noch hinzufügen). Falls dieses später gelöscht werden sollte (z.B. weil der Anwender die Playlist verändert), ist es im Player immer noch da und verursacht keine Zugriffsverletzungen.

Nachdem wir uns so eine Audiodatei kopiert haben, brechen wir zunächst die laufende Wiedergabe ab. Wir wollen ja immer nur ein Lied gleichzeitig hören.

procedure TMeMPPlayer.StopAndFree;
begin
  BASS_ChannelStop(fMainStream);
  BASS_StreamFree(fMainStream);
  fMainStream := 0;
end;

Zuletzt erzeugen wir einen neuen Stream, setzen dessen Lautstärke auf den gewünschten Wert und starten die Wiedergabe.

function TMeMPPlayer.MeMP_CreateStream(aFilename: String): DWord;
var flags: DWORD;
begin
  if (AnsiLowerCase(ExtractFileExt(aFilename)) = '.mp3') then
    flags := BASS_STREAM_PRESCAN
  else
    flags := 0;
  result := BASS_StreamCreateFile(False, PChar(aFilename), 0, 0, flags);
end;

Wir dröseln hier die Play-Methode in diverse Untermethoden auf, was zunächst nicht unbedingt nötig erscheint. Für Erweiterungen ist es aber durchaus sinnvoll. Man könnte zum Beispiel Parameter einführen, dass die Wiedergabe nicht abrupt beendet wird, sondern langsam ausgeblendet wird. Ebenso könnte man beim Start sanft einblenden. Auch das Erstellen eines Streams bei Webradio sieht etwas anders aus, so dass die Aufteilung sich später als sehr nützlich erweisen kann.

Hinweis zu den Flags: Das Prescan-Flag ist bei mp3-Dateien mit variabler Bitrate nötig. Andernfalls berechnet die bass.dll die Länge des Stückes fehlerhaft, und unsere Fortschrittsanzeige wird durcheinander kommen.
Bei einigen Systemen kann es hier später auch zu seltsamen Effekten kommen. Das lässt sich durch den zusätzlichen Flag BASS_SAMPLE_SOFTWARE beheben, oder durch aktuelle Soundkartentreiber.

Pause, Resume und Stop

Abspielen ist etwas kompliziert, anhalten und weiter fortfahren ganz einfach. Einfach die passende Methode in der Dokumentation der bass suchen und anwenden.

procedure TMeMPPlayer.Pause;
begin
  BASS_ChannelPause(fMainStream);
end;
 
procedure TMeMPPlayer.Stop;
begin
  BASS_ChannelStop(fMainStream);
end;
 
procedure TMeMPPlayer.Resume;
begin
  BASS_ChannelPlay(fMainStream, False);
end;

Auch hier könnte man das Ganze durch sanftes Ein- oder Ausblenden aufwerten. Als Suchbegriff für die Bass-Dokumentation sei an dieser Stelle nur BASS_ChannelSlideAttributes genannt.

Time, Progress und Volume

Auch nichts wildes dabei, der Vollständigkeit halber führen wir die Getter und Setter hier auch an. Nichts weiter als etwas Fehlerkorrektur und Aufrufen der passenden Bass-Funkionen. Ein Getter für die Lautstärke ist dabei nicht notwendig – da lesen wir einfach die private Variable fMainVolume aus.

procedure TMeMPPlayer.SetVolume(Value: single);
begin
  if Value < 0 then Value := 0;
  if Value > 1 then Value := 1;
  fMainVolume := Value;
  BASS_ChannelSetAttribute(fMainStream, BASS_ATTRIB_VOL, fMainVolume);
end;
 
function TMeMPPlayer.GetTime: Double;
begin
  if (fMainStream <> 0) then
    result := BASS_ChannelBytes2Seconds(fMainStream,
                    BASS_ChannelGetPosition(fMainStream, BASS_POS_BYTE))
  else
    result := 0;
end;
 
function TMeMPPlayer.GetProgress: Double;
begin
  if (fMainStream <> 0) then
    result := BASS_ChannelGetPosition(fMainStream, BASS_POS_BYTE) /
                    BASS_ChannelGetLength(fMainStream, BASS_POS_BYTE)
  else
    result := 0;
end;
 
procedure TMeMPPlayer.SetPosition(Value: Longword);
begin
  BASS_ChannelSetPosition(fMainStream, Value, BASS_POS_BYTE);
end;
 
procedure TMeMPPlayer.SetProgress(Value: Double);
begin
  if Value < 0 then Value := 0;
  if Value > 1 then Value := 1;
  BASS_ChannelSetPosition(fMainStream,
        Round(BASS_ChannelGetLength(fMainStream, BASS_POS_BYTE) * Value));
end;

Der Unterschied zwischen Time und Progress sollte eigentlich klar sein. In unserem fertigen Player wollen wir später eine Anzeige haben, an welcher Stelle wir uns im Lied befinden. Dies soll zum einen durch ein Label erfolgen, an dem die aktuelle Zeit abzulesen ist. Zum anderen wollen wir auch eine Art Scrollleiste haben, die uns den relativen Fortschritt anzeigt.

Einen Setter für die Zeit brauchen wir vorerst nicht (es sei denn, wir wollen später eine Funktion wie „Springe zum Anfang der dritten Minute“ realisieren). Den Fortschritt zu ändern, ist aber eine Standard-Funktion in jedem Player. Man packt das sich bewegende Dingens im Fenster mit der Maus an, und schiebt es ein Stückchen nach vorne oder nach hinten.

Status

Zuletzt eine kleine Mini-Funktion, die uns den Status der Bass-Engine nach außen liefert. Hierüber können wir erfahren, ob der Player gerade abspielt, pausiert oder gestoppt ist.

function TMeMPPlayer.GetBassStatus: DWord;
begin
  result := BASS_ChannelIsActive(fMainStream);
end;