aboutsummaryrefslogtreecommitdiffstats
path: root/Game/Code/Classes
diff options
context:
space:
mode:
authortobigun <tobigun@b956fd51-792f-4845-bead-9b4dfca2ff2c>2008-07-02 07:50:39 +0000
committertobigun <tobigun@b956fd51-792f-4845-bead-9b4dfca2ff2c>2008-07-02 07:50:39 +0000
commitfaf4c13bf41a17ce920a2194fc396f8bf7b44331 (patch)
tree0049a0dacad82a08b934167660bfabd6c8ea47a8 /Game/Code/Classes
parent67d0be6741c5466c786d8d389e34c83e1be7e3c0 (diff)
downloadusdx-faf4c13bf41a17ce920a2194fc396f8bf7b44331.tar.gz
usdx-faf4c13bf41a17ce920a2194fc396f8bf7b44331.tar.xz
usdx-faf4c13bf41a17ce920a2194fc396f8bf7b44331.zip
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
Diffstat (limited to 'Game/Code/Classes')
-rw-r--r--Game/Code/Classes/UAudioConverter.pas457
-rw-r--r--Game/Code/Classes/UAudioCore_Bass.pas18
-rw-r--r--Game/Code/Classes/UAudioCore_Portaudio.pas3
-rw-r--r--Game/Code/Classes/UAudioDecoder_Bass.pas242
-rw-r--r--Game/Code/Classes/UAudioDecoder_FFMpeg.pas1522
-rw-r--r--Game/Code/Classes/UAudioInput_Bass.pas39
-rw-r--r--Game/Code/Classes/UAudioInput_Portaudio.pas9
-rw-r--r--Game/Code/Classes/UAudioPlaybackBase.pas96
-rw-r--r--Game/Code/Classes/UAudioPlayback_Bass.pas686
-rw-r--r--Game/Code/Classes/UAudioPlayback_Portaudio.pas105
-rw-r--r--Game/Code/Classes/UAudioPlayback_SDL.pas60
-rw-r--r--Game/Code/Classes/UAudioPlayback_SoftMixer.pas1002
-rw-r--r--Game/Code/Classes/UCommon.pas61
-rw-r--r--Game/Code/Classes/UConfig.pas6
-rw-r--r--Game/Code/Classes/UGraphic.pas14
-rw-r--r--Game/Code/Classes/UMain.pas11
-rw-r--r--Game/Code/Classes/UMediaCore_FFMpeg.pas405
-rw-r--r--Game/Code/Classes/UMediaCore_SDL.pas38
-rw-r--r--Game/Code/Classes/UMedia_dummy.pas135
-rw-r--r--Game/Code/Classes/UMusic.pas877
-rw-r--r--Game/Code/Classes/URecord.pas259
-rw-r--r--Game/Code/Classes/URingBuffer.pas128
-rw-r--r--Game/Code/Classes/UTime.pas65
-rw-r--r--Game/Code/Classes/UVideo.pas601
-rw-r--r--Game/Code/Classes/UVisualizer.pas141
25 files changed, 4681 insertions, 2299 deletions
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
@@ -318,108 +426,200 @@ type
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.