aboutsummaryrefslogtreecommitdiffstats
path: root/src/media/UAudioDecoder_FFmpeg.pas
diff options
context:
space:
mode:
Diffstat (limited to 'src/media/UAudioDecoder_FFmpeg.pas')
-rw-r--r--src/media/UAudioDecoder_FFmpeg.pas1159
1 files changed, 1159 insertions, 0 deletions
diff --git a/src/media/UAudioDecoder_FFmpeg.pas b/src/media/UAudioDecoder_FFmpeg.pas
new file mode 100644
index 00000000..b44c7b11
--- /dev/null
+++ b/src/media/UAudioDecoder_FFmpeg.pas
@@ -0,0 +1,1159 @@
+{* 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
+ fStateLock: PSDL_Mutex;
+
+ fEOFState: boolean; // end-of-stream flag (locked by StateLock)
+ fErrorState: boolean; // error flag (locked by StateLock)
+
+ fQuitRequest: boolean; // (locked by StateLock)
+ fParserIdleCond: PSDL_Cond;
+
+ // parser pause/resume data
+ fParserLocked: boolean;
+ fParserPauseRequestCount: integer;
+ fParserUnlockedCond: PSDL_Cond;
+ fParserResumeCond: PSDL_Cond;
+
+ fSeekRequest: boolean; // (locked by StateLock)
+ fSeekFlags: integer; // (locked by StateLock)
+ fSeekPos: double; // stream position to seek for (in secs) (locked by StateLock)
+ fSeekFlush: boolean; // true if the buffers should be flushed after seeking (locked by StateLock)
+ SeekFinishedCond: PSDL_Cond;
+
+ fLoop: boolean; // (locked by StateLock)
+
+ fParseThread: PSDL_Thread;
+ fPacketQueue: TPacketQueue;
+
+ fFormatInfo: TAudioFormatInfo;
+
+ // FFmpeg specific data
+ fFormatCtx: PAVFormatContext;
+ fCodecCtx: PAVCodecContext;
+ fCodec: PAVCodec;
+
+ fAudioStreamIndex: integer;
+ fAudioStream: PAVStream;
+ fAudioStreamPos: double; // stream position in seconds (locked by DecoderLock)
+
+ // decoder pause/resume data
+ fDecoderLocked: boolean;
+ fDecoderPauseRequestCount: integer;
+ fDecoderUnlockedCond: PSDL_Cond;
+ fDecoderResumeCond: PSDL_Cond;
+
+ // state-vars for DecodeFrame (locked by DecoderLock)
+ fAudioPaket: TAVPacket;
+ fAudioPaketData: PByteArray;
+ fAudioPaketSize: integer;
+ fAudioPaketSilence: integer; // number of bytes of silence to return
+
+ // state-vars for AudioCallback (locked by DecoderLock)
+ fAudioBufferPos: integer;
+ fAudioBufferSize: integer;
+ fAudioBuffer: PByteArray;
+
+ fFilename: 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();
+
+ fStateLock := SDL_CreateMutex();
+ fParserUnlockedCond := SDL_CreateCond();
+ fParserResumeCond := SDL_CreateCond();
+ fParserIdleCond := SDL_CreateCond();
+ SeekFinishedCond := SDL_CreateCond();
+ fDecoderUnlockedCond := SDL_CreateCond();
+ fDecoderResumeCond := 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.
+ fAudioBuffer := GetAlignedMem(AUDIO_BUFFER_SIZE, 16);
+
+ Reset();
+end;
+
+procedure TFFmpegDecodeStream.Reset();
+begin
+ fParseThread := nil;
+
+ fEOFState := false;
+ fErrorState := false;
+ fLoop := false;
+ fQuitRequest := false;
+
+ fAudioPaketData := nil;
+ fAudioPaketSize := 0;
+ fAudioPaketSilence := 0;
+
+ fAudioBufferPos := 0;
+ fAudioBufferSize := 0;
+
+ fParserLocked := false;
+ fParserPauseRequestCount := 0;
+ fDecoderLocked := false;
+ fDecoderPauseRequestCount := 0;
+
+ FillChar(fAudioPaket, SizeOf(TAVPacket), 0);
+end;
+
+{*
+ * Frees the decode-stream data.
+ *}
+destructor TFFmpegDecodeStream.Destroy();
+begin
+ Close();
+
+ SDL_DestroyMutex(fStateLock);
+ SDL_DestroyCond(fParserUnlockedCond);
+ SDL_DestroyCond(fParserResumeCond);
+ SDL_DestroyCond(fParserIdleCond);
+ SDL_DestroyCond(SeekFinishedCond);
+ SDL_DestroyCond(fDecoderUnlockedCond);
+ SDL_DestroyCond(fDecoderResumeCond);
+
+ FreeAlignedMem(fAudioBuffer);
+
+ 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.fFilename := Filename;
+
+ // use custom 'ufile' protocol for UTF-8 support
+ if (av_open_input_file(fFormatCtx, 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
+ fFormatCtx^.flags := fFormatCtx^.flags or AVFMT_FLAG_GENPTS;
+
+ // retrieve stream information
+ if (av_find_stream_info(fFormatCtx) < 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
+ fFormatCtx^.pb.eof_reached := 0;
+
+ {$IFDEF DebugFFmpegDecode}
+ dump_format(fFormatCtx, 0, PAnsiChar(Filename.ToNative), 0);
+ {$ENDIF}
+
+ fAudioStreamIndex := FFmpegCore.FindAudioStreamIndex(fFormatCtx);
+ if (fAudioStreamIndex < 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');
+
+ fAudioStream := fFormatCtx.streams[fAudioStreamIndex];
+ fAudioStreamPos := 0;
+ fCodecCtx := fAudioStream^.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}
+ *)
+
+ fCodec := avcodec_find_decoder(fCodecCtx^.codec_id);
+ if (fCodec = nil) then
+ begin
+ Log.LogError('Unsupported codec!', 'UAudio_FFmpeg');
+ fCodecCtx := nil;
+ Close();
+ Exit;
+ end;
+
+ // set debug options
+ fCodecCtx^.debug_mv := 0;
+ fCodecCtx^.debug := 0;
+
+ // detect bug-workarounds automatically
+ fCodecCtx^.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(fCodecCtx, fCodec);
+ 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(fCodecCtx^.sample_fmt, SampleFormat)) then
+ begin
+ // try standard format
+ SampleFormat := asfS16;
+ end;
+ if fCodecCtx^.channels > 255 then
+ Log.LogStatus('Error: CodecCtx^.channels > 255', 'TFFmpegDecodeStream.Open');
+ fFormatInfo := TAudioFormatInfo.Create(
+ byte(fCodecCtx^.channels),
+ fCodecCtx^.sample_rate,
+ SampleFormat
+ );
+
+ fPacketQueue := TPacketQueue.Create();
+
+ // finally start the decode thread
+ fParseThread := 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(fPacketQueue)) then
+ fPacketQueue.Abort();
+
+ // send quit request (to parse-thread etc)
+ SDL_mutexP(fStateLock);
+ fQuitRequest := true;
+ SDL_CondBroadcast(fParserIdleCond);
+ SDL_mutexV(fStateLock);
+
+ // abort parse-thread
+ if (fParseThread <> nil) then
+ begin
+ // and wait until it terminates
+ SDL_WaitThread(fParseThread, ThreadResult);
+ fParseThread := nil;
+ end;
+
+ // Close the codec
+ if (fCodecCtx <> nil) then
+ begin
+ // avcodec_close() is not thread-safe
+ FFmpegCore.LockAVCodec();
+ try
+ avcodec_close(fCodecCtx);
+ finally
+ FFmpegCore.UnlockAVCodec();
+ end;
+ fCodecCtx := nil;
+ end;
+
+ // Close the video file
+ if (fFormatCtx <> nil) then
+ begin
+ av_close_input_file(fFormatCtx);
+ fFormatCtx := nil;
+ end;
+
+ PerformOnClose();
+
+ FreeAndNil(fPacketQueue);
+ FreeAndNil(fFormatInfo);
+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 := (fFormatCtx^.start_time + fFormatCtx^.duration) / AV_TIME_BASE;
+end;
+
+function TFFmpegDecodeStream.GetAudioFormatInfo(): TAudioFormatInfo;
+begin
+ Result := fFormatInfo;
+end;
+
+function TFFmpegDecodeStream.IsEOF(): boolean;
+begin
+ SDL_mutexP(fStateLock);
+ Result := fEOFState;
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.SetEOF(State: boolean);
+begin
+ SDL_mutexP(fStateLock);
+ fEOFState := State;
+ SDL_mutexV(fStateLock);
+end;
+
+function TFFmpegDecodeStream.IsError(): boolean;
+begin
+ SDL_mutexP(fStateLock);
+ Result := fErrorState;
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.SetError(State: boolean);
+begin
+ SDL_mutexP(fStateLock);
+ fErrorState := State;
+ SDL_mutexV(fStateLock);
+end;
+
+function TFFmpegDecodeStream.IsSeeking(): boolean;
+begin
+ SDL_mutexP(fStateLock);
+ Result := fSeekRequest;
+ SDL_mutexV(fStateLock);
+end;
+
+function TFFmpegDecodeStream.IsQuit(): boolean;
+begin
+ SDL_mutexP(fStateLock);
+ Result := fQuitRequest;
+ SDL_mutexV(fStateLock);
+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 := (fAudioBufferSize - fAudioBufferPos) /
+ fFormatInfo.BytesPerSec;
+
+ // subtract the size of unused buffer-data from the audio clock.
+ Result := fAudioStreamPos - BufferSizeSec;
+
+ ResumeDecoder();
+end;
+
+procedure TFFmpegDecodeStream.SetPosition(Time: real);
+begin
+ SetPositionIntern(Time, true, true);
+end;
+
+function TFFmpegDecodeStream.GetLoop(): boolean;
+begin
+ SDL_mutexP(fStateLock);
+ Result := fLoop;
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.SetLoop(Enabled: boolean);
+begin
+ SDL_mutexP(fStateLock);
+ fLoop := Enabled;
+ SDL_mutexV(fStateLock);
+end;
+
+
+(********************************************
+ * Parser section
+ ********************************************)
+
+procedure TFFmpegDecodeStream.PauseParser();
+begin
+ if (SDL_ThreadID() = fParseThread.threadid) then
+ Exit;
+
+ SDL_mutexP(fStateLock);
+ Inc(fParserPauseRequestCount);
+ while (fParserLocked) do
+ SDL_CondWait(fParserUnlockedCond, fStateLock);
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.ResumeParser();
+begin
+ if (SDL_ThreadID() = fParseThread.threadid) then
+ Exit;
+
+ SDL_mutexP(fStateLock);
+ Dec(fParserPauseRequestCount);
+ SDL_CondSignal(fParserResumeCond);
+ SDL_mutexV(fStateLock);
+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(fStateLock);
+ try
+ fEOFState := false;
+ fErrorState := 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 = fAudioStreamPos) then
+ Exit;
+
+ // configure seek parameters
+ fSeekPos := Time;
+ fSeekFlush := Flush;
+ fSeekFlags := AVSEEK_FLAG_ANY;
+ fSeekRequest := 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 <= fAudioStreamPos) then
+ fSeekFlags := fSeekFlags or AVSEEK_FLAG_BACKWARD;
+
+ // send a reuse signal in case the parser was stopped (e.g. because of an EOF)
+ SDL_CondSignal(fParserIdleCond);
+ finally
+ SDL_mutexV(fStateLock);
+ ResumeDecoder();
+ ResumeParser();
+ end;
+
+ // in blocking mode, wait until seeking is done
+ if (Blocking) then
+ begin
+ SDL_mutexP(fStateLock);
+ while (fSeekRequest) do
+ SDL_CondWait(SeekFinishedCond, fStateLock);
+ SDL_mutexV(fStateLock);
+ 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(fStateLock);
+ while (not (fSeekRequest or fQuitRequest)) do
+ SDL_CondWait(fParserIdleCond, fStateLock);
+ SDL_mutexV(fStateLock);
+ 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(fStateLock);
+ while (fParserPauseRequestCount > 0) do
+ SDL_CondWait(fParserResumeCond, fStateLock);
+ fParserLocked := true;
+ SDL_mutexV(fStateLock);
+ end;
+
+ procedure UnlockParser();
+ begin
+ SDL_mutexP(fStateLock);
+ fParserLocked := false;
+ SDL_CondBroadcast(fParserUnlockedCond);
+ SDL_mutexV(fStateLock);
+ 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 (fSeekRequest) then
+ begin
+ // first try: seek on the audio stream
+ SeekTarget := Round(fSeekPos / av_q2d(fAudioStream^.time_base));
+ StartSilence := 0;
+ if (SeekTarget < fAudioStream^.start_time) then
+ StartSilence := (fAudioStream^.start_time - SeekTarget) * av_q2d(fAudioStream^.time_base);
+ ErrorCode := av_seek_frame(fFormatCtx, fAudioStreamIndex, SeekTarget, fSeekFlags);
+
+ if (ErrorCode < 0) then
+ begin
+ // second try: seek on the default stream (necessary for flv-videos and some ogg-files)
+ SeekTarget := Round(fSeekPos * AV_TIME_BASE);
+ StartSilence := 0;
+ if (SeekTarget < fFormatCtx^.start_time) then
+ StartSilence := (fFormatCtx^.start_time - SeekTarget) / AV_TIME_BASE;
+ ErrorCode := av_seek_frame(fFormatCtx, -1, SeekTarget, fSeekFlags);
+ 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(fStateLock);
+ try
+ if (ErrorCode < 0) then
+ begin
+ // seeking failed
+ fErrorState := true;
+ Log.LogError('Seek Error in "'+fFormatCtx^.filename+'"', 'UAudioDecoder_FFmpeg');
+ end
+ else
+ begin
+ if (fSeekFlush) then
+ begin
+ // flush queue (we will send a Flush-Packet when seeking is finished)
+ fPacketQueue.Flush();
+
+ // flush the decode buffers
+ fAudioBufferSize := 0;
+ fAudioBufferPos := 0;
+ fAudioPaketSize := 0;
+ fAudioPaketSilence := 0;
+ FlushCodecBuffers();
+
+ // Set preliminary stream position. The position will be set to
+ // the correct value as soon as the first packet is decoded.
+ fAudioStreamPos := fSeekPos;
+ end
+ else
+ begin
+ // request avcodec buffer flush
+ fPacketQueue.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 fLoop)) then
+ begin
+ GetMem(StartSilencePtr, SizeOf(StartSilence));
+ StartSilencePtr^ := StartSilence;
+ fPacketQueue.PutStatus(PKT_STATUS_FLAG_EMPTY, StartSilencePtr);
+ end;
+ end;
+
+ fSeekRequest := false;
+ SDL_CondBroadcast(SeekFinishedCond);
+ finally
+ SDL_mutexV(fStateLock);
+ ResumeDecoder();
+ end;
+ end;
+
+ if (fPacketQueue.GetSize() > MAX_AUDIOQ_SIZE) then
+ begin
+ SDL_Delay(10);
+ Continue;
+ end;
+
+ if (av_read_frame(fFormatCtx, Packet) < 0) then
+ begin
+ // failed to read a frame, check reason
+ {$IF (LIBAVFORMAT_VERSION_MAJOR >= 52)}
+ ByteIOCtx := fFormatCtx^.pb;
+ {$ELSE}
+ ByteIOCtx := @fFormatCtx^.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
+ fPacketQueue.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
+ fPacketQueue.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 ((fFormatCtx^.file_size <> 0) and
+ (ByteIOCtx^.pos >= fFormatCtx^.file_size)) then
+ begin
+ fPacketQueue.PutStatus(PKT_STATUS_FLAG_EOF, nil);
+ Exit;
+ end;
+
+ // unknown error occured, exit
+ fPacketQueue.PutStatus(PKT_STATUS_FLAG_ERROR, nil);
+ Exit;
+ end;
+
+ if (Packet.stream_index = fAudioStreamIndex) then
+ fPacketQueue.Put(@Packet)
+ else
+ av_free_packet(@Packet);
+
+ finally
+ UnlockParser();
+ end;
+ end;
+end;
+
+
+(********************************************
+ * Decoder section
+ ********************************************)
+
+procedure TFFmpegDecodeStream.PauseDecoder();
+begin
+ SDL_mutexP(fStateLock);
+ Inc(fDecoderPauseRequestCount);
+ while (fDecoderLocked) do
+ SDL_CondWait(fDecoderUnlockedCond, fStateLock);
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.ResumeDecoder();
+begin
+ SDL_mutexP(fStateLock);
+ Dec(fDecoderPauseRequestCount);
+ SDL_CondSignal(fDecoderResumeCond);
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.FlushCodecBuffers();
+begin
+ // if no flush operation is specified, avcodec_flush_buffers will not do anything.
+ if (@fCodecCtx.codec.flush <> nil) then
+ begin
+ // flush buffers used by avcodec_decode_audio, etc.
+ avcodec_flush_buffers(fCodecCtx);
+ 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(fCodecCtx);
+ avcodec_open(fCodecCtx, fCodec);
+ 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 (fAudioPaketSilence > 0) then
+ begin
+ DataSize := Min(fAudioPaketSilence, BufferSize);
+ FillChar(Buffer[0], DataSize, 0);
+ Dec(fAudioPaketSilence, DataSize);
+ fAudioStreamPos := fAudioStreamPos + DataSize / fFormatInfo.BytesPerSec;
+ Result := DataSize;
+ Exit;
+ end;
+
+ // read packet data
+ while (fAudioPaketSize > 0) do
+ begin
+ DataSize := BufferSize;
+
+ {$IF LIBAVCODEC_VERSION >= 51030000} // 51.30.0
+ PaketDecodedSize := avcodec_decode_audio2(fCodecCtx, PSmallint(Buffer),
+ DataSize, fAudioPaketData, fAudioPaketSize);
+ {$ELSE}
+ PaketDecodedSize := avcodec_decode_audio(fCodecCtx, PSmallint(Buffer),
+ DataSize, fAudioPaketData, fAudioPaketSize);
+ {$IFEND}
+
+ if(PaketDecodedSize < 0) then
+ begin
+ // if error, skip frame
+ {$IFDEF DebugFFmpegDecode}
+ DebugWriteln('Skip audio frame');
+ {$ENDIF}
+ fAudioPaketSize := 0;
+ Break;
+ end;
+
+ Inc(PByte(fAudioPaketData), PaketDecodedSize);
+ Dec(fAudioPaketSize, 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
+ fAudioStreamPos := fAudioStreamPos + DataSize / fFormatInfo.BytesPerSec;
+
+ // we have data, return it and come back for more later
+ Result := DataSize;
+ Exit;
+ end;
+
+ // free old packet data
+ if (fAudioPaket.data <> nil) then
+ av_free_packet(@fAudioPaket);
+
+ // 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 (fPacketQueue.Get(fAudioPaket, BlockQueue) <= 0) then
+ Exit;
+
+ // handle Status-packet
+ if (PAnsiChar(fAudioPaket.data) = STATUS_PACKET) then
+ begin
+ fAudioPaket.data := nil;
+ fAudioPaketData := nil;
+ fAudioPaketSize := 0;
+
+ case (fAudioPaket.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(fPacketQueue.GetStatusInfo(fAudioPaket))^;
+ fAudioPaketSilence := Round(SilenceDuration * fFormatInfo.SampleRate) * fFormatInfo.FrameSize;
+ fPacketQueue.FreeStatusInfo(fAudioPaket);
+ end
+ else
+ begin
+ Log.LogStatus('Unknown status', 'TFFmpegDecodeStream.DecodeFrame');
+ end;
+ end;
+
+ Continue;
+ end;
+
+ fAudioPaketData := fAudioPaket.data;
+ fAudioPaketSize := fAudioPaket.size;
+
+ // if available, update the stream position to the presentation time of this package
+ if(fAudioPaket.pts <> AV_NOPTS_VALUE) then
+ begin
+ {$IFDEF DebugFFmpegDecode}
+ TmpPos := fAudioStreamPos;
+ {$ENDIF}
+ fAudioStreamPos := av_q2d(fAudioStream^.time_base) * fAudioPaket.pts;
+ {$IFDEF DebugFFmpegDecode}
+ DebugWriteln('Timestamp: ' + floattostrf(fAudioStreamPos, ffFixed, 15, 3) + ' ' +
+ '(Calc: ' + floattostrf(TmpPos, ffFixed, 15, 3) + '), ' +
+ 'Diff: ' + floattostrf(fAudioStreamPos-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(fStateLock);
+ while (fDecoderPauseRequestCount > 0) do
+ SDL_CondWait(fDecoderResumeCond, fStateLock);
+ fDecoderLocked := true;
+ SDL_mutexV(fStateLock);
+ end;
+
+ procedure UnlockDecoder();
+ begin
+ SDL_mutexP(fStateLock);
+ fDecoderLocked := false;
+ SDL_CondBroadcast(fDecoderUnlockedCond);
+ SDL_mutexV(fStateLock);
+ 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 (fAudioBufferPos >= fAudioBufferSize) then
+ begin
+ fAudioBufferPos := 0;
+
+ // we have already sent all our data; get more
+ fAudioBufferSize := DecodeFrame(fAudioBuffer, AUDIO_BUFFER_SIZE);
+
+ // check for errors or EOF
+ if(fAudioBufferSize < 0) then
+ begin
+ Result := BufferPos;
+ Exit;
+ end;
+ end;
+
+ // calc number of new bytes in the decode-buffer
+ CopyByteCount := fAudioBufferSize - fAudioBufferPos;
+ // 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(fAudioBuffer[fAudioBufferPos], Buffer[BufferPos], CopyByteCount);
+
+ Inc(BufferPos, CopyByteCount);
+ Inc(fAudioBufferPos, 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.