From faf4c13bf41a17ce920a2194fc396f8bf7b44331 Mon Sep 17 00:00:00 2001 From: tobigun Date: Wed, 2 Jul 2008 07:50:39 +0000 Subject: Audio/Video engine update: - lyrics<->audio synchronisation (TSyncSource) - better resampling (optional support for libsamplerate) - cleaner termination of audio/video streams/devices - improved decoders and decoder infrastructure - many other improvements/cleanups Currently just for testing (not enabled by default): - Background music - Voice-Passthrough (hear what you sing) - Video VSync git-svn-id: svn://svn.code.sf.net/p/ultrastardx/svn/trunk@1157 b956fd51-792f-4845-bead-9b4dfca2ff2c --- Game/Code/Classes/UAudioConverter.pas | 457 +++++++ Game/Code/Classes/UAudioCore_Bass.pas | 18 +- Game/Code/Classes/UAudioCore_Portaudio.pas | 3 +- Game/Code/Classes/UAudioDecoder_Bass.pas | 242 ++++ Game/Code/Classes/UAudioDecoder_FFMpeg.pas | 1522 +++++++++++++----------- Game/Code/Classes/UAudioInput_Bass.pas | 39 +- Game/Code/Classes/UAudioInput_Portaudio.pas | 9 +- Game/Code/Classes/UAudioPlaybackBase.pas | 96 +- Game/Code/Classes/UAudioPlayback_Bass.pas | 686 ++++++----- Game/Code/Classes/UAudioPlayback_Portaudio.pas | 105 +- Game/Code/Classes/UAudioPlayback_SDL.pas | 60 +- Game/Code/Classes/UAudioPlayback_SoftMixer.pas | 1002 ++++++++++------ Game/Code/Classes/UCommon.pas | 61 +- Game/Code/Classes/UConfig.pas | 6 + Game/Code/Classes/UGraphic.pas | 14 +- Game/Code/Classes/UMain.pas | 11 +- Game/Code/Classes/UMediaCore_FFMpeg.pas | 405 +++++++ Game/Code/Classes/UMediaCore_SDL.pas | 38 + Game/Code/Classes/UMedia_dummy.pas | 135 ++- Game/Code/Classes/UMusic.pas | 877 ++++++++++---- Game/Code/Classes/URecord.pas | 259 ++-- Game/Code/Classes/URingBuffer.pas | 128 ++ Game/Code/Classes/UTime.pas | 65 +- Game/Code/Classes/UVideo.pas | 601 +++++----- Game/Code/Classes/UVisualizer.pas | 141 +-- Game/Code/Menu/UMenu.pas | 3 - Game/Code/Screens/UScreenSing.pas | 104 +- Game/Code/UltraStar.dpr | 76 +- Game/Code/config-win.inc | 9 +- Game/Code/switches.inc | 10 +- 30 files changed, 4781 insertions(+), 2401 deletions(-) create mode 100644 Game/Code/Classes/UAudioConverter.pas create mode 100644 Game/Code/Classes/UAudioDecoder_Bass.pas create mode 100644 Game/Code/Classes/UMediaCore_FFMpeg.pas create mode 100644 Game/Code/Classes/UMediaCore_SDL.pas create mode 100644 Game/Code/Classes/URingBuffer.pas (limited to 'Game') diff --git a/Game/Code/Classes/UAudioConverter.pas b/Game/Code/Classes/UAudioConverter.pas new file mode 100644 index 00000000..aa7918fe --- /dev/null +++ b/Game/Code/Classes/UAudioConverter.pas @@ -0,0 +1,457 @@ +unit UAudioConverter; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + +uses + UMusic, + ULog, + {$IFDEF UseSRCResample} + samplerate, + {$ENDIF} + {$IFDEF UseFFMpegResample} + avcodec, + {$ENDIF} + UMediaCore_SDL, + sdl, + SysUtils, + Math; + +type + {* + * Notes: + * - 44.1kHz to 48kHz conversion or vice versa is not supported + * by SDL 1.2 (will be introduced in 1.3). + * No conversion takes place in this cases. + * This is because SDL just converts differences in powers of 2. + * So the result might not be that accurate. + * This IS audible (voice to high/low) and it needs good synchronization + * with the video or the lyrics timer. + * - float<->int16 conversion is not supported (will be part of 1.3) and + * SDL (<1.3) is not capable of handling floats at all. + * -> Using FFMpeg or libsamplerate for resampling is preferred. + * Use SDL for channel and format conversion only. + *} + TAudioConverter_SDL = class(TAudioConverter) + private + cvt: TSDL_AudioCVT; + public + function Init(SrcFormatInfo: TAudioFormatInfo; DstFormatInfo: TAudioFormatInfo): boolean; override; + destructor Destroy(); override; + + function Convert(InputBuffer: PChar; OutputBuffer: PChar; var InputSize: integer): integer; override; + function GetOutputBufferSize(InputSize: integer): integer; override; + function GetRatio(): double; override; + end; + + {$IFDEF UseFFMpegResample} + // Note: FFMpeg seems to be using "kaiser windowed sinc" for resampling, so + // the quality should be good. + TAudioConverter_FFMpeg = class(TAudioConverter) + private + // TODO: use SDL for multi-channel->stereo and format conversion + ResampleContext: PReSampleContext; + Ratio: double; + public + function Init(SrcFormatInfo: TAudioFormatInfo; DstFormatInfo: TAudioFormatInfo): boolean; override; + destructor Destroy(); override; + + function Convert(InputBuffer: PChar; OutputBuffer: PChar; var InputSize: integer): integer; override; + function GetOutputBufferSize(InputSize: integer): integer; override; + function GetRatio(): double; override; + end; + {$ENDIF} + + {$IFDEF UseSRCResample} + TAudioConverter_SRC = class(TAudioConverter) + private + ConverterState: PSRC_STATE; + ConversionData: SRC_DATA; + FormatConverter: TAudioConverter; + public + function Init(SrcFormatInfo: TAudioFormatInfo; DstFormatInfo: TAudioFormatInfo): boolean; override; + destructor Destroy(); override; + + function Convert(InputBuffer: PChar; OutputBuffer: PChar; var InputSize: integer): integer; override; + function GetOutputBufferSize(InputSize: integer): integer; override; + function GetRatio(): double; override; + end; + + // Note: SRC (=libsamplerate) provides several converters with different quality + // speed trade-offs. The SINC-types are slow but offer best quality. + // The SRC_SINC_* converters are too slow for realtime conversion, + // (SRC_SINC_FASTEST is approx. ten times slower than SRC_LINEAR) resulting + // in audible clicks and pops. + // SRC_LINEAR is very fast and should have a better quality than SRC_ZERO_ORDER_HOLD + // because it interpolates the samples. Normal "non-audiophile" users should not + // be able to hear a difference between the SINC_* ones and LINEAR. Especially + // if people sing along with the song. + // But FFMpeg might offer a better quality/speed ratio than SRC_LINEAR. + const + SRC_CONVERTER_TYPE = SRC_LINEAR; + {$ENDIF} + +implementation + +function TAudioConverter_SDL.Init(srcFormatInfo: TAudioFormatInfo; dstFormatInfo: TAudioFormatInfo): boolean; +var + srcFormat: UInt16; + dstFormat: UInt16; +begin + inherited Init(SrcFormatInfo, DstFormatInfo); + + Result := false; + + if (not ConvertAudioFormatToSDL(srcFormatInfo.Format, srcFormat) or + not ConvertAudioFormatToSDL(dstFormatInfo.Format, dstFormat)) then + begin + Log.LogError('Audio-format not supported by SDL', 'TSoftMixerPlaybackStream.InitFormatConversion'); + Exit; + end; + + if (SDL_BuildAudioCVT(@cvt, + srcFormat, srcFormatInfo.Channels, Round(srcFormatInfo.SampleRate), + dstFormat, dstFormatInfo.Channels, Round(dstFormatInfo.SampleRate)) = -1) then + begin + Log.LogError(SDL_GetError(), 'TSoftMixerPlaybackStream.InitFormatConversion'); + Exit; + end; + + Result := true; +end; + +destructor TAudioConverter_SDL.Destroy(); +begin + // nothing to be done here + inherited; +end; + +(* + * Returns the size of the output buffer. This might be bigger than the actual + * size of resampled audio data. + *) +function TAudioConverter_SDL.GetOutputBufferSize(InputSize: integer): integer; +begin + // Note: len_ratio must not be used here. Even if the len_ratio is 1.0, len_mult might be 2. + // Example: 44.1kHz/mono to 22.05kHz/stereo -> len_ratio=1, len_mult=2 + Result := InputSize * cvt.len_mult; +end; + +function TAudioConverter_SDL.GetRatio(): double; +begin + Result := cvt.len_ratio; +end; + +function TAudioConverter_SDL.Convert(InputBuffer: PChar; OutputBuffer: PChar; var InputSize: integer): integer; +begin + Result := -1; + + if (InputSize <= 0) then + begin + // avoid div-by-zero problems + if (InputSize = 0) then + Result := 0; + Exit; + end; + + // OutputBuffer is always bigger than or equal to InputBuffer + Move(InputBuffer[0], OutputBuffer[0], InputSize); + cvt.buf := PUint8(OutputBuffer); + cvt.len := InputSize; + if (SDL_ConvertAudio(@cvt) = -1) then + Exit; + + Result := cvt.len_cvt; +end; + + +{$IFDEF UseFFMpegResample} + +function TAudioConverter_FFMpeg.Init(SrcFormatInfo: TAudioFormatInfo; DstFormatInfo: TAudioFormatInfo): boolean; +begin + inherited Init(SrcFormatInfo, DstFormatInfo); + + Result := false; + + // Note: ffmpeg does not support resampling for more than 2 input channels + + if (srcFormatInfo.Format <> asfS16) then + begin + Log.LogError('Unsupported format', 'TAudioConverter_FFMpeg.Init'); + Exit; + end; + + // TODO: use SDL here + if (srcFormatInfo.Format <> dstFormatInfo.Format) then + begin + Log.LogError('Incompatible formats', 'TAudioConverter_FFMpeg.Init'); + Exit; + end; + + ResampleContext := audio_resample_init( + dstFormatInfo.Channels, srcFormatInfo.Channels, + Round(dstFormatInfo.SampleRate), Round(srcFormatInfo.SampleRate)); + if (ResampleContext = nil) then + begin + Log.LogError('audio_resample_init() failed', 'TAudioConverter_FFMpeg.Init'); + Exit; + end; + + // calculate ratio + Ratio := (dstFormatInfo.Channels / srcFormatInfo.Channels) * + (dstFormatInfo.SampleRate / srcFormatInfo.SampleRate); + + Result := true; +end; + +destructor TAudioConverter_FFMpeg.Destroy(); +begin + if (ResampleContext <> nil) then + audio_resample_close(ResampleContext); + inherited; +end; + +function TAudioConverter_FFMpeg.Convert(InputBuffer: PChar; OutputBuffer: PChar; var InputSize: integer): integer; +var + InputSampleCount: integer; + OutputSampleCount: integer; +begin + Result := -1; + + if (InputSize <= 0) then + begin + // avoid div-by-zero in audio_resample() + if (InputSize = 0) then + Result := 0; + Exit; + end; + + InputSampleCount := InputSize div SrcFormatInfo.FrameSize; + OutputSampleCount := audio_resample( + ResampleContext, PSmallInt(OutputBuffer), PSmallInt(InputBuffer), + InputSampleCount); + if (OutputSampleCount = -1) then + begin + Log.LogError('audio_resample() failed', 'TAudioConverter_FFMpeg.Convert'); + Exit; + end; + Result := OutputSampleCount * DstFormatInfo.FrameSize; +end; + +function TAudioConverter_FFMpeg.GetOutputBufferSize(InputSize: integer): integer; +begin + Result := Ceil(InputSize * GetRatio()); +end; + +function TAudioConverter_FFMpeg.GetRatio(): double; +begin + Result := Ratio; +end; + +{$ENDIF} + + +{$IFDEF UseSRCResample} + +function TAudioConverter_SRC.Init(SrcFormatInfo: TAudioFormatInfo; DstFormatInfo: TAudioFormatInfo): boolean; +var + error: integer; + TempSrcFormatInfo: TAudioFormatInfo; + TempDstFormatInfo: TAudioFormatInfo; +begin + inherited Init(SrcFormatInfo, DstFormatInfo); + + Result := false; + + FormatConverter := nil; + + // SRC does not handle channel or format conversion + if ((SrcFormatInfo.Channels <> DstFormatInfo.Channels) or + not (SrcFormatInfo.Format in [asfS16, asfFloat])) then + begin + // SDL can not convert to float, so we have to convert to SInt16 first + TempSrcFormatInfo := TAudioFormatInfo.Create( + SrcFormatInfo.Channels, SrcFormatInfo.SampleRate, SrcFormatInfo.Format); + TempDstFormatInfo := TAudioFormatInfo.Create( + DstFormatInfo.Channels, SrcFormatInfo.SampleRate, asfS16); + + // init format/channel conversion + FormatConverter := TAudioConverter_SDL.Create(); + if (not FormatConverter.Init(TempSrcFormatInfo, TempDstFormatInfo)) then + begin + Log.LogError('Unsupported input format', 'TAudioConverter_SRC.Init'); + FormatConverter.Free; + // exit after the format-info is freed + end; + + // this info was copied so we do not need it anymore + TempSrcFormatInfo.Free; + TempDstFormatInfo.Free; + + // leave if the format is not supported + if (not assigned(FormatConverter)) then + Exit; + + // adjust our copy of the input audio-format for SRC conversion + Self.SrcFormatInfo.Channels := DstFormatInfo.Channels; + Self.SrcFormatInfo.Format := asfS16; + end; + + if ((DstFormatInfo.Format <> asfS16) and + (DstFormatInfo.Format <> asfFloat)) then + begin + Log.LogError('Unsupported output format', 'TAudioConverter_SRC.Init'); + Exit; + end; + + ConversionData.src_ratio := DstFormatInfo.SampleRate / SrcFormatInfo.SampleRate; + if (src_is_valid_ratio(ConversionData.src_ratio) = 0) then + begin + Log.LogError('Invalid samplerate ratio', 'TAudioConverter_SRC.Init'); + Exit; + end; + + ConverterState := src_new(SRC_CONVERTER_TYPE, DstFormatInfo.Channels, @error); + if (ConverterState = nil) then + begin + Log.LogError('src_new() failed: ' + src_strerror(error), 'TAudioConverter_SRC.Init'); + Exit; + end; + + Result := true; +end; + +destructor TAudioConverter_SRC.Destroy(); +begin + if (ConverterState <> nil) then + src_delete(ConverterState); + FormatConverter.Free; + inherited; +end; + +function TAudioConverter_SRC.Convert(InputBuffer: PChar; OutputBuffer: PChar; var InputSize: integer): integer; +var + FloatInputBuffer: PSingle; + FloatOutputBuffer: PSingle; + TempBuffer: PChar; + TempSize: integer; + NumSamples: integer; + OutputSize: integer; + error: integer; +begin + Result := -1; + + TempBuffer := nil; + + // format conversion with external converter (to correct number of channels and format) + if (assigned(FormatConverter)) then + begin + TempSize := FormatConverter.GetOutputBufferSize(InputSize); + GetMem(TempBuffer, TempSize); + InputSize := FormatConverter.Convert(InputBuffer, TempBuffer, InputSize); + InputBuffer := TempBuffer; + end; + + if (InputSize <= 0) then + begin + // avoid div-by-zero problems + if (InputSize = 0) then + Result := 0; + if (TempBuffer <> nil) then + FreeMem(TempBuffer); + Exit; + end; + + if (SrcFormatInfo.Format = asfFloat) then + begin + FloatInputBuffer := PSingle(InputBuffer); + end else begin + NumSamples := InputSize div AudioSampleSize[SrcFormatInfo.Format]; + GetMem(FloatInputBuffer, NumSamples * SizeOf(Single)); + src_short_to_float_array(PSmallInt(InputBuffer), FloatInputBuffer, NumSamples); + end; + + // calculate approx. output size + OutputSize := Ceil(InputSize * ConversionData.src_ratio); + + if (DstFormatInfo.Format = asfFloat) then + begin + FloatOutputBuffer := PSingle(OutputBuffer); + end else begin + NumSamples := OutputSize div AudioSampleSize[DstFormatInfo.Format]; + GetMem(FloatOutputBuffer, NumSamples * SizeOf(Single)); + end; + + with ConversionData do + begin + data_in := FloatInputBuffer; + input_frames := InputSize div SrcFormatInfo.FrameSize; + data_out := FloatOutputBuffer; + output_frames := OutputSize div DstFormatInfo.FrameSize; + // TODO: set this to 1 at end of file-playback + end_of_input := 0; + end; + + error := src_process(ConverterState, @ConversionData); + if (error <> 0) then + begin + Log.LogError(src_strerror(error), 'TAudioConverter_SRC.Convert'); + if (SrcFormatInfo.Format <> asfFloat) then + FreeMem(FloatInputBuffer); + if (DstFormatInfo.Format <> asfFloat) then + FreeMem(FloatOutputBuffer); + if (TempBuffer <> nil) then + FreeMem(TempBuffer); + Exit; + end; + + if (SrcFormatInfo.Format <> asfFloat) then + FreeMem(FloatInputBuffer); + + if (DstFormatInfo.Format <> asfFloat) then + begin + NumSamples := ConversionData.output_frames_gen * DstFormatInfo.Channels; + src_float_to_short_array(FloatOutputBuffer, PSmallInt(OutputBuffer), NumSamples); + FreeMem(FloatOutputBuffer); + end; + + // free format conversion buffer if used + if (TempBuffer <> nil) then + FreeMem(TempBuffer); + + if (assigned(FormatConverter)) then + InputSize := ConversionData.input_frames_used * FormatConverter.SrcFormatInfo.FrameSize + else + InputSize := ConversionData.input_frames_used * SrcFormatInfo.FrameSize; + + // set result to output size according to SRC + Result := ConversionData.output_frames_gen * DstFormatInfo.FrameSize; +end; + +function TAudioConverter_SRC.GetOutputBufferSize(InputSize: integer): integer; +begin + Result := Ceil(InputSize * GetRatio()); +end; + +function TAudioConverter_SRC.GetRatio(): double; +begin + // if we need additional channel/format conversion, use this ratio + if (assigned(FormatConverter)) then + Result := FormatConverter.GetRatio() + else + Result := 1.0; + + // now the SRC ratio (Note: the format might change from SInt16 to float) + Result := Result * + ConversionData.src_ratio * + (DstFormatInfo.FrameSize / SrcFormatInfo.FrameSize); +end; + +{$ENDIF} + +end. \ No newline at end of file diff --git a/Game/Code/Classes/UAudioCore_Bass.pas b/Game/Code/Classes/UAudioCore_Bass.pas index 1f754be2..beb2db16 100644 --- a/Game/Code/Classes/UAudioCore_Bass.pas +++ b/Game/Code/Classes/UAudioCore_Bass.pas @@ -16,13 +16,13 @@ uses type TAudioCore_Bass = class - private - constructor Create(); public + constructor Create(); class function GetInstance(): TAudioCore_Bass; function ErrorGetString(): string; overload; function ErrorGetString(errCode: integer): string; overload; function ConvertAudioFormatToBASSFlags(Format: TAudioSampleFormat; out Flags: DWORD): boolean; + function ConvertBASSFlagsToAudioFormat(Flags: DWORD; out Format: TAudioSampleFormat): boolean; end; implementation @@ -41,7 +41,7 @@ end; class function TAudioCore_Bass.GetInstance(): TAudioCore_Bass; begin - if not assigned(Instance) then + if (not Assigned(Instance)) then Instance := TAudioCore_Bass.Create(); Result := Instance; end; @@ -108,4 +108,16 @@ begin Result := true; end; +function TAudioCore_Bass.ConvertBASSFlagsToAudioFormat(Flags: DWORD; out Format: TAudioSampleFormat): boolean; +begin + if ((Flags and BASS_SAMPLE_FLOAT) <> 0) then + Format := asfFloat + else if ((Flags and BASS_SAMPLE_8BITS) <> 0) then + Format := asfU8 + else + Format := asfS16; + + Result := true; +end; + end. diff --git a/Game/Code/Classes/UAudioCore_Portaudio.pas b/Game/Code/Classes/UAudioCore_Portaudio.pas index 90395cb8..bcc8a001 100644 --- a/Game/Code/Classes/UAudioCore_Portaudio.pas +++ b/Game/Code/Classes/UAudioCore_Portaudio.pas @@ -16,9 +16,8 @@ uses type TAudioCore_Portaudio = class - private - constructor Create(); public + constructor Create(); class function GetInstance(): TAudioCore_Portaudio; function GetPreferredApiIndex(): TPaHostApiIndex; function TestDevice(inParams, outParams: PPaStreamParameters; var sampleRate: Double): boolean; diff --git a/Game/Code/Classes/UAudioDecoder_Bass.pas b/Game/Code/Classes/UAudioDecoder_Bass.pas new file mode 100644 index 00000000..dba1fde4 --- /dev/null +++ b/Game/Code/Classes/UAudioDecoder_Bass.pas @@ -0,0 +1,242 @@ +unit UAudioDecoder_Bass; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + +implementation + +uses + Classes, + SysUtils, + UMain, + UMusic, + UAudioCore_Bass, + ULog, + bass; + +type + TBassDecodeStream = class(TAudioDecodeStream) + private + Handle: HSTREAM; + FormatInfo : TAudioFormatInfo; + Error: boolean; + public + constructor Create(Handle: HSTREAM); + destructor Destroy(); override; + + 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: PChar; BufSize: integer): integer; override; + end; + +type + TAudioDecoder_Bass = class( TInterfacedObject, IAudioDecoder ) + public + function GetName: string; + + function InitializeDecoder(): boolean; + function FinalizeDecoder(): boolean; + function Open(const Filename: string): TAudioDecodeStream; + end; + +var + BassCore: TAudioCore_Bass; + + +{ TBassDecodeStream } + +constructor TBassDecodeStream.Create(Handle: HSTREAM); +var + ChannelInfo: BASS_CHANNELINFO; + Format: TAudioSampleFormat; +begin + inherited Create(); + Self.Handle := Handle; + + // setup format info + if (not BASS_ChannelGetInfo(Handle, ChannelInfo)) then + begin + raise Exception.Create('Failed to open decode-stream'); + end; + BassCore.ConvertBASSFlagsToAudioFormat(ChannelInfo.flags, Format); + FormatInfo := TAudioFormatInfo.Create(ChannelInfo.chans, ChannelInfo.freq, format); + + Error := false; +end; + +destructor TBassDecodeStream.Destroy(); +begin + Close(); + inherited; +end; + +procedure TBassDecodeStream.Close(); +begin + if (Handle <> 0) then + begin + BASS_StreamFree(Handle); + Handle := 0; + end; + PerformOnClose(); + FreeAndNil(FormatInfo); + Error := false; +end; + +function TBassDecodeStream.GetAudioFormatInfo(): TAudioFormatInfo; +begin + Result := FormatInfo; +end; + +function TBassDecodeStream.GetLength(): real; +var + bytes: QWORD; +begin + bytes := BASS_ChannelGetLength(Handle, BASS_POS_BYTE); + Result := BASS_ChannelBytes2Seconds(Handle, bytes); +end; + +function TBassDecodeStream.GetPosition: real; +var + bytes: QWORD; +begin + bytes := BASS_ChannelGetPosition(Handle, BASS_POS_BYTE); + Result := BASS_ChannelBytes2Seconds(Handle, bytes); +end; + +procedure TBassDecodeStream.SetPosition(Time: real); +var + bytes: QWORD; +begin + bytes := BASS_ChannelSeconds2Bytes(Handle, Time); + BASS_ChannelSetPosition(Handle, bytes, BASS_POS_BYTE); +end; + +function TBassDecodeStream.GetLoop(): boolean; +var + flags: DWORD; +begin + // retrieve channel flags + flags := BASS_ChannelFlags(Handle, 0, 0); + if (flags = DWORD(-1)) then + begin + Log.LogError('BASS_ChannelFlags: ' + BassCore.ErrorGetString(), 'TBassDecodeStream.GetLoop'); + Result := false; + Exit; + end; + Result := (flags and BASS_SAMPLE_LOOP) <> 0; +end; + +procedure TBassDecodeStream.SetLoop(Enabled: boolean); +var + flags: DWORD; +begin + // set/unset loop-flag + if (Enabled) then + flags := BASS_SAMPLE_LOOP + else + flags := 0; + + // set new flag-bits + if (BASS_ChannelFlags(Handle, flags, BASS_SAMPLE_LOOP) = DWORD(-1)) then + begin + Log.LogError('BASS_ChannelFlags: ' + BassCore.ErrorGetString(), 'TBassDecodeStream.SetLoop'); + Exit; + end; +end; + +function TBassDecodeStream.IsEOF(): boolean; +begin + Result := (BASS_ChannelIsActive(Handle) = BASS_ACTIVE_STOPPED); +end; + +function TBassDecodeStream.IsError(): boolean; +begin + Result := Error; +end; + +function TBassDecodeStream.ReadData(Buffer: PChar; BufSize: integer): integer; +begin + Result := BASS_ChannelGetData(Handle, Buffer, BufSize); + // check error state (do not handle EOF as error) + if ((Result = -1) and (BASS_ErrorGetCode() <> BASS_ERROR_ENDED)) then + Error := true + else + Error := false; +end; + + +{ TAudioDecoder_Bass } + +function TAudioDecoder_Bass.GetName: String; +begin + result := 'BASS_Decoder'; +end; + +function TAudioDecoder_Bass.InitializeDecoder(): boolean; +begin + BassCore := TAudioCore_Bass.GetInstance(); + Result := true; +end; + +function TAudioDecoder_Bass.FinalizeDecoder(): boolean; +begin + Result := true; +end; + +function TAudioDecoder_Bass.Open(const Filename: string): TAudioDecodeStream; +var + Stream: HSTREAM; + ChannelInfo: BASS_CHANNELINFO; + FileExt: string; +begin + Result := nil; + + // check if BASS was initialized + // in case the decoder is not used with BASS playback, init the NO_SOUND device + if ((integer(BASS_GetDevice) = -1) and (BASS_ErrorGetCode() = BASS_ERROR_INIT)) then + BASS_Init(0, 44100, 0, 0, nil); + + // TODO: use BASS_STREAM_PRESCAN for accurate seeking in VBR-files? + // disadvantage: seeking will slow down. + Stream := BASS_StreamCreateFile(False, PChar(Filename), 0, 0, BASS_STREAM_DECODE); + if (Stream = 0) then + begin + //Log.LogError(BassCore.ErrorGetString(), 'TAudioDecoder_Bass.Open'); + Exit; + end; + + // check if BASS opened some erroneously recognized file-formats + if BASS_ChannelGetInfo(Stream, channelInfo) then + begin + fileExt := ExtractFileExt(Filename); + // BASS opens FLV-files (maybe others too) although it cannot handle them. + // Setting BASS_CONFIG_VERIFY to the max. value (100000) does not help. + if ((fileExt = '.flv') and (channelInfo.ctype = BASS_CTYPE_STREAM_MP1)) then + begin + BASS_StreamFree(Stream); + Exit; + end; + end; + + Result := TBassDecodeStream.Create(Stream); +end; + + +initialization + MediaManager.Add(TAudioDecoder_Bass.Create); + +end. diff --git a/Game/Code/Classes/UAudioDecoder_FFMpeg.pas b/Game/Code/Classes/UAudioDecoder_FFMpeg.pas index 2a9b7518..a9c5863b 100644 --- a/Game/Code/Classes/UAudioDecoder_FFMpeg.pas +++ b/Game/Code/Classes/UAudioDecoder_FFMpeg.pas @@ -1,15 +1,15 @@ 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 - -*******************************************************************************) + * + * 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 @@ -19,682 +19,1032 @@ interface {$I switches.inc} -//{$DEFINE DebugFFMpegDecode} - -uses - Classes, - SysUtils, - UMusic; +{.$DEFINE DebugFFMpegDecode} implementation uses + Classes, + SysUtils, + Math, + UMusic, UIni, UMain, - avcodec, // FFMpeg Audio file decoding + avcodec, avformat, avutil, - avio, // used for url_ferror + avio, mathematics, // used for av_rescale_q rational, + UMediaCore_FFMpeg, SDL, ULog, UCommon, UConfig; -type - PPacketQueue = ^TPacketQueue; - TPacketQueue = class - private - firstPkt, - lastPkt : PAVPacketList; - nbPackets : integer; - size : integer; - mutex : PSDL_Mutex; - cond : PSDL_Cond; - abortRequest: boolean; - - public - constructor Create(); - destructor Destroy(); override; - - function Put(pkt : PAVPacket): integer; - function PutStatus(statusFlag: integer; statusInfo: Pointer): integer; - function Get(var pkt: TAVPacket; block: boolean): integer; - procedure Flush(); - procedure Abort(); - end; - const MAX_AUDIOQ_SIZE = (5 * 16 * 1024); const - STATUS_PACKET: PChar = 'STATUS_PACKET'; -const - PKT_STATUS_FLAG_EOF = 1; - PKT_STATUS_FLAG_FLUSH = 2; - PKT_STATUS_FLAG_ERROR = 3; - -type - PAudioBuffer = ^TAudioBuffer; - // TODO: should (or must?) be aligned at a 2-byte boundary. - // ffmpeg provides a C-macro called DECLARE_ALIGNED for this. - // Records can be aligned with the $PACKRECORDS compiler-directive but - // are already aligned at a 2-byte boundary by default in FPC. - // But what about arrays, are they aligned by a 2-byte boundary too? - // Or maybe we have to define a fake record with only an array in it? - TAudioBuffer = array[0 .. (AVCODEC_MAX_AUDIO_FRAME_SIZE * 3 div 2)-1] of byte; + // 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 - decoderLock : PSDL_Mutex; - parserLock : PSDL_Mutex; - myint: integer; - - EOFState: boolean; // end-of-stream flag - ErrorState: boolean; - - resumeCond : PSDL_Cond; - - quitRequest : boolean; - - seekRequest: boolean; - seekFlags : integer; - seekPos : int64; - seekCond : PSDL_Cond; - - parseThread: PSDL_Thread; - packetQueue: TPacketQueue; - - formatInfo : TAudioFormatInfo; - - // FFMpeg internal data - pFormatCtx : PAVFormatContext; - pCodecCtx : PAVCodecContext; - pCodec : PAVCodec; - ffmpegStreamIndex : Integer; - ffmpegStream : PAVStream; - - audioClock: double; // stream position in seconds - - // state-vars for DecodeFrame - pkt : TAVPacket; - audio_pkt_data : PChar; - audio_pkt_size : integer; - - // state-vars for AudioCallback - audio_buf_index : integer; - audio_buf_size : integer; - audio_buf : TAudioBuffer; - - procedure LockParser(); {$IFDEF HasInline}inline;{$ENDIF} - procedure UnlockParser(); {$IFDEF HasInline}inline;{$ENDIF} - function GetParserMutex(): PSDL_Mutex; {$IFDEF HasInline}inline;{$ENDIF} - - procedure LockDecoder(); {$IFDEF HasInline}inline;{$ENDIF} - procedure UnlockDecoder(); {$IFDEF HasInline}inline;{$ENDIF} - - procedure SetEOF(state: boolean); {$IFDEF HasInline}inline;{$ENDIF} - procedure SetError(state: boolean); {$IFDEF HasInline}inline;{$ENDIF} - - procedure ParseAudio(); - function DecodeFrame(var buffer: TAudioBuffer; bufSize: integer): integer; + 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: PChar; + AudioPaketSize: integer; + AudioPaketSilence: integer; // number of bytes of silence to return + + // state-vars for AudioCallback (locked by DecoderLock) + AudioBufferPos: integer; + AudioBufferSize: integer; + AudioBuffer: PChar; + + Filename: string; + + 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: PChar; BufferSize: integer): integer; + procedure FlushCodecBuffers(); + procedure PauseDecoder(); + procedure ResumeDecoder(); public - constructor Create(pFormatCtx: PAVFormatContext; - pCodecCtx: PAVCodecContext; pCodec: PAVCodec; - ffmpegStreamIndex: Integer; ffmpegStream: PAVStream); + constructor Create(); destructor Destroy(); override; + function Open(const Filename: string): 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: PChar; BufSize: integer): integer; override; + function ReadData(Buffer: PChar; BufferSize: integer): integer; override; end; type TAudioDecoder_FFMpeg = class( TInterfacedObject, IAudioDecoder ) - private - class function FindAudioStreamIndex(pFormatCtx : PAVFormatContext): integer; public - function GetName: String; + function GetName: string; function InitializeDecoder(): boolean; function FinalizeDecoder(): boolean; function Open(const Filename: string): TAudioDecodeStream; end; -function DecodeThreadMain(streamPtr: Pointer): integer; cdecl; forward; - var - singleton_AudioDecoderFFMpeg : IAudioDecoder; + FFMpegCore: TMediaCore_FFMpeg; + +function ParseThreadMain(Data: Pointer): integer; cdecl; forward; { TFFMpegDecodeStream } -constructor TFFMpegDecodeStream.Create(pFormatCtx: PAVFormatContext; - pCodecCtx: PAVCodecContext; pCodec: PAVCodec; - ffmpegStreamIndex : Integer; ffmpegStream: PAVStream); -var - sampleFormat: TAudioSampleFormat; +constructor TFFMpegDecodeStream.Create(); begin inherited Create(); - packetQueue := TPacketQueue.Create(); - - audio_pkt_data := nil; - audio_pkt_size := 0; - - audio_buf_index := 0; - audio_buf_size := 0; + 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; - FillChar(pkt, sizeof(TAVPacket), 0); +procedure TFFMpegDecodeStream.Reset(); +begin + ParseThread := nil; - Self.pFormatCtx := pFormatCtx; - Self.pCodecCtx := pCodecCtx; - Self.pCodec := pCodec; - Self.ffmpegStreamIndex := ffmpegStreamIndex; - Self.ffmpegStream := ffmpegStream; + EOFState := false; + ErrorState := false; + Loop := false; + QuitRequest := false; - case pCodecCtx^.sample_fmt of - SAMPLE_FMT_U8: sampleFormat := asfU8; - SAMPLE_FMT_S16: sampleFormat := asfS16; - SAMPLE_FMT_S24: sampleFormat := asfS24; - SAMPLE_FMT_S32: sampleFormat := asfS32; - SAMPLE_FMT_FLT: sampleFormat := asfFloat; - else sampleFormat := asfS16; // try standard format - end; + AudioPaketData := nil; + AudioPaketSize := 0; + AudioPaketSilence := 0; - formatInfo := TAudioFormatInfo.Create( - pCodecCtx^.channels, - pCodecCtx^.sample_rate, - sampleFormat - ); + AudioBufferPos := 0; + AudioBufferSize := 0; - EOFState := false; - ErrorState := false; - decoderLock := SDL_CreateMutex(); - parserLock := SDL_CreateMutex(); - resumeCond := SDL_CreateCond(); - seekCond := SDL_CreateCond(); + ParserLocked := false; + ParserPauseRequestCount := 0; + DecoderLocked := false; + DecoderPauseRequestCount := 0; - parseThread := SDL_CreateThread(@DecodeThreadMain, Self); + FillChar(AudioPaket, SizeOf(TAVPacket), 0); end; {* * Frees the decode-stream data. - * IMPORTANT: call Close() before freeing the decode-stream to avoid dead-locks. - * This wakes-up every waiting audio-thread waiting in the packet-queue while - * performing a ReadData() request. - * Then assure that no thread uses ReadData anymore (e.g. by stopping the audio-callback). - * Now you can free the decode-stream. *} destructor TFFMpegDecodeStream.Destroy(); begin - // wake-up and terminate threads - // Note: should be called by the caller before Destroy() was called instead - // to wake-up a waiting audio-callback thread in the packet-queue. - // Otherwise dead-locks are possible. Close(); - // Close the codec - if (pCodecCtx <> nil) then + 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: string): boolean; +var + SampleFormat: TAudioSampleFormat; + AVResult: integer; +begin + Result := false; + + Close(); + Reset(); + + if (not FileExists(Filename)) then begin - avcodec_close(pCodecCtx); - pCodecCtx := nil; + Log.LogError('Audio-file does not exist: "' + Filename + '"', 'UAudio_FFMpeg'); + Exit; end; - // Close the video file - if (pFormatCtx <> nil) then + Self.Filename := Filename; + + // open audio file + if (av_open_input_file(FormatCtx, PChar(Filename), nil, 0, nil) <> 0) then + begin + Log.LogError('av_open_input_file failed: "' + Filename + '"', '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 - av_close_input_file(pFormatCtx); - pFormatCtx := nil; + Log.LogError('av_find_stream_info failed: "' + Filename + '"', 'UAudio_FFMpeg'); + Close(); + Exit; end; - FreeAndNil(packetQueue); - FreeAndNil(formatInfo); + // FIXME: hack used by ffplay. Maybe should not use url_feof() to test for the end + FormatCtx^.pb.eof_reached := 0; - SDL_DestroyMutex(decoderLock); - decoderLock := nil; - SDL_DestroyMutex(parserLock); - parserLock := nil; - SDL_DestroyCond(resumeCond); - resumeCond := nil; + {$IFDEF DebugFFMpegDecode} + dump_format(FormatCtx, 0, pchar(Filename), 0); + {$ENDIF} - inherited; + AudioStreamIndex := FFMpegCore.FindAudioStreamIndex(FormatCtx); + if (AudioStreamIndex < 0) then + begin + Log.LogError('FindAudioStreamIndex: No Audio-stream found "' + Filename + '"', 'UAudio_FFMpeg'); + Close(); + Exit; + end; + + //Log.LogStatus('AudioStreamIndex is: '+ inttostr(ffmpegStreamID), 'UAudio_FFMpeg'); + + AudioStream := FormatCtx.streams[AudioStreamIndex]; + 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; + + FormatInfo := TAudioFormatInfo.Create( + 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 - status: integer; + ThreadResult: integer; begin // wake threads waiting for packet-queue data - packetQueue.Abort(); + // 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 - LockParser(); - quitRequest := true; - SDL_CondBroadcast(resumeCond); - UnlockParser(); - // and wait until it terminates - if (parseThread <> nil) then + 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 - SDL_WaitThread(parseThread, status); - parseThread := nil; + // avcodec_close() is not thread-safe + FFMpegCore.LockAVCodec(); + try + avcodec_close(CodecCtx); + finally + FFMpegCore.UnlockAVCodec(); + end; + CodecCtx := nil; end; - // NOTE: we cannot free the codecCtx or formatCtx here because - // a formerly waiting thread in the packet-queue might require them - // and crash if it tries to access them. + // Close the video file + if (FormatCtx <> nil) then + begin + av_close_input_file(FormatCtx); + FormatCtx := nil; + end; + + PerformOnClose(); + + FreeAndNil(PacketQueue); + FreeAndNil(FormatInfo); end; -procedure TFFMpegDecodeStream.LockParser(); +function TFFMpegDecodeStream.GetLength(): real; begin - SDL_mutexP(parserLock); + // do not forget to consider the start_time value here + Result := (FormatCtx^.start_time + FormatCtx^.duration) / AV_TIME_BASE; end; -procedure TFFMpegDecodeStream.UnlockParser(); +function TFFMpegDecodeStream.GetAudioFormatInfo(): TAudioFormatInfo; begin - SDL_mutexV(parserLock); + Result := FormatInfo; end; -function TFFMpegDecodeStream.GetParserMutex(): PSDL_Mutex; +function TFFMpegDecodeStream.IsEOF(): boolean; begin - Result := parserLock; + SDL_mutexP(StateLock); + Result := EOFState; + SDL_mutexV(StateLock); end; -procedure TFFMpegDecodeStream.LockDecoder(); +procedure TFFMpegDecodeStream.SetEOF(State: boolean); begin - SDL_mutexP(decoderLock); + SDL_mutexP(StateLock); + EOFState := State; + SDL_mutexV(StateLock); end; -procedure TFFMpegDecodeStream.UnlockDecoder(); +function TFFMpegDecodeStream.IsError(): boolean; begin - SDL_mutexV(decoderLock); + SDL_mutexP(StateLock); + Result := ErrorState; + SDL_mutexV(StateLock); end; -function TFFMpegDecodeStream.GetLength(): real; +procedure TFFMpegDecodeStream.SetError(State: boolean); begin - Result := pFormatCtx^.duration / AV_TIME_BASE; + SDL_mutexP(StateLock); + ErrorState := State; + SDL_mutexV(StateLock); end; -function TFFMpegDecodeStream.GetAudioFormatInfo(): TAudioFormatInfo; +function TFFMpegDecodeStream.IsSeeking(): boolean; begin - Result := formatInfo; + SDL_mutexP(StateLock); + Result := SeekRequest; + SDL_mutexV(StateLock); end; -function TFFMpegDecodeStream.IsEOF(): boolean; +function TFFMpegDecodeStream.IsQuit(): boolean; begin - LockDecoder(); - Result := EOFState; - UnlockDecoder(); + SDL_mutexP(StateLock); + Result := QuitRequest; + SDL_mutexV(StateLock); end; -procedure TFFMpegDecodeStream.SetEOF(state: boolean); +function TFFMpegDecodeStream.GetPosition(): real; +var + BufferSizeSec: double; begin - LockDecoder(); - EOFState := state; - UnlockDecoder(); + 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; -function TFFMpegDecodeStream.IsError(): boolean; +procedure TFFMpegDecodeStream.SetPosition(Time: real); begin - LockDecoder(); - Result := ErrorState; - UnlockDecoder(); + SetPositionIntern(Time, true, true); end; -procedure TFFMpegDecodeStream.SetError(state: boolean); +function TFFMpegDecodeStream.GetLoop(): boolean; begin - LockDecoder(); - ErrorState := state; - UnlockDecoder(); + SDL_mutexP(StateLock); + Result := Loop; + SDL_mutexV(StateLock); end; -(* -procedure TFFMpegDecodeStream.SetError(state: boolean); +procedure TFFMpegDecodeStream.SetLoop(Enabled: boolean); begin - LockDecoder(); - ErrorState := state; - UnlockDecoder(); + SDL_mutexP(StateLock); + Loop := Enabled; + SDL_mutexV(StateLock); end; -function TFFMpegDecodeStream.IsSeeking(): boolean; + +(******************************************** + * Parser section + ********************************************) + +procedure TFFMpegDecodeStream.PauseParser(); begin - LockDecoder(); - Result := seekRequest; - UnlockDecoder(); + if (SDL_ThreadID() = ParseThread.threadid) then + Exit; + + SDL_mutexP(StateLock); + Inc(ParserPauseRequestCount); + while (ParserLocked) do + SDL_CondWait(ParserUnlockedCond, StateLock); + SDL_mutexV(StateLock); end; -*) -function TFFMpegDecodeStream.GetPosition(): real; +procedure TFFMpegDecodeStream.ResumeParser(); begin - // FIXME: the audio-clock might not be that accurate - // see: tutorial on synching (audio-clock) - Result := audioClock; + if (SDL_ThreadID() = ParseThread.threadid) then + Exit; + + SDL_mutexP(StateLock); + Dec(ParserPauseRequestCount); + SDL_CondSignal(ParserResumeCond); + SDL_mutexV(StateLock); end; -procedure TFFMpegDecodeStream.SetPosition(Time: real); +procedure TFFMpegDecodeStream.SetPositionIntern(Time: real; Flush: boolean; Blocking: boolean); begin - LockParser(); - - seekPos := Trunc(Time * AV_TIME_BASE); + // - 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); + + // configure seek parameters + SeekPos := Time; + SeekFlush := Flush; + SeekFlags := AVSEEK_FLAG_ANY; + SeekRequest := true; - seekFlags := 0; // 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 < audioClock) then - seekFlags := AVSEEK_FLAG_BACKWARD; - seekFlags := AVSEEK_FLAG_ANY; - -LockDecoder(); - seekRequest := true; -UnlockDecoder(); - SDL_CondSignal(resumeCond); - (* - while ((not quitRequest) and seekRequest) do - SDL_CondWait(seekCond, GetParserMutex()); - *) - UnlockParser(); + if (Time < AudioStreamPos) then + SeekFlags := SeekFlags or AVSEEK_FLAG_BACKWARD; + + EOFState := false; + ErrorState := false; + + // send a reuse signal in case the parser was stopped (e.g. because of an EOF) + SDL_CondSignal(ParserIdleCond); + + SDL_mutexV(StateLock); + ResumeDecoder(); + ResumeParser(); + + // 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 DecodeThreadMain(streamPtr: Pointer): integer; cdecl; +function ParseThreadMain(Data: Pointer): integer; cdecl; var - stream: TFFMpegDecodeStream; + Stream: TFFMpegDecodeStream; begin - stream := TFFMpegDecodeStream(streamPtr); - stream.ParseAudio(); - result := 0; + 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; -procedure TFFMpegDecodeStream.ParseAudio(); +(** + * 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; - statusPacket: PAVPacket; - seekTarget: int64; - stopParsing: boolean; - pbIOCtx: PByteIOContext; - err: integer; - index: integer; + Packet: TAVPacket; + StatusPacket: PAVPacket; + 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 - stopParsing := false; + Result := true; while (true) do begin LockParser(); + try - // wait if end-of-file reached - if (stopParsing) then - begin - // wait for reuse or destruction of stream - while not (seekRequest or quitRequest) do - SDL_CondWait(resumeCond, GetParserMutex()); - end; - - if (quitRequest) then - begin - UnlockParser(); - break; - end; - - // handle seek-request - if (seekRequest) then - begin - // reset status - SetEOF(false); - SetError(false); - stopParsing := false; - - seekTarget := av_rescale_q(seekPos, AV_TIME_BASE_Q, ffmpegStream^.time_base); - err := av_seek_frame(pFormatCtx, ffmpegStreamIndex, seekTarget, seekFlags); - // seeking failed -> retry with the default stream (necessary for flv-videos and some ogg-files) - if (err < 0) then - err := av_seek_frame(pFormatCtx, -1, seekPos, seekFlags); - // check if seeking failed - if (err < 0) then + if (IsQuit()) then begin - Log.LogStatus('Seek Error in "'+pFormatCtx^.filename+'"', 'UAudioDecoder_FFMpeg'); - end - else - begin - packetQueue.Flush(); - packetQueue.PutStatus(PKT_STATUS_FLAG_FLUSH, nil); + Result := false; + Exit; end; - LockDecoder(); - seekRequest := false; - UnlockDecoder(); - SDL_CondSignal(seekCond); - end; - - UnlockParser(); - if (packetQueue.size > MAX_AUDIOQ_SIZE) then - begin - SDL_Delay(10); - continue; - end; - - if (av_read_frame(pFormatCtx, packet) < 0) then - begin - // failed to read a frame, check reason + // 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; - {$IF (LIBAVFORMAT_VERSION_MAJOR >= 52)} - pbIOCtx := pFormatCtx^.pb; - {$ELSE} - pbIOCtx := @pFormatCtx^.pb; - {$IFEND} + // 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; - // check for end-of-file (eof is not an error) - if (url_feof(pbIOCtx) <> 0) then + if (PacketQueue.GetSize() > MAX_AUDIOQ_SIZE) then begin - // signal end-of-file - packetQueue.putStatus(PKT_STATUS_FLAG_EOF, nil); - stopParsing := true; - continue; + SDL_Delay(10); + Continue; end; - // check for errors - if (url_ferror(pbIOCtx) <> 0) then + if (av_read_frame(FormatCtx, Packet) < 0) then begin - // an error occured -> abort and wait for repositioning or termination - packetQueue.putStatus(PKT_STATUS_FLAG_ERROR, nil); - stopParsing := true; - continue; + // 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; + + // no error -> wait for user input + SDL_Delay(100); + Continue; end; - // no error -> wait for user input - SDL_Delay(100); - continue; + if (Packet.stream_index = AudioStreamIndex) then + PacketQueue.Put(@Packet) + else + av_free_packet(@Packet); + + finally + UnlockParser(); end; + end; +end; - if (packet.stream_index = ffmpegStreamIndex) then - begin - packetQueue.put(@packet); - end - else - begin - av_free_packet(@packet); + +(******************************************** + * 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(var buffer: TAudioBuffer; bufSize: integer): integer; +function TFFMpegDecodeStream.DecodeFrame(Buffer: PChar; BufferSize: integer): integer; var - len1, - data_size: integer; + 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; + Result := -1; - if EOF then - exit; + if (EOF) then + Exit; while(true) do begin - while (audio_pkt_size > 0) do + // 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 - data_size := bufSize; + DataSize := BufferSize; {$IF LIBAVCODEC_VERSION >= 51030000} // 51.30.0 - len1 := avcodec_decode_audio2(pCodecCtx, @buffer, - data_size, audio_pkt_data, audio_pkt_size); + PaketDecodedSize := avcodec_decode_audio2(CodecCtx, PSmallint(Buffer), + DataSize, AudioPaketData, AudioPaketSize); {$ELSE} - // FIXME: with avcodec_decode_audio a package could contain several frames - // this is not handled yet - len1 := avcodec_decode_audio(pCodecCtx, @buffer, - data_size, audio_pkt_data, audio_pkt_size); + PaketDecodedSize := avcodec_decode_audio(CodecCtx, PSmallint(Buffer), + DataSize, AudioPaketData, AudioPaketSize); {$IFEND} - if(len1 < 0) then + if(PaketDecodedSize < 0) then begin // if error, skip frame {$IFDEF DebugFFMpegDecode} - DebugWriteln( 'Skip audio frame' ); + DebugWriteln('Skip audio frame'); {$ENDIF} - audio_pkt_size := 0; - break; + AudioPaketSize := 0; + Break; end; - Inc(audio_pkt_data, len1); - Dec(audio_pkt_size, len1); - - if (data_size <= 0) then - begin - // no data yet, get more frames - continue; - end; + Inc(AudioPaketData, PaketDecodedSize); + Dec(AudioPaketSize, PaketDecodedSize); - //pts := audioClock; - audioClock := audioClock + data_size / - (1.0 * formatInfo.FrameSize * formatInfo.SampleRate); + // 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 := data_size; - exit; + Result := DataSize; + Exit; end; - if (pkt.data <> nil) then - begin - av_free_packet(@pkt); - end; + // free old packet data + if (AudioPaket.data <> nil) then + av_free_packet(@AudioPaket); - // do not use an aborted queue - if (packetQueue.abortRequest) then - exit; + // 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 non available. + // request a new packet and block if none available. // If this fails, the queue was aborted. - if (packetQueue.Get(pkt, true) < 0) then - exit; + if (PacketQueue.Get(AudioPaket, BlockQueue) <= 0) then + Exit; // handle Status-packet - if (PChar(pkt.data) = STATUS_PACKET) then + if (PChar(AudioPaket.data) = STATUS_PACKET) then begin - pkt.data := nil; - audio_pkt_data := nil; - audio_pkt_size := 0; + AudioPaket.data := nil; + AudioPaketData := nil; + AudioPaketSize := 0; - case (pkt.flags) of + case (AudioPaket.flags) of PKT_STATUS_FLAG_FLUSH: begin - avcodec_flush_buffers(pCodecCtx); + // just used if SetPositionIntern was called without the flush flag. + FlushCodecBuffers; end; PKT_STATUS_FLAG_EOF: // end-of-file begin - SetEOF(true); + // ignore EOF while seeking + if (not IsSeeking()) then + SetEOF(true); // buffer contains no data -> result = -1 - exit; + Exit; end; PKT_STATUS_FLAG_ERROR: begin SetError(true); Log.LogStatus('I/O Error', 'TFFMpegDecodeStream.DecodeFrame'); - exit; + 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; + Continue; end; - audio_pkt_data := PChar(pkt.data); - audio_pkt_size := pkt.size; + AudioPaketData := PChar(AudioPaket.data); + AudioPaketSize := AudioPaket.size; - // if available, update the audio clock with pts - if(pkt.pts <> AV_NOPTS_VALUE) then + // if available, update the stream position to the presentation time of this package + if(AudioPaket.pts <> AV_NOPTS_VALUE) then begin - audioClock := av_q2d(ffmpegStream^.time_base) * pkt.pts; + {$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 : PChar; BufSize: integer): integer; +function TFFMpegDecodeStream.ReadData(Buffer: PChar; BufferSize: integer): integer; var - nBytesCopy: integer; // number of bytes to copy - nBytesRemain: integer; // number of bytes left (remaining) to read -begin - result := -1; + CopyByteCount: integer; // number of bytes to copy + RemainByteCount: integer; // number of bytes left (remain) to read + BufferPos: integer; - // init number of bytes left to copy to the output buffer - nBytesRemain := BufSize; + // 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; - // leave if end-of-file was reached previously - if EOF then - exit; +begin + Result := -1; + + // set number of bytes to copy to the output buffer + BufferPos := 0; LockDecoder(); try - if (seekRequest) then - begin - Result := 0; + // leave if end-of-file is reached + if (EOF) then Exit; - end; - finally - UnlockDecoder(); - end; - // copy data to output buffer - while (nBytesRemain > 0) do begin - // check if we need more data - if (audio_buf_index >= audio_buf_size) then + // copy data to output buffer + while (BufferPos < BufferSize) do begin - // we have already sent all our data; get more - audio_buf_size := DecodeFrame(audio_buf, sizeof(TAudioBuffer)); - // check for errors or EOF - if(audio_buf_size < 0) then + // check if we need more data + if (AudioBufferPos >= AudioBufferSize) then begin - // fill decode-buffer with silence - audio_buf_size := 1024; - FillChar(audio_buf, audio_buf_size, #0); + 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; - audio_buf_index := 0; - end; - // calc number of new bytes in the decode-buffer - nBytesCopy := audio_buf_size - audio_buf_index; - // resize copy-count if more bytes available than needed (remaining bytes are used the next time) - if (nBytesCopy > nBytesRemain) then - nBytesCopy := nBytesRemain; + // 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(audio_buf[audio_buf_index], Buffer[0], nBytesCopy); + Move(AudioBuffer[AudioBufferPos], Buffer[BufferPos], CopyByteCount); - Dec(nBytesRemain, nBytesCopy); - Inc(Buffer, nBytesCopy); - Inc(audio_buf_index, nBytesCopy); + Inc(BufferPos, CopyByteCount); + Inc(AudioBufferPos, CopyByteCount); + end; + finally + UnlockDecoder(); end; - Result := BufSize; + Result := BufferSize; end; @@ -708,9 +1058,8 @@ end; function TAudioDecoder_FFMpeg.InitializeDecoder: boolean; begin //Log.LogStatus('InitializeDecoder', 'UAudioDecoder_FFMpeg'); - + FFMpegCore := TMediaCore_FFMpeg.GetInstance(); av_register_all(); - Result := true; end; @@ -719,293 +1068,24 @@ begin Result := true; end; -class function TAudioDecoder_FFMpeg.FindAudioStreamIndex(pFormatCtx : PAVFormatContext): integer; -var - i : integer; - streamIndex: integer; - stream : PAVStream; -begin - // find the first audio stream - streamIndex := -1; - - for i := 0 to pFormatCtx^.nb_streams-1 do - begin - //Log.LogStatus('aFormatCtx.streams[i] : ' + inttostr(i), 'UAudio_FFMpeg'); - stream := pFormatCtx^.streams[i]; - - if ( stream.codec^.codec_type = CODEC_TYPE_AUDIO ) then - begin - //Log.LogStatus('Found Audio Stream', 'UAudio_FFMpeg'); - streamIndex := i; - break; - end; - end; - - Result := streamIndex; -end; - function TAudioDecoder_FFMpeg.Open(const Filename: string): TAudioDecodeStream; var - pFormatCtx : PAVFormatContext; - pCodecCtx : PAVCodecContext; - pCodec : PAVCodec; - ffmpegStreamID : Integer; - ffmpegStream : PAVStream; - stream : TFFMpegDecodeStream; + Stream: TFFMpegDecodeStream; begin Result := nil; - if (not FileExists(Filename)) then - begin - Log.LogError('Audio-file does not exist: "' + Filename + '"', 'UAudio_FFMpeg'); - exit; - end; - - // open audio file - if (av_open_input_file(pFormatCtx, PChar(Filename), nil, 0, nil) <> 0) then + Stream := TFFMpegDecodeStream.Create(); + if (not Stream.Open(Filename)) then begin - Log.LogError('av_open_input_file failed: "' + Filename + '"', 'UAudio_FFMpeg'); - exit; - end; - - // TODO: do we need to generate PTS values if they do not exist? - //pFormatCtx^.flags := pFormatCtx^.flags or AVFMT_FLAG_GENPTS; - - // retrieve stream information - if (av_find_stream_info(pFormatCtx) < 0) then - begin - Log.LogError('av_find_stream_info failed: "' + Filename + '"', 'UAudio_FFMpeg'); - av_close_input_file(pFormatCtx); - exit; - end; - - // FIXME: hack used by ffplay. Maybe should not use url_feof() to test for the end - pFormatCtx^.pb.eof_reached := 0; - - {$IFDEF DebugFFMpegDecode} - dump_format(pFormatCtx, 0, pchar(Filename), 0); - {$ENDIF} - - ffmpegStreamID := FindAudioStreamIndex(pFormatCtx); - if (ffmpegStreamID < 0) then - begin - Log.LogError('FindAudioStreamIndex: No Audio-stream found "' + Filename + '"', 'UAudio_FFMpeg'); - av_close_input_file(pFormatCtx); - exit; - end; - - //Log.LogStatus('AudioStreamIndex is: '+ inttostr(ffmpegStreamID), 'UAudio_FFMpeg'); - - ffmpegStream := pFormatCtx.streams[ffmpegStreamID]; - pCodecCtx := ffmpegStream^.codec; - - pCodec := avcodec_find_decoder(pCodecCtx^.codec_id); - if (pCodec = nil) then - begin - Log.LogError('Unsupported codec!', 'UAudio_FFMpeg'); - av_close_input_file(pFormatCtx); - exit; - end; - - // set debug options - pCodecCtx^.debug_mv := 0; - pCodecCtx^.debug := 0; - - // detect bug-workarounds automatically - pCodecCtx^.workaround_bugs := FF_BUG_AUTODETECT; - // error resilience strategy (careful/compliant/agressive/very_aggressive) - //pCodecCtx^.error_resilience := FF_ER_CAREFUL; //FF_ER_COMPLIANT; - // allow non spec compliant speedup tricks. - //pCodecCtx^.flags2 := pCodecCtx^.flags2 or CODEC_FLAG2_FAST; - - // Note: avcodec_open() is not thread-safe! - if (avcodec_open(pCodecCtx, pCodec) < 0) then - begin - Log.LogError('avcodec_open failed!', 'UAudio_FFMpeg'); - exit; - end; - - // TODO: what about pCodecCtx^.start_time? Should we seek to this position here? - // ... - - stream := TFFMpegDecodeStream.Create(pFormatCtx, pCodecCtx, pCodec, - ffmpegStreamID, ffmpegStream); - - Result := stream; -end; - - -{ TPacketQueue } - -constructor TPacketQueue.Create(); -begin - inherited; - - firstPkt := nil; - lastPkt := nil; - nbPackets := 0; - size := 0; - - mutex := SDL_CreateMutex(); - cond := SDL_CreateCond(); -end; - -destructor TPacketQueue.Destroy(); -begin - Flush(); - SDL_DestroyMutex(mutex); - SDL_DestroyCond(cond); - inherited; -end; - -procedure TPacketQueue.Abort(); -begin - SDL_LockMutex(mutex); - - abortRequest := true; - - SDL_CondSignal(cond); - SDL_UnlockMutex(mutex); -end; - -function TPacketQueue.Put(pkt : PAVPacket): integer; -var - pkt1 : PAVPacketList; -begin - result := -1; - - if (pkt = nil) then - exit; - - if (PChar(pkt^.data) <> STATUS_PACKET) then - begin - if (av_dup_packet(pkt) < 0) then - exit; - end; - - pkt1 := av_malloc(sizeof(TAVPacketList)); - if (pkt1 = nil) then - exit; - - pkt1^.pkt := pkt^; - pkt1^.next := nil; - - SDL_LockMutex(Self.mutex); - try - if (Self.lastPkt = nil) then - Self.firstPkt := pkt1 - else - Self.lastPkt^.next := pkt1; - - Self.lastPkt := pkt1; - inc(Self.nbPackets); - - Self.size := Self.size + pkt1^.pkt.size; - SDL_CondSignal(Self.cond); - finally - SDL_UnlockMutex(Self.mutex); - end; - - Result := 0; -end; - -function TPacketQueue.PutStatus(statusFlag: integer; statusInfo: Pointer): integer; -var - pkt: PAVPacket; -begin - // create temp. package - pkt := av_malloc(SizeOf(TAVPacket)); - if (pkt = nil) then - begin - Result := -1; + Stream.Free; Exit; end; - // init package - av_init_packet(pkt^); - pkt^.data := Pointer(STATUS_PACKET); - pkt^.flags := statusFlag; - pkt^.priv := statusInfo; - // put a copy of the package into the queue - Result := Put(pkt); - // data has been copied -> delete temp. package - av_free(pkt); -end; - -function TPacketQueue.Get(var pkt: TAVPacket; block: boolean): integer; -var - pkt1 : PAVPacketList; -begin - Result := -1; - - SDL_LockMutex(Self.mutex); - try - while true do - begin - if (abortRequest) then - exit; - - pkt1 := Self.firstPkt; - if (pkt1 <> nil) then - begin - Self.firstPkt := pkt1^.next; - if (Self.firstPkt = nil) then - Self.lastPkt := nil; - dec(Self.nbPackets); - - Self.size := Self.size - pkt1^.pkt.size; - pkt := pkt1^.pkt; - av_free(pkt1); - - result := 1; - break; - end - else if (not block) then - begin - result := 0; - break; - end - else - begin - SDL_CondWait(Self.cond, Self.mutex); - end; - end; - finally - SDL_UnlockMutex(Self.mutex); - end; -end; - -procedure TPacketQueue.Flush(); -var - pkt, pkt1: PAVPacketList; -begin - SDL_LockMutex(Self.mutex); - pkt := Self.firstPkt; - while(pkt <> nil) do - begin - pkt1 := pkt^.next; - av_free_packet(@pkt^.pkt); - // Note: param must be a pointer to a pointer! - av_freep(@pkt); - pkt := pkt1; - end; - Self.lastPkt := nil; - Self.firstPkt := nil; - Self.nbPackets := 0; - Self.size := 0; - - SDL_UnlockMutex(Self.mutex); + Result := Stream; end; initialization - singleton_AudioDecoderFFMpeg := TAudioDecoder_FFMpeg.create(); - - //writeln( 'UAudioDecoder_FFMpeg - Register Decoder' ); - AudioManager.add( singleton_AudioDecoderFFMpeg ); - -finalization - AudioManager.Remove( singleton_AudioDecoderFFMpeg ); - + MediaManager.Add(TAudioDecoder_FFMpeg.Create); end. diff --git a/Game/Code/Classes/UAudioInput_Bass.pas b/Game/Code/Classes/UAudioInput_Bass.pas index d086a23a..65a4704d 100644 --- a/Game/Code/Classes/UAudioInput_Bass.pas +++ b/Game/Code/Classes/UAudioInput_Bass.pas @@ -57,8 +57,7 @@ type end; var - AudioCore: TAudioCore_Bass; - singleton_AudioInputBass : IAudioInput; + BassCore: TAudioCore_Bass; { Global } @@ -96,9 +95,9 @@ begin begin // get input settings flags := BASS_RecordGetInput(i, PSingle(nil)^); - if (flags = -1) then + if (flags = DWORD(-1)) then begin - Log.LogError('BASS_RecordGetInput: ' + AudioCore.ErrorGetString(), 'TBassInputDevice.GetInputSource'); + Log.LogError('BASS_RecordGetInput: ' + BassCore.ErrorGetString(), 'TBassInputDevice.GetInputSource'); Exit; end; @@ -130,7 +129,7 @@ begin // turn on selected source (turns off the others for single-in devices) if (not BASS_RecordSetInput(SourceIndex, BASS_INPUT_ON, -1)) then begin - Log.LogError('BASS_RecordSetInput: ' + AudioCore.ErrorGetString(), 'TBassInputDevice.Start'); + Log.LogError('BASS_RecordSetInput: ' + BassCore.ErrorGetString(), 'TBassInputDevice.Start'); Exit; end; @@ -143,9 +142,9 @@ begin continue; // get input settings flags := BASS_RecordGetInput(i, PSingle(nil)^); - if (flags = -1) then + if (flags = DWORD(-1)) then begin - Log.LogError('BASS_RecordGetInput: ' + AudioCore.ErrorGetString(), 'TBassInputDevice.GetInputSource'); + Log.LogError('BASS_RecordGetInput: ' + BassCore.ErrorGetString(), 'TBassInputDevice.GetInputSource'); Exit; end; // deselect source if selected @@ -169,11 +168,11 @@ begin if (not BASS_RecordInit(BassDeviceID)) then begin Log.LogError('BASS_RecordInit['+Name+']: ' + - AudioCore.ErrorGetString(), 'TBassInputDevice.Open'); + BassCore.ErrorGetString(), 'TBassInputDevice.Open'); Exit; end; - if (not AudioCore.ConvertAudioFormatToBASSFlags(AudioFormat.Format, FormatFlags)) then + if (not BassCore.ConvertAudioFormatToBASSFlags(AudioFormat.Format, FormatFlags)) then begin Log.LogError('Unhandled sample-format', 'TBassInputDevice.Open'); Exit; @@ -185,7 +184,7 @@ begin @MicrophoneCallback, Self); if (RecordStream = 0) then begin - Log.LogError('BASS_RecordStart: ' + AudioCore.ErrorGetString(), 'TBassInputDevice.Open'); + Log.LogError('BASS_RecordStart: ' + BassCore.ErrorGetString(), 'TBassInputDevice.Open'); BASS_RecordFree; Exit; end; @@ -222,7 +221,7 @@ begin if (not BASS_ChannelPlay(RecordStream, true)) then begin - Log.LogError('BASS_ChannelPlay: ' + AudioCore.ErrorGetString(), 'TBassInputDevice.Start'); + Log.LogError('BASS_ChannelPlay: ' + BassCore.ErrorGetString(), 'TBassInputDevice.Start'); Exit; end; @@ -241,7 +240,7 @@ begin if (not BASS_ChannelStop(RecordStream)) then begin - Log.LogError('BASS_ChannelStop: ' + AudioCore.ErrorGetString(), 'TBassInputDevice.Stop'); + Log.LogError('BASS_ChannelStop: ' + BassCore.ErrorGetString(), 'TBassInputDevice.Stop'); end; // TODO: Do not close the device here (takes too much time). @@ -259,7 +258,7 @@ begin // free data if (not BASS_RecordFree()) then begin - Log.LogError('BASS_RecordFree: ' + AudioCore.ErrorGetString(), 'TBassInputDevice.Close'); + Log.LogError('BASS_RecordFree: ' + BassCore.ErrorGetString(), 'TBassInputDevice.Close'); Result := false; end else @@ -286,9 +285,9 @@ begin Exit; end; - if (BASS_RecordGetInput(SourceIndex, lVolume) = -1) then + if (BASS_RecordGetInput(SourceIndex, lVolume) = DWORD(-1)) then begin - Log.LogError('BASS_RecordGetInput: ' + AudioCore.ErrorGetString() , 'TBassInputDevice.GetVolume'); + Log.LogError('BASS_RecordGetInput: ' + BassCore.ErrorGetString() , 'TBassInputDevice.GetVolume'); Exit; end; Result := lVolume; @@ -315,7 +314,7 @@ begin if (not BASS_RecordSetInput(SourceIndex, 0, Volume)) then begin - Log.LogError('BASS_RecordSetInput: ' + AudioCore.ErrorGetString() , 'TBassInputDevice.SetVolume'); + Log.LogError('BASS_RecordSetInput: ' + BassCore.ErrorGetString() , 'TBassInputDevice.SetVolume'); end; end; @@ -465,7 +464,7 @@ end; function TAudioInput_Bass.InitializeRecord(): boolean; begin - AudioCore := TAudioCore_Bass.GetInstance(); + BassCore := TAudioCore_Bass.GetInstance(); Result := EnumDevices(); end; @@ -477,10 +476,6 @@ end; initialization - singleton_AudioInputBass := TAudioInput_Bass.create(); - AudioManager.add( singleton_AudioInputBass ); - -finalization - AudioManager.Remove( singleton_AudioInputBass ); + MediaManager.Add(TAudioInput_Bass.Create); end. diff --git a/Game/Code/Classes/UAudioInput_Portaudio.pas b/Game/Code/Classes/UAudioInput_Portaudio.pas index 50543e17..9a1c3e99 100644 --- a/Game/Code/Classes/UAudioInput_Portaudio.pas +++ b/Game/Code/Classes/UAudioInput_Portaudio.pas @@ -63,9 +63,6 @@ function MicrophoneTestCallback(input: Pointer; output: Pointer; frameCount: Lon timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags; inputDevice: Pointer): Integer; cdecl; forward; -var - singleton_AudioInputPortaudio : IAudioInput; - { TPortaudioInputDevice } @@ -472,10 +469,6 @@ end; initialization - singleton_AudioInputPortaudio := TAudioInput_Portaudio.create(); - AudioManager.add( singleton_AudioInputPortaudio ); - -finalization - AudioManager.Remove( singleton_AudioInputPortaudio ); + MediaManager.add(TAudioInput_Portaudio.Create); end. diff --git a/Game/Code/Classes/UAudioPlaybackBase.pas b/Game/Code/Classes/UAudioPlaybackBase.pas index 0251b8e8..2337d43f 100644 --- a/Game/Code/Classes/UAudioPlaybackBase.pas +++ b/Game/Code/Classes/UAudioPlaybackBase.pas @@ -16,13 +16,17 @@ type protected OutputDeviceList: TAudioOutputDeviceList; MusicStream: TAudioPlaybackStream; - // open sound or music stream (used by Open() and OpenSound()) - function OpenStream(const Filename: string): TAudioPlaybackStream; virtual; abstract; + function CreatePlaybackStream(): TAudioPlaybackStream; virtual; abstract; procedure ClearOutputDeviceList(); + function GetLatency(): double; virtual; abstract; + + // open sound or music stream (used by Open() and OpenSound()) + function OpenStream(const Filename: string): TAudioPlaybackStream; + function OpenDecodeStream(const Filename: string): TAudioDecodeStream; public - function GetName: String; virtual; abstract; + function GetName: string; virtual; abstract; - function Open(const Filename: string): boolean; // true if succeed + function Open(const Filename: string): boolean; // true if succeed procedure Close; procedure Play; @@ -30,13 +34,15 @@ type procedure Stop; procedure FadeIn(Time: real; TargetVolume: single); + procedure SetSyncSource(SyncSource: TSyncSource); + procedure SetPosition(Time: real); function GetPosition: real; function InitializePlayback: boolean; virtual; abstract; function FinalizePlayback: boolean; virtual; - // function SetOutputDevice(Device: TAudioOutputDevice): boolean; + //function SetOutputDevice(Device: TAudioOutputDevice): boolean; function GetOutputDeviceList(): TAudioOutputDeviceList; procedure SetAppVolume(Volume: single); virtual; abstract; @@ -48,21 +54,24 @@ type function Length: real; // Sounds - function OpenSound(const Filename: String): TAudioPlaybackStream; - procedure PlaySound(stream: TAudioPlaybackStream); - procedure StopSound(stream: TAudioPlaybackStream); + function OpenSound(const Filename: string): TAudioPlaybackStream; + procedure PlaySound(Stream: TAudioPlaybackStream); + procedure StopSound(Stream: TAudioPlaybackStream); // Equalizer - procedure GetFFTData(var data: TFFTData); + procedure GetFFTData(var Data: TFFTData); // Interface for Visualizer - function GetPCMData(var data: TPCMData): Cardinal; + function GetPCMData(var Data: TPCMData): Cardinal; + + function CreateVoiceStream(Channel: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream; virtual; abstract; end; implementation uses + ULog, SysUtils; { TAudioPlaybackBase } @@ -71,6 +80,7 @@ function TAudioPlaybackBase.FinalizePlayback: boolean; begin FreeAndNil(MusicStream); ClearOutputDeviceList(); + Result := true; end; function TAudioPlaybackBase.Open(const Filename: string): boolean; @@ -92,8 +102,64 @@ end; procedure TAudioPlaybackBase.Close; begin - if assigned(MusicStream) then - MusicStream.Close(); + FreeAndNil(MusicStream); +end; + +function TAudioPlaybackBase.OpenDecodeStream(const Filename: String): TAudioDecodeStream; +var + i: integer; +begin + for i := 0 to AudioDecoders.Count-1 do + begin + Result := IAudioDecoder(AudioDecoders[i]).Open(Filename); + if (assigned(Result)) then + begin + Log.LogInfo('Using decoder ' + IAudioDecoder(AudioDecoders[i]).GetName() + + ' for "' + Filename + '"', 'TAudioPlaybackBase.OpenDecodeStream'); + Exit; + end; + end; + Result := nil; +end; + +procedure OnClosePlaybackStream(Stream: TAudioProcessingStream); +var + PlaybackStream: TAudioPlaybackStream; + SourceStream: TAudioSourceStream; +begin + PlaybackStream := TAudioPlaybackStream(Stream); + SourceStream := PlaybackStream.GetSourceStream(); + SourceStream.Free; +end; + +function TAudioPlaybackBase.OpenStream(const Filename: string): TAudioPlaybackStream; +var + PlaybackStream: TAudioPlaybackStream; + DecodeStream: TAudioDecodeStream; +begin + Result := nil; + + //Log.LogStatus('Loading Sound: "' + Filename + '"', 'TAudioPlayback_Bass.OpenStream'); + + DecodeStream := OpenDecodeStream(Filename); + if (not assigned(DecodeStream)) then + begin + Log.LogStatus('Could not open "' + Filename + '"', 'TAudioPlayback_Bass.OpenStream'); + Exit; + end; + + // create a matching playback-stream for the decoder + PlaybackStream := CreatePlaybackStream(); + if (not PlaybackStream.Open(DecodeStream)) then + begin + FreeAndNil(PlaybackStream); + FreeAndNil(DecodeStream); + Exit; + end; + + PlaybackStream.AddOnCloseHandler(OnClosePlaybackStream); + + Result := PlaybackStream; end; procedure TAudioPlaybackBase.Play; @@ -136,6 +202,12 @@ begin MusicStream.Position := Time; end; +procedure TAudioPlaybackBase.SetSyncSource(SyncSource: TSyncSource); +begin + if assigned(MusicStream) then + MusicStream.SetSyncSource(SyncSource); +end; + procedure TAudioPlaybackBase.Rewind; begin SetPosition(0); diff --git a/Game/Code/Classes/UAudioPlayback_Bass.pas b/Game/Code/Classes/UAudioPlayback_Bass.pas index 53fbd921..41a91173 100644 --- a/Game/Code/Classes/UAudioPlayback_Bass.pas +++ b/Game/Code/Classes/UAudioPlayback_Bass.pas @@ -13,64 +13,78 @@ implementation uses Classes, SysUtils, + Math, UIni, UMain, UMusic, UAudioPlaybackBase, UAudioCore_Bass, ULog, + sdl, bass; type PHDSP = ^HDSP; type - // Playback-stream decoded internally by BASS TBassPlaybackStream = class(TAudioPlaybackStream) private Handle: HSTREAM; + NeedsRewind: boolean; + PausedSeek: boolean; // true if a seek was performed in pause state + + procedure Reset(); + function IsEOF(): boolean; + protected + function GetLatency(): double; override; + function GetLoop(): boolean; override; + procedure SetLoop(Enabled: boolean); override; + function GetLength(): real; override; + function GetStatus(): TStreamStatus; override; + function GetVolume(): single; override; + procedure SetVolume(Volume: single); override; + function GetPosition: real; override; + procedure SetPosition(Time: real); override; public - constructor Create(stream: HSTREAM); + constructor Create(); destructor Destroy(); override; - procedure Reset(); + function Open(SourceStream: TAudioSourceStream): boolean; override; + procedure Close(); override; procedure Play(); override; procedure Pause(); override; procedure Stop(); override; procedure FadeIn(Time: real; TargetVolume: single); override; - procedure Close(); override; + procedure AddSoundEffect(Effect: TSoundEffect); override; + procedure RemoveSoundEffect(Effect: TSoundEffect); override; - function GetLoop(): boolean; override; - procedure SetLoop(Enabled: boolean); override; - function GetLength(): real; override; - function GetStatus(): TStreamStatus; override; - function GetVolume(): single; override; - procedure SetVolume(volume: single); override; + procedure GetFFTData(var Data: TFFTData); override; + function GetPCMData(var Data: TPCMData): Cardinal; override; - procedure AddSoundEffect(effect: TSoundEffect); override; - procedure RemoveSoundEffect(effect: TSoundEffect); override; + function GetAudioFormatInfo(): TAudioFormatInfo; override; - function GetPosition: real; override; - procedure SetPosition(Time: real); override; + function ReadData(Buffer: PChar; BufferSize: integer): integer; - procedure GetFFTData(var data: TFFTData); override; - function GetPCMData(var data: TPCMData): Cardinal; override; + property EOF: boolean READ IsEOF; end; - // Playback-stream decoded by an external decoder e.g. FFmpeg - TBassExtDecoderPlaybackStream = class(TBassPlaybackStream) +const + MAX_VOICE_DELAY = 0.020; // 20ms + +type + TBassVoiceStream = class(TAudioVoiceStream) private - DecodeStream: TAudioDecodeStream; + Handle: HSTREAM; public - procedure Stop(); override; - procedure Close(); override; - function GetLength(): real; override; - function GetPosition: real; override; - procedure SetPosition(Time: real); override; + function Open(ChannelMap: integer; FormatInfo: TAudioFormatInfo): boolean; override; + procedure Close(); override; - function SetDecodeStream(decodeStream: TAudioDecodeStream): boolean; + procedure WriteData(Buffer: PChar; BufferSize: integer); override; + function ReadData(Buffer: PChar; BufferSize: integer): integer; override; + function IsEOF(): boolean; override; + function IsError(): boolean; override; end; type @@ -78,12 +92,14 @@ type private function EnumDevices(): boolean; protected - function OpenStream(const Filename: string): TAudioPlaybackStream; override; + function GetLatency(): double; override; + function CreatePlaybackStream(): TAudioPlaybackStream; override; public function GetName: String; override; function InitializePlayback(): boolean; override; function FinalizePlayback: boolean; override; procedure SetAppVolume(Volume: single); override; + function CreateVoiceStream(ChannelMap: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream; override; end; TBassOutputDevice = class(TAudioOutputDevice) @@ -92,17 +108,107 @@ type end; var - AudioCore: TAudioCore_Bass; - singleton_AudioPlaybackBass : IAudioPlayback; + BassCore: TAudioCore_Bass; { TBassPlaybackStream } -constructor TBassPlaybackStream.Create(stream: HSTREAM); +function PlaybackStreamHandler(handle: HSTREAM; buffer: Pointer; length: DWORD; user: Pointer): DWORD; +{$IFDEF MSWINDOWS}stdcall;{$ELSE}cdecl;{$ENDIF} +var + PlaybackStream: TBassPlaybackStream; + BytesRead: integer; +begin + PlaybackStream := TBassPlaybackStream(user); + if (not assigned (PlaybackStream)) then + begin + Result := BASS_STREAMPROC_END; + Exit; + end; + + BytesRead := PlaybackStream.ReadData(buffer, length); + // check for errors + if (BytesRead < 0) then + Result := BASS_STREAMPROC_END + // check for EOF + else if (PlaybackStream.EOF) then + Result := BytesRead or BASS_STREAMPROC_END + // no error/EOF + else + Result := BytesRead; +end; + +function TBassPlaybackStream.ReadData(Buffer: PChar; BufferSize: integer): integer; +var + AdjustedSize: integer; + RequestedSourceSize, SourceSize: integer; + SkipCount: integer; + SourceFormatInfo: TAudioFormatInfo; + FrameSize: integer; + PadFrame: PChar; + //Info: BASS_INFO; + //Latency: double; +begin + Result := -1; + + if (not assigned(SourceStream)) then + Exit; + + // sanity check + if (BufferSize = 0) then + begin + Result := 0; + Exit; + end; + + SourceFormatInfo := SourceStream.GetAudioFormatInfo(); + FrameSize := SourceFormatInfo.FrameSize; + + // check how much data to fetch to be in synch + AdjustedSize := Synchronize(BufferSize, SourceFormatInfo); + + // skip data if we are too far behind + SkipCount := AdjustedSize - BufferSize; + while (SkipCount > 0) do + begin + RequestedSourceSize := Min(SkipCount, BufferSize); + SourceSize := SourceStream.ReadData(Buffer, RequestedSourceSize); + // if an error or EOF occured stop skipping and handle error/EOF with the next ReadData() + if (SourceSize <= 0) then + break; + Dec(SkipCount, SourceSize); + end; + + // get source data (e.g. from a decoder) + RequestedSourceSize := Min(AdjustedSize, BufferSize); + SourceSize := SourceStream.ReadData(Buffer, RequestedSourceSize); + if (SourceSize < 0) then + Exit; + + // set preliminary result + Result := SourceSize; + + // if we are to far ahead, fill output-buffer with last frame of source data + // Note that AdjustedSize is used instead of SourceSize as the SourceSize might + // be less than expected because of errors etc. + if (AdjustedSize < BufferSize) then + begin + // use either the last frame for padding or fill with zero + if (SourceSize >= FrameSize) then + PadFrame := @Buffer[SourceSize-FrameSize] + else + PadFrame := nil; + + FillBufferWithFrame(@Buffer[SourceSize], BufferSize - SourceSize, + PadFrame, FrameSize); + Result := BufferSize; + end; +end; + +constructor TBassPlaybackStream.Create(); begin - inherited Create(); + inherited; Reset(); - Handle := stream; end; destructor TBassPlaybackStream.Destroy(); @@ -111,32 +217,99 @@ begin inherited; end; -procedure TBassPlaybackStream.Reset(); +function TBassPlaybackStream.Open(SourceStream: TAudioSourceStream): boolean; +var + FormatInfo: TAudioFormatInfo; + FormatFlags: DWORD; begin + Result := false; + + // close previous stream and reset state + Reset(); + + // sanity check if stream is valid + if not assigned(SourceStream) then + Exit; + + Self.SourceStream := SourceStream; + FormatInfo := SourceStream.GetAudioFormatInfo(); + if (not BassCore.ConvertAudioFormatToBASSFlags(FormatInfo.Format, FormatFlags)) then + begin + Log.LogError('Unhandled sample-format', 'TBassPlaybackStream.Open'); + Exit; + end; + + // create matching playback stream + Handle := BASS_StreamCreate(Round(FormatInfo.SampleRate), FormatInfo.Channels, formatFlags, + @PlaybackStreamHandler, Self); + if (Handle = 0) then + begin + Log.LogError('BASS_StreamCreate failed: ' + BassCore.ErrorGetString(BASS_ErrorGetCode()), + 'TBassPlaybackStream.Open'); + Exit; + end; + + Result := true; +end; + +procedure TBassPlaybackStream.Close(); +begin + // stop and free stream if (Handle <> 0) then begin Bass_StreamFree(Handle); + Handle := 0; end; - Handle := 0; + + // Note: PerformOnClose must be called before SourceStream is invalidated + PerformOnClose(); + // unset source-stream + SourceStream := nil; +end; + +procedure TBassPlaybackStream.Reset(); +begin + Close(); + NeedsRewind := false; + PausedSeek := false; end; procedure TBassPlaybackStream.Play(); var - restart: boolean; + NeedsFlush: boolean; begin + if (not assigned(SourceStream)) then + Exit; + + NeedsFlush := true; + if (BASS_ChannelIsActive(Handle) = BASS_ACTIVE_PAUSED) then - restart := false // resume from last position - else - restart := true; // start from the beginning + begin + // only paused (and not seeked while paused) streams are not flushed + if (not PausedSeek) then + NeedsFlush := false; + // paused streams do not need a rewind + NeedsRewind := false; + end; + + // rewind if necessary. Cases that require no rewind are: + // - stream was created and never played + // - stream was paused and is resumed now + // - stream was stopped and set to a new position already + if (NeedsRewind) then + SourceStream.Position := 0; + + NeedsRewind := true; + PausedSeek := false; - BASS_ChannelPlay(Handle, restart); + // start playing and flush buffers on rewind + BASS_ChannelPlay(Handle, NeedsFlush); end; procedure TBassPlaybackStream.FadeIn(Time: real; TargetVolume: single); begin // start stream - BASS_ChannelPlay(Handle, true); - + Play(); // start fade-in: slide from fadeStart- to fadeEnd-volume in FadeInTime BASS_ChannelSlideAttribute(Handle, BASS_ATTRIB_VOL, TargetVolume, Trunc(Time * 1000)); end; @@ -151,9 +324,22 @@ begin BASS_ChannelStop(Handle); end; -procedure TBassPlaybackStream.Close(); +function TBassPlaybackStream.IsEOF(): boolean; begin - Reset(); + if (assigned(SourceStream)) then + Result := SourceStream.EOF + else + Result := true; +end; + +function TBassPlaybackStream.GetLatency(): double; +begin + // TODO: should we consider output latency for synching (needs BASS_DEVICE_LATENCY)? + //if (BASS_GetInfo(Info)) then + // Latency := Info.latency / 1000 + //else + // Latency := 0; + Result := 0; end; function TBassPlaybackStream.GetVolume(): single; @@ -162,7 +348,7 @@ var begin if (not BASS_ChannelGetAttribute(Handle, BASS_ATTRIB_VOL, lVolume)) then begin - Log.LogError('BASS_ChannelGetAttribute: ' + AudioCore.ErrorGetString(), + Log.LogError('BASS_ChannelGetAttribute: ' + BassCore.ErrorGetString(), 'TBassPlaybackStream.GetVolume'); Result := 0; Exit; @@ -170,235 +356,272 @@ begin Result := Round(lVolume); end; -procedure TBassPlaybackStream.SetVolume(volume: single); +procedure TBassPlaybackStream.SetVolume(Volume: single); begin // clamp volume - if volume < 0 then - volume := 0; - if volume > 1.0 then - volume := 1.0; + if Volume < 0 then + Volume := 0; + if Volume > 1.0 then + Volume := 1.0; // set volume - BASS_ChannelSetAttribute(Handle, BASS_ATTRIB_VOL, volume); + BASS_ChannelSetAttribute(Handle, BASS_ATTRIB_VOL, Volume); end; function TBassPlaybackStream.GetPosition: real; var - bytes: QWORD; + BufferPosByte: QWORD; + BufferPosSec: double; begin - bytes := BASS_ChannelGetPosition(Handle, BASS_POS_BYTE); - Result := BASS_ChannelBytes2Seconds(Handle, bytes); + if assigned(SourceStream) then + begin + BufferPosByte := BASS_ChannelGetData(Handle, nil, BASS_DATA_AVAILABLE); + BufferPosSec := BASS_ChannelBytes2Seconds(Handle, BufferPosByte); + // decrease the decoding position by the amount buffered (and hence not played) + // in the BASS playback stream. + Result := SourceStream.Position - BufferPosSec; + end + else + begin + Result := -1; + end; end; procedure TBassPlaybackStream.SetPosition(Time: real); var - bytes: QWORD; + ChannelState: DWORD; begin - bytes := BASS_ChannelSeconds2Bytes(Handle, Time); - BASS_ChannelSetPosition(Handle, bytes, BASS_POS_BYTE); + if assigned(SourceStream) then + begin + ChannelState := BASS_ChannelIsActive(Handle); + if (ChannelState = BASS_ACTIVE_STOPPED) then + begin + // if the stream is stopped, do not rewind when the stream is played next time + NeedsRewind := false + end + else if (ChannelState = BASS_ACTIVE_PAUSED) then + begin + // buffers must be flushed if in paused state but there is no + // BASS_ChannelFlush() function so we have to use BASS_ChannelPlay() called in Play(). + PausedSeek := true; + end; + + // set new position + SourceStream.Position := Time; + end; end; function TBassPlaybackStream.GetLength(): real; -var - bytes: QWORD; begin - bytes := BASS_ChannelGetLength(Handle, BASS_POS_BYTE); - Result := BASS_ChannelBytes2Seconds(Handle, bytes); + if assigned(SourceStream) then + Result := SourceStream.Length + else + Result := -1; end; function TBassPlaybackStream.GetStatus(): TStreamStatus; var - state: DWORD; + State: DWORD; begin - state := BASS_ChannelIsActive(Handle); - case state of - BASS_ACTIVE_PLAYING: - result := ssPlaying; - BASS_ACTIVE_PAUSED: - result := ssPaused; + State := BASS_ChannelIsActive(Handle); + case State of + BASS_ACTIVE_PLAYING, BASS_ACTIVE_STALLED: - result := ssBlocked; + Result := ssPlaying; + BASS_ACTIVE_PAUSED: + Result := ssPaused; BASS_ACTIVE_STOPPED: - result := ssStopped; + Result := ssStopped; else - result := ssUnknown; + begin + Log.LogError('Unknown status', 'TBassPlaybackStream.GetStatus'); + Result := ssStopped; + end; end; end; function TBassPlaybackStream.GetLoop(): boolean; -var - flags: DWORD; begin - // retrieve channel flags - flags := BASS_ChannelFlags(Handle, 0, 0); - if (flags = -1) then - begin - Log.LogError('BASS_ChannelFlags: ' + AudioCore.ErrorGetString(), 'TBassPlaybackStream.GetLoop'); + if assigned(SourceStream) then + Result := SourceStream.Loop + else Result := false; - Exit; - end; - Result := (flags and BASS_SAMPLE_LOOP) <> 0; end; procedure TBassPlaybackStream.SetLoop(Enabled: boolean); -var - flags: DWORD; begin - // set/unset loop-flag - if (Enabled) then - flags := BASS_SAMPLE_LOOP - else - flags := 0; - - // set new flag-bits - if (BASS_ChannelFlags(Handle, flags, BASS_SAMPLE_LOOP) = -1) then - begin - Log.LogError('BASS_ChannelFlags: ' + AudioCore.ErrorGetString(), 'TBassPlaybackStream.SetLoop'); - Exit; - end; + if assigned(SourceStream) then + SourceStream.Loop := Enabled; end; -procedure DSPProcHandler(handle: HDSP; channel: DWORD; buffer: Pointer; length: DWORD; user: Pointer); {$IFDEF MSWINDOWS}stdcall;{$ELSE}cdecl;{$ENDIF} +procedure DSPProcHandler(handle: HDSP; channel: DWORD; buffer: Pointer; length: DWORD; user: Pointer); +{$IFDEF MSWINDOWS}stdcall;{$ELSE}cdecl;{$ENDIF} var - effect: TSoundEffect; + Effect: TSoundEffect; begin - effect := TSoundEffect(user); - if assigned(effect) then - effect.Callback(buffer, length); + Effect := TSoundEffect(user); + if assigned(Effect) then + Effect.Callback(buffer, length); end; -procedure TBassPlaybackStream.AddSoundEffect(effect: TSoundEffect); +procedure TBassPlaybackStream.AddSoundEffect(Effect: TSoundEffect); var - dspHandle: HDSP; + DspHandle: HDSP; begin - if assigned(effect.engineData) then + if assigned(Effect.engineData) then begin Log.LogError('TSoundEffect.engineData already set', 'TBassPlaybackStream.AddSoundEffect'); Exit; end; - dspHandle := BASS_ChannelSetDSP(Handle, @DSPProcHandler, effect, 0); - if (dspHandle = 0) then + DspHandle := BASS_ChannelSetDSP(Handle, @DSPProcHandler, Effect, 0); + if (DspHandle = 0) then begin - Log.LogError(AudioCore.ErrorGetString(), 'TBassPlaybackStream.AddSoundEffect'); + Log.LogError(BassCore.ErrorGetString(), 'TBassPlaybackStream.AddSoundEffect'); Exit; end; - GetMem(effect.engineData, SizeOf(HDSP)); - PHDSP(effect.engineData)^ := dspHandle; + GetMem(Effect.EngineData, SizeOf(HDSP)); + PHDSP(Effect.EngineData)^ := DspHandle; end; -procedure TBassPlaybackStream.RemoveSoundEffect(effect: TSoundEffect); +procedure TBassPlaybackStream.RemoveSoundEffect(Effect: TSoundEffect); begin - if not assigned(effect.EngineData) then + if not assigned(Effect.EngineData) then begin Log.LogError('TSoundEffect.engineData invalid', 'TBassPlaybackStream.RemoveSoundEffect'); Exit; end; - if not BASS_ChannelRemoveDSP(Handle, PHDSP(effect.EngineData)^) then + if not BASS_ChannelRemoveDSP(Handle, PHDSP(Effect.EngineData)^) then begin - Log.LogError(AudioCore.ErrorGetString(), 'TBassPlaybackStream.RemoveSoundEffect'); + Log.LogError(BassCore.ErrorGetString(), 'TBassPlaybackStream.RemoveSoundEffect'); Exit; end; - FreeMem(effect.engineData); - effect.engineData := nil; + FreeMem(Effect.EngineData); + Effect.EngineData := nil; end; -procedure TBassPlaybackStream.GetFFTData(var data: TFFTData); +procedure TBassPlaybackStream.GetFFTData(var Data: TFFTData); begin - // Get Channel Data Mono and 256 Values - BASS_ChannelGetData(Handle, @data, BASS_DATA_FFT512); + // get FFT channel data (Mono, FFT512 -> 256 values) + BASS_ChannelGetData(Handle, @Data, BASS_DATA_FFT512); end; {* * Copies interleaved PCM SInt16 stereo samples into data. * Returns the number of frames *} -function TBassPlaybackStream.GetPCMData(var data: TPCMData): Cardinal; +function TBassPlaybackStream.GetPCMData(var Data: TPCMData): Cardinal; var - info: BASS_CHANNELINFO; + Info: BASS_CHANNELINFO; nBytes: DWORD; begin Result := 0; - FillChar(data, sizeof(TPCMData), 0); + FillChar(Data, SizeOf(TPCMData), 0); // no support for non-stereo files at the moment - BASS_ChannelGetInfo(Handle, info); - if (info.chans <> 2) then + BASS_ChannelGetInfo(Handle, Info); + if (Info.chans <> 2) then Exit; - nBytes := BASS_ChannelGetData(Handle, @data, sizeof(TPCMData)); + nBytes := BASS_ChannelGetData(Handle, @Data, SizeOf(TPCMData)); if(nBytes <= 0) then - result := 0 + Result := 0 else - result := nBytes div sizeof(TPCMStereoSample); + Result := nBytes div SizeOf(TPCMStereoSample); end; +function TBassPlaybackStream.GetAudioFormatInfo(): TAudioFormatInfo; +begin + if assigned(SourceStream) then + Result := SourceStream.GetAudioFormatInfo() + else + Result := nil; +end; -{ TBassExtDecoderPlaybackStream } -procedure TBassExtDecoderPlaybackStream.Stop(); +{ TBassVoiceStream } + +function TBassVoiceStream.Open(ChannelMap: integer; FormatInfo: TAudioFormatInfo): boolean; +var + Flags: DWORD; begin - inherited; - // rewind - if assigned(DecodeStream) then - DecodeStream.Position := 0; + Result := false; + + Close(); + + if (not inherited Open(ChannelMap, FormatInfo)) then + Exit; + + // get channel flags + BassCore.ConvertAudioFormatToBASSFlags(FormatInfo.Format, Flags); + + (* + // distribute the mics equally to both speakers + if ((ChannelMap and CHANNELMAP_LEFT) <> 0) then + Flags := Flags or BASS_SPEAKER_FRONTLEFT; + if ((ChannelMap and CHANNELMAP_RIGHT) <> 0) then + Flags := Flags or BASS_SPEAKER_FRONTRIGHT; + *) + + // create the channel + Handle := BASS_StreamCreate(Round(FormatInfo.SampleRate), 1, Flags, STREAMPROC_PUSH, nil); + + // start the channel + BASS_ChannelPlay(Handle, true); + + Result := true; end; -procedure TBassExtDecoderPlaybackStream.Close(); +procedure TBassVoiceStream.Close(); begin - // wake-up waiting audio-callback threads in the ReadData()-function - if assigned(decodeStream) then - DecodeStream.Close(); - // stop audio-callback on this stream - inherited; - // free decoder-data - FreeAndNil(DecodeStream); + if (Handle <> 0) then + begin + BASS_ChannelStop(Handle); + BASS_StreamFree(Handle); + end; + inherited Close(); end; -function TBassExtDecoderPlaybackStream.GetLength(): real; +procedure TBassVoiceStream.WriteData(Buffer: PChar; BufferSize: integer); +var QueueSize: DWORD; begin - if assigned(DecodeStream) then - result := DecodeStream.Length - else - result := -1; + if ((Handle <> 0) and (BufferSize > 0)) then + begin + // query the queue size (normally 0) + QueueSize := BASS_StreamPutData(Handle, nil, 0); + // flush the buffer if the delay would be too high + if (QueueSize > MAX_VOICE_DELAY * FormatInfo.BytesPerSec) then + BASS_ChannelPlay(Handle, true); + // send new data to playback buffer + BASS_StreamPutData(Handle, Buffer, BufferSize); + end; end; -function TBassExtDecoderPlaybackStream.GetPosition: real; +// Note: we do not need the read-function for the BASS implementation +function TBassVoiceStream.ReadData(Buffer: PChar; BufferSize: integer): integer; begin - if assigned(DecodeStream) then - result := DecodeStream.Position - else - result := -1; + Result := -1; end; -procedure TBassExtDecoderPlaybackStream.SetPosition(Time: real); +function TBassVoiceStream.IsEOF(): boolean; begin - if assigned(DecodeStream) then - DecodeStream.Position := Time; + Result := false; end; -function TBassExtDecoderPlaybackStream.SetDecodeStream(decodeStream: TAudioDecodeStream): boolean; +function TBassVoiceStream.IsError(): boolean; begin - result := false; - - BASS_ChannelStop(Handle); - - if not assigned(decodeStream) then - Exit; - Self.DecodeStream := decodeStream; - - result := true; + Result := false; end; { TAudioPlayback_Bass } -function TAudioPlayback_Bass.GetName: String; +function TAudioPlayback_Bass.GetName: String; begin - result := 'BASS_Playback'; + Result := 'BASS_Playback'; end; function TAudioPlayback_Bass.EnumDevices(): boolean; @@ -408,23 +631,25 @@ var Device: TBassOutputDevice; DeviceInfo: BASS_DEVICEINFO; begin + Result := true; + ClearOutputDeviceList(); // skip "no sound"-device (ID = 0) BassDeviceID := 1; - while true do + while (true) do begin - // Check for device + // check for device if (not BASS_GetDeviceInfo(BassDeviceID, DeviceInfo)) then - break; + Break; - // Set device info + // set device info Device := TBassOutputDevice.Create(); Device.Name := DeviceInfo.name; Device.BassDeviceID := BassDeviceID; - // Add device to list + // add device to list SetLength(OutputDeviceList, BassDeviceID); OutputDeviceList[BassDeviceID-1] := Device; @@ -433,19 +658,17 @@ begin end; function TAudioPlayback_Bass.InitializePlayback(): boolean; -var - Pet: integer; - S: integer; begin result := false; - AudioCore := TAudioCore_Bass.GetInstance(); + BassCore := TAudioCore_Bass.GetInstance(); EnumDevices(); //Log.BenchmarkStart(4); //Log.LogStatus('Initializing Playback Subsystem', 'Music Initialize'); + // TODO: use BASS_DEVICE_LATENCY to determine the latency if not BASS_Init(-1, 44100, 0, 0, nil) then begin Log.LogError('Could not initialize BASS', 'TAudioPlayback_Bass.InitializePlayback'); @@ -469,125 +692,40 @@ begin Result := true; end; -function DecodeStreamHandler(handle: HSTREAM; buffer: Pointer; length: DWORD; user: Pointer): DWORD; {$IFDEF MSWINDOWS}stdcall;{$ELSE}cdecl;{$ENDIF} -var - decodeStream: TAudioDecodeStream; - bytes: integer; +function TAudioPlayback_Bass.CreatePlaybackStream(): TAudioPlaybackStream; begin - decodeStream := TAudioDecodeStream(user); - bytes := decodeStream.ReadData(buffer, length); - // handle errors - if (bytes < 0) then - Result := BASS_STREAMPROC_END - // handle EOF - else if (DecodeStream.EOF) then - Result := bytes or BASS_STREAMPROC_END - else - Result := bytes; + Result := TBassPlaybackStream.Create(); +end; + +procedure TAudioPlayback_Bass.SetAppVolume(Volume: single); +begin + // set volume for this application (ranges from 0..10000 since BASS 2.4) + BASS_SetConfig(BASS_CONFIG_GVOL_STREAM, Round(Volume*10000)); end; -function TAudioPlayback_Bass.OpenStream(const Filename: string): TAudioPlaybackStream; +function TAudioPlayback_Bass.CreateVoiceStream(ChannelMap: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream; var - stream: HSTREAM; - playbackStream: TBassExtDecoderPlaybackStream; - decodeStream: TAudioDecodeStream; - formatInfo: TAudioFormatInfo; - formatFlags: DWORD; - channelInfo: BASS_CHANNELINFO; - fileExt: string; + VoiceStream: TAudioVoiceStream; begin Result := nil; - //Log.LogStatus('Loading Sound: "' + Filename + '"', 'LoadSoundFromFile'); - // TODO: use BASS_STREAM_PRESCAN for accurate seeking in VBR-files? - // disadvantage: seeking will slow down. - stream := BASS_StreamCreateFile(False, PChar(Filename), 0, 0, 0); - - // check if BASS opened some erroneously recognized file-formats - if (stream <> 0) then + VoiceStream := TBassVoiceStream.Create(); + if (not VoiceStream.Open(ChannelMap, FormatInfo)) then begin - if BASS_ChannelGetInfo(stream, channelInfo) then - begin - fileExt := ExtractFileExt(Filename); - // BASS opens FLV-files (maybe others too) although it cannot handle them. - // Setting BASS_CONFIG_VERIFY to the max. value (100000) does not help. - if ((fileExt = '.flv') and (channelInfo.ctype = BASS_CTYPE_STREAM_MP1)) then - begin - BASS_StreamFree(stream); - stream := 0; - end; - end; + VoiceStream.Free; + Exit; end; - // Check if BASS can handle the format or try another decoder otherwise - if (stream <> 0) then - begin - Result := TBassPlaybackStream.Create(stream); - end - else - begin - if (AudioDecoder = nil) then - begin - Log.LogError('Failed to open "' + Filename + '", ' + - AudioCore.ErrorGetString(), 'TAudioPlayback_Bass.Load'); - Exit; - end; - - decodeStream := AudioDecoder.Open(Filename); - if not assigned(decodeStream) then - begin - Log.LogStatus('Sound not found "' + Filename + '"', 'TAudioPlayback_Bass.Load'); - Exit; - end; - - formatInfo := decodeStream.GetAudioFormatInfo(); - if (not AudioCore.ConvertAudioFormatToBASSFlags(formatInfo.Format, formatFlags)) then - begin - Log.LogError('Unhandled sample-format in "' + Filename + '"', 'TAudioPlayback_Bass.Load'); - FreeAndNil(decodeStream); - Exit; - end; - - stream := BASS_StreamCreate(Round(formatInfo.SampleRate), formatInfo.Channels, formatFlags, - @DecodeStreamHandler, decodeStream); - if (stream = 0) then - begin - Log.LogError('Failed to open "' + Filename + '", ' + - AudioCore.ErrorGetString(BASS_ErrorGetCode()), 'TAudioPlayback_Bass.Load'); - FreeAndNil(decodeStream); - Exit; - end; - - playbackStream := TBassExtDecoderPlaybackStream.Create(stream); - if (not assigned(playbackStream)) then - begin - FreeAndNil(decodeStream); - Exit; - end; - - if (not playbackStream.SetDecodeStream(decodeStream)) then - begin - FreeAndNil(playbackStream); - FreeAndNil(decodeStream); - Exit; - end; - - Result := playbackStream; - end; + Result := VoiceStream; end; -procedure TAudioPlayback_Bass.SetAppVolume(Volume: single); +function TAudioPlayback_Bass.GetLatency(): double; begin - // Sets Volume only for this Application (now ranges from 0..10000) - BASS_SetConfig(BASS_CONFIG_GVOL_STREAM, Round(Volume*10000)); + Result := 0; end; initialization - singleton_AudioPlaybackBass := TAudioPlayback_Bass.create(); - AudioManager.add( singleton_AudioPlaybackBass ); - -finalization - AudioManager.Remove( singleton_AudioPlaybackBass ); + MediaManager.Add(TAudioPlayback_Bass.Create); end. diff --git a/Game/Code/Classes/UAudioPlayback_Portaudio.pas b/Game/Code/Classes/UAudioPlayback_Portaudio.pas index b27fa83c..c3717ba6 100644 --- a/Game/Code/Classes/UAudioPlayback_Portaudio.pas +++ b/Game/Code/Classes/UAudioPlayback_Portaudio.pas @@ -29,6 +29,7 @@ type private paStream: PPaStream; AudioCore: TAudioCore_Portaudio; + Latency: double; function OpenDevice(deviceIndex: TPaDeviceIndex): boolean; function EnumDevices(): boolean; protected @@ -36,6 +37,7 @@ type function StartAudioPlaybackEngine(): boolean; override; procedure StopAudioPlaybackEngine(); override; function FinalizeAudioPlaybackEngine(): boolean; override; + function GetLatency(): double; override; public function GetName: String; override; end; @@ -45,9 +47,6 @@ type PaDeviceIndex: TPaDeviceIndex; end; -var - singleton_AudioPlaybackPortaudio : IAudioPlayback; - { TAudioPlayback_Portaudio } @@ -55,63 +54,74 @@ function PortaudioAudioCallback(input: Pointer; output: Pointer; frameCount: Lon timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags; userData: Pointer): Integer; cdecl; var - engine: TAudioPlayback_Portaudio; + Engine: TAudioPlayback_Portaudio; begin - engine := TAudioPlayback_Portaudio(userData); - engine.AudioCallback(output, frameCount * engine.FormatInfo.FrameSize); - result := paContinue; + Engine := TAudioPlayback_Portaudio(userData); + // update latency + Engine.Latency := timeInfo.outputBufferDacTime - timeInfo.currentTime; + // call superclass callback + Engine.AudioCallback(output, frameCount * Engine.FormatInfo.FrameSize); + Result := paContinue; end; function TAudioPlayback_Portaudio.GetName: String; begin - result := 'Portaudio_Playback'; + Result := 'Portaudio_Playback'; end; function TAudioPlayback_Portaudio.OpenDevice(deviceIndex: TPaDeviceIndex): boolean; var - deviceInfo : PPaDeviceInfo; - sampleRate : double; - outParams : TPaStreamParameters; + DeviceInfo : PPaDeviceInfo; + SampleRate : double; + OutParams : TPaStreamParameters; + StreamInfo : PPaStreamInfo; err : TPaError; begin Result := false; - deviceInfo := Pa_GetDeviceInfo(deviceIndex); + DeviceInfo := Pa_GetDeviceInfo(deviceIndex); - Log.LogInfo('Audio-Output Device: ' + deviceInfo^.name, 'TAudioPlayback_Portaudio.OpenDevice'); + Log.LogInfo('Audio-Output Device: ' + DeviceInfo^.name, 'TAudioPlayback_Portaudio.OpenDevice'); - sampleRate := deviceInfo^.defaultSampleRate; + SampleRate := DeviceInfo^.defaultSampleRate; - with outParams do + with OutParams do begin device := deviceIndex; channelCount := 2; sampleFormat := paInt16; - suggestedLatency := deviceInfo^.defaultLowOutputLatency; + suggestedLatency := DeviceInfo^.defaultLowOutputLatency; hostApiSpecificStreamInfo := nil; end; // check souncard and adjust sample-rate - if not AudioCore.TestDevice(nil, @outParams, sampleRate) then + if not AudioCore.TestDevice(nil, @OutParams, SampleRate) then begin Log.LogStatus('TestDevice failed!', 'TAudioPlayback_Portaudio.OpenDevice'); - exit; + Exit; end; // open output stream - err := Pa_OpenStream(paStream, nil, @outParams, sampleRate, + err := Pa_OpenStream(paStream, nil, @OutParams, SampleRate, paFramesPerBufferUnspecified, paNoFlag, @PortaudioAudioCallback, Self); if(err <> paNoError) then begin Log.LogStatus(Pa_GetErrorText(err), 'TAudioPlayback_Portaudio.OpenDevice'); paStream := nil; - exit; + Exit; end; - + + // get estimated latency (will be updated with real latency in the callback) + StreamInfo := Pa_GetStreamInfo(paStream); + if (StreamInfo <> nil) then + Latency := StreamInfo^.outputLatency + else + Latency := 0; + FormatInfo := TAudioFormatInfo.Create( - outParams.channelCount, - sampleRate, + OutParams.channelCount, + SampleRate, asfS16 // FIXME: is paInt16 system-dependant or -independant? ); @@ -131,7 +141,7 @@ var err: TPaError; errMsg: string; paDevice: TPortaudioOutputDevice; - inputParams: TPaStreamParameters; + outputParams: TPaStreamParameters; stream: PPaStream; streamInfo: PPaStreamInfo; sampleRate: double; @@ -140,6 +150,7 @@ var cbWorks: boolean; begin Result := false; + (* // choose the best available Audio-API paApiIndex := AudioCore.GetPreferredApiIndex(); @@ -190,10 +201,10 @@ begin // TODO: correct too low latencies (what is a too low latency, maybe < 10ms?) latency := deviceInfo^.defaultLowInputLatency; - // setup desired input parameters - // TODO: retry with input-latency set to 20ms (defaultLowInputLatency might + // setup desired output parameters + // TODO: retry with input-latency set to 20ms (defaultLowOutputLatency might // not be set correctly in OSS) - with inputParams do + with outputParams do begin device := deviceIndex; channelCount := channelCnt; @@ -203,24 +214,24 @@ begin end; // check if mic-callback works (might not be called on some devices) - if (not TAudioCore_Portaudio.TestDevice(@inputParams, nil, sampleRate)) then + if (not TAudioCore_Portaudio.TestDevice(nil, @outputParams, sampleRate)) then begin // ignore device if callback did not work Log.LogError('Device "'+paDevice.Name+'" does not respond', - 'TAudioInput_Portaudio.InitializeRecord'); + 'TAudioPlayback_Portaudio.InitializeRecord'); paDevice.Free(); continue; end; // open device for further info - err := Pa_OpenStream(stream, @inputParams, nil, sampleRate, + err := Pa_OpenStream(stream, nil, @outputParams, sampleRate, paFramesPerBufferUnspecified, paNoFlag, @MicrophoneTestCallback, nil); if(err <> paNoError) then begin // unable to open device -> skip errMsg := Pa_GetErrorText(err); Log.LogError('Device error: "'+ deviceName +'" ('+ errMsg +')', - 'TAudioInput_Portaudio.InitializeRecord'); + 'TAudioPlayback_Portaudio.InitializeRecord'); paDevice.Free(); continue; end; @@ -237,7 +248,7 @@ begin sampleRate := streamInfo^.sampleRate; end; end; - + // create audio-format info and resize capture-buffer array paDevice.AudioFormat := TAudioFormatInfo.Create( channelCnt, @@ -246,11 +257,11 @@ begin ); SetLength(paDevice.CaptureChannel, paDevice.AudioFormat.Channels); - Log.LogStatus('InputDevice "'+paDevice.Name+'"@' + + Log.LogStatus('OutputDevice "'+paDevice.Name+'"@' + IntToStr(paDevice.AudioFormat.Channels)+'x'+ FloatToStr(paDevice.AudioFormat.SampleRate)+'Hz ('+ - FloatTostr(inputParams.suggestedLatency)+'sec)' , - 'Portaudio.InitializeRecord'); + FloatTostr(outputParams.suggestedLatency)+'sec)' , + 'TAudioInput_Portaudio.InitializeRecord'); // close test-stream Pa_CloseStream(stream); @@ -262,9 +273,9 @@ begin SetLength(OutputDeviceList, SC); Log.LogStatus('#Output-Devices: ' + inttostr(SC), 'Portaudio'); +*) Result := true; - *) end; function TAudioPlayback_Portaudio.InitializeAudioPlaybackEngine(): boolean; @@ -274,7 +285,7 @@ var paOutDevice : TPaDeviceIndex; err: TPaError; begin - result := false; + Result := false; AudioCore := TAudioCore_Portaudio.GetInstance(); @@ -304,14 +315,14 @@ begin Exit; end; - result := true; + Result := true; end; function TAudioPlayback_Portaudio.StartAudioPlaybackEngine(): boolean; var err: TPaError; begin - result := false; + Result := false; if (paStream = nil) then Exit; @@ -320,10 +331,10 @@ begin if(err <> paNoError) then begin Log.LogStatus('Pa_StartStream: '+Pa_GetErrorText(err), 'UAudioPlayback_Portaudio'); - exit; + Exit; end; - result := true; + Result := true; end; procedure TAudioPlayback_Portaudio.StopAudioPlaybackEngine(); @@ -338,13 +349,13 @@ begin Result := true; end; +function TAudioPlayback_Portaudio.GetLatency(): double; +begin + Result := Latency; +end; -initialization - singleton_AudioPlaybackPortaudio := TAudioPlayback_Portaudio.create(); - AudioManager.add( singleton_AudioPlaybackPortaudio ); - -finalization - AudioManager.Remove( singleton_AudioPlaybackPortaudio ); +initialization + MediaManager.Add(TAudioPlayback_Portaudio.Create); end. diff --git a/Game/Code/Classes/UAudioPlayback_SDL.pas b/Game/Code/Classes/UAudioPlayback_SDL.pas index 14990855..deef91e8 100644 --- a/Game/Code/Classes/UAudioPlayback_SDL.pas +++ b/Game/Code/Classes/UAudioPlayback_SDL.pas @@ -26,34 +26,33 @@ uses type TAudioPlayback_SDL = class(TAudioPlayback_SoftMixer) private + Latency: double; function EnumDevices(): boolean; protected function InitializeAudioPlaybackEngine(): boolean; override; function StartAudioPlaybackEngine(): boolean; override; procedure StopAudioPlaybackEngine(); override; function FinalizeAudioPlaybackEngine(): boolean; override; + function GetLatency(): double; override; public function GetName: String; override; procedure MixBuffers(dst, src: PChar; size: Cardinal; volume: Single); override; end; -var - singleton_AudioPlaybackSDL : IAudioPlayback; - { TAudioPlayback_SDL } procedure SDLAudioCallback(userdata: Pointer; stream: PChar; len: integer); cdecl; var - engine: TAudioPlayback_SDL; + Engine: TAudioPlayback_SDL; begin - engine := TAudioPlayback_SDL(userdata); - engine.AudioCallback(stream, len); + Engine := TAudioPlayback_SDL(userdata); + Engine.AudioCallback(stream, len); end; function TAudioPlayback_SDL.GetName: String; begin - result := 'SDL_Playback'; + Result := 'SDL_Playback'; end; function TAudioPlayback_SDL.EnumDevices(): boolean; @@ -68,28 +67,29 @@ end; function TAudioPlayback_SDL.InitializeAudioPlaybackEngine(): boolean; var - desiredAudioSpec, obtainedAudioSpec: TSDL_AudioSpec; + DesiredAudioSpec, ObtainedAudioSpec: TSDL_AudioSpec; SampleBufferSize: integer; begin - result := false; + Result := false; EnumDevices(); if (SDL_InitSubSystem(SDL_INIT_AUDIO) = -1) then begin Log.LogError('SDL_InitSubSystem failed!', 'TAudioPlayback_SDL.InitializeAudioPlaybackEngine'); - exit; + Exit; end; SampleBufferSize := IAudioOutputBufferSizeVals[Ini.AudioOutputBufferSizeIndex]; if (SampleBufferSize <= 0) then begin - // Automatic setting defaults to 1024 samples - SampleBufferSize := 1024; + // Automatic setting default + // FIXME: too much glitches with 1024 samples + SampleBufferSize := 2048; //1024; end; - FillChar(desiredAudioSpec, sizeof(desiredAudioSpec), 0); - with desiredAudioSpec do + FillChar(DesiredAudioSpec, SizeOf(DesiredAudioSpec), 0); + with DesiredAudioSpec do begin freq := 44100; format := AUDIO_S16SYS; @@ -99,27 +99,36 @@ begin userdata := Self; end; - if(SDL_OpenAudio(@desiredAudioSpec, @obtainedAudioSpec) = -1) then + // Note: always use the "obtained" parameter, otherwise SDL might try to convert + // the samples itself if the desired format is not available. This might lead + // to problems if for example ALSA does not support 44100Hz and proposes 48000Hz. + // Without the obtained parameter, SDL would try to convert 44.1kHz to 48kHz with + // its crappy (non working) converter resulting in a wrong (too high) pitch. + if(SDL_OpenAudio(@DesiredAudioSpec, @ObtainedAudioSpec) = -1) then begin Log.LogStatus('SDL_OpenAudio: ' + SDL_GetError(), 'TAudioPlayback_SDL.InitializeAudioPlaybackEngine'); - exit; + Exit; end; FormatInfo := TAudioFormatInfo.Create( - obtainedAudioSpec.channels, - obtainedAudioSpec.freq, + ObtainedAudioSpec.channels, + ObtainedAudioSpec.freq, asfS16 ); + // Note: SDL does not provide info of the internal buffer state. + // So we use the average buffer-size. + Latency := (ObtainedAudioSpec.samples/2) / FormatInfo.SampleRate; + Log.LogStatus('Opened audio device', 'TAudioPlayback_SDL.InitializeAudioPlaybackEngine'); - result := true; + Result := true; end; function TAudioPlayback_SDL.StartAudioPlaybackEngine(): boolean; begin SDL_PauseAudio(0); - result := true; + Result := true; end; procedure TAudioPlayback_SDL.StopAudioPlaybackEngine(); @@ -134,6 +143,11 @@ begin Result := true; end; +function TAudioPlayback_SDL.GetLatency(): double; +begin + Result := Latency; +end; + procedure TAudioPlayback_SDL.MixBuffers(dst, src: PChar; size: Cardinal; volume: Single); begin SDL_MixAudio(PUInt8(dst), PUInt8(src), size, Round(volume * SDL_MIX_MAXVOLUME)); @@ -141,10 +155,6 @@ end; initialization - singleton_AudioPlaybackSDL := TAudioPlayback_SDL.create(); - AudioManager.add( singleton_AudioPlaybackSDL ); - -finalization - AudioManager.Remove( singleton_AudioPlaybackSDL ); + MediaManager.add(TAudioPlayback_SDL.Create); end. diff --git a/Game/Code/Classes/UAudioPlayback_SoftMixer.pas b/Game/Code/Classes/UAudioPlayback_SoftMixer.pas index 714e19ae..31d0412b 100644 --- a/Game/Code/Classes/UAudioPlayback_SoftMixer.pas +++ b/Game/Code/Classes/UAudioPlayback_SoftMixer.pas @@ -13,6 +13,7 @@ uses Classes, SysUtils, sdl, + URingBuffer, UMusic, UAudioPlaybackBase; @@ -23,86 +24,90 @@ type private Engine: TAudioPlayback_SoftMixer; - DecodeStream: TAudioDecodeStream; - SampleBuffer : PChar; + SampleBuffer: PChar; + SampleBufferSize: integer; SampleBufferCount: integer; // number of available bytes in SampleBuffer - SampleBufferPos : cardinal; - BytesAvail: integer; - cvt: TSDL_AudioCVT; + SampleBufferPos: cardinal; - Status: TStreamStatus; - Loop: boolean; + SourceBuffer: PChar; + SourceBufferSize: integer; + SourceBufferCount: integer; // number of available bytes in SourceBuffer + Converter: TAudioConverter; + Status: TStreamStatus; InternalLock: PSDL_Mutex; - SoundEffects: TList; - - _volume: single; + fVolume: single; FadeInStartTime, FadeInTime: cardinal; FadeInStartVolume, FadeInTargetVolume: single; + NeedsRewind: boolean; + procedure Reset(); - class function ConvertAudioFormatToSDL(Format: TAudioSampleFormat; out SDLFormat: UInt16): boolean; + procedure ApplySoundEffects(Buffer: PChar; BufferSize: integer); function InitFormatConversion(): boolean; + procedure FlushBuffers(); - procedure Lock(); {$IFDEF HasInline}inline;{$ENDIF} - procedure Unlock(); {$IFDEF HasInline}inline;{$ENDIF} + procedure LockSampleBuffer(); {$IFDEF HasInline}inline;{$ENDIF} + procedure UnlockSampleBuffer(); {$IFDEF HasInline}inline;{$ENDIF} + protected + function GetLatency(): double; override; + function GetStatus(): TStreamStatus; override; + function GetVolume(): single; override; + procedure SetVolume(Volume: single); override; + function GetLength(): real; override; + function GetLoop(): boolean; override; + procedure SetLoop(Enabled: boolean); override; + function GetPosition: real; override; + procedure SetPosition(Time: real); override; public constructor Create(Engine: TAudioPlayback_SoftMixer); destructor Destroy(); override; - function SetDecodeStream(decodeStream: TAudioDecodeStream): boolean; + function Open(SourceStream: TAudioSourceStream): boolean; override; + procedure Close(); override; procedure Play(); override; procedure Pause(); override; procedure Stop(); override; procedure FadeIn(Time: real; TargetVolume: single); override; - procedure Close(); override; + function GetAudioFormatInfo(): TAudioFormatInfo; override; - function GetLength(): real; override; - function GetStatus(): TStreamStatus; override; - function GetVolume(): single; override; - procedure SetVolume(Volume: single); override; - function GetLoop(): boolean; override; - procedure SetLoop(Enabled: boolean); override; - function GetPosition: real; override; - procedure SetPosition(Time: real); override; + function ReadData(Buffer: PChar; BufferSize: integer): integer; - function ReadData(Buffer: PChar; BufSize: integer): integer; + function GetPCMData(var Data: TPCMData): Cardinal; override; + procedure GetFFTData(var Data: TFFTData); override; - function GetPCMData(var data: TPCMData): Cardinal; override; - procedure GetFFTData(var data: TFFTData); override; - - procedure AddSoundEffect(effect: TSoundEffect); override; - procedure RemoveSoundEffect(effect: TSoundEffect); override; + procedure AddSoundEffect(Effect: TSoundEffect); override; + procedure RemoveSoundEffect(Effect: TSoundEffect); override; end; TAudioMixerStream = class private Engine: TAudioPlayback_SoftMixer; - activeStreams: TList; - mixerBuffer: PChar; - internalLock: PSDL_Mutex; + ActiveStreams: TList; + MixerBuffer: PChar; + InternalLock: PSDL_Mutex; - appVolume: single; + AppVolume: single; procedure Lock(); {$IFDEF HasInline}inline;{$ENDIF} procedure Unlock(); {$IFDEF HasInline}inline;{$ENDIF} function GetVolume(): single; - procedure SetVolume(volume: single); + procedure SetVolume(Volume: single); public constructor Create(Engine: TAudioPlayback_SoftMixer); destructor Destroy(); override; - procedure AddStream(stream: TAudioPlaybackStream); - procedure RemoveStream(stream: TAudioPlaybackStream); - function ReadData(Buffer: PChar; BufSize: integer): integer; + procedure AddStream(Stream: TAudioPlaybackStream); + procedure RemoveStream(Stream: TAudioPlaybackStream); + function ReadData(Buffer: PChar; BufferSize: integer): integer; - property Volume: single READ GetVolume WRITE SetVolume; + property Volume: single read GetVolume write SetVolume; end; TAudioPlayback_SoftMixer = class(TAudioPlaybackBase) @@ -115,9 +120,9 @@ type function StartAudioPlaybackEngine(): boolean; virtual; abstract; procedure StopAudioPlaybackEngine(); virtual; abstract; function FinalizeAudioPlaybackEngine(): boolean; virtual; abstract; - procedure AudioCallback(buffer: PChar; size: integer); {$IFDEF HasInline}inline;{$ENDIF} + procedure AudioCallback(Buffer: PChar; Size: integer); {$IFDEF HasInline}inline;{$ENDIF} - function OpenStream(const Filename: String): TAudioPlaybackStream; override; + function CreatePlaybackStream(): TAudioPlaybackStream; override; public function GetName: String; override; abstract; function InitializePlayback(): boolean; override; @@ -125,20 +130,46 @@ type procedure SetAppVolume(Volume: single); override; + function CreateVoiceStream(ChannelMap: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream; override; + function GetMixer(): TAudioMixerStream; {$IFDEF HasInline}inline;{$ENDIF} function GetAudioFormatInfo(): TAudioFormatInfo; - procedure MixBuffers(dst, src: PChar; size: Cardinal; volume: Single); virtual; + procedure MixBuffers(DstBuffer, SrcBuffer: PChar; Size: Cardinal; Volume: Single); virtual; + end; + +type + TGenericVoiceStream = class(TAudioVoiceStream) + private + VoiceBuffer: TRingBuffer; + BufferLock: PSDL_Mutex; + PlaybackStream: TGenericPlaybackStream; + Engine: TAudioPlayback_SoftMixer; + public + constructor Create(Engine: TAudioPlayback_SoftMixer); + + function Open(ChannelMap: integer; FormatInfo: TAudioFormatInfo): boolean; override; + procedure Close(); override; + procedure WriteData(Buffer: PChar; BufferSize: integer); override; + function ReadData(Buffer: PChar; BufferSize: integer): integer; override; + function IsEOF(): boolean; override; + function IsError(): boolean; override; end; +const + SOURCE_BUFFER_FRAMES = 4096; + +const + MAX_VOICE_DELAY = 0.500; // 20ms + implementation uses Math, - //samplerate, - UFFT, ULog, UIni, + UFFT, + UAudioConverter, UMain; { TAudioMixerStream } @@ -149,123 +180,123 @@ begin Self.Engine := Engine; - activeStreams := TList.Create; - internalLock := SDL_CreateMutex(); - appVolume := 1.0; + ActiveStreams := TList.Create; + InternalLock := SDL_CreateMutex(); + AppVolume := 1.0; end; destructor TAudioMixerStream.Destroy(); begin - if assigned(mixerBuffer) then - Freemem(mixerBuffer); - activeStreams.Free; - SDL_DestroyMutex(internalLock); + if assigned(MixerBuffer) then + Freemem(MixerBuffer); + ActiveStreams.Free; + SDL_DestroyMutex(InternalLock); inherited; end; procedure TAudioMixerStream.Lock(); begin - SDL_mutexP(internalLock); + SDL_mutexP(InternalLock); end; procedure TAudioMixerStream.Unlock(); begin - SDL_mutexV(internalLock); + SDL_mutexV(InternalLock); end; function TAudioMixerStream.GetVolume(): single; begin Lock(); - result := appVolume; + Result := AppVolume; Unlock(); end; -procedure TAudioMixerStream.SetVolume(volume: single); +procedure TAudioMixerStream.SetVolume(Volume: single); begin Lock(); - appVolume := volume; + AppVolume := Volume; Unlock(); end; -procedure TAudioMixerStream.AddStream(stream: TAudioPlaybackStream); +procedure TAudioMixerStream.AddStream(Stream: TAudioPlaybackStream); begin - if not assigned(stream) then + if not assigned(Stream) then Exit; Lock(); // check if stream is already in list to avoid duplicates - if (activeStreams.IndexOf(Pointer(stream)) = -1) then - activeStreams.Add(Pointer(stream)); + if (ActiveStreams.IndexOf(Pointer(Stream)) = -1) then + ActiveStreams.Add(Pointer(Stream)); Unlock(); end; (* - * Sets the entry of stream in the activeStreams-List to nil - * but does not remove it from the list (count is not changed!). + * Sets the entry of stream in the ActiveStreams-List to nil + * but does not remove it from the list (Count is not changed!). * Otherwise iterations over the elements might fail due to a - * changed count-property. - * Call activeStreams.Pack() to remove the nil-pointers - * or check for nil-pointers when accessing activeStreams. + * changed Count-property. + * Call ActiveStreams.Pack() to remove the nil-pointers + * or check for nil-pointers when accessing ActiveStreams. *) -procedure TAudioMixerStream.RemoveStream(stream: TAudioPlaybackStream); +procedure TAudioMixerStream.RemoveStream(Stream: TAudioPlaybackStream); var - index: integer; + Index: integer; begin Lock(); - index := activeStreams.IndexOf(Pointer(stream)); - if (index <> -1) then + Index := activeStreams.IndexOf(Pointer(Stream)); + if (Index <> -1) then begin // remove entry but do not decrease count-property - activeStreams[index] := nil; + ActiveStreams[Index] := nil; end; Unlock(); end; -function TAudioMixerStream.ReadData(Buffer: PChar; BufSize: integer): integer; +function TAudioMixerStream.ReadData(Buffer: PChar; BufferSize: integer): integer; var i: integer; - size: integer; - stream: TGenericPlaybackStream; - needsPacking: boolean; + Size: integer; + Stream: TGenericPlaybackStream; + NeedsPacking: boolean; begin - result := BufSize; + Result := BufferSize; // zero target-buffer (silence) - FillChar(Buffer^, BufSize, 0); + FillChar(Buffer^, BufferSize, 0); // resize mixer-buffer if necessary - ReallocMem(mixerBuffer, BufSize); - if not assigned(mixerBuffer) then + ReallocMem(MixerBuffer, BufferSize); + if not assigned(MixerBuffer) then Exit; Lock(); - needsPacking := false; + NeedsPacking := false; // mix streams to one stream - for i := 0 to activeStreams.Count-1 do + for i := 0 to ActiveStreams.Count-1 do begin - if (activeStreams[i] = nil) then + if (ActiveStreams[i] = nil) then begin - needsPacking := true; + NeedsPacking := true; continue; end; - stream := TGenericPlaybackStream(activeStreams[i]); + Stream := TGenericPlaybackStream(ActiveStreams[i]); // fetch data from current stream - size := stream.ReadData(mixerBuffer, BufSize); - if (size > 0) then + Size := Stream.ReadData(MixerBuffer, BufferSize); + if (Size > 0) then begin // mix stream-data with mixer-buffer // Note: use Self.appVolume instead of Self.Volume to prevent recursive locking - Engine.MixBuffers(Buffer, mixerBuffer, size, appVolume * stream.Volume); + Engine.MixBuffers(Buffer, MixerBuffer, Size, AppVolume * Stream.Volume); end; end; // remove nil-pointers from list - if (needsPacking) then + if (NeedsPacking) then begin - activeStreams.Pack(); + ActiveStreams.Pack(); end; Unlock(); @@ -278,7 +309,7 @@ constructor TGenericPlaybackStream.Create(Engine: TAudioPlayback_SoftMixer); begin inherited Create(); Self.Engine := Engine; - internalLock := SDL_CreateMutex(); + InternalLock := SDL_CreateMutex(); SoundEffects := TList.Create; Status := ssStopped; Reset(); @@ -287,342 +318,419 @@ end; destructor TGenericPlaybackStream.Destroy(); begin Close(); - SDL_DestroyMutex(internalLock); + SDL_DestroyMutex(InternalLock); FreeAndNil(SoundEffects); inherited; end; procedure TGenericPlaybackStream.Reset(); begin - // wake-up sleeping audio-callback threads in the ReadData()-function - if assigned(decodeStream) then - decodeStream.Close(); - - // stop audio-callback on this stream - Stop(); - - // reset and/or free data - - Loop := false; + SourceStream := nil; - // TODO: use DecodeStream.Unref() instead of Free(); - FreeAndNil(DecodeStream); + FreeAndNil(Converter); FreeMem(SampleBuffer); SampleBuffer := nil; SampleBufferPos := 0; - BytesAvail := 0; - - _volume := 0; - SoundEffects.Clear; - FadeInTime := 0; -end; + SampleBufferSize := 0; + SampleBufferCount := 0; -procedure TGenericPlaybackStream.Lock(); -begin - SDL_mutexP(internalLock); -end; + FreeMem(SourceBuffer); + SourceBuffer := nil; + SourceBufferSize := 0; + SourceBufferCount := 0; -procedure TGenericPlaybackStream.Unlock(); -begin - SDL_mutexV(internalLock); -end; + NeedsRewind := false; -class function TGenericPlaybackStream.ConvertAudioFormatToSDL(Format: TAudioSampleFormat; out SDLFormat: UInt16): boolean; -begin - case Format of - asfU8: SDLFormat := AUDIO_U8; - asfS8: SDLFormat := AUDIO_S8; - asfU16LSB: SDLFormat := AUDIO_U16LSB; - asfS16LSB: SDLFormat := AUDIO_S16LSB; - asfU16MSB: SDLFormat := AUDIO_U16MSB; - asfS16MSB: SDLFormat := AUDIO_S16MSB; - asfU16: SDLFormat := AUDIO_U16; - asfS16: SDLFormat := AUDIO_S16; - else begin - Result := false; - Exit; - end; - end; - Result := true; + fVolume := 0; + SoundEffects.Clear; + FadeInTime := 0; end; -function TGenericPlaybackStream.InitFormatConversion(): boolean; -var - srcFormat: UInt16; - dstFormat: UInt16; - srcFormatInfo: TAudioFormatInfo; - dstFormatInfo: TAudioFormatInfo; +function TGenericPlaybackStream.Open(SourceStream: TAudioSourceStream): boolean; begin Result := false; - srcFormatInfo := DecodeStream.GetAudioFormatInfo(); - dstFormatInfo := Engine.GetAudioFormatInfo(); + Close(); - if (not ConvertAudioFormatToSDL(srcFormatInfo.Format, srcFormat) or - not ConvertAudioFormatToSDL(dstFormatInfo.Format, dstFormat)) then - begin - Log.LogError('Audio-format not supported by SDL', 'TSoftMixerPlaybackStream.InitFormatConversion'); + if (not assigned(SourceStream)) then Exit; - end; + Self.SourceStream := SourceStream; - if (SDL_BuildAudioCVT(@cvt, - srcFormat, srcFormatInfo.Channels, Round(srcFormatInfo.SampleRate), - dstFormat, dstFormatInfo.Channels, Round(dstFormatInfo.SampleRate)) = -1) then + if (not InitFormatConversion()) then begin - Log.LogError(SDL_GetError(), 'TSoftMixerPlaybackStream.InitFormatConversion'); + // reset decode-stream so it will not be freed on destruction + Self.SourceStream := nil; Exit; end; + SourceBufferSize := SOURCE_BUFFER_FRAMES * SourceStream.GetAudioFormatInfo().FrameSize; + GetMem(SourceBuffer, SourceBufferSize); + fVolume := 1.0; + Result := true; end; -function TGenericPlaybackStream.SetDecodeStream(decodeStream: TAudioDecodeStream): boolean; +procedure TGenericPlaybackStream.Close(); begin - result := false; + // stop audio-callback on this stream + Stop(); + // Note: PerformOnClose must be called before SourceStream is invalidated + PerformOnClose(); + // and free data Reset(); +end; - if not assigned(decodeStream) then - Exit; - Self.DecodeStream := decodeStream; - if not InitFormatConversion() then - Exit; - - _volume := 1.0; +procedure TGenericPlaybackStream.LockSampleBuffer(); +begin + SDL_mutexP(InternalLock); +end; - result := true; +procedure TGenericPlaybackStream.UnlockSampleBuffer(); +begin + SDL_mutexV(InternalLock); end; -procedure TGenericPlaybackStream.Close(); +function TGenericPlaybackStream.InitFormatConversion(): boolean; +var + SrcFormatInfo: TAudioFormatInfo; + DstFormatInfo: TAudioFormatInfo; begin - Reset(); + Result := false; + + SrcFormatInfo := SourceStream.GetAudioFormatInfo(); + DstFormatInfo := GetAudioFormatInfo(); + + // TODO: selection should not be done here, use a factory (TAudioConverterFactory) instead + {$IF Defined(UseFFMpegResample)} + Converter := TAudioConverter_FFMpeg.Create(); + {$ELSEIF Defined(UseSRCResample)} + Converter := TAudioConverter_SRC.Create(); + {$ELSE} + Converter := TAudioConverter_SDL.Create(); + {$IFEND} + + Result := Converter.Init(SrcFormatInfo, DstFormatInfo); end; procedure TGenericPlaybackStream.Play(); var - mixer: TAudioMixerStream; + Mixer: TAudioMixerStream; begin - if (status = ssPlaying) then - begin - // rewind - if assigned(DecodeStream) then - DecodeStream.Position := 0; - end; - status := ssPlaying; - - mixer := Engine.GetMixer(); - if (mixer <> nil) then - mixer.AddStream(Self); + // only paused streams are not flushed + if (Status = ssPaused) then + NeedsRewind := false; + + // rewind if necessary. Cases that require no rewind are: + // - stream was created and never played + // - stream was paused and is resumed now + // - stream was stopped and set to a new position already + if (NeedsRewind) then + SetPosition(0); + + // update status + Status := ssPlaying; + + NeedsRewind := true; + + // add this stream to the mixer + Mixer := Engine.GetMixer(); + if (Mixer <> nil) then + Mixer.AddStream(Self); end; procedure TGenericPlaybackStream.FadeIn(Time: real; TargetVolume: single); begin FadeInTime := Trunc(Time * 1000); FadeInStartTime := SDL_GetTicks(); - FadeInStartVolume := _volume; + FadeInStartVolume := fVolume; FadeInTargetVolume := TargetVolume; Play(); end; procedure TGenericPlaybackStream.Pause(); var - mixer: TAudioMixerStream; + Mixer: TAudioMixerStream; begin - status := ssPaused; + if (Status <> ssPlaying) then + Exit; + + Status := ssPaused; - mixer := Engine.GetMixer(); - if (mixer <> nil) then - mixer.RemoveStream(Self); + Mixer := Engine.GetMixer(); + if (Mixer <> nil) then + Mixer.RemoveStream(Self); end; procedure TGenericPlaybackStream.Stop(); var - mixer: TAudioMixerStream; + Mixer: TAudioMixerStream; begin - if (status = ssStopped) then + if (Status = ssStopped) then Exit; - status := ssStopped; - - mixer := Engine.GetMixer(); - if (mixer <> nil) then - mixer.RemoveStream(Self); + Status := ssStopped; - // rewind (note: DecodeStream might be closed already, but this is not a problem) - if assigned(DecodeStream) then - DecodeStream.Position := 0; + Mixer := Engine.GetMixer(); + if (Mixer <> nil) then + Mixer.RemoveStream(Self); end; function TGenericPlaybackStream.GetLoop(): boolean; begin - result := Loop; + if assigned(SourceStream) then + Result := SourceStream.Loop + else + Result := false; end; procedure TGenericPlaybackStream.SetLoop(Enabled: boolean); begin - Loop := Enabled; + if assigned(SourceStream) then + SourceStream.Loop := Enabled; end; function TGenericPlaybackStream.GetLength(): real; begin - if assigned(DecodeStream) then - result := DecodeStream.Length + if assigned(SourceStream) then + Result := SourceStream.Length else - result := -1; + Result := -1; +end; + +function TGenericPlaybackStream.GetLatency(): double; +begin + Result := Engine.GetLatency(); end; function TGenericPlaybackStream.GetStatus(): TStreamStatus; begin - result := status; + Result := Status; end; -{* - * Note: 44.1kHz to 48kHz conversion or vice versa is not supported - * by SDL at the moment. No conversion takes place in this cases. - * This is because SDL just converts differences in powers of 2. - * So the result might not be that accurate. Although this is not - * audible in most cases it needs synchronization with the video - * or the lyrics timer. - * Using libsamplerate might give better results. - *} -function TGenericPlaybackStream.ReadData(Buffer: PChar; BufSize: integer): integer; -var - decodeBufSize: integer; - sampleBufSize: integer; - nBytesDecoded: integer; - frameSize: integer; - remFrameBytes: integer; - copyCnt: integer; - BytesNeeded: integer; - i: integer; +function TGenericPlaybackStream.GetAudioFormatInfo(): TAudioFormatInfo; begin - Result := -1; + Result := Engine.GetAudioFormatInfo(); +end; - BytesNeeded := BufSize; +procedure TGenericPlaybackStream.FlushBuffers(); +begin + SampleBufferCount := 0; + SampleBufferPos := 0; + SourceBufferCount := 0; +end; - // copy remaining data from the last call to the result-buffer - if (BytesAvail > 0) then +procedure TGenericPlaybackStream.ApplySoundEffects(Buffer: PChar; BufferSize: integer); +var + i: integer; +begin + for i := 0 to SoundEffects.Count-1 do begin - copyCnt := Min(BufSize, BytesAvail); - Move(SampleBuffer[SampleBufferPos], Buffer[0], copyCnt); - Dec(BytesAvail, copyCnt); - Dec(BytesNeeded, copyCnt); - if (BytesNeeded = 0) then + if (SoundEffects[i] <> nil) then begin - // Result-Buffer is full -> no need to decode more data. - // The sample-buffer might even contain some data for the next call - Inc(SampleBufferPos, copyCnt); - Result := BufSize; - Exit; + TSoundEffect(SoundEffects[i]).Callback(Buffer, BufferSize); end; end; +end; - if not assigned(DecodeStream) then - Exit; +function TGenericPlaybackStream.ReadData(Buffer: PChar; BufferSize: integer): integer; +var + ConversionInputCount: integer; + ConversionOutputSize: integer; // max. number of converted data (= buffer size) + ConversionOutputCount: integer; // actual number of converted data + SourceSize: integer; + RequestedSourceSize: integer; + NeededSampleBufferSize: integer; + BytesNeeded, BytesAvail: integer; + SourceFormatInfo, OutputFormatInfo: TAudioFormatInfo; + SourceFrameSize, OutputFrameSize: integer; + SkipOutputCount: integer; // number of output-data bytes to skip + SkipSourceCount: integer; // number of source-data bytes to skip + FillCount: integer; // number of bytes to fill with padding data + CopyCount: integer; + PadFrame: PChar; + i: integer; +begin + Result := -1; - // calc number of bytes to decode - decodeBufSize := Ceil(BufSize / cvt.len_ratio); - // assure that the decode-size is a multiple of the frame size - frameSize := DecodeStream.GetAudioFormatInfo().FrameSize; - remFrameBytes := decodeBufSize mod frameSize; - if (remFrameBytes > 0) then - decodeBufSize := decodeBufSize + (frameSize - remFrameBytes); + // sanity check for the source-stream + if (not assigned(SourceStream)) then + Exit; + + SkipOutputCount := 0; + SkipSourceCount := 0; + FillCount := 0; + + SourceFormatInfo := SourceStream.GetAudioFormatInfo(); + SourceFrameSize := SourceFormatInfo.FrameSize; + OutputFormatInfo := GetAudioFormatInfo(); + OutputFrameSize := OutputFormatInfo.FrameSize; + + // synchronize (adjust buffer size) + BytesNeeded := Synchronize(BufferSize, OutputFormatInfo); + if (BytesNeeded > BufferSize) then + begin + SkipOutputCount := BytesNeeded - BufferSize; + BytesNeeded := BufferSize; + end + else if (BytesNeeded < BufferSize) then + begin + FillCount := BufferSize - BytesNeeded; + end; - Lock(); + // lock access to sample-buffer + LockSampleBuffer(); try - // calc buffer size - sampleBufSize := decodeBufSize * cvt.len_mult; - - // resize buffer if necessary. - // The required buffer-size will be smaller than the result-buffer - // in most cases (if the decoded signal is mono or has a lesser bitrate). - // If the output-rate is 44.1kHz and the decode-rate is 48kHz or 96kHz it - // will be ~1.09 or ~2.18 times bigger. Those extra memory consumption - // should be reasonable. If not we should call TDecodeStream.ReadData() - // multiple times. - // Note: we do not decrease the buffer by the count of bytes used from - // the previous call of this function (bytesAvail). Otherwise the - // buffer will be reallocated each time this function is called just to - // add or remove a few bytes from the buffer. - // By not doing this the buffer's size should be rather stable and it - // will not be reallocated/resized at all if the BufSize params does not - // change in consecutive calls. - ReallocMem(SampleBuffer, sampleBufSize); - if not assigned(SampleBuffer) then - Exit; - // decode data - nBytesDecoded := DecodeStream.ReadData(SampleBuffer, decodeBufSize); - if (nBytesDecoded = -1) then - Exit; + // skip sample-buffer data + SampleBufferPos := SampleBufferPos + SkipOutputCount; + // size of available bytes in SampleBuffer after skipping + SampleBufferCount := SampleBufferCount - SampleBufferPos; + // update byte skip-count + SkipOutputCount := -SampleBufferCount; - // end-of-file reached -> stop playback - if (DecodeStream.EOF) then + // now that we skipped all buffered data from the last pass, we have to skip + // data directly after fetching it from the source-stream. + if (SkipOutputCount > 0) then begin - Stop(); + SampleBufferCount := 0; + // convert skip-count to source-format units and resize to a multiple of + // the source frame-size. + SkipSourceCount := Round((SkipOutputCount * OutputFormatInfo.GetRatio(SourceFormatInfo)) / + SourceFrameSize) * SourceFrameSize; + SkipOutputCount := 0; end; - // resample decoded data - cvt.buf := PUint8(SampleBuffer); - cvt.len := nBytesDecoded; - if (SDL_ConvertAudio(@cvt) = -1) then - Exit; + // copy data to front of buffer + if ((SampleBufferCount > 0) and (SampleBufferPos > 0)) then + Move(SampleBuffer[SampleBufferPos], SampleBuffer[0], SampleBufferCount); + SampleBufferPos := 0; - SampleBufferCount := cvt.len_cvt; + // resize buffer to a reasonable size + if (BufferSize > SampleBufferCount) then + begin + // Note: use BufferSize instead of BytesNeeded to minimize the need for resizing + SampleBufferSize := BufferSize; + ReallocMem(SampleBuffer, SampleBufferSize); + if (not assigned(SampleBuffer)) then + Exit; + end; - // apply effects - for i := 0 to SoundEffects.Count-1 do + // fill sample-buffer (fetch and convert one block of source data per loop) + while (SampleBufferCount < BytesNeeded) do begin - if (SoundEffects[i] <> nil) then + // move remaining source data from the previous pass to front of buffer + if (SourceBufferCount > 0) then + begin + Move(SourceBuffer[SourceBufferSize-SourceBufferCount], + SourceBuffer[0], + SourceBufferCount); + end; + + SourceSize := SourceStream.ReadData( + @SourceBuffer[SourceBufferCount], SourceBufferSize-SourceBufferCount); + // break on error (-1) or if no data is available (0), e.g. while seeking + if (SourceSize <= 0) then + begin + // if we do not have data -> exit + if (SourceBufferCount = 0) then + begin + FlushBuffers(); + Exit; + end; + // if we have some data, stop retrieving data from the source stream + // and use the data we have so far + Break; + end; + + SourceBufferCount := SourceBufferCount + SourceSize; + + // end-of-file reached -> stop playback + if (SourceStream.EOF) then begin - TSoundEffect(SoundEffects[i]).Callback(SampleBuffer, SampleBufferCount); + if (Loop) then + SourceStream.Position := 0 + else + Stop(); end; + + if (SkipSourceCount > 0) then + begin + // skip data and update source buffer count + SourceBufferCount := SourceBufferCount - SkipSourceCount; + SkipSourceCount := -SourceBufferCount; + // continue with next pass if we skipped all data + if (SourceBufferCount <= 0) then + begin + SourceBufferCount := 0; + Continue; + end; + end; + + // calc buffer size (might be bigger than actual resampled byte count) + ConversionOutputSize := Converter.GetOutputBufferSize(SourceBufferCount); + NeededSampleBufferSize := SampleBufferCount + ConversionOutputSize; + + // resize buffer if necessary + if (SampleBufferSize < NeededSampleBufferSize) then + begin + SampleBufferSize := NeededSampleBufferSize; + ReallocMem(SampleBuffer, SampleBufferSize); + if (not assigned(SampleBuffer)) then + begin + FlushBuffers(); + Exit; + end; + end; + + // resample source data (Note: ConversionInputCount might be adjusted by Convert()) + ConversionInputCount := SourceBufferCount; + ConversionOutputCount := Converter.Convert( + SourceBuffer, @SampleBuffer[SampleBufferCount], ConversionInputCount); + if (ConversionOutputCount = -1) then + begin + FlushBuffers(); + Exit; + end; + + // adjust sample- and source-buffer count by the number of converted bytes + SampleBufferCount := SampleBufferCount + ConversionOutputCount; + SourceBufferCount := SourceBufferCount - ConversionInputCount; end; - finally - Unlock(); - end; - BytesAvail := SampleBufferCount; - SampleBufferPos := 0; + // apply effects + ApplySoundEffects(SampleBuffer, SampleBufferCount); - // copy data to result buffer - copyCnt := Min(BytesNeeded, BytesAvail); - Move(SampleBuffer[0], Buffer[BufSize - BytesNeeded], copyCnt); - Dec(BytesAvail, copyCnt); - Dec(BytesNeeded, copyCnt); - Inc(SampleBufferPos, copyCnt); + // copy data to result buffer + CopyCount := Min(BytesNeeded, SampleBufferCount); + Move(SampleBuffer[0], Buffer[BufferSize - BytesNeeded], CopyCount); + Dec(BytesNeeded, CopyCount); + SampleBufferPos := CopyCount; - Result := BufSize - BytesNeeded; -end; + // release buffer lock + finally + UnlockSampleBuffer(); + end; -(* TODO: libsamplerate support -function TGenericPlaybackStream.ReadData(Buffer: PChar; BufSize: integer): integer; -var - convState: PSRC_STATE; - convData: SRC_DATA; - error: integer; -begin - // Note: needs mono->stereo conversion, multi-channel->stereo, etc. - // maybe we should use SDL for the channel-conversion stuff - // and use libsamplerate afterwards for the frequency-conversion - - //convState := src_new(SRC_SINC_MEDIUM_QUALITY, 2, @error); - //src_short_to_float_array(input, output, len); - convData. - if (src_process(convState, @convData) <> 0) then + // pad the buffer with the last frame if we are to fast + if (FillCount > 0) then begin - Log.LogError(src_strerror(src_error(convState)), 'TSoftMixerPlaybackStream.ReadData'); - Exit; + if (CopyCount >= OutputFrameSize) then + PadFrame := @Buffer[CopyCount-OutputFrameSize] + else + PadFrame := nil; + FillBufferWithFrame(@Buffer[CopyCount], FillCount, + PadFrame, OutputFrameSize); end; - src_float_to_short_array(); - //src_delete(convState); + + // BytesNeeded now contains the number of remaining bytes we were not able to fetch + Result := BufferSize - BytesNeeded; end; -*) -function TGenericPlaybackStream.GetPCMData(var data: TPCMData): Cardinal; +function TGenericPlaybackStream.GetPCMData(var Data: TPCMData): Cardinal; var - nBytes: integer; + ByteCount: integer; begin Result := 0; @@ -634,23 +742,23 @@ begin end; // zero memory - FillChar(data, SizeOf(data), 0); + FillChar(Data, SizeOf(Data), 0); // TODO: At the moment just the first samples of the SampleBuffer // are returned, even if there is newer data in the upper samples. - Lock(); - nBytes := Min(SizeOf(data), SampleBufferCount); - if (nBytes > 0) then + LockSampleBuffer(); + ByteCount := Min(SizeOf(Data), SampleBufferCount); + if (ByteCount > 0) then begin - Move(SampleBuffer[0], data, nBytes); + Move(SampleBuffer[0], Data, ByteCount); end; - Unlock(); + UnlockSampleBuffer(); - Result := nBytes div SizeOf(TPCMStereoSample); + Result := ByteCount div SizeOf(TPCMStereoSample); end; -procedure TGenericPlaybackStream.GetFFTData(var data: TFFTData); +procedure TGenericPlaybackStream.GetFFTData(var Data: TFFTData); var i: integer; Frames: integer; @@ -658,16 +766,14 @@ var AudioFormat: TAudioFormatInfo; begin // only works with SInt16 and Float values at the moment - AudioFormat := Engine.GetAudioFormatInfo(); + AudioFormat := GetAudioFormatInfo(); DataIn := AllocMem(FFTSize * SizeOf(Single)); if (DataIn = nil) then Exit; - Lock(); + LockSampleBuffer(); // TODO: We just use the first Frames frames, the others are ignored. - // This is OK for the equalizer display but not if we want to use - // this function for voice-analysis someday (I don't think we want). Frames := Min(FFTSize, SampleBufferCount div AudioFormat.FrameSize); // use only first channel and convert data to float-values case AudioFormat.Format of @@ -682,56 +788,84 @@ begin DataIn[i] := PSingle(@SampleBuffer[i*AudioFormat.FrameSize])^; end; end; - Unlock(); + UnlockSampleBuffer(); WindowFunc(fwfHanning, FFTSize, DataIn); - PowerSpectrum(FFTSize, DataIn, @data); + PowerSpectrum(FFTSize, DataIn, @Data); FreeMem(DataIn); // resize data to a 0..1 range for i := 0 to High(TFFTData) do begin - data[i] := Sqrt(data[i]) / 100; + Data[i] := Sqrt(Data[i]) / 100; end; end; -procedure TGenericPlaybackStream.AddSoundEffect(effect: TSoundEffect); +procedure TGenericPlaybackStream.AddSoundEffect(Effect: TSoundEffect); begin - if (not assigned(effect)) then + if (not assigned(Effect)) then Exit; - Lock(); + + LockSampleBuffer(); // check if effect is already in list to avoid duplicates - if (SoundEffects.IndexOf(Pointer(effect)) = -1) then - SoundEffects.Add(Pointer(effect)); - Unlock(); + if (SoundEffects.IndexOf(Pointer(Effect)) = -1) then + SoundEffects.Add(Pointer(Effect)); + UnlockSampleBuffer(); end; -procedure TGenericPlaybackStream.RemoveSoundEffect(effect: TSoundEffect); +procedure TGenericPlaybackStream.RemoveSoundEffect(Effect: TSoundEffect); begin - Lock(); - SoundEffects.Remove(effect); - Unlock(); + LockSampleBuffer(); + SoundEffects.Remove(Effect); + UnlockSampleBuffer(); end; function TGenericPlaybackStream.GetPosition: real; +var + BufferedTime: double; begin - if assigned(DecodeStream) then - result := DecodeStream.Position + if assigned(SourceStream) then + begin + LockSampleBuffer(); + + // calc the time of source data that is buffered (in the SampleBuffer and SourceBuffer) + // but not yet outputed + BufferedTime := (SampleBufferCount - SampleBufferPos) / Engine.FormatInfo.BytesPerSec + + SourceBufferCount / SourceStream.GetAudioFormatInfo().BytesPerSec; + // and subtract it from the source position + Result := SourceStream.Position - BufferedTime; + + UnlockSampleBuffer(); + end else - result := -1; + begin + Result := -1; + end; end; procedure TGenericPlaybackStream.SetPosition(Time: real); begin - if assigned(DecodeStream) then - DecodeStream.Position := Time; + if assigned(SourceStream) then + begin + LockSampleBuffer(); + + SourceStream.Position := Time; + if (Status = ssStopped) then + NeedsRewind := false; + // do not use outdated data + FlushBuffers(); + + AvgSyncDiff := -1; + + UnlockSampleBuffer(); + end; end; function TGenericPlaybackStream.GetVolume(): single; var FadeAmount: Single; begin - Lock(); + LockSampleBuffer(); // adjust volume if fading is enabled if (FadeInTime > 0) then begin @@ -741,32 +875,131 @@ begin begin // target reached -> stop fading FadeInTime := 0; - _volume := FadeInTargetVolume; + fVolume := FadeInTargetVolume; end else begin // fading in progress - _volume := FadeAmount*FadeInTargetVolume + (1-FadeAmount)*FadeInStartVolume; + fVolume := FadeAmount*FadeInTargetVolume + (1-FadeAmount)*FadeInStartVolume; end; end; // return current volume - Result := _volume; - Unlock(); + Result := fVolume; + UnlockSampleBuffer(); end; -procedure TGenericPlaybackStream.SetVolume(volume: single); +procedure TGenericPlaybackStream.SetVolume(Volume: single); begin - Lock(); + LockSampleBuffer(); // stop fading FadeInTime := 0; // clamp volume - if (volume > 1.0) then - _volume := 1.0 - else if (volume < 0) then - _volume := 0 + if (Volume > 1.0) then + fVolume := 1.0 + else if (Volume < 0) then + fVolume := 0 else - _volume := volume; - Unlock(); + fVolume := Volume; + UnlockSampleBuffer(); +end; + + +{ TGenericVoiceStream } + +constructor TGenericVoiceStream.Create(Engine: TAudioPlayback_SoftMixer); +begin + inherited Create(); + Self.Engine := Engine; +end; + +function TGenericVoiceStream.Open(ChannelMap: integer; FormatInfo: TAudioFormatInfo): boolean; +var + BufferSize: integer; +begin + Result := false; + + Close(); + + if (not inherited Open(ChannelMap, FormatInfo)) then + Exit; + + // Note: + // - use Self.FormatInfo instead of FormatInfo as the latter one might have a + // channel size of 2. + // - the buffer-size must be a multiple of the FrameSize + BufferSize := (Ceil(MAX_VOICE_DELAY * Self.FormatInfo.BytesPerSec) div Self.FormatInfo.FrameSize) * + Self.FormatInfo.FrameSize; + VoiceBuffer := TRingBuffer.Create(BufferSize); + + BufferLock := SDL_CreateMutex(); + + + // create a matching playback stream for the voice-stream + PlaybackStream := TGenericPlaybackStream.Create(Engine); + // link voice- and playback-stream + if (not PlaybackStream.Open(Self)) then + begin + PlaybackStream.Free; + Exit; + end; + + // start voice passthrough + PlaybackStream.Play(); + + Result := true; +end; + +procedure TGenericVoiceStream.Close(); +begin + // stop and free the playback stream + FreeAndNil(PlaybackStream); + + // free data + FreeAndNil(VoiceBuffer); + if (BufferLock <> nil) then + SDL_DestroyMutex(BufferLock); + + inherited Close(); +end; + +procedure TGenericVoiceStream.WriteData(Buffer: PChar; BufferSize: integer); +begin + // lock access to buffer + SDL_mutexP(BufferLock); + try + if (VoiceBuffer = nil) then + Exit; + VoiceBuffer.Write(Buffer, BufferSize); + finally + SDL_mutexV(BufferLock); + end; +end; + +function TGenericVoiceStream.ReadData(Buffer: PChar; BufferSize: integer): integer; +begin + Result := -1; + + // lock access to buffer + SDL_mutexP(BufferLock); + try + if (VoiceBuffer = nil) then + Exit; + Result := VoiceBuffer.Read(Buffer, BufferSize); + finally + SDL_mutexV(BufferLock); + end; +end; + +function TGenericVoiceStream.IsEOF(): boolean; +begin + SDL_mutexP(BufferLock); + Result := (VoiceBuffer = nil); + SDL_mutexV(BufferLock); +end; + +function TGenericVoiceStream.IsError(): boolean; +begin + Result := false; end; @@ -774,7 +1007,7 @@ end; function TAudioPlayback_SoftMixer.InitializePlayback: boolean; begin - result := false; + Result := false; //Log.LogStatus('InitializePlayback', 'UAudioPlayback_SoftMixer'); @@ -786,7 +1019,7 @@ begin if(not StartAudioPlaybackEngine()) then Exit; - result := true; + Result := true; end; function TAudioPlayback_SoftMixer.FinalizePlayback: boolean; @@ -802,9 +1035,9 @@ begin Result := true; end; -procedure TAudioPlayback_SoftMixer.AudioCallback(buffer: PChar; size: integer); +procedure TAudioPlayback_SoftMixer.AudioCallback(Buffer: PChar; Size: integer); begin - MixerStream.ReadData(buffer, size); + MixerStream.ReadData(Buffer, Size); end; function TAudioPlayback_SoftMixer.GetMixer(): TAudioMixerStream; @@ -817,38 +1050,26 @@ begin Result := FormatInfo; end; -function TAudioPlayback_SoftMixer.OpenStream(const Filename: String): TAudioPlaybackStream; +function TAudioPlayback_SoftMixer.CreatePlaybackStream(): TAudioPlaybackStream; +begin + Result := TGenericPlaybackStream.Create(Self); +end; + +function TAudioPlayback_SoftMixer.CreateVoiceStream(ChannelMap: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream; var - decodeStream: TAudioDecodeStream; - playbackStream: TGenericPlaybackStream; + VoiceStream: TGenericVoiceStream; begin Result := nil; - if (AudioDecoder = nil) then - Exit; - - decodeStream := AudioDecoder.Open(Filename); - if not assigned(decodeStream) then + // create a voice stream + VoiceStream := TGenericVoiceStream.Create(Self); + if (not VoiceStream.Open(ChannelMap, FormatInfo)) then begin - Log.LogStatus('LoadSoundFromFile: Sound not found "' + Filename + '"', 'UAudioPlayback_SoftMixer'); + VoiceStream.Free; Exit; end; - playbackStream := TGenericPlaybackStream.Create(Self); - if (not assigned(playbackStream)) then - begin - FreeAndNil(decodeStream); - Exit; - end; - - if (not playbackStream.SetDecodeStream(decodeStream)) then - begin - FreeAndNil(playbackStream); - FreeAndNil(decodeStream); - Exit; - end; - - result := playbackStream; + Result := VoiceStream; end; procedure TAudioPlayback_SoftMixer.SetAppVolume(Volume: single); @@ -857,47 +1078,46 @@ begin MixerStream.Volume := Volume; end; -procedure TAudioPlayback_SoftMixer.MixBuffers(dst, src: PChar; size: Cardinal; volume: Single); +procedure TAudioPlayback_SoftMixer.MixBuffers(DstBuffer, SrcBuffer: PChar; Size: Cardinal; Volume: Single); var SampleIndex: Cardinal; SampleInt: Integer; SampleFlt: Single; begin - - // TODO: optimize this code, e.g. with assembler (MMX) - SampleIndex := 0; case FormatInfo.Format of asfS16: begin - while (SampleIndex < size) do + while (SampleIndex < Size) do begin // apply volume and sum with previous mixer value - SampleInt := PSmallInt(@dst[SampleIndex])^ + Round(PSmallInt(@src[SampleIndex])^ * volume); + SampleInt := PSmallInt(@DstBuffer[SampleIndex])^ + + Round(PSmallInt(@SrcBuffer[SampleIndex])^ * Volume); // clip result if (SampleInt > High(SmallInt)) then SampleInt := High(SmallInt) else if (SampleInt < Low(SmallInt)) then SampleInt := Low(SmallInt); // assign result - PSmallInt(@dst[SampleIndex])^ := SampleInt; + PSmallInt(@DstBuffer[SampleIndex])^ := SampleInt; // increase index by one sample Inc(SampleIndex, SizeOf(SmallInt)); end; end; asfFloat: begin - while (SampleIndex < size) do + while (SampleIndex < Size) do begin // apply volume and sum with previous mixer value - SampleFlt := PSingle(@dst[SampleIndex])^ + PSingle(@src[SampleIndex])^ * volume; + SampleFlt := PSingle(@DstBuffer[SampleIndex])^ + + PSingle(@SrcBuffer[SampleIndex])^ * Volume; // clip result if (SampleFlt > 1.0) then SampleFlt := 1.0 else if (SampleFlt < -1.0) then SampleFlt := -1.0; // assign result - PSingle(@dst[SampleIndex])^ := SampleFlt; + PSingle(@DstBuffer[SampleIndex])^ := SampleFlt; // increase index by one sample Inc(SampleIndex, SizeOf(Single)); end; diff --git a/Game/Code/Classes/UCommon.pas b/Game/Code/Classes/UCommon.pas index 3c32a804..1b0a6e6c 100644 --- a/Game/Code/Classes/UCommon.pas +++ b/Game/Code/Classes/UCommon.pas @@ -81,6 +81,9 @@ function IsControlChar(ch: WideChar): boolean; // A stable alternative to TList.Sort() (use TList.Sort() if applicable, see below) procedure MergeSort(List: TList; CompareFunc: TListSortCompare); +function GetAlignedMem(Size: cardinal; Alignment: integer): Pointer; +procedure FreeAlignedMem(P: Pointer); + implementation @@ -733,7 +736,7 @@ begin RightSize := BlockSize - LeftSize; MidPos := StartPos + LeftSize; - // sort left and right halves of this block by recursive calls of this function + // sort left and right halves of this block by recursive calls of this function if (LeftSize >= 2) then _MergeSort(InList, OutList, TempList, StartPos, LeftSize, CompareFunc) else @@ -798,6 +801,62 @@ begin end; +type + // stores the unaligned pointer of data allocated by GetAlignedMem() + PMemAlignHeader = ^TMemAlignHeader; + TMemAlignHeader = Pointer; + +(** + * Use this function to assure that allocated memory is aligned on a specific + * byte boundary. + * Alignment must be a power of 2. + * + * Important: Memory allocated with GetAlignedMem() MUST be freed with + * FreeAlignedMem(), FreeMem() will cause a segmentation fault. + * + * Hint: If you do not need dynamic memory, consider to allocate memory + * statically and use the {$ALIGN x} compiler directive. Note that delphi + * supports an alignment "x" of up to 8 bytes only whereas FPC supports + * alignments on 16 and 32 byte boundaries too. + *) +function GetAlignedMem(Size: cardinal; Alignment: integer): Pointer; +var + OrigPtr: Pointer; +const + MIN_ALIGNMENT = 16; +begin + // Delphi and FPC (tested with 2.2.0) align memory blocks allocated with + // GetMem() at least on 8 byte boundaries. Delphi uses a minimal alignment + // of either 8 or 16 bytes depending on the size of the requested block + // (see System.GetMinimumBlockAlignment). As we do not want to change the + // boundary for the worse, we align at least on MIN_ALIGN. + if (Alignment < MIN_ALIGNMENT) then + Alignment := MIN_ALIGNMENT; + + // allocate unaligned memory + GetMem(OrigPtr, SizeOf(TMemAlignHeader) + Size + Alignment); + if (OrigPtr = nil) then + begin + Result := nil; + Exit; + end; + + // reserve space for the header + Result := Pointer(PtrUInt(OrigPtr) + SizeOf(TMemAlignHeader)); + // align memory + Result := Pointer(PtrUInt(Result) + Alignment - PtrUInt(Result) mod Alignment); + + // set header with info on old pointer for FreeMem + PMemAlignHeader(PtrUInt(Result) - SizeOf(TMemAlignHeader))^ := OrigPtr; +end; + +procedure FreeAlignedMem(P: Pointer); +begin + if (P <> nil) then + FreeMem(PMemAlignHeader(PtrUInt(P) - SizeOf(TMemAlignHeader))^); +end; + + initialization InitConsoleOutput(); diff --git a/Game/Code/Classes/UConfig.pas b/Game/Code/Classes/UConfig.pas index 46ba2e74..58aa704c 100644 --- a/Game/Code/Classes/UConfig.pas +++ b/Game/Code/Classes/UConfig.pas @@ -164,6 +164,12 @@ const (PORTAUDIO_VERSION_RELEASE * VERSION_RELEASE); {$ENDIF} + {$IFDEF HaveLibsamplerate} + LIBSAMPLERATE_VERSION = (LIBSAMPLERATE_VERSION_MAJOR * VERSION_MAJOR) + + (LIBSAMPLERATE_VERSION_MINOR * VERSION_MINOR) + + (LIBSAMPLERATE_VERSION_RELEASE * VERSION_RELEASE); + {$ENDIF} + function USDXVersionStr(): string; function USDXShortVersionStr(): string; diff --git a/Game/Code/Classes/UGraphic.pas b/Game/Code/Classes/UGraphic.pas index cc876b65..ae145955 100644 --- a/Game/Code/Classes/UGraphic.pas +++ b/Game/Code/Classes/UGraphic.pas @@ -18,6 +18,7 @@ uses SysUtils, ULyrics, UImage, + UMusic, UScreenLoading, UScreenWelcome, UScreenMain, @@ -454,13 +455,15 @@ begin Log.LogBenchmark('--> Loading Fonts', 2); } + // Note: do not initialize video modules earlier. They might depend on some + // SDL video functions or OpenGL extensions initialized in InitializeScreen() + InitializeVideo(); + //Log.BenchmarkStart(2); Log.LogStatus('TDisplay.Create', 'UGraphic.Initialize3D'); Display := TDisplay.Create; - Log.LogStatus('SDL_EnableUnicode', 'UGraphic.Initialize3D'); - SDL_EnableUnicode(1); //Log.BenchmarkEnd(2); Log.LogBenchmark('====> Creating Display', 2); //Log.LogStatus('Loading Screens', 'Initialize3D'); @@ -551,9 +554,14 @@ begin SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 5); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5); SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 5); - SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); // Z-Buffer depth SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + // VSYNC works for windows only at the moment. SDL_GL_SWAP_CONTROL under + // linux uses GLX_MESA_swap_control which is not supported by nvidea cards. + // Maybe use glXSwapIntervalSGI(1) from the GLX_SGI_swap_control extension instead. + //SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, 1); // VSYNC (currently Windows only) + // If there is a resolution in Parameters, use it, else use the Ini value I := Params.Resolution; if (I <> -1) then diff --git a/Game/Code/Classes/UMain.pas b/Game/Code/Classes/UMain.pas index 46d86447..d045ee34 100644 --- a/Game/Code/Classes/UMain.pas +++ b/Game/Code/Classes/UMain.pas @@ -10,7 +10,6 @@ interface uses SDL, - UGraphic, UMusic, URecord, UTime, @@ -21,8 +20,7 @@ uses ULyrics, UScreenSing, USong, - gl, - UThemes; + gl; type PPLayerNote = ^TPlayerNote; @@ -138,9 +136,11 @@ uses UConfig, UCore, UCommon, + UGraphic, UGraphicClasses, UPluginDefs, - UPlatform; + UPlatform, + UThemes; @@ -167,6 +167,7 @@ begin // Initialize SDL // Without SDL_INIT_TIMER SDL_GetTicks() might return strange values SDL_Init(SDL_INIT_VIDEO or SDL_INIT_TIMER); + SDL_EnableUnicode(1); USTime := TTime.Create; VideoBGTimer := TRelativeTimer.Create; @@ -353,7 +354,7 @@ begin // call an uninitialize routine for every initialize step // or at least use the corresponding Free-Methods - FinalizeSound(); + FinalizeMedia(); TTF_Quit(); SDL_Quit(); diff --git a/Game/Code/Classes/UMediaCore_FFMpeg.pas b/Game/Code/Classes/UMediaCore_FFMpeg.pas new file mode 100644 index 00000000..b914f6be --- /dev/null +++ b/Game/Code/Classes/UMediaCore_FFMpeg.pas @@ -0,0 +1,405 @@ +unit UMediaCore_FFMpeg; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + +uses + UMusic, + avcodec, + avformat, + avutil, + ULog, + sdl; + +type + PPacketQueue = ^TPacketQueue; + TPacketQueue = class + private + FirstListEntry: PAVPacketList; + LastListEntry: PAVPacketList; + PacketCount: integer; + Mutex: PSDL_Mutex; + Condition: PSDL_Cond; + Size: integer; + AbortRequest: boolean; + public + constructor Create(); + destructor Destroy(); override; + + function Put(Packet : PAVPacket): integer; + function PutStatus(StatusFlag: integer; StatusInfo: Pointer): integer; + procedure FreeStatusInfo(var Packet: TAVPacket); + function GetStatusInfo(var Packet: TAVPacket): Pointer; + function Get(var Packet: TAVPacket; Blocking: boolean): integer; + function GetSize(): integer; + procedure Flush(); + procedure Abort(); + function IsAborted(): boolean; + end; + +const + STATUS_PACKET: PChar = 'STATUS_PACKET'; +const + PKT_STATUS_FLAG_EOF = 1; // signal end-of-file + PKT_STATUS_FLAG_FLUSH = 2; // request the decoder to flush its avcodec decode buffers + PKT_STATUS_FLAG_ERROR = 3; // signal an error state + PKT_STATUS_FLAG_EMPTY = 4; // request the decoder to output empty data (silence or black frames) + +type + TMediaCore_FFMpeg = class + private + AVCodecLock: PSDL_Mutex; + public + constructor Create(); + destructor Destroy(); override; + class function GetInstance(): TMediaCore_FFMpeg; + + function GetErrorString(ErrorNum: integer): string; + function FindStreamIDs(FormatCtx: PAVFormatContext; out FirstVideoStream, FirstAudioStream: integer ): boolean; + function FindAudioStreamIndex(FormatCtx: PAVFormatContext): integer; + function ConvertFFMpegToAudioFormat(FFMpegFormat: TSampleFormat; out Format: TAudioSampleFormat): boolean; + procedure LockAVCodec(); + procedure UnlockAVCodec(); + end; + +implementation + +uses + SysUtils; + +var + Instance: TMediaCore_FFMpeg; + +constructor TMediaCore_FFMpeg.Create(); +begin + inherited; + AVCodecLock := SDL_CreateMutex(); +end; + +destructor TMediaCore_FFMpeg.Destroy(); +begin + SDL_DestroyMutex(AVCodecLock); + inherited; +end; + +class function TMediaCore_FFMpeg.GetInstance(): TMediaCore_FFMpeg; +begin + if (not Assigned(Instance)) then + Instance := TMediaCore_FFMpeg.Create(); + Result := Instance; +end; + +procedure TMediaCore_FFMpeg.LockAVCodec(); +begin + SDL_mutexP(AVCodecLock); +end; + +procedure TMediaCore_FFMpeg.UnlockAVCodec(); +begin + SDL_mutexV(AVCodecLock); +end; + +function TMediaCore_FFMpeg.GetErrorString(ErrorNum: integer): string; +begin + case ErrorNum of + AVERROR_IO: Result := 'AVERROR_IO'; + AVERROR_NUMEXPECTED: Result := 'AVERROR_NUMEXPECTED'; + AVERROR_INVALIDDATA: Result := 'AVERROR_INVALIDDATA'; + AVERROR_NOMEM: Result := 'AVERROR_NOMEM'; + AVERROR_NOFMT: Result := 'AVERROR_NOFMT'; + AVERROR_NOTSUPP: Result := 'AVERROR_NOTSUPP'; + AVERROR_NOENT: Result := 'AVERROR_NOENT'; + AVERROR_PATCHWELCOME: Result := 'AVERROR_PATCHWELCOME'; + else Result := 'AVERROR_#'+inttostr(ErrorNum); + end; +end; + +{ + @param(FormatCtx is a PAVFormatContext returned from av_open_input_file ) + @param(FirstVideoStream is an OUT value of type integer, this is the index of the video stream) + @param(FirstAudioStream is an OUT value of type integer, this is the index of the audio stream) + @returns(@true on success, @false otherwise) +} +function TMediaCore_FFMpeg.FindStreamIDs(FormatCtx: PAVFormatContext; out FirstVideoStream, FirstAudioStream: integer): boolean; +var + i: integer; + Stream: PAVStream; +begin + // find the first video stream + FirstAudioStream := -1; + FirstVideoStream := -1; + + for i := 0 to FormatCtx.nb_streams-1 do + begin + Stream := FormatCtx.streams[i]; + + if (Stream.codec.codec_type = CODEC_TYPE_VIDEO) and + (FirstVideoStream < 0) then + begin + FirstVideoStream := i; + end; + + if (Stream.codec.codec_type = CODEC_TYPE_AUDIO) and + (FirstAudioStream < 0) then + begin + FirstAudioStream := i; + end; + end; + + // return true if either an audio- or video-stream was found + Result := (FirstAudioStream > -1) or + (FirstVideoStream > -1) ; +end; + +function TMediaCore_FFMpeg.FindAudioStreamIndex(FormatCtx: PAVFormatContext): integer; +var + i: integer; + StreamIndex: integer; + Stream: PAVStream; +begin + // find the first audio stream + StreamIndex := -1; + + for i := 0 to FormatCtx^.nb_streams-1 do + begin + Stream := FormatCtx^.streams[i]; + + if (Stream.codec^.codec_type = CODEC_TYPE_AUDIO) then + begin + StreamIndex := i; + Break; + end; + end; + + Result := StreamIndex; +end; + +function TMediaCore_FFMpeg.ConvertFFMpegToAudioFormat(FFMpegFormat: TSampleFormat; out Format: TAudioSampleFormat): boolean; +begin + case FFMpegFormat of + SAMPLE_FMT_U8: Format := asfU8; + SAMPLE_FMT_S16: Format := asfS16; + SAMPLE_FMT_S24: Format := asfS24; + SAMPLE_FMT_S32: Format := asfS32; + SAMPLE_FMT_FLT: Format := asfFloat; + else begin + Result := false; + Exit; + end; + end; + Result := true; +end; + +{ TPacketQueue } + +constructor TPacketQueue.Create(); +begin + inherited; + + FirstListEntry := nil; + LastListEntry := nil; + PacketCount := 0; + Size := 0; + + Mutex := SDL_CreateMutex(); + Condition := SDL_CreateCond(); +end; + +destructor TPacketQueue.Destroy(); +begin + Flush(); + SDL_DestroyMutex(Mutex); + SDL_DestroyCond(Condition); + inherited; +end; + +procedure TPacketQueue.Abort(); +begin + SDL_LockMutex(Mutex); + + AbortRequest := true; + + SDL_CondBroadcast(Condition); + SDL_UnlockMutex(Mutex); +end; + +function TPacketQueue.IsAborted(): boolean; +begin + SDL_LockMutex(Mutex); + Result := AbortRequest; + SDL_UnlockMutex(Mutex); +end; + +function TPacketQueue.Put(Packet : PAVPacket): integer; +var + CurrentListEntry : PAVPacketList; +begin + Result := -1; + + if (Packet = nil) then + Exit; + + if (PChar(Packet^.data) <> STATUS_PACKET) then + begin + if (av_dup_packet(Packet) < 0) then + Exit; + end; + + CurrentListEntry := av_malloc(SizeOf(TAVPacketList)); + if (CurrentListEntry = nil) then + Exit; + + CurrentListEntry^.pkt := Packet^; + CurrentListEntry^.next := nil; + + SDL_LockMutex(Mutex); + try + if (LastListEntry = nil) then + FirstListEntry := CurrentListEntry + else + LastListEntry^.next := CurrentListEntry; + + LastListEntry := CurrentListEntry; + Inc(PacketCount); + + Size := Size + CurrentListEntry^.pkt.size; + SDL_CondSignal(Condition); + finally + SDL_UnlockMutex(Mutex); + end; + + Result := 0; +end; + +(** + * Adds a status packet (EOF, Flush, etc.) to the end of the queue. + * StatusInfo can be used to pass additional information to the decoder. + * Only assign nil or a valid pointer to data allocated with Getmem() to + * StatusInfo because the pointer will be disposed with Freemem() on a call + * to Flush(). If the packet is removed from the queue it is the decoder's + * responsibility to free the StatusInfo data with FreeStatusInfo(). + *) +function TPacketQueue.PutStatus(StatusFlag: integer; StatusInfo: Pointer): integer; +var + TempPacket: PAVPacket; +begin + // create temp. package + TempPacket := av_malloc(SizeOf(TAVPacket)); + if (TempPacket = nil) then + begin + Result := -1; + Exit; + end; + // init package + av_init_packet(TempPacket^); + TempPacket^.data := Pointer(STATUS_PACKET); + TempPacket^.flags := StatusFlag; + TempPacket^.priv := StatusInfo; + // put a copy of the package into the queue + Result := Put(TempPacket); + // data has been copied -> delete temp. package + av_free(TempPacket); +end; + +procedure TPacketQueue.FreeStatusInfo(var Packet: TAVPacket); +begin + if (Packet.priv <> nil) then + FreeMem(Packet.priv); +end; + +function TPacketQueue.GetStatusInfo(var Packet: TAVPacket): Pointer; +begin + Result := Packet.priv; +end; + +function TPacketQueue.Get(var Packet: TAVPacket; Blocking: boolean): integer; +var + CurrentListEntry: PAVPacketList; +const + WAIT_TIMEOUT = 10; // timeout in ms +begin + Result := -1; + + SDL_LockMutex(Mutex); + try + while (true) do + begin + if (AbortRequest) then + Exit; + + CurrentListEntry := FirstListEntry; + if (CurrentListEntry <> nil) then + begin + FirstListEntry := CurrentListEntry^.next; + if (FirstListEntry = nil) then + LastListEntry := nil; + Dec(PacketCount); + + Size := Size - CurrentListEntry^.pkt.size; + Packet := CurrentListEntry^.pkt; + av_free(CurrentListEntry); + + Result := 1; + Break; + end + else if (not Blocking) then + begin + Result := 0; + Break; + end + else + begin + // block until a new package arrives, + // but do not wait till infinity to avoid deadlocks + if (SDL_CondWaitTimeout(Condition, Mutex, WAIT_TIMEOUT) = SDL_MUTEX_TIMEDOUT) then + begin + Result := 0; + Break; + end; + end; + end; + finally + SDL_UnlockMutex(Mutex); + end; +end; + +function TPacketQueue.GetSize(): integer; +begin + SDL_LockMutex(Mutex); + Result := Size; + SDL_UnlockMutex(Mutex); +end; + +procedure TPacketQueue.Flush(); +var + CurrentListEntry, TempListEntry: PAVPacketList; +begin + SDL_LockMutex(Mutex); + + CurrentListEntry := FirstListEntry; + while(CurrentListEntry <> nil) do + begin + TempListEntry := CurrentListEntry^.next; + // free status data + if (PChar(CurrentListEntry^.pkt.data) = STATUS_PACKET) then + FreeStatusInfo(CurrentListEntry^.pkt); + // free packet data + av_free_packet(@CurrentListEntry^.pkt); + // Note: param must be a pointer to a pointer! + av_freep(@CurrentListEntry); + CurrentListEntry := TempListEntry; + end; + LastListEntry := nil; + FirstListEntry := nil; + PacketCount := 0; + Size := 0; + + SDL_UnlockMutex(Mutex); +end; + +end. diff --git a/Game/Code/Classes/UMediaCore_SDL.pas b/Game/Code/Classes/UMediaCore_SDL.pas new file mode 100644 index 00000000..252f72a0 --- /dev/null +++ b/Game/Code/Classes/UMediaCore_SDL.pas @@ -0,0 +1,38 @@ +unit UMediaCore_SDL; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + +uses + UMusic, + sdl; + +function ConvertAudioFormatToSDL(Format: TAudioSampleFormat; out SDLFormat: UInt16): boolean; + +implementation + +function ConvertAudioFormatToSDL(Format: TAudioSampleFormat; out SDLFormat: UInt16): boolean; +begin + case Format of + asfU8: SDLFormat := AUDIO_U8; + asfS8: SDLFormat := AUDIO_S8; + asfU16LSB: SDLFormat := AUDIO_U16LSB; + asfS16LSB: SDLFormat := AUDIO_S16LSB; + asfU16MSB: SDLFormat := AUDIO_U16MSB; + asfS16MSB: SDLFormat := AUDIO_S16MSB; + asfU16: SDLFormat := AUDIO_U16; + asfS16: SDLFormat := AUDIO_S16; + else begin + Result := false; + Exit; + end; + end; + Result := true; +end; + +end. diff --git a/Game/Code/Classes/UMedia_dummy.pas b/Game/Code/Classes/UMedia_dummy.pas index 62a94aef..438b89ab 100644 --- a/Game/Code/Classes/UMedia_dummy.pas +++ b/Game/Code/Classes/UMedia_dummy.pas @@ -1,14 +1,4 @@ unit UMedia_dummy; -{< ############################################################################# -# FFmpeg support for UltraStar deluxe # -# # -# Created by b1indy # -# based on 'An ffmpeg and SDL Tutorial' (http://www.dranger.com/ffmpeg/) # -# # -# http://www.mail-archive.com/fpc-pascal@lists.freepascal.org/msg09949.html # -# http://www.nabble.com/file/p11795857/mpegpas01.zip # -# # -############################################################################## } interface @@ -25,18 +15,16 @@ uses math, UMusic; -var - singleton_dummy : IVideoPlayback; - type TMedia_dummy = class( TInterfacedObject, IVideoPlayback, IVideoVisualization, IAudioPlayback, IAudioInput ) private DummyOutputDeviceList: TAudioOutputDeviceList; public - constructor create(); + constructor Create(); function GetName: string; - procedure init(); + function Init(): boolean; + function Finalize(): boolean; function Open(const aFileName : string): boolean; // true if succeed procedure Close; @@ -48,6 +36,8 @@ type procedure SetPosition(Time: real); function GetPosition: real; + procedure SetSyncSource(SyncSource: TSyncSource); + procedure GetFrame(Time: Extended); procedure DrawGL(Screen: integer); @@ -74,157 +64,180 @@ type function Length: real; function OpenSound(const Filename: string): TAudioPlaybackStream; + procedure CloseSound(var PlaybackStream: TAudioPlaybackStream); procedure PlaySound(stream: TAudioPlaybackStream); procedure StopSound(stream: TAudioPlaybackStream); + + function CreateVoiceStream(Channel: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream; + procedure CloseVoiceStream(var VoiceStream: TAudioVoiceStream); end; -function Tmedia_dummy.GetName: string; +function TMedia_dummy.GetName: string; begin - result := 'dummy'; + Result := 'dummy'; end; -procedure Tmedia_dummy.GetFrame(Time: Extended); +procedure TMedia_dummy.GetFrame(Time: Extended); begin end; -procedure Tmedia_dummy.DrawGL(Screen: integer); +procedure TMedia_dummy.DrawGL(Screen: integer); begin end; -constructor Tmedia_dummy.create(); +constructor TMedia_dummy.Create(); begin inherited; end; -procedure Tmedia_dummy.init(); +function TMedia_dummy.Init(): boolean; +begin + Result := true; +end; + +function TMedia_dummy.Finalize(): boolean; begin + Result := true; end; -function Tmedia_dummy.Open(const aFileName : string): boolean; // true if succeed +function TMedia_dummy.Open(const aFileName : string): boolean; // true if succeed begin - result := false; + Result := false; end; -procedure Tmedia_dummy.Close; +procedure TMedia_dummy.Close; begin end; -procedure Tmedia_dummy.Play; +procedure TMedia_dummy.Play; begin end; -procedure Tmedia_dummy.Pause; +procedure TMedia_dummy.Pause; begin end; -procedure Tmedia_dummy.Stop; +procedure TMedia_dummy.Stop; begin end; -procedure Tmedia_dummy.SetPosition(Time: real); +procedure TMedia_dummy.SetPosition(Time: real); begin end; -function Tmedia_dummy.GetPosition: real; +function TMedia_dummy.GetPosition: real; +begin + Result := 0; +end; + +procedure TMedia_dummy.SetSyncSource(SyncSource: TSyncSource); begin - result := 0; end; // IAudioInput -function Tmedia_dummy.InitializeRecord: boolean; +function TMedia_dummy.InitializeRecord: boolean; begin - result := true; + Result := true; end; -function Tmedia_dummy.FinalizeRecord: boolean; +function TMedia_dummy.FinalizeRecord: boolean; begin - result := true; + Result := true; end; -procedure Tmedia_dummy.CaptureStart; +procedure TMedia_dummy.CaptureStart; begin end; -procedure Tmedia_dummy.CaptureStop; +procedure TMedia_dummy.CaptureStop; begin end; -procedure Tmedia_dummy.GetFFTData(var data: TFFTData); +procedure TMedia_dummy.GetFFTData(var data: TFFTData); begin end; -function Tmedia_dummy.GetPCMData(var data: TPCMData): Cardinal; +function TMedia_dummy.GetPCMData(var data: TPCMData): Cardinal; begin - result := 0; + Result := 0; end; // IAudioPlayback -function Tmedia_dummy.InitializePlayback: boolean; +function TMedia_dummy.InitializePlayback: boolean; begin SetLength(DummyOutputDeviceList, 1); DummyOutputDeviceList[0] := TAudioOutputDevice.Create(); DummyOutputDeviceList[0].Name := '[Dummy Device]'; - result := true; + Result := true; end; -function Tmedia_dummy.FinalizePlayback: boolean; +function TMedia_dummy.FinalizePlayback: boolean; begin - result := true; + Result := true; end; -function Tmedia_dummy.GetOutputDeviceList(): TAudioOutputDeviceList; +function TMedia_dummy.GetOutputDeviceList(): TAudioOutputDeviceList; begin Result := DummyOutputDeviceList; end; -procedure Tmedia_dummy.SetAppVolume(Volume: single); +procedure TMedia_dummy.SetAppVolume(Volume: single); begin end; -procedure Tmedia_dummy.SetVolume(Volume: single); +procedure TMedia_dummy.SetVolume(Volume: single); begin end; -procedure Tmedia_dummy.SetLoop(Enabled: boolean); +procedure TMedia_dummy.SetLoop(Enabled: boolean); begin end; -procedure Tmedia_dummy.FadeIn(Time: real; TargetVolume: single); +procedure TMedia_dummy.FadeIn(Time: real; TargetVolume: single); begin end; -procedure Tmedia_dummy.Rewind; +procedure TMedia_dummy.Rewind; begin end; -function Tmedia_dummy.Finished: boolean; +function TMedia_dummy.Finished: boolean; begin - result := false; + Result := false; end; -function Tmedia_dummy.Length: real; +function TMedia_dummy.Length: real; begin Result := 60; end; -function Tmedia_dummy.OpenSound(const Filename: string): TAudioPlaybackStream; +function TMedia_dummy.OpenSound(const Filename: string): TAudioPlaybackStream; begin - result := nil; + Result := nil; end; -procedure Tmedia_dummy.PlaySound(stream: TAudioPlaybackStream); +procedure TMedia_dummy.CloseSound(var PlaybackStream: TAudioPlaybackStream); begin end; -procedure Tmedia_dummy.StopSound(stream: TAudioPlaybackStream); +procedure TMedia_dummy.PlaySound(stream: TAudioPlaybackStream); begin end; -initialization - singleton_dummy := Tmedia_dummy.create(); - AudioManager.add( singleton_dummy ); +procedure TMedia_dummy.StopSound(stream: TAudioPlaybackStream); +begin +end; + +function TMedia_dummy.CreateVoiceStream(Channel: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream; +begin + Result := nil; +end; -finalization - AudioManager.Remove( singleton_dummy ); +procedure TMedia_dummy.CloseVoiceStream(var VoiceStream: TAudioVoiceStream); +begin +end; + +initialization + MediaManager.Add(TMedia_dummy.Create); end. diff --git a/Game/Code/Classes/UMusic.pas b/Game/Code/Classes/UMusic.pas index 35c67ae1..38f7d53a 100644 --- a/Game/Code/Classes/UMusic.pas +++ b/Game/Code/Classes/UMusic.pas @@ -68,6 +68,9 @@ type TLineState = class private Timer: TRelativeTimer; // keeps track of the current time + + function GetCurrentTime(): real; + procedure SetCurrentTime(Time: real); public OldBeat: integer; // previous discovered beat CurrentBeat: integer; @@ -95,11 +98,11 @@ type constructor Create(); procedure Pause(); procedure Resume(); - function GetCurrentTime(): real; - procedure SetCurrentTime(Time: real); - // current song time used as base-timer for lyrics etc. - property CurrentTime: real READ GetCurrentTime WRITE SetCurrentTime; + (** + * current song time used as base-timer for lyrics etc. + *) + property CurrentTime: real read GetCurrentTime write SetCurrentTime; end; @@ -114,10 +117,10 @@ type TPCMData = array[0..511] of TPCMStereoSample; type - TStreamStatus = (ssStopped, ssPlaying, ssPaused, ssBlocked, ssUnknown); + TStreamStatus = (ssStopped, ssPlaying, ssPaused); const StreamStatusStr: array[TStreamStatus] of string = - ('Stopped', 'Playing', 'Paused', 'Blocked', 'Unknown'); + ('Stopped', 'Playing', 'Paused'); type TAudioSampleFormat = ( @@ -142,15 +145,39 @@ const 4 // asfFloat ); +const + CHANNELMAP_LEFT = 1; + CHANNELMAP_RIGHT = 2; + CHANNELMAP_FRONT = CHANNELMAP_LEFT or CHANNELMAP_RIGHT; + type TAudioFormatInfo = class + private + fSampleRate : double; + fChannels : byte; + fFormat : TAudioSampleFormat; + fFrameSize : integer; + + procedure SetChannels(Channels: byte); + procedure SetFormat(Format: TAudioSampleFormat); + procedure UpdateFrameSize(); + function GetBytesPerSec(): double; public - Channels : byte; - SampleRate : double; - Format : TAudioSampleFormat; - FrameSize : integer; // calculated on construction - constructor Create(Channels: byte; SampleRate: double; Format: TAudioSampleFormat); + function Copy(): TAudioFormatInfo; + + (** + * Returns the inverse ratio of the size of data in this format to its + * size in a given target format. + * Example: SrcSize*SrcInfo.GetRatio(TgtInfo) = TgtSize + *) + function GetRatio(TargetInfo: TAudioFormatInfo): double; + + property SampleRate: double read fSampleRate write fSampleRate; + property Channels: byte read fChannels write SetChannels; + property Format: TAudioSampleFormat read fFormat write SetFormat; + property FrameSize: integer read fFrameSize; + property BytesPerSec: double read GetBytesPerSec; end; type @@ -166,22 +193,89 @@ type end; type + TSyncSource = class + function GetClock(): real; virtual; abstract; + end; + + TAudioProcessingStream = class; + TOnCloseHandler = procedure(Stream: TAudioProcessingStream); + TAudioProcessingStream = class + protected + OnCloseHandlers: array of TOnCloseHandler; + + function GetLength(): real; virtual; abstract; + function GetPosition(): real; virtual; abstract; + procedure SetPosition(Time: real); virtual; abstract; + function GetLoop(): boolean; virtual; abstract; + procedure SetLoop(Enabled: boolean); virtual; abstract; + + procedure PerformOnClose(); + public + function GetAudioFormatInfo(): TAudioFormatInfo; virtual; abstract; + procedure Close(); virtual; abstract; + + (** + * Adds a new OnClose action handler. + * The handlers are performed in the order they were added. + * If not stated explicitely, member-variables might have been invalidated + * already. So do not use any member (variable/method/...) if you are not + * sure it is valid. + *) + procedure AddOnCloseHandler(Handler: TOnCloseHandler); + + property Length: real read GetLength; + property Position: real read GetPosition write SetPosition; + property Loop: boolean read GetLoop write SetLoop; + end; + + TAudioSourceStream = class(TAudioProcessingStream) + protected + function IsEOF(): boolean; virtual; abstract; + function IsError(): boolean; virtual; abstract; public - procedure Close(); virtual; abstract; + function ReadData(Buffer: PChar; BufferSize: integer): integer; virtual; abstract; + + property EOF: boolean read IsEOF; + property Error: boolean read IsError; end; + (* + * State-Chart for playback-stream state transitions + * []: Transition, (): State + * + * /---[Play/FadeIn]--->-\ /-------[Pause]----->-\ + * -[Create]->(Stop) (Play) (Pause) + * \\-<-[Stop/EOF*/Error]-/ \-<---[Play/FadeIn]--// + * \-<------------[Stop/EOF*/Error]--------------/ + * + * *: if not looped, otherwise stream is repeated + * Note: SetPosition() does not change the state. + *) + TAudioPlaybackStream = class(TAudioProcessingStream) protected - function GetPosition: real; virtual; abstract; - procedure SetPosition(Time: real); virtual; abstract; - function GetLength(): real; virtual; abstract; + SyncSource: TSyncSource; + AvgSyncDiff: double; + SourceStream: TAudioSourceStream; + + function GetLatency(): double; virtual; abstract; function GetStatus(): TStreamStatus; virtual; abstract; function GetVolume(): single; virtual; abstract; procedure SetVolume(Volume: single); virtual; abstract; - function GetLoop(): boolean; virtual; abstract; - procedure SetLoop(Enabled: boolean); virtual; abstract; - public + function Synchronize(BufferSize: integer; FormatInfo: TAudioFormatInfo): integer; + procedure FillBufferWithFrame(Buffer: PChar; BufferSize: integer; Frame: PChar; FrameSize: integer); + public + (** + * Opens a SourceStream for playback. + * Note that the caller (not the TAudioPlaybackStream) is responsible to + * free the SourceStream after the Playback-Stream is closed. + * You may use an OnClose-handler to achieve this. GetSourceStream() + * guarantees to deliver this method's SourceStream parameter to + * the OnClose-handler. Freeing SourceStream at OnClose is allowed. + *) + function Open(SourceStream: TAudioSourceStream): boolean; virtual; abstract; + procedure Play(); virtual; abstract; procedure Pause(); virtual; abstract; procedure Stop(); virtual; abstract; @@ -190,38 +284,40 @@ type procedure GetFFTData(var data: TFFTData); virtual; abstract; function GetPCMData(var data: TPCMData): Cardinal; virtual; abstract; - procedure AddSoundEffect(effect: TSoundEffect); virtual; abstract; - procedure RemoveSoundEffect(effect: TSoundEffect); virtual; abstract; + procedure AddSoundEffect(Effect: TSoundEffect); virtual; abstract; + procedure RemoveSoundEffect(Effect: TSoundEffect); virtual; abstract; + + procedure SetSyncSource(SyncSource: TSyncSource); + function GetSourceStream(): TAudioSourceStream; - property Length: real READ GetLength; - property Position: real READ GetPosition WRITE SetPosition; - property Status: TStreamStatus READ GetStatus; - property Volume: single READ GetVolume WRITE SetVolume; - property Loop: boolean READ GetLoop WRITE SetLoop; + property Status: TStreamStatus read GetStatus; + property Volume: single read GetVolume write SetVolume; end; - TAudioDecodeStream = class(TAudioProcessingStream) + TAudioDecodeStream = class(TAudioSourceStream) + end; + + TAudioVoiceStream = class(TAudioSourceStream) protected - function GetLength(): real; virtual; abstract; - function GetPosition(): real; virtual; abstract; - procedure SetPosition(Time: real); virtual; abstract; - function IsEOF(): boolean; virtual; abstract; - function IsError(): boolean; virtual; abstract; + FormatInfo: TAudioFormatInfo; + ChannelMap: integer; public - function ReadData(Buffer: PChar; BufSize: integer): integer; virtual; abstract; - function GetAudioFormatInfo(): TAudioFormatInfo; virtual; abstract; + destructor Destroy; override; + + function Open(ChannelMap: integer; FormatInfo: TAudioFormatInfo): boolean; virtual; + procedure Close(); override; + + procedure WriteData(Buffer: PChar; BufferSize: integer); virtual; abstract; + function GetAudioFormatInfo(): TAudioFormatInfo; override; - property Length: real READ GetLength; - property Position: real READ GetPosition WRITE SetPosition; - property EOF: boolean READ IsEOF; + function GetLength(): real; override; + function GetPosition(): real; override; + procedure SetPosition(Time: real); override; + function GetLoop(): boolean; override; + procedure SetLoop(Enabled: boolean); override; end; type - TAudioVoiceStream = class(TAudioProcessingStream) - public - function ReadData(Buffer: PChar; BufSize: integer): integer; virtual; abstract; - function GetAudioFormatInfo(): TAudioFormatInfo; virtual; abstract; - end; // soundcard output-devices information TAudioOutputDevice = class public @@ -234,7 +330,7 @@ type ['{63A5EBC3-3F4D-4F23-8DFB-B5165FCE33DD}'] function GetName: String; - function Open(const Filename: string): boolean; // true if succeed + function Open(const Filename: string): boolean; // true if succeed procedure Close; procedure Play; @@ -242,14 +338,15 @@ type procedure Stop; procedure SetPosition(Time: real); - function GetPosition: real; + function GetPosition: real; - property Position : real READ GetPosition WRITE SetPosition; + property Position: real read GetPosition write SetPosition; end; IVideoPlayback = Interface( IGenericPlayback ) ['{3574C40C-28AE-4201-B3D1-3D1F0759B131}'] - procedure init(); + function Init(): boolean; + function Finalize: boolean; procedure GetFrame(Time: Extended); // WANT TO RENAME THESE TO BE MORE GENERIC procedure DrawGL(Screen: integer); // WANT TO RENAME THESE TO BE MORE GENERIC @@ -266,25 +363,36 @@ type function FinalizePlayback: boolean; function GetOutputDeviceList(): TAudioOutputDeviceList; + procedure SetAppVolume(Volume: single); procedure SetVolume(Volume: single); procedure SetLoop(Enabled: boolean); + procedure FadeIn(Time: real; TargetVolume: single); + procedure SetSyncSource(SyncSource: TSyncSource); procedure Rewind; function Finished: boolean; function Length: real; // Sounds + // TODO: + // add a TMediaDummyPlaybackStream implementation that will + // be used by the TSoundLib whenever OpenSound() fails, so checking for + // nil-pointers is not neccessary anymore. + // PlaySound/StopSound will be removed then, OpenSound will be renamed to + // CreateSound. function OpenSound(const Filename: String): TAudioPlaybackStream; - procedure PlaySound(stream: TAudioPlaybackStream); - procedure StopSound(stream: TAudioPlaybackStream); + procedure PlaySound(Stream: TAudioPlaybackStream); + procedure StopSound(Stream: TAudioPlaybackStream); // Equalizer - procedure GetFFTData(var data: TFFTData); + procedure GetFFTData(var Data: TFFTData); // Interface for Visualizer - function GetPCMData(var data: TPCMData): Cardinal; + function GetPCMData(var Data: TPCMData): Cardinal; + + function CreateVoiceStream(ChannelMap: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream; end; IGenericDecoder = Interface @@ -317,109 +425,201 @@ type procedure CaptureStop; end; +type + TAudioConverter = class + protected + fSrcFormatInfo: TAudioFormatInfo; + fDstFormatInfo: TAudioFormatInfo; + public + function Init(SrcFormatInfo: TAudioFormatInfo; DstFormatInfo: TAudioFormatInfo): boolean; virtual; + destructor Destroy(); override; + + (** + * Converts the InputBuffer and stores the result in OutputBuffer. + * If the result is not -1, InputSize will be set to the actual number of + * input-buffer bytes used. + * Returns the number of bytes written to the output-buffer or -1 if an error occured. + *) + function Convert(InputBuffer: PChar; OutputBuffer: PChar; var InputSize: integer): integer; virtual; abstract; + + (** + * Destination/Source size ratio + *) + function GetRatio(): double; virtual; abstract; + + function GetOutputBufferSize(InputSize: integer): integer; virtual; abstract; + property SrcFormatInfo: TAudioFormatInfo read fSrcFormatInfo; + property DstFormatInfo: TAudioFormatInfo read fDstFormatInfo; + end; + +(* TODO +const + SOUNDID_START = 0; + SOUNDID_BACK = 1; + SOUNDID_SWOOSH = 2; + SOUNDID_CHANGE = 3; + SOUNDID_OPTION = 4; + SOUNDID_CLICK = 5; + LAST_SOUNDID = SOUNDID_CLICK; + + BaseSoundFilenames: array[0..LAST_SOUNDID] of string = ( + '%SOUNDPATH%/Common start.mp3', // Start + '%SOUNDPATH%/Common back.mp3', // Back + '%SOUNDPATH%/menu swoosh.mp3', // Swoosh + '%SOUNDPATH%/select music change music 50.mp3', // Change + '%SOUNDPATH%/option change col.mp3', // Option + '%SOUNDPATH%/rimshot022b.mp3' // Click + { + '%SOUNDPATH%/bassdrumhard076b.mp3', // Drum (unused) + '%SOUNDPATH%/hihatclosed068b.mp3', // Hihat (unused) + '%SOUNDPATH%/claps050b.mp3', // Clap (unused) + '%SOUNDPATH%/Shuffle.mp3' // Shuffle (unused) + } + ); +*) + type TSoundLibrary = class + private + // TODO + //Sounds: array of TAudioPlaybackStream; public + // TODO: move sounds to the private section + // and provide IDs instead. Start: TAudioPlaybackStream; Back: TAudioPlaybackStream; Swoosh: TAudioPlaybackStream; Change: TAudioPlaybackStream; Option: TAudioPlaybackStream; Click: TAudioPlaybackStream; - Drum: TAudioPlaybackStream; - Hihat: TAudioPlaybackStream; - Clap: TAudioPlaybackStream; - Shuffle: TAudioPlaybackStream; + BGMusic: TAudioPlaybackStream; constructor Create(); destructor Destroy(); override; procedure LoadSounds(); procedure UnloadSounds(); - end; -var // TODO : JB --- THESE SHOULD NOT BE GLOBAL - // czesci z nutami; - Lines: array of TLines; - - // LineState - LineState: TLineState; + // TODO + //function AddSound(Filename: string): integer; + //procedure RemoveSound(ID: integer); + //function GetSound(ID: integer): TAudioPlaybackStream; + //property Sound[ID: integer]: TAudioPlaybackStream read GetSound; default; + end; +var + // TODO: JB --- THESE SHOULD NOT BE GLOBAL + Lines: array of TLines; + LineState: TLineState; SoundLib: TSoundLibrary; procedure InitializeSound; -procedure FinalizeSound; +procedure InitializeVideo; +procedure FinalizeMedia; function Visualization(): IVideoPlayback; function VideoPlayback(): IVideoPlayback; function AudioPlayback(): IAudioPlayback; function AudioInput(): IAudioInput; -function AudioDecoder(): IAudioDecoder; +function AudioDecoders(): TInterfaceList; -function AudioManager: TInterfaceList; +function MediaManager: TInterfaceList; +procedure DumpMediaInterfaces(); implementation uses sysutils, + math, UMain, UCommandLine, URecord, ULog; var - singleton_VideoPlayback : IVideoPlayback = nil; - singleton_Visualization : IVideoPlayback = nil; - singleton_AudioPlayback : IAudioPlayback = nil; - singleton_AudioInput : IAudioInput = nil; - singleton_AudioDecoder : IAudioDecoder = nil; - - singleton_AudioManager : TInterfaceList = nil; + DefaultVideoPlayback : IVideoPlayback; + DefaultVisualization : IVideoPlayback; + DefaultAudioPlayback : IAudioPlayback; + DefaultAudioInput : IAudioInput; + AudioDecoderList : TInterfaceList; + MediaInterfaceList : TInterfaceList; constructor TAudioFormatInfo.Create(Channels: byte; SampleRate: double; Format: TAudioSampleFormat); begin inherited Create(); - Self.Channels := Channels; - Self.SampleRate := SampleRate; - Self.Format := Format; - Self.FrameSize := AudioSampleSize[Format] * Channels; + fChannels := Channels; + fSampleRate := SampleRate; + fFormat := Format; + UpdateFrameSize(); +end; + +procedure TAudioFormatInfo.SetChannels(Channels: byte); +begin + fChannels := Channels; + UpdateFrameSize(); +end; + +procedure TAudioFormatInfo.SetFormat(Format: TAudioSampleFormat); +begin + fFormat := Format; + UpdateFrameSize(); +end; + +function TAudioFormatInfo.GetBytesPerSec(): double; +begin + Result := FrameSize * SampleRate; +end; + +procedure TAudioFormatInfo.UpdateFrameSize(); +begin + fFrameSize := AudioSampleSize[fFormat] * fChannels; +end; + +function TAudioFormatInfo.Copy(): TAudioFormatInfo; +begin + Result := TAudioFormatInfo.Create(Self.Channels, Self.SampleRate, Self.Format); end; -function AudioManager: TInterfaceList; +function TAudioFormatInfo.GetRatio(TargetInfo: TAudioFormatInfo): double; begin - if singleton_AudioManager = nil then - singleton_AudioManager := TInterfaceList.Create(); - - Result := singleton_AudioManager; -end; //CompressionPluginManager + Result := (TargetInfo.FrameSize / Self.FrameSize) * + (TargetInfo.SampleRate / Self.SampleRate) +end; +function MediaManager: TInterfaceList; +begin + if (not assigned(MediaInterfaceList)) then + MediaInterfaceList := TInterfaceList.Create(); + Result := MediaInterfaceList; +end; + function VideoPlayback(): IVideoPlayback; begin - result := singleton_VideoPlayback; + Result := DefaultVideoPlayback; end; function Visualization(): IVideoPlayback; begin - result := singleton_Visualization; + Result := DefaultVisualization; end; function AudioPlayback(): IAudioPlayback; begin - result := singleton_AudioPlayback; + Result := DefaultAudioPlayback; end; function AudioInput(): IAudioInput; begin - result := singleton_AudioInput; + Result := DefaultAudioInput; end; -function AudioDecoder(): IAudioDecoder; +function AudioDecoders(): TInterfaceList; begin - result := singleton_AudioDecoder; + Result := AudioDecoderList; end; procedure FilterInterfaceList(const IID: TGUID; InList, OutList: TInterfaceList); @@ -442,129 +642,169 @@ begin end; end; -procedure AssignSingletonObjects(); +procedure InitializeSound; var - lTmpInterface : IInterface; - iCount : Integer; + i: integer; + InterfaceList: TInterfaceList; + CurrentAudioDecoder: IAudioDecoder; + CurrentAudioPlayback: IAudioPlayback; + CurrentAudioInput: IAudioInput; begin - lTmpInterface := nil; + // create a temporary list for interface enumeration + InterfaceList := TInterfaceList.Create(); - for iCount := 0 to AudioManager.Count - 1 do + // initialize all audio-decoders first + FilterInterfaceList(IAudioDecoder, MediaManager, InterfaceList); + for i := 0 to InterfaceList.Count-1 do begin - if assigned( AudioManager[iCount] ) then + CurrentAudioDecoder := IAudioDecoder(InterfaceList[i]); + if (not CurrentAudioDecoder.InitializeDecoder()) then begin - // if this interface is a Playback, then set it as the default used - - if ( AudioManager[iCount].QueryInterface( IAudioPlayback, lTmpInterface ) = 0 ) AND - ( true ) then //not assigned( singleton_AudioPlayback ) ) then - begin - singleton_AudioPlayback := IAudioPlayback( lTmpInterface ); - end; - - // if this interface is a Input, then set it as the default used - if ( AudioManager[iCount].QueryInterface( IAudioInput, lTmpInterface ) = 0 ) AND - ( true ) then //not assigned( singleton_AudioInput ) ) then - begin - singleton_AudioInput := IAudioInput( lTmpInterface ); - end; - - // if this interface is a Decoder, then set it as the default used - if ( AudioManager[iCount].QueryInterface( IAudioDecoder, lTmpInterface ) = 0 ) AND - ( true ) then //not assigned( singleton_AudioDecoder ) ) then - begin - singleton_AudioDecoder := IAudioDecoder( lTmpInterface ); - end; - - // if this interface is a Input, then set it as the default used - if ( AudioManager[iCount].QueryInterface( IVideoPlayback, lTmpInterface ) = 0 ) AND - ( true ) then //not assigned( singleton_VideoPlayback ) ) then - begin - singleton_VideoPlayback := IVideoPlayback( lTmpInterface ); - end; - - if ( AudioManager[iCount].QueryInterface( IVideoVisualization, lTmpInterface ) = 0 ) AND - ( true ) then //not assigned( singleton_Visualization ) ) then - begin - singleton_Visualization := IVideoPlayback( lTmpInterface ); - end; - + Log.LogError('Initialize failed, Removing - '+ CurrentAudioDecoder.GetName); + MediaManager.Remove(CurrentAudioDecoder); end; end; -end; -procedure InitializeSound; -begin - singleton_AudioPlayback := nil; - singleton_AudioInput := nil; - singleton_AudioDecoder := nil; - singleton_VideoPlayback := nil; - singleton_Visualization := nil; - - AssignSingletonObjects(); + // create and setup decoder-list (see AudioDecoders()) + AudioDecoderList := TInterfaceList.Create; + FilterInterfaceList(IAudioDecoder, MediaManager, AudioDecoders); - if VideoPlayback <> nil then + // find and initialize playback interface + DefaultAudioPlayback := nil; + FilterInterfaceList(IAudioPlayback, MediaManager, InterfaceList); + for i := 0 to InterfaceList.Count-1 do begin + CurrentAudioPlayback := IAudioPlayback(InterfaceList[i]); + if (CurrentAudioPlayback.InitializePlayback()) then + begin + DefaultAudioPlayback := CurrentAudioPlayback; + break; + end; + Log.LogError('Initialize failed, Removing - '+ CurrentAudioPlayback.GetName); + MediaManager.Remove(CurrentAudioPlayback); end; - if AudioDecoder <> nil then + // find and initialize input interface + DefaultAudioInput := nil; + FilterInterfaceList(IAudioInput, MediaManager, InterfaceList); + for i := 0 to InterfaceList.Count-1 do begin - while not AudioDecoder.InitializeDecoder do + CurrentAudioInput := IAudioInput(InterfaceList[i]); + if (CurrentAudioInput.InitializeRecord()) then begin - Log.LogError('Initialize failed, Removing - '+ AudioDecoder.GetName); - AudioManager.remove( AudioDecoder ); - singleton_AudioDecoder := nil; - AssignSingletonObjects(); + DefaultAudioInput := CurrentAudioInput; + break; end; + Log.LogError('Initialize failed, Removing - '+ CurrentAudioInput.GetName); + MediaManager.Remove(CurrentAudioInput); end; - if AudioPlayback <> nil then + InterfaceList.Free; + + // Update input-device list with registered devices + AudioInputProcessor.UpdateInputDeviceConfig(); + + // Load in-game sounds + SoundLib := TSoundLibrary.Create; + AudioPlayback.PlaySound(SoundLib.BGMusic); +end; + +procedure InitializeVideo(); +var + i: integer; + InterfaceList: TInterfaceList; + VideoInterface: IVideoPlayback; + VisualInterface: IVideoVisualization; +begin + InterfaceList := TInterfaceList.Create; + + // initialize and set video-playback singleton + DefaultVideoPlayback := nil; + FilterInterfaceList(IVideoPlayback, MediaManager, InterfaceList); + for i := 0 to InterfaceList.Count-1 do begin - while not AudioPlayback.InitializePlayback do + VideoInterface := IVideoPlayback(InterfaceList[i]); + if (VideoInterface.Init()) then begin - Log.LogError('Initialize failed, Removing - '+ AudioPlayback.GetName); - AudioManager.remove( AudioPlayback ); - singleton_AudioPlayback := nil; - AssignSingletonObjects(); + DefaultVideoPlayback := VideoInterface; + break; end; + Log.LogError('Initialize failed, Removing - '+ VideoInterface.GetName); + MediaManager.Remove(VideoInterface); end; - if AudioInput <> nil then + // initialize and set visualization singleton + DefaultVisualization := nil; + FilterInterfaceList(IVideoVisualization, MediaManager, InterfaceList); + for i := 0 to InterfaceList.Count-1 do begin - while not AudioInput.InitializeRecord do + VisualInterface := IVideoVisualization(InterfaceList[i]); + if (VisualInterface.Init()) then begin - Log.LogError('Initialize failed, Removing - '+ AudioInput.GetName); - AudioManager.remove( AudioInput ); - singleton_AudioInput := nil; - AssignSingletonObjects(); - end; + DefaultVisualization := VisualInterface; + break; + end; + Log.LogError('Initialize failed, Removing - '+ VisualInterface.GetName); + MediaManager.Remove(VisualInterface); end; - // Update input-device list with registered devices - AudioInputProcessor.UpdateInputDeviceConfig(); - // Load in-game sounds - SoundLib := TSoundLibrary.Create; + InterfaceList.Free; + // now that we have all interfaces, we can dump them + // TODO: move this to another place if FindCmdLineSwitch( cMediaInterfaces ) then begin - writeln( '' ); - writeln( '--------------------------------------------------------------' ); - writeln( ' In-use Media Interfaces ' ); - writeln( '--------------------------------------------------------------' ); - writeln( 'Registered Audio Playback Interface : ' + AudioPlayback.GetName ); - writeln( 'Registered Audio Input Interface : ' + AudioInput.GetName ); - writeln( 'Registered Video Playback Interface : ' + VideoPlayback.GetName ); - writeln( 'Registered Visualization Interface : ' + Visualization.GetName ); - writeln( '--------------------------------------------------------------' ); - writeln( '' ); - + DumpMediaInterfaces(); halt; end; end; -procedure FinalizeSound; +procedure UnloadMediaModules; var i: integer; - AudioIntfList: TInterfaceList; + InterfaceList: TInterfaceList; +begin + FreeAndNil(AudioDecoderList); + DefaultAudioPlayback := nil; + DefaultAudioInput := nil; + DefaultVideoPlayback := nil; + DefaultVisualization := nil; + + // create temporary interface list + InterfaceList := TInterfaceList.Create(); + + // finalize audio playback interfaces (should be done before the decoders) + FilterInterfaceList(IAudioPlayback, MediaManager, InterfaceList); + for i := 0 to InterfaceList.Count-1 do + IAudioPlayback(InterfaceList[i]).FinalizePlayback(); + + // finalize audio input interfaces + FilterInterfaceList(IAudioInput, MediaManager, InterfaceList); + for i := 0 to InterfaceList.Count-1 do + IAudioInput(InterfaceList[i]).FinalizeRecord(); + + // finalize audio decoder interfaces + FilterInterfaceList(IAudioDecoder, MediaManager, InterfaceList); + for i := 0 to InterfaceList.Count-1 do + IAudioDecoder(InterfaceList[i]).FinalizeDecoder(); + + // finalize video interfaces + FilterInterfaceList(IVideoPlayback, MediaManager, InterfaceList); + for i := 0 to InterfaceList.Count-1 do + IVideoPlayback(InterfaceList[i]).Finalize(); + + // finalize audio decoder interfaces + FilterInterfaceList(IVideoVisualization, MediaManager, InterfaceList); + for i := 0 to InterfaceList.Count-1 do + IVideoVisualization(InterfaceList[i]).Finalize(); + + InterfaceList.Free; + + // finally free interfaces (by removing all references to them) + FreeAndNil(MediaInterfaceList); +end; + +procedure FinalizeMedia; begin // stop, close and free sounds SoundLib.Free; @@ -577,35 +817,30 @@ begin if (AudioInput <> nil) then AudioInput.CaptureStop; - singleton_AudioPlayback := nil; - singleton_AudioDecoder := nil; - singleton_AudioInput := nil; - - // create temporary interface list - AudioIntfList := TInterfaceList.Create(); - - // finalize audio playback interfaces (should be done before the decoders) - FilterInterfaceList(IAudioPlayback, AudioManager, AudioIntfList); - for i := 0 to AudioIntfList.Count-1 do - IAudioPlayback(AudioIntfList[i]).FinalizePlayback(); + if (VideoPlayback <> nil) then + VideoPlayback.Close; - // finalize audio input interfaces - FilterInterfaceList(IAudioInput, AudioManager, AudioIntfList); - for i := 0 to AudioIntfList.Count-1 do - IAudioInput(AudioIntfList[i]).FinalizeRecord(); + if (Visualization <> nil) then + Visualization.Close; - // finalize audio decoder interfaces - FilterInterfaceList(IAudioDecoder, AudioManager, AudioIntfList); - for i := 0 to AudioIntfList.Count-1 do - IAudioDecoder(AudioIntfList[i]).FinalizeDecoder(); - - AudioIntfList.Free; + UnloadMediaModules(); +end; - // free audio interfaces - while (AudioManager.Count > 0) do - AudioManager.Delete(0); +procedure DumpMediaInterfaces(); +begin + writeln( '' ); + writeln( '--------------------------------------------------------------' ); + writeln( ' In-use Media Interfaces ' ); + writeln( '--------------------------------------------------------------' ); + writeln( 'Registered Audio Playback Interface : ' + AudioPlayback.GetName ); + writeln( 'Registered Audio Input Interface : ' + AudioInput.GetName ); + writeln( 'Registered Video Playback Interface : ' + VideoPlayback.GetName ); + writeln( 'Registered Visualization Interface : ' + Visualization.GetName ); + writeln( '--------------------------------------------------------------' ); + writeln( '' ); end; + { TSoundLibrary } constructor TSoundLibrary.Create(); @@ -634,11 +869,8 @@ begin Option := AudioPlayback.OpenSound(SoundPath + 'option change col.mp3'); Click := AudioPlayback.OpenSound(SoundPath + 'rimshot022b.mp3'); - //Drum := AudioPlayback.OpenSound(SoundPath + 'bassdrumhard076b.mp3'); - //Hihat := AudioPlayback.OpenSound(SoundPath + 'hihatclosed068b.mp3'); - //Clap := AudioPlayback.OpenSound(SoundPath + 'claps050b.mp3'); - - //Shuffle := AudioPlayback.OpenSound(SoundPath + 'Shuffle.mp3'); + //BGMusic := AudioPlayback.OpenSound(SoundPath + '18982__bebeto__Loop010_ambient.mp3'); + //BGMusic.SetLoop(true); //Log.BenchmarkEnd(4); //Log.LogBenchmark('--> Loading Sounds', 4); @@ -646,19 +878,26 @@ end; procedure TSoundLibrary.UnloadSounds(); begin - Start.Free; - Back.Free; - Swoosh.Free; - Change.Free; - Option.Free; - Click.Free; - - //Drum.Free; - //Hihat.Free; - //Clap.Free; + FreeAndNil(Start); + FreeAndNil(Back); + FreeAndNil(Swoosh); + FreeAndNil(Change); + FreeAndNil(Option); + FreeAndNil(Click); + FreeAndNil(BGMusic); +end; - //Shuffle.Free; +(* TODO +function TSoundLibrary.GetSound(ID: integer): TAudioPlaybackStream; +begin + if ((ID >= 0) and (ID < Length(Sounds))) then + Result := Sounds[ID] + else + Result := nil; end; +*) + +{ TVoiceRemoval } procedure TVoiceRemoval.Callback(Buffer: PChar; BufSize: integer); var @@ -687,7 +926,9 @@ end; constructor TLineState.Create(); begin - Timer := TRelativeTimer.Create(); + // create a triggered timer, so we can Pause() it, set the time + // and Resume() it afterwards for better synching. + Timer := TRelativeTimer.Create(true); end; procedure TLineState.Pause(); @@ -702,7 +943,9 @@ end; procedure TLineState.SetCurrentTime(Time: real); begin - Timer.SetTime(Time); + // do not start the timer (if not started already), + // after setting the current time + Timer.SetTime(Time, false); end; function TLineState.GetCurrentTime(): real; @@ -711,14 +954,206 @@ begin end; -initialization +{ TAudioConverter } + +function TAudioConverter.Init(SrcFormatInfo: TAudioFormatInfo; DstFormatInfo: TAudioFormatInfo): boolean; +begin + fSrcFormatInfo := SrcFormatInfo.Copy(); + fDstFormatInfo := DstFormatInfo.Copy(); + Result := true; +end; + +destructor TAudioConverter.Destroy(); +begin + FreeAndNil(fSrcFormatInfo); + FreeAndNil(fDstFormatInfo); +end; + + +{ TAudioProcessingStream } + +procedure TAudioProcessingStream.AddOnCloseHandler(Handler: TOnCloseHandler); +begin + if (@Handler <> nil) then + begin + SetLength(OnCloseHandlers, System.Length(OnCloseHandlers)+1); + OnCloseHandlers[High(OnCloseHandlers)] := @Handler; + end; +end; + +procedure TAudioProcessingStream.PerformOnClose(); +var i: integer; +begin + for i := 0 to High(OnCloseHandlers) do + begin + OnCloseHandlers[i](Self); + end; +end; + + +{ TAudioPlaybackStream } + +function TAudioPlaybackStream.GetSourceStream(): TAudioSourceStream; +begin + Result := SourceStream; +end; + +procedure TAudioPlaybackStream.SetSyncSource(SyncSource: TSyncSource); +begin + Self.SyncSource := SyncSource; + AvgSyncDiff := -1; +end; + +(* + * Results an adjusted size of the input buffer size to keep the stream in sync + * with the SyncSource. If no SyncSource was assigned to this stream, the + * input buffer size will be returned, so this method will have no effect. + * + * These are the possible cases: + * - Result > BufferSize: stream is behind the sync-source (stream is too slow), + * (Result-BufferSize) bytes of the buffer must be skipped. + * - Result = BufferSize: stream is in sync, + * there is nothing to do. + * - Result < BufferSize: stream is ahead of the sync-source (stream is too fast), + * (BufferSize-Result) bytes of the buffer must be padded. + *) +function TAudioPlaybackStream.Synchronize(BufferSize: integer; FormatInfo: TAudioFormatInfo): integer; +var + TimeDiff: double; + TimeCorrectionFactor: double; +const + AVG_HISTORY_FACTOR = 0.9; + SYNC_THRESHOLD = 0.045; + MAX_SYNC_DIFF_TIME = 0.002; +begin + Result := BufferSize; + + if (not assigned(SyncSource)) then + Exit; + + if (BufferSize <= 0) then + Exit; + + // difference between sync-source and stream position + // (negative if the music-stream's position is ahead of the master clock) + TimeDiff := SyncSource.GetClock() - (Position - GetLatency()); + + // calculate average time difference (some sort of weighted mean). + // The bigger AVG_HISTORY_FACTOR is, the smoother is the average diff. + // This means that older diffs are weighted more with a higher history factor + // than with a lower. Do not use a too low history factor. FFMpeg produces + // very instable timestamps (pts) for ogg due to some bugs. They may differ + // +-50ms from the real stream position. Without filtering those glitches we + // would synch without any need, resulting in ugly plopping sounds. + if (AvgSyncDiff = -1) then + AvgSyncDiff := TimeDiff + else + AvgSyncDiff := TimeDiff * (1-AVG_HISTORY_FACTOR) + + AvgSyncDiff * AVG_HISTORY_FACTOR; + + // check if sync needed + if (Abs(AvgSyncDiff) >= SYNC_THRESHOLD) then + begin + // TODO: use SetPosition if diff is too large (>5s) + if (TimeDiff < 1) then + TimeCorrectionFactor := Sign(TimeDiff)*TimeDiff*TimeDiff + else + TimeCorrectionFactor := TimeDiff; + + // calculate adapted buffer size + // reduce size of data to fetch if music is ahead, increase otherwise + Result := BufferSize + Round(TimeCorrectionFactor * FormatInfo.SampleRate) * FormatInfo.FrameSize; + if (Result < 0) then + Result := 0; + + // reset average + AvgSyncDiff := -1; + end; + + (* + DebugWriteln('Diff: ' + floattostrf(TimeDiff, ffFixed, 15, 3) + + '| SyS: ' + floattostrf(SyncSource.GetClock(), ffFixed, 15, 3) + + '| Pos: ' + floattostrf(Position, ffFixed, 15, 3) + + '| Avg: ' + floattostrf(AvgSyncDiff, ffFixed, 15, 3)); + *) +end; + +(* + * Fills a buffer with copies of the given frame or with 0 if frame. + *) +procedure TAudioPlaybackStream.FillBufferWithFrame(Buffer: PChar; BufferSize: integer; Frame: PChar; FrameSize: integer); +var + i: integer; + FrameCopyCount: integer; +begin + // the buffer must at least contain place for one copy of the frame. + if ((Buffer = nil) or (BufferSize <= 0) or (BufferSize < FrameSize)) then + Exit; + + // no valid frame -> fill with 0 + if ((Frame = nil) or (FrameSize <= 0)) then + begin + FillChar(Buffer[0], BufferSize, 0); + Exit; + end; + + // number of frames to copy + FrameCopyCount := BufferSize div FrameSize; + // insert as many copies of frame into the buffer as possible + for i := 0 to FrameCopyCount-1 do + Move(Frame[0], Buffer[i*FrameSize], FrameSize); +end; + +{ TAudioVoiceStream } + +function TAudioVoiceStream.Open(ChannelMap: integer; FormatInfo: TAudioFormatInfo): boolean; +begin + Self.ChannelMap := ChannelMap; + Self.FormatInfo := FormatInfo.Copy(); + // a voice stream is always mono, reassure the the format is correct + Self.FormatInfo.Channels := 1; + Result := true; +end; + +destructor TAudioVoiceStream.Destroy; +begin + Close(); + inherited; +end; + +procedure TAudioVoiceStream.Close(); +begin + PerformOnClose(); + FreeAndNil(FormatInfo); +end; + +function TAudioVoiceStream.GetAudioFormatInfo(): TAudioFormatInfo; +begin + Result := FormatInfo; +end; + +function TAudioVoiceStream.GetLength(): real; +begin + Result := -1; +end; + +function TAudioVoiceStream.GetPosition(): real; +begin + Result := -1; +end; + +procedure TAudioVoiceStream.SetPosition(Time: real); begin - singleton_AudioManager := TInterfaceList.Create(); +end; +function TAudioVoiceStream.GetLoop(): boolean; +begin + Result := false; +end; + +procedure TAudioVoiceStream.SetLoop(Enabled: boolean); +begin end; -finalization - singleton_AudioManager.clear; - FreeAndNil( singleton_AudioManager ); end. diff --git a/Game/Code/Classes/URecord.pas b/Game/Code/Classes/URecord.pas index 6d24e0f4..87aa6ea3 100644 --- a/Game/Code/Classes/URecord.pas +++ b/Game/Code/Classes/URecord.pas @@ -8,6 +8,8 @@ interface {$I switches.inc} +{.$DEFINE VOICE_PASSTHROUGH} + uses Classes, Math, SysUtils, @@ -22,7 +24,7 @@ const type TCaptureBuffer = class private - BufferNew: TMemoryStream; // buffer for newest samples + VoiceStream: TAudioVoiceStream; // stream for voice passthrough function GetToneString: string; // converts a tone to its string represenatation; public @@ -33,6 +35,7 @@ type AudioFormat: TAudioFormatInfo; // pitch detection + // TODO: remove ToneValid, set Tone/ToneAbs=-1 if invalid instead ToneValid: boolean; // true if Tone contains a valid value (otherwise it contains noise) Tone: integer; // tone relative to one octave (e.g. C2=C3=C4). Range: 0-11 ToneAbs: integer; // absolute (full range) tone (e.g. C2<>C3). Range: 0..NumHalftones-1 @@ -43,7 +46,9 @@ type procedure Clear; - procedure ProcessNewBuffer; + procedure BoostBuffer(Buffer: PChar; Size: Cardinal); + procedure ProcessNewBuffer(Buffer: PChar; BufferSize: integer); + // use to analyze sound from buffers to get new pitch procedure AnalyzeBuffer; // we call it to analyze sound by checking Autocorrelation @@ -95,11 +100,12 @@ type DeviceList: array of TAudioInputDevice; constructor Create; + destructor Destroy; override; procedure UpdateInputDeviceConfig; // handle microphone input - procedure HandleMicrophoneData(Buffer: Pointer; Size: Cardinal; + procedure HandleMicrophoneData(Buffer: PChar; Size: Cardinal; InputDevice: TAudioInputDevice); end; @@ -118,8 +124,8 @@ type end; - SmallIntArray = array [0..maxInt shr 1-1] of smallInt; - PSmallIntArray = ^SmallIntArray; + TSmallIntArray = array [0..(MaxInt div SizeOf(SmallInt))-1] of SmallInt; + PSmallIntArray = ^TSmallIntArray; function AudioInputProcessor(): TAudioInputProcessor; @@ -133,9 +139,9 @@ var singleton_AudioInputProcessor : TAudioInputProcessor = nil; -// FIXME: Race-Conditions between Callback-thread and main-thread -// on BufferArray (maybe BufferNew also). -// Use SDL-mutexes to solve this problem. +// FIXME: +// Race-Conditions between Callback-thread and main-thread on BufferArray. +// Use mutexes to solve this problem. { Global } @@ -161,20 +167,41 @@ begin end; procedure TAudioInputDevice.LinkCaptureBuffer(ChannelIndex: integer; Sound: TCaptureBuffer); +var + DeviceCfg: PInputDeviceConfig; + OldSound: TCaptureBuffer; begin // check bounds if ((ChannelIndex < 0) or (ChannelIndex > High(CaptureChannel))) then Exit; - // reset audio-format of old capture-buffer - if (CaptureChannel[ChannelIndex] <> nil) then - CaptureChannel[ChannelIndex].AudioFormat := nil; + // reset previously assigned (old) capture-buffer + OldSound := CaptureChannel[ChannelIndex]; + if (OldSound <> nil) then + begin + // close voice stream + FreeAndNil(OldSound.VoiceStream); + // free old audio-format info + FreeAndNil(OldSound.AudioFormat); + end; // set audio-format of new capture-buffer if (Sound <> nil) then - Sound.AudioFormat := AudioFormat; + begin + // copy the input-device audio-format ... + Sound.AudioFormat := AudioFormat.Copy; + // and adjust it because capture buffers are always mono + Sound.AudioFormat.Channels := 1; + DeviceCfg := @Ini.InputDeviceConfig[CfgIndex]; +// TODO: make this an ini-var, e.g. VoicePassthrough, VoiceRepeat or LiveVoice +{$IFDEF VOICE_PASSTHROUGH} + // create a voice-stream for passthrough + // TODO: map odd players to the left and even players to the right speaker + Sound.VoiceStream := AudioPlayback.CreateVoiceStream(CHANNELMAP_FRONT, AudioFormat); +{$ENDIF} + end; - // replace old with new buffer + // replace old with new buffer (Note: Sound might be nil) CaptureChannel[ChannelIndex] := Sound; end; @@ -183,65 +210,72 @@ end; constructor TCaptureBuffer.Create; begin inherited; - BufferNew := TMemoryStream.Create; BufferLong := TMemoryStream.Create; AnalysisBufferSize := Min(4*1024, Length(BufferArray)); end; destructor TCaptureBuffer.Destroy; begin - AudioFormat := nil; - FreeAndNil(BufferNew); FreeAndNil(BufferLong); + FreeAndNil(VoiceStream); + FreeAndNil(AudioFormat); inherited; end; procedure TCaptureBuffer.Clear; begin - if assigned(BufferNew) then - BufferNew.Clear; if assigned(BufferLong) then BufferLong.Clear; FillChar(BufferArray[0], Length(BufferArray) * SizeOf(SmallInt), 0); end; -procedure TCaptureBuffer.ProcessNewBuffer; +procedure TCaptureBuffer.ProcessNewBuffer(Buffer: PChar; BufferSize: integer); var - SkipCount: integer; - NumSamples: integer; - SampleIndex: integer; + BufferOffset: integer; + SampleCount: integer; + i: integer; begin + // apply software boost + //BoostBuffer(Buffer, Size); + + // voice passthrough (send data to playback-device) + if (assigned(VoiceStream)) then + VoiceStream.WriteData(Buffer, BufferSize); + + // we assume that samples are in S16Int format + // TODO: support float too + if (AudioFormat.Format <> asfS16) then + Exit; + // process BufferArray - SkipCount := 0; - NumSamples := BufferNew.Size div 2; + BufferOffset := 0; + + SampleCount := BufferSize div SizeOf(SmallInt); // check if we have more new samples than we can store - if (NumSamples > Length(BufferArray)) then + if (SampleCount > Length(BufferArray)) then begin // discard the oldest of the new samples - SkipCount := NumSamples - Length(BufferArray); - NumSamples := Length(BufferArray); + BufferOffset := (SampleCount - Length(BufferArray)) * SizeOf(SmallInt); + SampleCount := Length(BufferArray); end; // move old samples to the beginning of the array (if necessary) - // TODO: should be a ring-buffer instead - for SampleIndex := NumSamples to High(BufferArray) do - BufferArray[SampleIndex-NumSamples] := BufferArray[SampleIndex]; + for i := 0 to High(BufferArray)-SampleCount do + BufferArray[i] := BufferArray[i+SampleCount]; - // skip samples if necessary - BufferNew.Seek(2*SkipCount, soBeginning); - // copy samples - BufferNew.ReadBuffer(BufferArray[Length(BufferArray)-NumSamples], 2*NumSamples); + // copy samples to analysis buffer + Move(Buffer[BufferOffset], BufferArray[Length(BufferArray)-SampleCount], + SampleCount * SizeOf(SmallInt)); - // save capture-data to BufferLong if neccessary + // save capture-data to BufferLong if enabled if (Ini.SavePlayback = 1) then begin // this is just for debugging (approx 15MB per player for a 3min song!!!) // For an in-game replay-mode we need to compress data so we do not // waste that much memory. Maybe ogg-vorbis with voice-preset in fast-mode? // Or we could use a faster but not that efficient lossless compression. - BufferNew.Seek(0, soBeginning); - BufferLong.CopyFrom(BufferNew, BufferNew.Size); + BufferLong.WriteBuffer(Buffer, BufferSize); end; end; @@ -371,19 +405,71 @@ begin Result := '-'; end; +procedure TCaptureBuffer.BoostBuffer(Buffer: PChar; Size: Cardinal); +var + i: integer; + Value: Longint; + SampleCount: integer; + SampleBuffer: PSmallIntArray; // buffer handled as array of samples + Boost: byte; +begin + // TODO: set boost per device + { + case Ini.MicBoost of + 0: Boost := 1; + 1: Boost := 2; + 2: Boost := 4; + 3: Boost := 8; + else Boost := 1; + end; + } + Boost := 1; + + // at the moment we will boost SInt16 data only + if (AudioFormat.Format = asfS16) then + begin + // interpret buffer as buffer of bytes + SampleBuffer := PSmallIntArray(Buffer); + SampleCount := Size div AudioFormat.FrameSize; + + // boost buffer + for i := 0 to SampleCount-1 do + begin + Value := SampleBuffer^[i] * Boost; + + // TODO : JB - This will clip the audio... cant we reduce the "Boost" if the data clips ?? + if Value > High(Smallint) then + Value := High(Smallint); + + if Value < Low(Smallint) then + Value := Low(Smallint); + + SampleBuffer^[i] := Value; + end; + end; +end; + { TAudioInputProcessor } constructor TAudioInputProcessor.Create; var - i: integer; + i: integer; begin inherited; SetLength(Sound, 6 {max players});//Ini.Players+1); for i := 0 to High(Sound) do - begin Sound[i] := TCaptureBuffer.Create; - end; +end; + +destructor TAudioInputProcessor.Destroy; +var + i: integer; +begin + for i := 0 to High(Sound) do + Sound[i].Free; + SetLength(Sound, 0); + inherited; end; // updates InputDeviceConfig with current input-device information @@ -459,7 +545,7 @@ begin end; {* - * Handle captured microphone input data. + * Handles captured microphone input data. * Params: * Buffer - buffer of signed 16bit interleaved stereo PCM-samples. * Interleaved means that a right-channel sample follows a left- @@ -467,86 +553,48 @@ end; * Length - number of bytes in Buffer * Input - Soundcard-Input used for capture *} -procedure TAudioInputProcessor.HandleMicrophoneData(Buffer: Pointer; Size: Cardinal; InputDevice: TAudioInputDevice); +procedure TAudioInputProcessor.HandleMicrophoneData(Buffer: PChar; Size: Cardinal; InputDevice: TAudioInputDevice); var - Value: integer; - ChannelBuffer: PChar; // buffer handled as array of bytes (offset relative to channel) - SampleBuffer: PSmallIntArray; // buffer handled as array of samples - Boost: byte; + MultiChannelBuffer: PChar; // buffer handled as array of bytes (offset relative to channel) + SingleChannelBuffer: PChar; // temporary buffer for new samples per channel + SingleChannelBufferSize: integer; ChannelIndex: integer; CaptureChannel: TCaptureBuffer; AudioFormat: TAudioFormatInfo; - FrameSize: integer; - NumSamples: integer; - NumFrames: integer; // number of frames (stereo: 2xsamples) + SampleSize: integer; + SampleCount: integer; + SamplesPerChannel: integer; i: integer; begin - // set boost - case Ini.MicBoost of - 0: Boost := 1; - 1: Boost := 2; - 2: Boost := 4; - 3: Boost := 8; - else Boost := 1; - end; - AudioFormat := InputDevice.AudioFormat; + SampleSize := AudioSampleSize[AudioFormat.Format]; + SampleCount := Size div SampleSize; + SamplesPerChannel := Size div AudioFormat.FrameSize; - // FIXME: At the moment we assume a SInt16 format - // TODO: use SDL_AudioConvert to convert to SInt16 but do NOT change the - // samplerate (SDL does not convert 44.1kHz to 48kHz so we might get wrong - // results in the analysis phase otherwise) - if (AudioFormat.Format <> asfS16) then - begin - // this only occurs if a developer choosed an unsupported input sample-format - Log.CriticalError('TAudioInputProcessor.HandleMicrophoneData: Wrong sample-format'); - Exit; - end; - - // interpret buffer as buffer of bytes - SampleBuffer := Buffer; - - NumSamples := Size div SizeOf(Smallint); - - // boost buffer - // TODO: remove this senseless stuff - adjust the threshold instead - for i := 0 to NumSamples-1 do - begin - Value := SampleBuffer^[i] * Boost; - - // TODO : JB - This will clip the audio... cant we reduce the "Boost" if the data clips ?? - if Value > High(Smallint) then - Value := High(Smallint); - - if Value < Low(Smallint) then - Value := Low(Smallint); - - SampleBuffer^[i] := Value; - end; - - // samples per channel - FrameSize := AudioFormat.Channels * SizeOf(SmallInt); - NumFrames := Size div FrameSize; + SingleChannelBufferSize := SamplesPerChannel * SampleSize; + GetMem(SingleChannelBuffer, SingleChannelBufferSize); // process channels for ChannelIndex := 0 to High(InputDevice.CaptureChannel) do begin CaptureChannel := InputDevice.CaptureChannel[ChannelIndex]; + // check if a capture buffer was assigned, otherwise there is nothing to do if (CaptureChannel <> nil) then begin // set offset according to channel index - ChannelBuffer := @PChar(Buffer)[ChannelIndex * SizeOf(SmallInt)]; - - // TODO: remove BufferNew and write to BufferArray directly - - CaptureChannel.BufferNew.Clear; - for i := 0 to NumFrames-1 do + MultiChannelBuffer := @Buffer[ChannelIndex * SampleSize]; + // seperate channel-data from interleaved multi-channel (e.g. stereo) data + for i := 0 to SamplesPerChannel-1 do begin - CaptureChannel.BufferNew.Write(ChannelBuffer[i*FrameSize], SizeOf(SmallInt)); + Move(MultiChannelBuffer[i*AudioFormat.FrameSize], + SingleChannelBuffer[i*SampleSize], + SampleSize); end; - CaptureChannel.ProcessNewBuffer(); + CaptureChannel.ProcessNewBuffer(SingleChannelBuffer, SingleChannelBufferSize); end; end; + + FreeMem(SingleChannelBuffer); end; @@ -559,6 +607,7 @@ begin for i := 0 to High(AudioInputProcessor.DeviceList) do AudioInputProcessor.DeviceList[i].Free(); AudioInputProcessor.DeviceList := nil; + Result := true; end; {* @@ -625,14 +674,22 @@ end; procedure TAudioInputBase.CaptureStop; var DeviceIndex: integer; + ChannelIndex: integer; Device: TAudioInputDevice; + DeviceCfg: PInputDeviceConfig; begin for DeviceIndex := 0 to High(AudioInputProcessor.DeviceList) do begin Device := AudioInputProcessor.DeviceList[DeviceIndex]; if not assigned(Device) then continue; + Device.Stop(); + + // disconnect capture buffers + DeviceCfg := @Ini.InputDeviceConfig[Device.CfgIndex]; + for ChannelIndex := 0 to High(DeviceCfg.ChannelToPlayerMap) do + Device.LinkCaptureBuffer(ChannelIndex, nil); end; Started := false; diff --git a/Game/Code/Classes/URingBuffer.pas b/Game/Code/Classes/URingBuffer.pas new file mode 100644 index 00000000..ce51e209 --- /dev/null +++ b/Game/Code/Classes/URingBuffer.pas @@ -0,0 +1,128 @@ +unit URingBuffer; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + +uses + SysUtils; + +type + TRingBuffer = class + private + RingBuffer: PChar; + BufferCount: integer; + BufferSize: integer; + WritePos: integer; + ReadPos: integer; + public + constructor Create(Size: integer); + destructor Destroy; override; + function Read(Buffer: PChar; Count: integer): integer; + function Write(Buffer: PChar; Count: integer): integer; + procedure Flush(); + end; + +implementation + +uses + Math; + +constructor TRingBuffer.Create(Size: integer); +begin + BufferSize := Size; + + GetMem(RingBuffer, Size); + if (RingBuffer = nil) then + raise Exception.Create('No memory'); +end; + +destructor TRingBuffer.Destroy; +begin + FreeMem(RingBuffer); +end; + +function TRingBuffer.Read(Buffer: PChar; Count: integer): integer; +var + PartCount: integer; +begin + // adjust output count + if (Count > BufferCount) then + begin + //DebugWriteln('Read too much: ' + inttostr(count) +',count:'+ inttostr(BufferCount) + '/size:' + inttostr(BufferSize)); + Count := BufferCount; + end; + + // check if there is something to do + if (Count <= 0) then + begin + Result := Count; + Exit; + end; + + // copy data to output buffer + + // first step: copy from the area between the read-position and the end of the buffer + PartCount := Min(Count, BufferSize - ReadPos); + Move(RingBuffer[ReadPos], Buffer[0], PartCount); + + // second step: if we need more data, copy from the beginning of the buffer + if (PartCount < Count) then + Move(RingBuffer[0], Buffer[0], Count-PartCount); + + // mark the copied part of the buffer as free + BufferCount := BufferCount - Count; + ReadPos := (ReadPos + Count) mod BufferSize; + + Result := Count; +end; + +function TRingBuffer.Write(Buffer: PChar; Count: integer): integer; +var + PartCount: integer; +begin + // check for a reasonable request + if (Count <= 0) then + begin + Result := Count; + Exit; + end; + + // skip input data if the input buffer is bigger than the ring-buffer + if (Count > BufferSize) then + begin + //DebugWriteln('Write skip data:' + inttostr(count) +',count:'+ inttostr(BufferCount) + '/size:' + inttostr(BufferSize)); + Buffer := @Buffer[Count - BufferSize]; + Count := BufferSize; + end; + + // first step: copy to the area between the write-position and the end of the buffer + PartCount := Min(Count, BufferSize - WritePos); + Move(Buffer[0], RingBuffer[WritePos], PartCount); + + // second step: copy data to front of buffer + if (PartCount < Count) then + Move(Buffer[PartCount], RingBuffer[0], Count-PartCount); + + // update info + BufferCount := Min(BufferCount + Count, BufferSize); + WritePos := (WritePos + Count) mod BufferSize; + // if the buffer is full, we have to reposition the read-position + if (BufferCount = BufferSize) then + ReadPos := WritePos; + + Result := Count; +end; + +procedure TRingBuffer.Flush(); +begin + ReadPos := 0; + WritePos := 0; + BufferCount := 0; +end; + +end. \ No newline at end of file diff --git a/Game/Code/Classes/UTime.pas b/Game/Code/Classes/UTime.pas index 2acca35c..f8ae91c4 100644 --- a/Game/Code/Classes/UTime.pas +++ b/Game/Code/Classes/UTime.pas @@ -20,12 +20,15 @@ type AbsoluteTime: int64; // system-clock reference time for calculation of CurrentTime RelativeTimeOffset: real; Paused: boolean; + TriggerMode: boolean; public - constructor Create; + constructor Create(TriggerMode: boolean = false); procedure Pause(); procedure Resume(); function GetTime(): real; - procedure SetTime(Time: real); + function GetAndResetTime(): real; + procedure SetTime(Time: real; Trigger: boolean = true); + procedure Reset(); end; procedure CountSkipTimeSet; @@ -98,11 +101,18 @@ end; * TRelativeTimer **} -constructor TRelativeTimer.Create; +(* + * Creates a new timer. + * If TriggerMode is false (default), the timer + * will immediately begin with counting. + * If TriggerMode is true, it will wait until Get/SetTime() or Pause() is called + * for the first time. + *) +constructor TRelativeTimer.Create(TriggerMode: boolean); begin - inherited; - RelativeTimeOffset := 0; - AbsoluteTime := SDL_GetTicks(); + inherited Create(); + Self.TriggerMode := TriggerMode; + Reset(); Paused := false; end; @@ -118,19 +128,58 @@ begin Paused := false; end; +(* + * Returns the counter of the timer. + * If in TriggerMode it will return 0 and start the counter on the first call. + *) function TRelativeTimer.GetTime: real; begin + // initialize absolute time on first call in triggered mode + if (TriggerMode and (AbsoluteTime = 0)) then + begin + AbsoluteTime := SDL_GetTicks(); + Result := RelativeTimeOffset; + Exit; + end; + if Paused then Result := RelativeTimeOffset else Result := RelativeTimeOffset + (SDL_GetTicks() - AbsoluteTime) / cSDLCorrectionRatio; end; -procedure TRelativeTimer.SetTime(Time: real); +(* + * Returns the counter of the timer and resets the counter to 0 afterwards. + * Note: In TriggerMode the counter will not be stopped as with Reset(). + *) +function TRelativeTimer.GetAndResetTime(): real; +begin + Result := GetTime(); + SetTime(0); +end; + +(* + * Sets the timer to the given time. This will trigger in TriggerMode if + * Trigger is set to true. Otherwise the counter's state will not change. + *) +procedure TRelativeTimer.SetTime(Time: real; Trigger: boolean); begin RelativeTimeOffset := Time; - AbsoluteTime := SDL_GetTicks(); + if ((not TriggerMode) or Trigger) then + AbsoluteTime := SDL_GetTicks(); end; +(* + * Resets the counter of the timer to 0. + * If in TriggerMode the timer will not start counting until it is triggered again. + *) +procedure TRelativeTimer.Reset(); +begin + RelativeTimeOffset := 0; + if (TriggerMode) then + AbsoluteTime := 0 + else + AbsoluteTime := SDL_GetTicks(); +end; end. diff --git a/Game/Code/Classes/UVideo.pas b/Game/Code/Classes/UVideo.pas index bef77728..9b55aa35 100644 --- a/Game/Code/Classes/UVideo.pas +++ b/Game/Code/Classes/UVideo.pas @@ -12,10 +12,11 @@ unit UVideo; -//{$define DebugDisplay} // uncomment if u want to see the debug stuff -//{$define DebugFrames} -//{$define VideoBenchmark} -//{$define Info} +// uncomment if you want to see the debug stuff +{.$define DebugDisplay} +{.$define DebugFrames} +{.$define VideoBenchmark} +{.$define Info} interface @@ -25,12 +26,10 @@ interface {$I switches.inc} -(* - TODO: look into av_read_play -*) - -// use BGR-format for accelerated colorspace conversion with swscale -{.$DEFINE PIXEL_FMT_BGR} +// use BGR-format for accelerated colorspace conversion with swscale +{$IFDEF UseSWScale} + {$DEFINE PIXEL_FMT_BGR} +{$ENDIF} implementation @@ -45,6 +44,7 @@ uses {$IFDEF UseSWScale} swscale, {$ENDIF} + UMediaCore_FFMpeg, math, gl, glext, @@ -66,7 +66,7 @@ const {$ENDIF} type - TVideoPlayback_ffmpeg = class( TInterfacedObject, IVideoPlayback ) + TVideoPlayback_FFMpeg = class( TInterfacedObject, IVideoPlayback ) private fVideoOpened, fVideoPaused: Boolean; @@ -95,14 +95,16 @@ type EOF: boolean; Loop: boolean; + Initialized: boolean; + procedure Reset(); function DecodeFrame(var AVPacket: TAVPacket; out pts: double): boolean; - function FindStreamIDs( const aFormatCtx : PAVFormatContext; out aFirstVideoStream, aFirstAudioStream : integer ): boolean; - procedure SynchronizeVideo(pFrame: PAVFrame; var pts: double); + procedure SynchronizeVideo(Frame: PAVFrame; var pts: double); public - constructor Create(); function GetName: String; - procedure Init(); + + function Init(): boolean; + function Finalize: boolean; function Open(const aFileName : string): boolean; // true if succeed procedure Close; @@ -116,53 +118,36 @@ type procedure GetFrame(Time: Extended); procedure DrawGL(Screen: integer); - end; var - singleton_VideoFFMpeg : IVideoPlayback; - - - -function FFMpegErrorString(Errnum: integer): string; -begin - case Errnum of - AVERROR_IO: Result := 'AVERROR_IO'; - AVERROR_NUMEXPECTED: Result := 'AVERROR_NUMEXPECTED'; - AVERROR_INVALIDDATA: Result := 'AVERROR_INVALIDDATA'; - AVERROR_NOMEM: Result := 'AVERROR_NOMEM'; - AVERROR_NOFMT: Result := 'AVERROR_NOFMT'; - AVERROR_NOTSUPP: Result := 'AVERROR_NOTSUPP'; - AVERROR_NOENT: Result := 'AVERROR_NOENT'; - AVERROR_PATCHWELCOME: Result := 'AVERROR_PATCHWELCOME'; - else Result := 'AVERROR_#'+inttostr(Errnum); - end; -end; + FFMpegCore: TMediaCore_FFMpeg; + // These are called whenever we allocate a frame buffer. // We use this to store the global_pts in a frame at the time it is allocated. -function PtsGetBuffer(pCodecCtx: PAVCodecContext; pFrame: PAVFrame): integer; cdecl; +function PtsGetBuffer(CodecCtx: PAVCodecContext; Frame: PAVFrame): integer; cdecl; var pts: Pint64; VideoPktPts: Pint64; begin - Result := avcodec_default_get_buffer(pCodecCtx, pFrame); - VideoPktPts := pCodecCtx^.opaque; + Result := avcodec_default_get_buffer(CodecCtx, Frame); + VideoPktPts := CodecCtx^.opaque; if (VideoPktPts <> nil) then begin // Note: we must copy the pts instead of passing a pointer, because the packet // (and with it the pts) might change before a frame is returned by av_decode_video. pts := av_malloc(sizeof(int64)); pts^ := VideoPktPts^; - pFrame^.opaque := pts; + Frame^.opaque := pts; end; end; -procedure PtsReleaseBuffer(pCodecCtx: PAVCodecContext; pFrame: PAVFrame); cdecl; +procedure PtsReleaseBuffer(CodecCtx: PAVCodecContext; Frame: PAVFrame); cdecl; begin - if (pFrame <> nil) then - av_freep(@pFrame^.opaque); - avcodec_default_release_buffer(pCodecCtx, pFrame); + if (Frame <> nil) then + av_freep(@Frame^.opaque); + avcodec_default_release_buffer(CodecCtx, Frame); end; @@ -170,53 +155,258 @@ end; * TVideoPlayback_ffmpeg *------------------------------------------------------------------------------} -function TVideoPlayback_ffmpeg.GetName: String; +function TVideoPlayback_FFMpeg.GetName: String; begin result := 'FFMpeg_Video'; end; -{ - @param(aFormatCtx is a PAVFormatContext returned from av_open_input_file ) - @param(aFirstVideoStream is an OUT value of type integer, this is the index of the video stream) - @param(aFirstAudioStream is an OUT value of type integer, this is the index of the audio stream) - @returns(@true on success, @false otherwise) -} -function TVideoPlayback_ffmpeg.FindStreamIDs(const aFormatCtx: PAVFormatContext; out aFirstVideoStream, aFirstAudioStream: integer): boolean; +function TVideoPlayback_FFMpeg.Init(): boolean; +begin + Result := true; + + if (Initialized) then + Exit; + Initialized := true; + + FFMpegCore := TMediaCore_FFMpeg.GetInstance(); + + Reset(); + av_register_all(); + glGenTextures(1, PGLuint(@fVideoTex)); +end; + +function TVideoPlayback_FFMpeg.Finalize(): boolean; +begin + Close(); + glDeleteTextures(1, PGLuint(@fVideoTex)); + Result := true; +end; + +procedure TVideoPlayback_FFMpeg.Reset(); +begin + // close previously opened video + Close(); + + fVideoOpened := False; + fVideoPaused := False; + VideoTimeBase := 0; + VideoTime := 0; + VideoStream := nil; + VideoStreamIndex := -1; + + EOF := false; + + // TODO: do we really want this by default? + Loop := true; + fLoopTime := 0; +end; + +function TVideoPlayback_FFMpeg.Open(const aFileName : string): boolean; // true if succeed var - i : integer; - st : PAVStream; + errnum: Integer; + AudioStreamIndex: integer; begin - // Find the first video stream - aFirstAudioStream := -1; - aFirstVideoStream := -1; + Result := false; - {$IFDEF DebugDisplay} - debugwriteln('aFormatCtx.nb_streams : ' + inttostr(aFormatCtx.nb_streams)); - {$ENDIF} + Reset(); - for i := 0 to aFormatCtx.nb_streams-1 do + errnum := av_open_input_file(VideoFormatContext, PChar(aFileName), nil, 0, nil); + if (errnum <> 0) then begin - st := aFormatCtx.streams[i]; + Log.LogError('Failed to open file "'+aFileName+'" ('+FFMpegCore.GetErrorString(errnum)+')'); + Exit; + end; - if (st.codec.codec_type = CODEC_TYPE_VIDEO) and - (aFirstVideoStream < 0) then - begin - aFirstVideoStream := i; - end; + // update video info + if (av_find_stream_info(VideoFormatContext) < 0) then + begin + Log.LogError('No stream info found', 'TVideoPlayback_ffmpeg.Open'); + Close(); + Exit; + end; + Log.LogInfo('VideoStreamIndex : ' + inttostr(VideoStreamIndex), 'TVideoPlayback_ffmpeg.Open'); - if (st.codec.codec_type = CODEC_TYPE_AUDIO) and - (aFirstAudioStream < 0) then - begin - aFirstAudioStream := i; + // find video stream + FFMpegCore.FindStreamIDs(VideoFormatContext, VideoStreamIndex, AudioStreamIndex); + if (VideoStreamIndex < 0) then + begin + Log.LogError('No video stream found', 'TVideoPlayback_ffmpeg.Open'); + Close(); + Exit; + end; + + VideoStream := VideoFormatContext^.streams[VideoStreamIndex]; + VideoCodecContext := VideoStream^.codec; + + VideoCodec := avcodec_find_decoder(VideoCodecContext^.codec_id); + if (VideoCodec = nil) then + begin + Log.LogError('No matching codec found', 'TVideoPlayback_ffmpeg.Open'); + Close(); + Exit; + end; + + // set debug options + VideoCodecContext^.debug_mv := 0; + VideoCodecContext^.debug := 0; + + // detect bug-workarounds automatically + VideoCodecContext^.workaround_bugs := FF_BUG_AUTODETECT; + // error resilience strategy (careful/compliant/agressive/very_aggressive) + //VideoCodecContext^.error_resilience := FF_ER_CAREFUL; //FF_ER_COMPLIANT; + // allow non spec compliant speedup tricks. + //VideoCodecContext^.flags2 := VideoCodecContext^.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 + errnum := avcodec_open(VideoCodecContext, VideoCodec); + finally + FFMpegCore.UnlockAVCodec(); + end; + if (errnum < 0) then + begin + Log.LogError('No matching codec found', 'TVideoPlayback_ffmpeg.Open'); + Close(); + Exit; + end; + + // register custom callbacks for pts-determination + VideoCodecContext^.get_buffer := PtsGetBuffer; + VideoCodecContext^.release_buffer := PtsReleaseBuffer; + + {$ifdef DebugDisplay} + DebugWriteln('Found a matching Codec: '+ VideoCodecContext^.Codec.Name + sLineBreak + + sLineBreak + + ' Width = '+inttostr(VideoCodecContext^.width) + + ', Height='+inttostr(VideoCodecContext^.height) + sLineBreak + + ' Aspect : '+inttostr(VideoCodecContext^.sample_aspect_ratio.num) + '/' + + inttostr(VideoCodecContext^.sample_aspect_ratio.den) + sLineBreak + + ' Framerate : '+inttostr(VideoCodecContext^.time_base.num) + '/' + + inttostr(VideoCodecContext^.time_base.den)); + {$endif} + + // allocate space for decoded frame and rgb frame + AVFrame := avcodec_alloc_frame(); + AVFrameRGB := avcodec_alloc_frame(); + FrameBuffer := av_malloc(avpicture_get_size(PIXEL_FMT_FFMPEG, + VideoCodecContext^.width, VideoCodecContext^.height)); + + if ((AVFrame = nil) or (AVFrameRGB = nil) or (FrameBuffer = nil)) then + begin + Log.LogError('Failed to allocate buffers', 'TVideoPlayback_ffmpeg.Open'); + Close(); + Exit; + end; + + // TODO: pad data for OpenGL to GL_UNPACK_ALIGNMENT + // (otherwise video will be distorted if width/height is not a multiple of the alignment) + errnum := avpicture_fill(PAVPicture(AVFrameRGB), FrameBuffer, PIXEL_FMT_FFMPEG, + VideoCodecContext^.width, VideoCodecContext^.height); + if (errnum < 0) then + begin + Log.LogError('avpicture_fill failed: ' + FFMpegCore.GetErrorString(errnum), 'TVideoPlayback_ffmpeg.Open'); + Close(); + Exit; + end; + + // calculate some information for video display + VideoAspect := av_q2d(VideoCodecContext^.sample_aspect_ratio); + if (VideoAspect = 0) then + VideoAspect := VideoCodecContext^.width / + VideoCodecContext^.height + else + VideoAspect := VideoAspect * VideoCodecContext^.width / + VideoCodecContext^.height; + + VideoTimeBase := 1/av_q2d(VideoStream^.r_frame_rate); + + // hack to get reasonable timebase (for divx and others) + if (VideoTimeBase < 0.02) then // 0.02 <-> 50 fps + begin + VideoTimeBase := av_q2d(VideoStream^.r_frame_rate); + while (VideoTimeBase > 50) do + VideoTimeBase := VideoTimeBase/10; + VideoTimeBase := 1/VideoTimeBase; + end; + + Log.LogInfo('VideoTimeBase: ' + floattostr(VideoTimeBase), 'TVideoPlayback_ffmpeg.Open'); + Log.LogInfo('Framerate: '+inttostr(floor(1/VideoTimeBase))+'fps', 'TVideoPlayback_ffmpeg.Open'); + + {$IFDEF UseSWScale} + // if available get a SWScale-context -> faster than the deprecated img_convert(). + // SWScale has accelerated support for PIX_FMT_RGB32/PIX_FMT_BGR24/PIX_FMT_BGR565/PIX_FMT_BGR555. + // Note: PIX_FMT_RGB32 is a BGR- and not an RGB-format (maybe a bug)!!! + // The BGR565-formats (GL_UNSIGNED_SHORT_5_6_5) is way too slow because of its + // bad OpenGL support. The BGR formats have MMX(2) implementations but no speed-up + // could be observed in comparison to the RGB versions. + SoftwareScaleContext := sws_getContext( + VideoCodecContext^.width, VideoCodecContext^.height, + integer(VideoCodecContext^.pix_fmt), + VideoCodecContext^.width, VideoCodecContext^.height, + integer(PIXEL_FMT_FFMPEG), + SWS_FAST_BILINEAR, nil, nil, nil); + if (SoftwareScaleContext = nil) then + begin + Log.LogError('Failed to get swscale context', 'TVideoPlayback_ffmpeg.Open'); + Close(); + Exit; + end; + {$ENDIF} + + TexWidth := Round(Power(2, Ceil(Log2(VideoCodecContext^.width)))); + TexHeight := Round(Power(2, Ceil(Log2(VideoCodecContext^.height)))); + + // we retrieve a texture just once with glTexImage2D and update it with glTexSubImage2D later. + // Benefits: glTexSubImage2D is faster and supports non-power-of-two widths/height. + glBindTexture(GL_TEXTURE_2D, fVideoTex); + glTexEnvi(GL_TEXTURE_2D, GL_TEXTURE_ENV_MODE, GL_REPLACE); + glTexImage2D(GL_TEXTURE_2D, 0, 3, TexWidth, TexHeight, 0, + PIXEL_FMT_OPENGL, GL_UNSIGNED_BYTE, nil); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + + fVideoOpened := True; + + Result := true; +end; + +procedure TVideoPlayback_FFMpeg.Close; +begin + if (FrameBuffer <> nil) then + av_free(FrameBuffer); + if (AVFrameRGB <> nil) then + av_free(AVFrameRGB); + if (AVFrame <> nil) then + av_free(AVFrame); + + AVFrame := nil; + AVFrameRGB := nil; + FrameBuffer := nil; + + if (VideoCodecContext <> nil) then + begin + // avcodec_close() is not thread-safe + FFMpegCore.LockAVCodec(); + try + avcodec_close(VideoCodecContext); + finally + FFMpegCore.UnlockAVCodec(); end; end; - // return true if either an audio- or video-stream was found - result := (aFirstAudioStream > -1) or - (aFirstVideoStream > -1) ; + if (VideoFormatContext <> nil) then + av_close_input_file(VideoFormatContext); + + VideoCodecContext := nil; + VideoFormatContext := nil; + + fVideoOpened := False; end; -procedure TVideoPlayback_ffmpeg.SynchronizeVideo(pFrame: PAVFrame; var pts: double); +procedure TVideoPlayback_FFMpeg.SynchronizeVideo(Frame: PAVFrame; var pts: double); var FrameDelay: double; begin @@ -232,11 +422,11 @@ begin // update the video clock FrameDelay := av_q2d(VideoCodecContext^.time_base); // if we are repeating a frame, adjust clock accordingly - FrameDelay := FrameDelay + pFrame^.repeat_pict * (FrameDelay * 0.5); + FrameDelay := FrameDelay + Frame^.repeat_pict * (FrameDelay * 0.5); VideoTime := VideoTime + FrameDelay; end; -function TVideoPlayback_ffmpeg.DecodeFrame(var AVPacket: TAVPacket; out pts: double): boolean; +function TVideoPlayback_FFMpeg.DecodeFrame(var AVPacket: TAVPacket; out pts: double): boolean; var FrameFinished: Integer; VideoPktPts: int64; @@ -330,7 +520,7 @@ begin Result := true; end; -procedure TVideoPlayback_ffmpeg.GetFrame(Time: Extended); +procedure TVideoPlayback_FFMpeg.GetFrame(Time: Extended); var AVPacket: TAVPacket; errnum: Integer; @@ -429,7 +619,7 @@ begin {$ELSE} errnum := img_convert(PAVPicture(AVFrameRGB), PIXEL_FMT_FFMPEG, PAVPicture(AVFrame), VideoCodecContext^.pix_fmt, - VideoCodecContext^.width, VideoCodecContext^.height); + VideoCodecContext^.width, VideoCodecContext^.height); {$ENDIF} if (errnum < 0) then @@ -464,7 +654,7 @@ begin {$ENDIF} end; -procedure TVideoPlayback_ffmpeg.DrawGL(Screen: integer); +procedure TVideoPlayback_FFMpeg.DrawGL(Screen: integer); var TexVideoRightPos, TexVideoLowerPos: Single; ScreenLeftPos, ScreenRightPos: Single; @@ -488,6 +678,10 @@ begin Log.BenchmarkStart(15); {$ENDIF} + // TODO: add a SetAspectCorrectionMode() function so we can switch + // aspect correction. The screens video backgrounds look very ugly with aspect + // correction because of the white bars at the top and bottom. + ScreenAspect := ScreenW / ScreenH; RenderAspect := RenderW / RenderH; ScaledVideoWidth := RenderW; @@ -574,250 +768,20 @@ begin {$ENDIF} end; -constructor TVideoPlayback_ffmpeg.Create(); -begin - inherited; - Reset(); - av_register_all(); -end; - -procedure TVideoPlayback_ffmpeg.Init(); -begin - glGenTextures(1, PGLuint(@fVideoTex)); -end; - -procedure TVideoPlayback_ffmpeg.Reset(); -begin - // close previously opened video - Close(); - - fVideoOpened := False; - fVideoPaused := False; - VideoTimeBase := 0; - VideoTime := 0; - VideoStream := nil; - VideoFormatContext := nil; - VideoCodecContext := nil; - VideoStreamIndex := -1; - - AVFrame := nil; - AVFrameRGB := nil; - FrameBuffer := nil; - - EOF := false; - - // TODO: do we really want this by default? - Loop := true; - fLoopTime := 0; -end; - -function TVideoPlayback_ffmpeg.Open(const aFileName : string): boolean; // true if succeed -var - errnum: Integer; - err: GLenum; - AudioStreamIndex: integer; - - procedure CleanOnError(); - begin - if (VideoCodecContext <> nil) then - avcodec_close(VideoCodecContext); - if (VideoFormatContext <> nil) then - av_close_input_file(VideoFormatContext); - av_free(AVFrameRGB); - av_free(AVFrame); - av_free(FrameBuffer); - end; - -begin - Result := false; - - Reset(); - - errnum := av_open_input_file(VideoFormatContext, pchar( aFileName ), nil, 0, nil); - if (errnum <> 0) then - begin - Log.LogError('Failed to open file "'+aFileName+'" ('+FFMpegErrorString(errnum)+')'); - Exit; - end; - - // update video info - if (av_find_stream_info(VideoFormatContext) < 0) then - begin - Log.LogError('No stream info found', 'TVideoPlayback_ffmpeg.Open'); - CleanOnError(); - Exit; - end; - Log.LogInfo('VideoStreamIndex : ' + inttostr(VideoStreamIndex), 'TVideoPlayback_ffmpeg.Open'); - - // find video stream - FindStreamIDs(VideoFormatContext, VideoStreamIndex, AudioStreamIndex); - if (VideoStreamIndex < 0) then - begin - Log.LogError('No video stream found', 'TVideoPlayback_ffmpeg.Open'); - CleanOnError(); - Exit; - end; - - VideoStream := VideoFormatContext^.streams[VideoStreamIndex]; - VideoCodecContext := VideoStream^.codec; - - VideoCodec := avcodec_find_decoder(VideoCodecContext^.codec_id); - if (VideoCodec = nil) then - begin - Log.LogError('No matching codec found', 'TVideoPlayback_ffmpeg.Open'); - CleanOnError(); - Exit; - end; - - // set debug options - VideoCodecContext^.debug_mv := 0; - VideoCodecContext^.debug := 0; - - // detect bug-workarounds automatically - VideoCodecContext^.workaround_bugs := FF_BUG_AUTODETECT; - // error resilience strategy (careful/compliant/agressive/very_aggressive) - //VideoCodecContext^.error_resilience := FF_ER_CAREFUL; //FF_ER_COMPLIANT; - // allow non spec compliant speedup tricks. - //VideoCodecContext^.flags2 := VideoCodecContext^.flags2 or CODEC_FLAG2_FAST; - - errnum := avcodec_open(VideoCodecContext, VideoCodec); - if (errnum < 0) then - begin - Log.LogError('No matching codec found', 'TVideoPlayback_ffmpeg.Open'); - CleanOnError(); - Exit; - end; - - // register custom callbacks for pts-determination - VideoCodecContext^.get_buffer := PtsGetBuffer; - VideoCodecContext^.release_buffer := PtsReleaseBuffer; - - {$ifdef DebugDisplay} - DebugWriteln('Found a matching Codec: '+ VideoCodecContext^.Codec.Name + sLineBreak + - sLineBreak + - ' Width = '+inttostr(VideoCodecContext^.width) + - ', Height='+inttostr(VideoCodecContext^.height) + sLineBreak + - ' Aspect : '+inttostr(VideoCodecContext^.sample_aspect_ratio.num) + '/' + - inttostr(VideoCodecContext^.sample_aspect_ratio.den) + sLineBreak + - ' Framerate : '+inttostr(VideoCodecContext^.time_base.num) + '/' + - inttostr(VideoCodecContext^.time_base.den)); - {$endif} - - // allocate space for decoded frame and rgb frame - AVFrame := avcodec_alloc_frame(); - AVFrameRGB := avcodec_alloc_frame(); - FrameBuffer := av_malloc(avpicture_get_size(PIXEL_FMT_FFMPEG, - VideoCodecContext^.width, VideoCodecContext^.height)); - - if ((AVFrame = nil) or (AVFrameRGB = nil) or (FrameBuffer = nil)) then - begin - Log.LogError('Failed to allocate buffers', 'TVideoPlayback_ffmpeg.Open'); - CleanOnError(); - Exit; - end; - - // TODO: pad data for OpenGL to GL_UNPACK_ALIGNMENT - // (otherwise video will be distorted if width/height is not a multiple of the alignment) - errnum := avpicture_fill(PAVPicture(AVFrameRGB), FrameBuffer, PIXEL_FMT_FFMPEG, - VideoCodecContext^.width, VideoCodecContext^.height); - if (errnum < 0) then - begin - Log.LogError('avpicture_fill failed: ' + FFMpegErrorString(errnum), 'TVideoPlayback_ffmpeg.Open'); - CleanOnError(); - Exit; - end; - - // calculate some information for video display - VideoAspect := av_q2d(VideoCodecContext^.sample_aspect_ratio); - if (VideoAspect = 0) then - VideoAspect := VideoCodecContext^.width / - VideoCodecContext^.height - else - VideoAspect := VideoAspect * VideoCodecContext^.width / - VideoCodecContext^.height; - - VideoTimeBase := 1/av_q2d(VideoStream^.r_frame_rate); - - // hack to get reasonable timebase (for divx and others) - if (VideoTimeBase < 0.02) then // 0.02 <-> 50 fps - begin - VideoTimeBase := av_q2d(VideoStream^.r_frame_rate); - while (VideoTimeBase > 50) do - VideoTimeBase := VideoTimeBase/10; - VideoTimeBase := 1/VideoTimeBase; - end; - - Log.LogInfo('VideoTimeBase: ' + floattostr(VideoTimeBase), 'TVideoPlayback_ffmpeg.Open'); - Log.LogInfo('Framerate: '+inttostr(floor(1/VideoTimeBase))+'fps', 'TVideoPlayback_ffmpeg.Open'); - - {$IFDEF UseSWScale} - // if available get a SWScale-context -> faster than the deprecated img_convert(). - // SWScale has accelerated support for PIX_FMT_RGB32/PIX_FMT_BGR24/PIX_FMT_BGR565/PIX_FMT_BGR555. - // Note: PIX_FMT_RGB32 is a BGR- and not an RGB-format (maybe a bug)!!! - // The BGR565-formats (GL_UNSIGNED_SHORT_5_6_5) is way too slow because of its - // bad OpenGL support. The BGR formats have MMX(2) implementations but no speed-up - // could be observed in comparison to the RGB versions. - SoftwareScaleContext := sws_getContext( - VideoCodecContext^.width, VideoCodecContext^.height, - integer(VideoCodecContext^.pix_fmt), - VideoCodecContext^.width, VideoCodecContext^.height, - integer(PIXEL_FMT_FFMPEG), - SWS_FAST_BILINEAR, nil, nil, nil); - if (SoftwareScaleContext = nil) then - begin - Log.LogError('Failed to get swscale context', 'TVideoPlayback_ffmpeg.Open'); - CleanOnError(); - Exit; - end; - {$ENDIF} - - TexWidth := Round(Power(2, Ceil(Log2(VideoCodecContext^.width)))); - TexHeight := Round(Power(2, Ceil(Log2(VideoCodecContext^.height)))); - - // we retrieve a texture just once with glTexImage2D and update it with glTexSubImage2D later. - // Benefits: glTexSubImage2D is faster and supports non-power-of-two widths/height. - glBindTexture(GL_TEXTURE_2D, fVideoTex); - glTexEnvi(GL_TEXTURE_2D, GL_TEXTURE_ENV_MODE, GL_REPLACE); - glTexImage2D(GL_TEXTURE_2D, 0, 3, TexWidth, TexHeight, 0, - PIXEL_FMT_OPENGL, GL_UNSIGNED_BYTE, nil); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - - - fVideoOpened := True; - - Result := true; -end; - -procedure TVideoPlayback_ffmpeg.Close; -begin - if fVideoOpened then - begin - av_free(FrameBuffer); - av_free(AVFrameRGB); - av_free(AVFrame); - - avcodec_close(VideoCodecContext); - av_close_input_file(VideoFormatContext); - - fVideoOpened := False; - end; -end; - -procedure TVideoPlayback_ffmpeg.Play; +procedure TVideoPlayback_FFMpeg.Play; begin end; -procedure TVideoPlayback_ffmpeg.Pause; +procedure TVideoPlayback_FFMpeg.Pause; begin fVideoPaused := not fVideoPaused; end; -procedure TVideoPlayback_ffmpeg.Stop; +procedure TVideoPlayback_FFMpeg.Stop; begin end; -procedure TVideoPlayback_ffmpeg.SetPosition(Time: real); +procedure TVideoPlayback_FFMpeg.SetPosition(Time: real); var SeekFlags: integer; begin @@ -838,20 +802,19 @@ begin if (av_seek_frame(VideoFormatContext, VideoStreamIndex, Floor(Time/VideoTimeBase), SeekFlags) < 0) then begin Log.LogError('av_seek_frame() failed', 'TVideoPlayback_ffmpeg.SetPosition'); + Exit; end; + + avcodec_flush_buffers(VideoCodecContext); end; -function TVideoPlayback_ffmpeg.GetPosition: real; +function TVideoPlayback_FFMpeg.GetPosition: real; begin // TODO: return video-position in seconds - result := VideoTime; + Result := VideoTime; end; initialization - singleton_VideoFFMpeg := TVideoPlayback_ffmpeg.create(); - AudioManager.add( singleton_VideoFFMpeg ); - -finalization - AudioManager.Remove( singleton_VideoFFMpeg ); + MediaManager.Add(TVideoPlayback_FFMpeg.Create); end. diff --git a/Game/Code/Classes/UVisualizer.pas b/Game/Code/Classes/UVisualizer.pas index d778eff7..6864b6b9 100644 --- a/Game/Code/Classes/UVisualizer.pas +++ b/Game/Code/Classes/UVisualizer.pas @@ -1,13 +1,15 @@ -{############################################################################ -# Visualizer support for UltraStar deluxe # -# # -# Created by hennymcc # -# Slight modifications by Jay Binks # -# based on UVideo.pas # -#############################################################################} - unit UVisualizer; +(* TODO: + * - fix video/visualizer switching + * - use GL_EXT_framebuffer_object for rendering to a separate framebuffer, + * this will prevent plugins from messing up our render-context + * (-> no stack corruption anymore, no need for Save/RestoreOpenGLState()). + * - create a generic (C-compatible) interface for visualization plugins + * - create a visualization plugin manager + * - write a plugin for projectM in C/C++ (so we need no wrapper anymore) + *) + interface {$IFDEF FPC} @@ -35,27 +37,6 @@ uses UConfig, ULog; -(* - * TODO: - * - fix video/visualizer switching and initialisation - * - use GL_EXT_framebuffer_object for rendering to a separate framebuffer, - * this will prevent plugins from messing up our render-context - * (-> no stack corruption anymore, no need for Save/RestoreOpenGLState()). - * - create a generic (C-compatible) interface for visualization plugins - * - create a visualization plugin manager - * - write a plugin for projectM in C/C++ (so we need no wrapper anymore) - *) - -var - singleton_VideoProjectM : IVideoPlayback; - -var - ProjectMPath : string; - - // FIXME: dirty fix needed because the init method is not - // called yet. - inited: boolean; - {$IF PROJECTM_VERSION < 1000000} // < 1.0 const meshX = 32; @@ -67,15 +48,16 @@ const type TVideoPlayback_ProjectM = class( TInterfacedObject, IVideoPlayback, IVideoVisualization ) private - pm : TProjectM; + pm: TProjectM; + ProjectMPath : string; + Initialized: boolean; - VisualizerStarted , - VisualizerPaused : Boolean; + VisualizerStarted: boolean; + VisualizerPaused: boolean; - VisualTex : glUint; - PCMData : TPCMData; - - RndPCMcount : integer; + VisualTex: GLuint; + PCMData: TPCMData; + RndPCMcount: integer; projMatrix: array[0..3, 0..3] of GLdouble; texMatrix: array[0..3, 0..3] of GLdouble; @@ -91,31 +73,38 @@ type procedure RestoreOpenGLState(); public - procedure Init(); - function GetName: String; + function GetName: String; + + function Init(): boolean; + function Finalize(): boolean; - function Open(const aFileName : string): boolean; // true if succeed - procedure Close; + function Open(const aFileName : string): boolean; // true if succeed + procedure Close; - procedure Play; - procedure Pause; - procedure Stop; + procedure Play; + procedure Pause; + procedure Stop; - procedure SetPosition(Time: real); - function GetPosition: real; + procedure SetPosition(Time: real); + function GetPosition: real; - procedure GetFrame(Time: Extended); - procedure DrawGL(Screen: integer); + procedure GetFrame(Time: Extended); + procedure DrawGL(Screen: integer); end; -procedure TVideoPlayback_ProjectM.Init(); +function TVideoPlayback_ProjectM.GetName: String; begin - // FIXME: dirty fix needed because the init method is not - // called yet. - if (inited) then + result := 'ProjectM'; +end; + +function TVideoPlayback_ProjectM.Init(): boolean; +begin + Result := true; + + if (Initialized) then Exit; - inited := true; + Initialized := true; RndPCMcount := 0; @@ -133,20 +122,23 @@ begin {$ENDIF} end; -function TVideoPlayback_ProjectM.GetName: String; +function TVideoPlayback_ProjectM.Finalize(): boolean; begin - result := 'ProjectM'; + VisualizerStop(); + {$IFDEF UseTexture} + glDeleteTextures(1, PglUint(@VisualTex)); + {$ENDIF} + Result := true; end; - function TVideoPlayback_ProjectM.Open(const aFileName : string): boolean; // true if succeed begin - VisualizerStart(); - result := true; + result := false; end; procedure TVideoPlayback_ProjectM.Close; begin + VisualizerStop(); end; procedure TVideoPlayback_ProjectM.Play; @@ -232,18 +224,21 @@ begin if VisualizerStarted then Exit; - // FIXME: dirty fix needed because the init method is not - // called yet. - if (not inited) then - Init(); - - {$IF PROJECTM_VERSION >= 1000000} // >= 1.0 - pm := TProjectM.Create(ProjectMPath + 'config.inp'); - {$ELSE} - pm := TProjectM.Create( - meshX, meshY, fps, textureSize, ScreenW, ScreenH, - ProjectMPath + 'presets', ProjectMPath + 'fonts'); - {$IFEND} + try + {$IF PROJECTM_VERSION >= 1000000} // >= 1.0 + pm := TProjectM.Create(ProjectMPath + 'config.inp'); + {$ELSE} + pm := TProjectM.Create( + meshX, meshY, fps, textureSize, ScreenW, ScreenH, + ProjectMPath + 'presets', ProjectMPath + 'fonts'); + {$IFEND} + except on E: Exception do + begin + // Create() might fail if the config-file is not found + Log.LogError('TProjectM.Create: ' + E.Message, 'TVideoPlayback_ProjectM.VisualizerStart'); + Exit; + end; + end; VisualizerStarted := True; @@ -260,7 +255,7 @@ begin if VisualizerStarted then begin VisualizerStarted := False; - pm.Free(); + FreeAndNil(pm); end; end; @@ -380,10 +375,6 @@ end; initialization - singleton_VideoProjectM := TVideoPlayback_ProjectM.create(); - AudioManager.add( singleton_VideoProjectM ); - -finalization - AudioManager.Remove( singleton_VideoProjectM ); + MediaManager.Add(TVideoPlayback_ProjectM.Create); end. diff --git a/Game/Code/Menu/UMenu.pas b/Game/Code/Menu/UMenu.pas index 9c467e7d..625e051f 100644 --- a/Game/Code/Menu/UMenu.pas +++ b/Game/Code/Menu/UMenu.pas @@ -189,9 +189,6 @@ begin //Set ButtonPos to Autoset Length ButtonPos := -1; - - - VideoPlayback.Init; end; { constructor TMenu.Create(Back: String); diff --git a/Game/Code/Screens/UScreenSing.pas b/Game/Code/Screens/UScreenSing.pas index 3dc90da3..6dcec3cb 100644 --- a/Game/Code/Screens/UScreenSing.pas +++ b/Game/Code/Screens/UScreenSing.pas @@ -27,10 +27,16 @@ uses UMenu, UGraphicClasses, USingScores; +type + TLyricsSyncSource = class(TSyncSource) + function GetClock(): real; override; + end; + type TScreenSing = class(TMenu) protected - paused: boolean; //Pause Mod + Paused: boolean; //Pause Mod + LyricsSync: TLyricsSyncSource; NumEmptySentences: integer; public //TextTime: integer; @@ -140,7 +146,7 @@ begin if (Ini.AskbeforeDel <> 1) then Finish //else just Pause and let the Popup make the Work - else if not paused then + else if not Paused then Pause; Result := false; @@ -211,10 +217,10 @@ end; //Pause Mod procedure TScreenSing.Pause; begin - if not paused then //enable Pause + if (not Paused) then //enable Pause begin // pause Time - Paused := true; + Paused := true; LineState.Pause(); @@ -230,11 +236,6 @@ begin begin LineState.Resume(); - // Position of Music - // FIXME: remove this and provide LineState.CurrentTime as sync-source instead - // so every stream can synch itself - AudioPlayback.Position := LineState.CurrentTime; - // Play Music AudioPlayback.Play; @@ -326,8 +327,7 @@ begin Lyrics := TLyricEngine.Create(80,Skin_LyricsT,640,12,80,Skin_LyricsT+36,640,12); - if assigned( fCurrentVideoPlaybackEngine ) then - fCurrentVideoPlaybackEngine.Init(); + LyricsSync := TLyricsSyncSource.Create(); end; procedure TScreenSing.onShow; @@ -487,9 +487,6 @@ begin // reset video playback engine, to play video clip... - - Visualization.Init(); - fCurrentVideoPlaybackEngine.Close; fCurrentVideoPlaybackEngine := VideoPlayback; @@ -515,17 +512,19 @@ begin else Tex_Background.TexNum := 0; + // prepare lyrics timer + LineState.Pause(); + LineState.CurrentTime := CurrentSong.Start; + LineState.TotalTime := AudioPlayback.Length; - - // play music (I) - AudioInput.CaptureStart; + // prepare music + AudioPlayback.Stop(); AudioPlayback.Position := CurrentSong.Start; -// Music.Play; + // synchronize music to the lyrics + AudioPlayback.SetSyncSource(LyricsSync); - // prepare timer (I) -// CountSkipTimeSet; - LineState.CurrentTime := CurrentSong.Start; - LineState.TotalTime := AudioPlayback.Length; + // prepare and start voice-capture + AudioInput.CaptureStart; if (CurrentSong.Finish > 0) then LineState.TotalTime := CurrentSong.Finish / 1000; @@ -911,32 +910,13 @@ end; procedure TScreenSing.onShowFinish; begin - // play movie (II) - - if CurrentSong.VideoLoaded then - begin - try - fCurrentVideoPlaybackEngine.GetFrame(LineState.CurrentTime); - fCurrentVideoPlaybackEngine.DrawGL(ScreenAct); - except on E : Exception do - begin - //If an error occurs reading video: prevent video from being drawn again and close video - CurrentSong.VideoLoaded := False; - Log.LogError('Error drawing Video, Video has been disabled for this Song/Session.'); - Log.LogError('Error Message : '+ E.message ); - Log.LogError(' In : '+ E.ClassName +' (TScreenSing.onShowFinish)' ); - Log.LogError('Corrupted File: ' + CurrentSong.Video); - - fCurrentVideoPlaybackEngine.Close; - end; - end; - end; + // start lyrics + LineState.Resume(); + // start music + AudioPlayback.Play(); - // play music (II) - AudioPlayback.Play; - - // prepare timer (II) + // start timer CountSkipTimeSet; end; @@ -1185,29 +1165,11 @@ begin if (ShowFinish and (CurrentSong.VideoLoaded or fShowVisualization)) then begin - //try - // TODO: find a way to determine, when a new frame is needed - // TODO: same for the need to skip frames - if assigned( fCurrentVideoPlaybackEngine ) then - begin - fCurrentVideoPlaybackEngine.GetFrame(LineState.CurrentTime); - fCurrentVideoPlaybackEngine.DrawGL(ScreenAct); - end; - (* - except - on E : Exception do - begin - //If an Error occurs drawing: prevent Video from being Drawn again and Close Video - CurrentSong.VideoLoaded := False; - log.LogError('Error drawing Video, Video has been disabled for this Song/Session.'); - Log.LogError('Error Message : '+ E.message ); - Log.LogError(' In : '+ E.ClassName +' (TScreenSing.Draw)' ); - - Log.LogError('Corrupted File: ' + CurrentSong.Video); - fCurrentVideoPlaybackEngine.Close; - end; + if assigned( fCurrentVideoPlaybackEngine ) then + begin + fCurrentVideoPlaybackEngine.GetFrame(LineState.CurrentTime); + fCurrentVideoPlaybackEngine.DrawGL(ScreenAct); end; - *) end; // draw static menu (FG) @@ -1288,6 +1250,7 @@ procedure TScreenSing.Finish; begin AudioInput.CaptureStop; AudioPlayback.Stop; + AudioPlayback.SetSyncSource(nil); if (Ini.SavePlayback = 1) then begin Log.BenchmarkStart(0); @@ -1436,4 +1399,9 @@ begin //GoldenStarsTwinkle Mod End end; +function TLyricsSyncSource.GetClock(): real; +begin + Result := LineState.CurrentTime; +end; + end. diff --git a/Game/Code/UltraStar.dpr b/Game/Code/UltraStar.dpr index 75e82207..edb8d7de 100644 --- a/Game/Code/UltraStar.dpr +++ b/Game/Code/UltraStar.dpr @@ -42,6 +42,7 @@ uses sdl_image in 'lib\JEDI-SDL\SDL_Image\Pas\sdl_image.pas', sdl_ttf in 'lib\JEDI-SDL\SDL_ttf\Pas\sdl_ttf.pas', sdlutils in 'lib\JEDI-SDL\SDL\Pas\sdlutils.pas', + UMediaCore_SDL in 'Classes\UMediaCore_SDL.pas', zlib in 'lib\zlib\zlib.pas', png in 'lib\libpng\png.pas', @@ -76,18 +77,23 @@ uses {$ENDIF} {$IFDEF UseFFMpeg} - avcodec in 'lib\ffmpeg\avcodec.pas', - avformat in 'lib\ffmpeg\avformat.pas', - avutil in 'lib\ffmpeg\avutil.pas', - rational in 'lib\ffmpeg\rational.pas', - opt in 'lib\ffmpeg\opt.pas', - avio in 'lib\ffmpeg\avio.pas', - mathematics in 'lib\ffmpeg\mathematics.pas', + avcodec in 'lib\ffmpeg\avcodec.pas', + avformat in 'lib\ffmpeg\avformat.pas', + avutil in 'lib\ffmpeg\avutil.pas', + rational in 'lib\ffmpeg\rational.pas', + opt in 'lib\ffmpeg\opt.pas', + avio in 'lib\ffmpeg\avio.pas', + mathematics in 'lib\ffmpeg\mathematics.pas', + UMediaCore_FFMpeg in 'Classes\UMediaCore_FFMpeg.pas', {$IFDEF UseSWScale} - swscale in 'lib\ffmpeg\swscale.pas', + swscale in 'lib\ffmpeg\swscale.pas', {$ENDIF} {$ENDIF} + {$IFDEF UseSRCResample} + samplerate in 'lib\samplerate\samplerate.pas', + {$ENDIF} + {$IFDEF UseProjectM} projectM in 'lib\projectM\projectM.pas', {$ENDIF} @@ -145,6 +151,7 @@ uses UDLLManager in 'Classes\UDLLManager.pas', UPlaylist in 'Classes\UPlaylist.pas', UCommandLine in 'Classes\UCommandLine.pas', + URingBuffer in 'Classes\URingBuffer.pas', UTextClasses in 'Classes\UTextClasses.pas', USingScores in 'Classes\USingScores.pas', USingNotes in 'Classes\USingNotes.pas', @@ -170,43 +177,56 @@ uses {$ENDIF} //------------------------------ - //Includes - Media support classes.... - // Make sure UMedia always first, then UMedia_dummy + //Includes - Media //------------------------------ - // TODO : these all need to be renamed like UMedia_******** for consistency UMusic in 'Classes\UMusic.pas', UAudioPlaybackBase in 'Classes\UAudioPlaybackBase.pas', - UMedia_dummy in 'Classes\UMedia_dummy.pas', // Must be first UMedia Unit, all others will override available interfaces -{$IFDEF UseProjectM} - UVisualizer in 'Classes\UVisualizer.pas', // MUST be before Video... so video can override... -{$ENDIF} +{$IF Defined(UsePortaudioPlayback) or Defined(UseSDLPlayback)} + UFFT in 'lib\fft\UFFT.pas', + UAudioPlayback_Softmixer in 'Classes\UAudioPlayback_SoftMixer.pas', +{$IFEND} + UAudioConverter in 'Classes\UAudioConverter.pas', + + //****************************** + //Pluggable media modules + // The modules are prioritized as in the include list below. + // This means the first entry has highest priority, the last lowest. + //****************************** + + // TODO : these all should be moved to a media folder + {$IFDEF UseFFMpegVideo} - UVideo in 'Classes\UVideo.pas', + UVideo in 'Classes\UVideo.pas', {$ENDIF} -{$IFDEF UseFFMpegDecoder} - UAudioDecoder_FFMpeg in 'Classes\UAudioDecoder_FFMpeg.pas', // MUST be before Playback-classes +{$IFDEF UseProjectM} + // must be after UVideo, so it will not be the default video module + UVisualizer in 'Classes\UVisualizer.pas', {$ENDIF} {$IFDEF UseBASSInput} - UAudioInput_Bass in 'Classes\UAudioInput_Bass.pas', + UAudioInput_Bass in 'Classes\UAudioInput_Bass.pas', +{$ENDIF} +{$IFDEF UseBASSDecoder} + // prefer Bass to FFMpeg if possible + UAudioDecoder_Bass in 'Classes\UAudioDecoder_Bass.pas', {$ENDIF} {$IFDEF UseBASSPlayback} - UAudioPlayback_Bass in 'Classes\UAudioPlayback_Bass.pas', + UAudioPlayback_Bass in 'Classes\UAudioPlayback_Bass.pas', +{$ENDIF} +{$IFDEF UseSDLPlayback} + UAudioPlayback_SDL in 'Classes\UAudioPlayback_SDL.pas', {$ENDIF} {$IFDEF UsePortaudioInput} - UAudioInput_Portaudio in 'Classes\UAudioInput_Portaudio.pas', + UAudioInput_Portaudio in 'Classes\UAudioInput_Portaudio.pas', {$ENDIF} -{$IF Defined(UsePortaudioPlayback) or Defined(UseSDLPlayback)} - UFFT in 'lib\fft\UFFT.pas', - //samplerate in 'lib\samplerate\samplerate.pas', - UAudioPlayback_Softmixer in 'Classes\UAudioPlayback_SoftMixer.pas', -{$IFEND} {$IFDEF UsePortaudioPlayback} UAudioPlayback_Portaudio in 'Classes\UAudioPlayback_Portaudio.pas', {$ENDIF} -{$IFDEF UseSDLPlayback} - UAudioPlayback_SDL in 'Classes\UAudioPlayback_SDL.pas', +{$IFDEF UseFFMpegDecoder} + UAudioDecoder_FFMpeg in 'Classes\UAudioDecoder_FFMpeg.pas', {$ENDIF} + // fallback dummy, must be last + UMedia_dummy in 'Classes\UMedia_dummy.pas', //------------------------------ diff --git a/Game/Code/config-win.inc b/Game/Code/config-win.inc index 47676ee1..d870a9cd 100644 --- a/Game/Code/config-win.inc +++ b/Game/Code/config-win.inc @@ -19,7 +19,7 @@ av__util = 'avutil-49'; LIBAVUTIL_VERSION_MAJOR = 49; LIBAVUTIL_VERSION_MINOR = 0; - LIBAVUTIL_VERSION_RELEASE = 0; + LIBAVUTIL_VERSION_RELEASE = 1; {$IFEND} {$UNDEF HaveSWScale} @@ -47,3 +47,10 @@ {$UNDEF HavePortmixer} +{$UNDEF HaveLibsamplerate} +{$IF Defined(HaveLibsamplerate) and Defined(IncludeConstants)} + LIBSAMPLERATE_VERSION_MAJOR = 0; + LIBSAMPLERATE_VERSION_MINOR = 1; + LIBSAMPLERATE_VERSION_RELEASE = 3; +{$IFEND} + diff --git a/Game/Code/switches.inc b/Game/Code/switches.inc index 52df7e7d..bb9a50fe 100644 --- a/Game/Code/switches.inc +++ b/Game/Code/switches.inc @@ -74,10 +74,11 @@ // audio config {$IF Defined(HaveBASS)} {$DEFINE UseBASSPlayback} + {$DEFINE UseBASSDecoder} {$DEFINE UseBASSInput} {$ELSEIF Defined(HavePortaudio)} - {.$DEFINE UsePortaudioPlayback} {$DEFINE UseSDLPlayback} + {.$DEFINE UsePortaudioPlayback} {$DEFINE UsePortaudioInput} {$IFDEF HavePortmixer} {$DEFINE UsePortmixer} @@ -87,12 +88,17 @@ // ffmpeg config {$IFDEF HaveFFMpeg} {$DEFINE UseFFMpegDecoder} + {$DEFINE UseFFMpegResample} {$DEFINE UseFFMpegVideo} {$IFDEF HaveSWScale} {$DEFINE UseSWScale} {$ENDIF} {$ENDIF} +{$IFDEF HaveLibsamplerate} + {$DEFINE UseSRCResample} +{$ENDIF} + // projectM config {$IF Defined(HaveProjectM)} {$DEFINE UseProjectM} @@ -104,7 +110,7 @@ {$DEFINE UseFFMpeg} {$IFEND} -{$IF Defined(UseBASSInput) or Defined(UseBASSPlayback)} +{$IF Defined(UseBASSInput) or Defined(UseBASSPlayback) or Defined(UseBASSDecoder)} {$DEFINE UseBASS} {$IFEND} -- cgit v1.2.3