Dzisiaj przejdziemy do budowy silnika, którego zadaniem będzie odtwarzanie internetowych stacji radiowych. Taki sposób pozwoli na zamknięcie wszystkich składowych odtwarzacza w jednym miejscu i odseparowanie go od innych części aplikacji. Możliwe stanie się również np. bezpośrednie podpięcie silnika pod aplikację konsolową.
Założenia
Każe uruchomienie bądź przełączenie stacji spowoduje utworzenie nowego wątku (z określonej puli). Jego zadaniem będzie próba utworzenia połączenia z serwerem i załadowanie wstępnych danych. Jeżeli operacja się powiedzie, nastąpi płynne przejście z odtwarzania jednej stacji do drugiej. Po pełnym przełączeniu wszystkie nieaktywne wątki zostaną zastopowane i usunięte.
Biblioteka Bass
Do odtwarzania użyjemy biblioteki Bass. Można ją pobrać ze strony www.un4seen.com. Z pobranego archiwum potrzebny jest nam tylko plik biblioteki. Po wypakowaniu umieszczamy go w odpowiednim katalogu:
- Windows – plik bass.dll umieszamy w TinyRadioPlayer\bin\i386-win32\data\lib
- Linux – plik libbass.so umieszczamy w TinyRadioPlayer/bin/x86_64-linux/data/lib
- Mac OSX – plik libbass.dylib umieszczamy w TinyRadioPlayer/bin/i386-darwin/data/lib
Z racji tego, że biblioteka Bass we Free Pascalu powinna zostać załadowana dynamicznie, to zamiast używać, dostępnego w spakowanym archiwum, pliku bass.pas użyjemy lazdynamic_bass.pas. W taki sposób będziemy w stanie dynamicznie załadować plik biblioteki a następnie, przy zamknięciu aplikacji, wyładować go z pamięci. Po pobraniu umieszczamy go w źródłach aplikacji.
Wielowątkowość i zarządzanie pamięcią
W systemie Linux i Mac OSX, aby tworzyć aplikacje wielowątkowe musimy użyć modułu cthreads. Powinien on zostać dodany jako pierwszy moduł projektu w pliku TinyRadioPlayer.lpr.
program TinyRadioPlayer; {$mode objfpc}{$H+} uses {$IFDEF UNIX}{$IFDEF UseCThreads} cthreads, cmem, // the c memory manager is on some systems much faster for multi-threading {$ENDIF}{$ENDIF} {$IFDEF DEBUG} // You can not use the -gh switch with the cmem unit. // The -gh switch uses the heaptrc unit, which extends the heap manager. // Therefore the heaptrc unit must be used after the cmem unit. heaptrc, {$ENDIF} Interfaces, // this includes the LCL widgetset
Dodatkowo, aby zoptymalizować proces zarządzania pamięcią dodajemy moduł cmem. Ta zmiana niestety pociąga za sobą konieczność wyłączenia analizy wycieków pamięci z ustawień trybu Debug. Aby to zrobić otwieramy ustawienia projektu Project -> Project Options… -> Compiler Options -> Debugging i odznaczamy Use Heaptrc unit (check for mem-leaks) (-gh). Zmiany wprowadzamy tylko dla trybu Debug.
Na koniec zostaje tylko dodać dyrektywy kompilatora, czyli UseCThreads i DEBUG. Otwieramy Project -> Project Options… -> Compiler Options -> Custom Options i dla trybu Debug dodajemy -dUseCThreads i -dDEBUG a dla Release -dUseCThreads i -dRELEASE.
Własna klasa wątku
Nową klasę nazwiemy TRadioPlayerThread i utworzymy ją jako rozszerzenie klasy TThread.
TRadioPlayerThread = class(TThread) private FActive: boolean; FThreadIndex: integer; FChannel: HSTREAM; FVolume: integer; // main volume FReq: DWord; FCritSection: TCriticalSection; FChannelStatus: DWord; FStreamUrlToPlay: string; FPlayerMessage: string; FPlayerMessageType: TPlayerMessageType; // floating-point channel support? 0 = no, else yes FFloatable: DWord; // Custom events FOnStreamPlaying: TStreamStatusEvent; FOnStreamStopped: TNotifyEvent; FOnStreamPaused: TNotifyEvent; FOnStreamStalled: TNotifyEvent; FOnStreamGetTags: TStreamGetTagsEvent; procedure DoFadeIn(time: integer = 1000); procedure DoFadeOut(time: integer = 200); procedure OpenURL(url: PChar); procedure StreamStop; procedure MetaStream; procedure SendPlayerMessage(AMessage: string; AMessageType: TPlayerMessageType); procedure CheckBufferProgress; protected procedure Execute; override; procedure SynchronizePlayerMessage; procedure SynchronizeOnStreamPlaying; procedure SynchronizeOnStreamStopped; procedure SynchronizeOnStreamPaused; procedure SynchronizeOnStreamStalled; public constructor Create(CreateSuspended : boolean; Floatable: DWord = 0); destructor Destroy; override; function ErrorGetCode: Integer; function ChannelGetLevel: DWORD; function StreamGetFilePosition(mode: DWORD): QWORD; function ChannelIsActiveAndPlaying: Boolean; function ChannelIsActiveAndPaused: Boolean; function ChannelIsActiveAndStopped: Boolean; function ChannelIsActiveAndStalled: Boolean; function Pause: Boolean; function Stop: Boolean; function ChangeVolume(Value: Integer): Boolean; procedure PlayURL(AStreamUrl: string; const AVolume: Integer; const AThreadIndex: Integer); property OnStreamPlaying: TStreamStatusEvent read FOnStreamPlaying write FOnStreamPlaying; property OnStreamStopped: TNotifyEvent read FOnStreamStopped write FOnStreamStopped; property OnStreamPaused: TNotifyEvent read FOnStreamPaused write FOnStreamPaused; property OnStreamStalled: TNotifyEvent read FOnStreamStalled write FOnStreamStalled; property OnStreamGetTags: TStreamGetTagsEvent read FOnStreamGetTags write FOnStreamGetTags; property Active: boolean read FActive; end;
Implementację i opis wszystkich metod znajdziecie w repozytorium na GitHubie. W tym miejscu chciałbym jedynie przedstawić ogólne założenia. Po utworzeniu wątku zaczynamy oczekiwać na przekazanie adresu url stacji. Jeżeli taki się pojawi to próbujemy go otworzyć. Następnie podczas odtwarzania pobieramy informacje udostępniane przez daną stację. Każdy typ informacji przekazujemy przy pomocy podpiętych zdarzeń. Informujemy również o rozpoczęciu, zakończeniu i wstrzymaniu odtwarzania. Jeżeli zostanie przekazany kolejny url to aktualnie odtwarzana stacja zostaje wyciszona po czym następuje próba uruchomienia kolejnej stacji i stopniowe zwiększenie głośności.
Zarządzanie wątkami
Do zarządzania wątkami utworzymy oddzielną klasę o nazwie TRadioPlayer. Pod spodem będzie zajmować się tworzeniem, przełączaniem i usuwaniem wątków oraz inicjowaniem biblioteki bass oraz jej rozszerzeń. Na zewnątrz zostaną natomiast wystawione metody do zarządzania odtwarzaniem. Będzie naszym zewnętrznym interfejsem silnika.
TRadioPlayer = Class(TObject) private FActiveRadioPlayerThread: integer; FRadioPlayerThreads: array [1..MAX_PLAYER_THREADS] of TRadioPlayerThread; FFloatable: DWord; FThreadWatcher: TTimer; FOnRadioPlayerTags: TRadioPlayerTagsEvent; FOnRadioPlay: TNotifyEvent; procedure Error(msg: string); procedure RadioInit; function LoadBassPlugins: Boolean; procedure RadioPlayerThreadsOnStreamPlaying(ASender: TObject; AThreadIndex: integer); procedure RadioPlayerThreadsStreamGetTags(ASender: TObject; AMessage: string; APlayerMessageType: TPlayerMessageType); procedure ThreadWatcherTimer(Sender: TObject); procedure TerminateThread(ThreadIndex: Integer; TerminateIfNotActive: Boolean); procedure CreateAndLaunchNewThread(ThreadIndex: Integer); public constructor Create; overload; destructor Destroy; override; procedure PlayURL(const AStreamUrl: string; const AVolume: ShortInt); function Stop(): Boolean; procedure Volume(Value: Integer); function ChannelIsActiveAndPlaying: Boolean; function ChannelGetLevel: DWORD; function NumberOfRunningThreads: integer; property OnRadioPlayerTags: TRadioPlayerTagsEvent read FOnRadioPlayerTags write FOnRadioPlayerTags; property OnRadioPlay: TNotifyEvent read FOnRadioPlay write FOnRadioPlay; end;
Maksymalną liczbę wątków MAX_PLAYER_THREADS możemy ustawić na 3. W taki sposób jeden z nich będzie zajmował się odtwarzaniem a dwa pozostałe (jeżeli zajdzie taka potrzeba) ustanawianiem połączenia. Jeżeli jakiś z wątków zwróci informację o rozpoczęciu odtwarzania to płynie się na niego przełączamy a pozostałe wątki zatrzymujemy i usuwamy. Aby sprawdzić, czy przełączanie faktycznie działa możemy zmienić ilość wątków na 1 i kilka razy kliknąć przycisk [Play]. Pomiędzy każdym kliknięciem powinna pojawić się przerwa w odtwarzaniu oznaczająca nawiązywanie połączenia. Jeżeli korzystamy z większej ilości wątków to nawiązywanie połączenia i buforowanie odbywa się w osobnych wątkach co eliminuje chwilę ciszy zastępując ją płynnym przejściem w odtwarzaniu wątków.
Podpięcie silnika pod GUI
Na zakończenie tworzymy instancję klasy silnika i podpinamy zdarzenia pod odpowiednie elementy GUI.
unit MainFormUnit; {$mode objfpc}{$H+} interface uses Classes, SysUtils, FileUtil, BCButton, BGRAFlashProgressBar, BCLabel, Forms, Controls, Graphics, Dialogs, LCLType, StdCtrls, ExtCtrls, Helpers, RadioPlayer, RadioPlayerTypes; type TMainForm = class(TForm) btnStop: TBCButton; pbLeftLevelMeter: TBGRAFlashProgressBar; btnPlay: TBCButton; edtStreamUrl: TEdit; lblInfo1: TLabel; lblInfo2: TLabel; pbRightLevelMeter: TBGRAFlashProgressBar; sbVolume: TScrollBar; Timer1: TTimer; procedure btnPlayClick(Sender: TObject); procedure btnStopClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure RadioPlayerRadioPlay(Sender: TObject); procedure RadioPlayerRadioPlayerTags(AMessage: string; APlayerMessageType: TPlayerMessageType); procedure sbVolumeChange(Sender: TObject); procedure Timer1Timer(Sender: TObject); private public RadioPlayer: TRadioPlayer; end; var MainForm: TMainForm; implementation {$R *.lfm} procedure TMainForm.btnPlayClick(Sender: TObject); begin RadioPlayer.PlayURL(edtStreamUrl.Text, sbVolume.Position); end; procedure TMainForm.btnStopClick(Sender: TObject); begin RadioPlayer.Stop(); end; procedure TMainForm.FormCreate(Sender: TObject); begin MainWindowHandle := Handle; RadioPlayer := TRadioPlayer.Create; RadioPlayer.OnRadioPlayerTags := @RadioPlayerRadioPlayerTags; RadioPlayer.OnRadioPlay := @RadioPlayerRadioPlay; end; procedure TMainForm.FormDestroy(Sender: TObject); begin FreeAndNil(RadioPlayer); end; procedure TMainForm.RadioPlayerRadioPlay(Sender: TObject); begin end; procedure TMainForm.RadioPlayerRadioPlayerTags(AMessage: string; APlayerMessageType: TPlayerMessageType); begin case APlayerMessageType of Connecting: begin lblInfo1.Caption := 'Connecting'; end; Error: begin lblInfo1.Caption := 'Idle: ' + AMessage; end; Progress: begin // Buffering progress end; StreamName: begin lblInfo2.Caption := AMessage; end; Bitrate: begin // bitrate end; StreamTitle: begin // title name, song name lblInfo1.Caption := AMessage; end; Other: begin lblInfo2.Caption := AMessage; end; end; end; procedure TMainForm.sbVolumeChange(Sender: TObject); begin RadioPlayer.Volume(sbVolume.Position); end; procedure TMainForm.Timer1Timer(Sender: TObject); var level: DWORD; begin if RadioPlayer.ChannelIsActiveAndPlaying then begin level := RadioPlayer.ChannelGetLevel; pbLeftLevelMeter.Value := MulDiv(100, LoWord(level), 32768); pbRightLevelMeter.Value := MulDiv(100, HiWord(level), 32768); end else if (pbLeftLevelMeter.Value <> 0) or (pbRightLevelMeter.Value <> 0) then begin pbLeftLevelMeter.Value := 0; pbRightLevelMeter.Value := 0; end; end; end.
Dodatkowo na głównej formie umieszczamy Timer, który co 33 milisekundy będzie pobierał informację o natężeniu dźwięku i prezentował ją na progress barach.
I to już wszystko na dzisiaj. Cały kod aplikacji dostępny jest na GitHubie a zmiany dotyczące tego wpisu znajdziesz tu.
- Tiny Radio Player #01 – Wprowadzenie
- Tiny Radio Player #02 – Instalacja komponentów
- Tiny Radio Player #03 – Roboczy interfejs aplikacji
- Jesteś tu => Tiny Radio Player #04 – Budujemy silnik
- Tiny Radio Player #05 – Zapis ustawień aplikacji
- Tiny Radio Player #06 – Zmiana języka aplikacji
- Tiny Radio Player #07 – Logowanie błędów
- Tiny Radio Player #08 – Baza danych
- Tiny Radio Player #09 – Zarządzanie bazą SQLite
- Tiny Radio Player #10 – Lista stacji radiowych, konfiguracja VirtualTreeView