{* 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, ctypes, 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. {$IF FFMPEG_VERSION_INT >= 2000000} AUDIO_BUFFER_SIZE = (192000 * 3) div 2; {$ELSE} AUDIO_BUFFER_SIZE = (AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) div 2; {$ENDIF} 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; { TestFrame: TAVPacket;} 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 LIBAVFORMAT_VERSION < 53001003} AVResult := av_open_input_file(fFormatCtx, PAnsiChar('ufile:'+FileName.ToUTF8), nil, 0, nil); {$ELSEIF LIBAVFORMAT_VERSION < 54029104} AVResult := avformat_open_input(@fFormatCtx, PAnsiChar('ufile:'+FileName.ToUTF8), nil, nil); {$ELSE} AVResult := FFmpegCore.AVFormatOpenInput(@fFormatCtx, PAnsiChar('ufile:'+FileName.ToUTF8)); {$IFEND} if (AVResult <> 0) then begin Log.LogError('Failed to open file "' + Filename.ToNative + '" ('+FFmpegCore.GetErrorString(AVResult)+')', 'UAudio_FFmpeg'); Exit; end; // generate PTS values if they do not exist fFormatCtx^.flags := fFormatCtx^.flags or AVFMT_FLAG_GENPTS; // retrieve stream information {$IF LIBAVFORMAT_VERSION >= 53002000)} AVResult := avformat_find_stream_info(fFormatCtx, nil); {$ELSE} AVResult := av_find_stream_info(fFormatCtx); {$IFEND} if (AVResult < 0) then begin Log.LogError('No stream info found: "' + Filename.ToNative + '"', 'UAudio_FFmpeg'); Close(); Exit; end; { av_find_stream_info is deprecated and should be replaced by av_read_frame. Untested. if (av_read_frame(fFormatCtx, TestFrame) < 0) then begin Log.LogError('av_read_frame 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'); {$IF LIBAVFORMAT_VERSION <= 52111000} // <= 52.111.0 fAudioStream := fFormatCtx.streams[fAudioStreamIndex]; {$ELSE} fAudioStream := PPAVStream(PtrUInt(fFormatCtx.streams) + fAudioStreamIndex * Sizeof(pointer))^; {$IFEND} 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; {$IF FFMPEG_VERSION_INT >= 1001000} // request required sample format // reference: // http://stackoverflow.com/questions/16479662/ffmpeg-1-0-causing-audio-playback-issues // without this avcodec_open2 returns AV_SAMPLE_FMT_S16P fCodecCtx^.request_sample_fmt := AV_SAMPLE_FMT_S16; {$IFEND} // 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 {$IF LIBAVCODEC_VERSION >= 53005000} AVResult := avcodec_open2(fCodecCtx, fCodec, nil); {$ELSE} AVResult := avcodec_open(fCodecCtx, fCodec); {$IFEND} 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 {$IF LIBAVFORMAT_VERSION < 53024002} av_close_input_file(fFormatCtx); {$ELSEIF LIBAVFORMAT_VERSION < 54029104} avformat_close_input(@fFormatCtx); {$ELSE} FFmpegCore.AVFormatCloseInput(@fFormatCtx); {$IFEND} fFormatCtx := nil; end; PerformOnClose(); FreeAndNil(fPacketQueue); FreeAndNil(fFormatInfo); end; function TFFmpegDecodeStream.GetLength(): real; var start_time: cint64; begin start_time := fFormatCtx^.start_time; // AV_NOPTS_VALUE is returned if no explicit start_time is available. if start_time = AV_NOPTS_VALUE then start_time := 0; // 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 := (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; {$IF FFMPEG_VERSION_INT < 1001000} ByteIOCtx: PByteIOContext; {$ELSE} ByteIOCtx: PAVIOContext; {$ENDIF} ErrorCode: integer; StartSilence: double; // duration of silence at start of stream StartSilencePtr: PDouble; // pointer for the EMPTY status packet fileSize: integer; urlError: integer; // 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 (LIBAVFORMAT_VERSION_MAJOR < 56)} if (url_feof(ByteIOCtx) <> 0) then {$ELSE} if (avio_feof(ByteIOCtx) <> 0) then {$IFEND} 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 (LIBAVFORMAT_VERSION >= 52103000)} urlError := ByteIOCtx^.error; {$ELSE} urlError := url_ferror(ByteIOCtx); {$IFEND} if (urlError <> 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 (LIBAVFORMAT_VERSION >= 53009000)} fileSize := avio_size(fFormatCtx^.pb); {$ELSE} fileSize := fFormatCtx^.file_size; {$IFEND} if ((fileSize <> 0) and (ByteIOCtx^.pos >= fileSize)) 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); {$IF LIBAVCODEC_VERSION >= 53005000} avcodec_open2(fCodecCtx, fCodec, nil); {$ELSE} avcodec_open(fCodecCtx, fCodec); {$IFEND} 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 >= 53025000} // 53.25.0 // PaketDecodedSize := avcodec_decode_audio4(fCodecCtx, AVFrame, // @got_frame_ptr, @fAudioPaket); // DataSize := AVFrame.nb_samples; // Buffer := PByteArray(AVFrame.data[0]); {$IF LIBAVCODEC_VERSION >= 52122000} // 52.122.0 PaketDecodedSize := avcodec_decode_audio3(fCodecCtx, PSmallint(Buffer), DataSize, @fAudioPaket); {$ELSEIF 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.