{* UltraStar Deluxe - Karaoke Game
*
* UltraStar Deluxe is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
* $URL$
* $Id$
*}
unit UAudioDecoder_FFmpeg;
(*******************************************************************************
*
* 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}
// show FFmpeg specific debug output
{.$DEFINE DebugFFmpegDecode}
// FFmpeg is very verbose and shows a bunch of errors.
// Those errors (they can be considered as warnings by us) can be ignored
// as they do not give any useful information.
// There is no solution to fix this except for turning them off.
{.$DEFINE EnableFFmpegErrorOutput}
implementation
uses
SDL, // SDL redefines some base types -> include before SysUtils to ignore them
Classes,
Math,
SysUtils,
avcodec,
avformat,
avutil,
avio,
mathematics, // used for av_rescale_q
rational,
UMusic,
UIni,
UMain,
UMediaCore_FFmpeg,
ULog,
UCommon,
UConfig,
UPath;
const
MAX_AUDIOQ_SIZE = (5 * 16 * 1024);
const
// TODO: The factor 3/2 might not be necessary as we do not need extra
// space for synchronizing as in the tutorial.
AUDIO_BUFFER_SIZE = (AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) div 2;
type
TFFmpegDecodeStream = class(TAudioDecodeStream)
private
StateLock: PSDL_Mutex;
EOFState: boolean; // end-of-stream flag (locked by StateLock)
ErrorState: boolean; // error flag (locked by StateLock)
QuitRequest: boolean; // (locked by StateLock)
ParserIdleCond: PSDL_Cond;
// parser pause/resume data
ParserLocked: boolean;
ParserPauseRequestCount: integer;
ParserUnlockedCond: PSDL_Cond;
ParserResumeCond: PSDL_Cond;
SeekRequest: boolean; // (locked by StateLock)
SeekFlags: integer; // (locked by StateLock)
SeekPos: double; // stream position to seek for (in secs) (locked by StateLock)
SeekFlush: boolean; // true if the buffers should be flushed after seeking (locked by StateLock)
SeekFinishedCond: PSDL_Cond;
Loop: boolean; // (locked by StateLock)
ParseThread: PSDL_Thread;
PacketQueue: TPacketQueue;
FormatInfo: TAudioFormatInfo;
// FFmpeg specific data
FormatCtx: PAVFormatContext;
CodecCtx: PAVCodecContext;
Codec: PAVCodec;
AudioStreamIndex: integer;
AudioStream: PAVStream;
AudioStreamPos: double; // stream position in seconds (locked by DecoderLock)
// decoder pause/resume data
DecoderLocked: boolean;
DecoderPauseRequestCount: integer;
DecoderUnlockedCond: PSDL_Cond;
DecoderResumeCond: PSDL_Cond;
// state-vars for DecodeFrame (locked by DecoderLock)
AudioPaket: TAVPacket;
AudioPaketData: PByteArray;
AudioPaketSize: integer;
AudioPaketSilence: integer; // number of bytes of silence to return
// state-vars for AudioCallback (locked by DecoderLock)
AudioBufferPos: integer;
AudioBufferSize: integer;
AudioBuffer: PByteArray;
Filename: IPath;
procedure SetPositionIntern(Time: real; Flush: boolean; Blocking: boolean);
procedure SetEOF(State: boolean); {$IFDEF HasInline}inline;{$ENDIF}
procedure SetError(State: boolean); {$IFDEF HasInline}inline;{$ENDIF}
function IsSeeking(): boolean;
function IsQuit(): boolean;
procedure Reset();
procedure Parse();
function ParseLoop(): boolean;
procedure PauseParser();
procedure ResumeParser();
function DecodeFrame(Buffer: PByteArray; BufferSize: integer): integer;
procedure FlushCodecBuffers();
procedure PauseDecoder();
procedure ResumeDecoder();
public
constructor Create();
destructor Destroy(); override;
function Open(const Filename: IPath): boolean;
procedure Close(); override;
function GetLength(): real; override;
function GetAudioFormatInfo(): TAudioFormatInfo; override;
function GetPosition: real; override;
procedure SetPosition(Time: real); override;
function GetLoop(): boolean; override;
procedure SetLoop(Enabled: boolean); override;
function IsEOF(): boolean; override;
function IsError(): boolean; override;
function ReadData(Buffer: PByteArray; BufferSize: integer): integer; override;
end;
type
TAudioDecoder_FFmpeg = class(TInterfacedObject, IAudioDecoder)
public
function GetName: string;
function InitializeDecoder(): boolean;
function FinalizeDecoder(): boolean;
function Open(const Filename: IPath): TAudioDecodeStream;
end;
var
FFmpegCore: TMediaCore_FFmpeg;
function ParseThreadMain(Data: Pointer): integer; cdecl; forward;
{ TFFmpegDecodeStream }
constructor TFFmpegDecodeStream.Create();
begin
inherited Create();
StateLock := SDL_CreateMutex();
ParserUnlockedCond := SDL_CreateCond();
ParserResumeCond := SDL_CreateCond();
ParserIdleCond := SDL_CreateCond();
SeekFinishedCond := SDL_CreateCond();
DecoderUnlockedCond := SDL_CreateCond();
DecoderResumeCond := SDL_CreateCond();
// according to the documentation of avcodec_decode_audio(2), sample-data
// should be aligned on a 16 byte boundary. Otherwise internal calls
// (e.g. to SSE or Altivec operations) might fail or lack performance on some
// CPUs. Although GetMem() in Delphi and FPC seems to use a 16 byte or higher
// alignment for buffers of this size (alignment depends on the size of the
// requested buffer), we will set the alignment explicitly as the minimum
// alignment used by Delphi and FPC is on an 8 byte boundary.
//
// Note: AudioBuffer was previously defined as a field of type TAudioBuffer
// (array[0..AUDIO_BUFFER_SIZE-1] of byte) and hence statically allocated.
// Fields of records are aligned different to memory allocated with GetMem(),
// aligning depending on the type but will be at least 2 bytes.
// AudioBuffer was not aligned to a 16 byte boundary. The {$ALIGN x} directive
// was not applicable as Delphi in contrast to FPC provides at most 8 byte
// alignment ({$ALIGN 16} is not supported) by this directive.
AudioBuffer := GetAlignedMem(AUDIO_BUFFER_SIZE, 16);
Reset();
end;
procedure TFFmpegDecodeStream.Reset();
begin
ParseThread := nil;
EOFState := false;
ErrorState := false;
Loop := false;
QuitRequest := false;
AudioPaketData := nil;
AudioPaketSize := 0;
AudioPaketSilence := 0;
AudioBufferPos := 0;
AudioBufferSize := 0;
ParserLocked := false;
ParserPauseRequestCount := 0;
DecoderLocked := false;
DecoderPauseRequestCount := 0;
FillChar(AudioPaket, SizeOf(TAVPacket), 0);
end;
{*
* Frees the decode-stream data.
*}
destructor TFFmpegDecodeStream.Destroy();
begin
Close();
SDL_DestroyMutex(StateLock);
SDL_DestroyCond(ParserUnlockedCond);
SDL_DestroyCond(ParserResumeCond);
SDL_DestroyCond(ParserIdleCond);
SDL_DestroyCond(SeekFinishedCond);
SDL_DestroyCond(DecoderUnlockedCond);
SDL_DestroyCond(DecoderResumeCond);
FreeAlignedMem(AudioBuffer);
inherited;
end;
function TFFmpegDecodeStream.Open(const Filename: IPath): boolean;
var
SampleFormat: TAudioSampleFormat;
AVResult: integer;
begin
Result := false;
Close();
Reset();
if (not Filename.IsFile) then
begin
Log.LogError('Audio-file does not exist: "' + Filename.ToNative + '"', 'UAudio_FFmpeg');
Exit;
end;
Self.Filename := Filename;
// use custom 'ufile' protocol for UTF-8 support
if (av_open_input_file(FormatCtx, PAnsiChar('ufile:'+FileName.ToUTF8), nil, 0, nil) <> 0) then
begin
Log.LogError('av_open_input_file failed: "' + Filename.ToNative + '"', 'UAudio_FFmpeg');
Exit;
end;
// generate PTS values if they do not exist
FormatCtx^.flags := FormatCtx^.flags or AVFMT_FLAG_GENPTS;
// retrieve stream information
if (av_find_stream_info(FormatCtx) < 0) then
begin
Log.LogError('av_find_stream_info failed: "' + Filename.ToNative + '"', 'UAudio_FFmpeg');
Close();
Exit;
end;
// FIXME: hack used by ffplay. Maybe should not use url_feof() to test for the end
FormatCtx^.pb.eof_reached := 0;
{$IFDEF DebugFFmpegDecode}
dump_format(FormatCtx, 0, PAnsiChar(Filename.ToNative), 0);
{$ENDIF}
AudioStreamIndex := FFmpegCore.FindAudioStreamIndex(FormatCtx);
if (AudioStreamIndex < 0) then
begin
Log.LogError('FindAudioStreamIndex: No Audio-stream found "' + Filename.ToNative + '"', 'UAudio_FFmpeg');
Close();
Exit;
end;
//Log.LogStatus('AudioStreamIndex is: '+ inttostr(ffmpegStreamID), 'UAudio_FFmpeg');
AudioStream := FormatCtx.streams[AudioStreamIndex];
AudioStreamPos := 0;
CodecCtx := AudioStream^.codec;
// TODO: should we use this or not? Should we allow 5.1 channel audio?
(*
{$IF LIBAVCODEC_VERSION >= 51042000}
if (CodecCtx^.channels > 0) then
CodecCtx^.request_channels := Min(2, CodecCtx^.channels)
else
CodecCtx^.request_channels := 2;
{$IFEND}
*)
Codec := avcodec_find_decoder(CodecCtx^.codec_id);
if (Codec = nil) then
begin
Log.LogError('Unsupported codec!', 'UAudio_FFmpeg');
CodecCtx := nil;
Close();
Exit;
end;
// set debug options
CodecCtx^.debug_mv := 0;
CodecCtx^.debug := 0;
// detect bug-workarounds automatically
CodecCtx^.workaround_bugs := FF_BUG_AUTODETECT;
// error resilience strategy (careful/compliant/agressive/very_aggressive)
//CodecCtx^.error_resilience := FF_ER_CAREFUL; //FF_ER_COMPLIANT;
// allow non spec compliant speedup tricks.
//CodecCtx^.flags2 := CodecCtx^.flags2 or CODEC_FLAG2_FAST;
// Note: avcodec_open() and avcodec_close() are not thread-safe and will
// fail if called concurrently by different threads.
FFmpegCore.LockAVCodec();
try
AVResult := avcodec_open(CodecCtx, Codec);
finally
FFmpegCore.UnlockAVCodec();
end;
if (AVResult < 0) then
begin
Log.LogError('avcodec_open failed!', 'UAudio_FFmpeg');
Close();
Exit;
end;
// now initialize the audio-format
if (not FFmpegCore.ConvertFFmpegToAudioFormat(CodecCtx^.sample_fmt, SampleFormat)) then
begin
// try standard format
SampleFormat := asfS16;
end;
if CodecCtx^.channels > 255 then
Log.LogStatus('Error: CodecCtx^.channels > 255', 'TFFmpegDecodeStream.Open');
FormatInfo := TAudioFormatInfo.Create(
byte(CodecCtx^.channels),
CodecCtx^.sample_rate,
SampleFormat
);
PacketQueue := TPacketQueue.Create();
// finally start the decode thread
ParseThread := SDL_CreateThread(@ParseThreadMain, Self);
Result := true;
end;
procedure TFFmpegDecodeStream.Close();
var
ThreadResult: integer;
begin
// wake threads waiting for packet-queue data
// Note: normally, there are no waiting threads. If there were waiting
// ones, they would block the audio-callback thread.
if (assigned(PacketQueue)) then
PacketQueue.Abort();
// send quit request (to parse-thread etc)
SDL_mutexP(StateLock);
QuitRequest := true;
SDL_CondBroadcast(ParserIdleCond);
SDL_mutexV(StateLock);
// abort parse-thread
if (ParseThread <> nil) then
begin
// and wait until it terminates
SDL_WaitThread(ParseThread, ThreadResult);
ParseThread := nil;
end;
// Close the codec
if (CodecCtx <> nil) then
begin
// avcodec_close() is not thread-safe
FFmpegCore.LockAVCodec();
try
avcodec_close(CodecCtx);
finally
FFmpegCore.UnlockAVCodec();
end;
CodecCtx := nil;
end;
// Close the video file
if (FormatCtx <> nil) then
begin
av_close_input_file(FormatCtx);
FormatCtx := nil;
end;
PerformOnClose();
FreeAndNil(PacketQueue);
FreeAndNil(FormatInfo);
end;
function TFFmpegDecodeStream.GetLength(): real;
begin
// do not forget to consider the start_time value here
// there is a type size mismatch warnign because start_time and duration are cint64.
// So, in principle there could be an overflow when doing the sum.
Result := (FormatCtx^.start_time + FormatCtx^.duration) / AV_TIME_BASE;
end;
function TFFmpegDecodeStream.GetAudioFormatInfo(): TAudioFormatInfo;
begin
Result := FormatInfo;
end;
function TFFmpegDecodeStream.IsEOF(): boolean;
begin
SDL_mutexP(StateLock);
Result := EOFState;
SDL_mutexV(StateLock);
end;
procedure TFFmpegDecodeStream.SetEOF(State: boolean);
begin
SDL_mutexP(StateLock);
EOFState := State;
SDL_mutexV(StateLock);
end;
function TFFmpegDecodeStream.IsError(): boolean;
begin
SDL_mutexP(StateLock);
Result := ErrorState;
SDL_mutexV(StateLock);
end;
procedure TFFmpegDecodeStream.SetError(State: boolean);
begin
SDL_mutexP(StateLock);
ErrorState := State;
SDL_mutexV(StateLock);
end;
function TFFmpegDecodeStream.IsSeeking(): boolean;
begin
SDL_mutexP(StateLock);
Result := SeekRequest;
SDL_mutexV(StateLock);
end;
function TFFmpegDecodeStream.IsQuit(): boolean;
begin
SDL_mutexP(StateLock);
Result := QuitRequest;
SDL_mutexV(StateLock);
end;
function TFFmpegDecodeStream.GetPosition(): real;
var
BufferSizeSec: double;
begin
PauseDecoder();
// ReadData() does not return all of the buffer retrieved by DecodeFrame().
// Determine the size of the unused part of the decode-buffer.
BufferSizeSec := (AudioBufferSize - AudioBufferPos) /
FormatInfo.BytesPerSec;
// subtract the size of unused buffer-data from the audio clock.
Result := AudioStreamPos - BufferSizeSec;
ResumeDecoder();
end;
procedure TFFmpegDecodeStream.SetPosition(Time: real);
begin
SetPositionIntern(Time, true, true);
end;
function TFFmpegDecodeStream.GetLoop(): boolean;
begin
SDL_mutexP(StateLock);
Result := Loop;
SDL_mutexV(StateLock);
end;
procedure TFFmpegDecodeStream.SetLoop(Enabled: boolean);
begin
SDL_mutexP(StateLock);
Loop := Enabled;
SDL_mutexV(StateLock);
end;
(********************************************
* Parser section
********************************************)
procedure TFFmpegDecodeStream.PauseParser();
begin
if (SDL_ThreadID() = ParseThread.threadid) then
Exit;
SDL_mutexP(StateLock);
Inc(ParserPauseRequestCount);
while (ParserLocked) do
SDL_CondWait(ParserUnlockedCond, StateLock);
SDL_mutexV(StateLock);
end;
procedure TFFmpegDecodeStream.ResumeParser();
begin
if (SDL_ThreadID() = ParseThread.threadid) then
Exit;
SDL_mutexP(StateLock);
Dec(ParserPauseRequestCount);
SDL_CondSignal(ParserResumeCond);
SDL_mutexV(StateLock);
end;
procedure TFFmpegDecodeStream.SetPositionIntern(Time: real; Flush: boolean; Blocking: boolean);
begin
// - Pause the parser first to prevent it from putting obsolete packages
// into the queue after the queue was flushed and before seeking is done.
// Otherwise we will hear fragments of old data, if the stream was seeked
// in stopped mode and resumed afterwards (applies to non-blocking mode only).
// - Pause the decoder to avoid race-condition that might occur otherwise.
// - Last lock the state lock because we are manipulating some shared state-vars.
PauseParser();
PauseDecoder();
SDL_mutexP(StateLock);
try
EOFState := false;
ErrorState := false;
// do not seek if we are already at the correct position.
// This is important especially for seeking to position 0 if we already are
// at the beginning. Although seeking with AVSEEK_FLAG_BACKWARD for pos 0 works,
// it is still a bit choppy (although much better than w/o AVSEEK_FLAG_BACKWARD).
if (Time = AudioStreamPos) then
Exit;
// configure seek parameters
SeekPos := Time;
SeekFlush := Flush;
SeekFlags := AVSEEK_FLAG_ANY;
SeekRequest := true;
// Note: the BACKWARD-flag seeks to the first position <= the position
// searched for. Otherwise e.g. position 0 might not be seeked correct.
// For some reason ffmpeg sometimes doesn't use position 0 but the key-frame
// following. In streams with few key-frames (like many flv-files) the next
// key-frame after 0 might be 5secs ahead.
if (Time <= AudioStreamPos) then
SeekFlags := SeekFlags or AVSEEK_FLAG_BACKWARD;
// send a reuse signal in case the parser was stopped (e.g. because of an EOF)
SDL_CondSignal(ParserIdleCond);
finally
SDL_mutexV(StateLock);
ResumeDecoder();
ResumeParser();
end;
// in blocking mode, wait until seeking is done
if (Blocking) then
begin
SDL_mutexP(StateLock);
while (SeekRequest) do
SDL_CondWait(SeekFinishedCond, StateLock);
SDL_mutexV(StateLock);
end;
end;
function ParseThreadMain(Data: Pointer): integer; cdecl;
var
Stream: TFFmpegDecodeStream;
begin
Stream := TFFmpegDecodeStream(Data);
if (Stream <> nil) then
Stream.Parse();
Result := 0;
end;
procedure TFFmpegDecodeStream.Parse();
begin
// reuse thread as long as the stream is not terminated
while (ParseLoop()) do
begin
// wait for reuse or destruction of stream
SDL_mutexP(StateLock);
while (not (SeekRequest or QuitRequest)) do
SDL_CondWait(ParserIdleCond, StateLock);
SDL_mutexV(StateLock);
end;
end;
(**
* Parser main loop.
* Will not return until parsing of the stream is finished.
* Reasons for the parser to return are:
* - the end-of-file is reached
* - an error occured
* - the stream was quited (received a quit-request)
* Returns true if the stream can be resumed or false if the stream has to
* be terminated.
*)
function TFFmpegDecodeStream.ParseLoop(): boolean;
var
Packet: TAVPacket;
SeekTarget: int64;
ByteIOCtx: PByteIOContext;
ErrorCode: integer;
StartSilence: double; // duration of silence at start of stream
StartSilencePtr: PDouble; // pointer for the EMPTY status packet
// Note: pthreads wakes threads waiting on a mutex in the order of their
// priority and not in FIFO order. SDL does not provide any option to
// control priorities. This might (and already did) starve threads waiting
// on the mutex (e.g. SetPosition) making usdx look like it was froozen.
// Instead of simply locking the critical section we set a ParserLocked flag
// instead and give priority to the threads requesting the parser to pause.
procedure LockParser();
begin
SDL_mutexP(StateLock);
while (ParserPauseRequestCount > 0) do
SDL_CondWait(ParserResumeCond, StateLock);
ParserLocked := true;
SDL_mutexV(StateLock);
end;
procedure UnlockParser();
begin
SDL_mutexP(StateLock);
ParserLocked := false;
SDL_CondBroadcast(ParserUnlockedCond);
SDL_mutexV(StateLock);
end;
begin
Result := true;
while (true) do
begin
LockParser();
try
if (IsQuit()) then
begin
Result := false;
Exit;
end;
// handle seek-request (Note: no need to lock SeekRequest here)
if (SeekRequest) then
begin
// first try: seek on the audio stream
SeekTarget := Round(SeekPos / av_q2d(AudioStream^.time_base));
StartSilence := 0;
if (SeekTarget < AudioStream^.start_time) then
StartSilence := (AudioStream^.start_time - SeekTarget) * av_q2d(AudioStream^.time_base);
ErrorCode := av_seek_frame(FormatCtx, AudioStreamIndex, SeekTarget, SeekFlags);
if (ErrorCode < 0) then
begin
// second try: seek on the default stream (necessary for flv-videos and some ogg-files)
SeekTarget := Round(SeekPos * AV_TIME_BASE);
StartSilence := 0;
if (SeekTarget < FormatCtx^.start_time) then
StartSilence := (FormatCtx^.start_time - SeekTarget) / AV_TIME_BASE;
ErrorCode := av_seek_frame(FormatCtx, -1, SeekTarget, SeekFlags);
end;
// pause decoder and lock state (keep the lock-order to avoid deadlocks).
// Note that the decoder does not block in the packet-queue in seeking state,
// so locking the decoder here does not cause a dead-lock.
PauseDecoder();
SDL_mutexP(StateLock);
try
if (ErrorCode < 0) then
begin
// seeking failed
ErrorState := true;
Log.LogStatus('Seek Error in "'+FormatCtx^.filename+'"', 'UAudioDecoder_FFmpeg');
end
else
begin
if (SeekFlush) then
begin
// flush queue (we will send a Flush-Packet when seeking is finished)
PacketQueue.Flush();
// flush the decode buffers
AudioBufferSize := 0;
AudioBufferPos := 0;
AudioPaketSize := 0;
AudioPaketSilence := 0;
FlushCodecBuffers();
// Set preliminary stream position. The position will be set to
// the correct value as soon as the first packet is decoded.
AudioStreamPos := SeekPos;
end
else
begin
// request avcodec buffer flush
PacketQueue.PutStatus(PKT_STATUS_FLAG_FLUSH, nil);
end;
// fill the gap between position 0 and start_time with silence
// but not if we are in loop mode
if ((StartSilence > 0) and (not Loop)) then
begin
GetMem(StartSilencePtr, SizeOf(StartSilence));
StartSilencePtr^ := StartSilence;
PacketQueue.PutStatus(PKT_STATUS_FLAG_EMPTY, StartSilencePtr);
end;
end;
SeekRequest := false;
SDL_CondBroadcast(SeekFinishedCond);
finally
SDL_mutexV(StateLock);
ResumeDecoder();
end;
end;
if (PacketQueue.GetSize() > MAX_AUDIOQ_SIZE) then
begin
SDL_Delay(10);
Continue;
end;
if (av_read_frame(FormatCtx, Packet) < 0) then
begin
// failed to read a frame, check reason
{$IF (LIBAVFORMAT_VERSION_MAJOR >= 52)}
ByteIOCtx := FormatCtx^.pb;
{$ELSE}
ByteIOCtx := @FormatCtx^.pb;
{$IFEND}
// check for end-of-file (eof is not an error)
if (url_feof(ByteIOCtx) <> 0) then
begin
if (GetLoop()) then
begin
// rewind stream (but do not flush)
SetPositionIntern(0, false, false);
Continue;
end
else
begin
// signal end-of-file
PacketQueue.PutStatus(PKT_STATUS_FLAG_EOF, nil);
Exit;
end;
end;
// check for errors
if (url_ferror(ByteIOCtx) <> 0) then
begin
// an error occured -> abort and wait for repositioning or termination
PacketQueue.PutStatus(PKT_STATUS_FLAG_ERROR, nil);
Exit;
end;
// url_feof() does not detect an EOF for some files
// so we have to do it this way.
if ((FormatCtx^.file_size <> 0) and
(ByteIOCtx^.pos >= FormatCtx^.file_size)) then
begin
PacketQueue.PutStatus(PKT_STATUS_FLAG_EOF, nil);
Exit;
end;
// unknown error occured, exit
PacketQueue.PutStatus(PKT_STATUS_FLAG_ERROR, nil);
Exit;
end;
if (Packet.stream_index = AudioStreamIndex) then
PacketQueue.Put(@Packet)
else
av_free_packet(@Packet);
finally
UnlockParser();
end;
end;
end;
(********************************************
* Decoder section
********************************************)
procedure TFFmpegDecodeStream.PauseDecoder();
begin
SDL_mutexP(StateLock);
Inc(DecoderPauseRequestCount);
while (DecoderLocked) do
SDL_CondWait(DecoderUnlockedCond, StateLock);
SDL_mutexV(StateLock);
end;
procedure TFFmpegDecodeStream.ResumeDecoder();
begin
SDL_mutexP(StateLock);
Dec(DecoderPauseRequestCount);
SDL_CondSignal(DecoderResumeCond);
SDL_mutexV(StateLock);
end;
procedure TFFmpegDecodeStream.FlushCodecBuffers();
begin
// if no flush operation is specified, avcodec_flush_buffers will not do anything.
if (@CodecCtx.codec.flush <> nil) then
begin
// flush buffers used by avcodec_decode_audio, etc.
avcodec_flush_buffers(CodecCtx);
end
else
begin
// we need a Workaround to avoid plopping noise with ogg-vorbis and
// mp3 (in older versions of FFmpeg).
// We will just reopen the codec.
FFmpegCore.LockAVCodec();
try
avcodec_close(CodecCtx);
avcodec_open(CodecCtx, Codec);
finally
FFmpegCore.UnlockAVCodec();
end;
end;
end;
function TFFmpegDecodeStream.DecodeFrame(Buffer: PByteArray; BufferSize: integer): integer;
var
PaketDecodedSize: integer; // size of packet data used for decoding
DataSize: integer; // size of output data decoded by FFmpeg
BlockQueue: boolean;
SilenceDuration: double;
{$IFDEF DebugFFmpegDecode}
TmpPos: double;
{$ENDIF}
begin
Result := -1;
if (EOF) then
Exit;
while(true) do
begin
// for titles with start_time > 0 we have to generate silence
// until we reach the pts of the first data packet.
if (AudioPaketSilence > 0) then
begin
DataSize := Min(AudioPaketSilence, BufferSize);
FillChar(Buffer[0], DataSize, 0);
Dec(AudioPaketSilence, DataSize);
AudioStreamPos := AudioStreamPos + DataSize / FormatInfo.BytesPerSec;
Result := DataSize;
Exit;
end;
// read packet data
while (AudioPaketSize > 0) do
begin
DataSize := BufferSize;
{$IF LIBAVCODEC_VERSION >= 51030000} // 51.30.0
PaketDecodedSize := avcodec_decode_audio2(CodecCtx, PSmallint(Buffer),
DataSize, AudioPaketData, AudioPaketSize);
{$ELSE}
PaketDecodedSize := avcodec_decode_audio(CodecCtx, PSmallint(Buffer),
DataSize, AudioPaketData, AudioPaketSize);
{$IFEND}
if(PaketDecodedSize < 0) then
begin
// if error, skip frame
{$IFDEF DebugFFmpegDecode}
DebugWriteln('Skip audio frame');
{$ENDIF}
AudioPaketSize := 0;
Break;
end;
Inc(AudioPaketData, PaketDecodedSize);
Dec(AudioPaketSize, PaketDecodedSize);
// check if avcodec_decode_audio returned data, otherwise fetch more frames
if (DataSize <= 0) then
Continue;
// update stream position by the amount of fetched data
AudioStreamPos := AudioStreamPos + DataSize / FormatInfo.BytesPerSec;
// we have data, return it and come back for more later
Result := DataSize;
Exit;
end;
// free old packet data
if (AudioPaket.data <> nil) then
av_free_packet(@AudioPaket);
// do not block queue on seeking (to avoid deadlocks on the DecoderLock)
if (IsSeeking()) then
BlockQueue := false
else
BlockQueue := true;
// request a new packet and block if none available.
// If this fails, the queue was aborted.
if (PacketQueue.Get(AudioPaket, BlockQueue) <= 0) then
Exit;
// handle Status-packet
if (PAnsiChar(AudioPaket.data) = STATUS_PACKET) then
begin
AudioPaket.data := nil;
AudioPaketData := nil;
AudioPaketSize := 0;
case (AudioPaket.flags) of
PKT_STATUS_FLAG_FLUSH:
begin
// just used if SetPositionIntern was called without the flush flag.
FlushCodecBuffers;
end;
PKT_STATUS_FLAG_EOF: // end-of-file
begin
// ignore EOF while seeking
if (not IsSeeking()) then
SetEOF(true);
// buffer contains no data -> result = -1
Exit;
end;
PKT_STATUS_FLAG_ERROR:
begin
SetError(true);
Log.LogStatus('I/O Error', 'TFFmpegDecodeStream.DecodeFrame');
Exit;
end;
PKT_STATUS_FLAG_EMPTY:
begin
SilenceDuration := PDouble(PacketQueue.GetStatusInfo(AudioPaket))^;
AudioPaketSilence := Round(SilenceDuration * FormatInfo.SampleRate) * FormatInfo.FrameSize;
PacketQueue.FreeStatusInfo(AudioPaket);
end
else
begin
Log.LogStatus('Unknown status', 'TFFmpegDecodeStream.DecodeFrame');
end;
end;
Continue;
end;
AudioPaketData := AudioPaket.data;
AudioPaketSize := AudioPaket.size;
// if available, update the stream position to the presentation time of this package
if(AudioPaket.pts <> AV_NOPTS_VALUE) then
begin
{$IFDEF DebugFFmpegDecode}
TmpPos := AudioStreamPos;
{$ENDIF}
AudioStreamPos := av_q2d(AudioStream^.time_base) * AudioPaket.pts;
{$IFDEF DebugFFmpegDecode}
DebugWriteln('Timestamp: ' + floattostrf(AudioStreamPos, ffFixed, 15, 3) + ' ' +
'(Calc: ' + floattostrf(TmpPos, ffFixed, 15, 3) + '), ' +
'Diff: ' + floattostrf(AudioStreamPos-TmpPos, ffFixed, 15, 3));
{$ENDIF}
end;
end;
end;
function TFFmpegDecodeStream.ReadData(Buffer: PByteArray; BufferSize: integer): integer;
var
CopyByteCount: integer; // number of bytes to copy
RemainByteCount: integer; // number of bytes left (remain) to read
BufferPos: integer;
// prioritize pause requests
procedure LockDecoder();
begin
SDL_mutexP(StateLock);
while (DecoderPauseRequestCount > 0) do
SDL_CondWait(DecoderResumeCond, StateLock);
DecoderLocked := true;
SDL_mutexV(StateLock);
end;
procedure UnlockDecoder();
begin
SDL_mutexP(StateLock);
DecoderLocked := false;
SDL_CondBroadcast(DecoderUnlockedCond);
SDL_mutexV(StateLock);
end;
begin
Result := -1;
// set number of bytes to copy to the output buffer
BufferPos := 0;
LockDecoder();
try
// leave if end-of-file is reached
if (EOF) then
Exit;
// copy data to output buffer
while (BufferPos < BufferSize) do
begin
// check if we need more data
if (AudioBufferPos >= AudioBufferSize) then
begin
AudioBufferPos := 0;
// we have already sent all our data; get more
AudioBufferSize := DecodeFrame(AudioBuffer, AUDIO_BUFFER_SIZE);
// check for errors or EOF
if(AudioBufferSize < 0) then
begin
Result := BufferPos;
Exit;
end;
end;
// calc number of new bytes in the decode-buffer
CopyByteCount := AudioBufferSize - AudioBufferPos;
// resize copy-count if more bytes available than needed (remaining bytes are used the next time)
RemainByteCount := BufferSize - BufferPos;
if (CopyByteCount > RemainByteCount) then
CopyByteCount := RemainByteCount;
Move(AudioBuffer[AudioBufferPos], Buffer[BufferPos], CopyByteCount);
Inc(BufferPos, CopyByteCount);
Inc(AudioBufferPos, CopyByteCount);
end;
finally
UnlockDecoder();
end;
Result := BufferSize;
end;
{ TAudioDecoder_FFmpeg }
function TAudioDecoder_FFmpeg.GetName: String;
begin
Result := 'FFmpeg_Decoder';
end;
function TAudioDecoder_FFmpeg.InitializeDecoder: boolean;
begin
//Log.LogStatus('InitializeDecoder', 'UAudioDecoder_FFmpeg');
FFmpegCore := TMediaCore_FFmpeg.GetInstance();
av_register_all();
// Do not show uninformative error messages by default.
// FFmpeg prints all error-infos on the console by default what
// is very confusing as the playback of the files is correct.
// We consider these errors to be internal to FFMpeg. They can be fixed
// by the FFmpeg guys only and do not provide any useful information in
// respect to USDX.
{$IFNDEF EnableFFmpegErrorOutput}
{$IF LIBAVUTIL_VERSION_MAJOR >= 50}
av_log_set_level(AV_LOG_FATAL);
{$ELSE}
// FATAL and ERROR share one log-level, so we have to use QUIET
av_log_set_level(AV_LOG_QUIET);
{$IFEND}
{$ENDIF}
Result := true;
end;
function TAudioDecoder_FFmpeg.FinalizeDecoder(): boolean;
begin
Result := true;
end;
function TAudioDecoder_FFmpeg.Open(const Filename: IPath): TAudioDecodeStream;
var
Stream: TFFmpegDecodeStream;
begin
Result := nil;
Stream := TFFmpegDecodeStream.Create();
if (not Stream.Open(Filename)) then
begin
Stream.Free;
Exit;
end;
Result := Stream;
end;
initialization
MediaManager.Add(TAudioDecoder_FFmpeg.Create);
end.