unit UAudio_FFMpeg_Pa;
(*******************************************************************************
This unit is primarily based upon -
http://www.dranger.com/ffmpeg/ffmpegtutorial_all.html
and tutorial03.c
http://www.inb.uni-luebeck.de/~boehme/using_libavcodec.html
*******************************************************************************)
interface
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I switches.inc}
uses Classes,
{$IFDEF win32}
windows,
{$ENDIF}
Messages,
SysUtils,
{$IFNDEF FPC}
Forms,
{$ENDIF}
SDL, // Used for Audio output Interface
avcodec, // FFMpeg Audio file decoding
avformat,
avutil,
ULog,
UMusic;
implementation
uses
{$IFDEF LAZARUS}
lclintf,
{$ifndef win32}
libc, // not available in win32
{$endif}
{$ENDIF}
portaudio,
UIni,
UMain,
UThemes;
type
PPacketQueue = ^TPacketQueue;
TPacketQueue = class
private
firstPkt,
lastPkt : PAVPacketList;
nbPackets : integer;
size : integer;
mutex : PSDL_Mutex;
cond : PSDL_Cond;
quit : boolean;
public
constructor Create();
function Put(pkt : PAVPacket): integer;
function Get(var pkt: TAVPacket; block: boolean): integer;
end;
type
TStreamStatus = (sNotReady, sStopped, sPlaying, sSeeking, sPaused, sOpen);
const
StreamStatusStr: array[TStreamStatus] of string = ('Not ready', 'Stopped', 'Playing', 'Seeking', 'Paused', 'Open');
type
PAudioBuffer = ^TAudioBuffer;
TAudioBuffer = array[0 .. (AVCODEC_MAX_AUDIO_FRAME_SIZE * 3 div 2)-1] of byte;
const
SDL_AUDIO_BUFFER_SIZE = 1024;
type
TFFMpegOutputStream = class(TAudioOutputStream)
private
parseThread: PSDL_Thread;
packetQueue: TPacketQueue;
status: TStreamStatus;
// FFMpeg internal data
pFormatCtx : PAVFormatContext;
pCodecCtx : PAVCodecContext;
pCodec : PAVCodec;
ffmpegStreamID : Integer;
ffmpegStream : PAVStream;
// static vars for AudioDecodeFrame
pkt : TAVPacket;
audio_pkt_data : PChar;
audio_pkt_size : integer;
// static vars for AudioCallback
audio_buf_index : cardinal;
audio_buf_size : cardinal;
audio_buf : TAudioBuffer;
channel: PPaStream;
public
constructor Create(); overload;
constructor Create(pFormatCtx: PAVFormatContext;
pCodecCtx: PAVCodecContext; pCodec: PAVCodec;
ffmpegStreamID : Integer; ffmpegStream: PAVStream); overload;
procedure Play();
procedure Pause();
procedure Stop();
procedure Close();
function AudioDecodeFrame(buffer : PUInt8; bufSize: integer): integer;
end;
type
TAudio_FFMpeg = class( TInterfacedObject, IAudioPlayback )
private
MusicStream: TFFMpegOutputStream;
StartSoundStream: TFFMpegOutputStream;
BackSoundStream: TFFMpegOutputStream;
SwooshSoundStream: TFFMpegOutputStream;
ChangeSoundStream: TFFMpegOutputStream;
OptionSoundStream: TFFMpegOutputStream;
ClickSoundStream: TFFMpegOutputStream;
DrumSoundStream: TFFMpegOutputStream;
HihatSoundStream: TFFMpegOutputStream;
ClapSoundStream: TFFMpegOutputStream;
ShuffleSoundStream: TFFMpegOutputStream;
//Custom Sounds
CustomSounds: array of TCustomSoundEntry;
Loaded: boolean;
Loop: boolean;
function FindAudioStreamID(pFormatCtx : PAVFormatContext): integer;
public
function GetName: String;
procedure InitializePlayback;
procedure SetVolume(Volume: integer);
procedure SetMusicVolume(Volume: integer);
procedure SetLoop(Enabled: boolean);
function Open(Name: string): boolean; // true if succeed
procedure Rewind;
procedure MoveTo(Time: real);
procedure Play;
procedure Pause; //Pause Mod
procedure Stop;
procedure Close;
function Finished: boolean;
function Length: real;
function getPosition: real;
procedure PlayStart;
procedure PlayBack;
procedure PlaySwoosh;
procedure PlayChange;
procedure PlayOption;
procedure PlayClick;
procedure PlayDrum;
procedure PlayHihat;
procedure PlayClap;
procedure PlayShuffle;
procedure StopShuffle;
function LoadSoundFromFile(Name: string): TFFMpegOutputStream;
//Equalizer
function GetFFTData: TFFTData;
// Interface for Visualizer
function GetPCMData(var data: TPCMData): Cardinal;
//Custom Sounds
function LoadCustomSound(const Filename: String): Cardinal;
procedure PlayCustomSound(const Index: Cardinal );
end;
var
test: TFFMpegOutputStream;
it: integer;
var
singleton_MusicFFMpeg : IAudioPlayback = nil;
function ParseAudio(data: Pointer): integer; cdecl; forward;
procedure SDL_AudioCallback( userdata: Pointer; stream: PUInt8; len: Integer ); cdecl; forward;
function Pa_AudioCallback(input: Pointer; output: Pointer; frameCount: Longword;
timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
userData: Pointer): Integer; cdecl; forward;
constructor TFFMpegOutputStream.Create();
begin
inherited;
packetQueue := TPacketQueue.Create();
FillChar(pkt, sizeof(TAVPacket), #0);
status := sStopped;
audio_pkt_data := nil;
audio_pkt_size := 0;
audio_buf_index := 0;
audio_buf_size := 0;
end;
constructor TFFMpegOutputStream.Create(pFormatCtx: PAVFormatContext;
pCodecCtx: PAVCodecContext; pCodec: PAVCodec;
ffmpegStreamID : Integer; ffmpegStream: PAVStream);
begin
Create();
Self.pFormatCtx := pFormatCtx;
Self.pCodecCtx := pCodecCtx;
Self.pCodec := pCodec;
Self.ffmpegStreamID := ffmpegStreamID;
Self.ffmpegStream := ffmpegStream;
test := Self;
it:=0;
end;
procedure TFFMpegOutputStream.Play();
var
err: TPaError;
begin
writeln('Play request');
if(status = sStopped) then
begin
writeln('Play ok');
status := sPlaying;
Self.parseThread := SDL_CreateThread(@ParseAudio, Self);
//SDL_PauseAudio(0);
err := Pa_StartStream(Self.channel);
end;
end;
procedure TFFMpegOutputStream.Pause();
begin
end;
procedure TFFMpegOutputStream.Stop();
begin
end;
procedure TFFMpegOutputStream.Close();
begin
// Close the codec
avcodec_close(pCodecCtx);
// Close the video file
av_close_input_file(pFormatCtx);
end;
function TAudio_FFMpeg.GetName: String;
begin
result := 'FFMpeg';
end;
procedure TAudio_FFMpeg.InitializePlayback;
begin
Log.LogStatus('InitializePlayback', 'UAudio_FFMpeg');
Loaded := false;
Loop := false;
av_register_all();
//SDL_Init(SDL_INIT_AUDIO);
Pa_Initialize();
StartSoundStream := LoadSoundFromFile(SoundPath + 'Common start.mp3');
{
BackSoundStream := LoadSoundFromFile(SoundPath + 'Common Back.mp3');
SwooshSoundStream := LoadSoundFromFile(SoundPath + 'menu swoosh.mp3');
ChangeSoundStream := LoadSoundFromFile(SoundPath + 'select music change music 50.mp3');
OptionSoundStream := LoadSoundFromFile(SoundPath + 'option change col.mp3');
ClickSoundStream := LoadSoundFromFile(SoundPath + 'rimshot022b.mp3');
}
// DrumSoundStream := LoadSoundFromFile(SoundPath + 'bassdrumhard076b.mp3');
// HihatSoundStream := LoadSoundFromFile(SoundPath + 'hihatclosed068b.mp3');
// ClapSoundStream := LoadSoundFromFile(SoundPath + 'claps050b.mp3');
// ShuffleSoundStream := LoadSoundFromFile(SoundPath + 'Shuffle.mp3');
end;
procedure TAudio_FFMpeg.SetVolume(Volume: integer);
begin
//New: Sets Volume only for this Application
(*
BASS_SetConfig(BASS_CONFIG_GVOL_SAMPLE, Volume);
BASS_SetConfig(BASS_CONFIG_GVOL_STREAM, Volume);
BASS_SetConfig(BASS_CONFIG_GVOL_MUSIC, Volume);
*)
end;
procedure TAudio_FFMpeg.SetMusicVolume(Volume: Integer);
begin
//Max Volume Prevention
if Volume > 100 then
Volume := 100;
if Volume < 0 then
Volume := 0;
//Set Volume
// BASS_ChannelSetAttributes (Bass, -1, Volume, -101);
end;
procedure TAudio_FFMpeg.SetLoop(Enabled: boolean);
begin
Loop := Enabled;
end;
function TAudio_FFMpeg.Open(Name: string): boolean;
begin
Loaded := false;
if FileExists(Name) then
begin
// Bass := Bass_StreamCreateFile(false, pchar(Name), 0, 0, 0);
Loaded := true;
//Set Max Volume
SetMusicVolume (100);
end;
Result := Loaded;
end;
procedure TAudio_FFMpeg.Rewind;
begin
if Loaded then
begin
end;
end;
procedure TAudio_FFMpeg.MoveTo(Time: real);
var
bytes: integer;
begin
// bytes := BASS_ChannelSeconds2Bytes(Bass, Time);
// BASS_ChannelSetPosition(Bass, bytes);
end;
procedure TAudio_FFMpeg.Play;
begin
if MusicStream <> nil then
if Loaded then
begin
if Loop then
begin
end;
// start from beginning...
// actually bass itself does not loop, nor does this TAudio_FFMpeg Class
MusicStream.Play();
end;
end;
procedure TAudio_FFMpeg.Pause; //Pause Mod
begin
if MusicStream <> nil then
if Loaded then begin
MusicStream.Pause(); // Pauses Song
end;
end;
procedure TAudio_FFMpeg.Stop;
begin
if MusicStream <> nil then
MusicStream.Stop();
end;
procedure TAudio_FFMpeg.Close;
begin
if MusicStream <> nil then
MusicStream.Close();
end;
function TAudio_FFMpeg.Length: real;
var
bytes: integer;
begin
Result := 0;
// Todo : why is Music stream always nil !?
if assigned( MusicStream ) then
begin
Result := MusicStream.pFormatCtx^.duration / AV_TIME_BASE;
end;
end;
function TAudio_FFMpeg.getPosition: real;
var
bytes: integer;
begin
Result := 0;
(*
bytes := BASS_ChannelGetPosition(BASS);
Result := BASS_ChannelBytes2Seconds(BASS, bytes);
*)
end;
function TAudio_FFMpeg.Finished: boolean;
begin
Result := false;
(*
if BASS_ChannelIsActive(BASS) = BASS_ACTIVE_STOPPED then
begin
Result := true;
end;
*)
end;
procedure TAudio_FFMpeg.PlayStart;
begin
if StartSoundStream <> nil then
StartSoundStream.Play();
end;
procedure TAudio_FFMpeg.PlayBack;
begin
if BackSoundStream <> nil then
BackSoundStream.Play();
end;
procedure TAudio_FFMpeg.PlaySwoosh;
begin
if SwooshSoundStream <> nil then
SwooshSoundStream.Play();
end;
procedure TAudio_FFMpeg.PlayChange;
begin
if ChangeSoundStream <> nil then
ChangeSoundStream.Play();
end;
procedure TAudio_FFMpeg.PlayOption;
begin
if OptionSoundStream <> nil then
OptionSoundStream.Play();
end;
procedure TAudio_FFMpeg.PlayClick;
begin
if ClickSoundStream <> nil then
ClickSoundStream.Play();
end;
procedure TAudio_FFMpeg.PlayDrum;
begin
if DrumSoundStream <> nil then
DrumSoundStream.Play();
end;
procedure TAudio_FFMpeg.PlayHihat;
begin
if HihatSoundStream <> nil then
HihatSoundStream.Play();
end;
procedure TAudio_FFMpeg.PlayClap;
begin
if ClapSoundStream <> nil then
ClapSoundStream.Play();
end;
procedure TAudio_FFMpeg.PlayShuffle;
begin
if ShuffleSoundStream <> nil then
ShuffleSoundStream.Play();
end;
procedure TAudio_FFMpeg.StopShuffle;
begin
if ShuffleSoundStream <> nil then
ShuffleSoundStream.Stop();
end;
function TFFMpegOutputStream.AudioDecodeFrame(buffer : PUInt8; bufSize: integer): integer;
var
len1,
data_size: integer;
begin
result := -1;
if (buffer = nil) then
exit;
while true do
begin
while (audio_pkt_size > 0) do
begin
// writeln( 'got audio packet' );
data_size := bufSize;
if(pCodecCtx = nil) then begin
// writeln('Das wars');
exit;
end;
// TODO: should be avcodec_decode_audio2 but this wont link on my ubuntu box.
len1 := avcodec_decode_audio(pCodecCtx, Pointer(buffer),
data_size, audio_pkt_data, audio_pkt_size);
//writeln('avcodec_decode_audio : ' + inttostr( len1 ));
if(len1 < 0) then
begin
// if error, skip frame
// writeln( 'Skip audio frame' );
audio_pkt_size := 0;
break;
end;
audio_pkt_data := audio_pkt_data + len1;
audio_pkt_size := audio_pkt_size + len1;
if (data_size <= 0) then
begin
// No data yet, get more frames
continue;
end;
// We have data, return it and come back for more later
result := data_size;
exit;
end;
inc(it);
if (pkt.data <> nil) then
begin
av_free_packet(pkt);
end;
if (packetQueue.quit) then
begin
result := -1;
exit;
end;
// writeln(it);
if (packetQueue.Get(pkt, true) < 0) then
begin
result := -1;
exit;
end;
audio_pkt_data := PChar(pkt.data);
audio_pkt_size := pkt.size;
// writeln( 'Audio Packet Size - ' + inttostr(audio_pkt_size) );
end;
end;
procedure SDL_AudioCallback(userdata: Pointer; stream: PUInt8; len: Integer); cdecl;
var
outStream : TFFMpegOutputStream;
len1,
audio_size : integer;
pSrc : Pointer;
begin
outStream := TFFMpegOutputStream(userdata);
while (len > 0) do
with outStream do begin
if (audio_buf_index >= audio_buf_size) then
begin
// We have already sent all our data; get more
audio_size := AudioDecodeFrame(@audio_buf, sizeof(TAudioBuffer));
//writeln('audio_decode_frame : '+ inttostr(audio_size));
if(audio_size < 0) then
begin
// If error, output silence
audio_buf_size := 1024;
FillChar(audio_buf, audio_buf_size, #0);
//writeln( 'Silence' );
end
else
begin
audio_buf_size := audio_size;
end;
audio_buf_index := 0; // Todo : jb - SegFault ?
end;
len1 := audio_buf_size - audio_buf_index;
if (len1 > len) then
len1 := len;
pSrc := PChar(@audio_buf) + audio_buf_index;
{$ifdef WIN32}
CopyMemory(stream, pSrc , len1);
{$else}
memcpy(stream, pSrc , len1);
{$endif}
Dec(len, len1);
Inc(stream, len1);
Inc(audio_buf_index, len1);
end;
end;
function Pa_AudioCallback(input: Pointer; output: Pointer; frameCount: Longword;
timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
userData: Pointer): Integer; cdecl;
var
outStream : TFFMpegOutputStream;
len1,
audio_size : integer;
pSrc : Pointer;
len : integer;
begin
outStream := TFFMpegOutputStream(userData);
len := frameCount * 2; // use *2 for mono-files
while (len > 0) do
with outStream do begin
if (audio_buf_index >= audio_buf_size) then
begin
// We have already sent all our data; get more
audio_size := AudioDecodeFrame(@audio_buf, sizeof(TAudioBuffer));
//writeln('audio_decode_frame : '+ inttostr(audio_size));
if(audio_size < 0) then
begin
// If error, output silence
audio_buf_size := 1024;
FillChar(audio_buf, audio_buf_size, #0);
//writeln( 'Silence' );
end
else
begin
audio_buf_size := audio_size;
end;
audio_buf_index := 0; // Todo : jb - SegFault ?
end;
len1 := audio_buf_size - audio_buf_index;
if (len1 > len) then
len1 := len;
pSrc := PChar(@audio_buf) + audio_buf_index;
{$ifdef WIN32}
CopyMemory(output, pSrc , len1);
{$else}
memcpy(output, pSrc , len1);
{$endif}
Dec(len, len1);
Inc(PChar(output), len1);
Inc(audio_buf_index, len1);
end;
result := paContinue;
end;
function TAudio_FFMpeg.FindAudioStreamID(pFormatCtx : PAVFormatContext): integer;
var
i : integer;
streamID: integer;
stream : PAVStream;
begin
// Find the first audio stream
streamID := -1;
for i := 0 to pFormatCtx^.nb_streams-1 do
begin
//Log.LogStatus('aFormatCtx.streams[i] : ' + inttostr(i), 'UAudio_FFMpeg');
stream := pFormatCtx^.streams[i];
if ( stream.codec^.codec_type = CODEC_TYPE_AUDIO ) then
begin
Log.LogStatus('Found Audio Stream', 'UAudio_FFMpeg');
streamID := i;
break;
end;
end;
result := streamID;
end;
function ParseAudio(data: Pointer): integer; cdecl;
var
packet: TAVPacket;
stream: TFFMpegOutputStream;
begin
stream := TFFMpegOutputStream(data);
while (av_read_frame(stream.pFormatCtx, packet) >= 0) do
begin
//writeln( 'ffmpeg - av_read_frame' );
if (packet.stream_index = stream.ffmpegStreamID) then
begin
//writeln( 'packet_queue_put' );
stream.packetQueue.put(@packet);
end
else
begin
av_free_packet(packet);
end;
end;
//Writeln('Done: ' + inttostr(stream.packetQueue.nbPackets));
result := 0;
end;
function TAudio_FFMpeg.LoadSoundFromFile(Name: string): TFFMpegOutputStream;
var
pFormatCtx : PAVFormatContext;
pCodecCtx : PAVCodecContext;
pCodec : PAVCodec;
ffmpegStreamID : Integer;
ffmpegStream : PAVStream;
wanted_spec,
spec : TSDL_AudioSpec;
csIndex : integer;
stream : TFFMpegOutputStream;
err : TPaError;
begin
result := nil;
if (not FileExists(Name)) then
begin
Log.LogStatus('LoadSoundFromFile: Sound not found "' + Name + '"', 'UAudio_FFMpeg');
writeln('ERROR : LoadSoundFromFile: Sound not found "' + Name + '"', 'UAudio_FFMpeg');
exit;
end;
// Open audio file
if (av_open_input_file(pFormatCtx, PChar(Name), nil, 0, nil) > 0) then
exit;
// Retrieve stream information
if (av_find_stream_info(pFormatCtx) < 0) then
exit;
dump_format(pFormatCtx, 0, pchar(Name), 0);
ffmpegStreamID := FindAudioStreamID(pFormatCtx);
if (ffmpegStreamID < 0) then
exit;
//Log.LogStatus('Audio Stream ID is : '+ inttostr(ffmpegStreamID), 'UAudio_FFMpeg');
ffmpegStream := pFormatCtx.streams[ffmpegStreamID];
pCodecCtx := ffmpegStream^.codec;
pCodec := avcodec_find_decoder(pCodecCtx^.codec_id);
if (pCodec = nil) then
begin
Log.LogStatus('Unsupported codec!', 'UAudio_FFMpeg');
exit;
end;
avcodec_open(pCodecCtx, pCodec);
//writeln( 'Opened the codec' );
stream := TFFMpegOutputStream.Create(pFormatCtx, pCodecCtx, pCodec,
ffmpegStreamID, ffmpegStream);
{
// Set SDL audio settings from codec info
wanted_spec.freq := pCodecCtx^.sample_rate;
wanted_spec.format := AUDIO_S16SYS;
wanted_spec.channels := pCodecCtx^.channels;
wanted_spec.silence := 0;
wanted_spec.samples := SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback := AudioCallback;
wanted_spec.userdata := stream;
// TODO: this works only one time (?)
if (SDL_OpenAudio(@wanted_spec, @spec) < 0) then
begin
Log.LogStatus('SDL_OpenAudio: '+SDL_GetError(), 'UAudio_FFMpeg');
stream.Free();
exit;
end;
}
err := Pa_OpenDefaultStream(stream.channel, 0, pCodecCtx^.channels, paInt16,
pCodecCtx^.sample_rate, SDL_AUDIO_BUFFER_SIZE div 2, //paFramesPerBufferUnspecified,
@PA_AudioCallback, stream);
if(err <> paNoError) then begin
Log.LogStatus('Pa_OpenDefaultStream: '+Pa_GetErrorText(err), 'UAudio_FFMpeg');
stream.Free();
exit;
end;
Log.LogStatus('SDL opened audio device', 'UAudio_FFMpeg');
//Add CustomSound
csIndex := High(CustomSounds) + 1;
SetLength (CustomSounds, csIndex + 1);
CustomSounds[csIndex].Filename := Name;
CustomSounds[csIndex].Stream := stream;
result := stream;
end;
//Equalizer
function TAudio_FFMpeg.GetFFTData: TFFTData;
var
data: TFFTData;
begin
//Get Channel Data Mono and 256 Values
// BASS_ChannelGetData(Bass, @Result, BASS_DATA_FFT512);
result := data;
end;
// Interface for Visualizer
function TAudio_FFMpeg.GetPCMData(var data: TPCMData): Cardinal;
begin
result := 0;
end;
function TAudio_FFMpeg.LoadCustomSound(const Filename: String): Cardinal;
var
S: TFFMpegOutputStream;
I: Integer;
F: String;
begin
//Search for Sound in already loaded Sounds
F := UpperCase(SoundPath + FileName);
For I := 0 to High(CustomSounds) do
begin
if (UpperCase(CustomSounds[I].Filename) = F) then
begin
Result := I;
Exit;
end;
end;
S := LoadSoundFromFile(SoundPath + Filename);
if (S <> nil) then
Result := High(CustomSounds)
else
Result := 0;
end;
procedure TAudio_FFMpeg.PlayCustomSound(const Index: Cardinal );
begin
if Index <= High(CustomSounds) then
(CustomSounds[Index].Stream as TFFMpegOutputStream).Play();
end;
constructor TPacketQueue.Create();
begin
inherited;
firstPkt := nil;
lastPkt := nil;
nbPackets := 0;
size := 0;
mutex := SDL_CreateMutex();
cond := SDL_CreateCond();
end;
function TPacketQueue.Put(pkt : PAVPacket): integer;
var
pkt1 : PAVPacketList;
begin
result := -1;
if (av_dup_packet(pkt) < 0) then
exit;
pkt1 := av_malloc(sizeof(TAVPacketList));
if (pkt1 = nil) then
exit;
pkt1^.pkt := pkt^;
pkt1^.next := nil;
SDL_LockMutex(Self.mutex);
try
if (Self.lastPkt = nil) then
Self.firstPkt := pkt1
else
Self.lastPkt^.next := pkt1;
Self.lastPkt := pkt1;
inc(Self.nbPackets);
// Writeln('Put: ' + inttostr(nbPackets));
Self.size := Self.size + pkt1^.pkt.size;
SDL_CondSignal(Self.cond);
finally
SDL_UnlockMutex(Self.mutex);
end;
result := 0;
end;
function TPacketQueue.Get(var pkt: TAVPacket; block: boolean): integer;
var
pkt1 : PAVPacketList;
begin
result := -1;
SDL_LockMutex(Self.mutex);
try
while true do
begin
if (quit) then
exit;
pkt1 := Self.firstPkt;
if (pkt1 <> nil) then
begin
Self.firstPkt := pkt1.next;
if (Self.firstPkt = nil) then
Self.lastPkt := nil;
dec(Self.nbPackets);
// Writeln('Get: ' + inttostr(nbPackets));
Self.size := Self.size - pkt1^.pkt.size;
pkt := pkt1^.pkt;
av_free(pkt1);
result := 1;
break;
end
else
if (not block) then
begin
result := 0;
break;
end
else
begin
SDL_CondWait(Self.cond, Self.mutex);
end;
end;
finally
SDL_UnlockMutex(Self.mutex);
end;
end;
initialization
singleton_MusicFFMpeg := TAudio_FFMpeg.create();
writeln( 'UAudio_FFMpeg - Register Playback' );
AudioManager.add( IAudioPlayback( singleton_MusicFFMpeg ) );
finalization
AudioManager.Remove( IAudioPlayback( singleton_MusicFFMpeg ) );
end.