aboutsummaryrefslogtreecommitdiffstats
path: root/src/media
diff options
context:
space:
mode:
Diffstat (limited to 'src/media')
-rw-r--r--src/media/UAudioConverter.pas483
-rw-r--r--src/media/UAudioCore_Bass.pas177
-rw-r--r--src/media/UAudioCore_Portaudio.pas337
-rw-r--r--src/media/UAudioDecoder_Bass.pas278
-rw-r--r--src/media/UAudioDecoder_FFmpeg.pas1159
-rw-r--r--src/media/UAudioInput_Bass.pas519
-rw-r--r--src/media/UAudioInput_Portaudio.pas537
-rw-r--r--src/media/UAudioPlaybackBase.pas319
-rw-r--r--src/media/UAudioPlayback_Bass.pas758
-rw-r--r--src/media/UAudioPlayback_Portaudio.pas385
-rw-r--r--src/media/UAudioPlayback_SDL.pas182
-rw-r--r--src/media/UAudioPlayback_SoftMixer.pas1204
-rw-r--r--src/media/UMediaCore_FFmpeg.pas645
-rw-r--r--src/media/UMediaCore_SDL.pas63
-rw-r--r--src/media/UMedia_dummy.pas492
-rw-r--r--src/media/UVideo.pas1436
-rw-r--r--src/media/UVisualizer.pas685
17 files changed, 9659 insertions, 0 deletions
diff --git a/src/media/UAudioConverter.pas b/src/media/UAudioConverter.pas
new file mode 100644
index 00000000..657b80dd
--- /dev/null
+++ b/src/media/UAudioConverter.pas
@@ -0,0 +1,483 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UAudioConverter;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+uses
+ UMusic,
+ ULog,
+ ctypes,
+ {$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: PByteArray; OutputBuffer: PByteArray; 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: PByteArray; OutputBuffer: PByteArray; 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: PByteArray; OutputBuffer: PByteArray; 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: PByteArray; OutputBuffer: PByteArray; 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: PByteArray; OutputBuffer: PByteArray; 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: PByteArray; OutputBuffer: PByteArray; var InputSize: integer): integer;
+var
+ FloatInputBuffer: PSingle;
+ FloatOutputBuffer: PSingle;
+ TempBuffer: PByteArray;
+ 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(PCshort(InputBuffer), PCfloat(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 := PCFloat(FloatInputBuffer);
+ input_frames := InputSize div SrcFormatInfo.FrameSize;
+ data_out := PCFloat(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(PCfloat(FloatOutputBuffer), PCshort(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/src/media/UAudioCore_Bass.pas b/src/media/UAudioCore_Bass.pas
new file mode 100644
index 00000000..3a84dcd7
--- /dev/null
+++ b/src/media/UAudioCore_Bass.pas
@@ -0,0 +1,177 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UAudioCore_Bass;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+uses
+ Classes,
+ SysUtils,
+ UMusic,
+ bass; // (Note: DWORD is defined here)
+
+type
+ TAudioCore_Bass = class
+ public
+ constructor Create();
+ class function GetInstance(): TAudioCore_Bass;
+ function CheckVersion(): boolean;
+ 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;
+ private
+ function DecodeVersion(VersionHex: integer): string;
+ end;
+
+implementation
+
+uses
+ UMain,
+ ULog;
+
+const
+ // BASS 2.4.2 is not ABI compatible with older versions
+ // as (BASS_RECORDINFO.driver was removed)
+ BASS_MIN_REQUIRED_VERSION = $02040201;
+
+var
+ Instance: TAudioCore_Bass;
+
+constructor TAudioCore_Bass.Create();
+begin
+ inherited;
+end;
+
+class function TAudioCore_Bass.GetInstance(): TAudioCore_Bass;
+begin
+ if (not Assigned(Instance)) then
+ Instance := TAudioCore_Bass.Create();
+ Result := Instance;
+end;
+
+function TAudioCore_Bass.DecodeVersion(VersionHex: integer): string;
+var
+ Version: array [0..3] of integer;
+begin
+ Version[0] := (VersionHex shr 24) and $FF;
+ Version[1] := (VersionHex shr 16) and $FF;
+ Version[2] := (VersionHex shr 8) and $FF;
+ Version[3] := (VersionHex shr 0) and $FF;
+ Result := Format('%x.%x.%x.%x', [Version[0], Version[1], Version[2], Version[3]]);
+end;
+
+function TAudioCore_Bass.CheckVersion(): boolean;
+begin
+ Result := BASS_GetVersion() >= BASS_MIN_REQUIRED_VERSION;
+ if (not Result) then
+ begin
+ Log.LogWarn('Could not init BASS audio library. ''bass.dll'' version is ' + DecodeVersion(BASS_GetVersion()) + ' but ' + DecodeVersion(BASS_MIN_REQUIRED_VERSION) + ' or higher is required.',
+ 'TAudioCore_Bass.CheckVersion');
+ end;
+end;
+
+function TAudioCore_Bass.ErrorGetString(): string;
+begin
+ Result := ErrorGetString(BASS_ErrorGetCode());
+end;
+
+function TAudioCore_Bass.ErrorGetString(errCode: integer): string;
+begin
+ case errCode of
+ BASS_OK: result := 'No error';
+ BASS_ERROR_MEM: result := 'Insufficient memory';
+ BASS_ERROR_FILEOPEN: result := 'File could not be opened';
+ BASS_ERROR_DRIVER: result := 'Device driver not available';
+ BASS_ERROR_BUFLOST: result := 'Buffer lost';
+ BASS_ERROR_HANDLE: result := 'Invalid Handle';
+ BASS_ERROR_FORMAT: result := 'Sample-Format not supported';
+ BASS_ERROR_POSITION: result := 'Illegal position';
+ BASS_ERROR_INIT: result := 'BASS_Init has not been successfully called';
+ BASS_ERROR_START: result := 'Paused/stopped';
+ BASS_ERROR_ALREADY: result := 'Already created/used';
+ BASS_ERROR_NOCHAN: result := 'No free channels';
+ BASS_ERROR_ILLTYPE: result := 'Type is invalid';
+ BASS_ERROR_ILLPARAM: result := 'Illegal parameter';
+ BASS_ERROR_NO3D: result := 'No 3D support';
+ BASS_ERROR_NOEAX: result := 'No EAX support';
+ BASS_ERROR_DEVICE: result := 'Invalid device number';
+ BASS_ERROR_NOPLAY: result := 'Channel not playing';
+ BASS_ERROR_FREQ: result := 'Freq out of range';
+ BASS_ERROR_NOTFILE: result := 'Not a file stream';
+ BASS_ERROR_NOHW: result := 'No hardware support';
+ BASS_ERROR_EMPTY: result := 'Is empty';
+ BASS_ERROR_NONET: result := 'Network unavailable';
+ BASS_ERROR_CREATE: result := 'Creation error';
+ BASS_ERROR_NOFX: result := 'DX8 effects unavailable';
+ BASS_ERROR_NOTAVAIL: result := 'Not available';
+ BASS_ERROR_DECODE: result := 'Is a decoding channel';
+ BASS_ERROR_DX: result := 'Insufficient version of DirectX';
+ BASS_ERROR_TIMEOUT: result := 'Timeout';
+ BASS_ERROR_FILEFORM: result := 'File-Format not recognised/supported';
+ BASS_ERROR_SPEAKER: result := 'Requested speaker(s) not support';
+ BASS_ERROR_VERSION: result := 'Version error';
+ BASS_ERROR_CODEC: result := 'Codec not available/supported';
+ BASS_ERROR_ENDED: result := 'The channel/file has ended';
+ BASS_ERROR_UNKNOWN: result := 'Unknown error';
+ else result := 'Unknown error';
+ end;
+end;
+
+function TAudioCore_Bass.ConvertAudioFormatToBASSFlags(Format: TAudioSampleFormat; out Flags: DWORD): boolean;
+begin
+ case Format of
+ asfS16: Flags := 0;
+ asfFloat: Flags := BASS_SAMPLE_FLOAT;
+ asfU8: Flags := BASS_SAMPLE_8BITS;
+ else begin
+ Result := false;
+ Exit;
+ end;
+ end;
+
+ 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/src/media/UAudioCore_Portaudio.pas b/src/media/UAudioCore_Portaudio.pas
new file mode 100644
index 00000000..c97b5d10
--- /dev/null
+++ b/src/media/UAudioCore_Portaudio.pas
@@ -0,0 +1,337 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UAudioCore_Portaudio;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I ../switches.inc}
+
+uses
+ Classes,
+ SysUtils,
+ portaudio;
+
+type
+ TAudioCore_Portaudio = class
+ private
+ InitCount: integer; ///< keeps track of the number of Initialize/Terminate calls
+ public
+ constructor Create();
+ class function GetInstance(): TAudioCore_Portaudio;
+ function Initialize(): boolean;
+ function Terminate(): boolean;
+ function GetPreferredApiIndex(): TPaHostApiIndex;
+ function TestDevice(inParams, outParams: PPaStreamParameters; var sampleRate: double): boolean;
+ end;
+
+implementation
+
+uses
+ ULog;
+
+{*
+ * The default API used by Portaudio is the least common denominator
+ * and might lack efficiency. In addition it might not even work.
+ * We use an array named ApiPreferenceOrder with which we define the order of
+ * preferred APIs to use. The first API-type in the list is tried first.
+ * If it is not available the next one is tried and so on ...
+ * If none of the preferred APIs was found the default API (detected by
+ * portaudio) is used.
+ *
+ * Pascal does not permit zero-length static arrays, so you must use paDefaultApi
+ * as an array's only member if you do not have any preferences.
+ * You can also append paDefaultApi to a non-zero length preferences array but
+ * this is optional because the default API is always used as a fallback.
+ *}
+const
+ paDefaultApi = -1;
+const
+ ApiPreferenceOrder:
+{$IF Defined(MSWINDOWS)}
+ // Note1: Portmixer has no mixer support for paASIO and paWASAPI at the moment
+ // Note2: Windows Default-API is MME, but DirectSound is faster
+ array[0..0] of TPaHostApiTypeId = ( paDirectSound );
+{$ELSEIF Defined(DARWIN)}
+ array[0..0] of TPaHostApiTypeId = ( paDefaultApi ); // paCoreAudio
+{$ELSEIF Defined(UNIX)}
+ // Note: Portmixer has no mixer support for JACK at the moment
+ array[0..2] of TPaHostApiTypeId = ( paALSA, paJACK, paOSS );
+{$ELSE}
+ array[0..0] of TPaHostApiTypeId = ( paDefaultApi );
+{$IFEND}
+
+
+{ TAudioInput_Portaudio }
+
+var
+ Instance: TAudioCore_Portaudio;
+
+constructor TAudioCore_Portaudio.Create();
+begin
+ inherited;
+ InitCount := 0;
+end;
+
+class function TAudioCore_Portaudio.GetInstance(): TAudioCore_Portaudio;
+begin
+ if not assigned(Instance) then
+ Instance := TAudioCore_Portaudio.Create();
+ Result := Instance;
+end;
+
+function TAudioCore_Portaudio.Initialize(): boolean;
+var
+ Err: TPaError;
+begin
+ // initialize only once
+ if (InitCount > 0) then
+ begin
+ Inc(InitCount);
+ Result := true;
+ Exit;
+ end;
+
+ // init Portaudio
+ Err := Pa_Initialize();
+ if (Err <> paNoError) then
+ begin
+ Log.LogError(Pa_GetErrorText(Err), 'TAudioCore_Portaudio.Initialize');
+ Result := false;
+ Exit;
+ end;
+
+ // only increment on success
+ Inc(InitCount);
+ Result := true;
+end;
+
+function TAudioCore_Portaudio.Terminate(): boolean;
+var
+ Err: TPaError;
+begin
+ // decrement usage count
+ Dec(InitCount);
+ if (InitCount > 0) then
+ begin
+ // do not terminate yet
+ Result := true;
+ Exit;
+ end;
+
+ // terminate if usage count is 0
+ Err := Pa_Terminate();
+ if (Err <> paNoError) then
+ begin
+ Log.LogError(Pa_GetErrorText(Err), 'TAudioCore_Portaudio.Terminate');
+ Result := false;
+ Exit;
+ end;
+
+ Result := true;
+end;
+
+function TAudioCore_Portaudio.GetPreferredApiIndex(): TPaHostApiIndex;
+var
+ i: integer;
+ apiIndex: TPaHostApiIndex;
+ apiInfo: PPaHostApiInfo;
+begin
+ result := -1;
+
+ // select preferred sound-API
+ for i:= 0 to High(ApiPreferenceOrder) do
+ begin
+ if (ApiPreferenceOrder[i] <> paDefaultApi) then
+ begin
+ // check if API is available
+ apiIndex := Pa_HostApiTypeIdToHostApiIndex(ApiPreferenceOrder[i]);
+ if (apiIndex >= 0) then
+ begin
+ // we found an API but we must check if it works
+ // (on linux portaudio might detect OSS but does not provide
+ // any devices if ALSA is enabled)
+ apiInfo := Pa_GetHostApiInfo(apiIndex);
+ if (apiInfo^.deviceCount > 0) then
+ begin
+ Result := apiIndex;
+ break;
+ end;
+ end;
+ end;
+ end;
+
+ // None of the preferred APIs is available -> use default
+ if (result < 0) then
+ begin
+ result := Pa_GetDefaultHostApi();
+ end;
+end;
+
+{*
+ * Portaudio test callback used by TestDevice().
+ *}
+function TestCallback(input: pointer; output: pointer; frameCount: longword;
+ timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
+ inputDevice: pointer): integer; cdecl;
+begin
+ // this callback is called only once
+ result := paAbort;
+end;
+
+(*
+ * Tests if the callback works. Some devices can be opened without
+ * an error but the callback is never called. Calling Pa_StopStream() on such
+ * a stream freezes USDX then. Probably because the callback-thread is deadlocked
+ * due to some bug in portaudio. The blocking Pa_ReadStream() and Pa_WriteStream()
+ * block forever too and though can't be used for testing.
+ *
+ * To avoid freezing Pa_AbortStream (or Pa_CloseStream which calls Pa_AbortStream)
+ * can be used to force the stream to stop. But for some reason this stops debugging
+ * in gdb with a "no process found" message.
+ *
+ * Because freezing devices are non-working devices we test the devices here to
+ * be able to exclude them from the device-selection list.
+ *
+ * Portaudio does not provide any test to check this error case (probably because
+ * it should not even occur). So we have to open the device, start the stream and
+ * check if the callback is called (the stream is stopped if the callback is called
+ * for the first time, so we can poll until the stream is stopped).
+ *
+ * Another error that occurs is that some devices (even the default device) might
+ * work at the beginning but stop after a few calls (maybe 50) of the callback.
+ * For me this problem occurs with the default output-device. The "dmix" or "front"
+ * device must be selected instead. Another problem is that (due to a bug in
+ * portaudio or ALSA) the "front" device is not detected every time portaudio
+ * is started. Sometimes it needs two or more restarts.
+ *
+ * There is no reasonable way to test for these errors. For the first error-case
+ * we could test if the callback is called 50 times but this can take a second
+ * for each device and it can fail in the 51st or even 100th callback call then.
+ *
+ * The second error-case cannot be tested at all. How should we now that one
+ * device is missing if portaudio is not even able to detect it.
+ * We could start and terminate Portaudio for several times and see if the device
+ * count changes but this is ugly.
+ *
+ * Conclusion: We are not able to autodetect a working device with
+ * portaudio (at least not with the newest v19_20071207) at the moment.
+ * So we have to provide the possibility to manually select an output device
+ * in the UltraStar options if we want to use portaudio instead of SDL.
+ *)
+function TAudioCore_Portaudio.TestDevice(inParams, outParams: PPaStreamParameters; var sampleRate: double): boolean;
+const
+ altSampleRates: array[0..1] of double = (44100, 48000); // alternative sample-rates
+var
+ stream: PPaStream;
+ err: TPaError;
+ cbWorks: boolean;
+ cbPolls: integer;
+ i: integer;
+begin
+ Result := false;
+
+ if (sampleRate <= 0) then
+ sampleRate := 44100;
+
+ // check if device supports our input-format
+ err := Pa_IsFormatSupported(inParams, outParams, sampleRate);
+ if (err <> paNoError) then
+ begin
+ // we cannot fix the error -> exit
+ if (err <> paInvalidSampleRate) then
+ Exit;
+
+ // try alternative sample-rates to the detected one
+ sampleRate := 0;
+ for i := 0 to High(altSampleRates) do
+ begin
+ // do not check the detected sample-rate twice
+ if (altSampleRates[i] = sampleRate) then
+ continue;
+ // check alternative
+ err := Pa_IsFormatSupported(inParams, outParams, altSampleRates[i]);
+ if (err = paNoError) then
+ begin
+ // sample-rate works
+ sampleRate := altSampleRates[i];
+ break;
+ end;
+ end;
+ // no working sample-rate found
+ if (sampleRate = 0) then
+ Exit;
+ end;
+
+ // FIXME: for some reason gdb stops after a call of Pa_AbortStream()
+ // which is implicitely called by Pa_CloseStream().
+ // gdb's stops with the message: "ptrace: no process found".
+ // Probably because the callback-thread is killed what confuses gdb.
+ {$IF Defined(Debug) and Defined(Linux)}
+ cbWorks := true;
+ {$ELSE}
+ // open device for testing
+ err := Pa_OpenStream(stream, inParams, outParams, sampleRate,
+ paFramesPerBufferUnspecified,
+ paNoFlag, @TestCallback, nil);
+ if (err <> paNoError) then
+ begin
+ exit;
+ end;
+
+ // start the callback
+ err := Pa_StartStream(stream);
+ if (err <> paNoError) then
+ begin
+ Pa_CloseStream(stream);
+ exit;
+ end;
+
+ cbWorks := false;
+ // check if the callback was called (poll for max. 200ms)
+ for cbPolls := 1 to 20 do
+ begin
+ // if the test-callback was called it should be aborted now
+ if (Pa_IsStreamActive(stream) = 0) then
+ begin
+ cbWorks := true;
+ break;
+ end;
+ // not yet aborted, wait and try (poll) again
+ Pa_Sleep(10);
+ end;
+
+ // finally abort the stream
+ Pa_CloseStream(stream);
+ {$IFEND}
+
+ Result := cbWorks;
+end;
+
+end.
diff --git a/src/media/UAudioDecoder_Bass.pas b/src/media/UAudioDecoder_Bass.pas
new file mode 100644
index 00000000..d6d2425a
--- /dev/null
+++ b/src/media/UAudioDecoder_Bass.pas
@@ -0,0 +1,278 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UAudioDecoder_Bass;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+implementation
+
+uses
+ Classes,
+ SysUtils,
+ bass,
+ UMain,
+ UMusic,
+ UAudioCore_Bass,
+ ULog,
+ UPath;
+
+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: PByteArray; 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: IPath): 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: PByteArray; 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
+ Result := false;
+ BassCore := TAudioCore_Bass.GetInstance();
+ if not BassCore.CheckVersion then
+ Exit;
+ Result := true;
+end;
+
+function TAudioDecoder_Bass.FinalizeDecoder(): boolean;
+begin
+ Result := true;
+end;
+
+function TAudioDecoder_Bass.Open(const Filename: IPath): 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.
+
+ {$IFDEF MSWINDOWS}
+ // Windows: Use UTF-16 version
+ Stream := BASS_StreamCreateFile(False, PWideChar(Filename.ToWide), 0, 0, BASS_STREAM_DECODE or BASS_UNICODE);
+ {$ELSE}
+ // Mac OS X: Use UTF8/ANSI version
+ Stream := BASS_StreamCreateFile(False, PAnsiChar(Filename.ToNative), 0, 0, BASS_STREAM_DECODE);
+ {$ENDIF}
+ 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 := Filename.GetExtension.ToUTF8;
+ // 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/src/media/UAudioDecoder_FFmpeg.pas b/src/media/UAudioDecoder_FFmpeg.pas
new file mode 100644
index 00000000..b44c7b11
--- /dev/null
+++ b/src/media/UAudioDecoder_FFmpeg.pas
@@ -0,0 +1,1159 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UAudioDecoder_FFmpeg;
+
+(*******************************************************************************
+ *
+ * This unit is primarily based upon -
+ * http://www.dranger.com/ffmpeg/ffmpegtutorial_all.html
+ *
+ * and tutorial03.c
+ *
+ * http://www.inb.uni-luebeck.de/~boehme/using_libavcodec.html
+ *
+ *******************************************************************************)
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+// show FFmpeg specific debug output
+{.$DEFINE DebugFFmpegDecode}
+
+// FFmpeg is very verbose and shows a bunch of errors.
+// Those errors (they can be considered as warnings by us) can be ignored
+// as they do not give any useful information.
+// There is no solution to fix this except for turning them off.
+{.$DEFINE EnableFFmpegErrorOutput}
+
+implementation
+
+uses
+ SDL, // SDL redefines some base types -> include before SysUtils to ignore them
+ Classes,
+ Math,
+ SysUtils,
+ avcodec,
+ avformat,
+ avutil,
+ avio,
+ mathematics, // used for av_rescale_q
+ rational,
+ UMusic,
+ UIni,
+ UMain,
+ UMediaCore_FFmpeg,
+ ULog,
+ UCommon,
+ UConfig,
+ UPath;
+
+const
+ MAX_AUDIOQ_SIZE = (5 * 16 * 1024);
+
+const
+ // TODO: The factor 3/2 might not be necessary as we do not need extra
+ // space for synchronizing as in the tutorial.
+ AUDIO_BUFFER_SIZE = (AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) div 2;
+
+type
+ TFFmpegDecodeStream = class(TAudioDecodeStream)
+ private
+ fStateLock: PSDL_Mutex;
+
+ fEOFState: boolean; // end-of-stream flag (locked by StateLock)
+ fErrorState: boolean; // error flag (locked by StateLock)
+
+ fQuitRequest: boolean; // (locked by StateLock)
+ fParserIdleCond: PSDL_Cond;
+
+ // parser pause/resume data
+ fParserLocked: boolean;
+ fParserPauseRequestCount: integer;
+ fParserUnlockedCond: PSDL_Cond;
+ fParserResumeCond: PSDL_Cond;
+
+ fSeekRequest: boolean; // (locked by StateLock)
+ fSeekFlags: integer; // (locked by StateLock)
+ fSeekPos: double; // stream position to seek for (in secs) (locked by StateLock)
+ fSeekFlush: boolean; // true if the buffers should be flushed after seeking (locked by StateLock)
+ SeekFinishedCond: PSDL_Cond;
+
+ fLoop: boolean; // (locked by StateLock)
+
+ fParseThread: PSDL_Thread;
+ fPacketQueue: TPacketQueue;
+
+ fFormatInfo: TAudioFormatInfo;
+
+ // FFmpeg specific data
+ fFormatCtx: PAVFormatContext;
+ fCodecCtx: PAVCodecContext;
+ fCodec: PAVCodec;
+
+ fAudioStreamIndex: integer;
+ fAudioStream: PAVStream;
+ fAudioStreamPos: double; // stream position in seconds (locked by DecoderLock)
+
+ // decoder pause/resume data
+ fDecoderLocked: boolean;
+ fDecoderPauseRequestCount: integer;
+ fDecoderUnlockedCond: PSDL_Cond;
+ fDecoderResumeCond: PSDL_Cond;
+
+ // state-vars for DecodeFrame (locked by DecoderLock)
+ fAudioPaket: TAVPacket;
+ fAudioPaketData: PByteArray;
+ fAudioPaketSize: integer;
+ fAudioPaketSilence: integer; // number of bytes of silence to return
+
+ // state-vars for AudioCallback (locked by DecoderLock)
+ fAudioBufferPos: integer;
+ fAudioBufferSize: integer;
+ fAudioBuffer: PByteArray;
+
+ fFilename: IPath;
+
+ procedure SetPositionIntern(Time: real; Flush: boolean; Blocking: boolean);
+ procedure SetEOF(State: boolean); {$IFDEF HasInline}inline;{$ENDIF}
+ procedure SetError(State: boolean); {$IFDEF HasInline}inline;{$ENDIF}
+ function IsSeeking(): boolean;
+ function IsQuit(): boolean;
+
+ procedure Reset();
+
+ procedure Parse();
+ function ParseLoop(): boolean;
+ procedure PauseParser();
+ procedure ResumeParser();
+
+ function DecodeFrame(Buffer: PByteArray; BufferSize: integer): integer;
+ procedure FlushCodecBuffers();
+ procedure PauseDecoder();
+ procedure ResumeDecoder();
+ public
+ constructor Create();
+ destructor Destroy(); override;
+
+ function Open(const Filename: IPath): boolean;
+ procedure Close(); override;
+
+ function GetLength(): real; override;
+ function GetAudioFormatInfo(): TAudioFormatInfo; override;
+ function GetPosition: real; override;
+ procedure SetPosition(Time: real); override;
+ function GetLoop(): boolean; override;
+ procedure SetLoop(Enabled: boolean); override;
+ function IsEOF(): boolean; override;
+ function IsError(): boolean; override;
+
+ function ReadData(Buffer: PByteArray; BufferSize: integer): integer; override;
+ end;
+
+type
+ TAudioDecoder_FFmpeg = class(TInterfacedObject, IAudioDecoder)
+ public
+ function GetName: string;
+
+ function InitializeDecoder(): boolean;
+ function FinalizeDecoder(): boolean;
+ function Open(const Filename: IPath): TAudioDecodeStream;
+ end;
+
+var
+ FFmpegCore: TMediaCore_FFmpeg;
+
+function ParseThreadMain(Data: Pointer): integer; cdecl; forward;
+
+
+{ TFFmpegDecodeStream }
+
+constructor TFFmpegDecodeStream.Create();
+begin
+ inherited Create();
+
+ fStateLock := SDL_CreateMutex();
+ fParserUnlockedCond := SDL_CreateCond();
+ fParserResumeCond := SDL_CreateCond();
+ fParserIdleCond := SDL_CreateCond();
+ SeekFinishedCond := SDL_CreateCond();
+ fDecoderUnlockedCond := SDL_CreateCond();
+ fDecoderResumeCond := SDL_CreateCond();
+
+ // according to the documentation of avcodec_decode_audio(2), sample-data
+ // should be aligned on a 16 byte boundary. Otherwise internal calls
+ // (e.g. to SSE or Altivec operations) might fail or lack performance on some
+ // CPUs. Although GetMem() in Delphi and FPC seems to use a 16 byte or higher
+ // alignment for buffers of this size (alignment depends on the size of the
+ // requested buffer), we will set the alignment explicitly as the minimum
+ // alignment used by Delphi and FPC is on an 8 byte boundary.
+ //
+ // Note: AudioBuffer was previously defined as a field of type TAudioBuffer
+ // (array[0..AUDIO_BUFFER_SIZE-1] of byte) and hence statically allocated.
+ // Fields of records are aligned different to memory allocated with GetMem(),
+ // aligning depending on the type but will be at least 2 bytes.
+ // AudioBuffer was not aligned to a 16 byte boundary. The {$ALIGN x} directive
+ // was not applicable as Delphi in contrast to FPC provides at most 8 byte
+ // alignment ({$ALIGN 16} is not supported) by this directive.
+ fAudioBuffer := GetAlignedMem(AUDIO_BUFFER_SIZE, 16);
+
+ Reset();
+end;
+
+procedure TFFmpegDecodeStream.Reset();
+begin
+ fParseThread := nil;
+
+ fEOFState := false;
+ fErrorState := false;
+ fLoop := false;
+ fQuitRequest := false;
+
+ fAudioPaketData := nil;
+ fAudioPaketSize := 0;
+ fAudioPaketSilence := 0;
+
+ fAudioBufferPos := 0;
+ fAudioBufferSize := 0;
+
+ fParserLocked := false;
+ fParserPauseRequestCount := 0;
+ fDecoderLocked := false;
+ fDecoderPauseRequestCount := 0;
+
+ FillChar(fAudioPaket, SizeOf(TAVPacket), 0);
+end;
+
+{*
+ * Frees the decode-stream data.
+ *}
+destructor TFFmpegDecodeStream.Destroy();
+begin
+ Close();
+
+ SDL_DestroyMutex(fStateLock);
+ SDL_DestroyCond(fParserUnlockedCond);
+ SDL_DestroyCond(fParserResumeCond);
+ SDL_DestroyCond(fParserIdleCond);
+ SDL_DestroyCond(SeekFinishedCond);
+ SDL_DestroyCond(fDecoderUnlockedCond);
+ SDL_DestroyCond(fDecoderResumeCond);
+
+ FreeAlignedMem(fAudioBuffer);
+
+ inherited;
+end;
+
+function TFFmpegDecodeStream.Open(const Filename: IPath): boolean;
+var
+ SampleFormat: TAudioSampleFormat;
+ AVResult: integer;
+begin
+ Result := false;
+
+ Close();
+ Reset();
+
+ if (not Filename.IsFile) then
+ begin
+ Log.LogError('Audio-file does not exist: "' + Filename.ToNative + '"', 'UAudio_FFmpeg');
+ Exit;
+ end;
+
+ Self.fFilename := Filename;
+
+ // use custom 'ufile' protocol for UTF-8 support
+ if (av_open_input_file(fFormatCtx, PAnsiChar('ufile:'+FileName.ToUTF8), nil, 0, nil) <> 0) then
+ begin
+ Log.LogError('av_open_input_file failed: "' + Filename.ToNative + '"', 'UAudio_FFmpeg');
+ Exit;
+ end;
+
+ // generate PTS values if they do not exist
+ fFormatCtx^.flags := fFormatCtx^.flags or AVFMT_FLAG_GENPTS;
+
+ // retrieve stream information
+ if (av_find_stream_info(fFormatCtx) < 0) then
+ begin
+ Log.LogError('av_find_stream_info failed: "' + Filename.ToNative + '"', 'UAudio_FFmpeg');
+ Close();
+ Exit;
+ end;
+
+ // FIXME: hack used by ffplay. Maybe should not use url_feof() to test for the end
+ fFormatCtx^.pb.eof_reached := 0;
+
+ {$IFDEF DebugFFmpegDecode}
+ dump_format(fFormatCtx, 0, PAnsiChar(Filename.ToNative), 0);
+ {$ENDIF}
+
+ fAudioStreamIndex := FFmpegCore.FindAudioStreamIndex(fFormatCtx);
+ if (fAudioStreamIndex < 0) then
+ begin
+ Log.LogError('FindAudioStreamIndex: No Audio-stream found "' + Filename.ToNative + '"', 'UAudio_FFmpeg');
+ Close();
+ Exit;
+ end;
+
+ //Log.LogStatus('AudioStreamIndex is: '+ inttostr(ffmpegStreamID), 'UAudio_FFmpeg');
+
+ fAudioStream := fFormatCtx.streams[fAudioStreamIndex];
+ fAudioStreamPos := 0;
+ fCodecCtx := fAudioStream^.codec;
+
+ // TODO: should we use this or not? Should we allow 5.1 channel audio?
+ (*
+ {$IF LIBAVCODEC_VERSION >= 51042000}
+ if (CodecCtx^.channels > 0) then
+ CodecCtx^.request_channels := Min(2, CodecCtx^.channels)
+ else
+ CodecCtx^.request_channels := 2;
+ {$IFEND}
+ *)
+
+ fCodec := avcodec_find_decoder(fCodecCtx^.codec_id);
+ if (fCodec = nil) then
+ begin
+ Log.LogError('Unsupported codec!', 'UAudio_FFmpeg');
+ fCodecCtx := nil;
+ Close();
+ Exit;
+ end;
+
+ // set debug options
+ fCodecCtx^.debug_mv := 0;
+ fCodecCtx^.debug := 0;
+
+ // detect bug-workarounds automatically
+ fCodecCtx^.workaround_bugs := FF_BUG_AUTODETECT;
+ // error resilience strategy (careful/compliant/agressive/very_aggressive)
+ //CodecCtx^.error_resilience := FF_ER_CAREFUL; //FF_ER_COMPLIANT;
+ // allow non spec compliant speedup tricks.
+ //CodecCtx^.flags2 := CodecCtx^.flags2 or CODEC_FLAG2_FAST;
+
+ // Note: avcodec_open() and avcodec_close() are not thread-safe and will
+ // fail if called concurrently by different threads.
+ FFmpegCore.LockAVCodec();
+ try
+ AVResult := avcodec_open(fCodecCtx, fCodec);
+ finally
+ FFmpegCore.UnlockAVCodec();
+ end;
+ if (AVResult < 0) then
+ begin
+ Log.LogError('avcodec_open failed!', 'UAudio_FFmpeg');
+ Close();
+ Exit;
+ end;
+
+ // now initialize the audio-format
+
+ if (not FFmpegCore.ConvertFFmpegToAudioFormat(fCodecCtx^.sample_fmt, SampleFormat)) then
+ begin
+ // try standard format
+ SampleFormat := asfS16;
+ end;
+ if fCodecCtx^.channels > 255 then
+ Log.LogStatus('Error: CodecCtx^.channels > 255', 'TFFmpegDecodeStream.Open');
+ fFormatInfo := TAudioFormatInfo.Create(
+ byte(fCodecCtx^.channels),
+ fCodecCtx^.sample_rate,
+ SampleFormat
+ );
+
+ fPacketQueue := TPacketQueue.Create();
+
+ // finally start the decode thread
+ fParseThread := SDL_CreateThread(@ParseThreadMain, Self);
+
+ Result := true;
+end;
+
+procedure TFFmpegDecodeStream.Close();
+var
+ ThreadResult: integer;
+begin
+ // wake threads waiting for packet-queue data
+ // Note: normally, there are no waiting threads. If there were waiting
+ // ones, they would block the audio-callback thread.
+ if (assigned(fPacketQueue)) then
+ fPacketQueue.Abort();
+
+ // send quit request (to parse-thread etc)
+ SDL_mutexP(fStateLock);
+ fQuitRequest := true;
+ SDL_CondBroadcast(fParserIdleCond);
+ SDL_mutexV(fStateLock);
+
+ // abort parse-thread
+ if (fParseThread <> nil) then
+ begin
+ // and wait until it terminates
+ SDL_WaitThread(fParseThread, ThreadResult);
+ fParseThread := nil;
+ end;
+
+ // Close the codec
+ if (fCodecCtx <> nil) then
+ begin
+ // avcodec_close() is not thread-safe
+ FFmpegCore.LockAVCodec();
+ try
+ avcodec_close(fCodecCtx);
+ finally
+ FFmpegCore.UnlockAVCodec();
+ end;
+ fCodecCtx := nil;
+ end;
+
+ // Close the video file
+ if (fFormatCtx <> nil) then
+ begin
+ av_close_input_file(fFormatCtx);
+ fFormatCtx := nil;
+ end;
+
+ PerformOnClose();
+
+ FreeAndNil(fPacketQueue);
+ FreeAndNil(fFormatInfo);
+end;
+
+function TFFmpegDecodeStream.GetLength(): real;
+begin
+ // do not forget to consider the start_time value here
+ // there is a type size mismatch warnign because start_time and duration are cint64.
+ // So, in principle there could be an overflow when doing the sum.
+ Result := (fFormatCtx^.start_time + fFormatCtx^.duration) / AV_TIME_BASE;
+end;
+
+function TFFmpegDecodeStream.GetAudioFormatInfo(): TAudioFormatInfo;
+begin
+ Result := fFormatInfo;
+end;
+
+function TFFmpegDecodeStream.IsEOF(): boolean;
+begin
+ SDL_mutexP(fStateLock);
+ Result := fEOFState;
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.SetEOF(State: boolean);
+begin
+ SDL_mutexP(fStateLock);
+ fEOFState := State;
+ SDL_mutexV(fStateLock);
+end;
+
+function TFFmpegDecodeStream.IsError(): boolean;
+begin
+ SDL_mutexP(fStateLock);
+ Result := fErrorState;
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.SetError(State: boolean);
+begin
+ SDL_mutexP(fStateLock);
+ fErrorState := State;
+ SDL_mutexV(fStateLock);
+end;
+
+function TFFmpegDecodeStream.IsSeeking(): boolean;
+begin
+ SDL_mutexP(fStateLock);
+ Result := fSeekRequest;
+ SDL_mutexV(fStateLock);
+end;
+
+function TFFmpegDecodeStream.IsQuit(): boolean;
+begin
+ SDL_mutexP(fStateLock);
+ Result := fQuitRequest;
+ SDL_mutexV(fStateLock);
+end;
+
+function TFFmpegDecodeStream.GetPosition(): real;
+var
+ BufferSizeSec: double;
+begin
+ PauseDecoder();
+
+ // ReadData() does not return all of the buffer retrieved by DecodeFrame().
+ // Determine the size of the unused part of the decode-buffer.
+ BufferSizeSec := (fAudioBufferSize - fAudioBufferPos) /
+ fFormatInfo.BytesPerSec;
+
+ // subtract the size of unused buffer-data from the audio clock.
+ Result := fAudioStreamPos - BufferSizeSec;
+
+ ResumeDecoder();
+end;
+
+procedure TFFmpegDecodeStream.SetPosition(Time: real);
+begin
+ SetPositionIntern(Time, true, true);
+end;
+
+function TFFmpegDecodeStream.GetLoop(): boolean;
+begin
+ SDL_mutexP(fStateLock);
+ Result := fLoop;
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.SetLoop(Enabled: boolean);
+begin
+ SDL_mutexP(fStateLock);
+ fLoop := Enabled;
+ SDL_mutexV(fStateLock);
+end;
+
+
+(********************************************
+ * Parser section
+ ********************************************)
+
+procedure TFFmpegDecodeStream.PauseParser();
+begin
+ if (SDL_ThreadID() = fParseThread.threadid) then
+ Exit;
+
+ SDL_mutexP(fStateLock);
+ Inc(fParserPauseRequestCount);
+ while (fParserLocked) do
+ SDL_CondWait(fParserUnlockedCond, fStateLock);
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.ResumeParser();
+begin
+ if (SDL_ThreadID() = fParseThread.threadid) then
+ Exit;
+
+ SDL_mutexP(fStateLock);
+ Dec(fParserPauseRequestCount);
+ SDL_CondSignal(fParserResumeCond);
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.SetPositionIntern(Time: real; Flush: boolean; Blocking: boolean);
+begin
+ // - Pause the parser first to prevent it from putting obsolete packages
+ // into the queue after the queue was flushed and before seeking is done.
+ // Otherwise we will hear fragments of old data, if the stream was seeked
+ // in stopped mode and resumed afterwards (applies to non-blocking mode only).
+ // - Pause the decoder to avoid race-condition that might occur otherwise.
+ // - Last lock the state lock because we are manipulating some shared state-vars.
+ PauseParser();
+ PauseDecoder();
+ SDL_mutexP(fStateLock);
+ try
+ fEOFState := false;
+ fErrorState := false;
+
+ // do not seek if we are already at the correct position.
+ // This is important especially for seeking to position 0 if we already are
+ // at the beginning. Although seeking with AVSEEK_FLAG_BACKWARD for pos 0 works,
+ // it is still a bit choppy (although much better than w/o AVSEEK_FLAG_BACKWARD).
+ if (Time = fAudioStreamPos) then
+ Exit;
+
+ // configure seek parameters
+ fSeekPos := Time;
+ fSeekFlush := Flush;
+ fSeekFlags := AVSEEK_FLAG_ANY;
+ fSeekRequest := true;
+
+ // Note: the BACKWARD-flag seeks to the first position <= the position
+ // searched for. Otherwise e.g. position 0 might not be seeked correct.
+ // For some reason ffmpeg sometimes doesn't use position 0 but the key-frame
+ // following. In streams with few key-frames (like many flv-files) the next
+ // key-frame after 0 might be 5secs ahead.
+ if (Time <= fAudioStreamPos) then
+ fSeekFlags := fSeekFlags or AVSEEK_FLAG_BACKWARD;
+
+ // send a reuse signal in case the parser was stopped (e.g. because of an EOF)
+ SDL_CondSignal(fParserIdleCond);
+ finally
+ SDL_mutexV(fStateLock);
+ ResumeDecoder();
+ ResumeParser();
+ end;
+
+ // in blocking mode, wait until seeking is done
+ if (Blocking) then
+ begin
+ SDL_mutexP(fStateLock);
+ while (fSeekRequest) do
+ SDL_CondWait(SeekFinishedCond, fStateLock);
+ SDL_mutexV(fStateLock);
+ end;
+end;
+
+function ParseThreadMain(Data: Pointer): integer; cdecl;
+var
+ Stream: TFFmpegDecodeStream;
+begin
+ Stream := TFFmpegDecodeStream(Data);
+ if (Stream <> nil) then
+ Stream.Parse();
+ Result := 0;
+end;
+
+procedure TFFmpegDecodeStream.Parse();
+begin
+ // reuse thread as long as the stream is not terminated
+ while (ParseLoop()) do
+ begin
+ // wait for reuse or destruction of stream
+ SDL_mutexP(fStateLock);
+ while (not (fSeekRequest or fQuitRequest)) do
+ SDL_CondWait(fParserIdleCond, fStateLock);
+ SDL_mutexV(fStateLock);
+ end;
+end;
+
+(**
+ * Parser main loop.
+ * Will not return until parsing of the stream is finished.
+ * Reasons for the parser to return are:
+ * - the end-of-file is reached
+ * - an error occured
+ * - the stream was quited (received a quit-request)
+ * Returns true if the stream can be resumed or false if the stream has to
+ * be terminated.
+ *)
+function TFFmpegDecodeStream.ParseLoop(): boolean;
+var
+ Packet: TAVPacket;
+ SeekTarget: int64;
+ ByteIOCtx: PByteIOContext;
+ ErrorCode: integer;
+ StartSilence: double; // duration of silence at start of stream
+ StartSilencePtr: PDouble; // pointer for the EMPTY status packet
+
+ // Note: pthreads wakes threads waiting on a mutex in the order of their
+ // priority and not in FIFO order. SDL does not provide any option to
+ // control priorities. This might (and already did) starve threads waiting
+ // on the mutex (e.g. SetPosition) making usdx look like it was froozen.
+ // Instead of simply locking the critical section we set a ParserLocked flag
+ // instead and give priority to the threads requesting the parser to pause.
+ procedure LockParser();
+ begin
+ SDL_mutexP(fStateLock);
+ while (fParserPauseRequestCount > 0) do
+ SDL_CondWait(fParserResumeCond, fStateLock);
+ fParserLocked := true;
+ SDL_mutexV(fStateLock);
+ end;
+
+ procedure UnlockParser();
+ begin
+ SDL_mutexP(fStateLock);
+ fParserLocked := false;
+ SDL_CondBroadcast(fParserUnlockedCond);
+ SDL_mutexV(fStateLock);
+ end;
+
+begin
+ Result := true;
+
+ while (true) do
+ begin
+ LockParser();
+ try
+
+ if (IsQuit()) then
+ begin
+ Result := false;
+ Exit;
+ end;
+
+ // handle seek-request (Note: no need to lock SeekRequest here)
+ if (fSeekRequest) then
+ begin
+ // first try: seek on the audio stream
+ SeekTarget := Round(fSeekPos / av_q2d(fAudioStream^.time_base));
+ StartSilence := 0;
+ if (SeekTarget < fAudioStream^.start_time) then
+ StartSilence := (fAudioStream^.start_time - SeekTarget) * av_q2d(fAudioStream^.time_base);
+ ErrorCode := av_seek_frame(fFormatCtx, fAudioStreamIndex, SeekTarget, fSeekFlags);
+
+ if (ErrorCode < 0) then
+ begin
+ // second try: seek on the default stream (necessary for flv-videos and some ogg-files)
+ SeekTarget := Round(fSeekPos * AV_TIME_BASE);
+ StartSilence := 0;
+ if (SeekTarget < fFormatCtx^.start_time) then
+ StartSilence := (fFormatCtx^.start_time - SeekTarget) / AV_TIME_BASE;
+ ErrorCode := av_seek_frame(fFormatCtx, -1, SeekTarget, fSeekFlags);
+ end;
+
+ // pause decoder and lock state (keep the lock-order to avoid deadlocks).
+ // Note that the decoder does not block in the packet-queue in seeking state,
+ // so locking the decoder here does not cause a dead-lock.
+ PauseDecoder();
+ SDL_mutexP(fStateLock);
+ try
+ if (ErrorCode < 0) then
+ begin
+ // seeking failed
+ fErrorState := true;
+ Log.LogError('Seek Error in "'+fFormatCtx^.filename+'"', 'UAudioDecoder_FFmpeg');
+ end
+ else
+ begin
+ if (fSeekFlush) then
+ begin
+ // flush queue (we will send a Flush-Packet when seeking is finished)
+ fPacketQueue.Flush();
+
+ // flush the decode buffers
+ fAudioBufferSize := 0;
+ fAudioBufferPos := 0;
+ fAudioPaketSize := 0;
+ fAudioPaketSilence := 0;
+ FlushCodecBuffers();
+
+ // Set preliminary stream position. The position will be set to
+ // the correct value as soon as the first packet is decoded.
+ fAudioStreamPos := fSeekPos;
+ end
+ else
+ begin
+ // request avcodec buffer flush
+ fPacketQueue.PutStatus(PKT_STATUS_FLAG_FLUSH, nil);
+ end;
+
+ // fill the gap between position 0 and start_time with silence
+ // but not if we are in loop mode
+ if ((StartSilence > 0) and (not fLoop)) then
+ begin
+ GetMem(StartSilencePtr, SizeOf(StartSilence));
+ StartSilencePtr^ := StartSilence;
+ fPacketQueue.PutStatus(PKT_STATUS_FLAG_EMPTY, StartSilencePtr);
+ end;
+ end;
+
+ fSeekRequest := false;
+ SDL_CondBroadcast(SeekFinishedCond);
+ finally
+ SDL_mutexV(fStateLock);
+ ResumeDecoder();
+ end;
+ end;
+
+ if (fPacketQueue.GetSize() > MAX_AUDIOQ_SIZE) then
+ begin
+ SDL_Delay(10);
+ Continue;
+ end;
+
+ if (av_read_frame(fFormatCtx, Packet) < 0) then
+ begin
+ // failed to read a frame, check reason
+ {$IF (LIBAVFORMAT_VERSION_MAJOR >= 52)}
+ ByteIOCtx := fFormatCtx^.pb;
+ {$ELSE}
+ ByteIOCtx := @fFormatCtx^.pb;
+ {$IFEND}
+
+ // check for end-of-file (eof is not an error)
+ if (url_feof(ByteIOCtx) <> 0) then
+ begin
+ if (GetLoop()) then
+ begin
+ // rewind stream (but do not flush)
+ SetPositionIntern(0, false, false);
+ Continue;
+ end
+ else
+ begin
+ // signal end-of-file
+ fPacketQueue.PutStatus(PKT_STATUS_FLAG_EOF, nil);
+ Exit;
+ end;
+ end;
+
+ // check for errors
+ if (url_ferror(ByteIOCtx) <> 0) then
+ begin
+ // an error occured -> abort and wait for repositioning or termination
+ fPacketQueue.PutStatus(PKT_STATUS_FLAG_ERROR, nil);
+ Exit;
+ end;
+
+ // url_feof() does not detect an EOF for some files
+ // so we have to do it this way.
+ if ((fFormatCtx^.file_size <> 0) and
+ (ByteIOCtx^.pos >= fFormatCtx^.file_size)) then
+ begin
+ fPacketQueue.PutStatus(PKT_STATUS_FLAG_EOF, nil);
+ Exit;
+ end;
+
+ // unknown error occured, exit
+ fPacketQueue.PutStatus(PKT_STATUS_FLAG_ERROR, nil);
+ Exit;
+ end;
+
+ if (Packet.stream_index = fAudioStreamIndex) then
+ fPacketQueue.Put(@Packet)
+ else
+ av_free_packet(@Packet);
+
+ finally
+ UnlockParser();
+ end;
+ end;
+end;
+
+
+(********************************************
+ * Decoder section
+ ********************************************)
+
+procedure TFFmpegDecodeStream.PauseDecoder();
+begin
+ SDL_mutexP(fStateLock);
+ Inc(fDecoderPauseRequestCount);
+ while (fDecoderLocked) do
+ SDL_CondWait(fDecoderUnlockedCond, fStateLock);
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.ResumeDecoder();
+begin
+ SDL_mutexP(fStateLock);
+ Dec(fDecoderPauseRequestCount);
+ SDL_CondSignal(fDecoderResumeCond);
+ SDL_mutexV(fStateLock);
+end;
+
+procedure TFFmpegDecodeStream.FlushCodecBuffers();
+begin
+ // if no flush operation is specified, avcodec_flush_buffers will not do anything.
+ if (@fCodecCtx.codec.flush <> nil) then
+ begin
+ // flush buffers used by avcodec_decode_audio, etc.
+ avcodec_flush_buffers(fCodecCtx);
+ end
+ else
+ begin
+ // we need a Workaround to avoid plopping noise with ogg-vorbis and
+ // mp3 (in older versions of FFmpeg).
+ // We will just reopen the codec.
+ FFmpegCore.LockAVCodec();
+ try
+ avcodec_close(fCodecCtx);
+ avcodec_open(fCodecCtx, fCodec);
+ finally
+ FFmpegCore.UnlockAVCodec();
+ end;
+ end;
+end;
+
+function TFFmpegDecodeStream.DecodeFrame(Buffer: PByteArray; BufferSize: integer): integer;
+var
+ PaketDecodedSize: integer; // size of packet data used for decoding
+ DataSize: integer; // size of output data decoded by FFmpeg
+ BlockQueue: boolean;
+ SilenceDuration: double;
+ {$IFDEF DebugFFmpegDecode}
+ TmpPos: double;
+ {$ENDIF}
+begin
+ Result := -1;
+
+ if (EOF) then
+ Exit;
+
+ while(true) do
+ begin
+ // for titles with start_time > 0 we have to generate silence
+ // until we reach the pts of the first data packet.
+ if (fAudioPaketSilence > 0) then
+ begin
+ DataSize := Min(fAudioPaketSilence, BufferSize);
+ FillChar(Buffer[0], DataSize, 0);
+ Dec(fAudioPaketSilence, DataSize);
+ fAudioStreamPos := fAudioStreamPos + DataSize / fFormatInfo.BytesPerSec;
+ Result := DataSize;
+ Exit;
+ end;
+
+ // read packet data
+ while (fAudioPaketSize > 0) do
+ begin
+ DataSize := BufferSize;
+
+ {$IF LIBAVCODEC_VERSION >= 51030000} // 51.30.0
+ PaketDecodedSize := avcodec_decode_audio2(fCodecCtx, PSmallint(Buffer),
+ DataSize, fAudioPaketData, fAudioPaketSize);
+ {$ELSE}
+ PaketDecodedSize := avcodec_decode_audio(fCodecCtx, PSmallint(Buffer),
+ DataSize, fAudioPaketData, fAudioPaketSize);
+ {$IFEND}
+
+ if(PaketDecodedSize < 0) then
+ begin
+ // if error, skip frame
+ {$IFDEF DebugFFmpegDecode}
+ DebugWriteln('Skip audio frame');
+ {$ENDIF}
+ fAudioPaketSize := 0;
+ Break;
+ end;
+
+ Inc(PByte(fAudioPaketData), PaketDecodedSize);
+ Dec(fAudioPaketSize, PaketDecodedSize);
+
+ // check if avcodec_decode_audio returned data, otherwise fetch more frames
+ if (DataSize <= 0) then
+ Continue;
+
+ // update stream position by the amount of fetched data
+ fAudioStreamPos := fAudioStreamPos + DataSize / fFormatInfo.BytesPerSec;
+
+ // we have data, return it and come back for more later
+ Result := DataSize;
+ Exit;
+ end;
+
+ // free old packet data
+ if (fAudioPaket.data <> nil) then
+ av_free_packet(@fAudioPaket);
+
+ // do not block queue on seeking (to avoid deadlocks on the DecoderLock)
+ if (IsSeeking()) then
+ BlockQueue := false
+ else
+ BlockQueue := true;
+
+ // request a new packet and block if none available.
+ // If this fails, the queue was aborted.
+ if (fPacketQueue.Get(fAudioPaket, BlockQueue) <= 0) then
+ Exit;
+
+ // handle Status-packet
+ if (PAnsiChar(fAudioPaket.data) = STATUS_PACKET) then
+ begin
+ fAudioPaket.data := nil;
+ fAudioPaketData := nil;
+ fAudioPaketSize := 0;
+
+ case (fAudioPaket.flags) of
+ PKT_STATUS_FLAG_FLUSH:
+ begin
+ // just used if SetPositionIntern was called without the flush flag.
+ FlushCodecBuffers;
+ end;
+ PKT_STATUS_FLAG_EOF: // end-of-file
+ begin
+ // ignore EOF while seeking
+ if (not IsSeeking()) then
+ SetEOF(true);
+ // buffer contains no data -> result = -1
+ Exit;
+ end;
+ PKT_STATUS_FLAG_ERROR:
+ begin
+ SetError(true);
+ Log.LogStatus('I/O Error', 'TFFmpegDecodeStream.DecodeFrame');
+ Exit;
+ end;
+ PKT_STATUS_FLAG_EMPTY:
+ begin
+ SilenceDuration := PDouble(fPacketQueue.GetStatusInfo(fAudioPaket))^;
+ fAudioPaketSilence := Round(SilenceDuration * fFormatInfo.SampleRate) * fFormatInfo.FrameSize;
+ fPacketQueue.FreeStatusInfo(fAudioPaket);
+ end
+ else
+ begin
+ Log.LogStatus('Unknown status', 'TFFmpegDecodeStream.DecodeFrame');
+ end;
+ end;
+
+ Continue;
+ end;
+
+ fAudioPaketData := fAudioPaket.data;
+ fAudioPaketSize := fAudioPaket.size;
+
+ // if available, update the stream position to the presentation time of this package
+ if(fAudioPaket.pts <> AV_NOPTS_VALUE) then
+ begin
+ {$IFDEF DebugFFmpegDecode}
+ TmpPos := fAudioStreamPos;
+ {$ENDIF}
+ fAudioStreamPos := av_q2d(fAudioStream^.time_base) * fAudioPaket.pts;
+ {$IFDEF DebugFFmpegDecode}
+ DebugWriteln('Timestamp: ' + floattostrf(fAudioStreamPos, ffFixed, 15, 3) + ' ' +
+ '(Calc: ' + floattostrf(TmpPos, ffFixed, 15, 3) + '), ' +
+ 'Diff: ' + floattostrf(fAudioStreamPos-TmpPos, ffFixed, 15, 3));
+ {$ENDIF}
+ end;
+ end;
+end;
+
+function TFFmpegDecodeStream.ReadData(Buffer: PByteArray; BufferSize: integer): integer;
+var
+ CopyByteCount: integer; // number of bytes to copy
+ RemainByteCount: integer; // number of bytes left (remain) to read
+ BufferPos: integer;
+
+ // prioritize pause requests
+ procedure LockDecoder();
+ begin
+ SDL_mutexP(fStateLock);
+ while (fDecoderPauseRequestCount > 0) do
+ SDL_CondWait(fDecoderResumeCond, fStateLock);
+ fDecoderLocked := true;
+ SDL_mutexV(fStateLock);
+ end;
+
+ procedure UnlockDecoder();
+ begin
+ SDL_mutexP(fStateLock);
+ fDecoderLocked := false;
+ SDL_CondBroadcast(fDecoderUnlockedCond);
+ SDL_mutexV(fStateLock);
+ end;
+
+begin
+ Result := -1;
+
+ // set number of bytes to copy to the output buffer
+ BufferPos := 0;
+
+ LockDecoder();
+ try
+ // leave if end-of-file is reached
+ if (EOF) then
+ Exit;
+
+ // copy data to output buffer
+ while (BufferPos < BufferSize) do
+ begin
+ // check if we need more data
+ if (fAudioBufferPos >= fAudioBufferSize) then
+ begin
+ fAudioBufferPos := 0;
+
+ // we have already sent all our data; get more
+ fAudioBufferSize := DecodeFrame(fAudioBuffer, AUDIO_BUFFER_SIZE);
+
+ // check for errors or EOF
+ if(fAudioBufferSize < 0) then
+ begin
+ Result := BufferPos;
+ Exit;
+ end;
+ end;
+
+ // calc number of new bytes in the decode-buffer
+ CopyByteCount := fAudioBufferSize - fAudioBufferPos;
+ // resize copy-count if more bytes available than needed (remaining bytes are used the next time)
+ RemainByteCount := BufferSize - BufferPos;
+ if (CopyByteCount > RemainByteCount) then
+ CopyByteCount := RemainByteCount;
+
+ Move(fAudioBuffer[fAudioBufferPos], Buffer[BufferPos], CopyByteCount);
+
+ Inc(BufferPos, CopyByteCount);
+ Inc(fAudioBufferPos, CopyByteCount);
+ end;
+ finally
+ UnlockDecoder();
+ end;
+
+ Result := BufferSize;
+end;
+
+
+{ TAudioDecoder_FFmpeg }
+
+function TAudioDecoder_FFmpeg.GetName: String;
+begin
+ Result := 'FFmpeg_Decoder';
+end;
+
+function TAudioDecoder_FFmpeg.InitializeDecoder: boolean;
+begin
+ //Log.LogStatus('InitializeDecoder', 'UAudioDecoder_FFmpeg');
+ FFmpegCore := TMediaCore_FFmpeg.GetInstance();
+ av_register_all();
+
+ // Do not show uninformative error messages by default.
+ // FFmpeg prints all error-infos on the console by default what
+ // is very confusing as the playback of the files is correct.
+ // We consider these errors to be internal to FFMpeg. They can be fixed
+ // by the FFmpeg guys only and do not provide any useful information in
+ // respect to USDX.
+ {$IFNDEF EnableFFmpegErrorOutput}
+ {$IF LIBAVUTIL_VERSION_MAJOR >= 50}
+ av_log_set_level(AV_LOG_FATAL);
+ {$ELSE}
+ // FATAL and ERROR share one log-level, so we have to use QUIET
+ av_log_set_level(AV_LOG_QUIET);
+ {$IFEND}
+ {$ENDIF}
+
+ Result := true;
+end;
+
+function TAudioDecoder_FFmpeg.FinalizeDecoder(): boolean;
+begin
+ Result := true;
+end;
+
+function TAudioDecoder_FFmpeg.Open(const Filename: IPath): TAudioDecodeStream;
+var
+ Stream: TFFmpegDecodeStream;
+begin
+ Result := nil;
+
+ Stream := TFFmpegDecodeStream.Create();
+ if (not Stream.Open(Filename)) then
+ begin
+ Stream.Free;
+ Exit;
+ end;
+
+ Result := Stream;
+end;
+
+
+initialization
+ MediaManager.Add(TAudioDecoder_FFmpeg.Create);
+
+end.
diff --git a/src/media/UAudioInput_Bass.pas b/src/media/UAudioInput_Bass.pas
new file mode 100644
index 00000000..0e79b343
--- /dev/null
+++ b/src/media/UAudioInput_Bass.pas
@@ -0,0 +1,519 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UAudioInput_Bass;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+uses
+ Classes,
+ SysUtils,
+ URecord,
+ UMusic;
+
+implementation
+
+uses
+ UMain,
+ UIni,
+ ULog,
+ UAudioCore_Bass,
+ UTextEncoding,
+ UCommon, // (Note: for MakeLong on non-windows platforms)
+ {$IFDEF MSWINDOWS}
+ Windows, // (Note: for MakeLong)
+ {$ENDIF}
+ bass; // (Note: DWORD is redefined here -> insert after Windows-unit)
+
+type
+ TAudioInput_Bass = class(TAudioInputBase)
+ private
+ function EnumDevices(): boolean;
+ public
+ function GetName: String; override;
+ function InitializeRecord: boolean; override;
+ function FinalizeRecord: boolean; override;
+ end;
+
+ TBassInputDevice = class(TAudioInputDevice)
+ private
+ RecordStream: HSTREAM;
+ BassDeviceID: DWORD; // DeviceID used by BASS
+ SingleIn: boolean;
+
+ function SetInputSource(SourceIndex: integer): boolean;
+ function GetInputSource(): integer;
+ public
+ function Open(): boolean;
+ function Close(): boolean;
+ function Start(): boolean; override;
+ function Stop(): boolean; override;
+
+ function GetVolume(): single; override;
+ procedure SetVolume(Volume: single); override;
+ end;
+
+var
+ BassCore: TAudioCore_Bass;
+
+
+{ Global }
+
+{*
+ * Bass input capture callback.
+ * Params:
+ * stream - BASS input stream
+ * buffer - buffer of captured samples
+ * len - size of buffer in bytes
+ * user - players associated with left/right channels
+ *}
+function MicrophoneCallback(stream: HSTREAM; buffer: Pointer;
+ len: integer; inputDevice: Pointer): boolean; {$IFDEF MSWINDOWS}stdcall;{$ELSE}cdecl;{$ENDIF}
+begin
+ AudioInputProcessor.HandleMicrophoneData(buffer, len, inputDevice);
+ Result := true;
+end;
+
+
+{ TBassInputDevice }
+
+function TBassInputDevice.GetInputSource(): integer;
+var
+ SourceCnt: integer;
+ i: integer;
+ flags: DWORD;
+begin
+ // get input-source config (subtract virtual device to get BASS indices)
+ SourceCnt := Length(Source)-1;
+
+ // find source
+ Result := -1;
+ for i := 0 to SourceCnt-1 do
+ begin
+ // get input settings
+ flags := BASS_RecordGetInput(i, PSingle(nil)^);
+ if (flags = DWORD(-1)) then
+ begin
+ Log.LogError('BASS_RecordGetInput: ' + BassCore.ErrorGetString(), 'TBassInputDevice.GetInputSource');
+ Exit;
+ end;
+
+ // check if current source is selected
+ if ((flags and BASS_INPUT_OFF) = 0) then
+ begin
+ // selected source found
+ Result := i;
+ Exit;
+ end;
+ end;
+end;
+
+function TBassInputDevice.SetInputSource(SourceIndex: integer): boolean;
+var
+ SourceCnt: integer;
+ i: integer;
+ flags: DWORD;
+begin
+ Result := false;
+
+ // check for invalid source index
+ if (SourceIndex < 0) then
+ Exit;
+
+ // get input-source config (subtract virtual device to get BASS indices)
+ SourceCnt := Length(Source)-1;
+
+ // 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: ' + BassCore.ErrorGetString(), 'TBassInputDevice.Start');
+ Exit;
+ end;
+
+ // turn off all other sources (not needed for single-in devices)
+ if (not SingleIn) then
+ begin
+ for i := 0 to SourceCnt-1 do
+ begin
+ if (i = SourceIndex) then
+ continue;
+ // get input settings
+ flags := BASS_RecordGetInput(i, PSingle(nil)^);
+ if (flags = DWORD(-1)) then
+ begin
+ Log.LogError('BASS_RecordGetInput: ' + BassCore.ErrorGetString(), 'TBassInputDevice.GetInputSource');
+ Exit;
+ end;
+ // deselect source if selected
+ if ((flags and BASS_INPUT_OFF) = 0) then
+ BASS_RecordSetInput(i, BASS_INPUT_OFF, -1);
+ end;
+ end;
+
+ Result := true;
+end;
+
+function TBassInputDevice.Open(): boolean;
+var
+ FormatFlags: DWORD;
+ SourceIndex: integer;
+const
+ latency = 20; // 20ms callback period (= latency)
+begin
+ Result := false;
+
+ if (not BASS_RecordInit(BassDeviceID)) then
+ begin
+ Log.LogError('BASS_RecordInit['+Name+']: ' +
+ BassCore.ErrorGetString(), 'TBassInputDevice.Open');
+ Exit;
+ end;
+
+ if (not BassCore.ConvertAudioFormatToBASSFlags(AudioFormat.Format, FormatFlags)) then
+ begin
+ Log.LogError('Unhandled sample-format', 'TBassInputDevice.Open');
+ Exit;
+ end;
+
+ // start capturing in paused state
+ RecordStream := BASS_RecordStart(Round(AudioFormat.SampleRate), AudioFormat.Channels,
+ MakeLong(FormatFlags or BASS_RECORD_PAUSE, latency),
+ @MicrophoneCallback, Self);
+ if (RecordStream = 0) then
+ begin
+ Log.LogError('BASS_RecordStart: ' + BassCore.ErrorGetString(), 'TBassInputDevice.Open');
+ BASS_RecordFree;
+ Exit;
+ end;
+
+ // save current source selection and select new source
+ SourceIndex := Ini.InputDeviceConfig[CfgIndex].Input-1;
+ if (SourceIndex = -1) then
+ begin
+ // nothing to do if default source is used
+ SourceRestore := -1;
+ end
+ else
+ begin
+ // store current source-index and select new source
+ SourceRestore := GetInputSource();
+ SetInputSource(SourceIndex);
+ end;
+
+ Result := true;
+end;
+
+{* Start input-capturing on this device. *}
+function TBassInputDevice.Start(): boolean;
+begin
+ Result := false;
+
+ // recording already started -> stop first
+ if (RecordStream <> 0) then
+ Stop();
+
+ // TODO: Do not open the device here (takes too much time).
+ if not Open() then
+ Exit;
+
+ if (not BASS_ChannelPlay(RecordStream, true)) then
+ begin
+ Log.LogError('BASS_ChannelPlay: ' + BassCore.ErrorGetString(), 'TBassInputDevice.Start');
+ Exit;
+ end;
+
+ Result := true;
+end;
+
+{* Stop input-capturing on this device. *}
+function TBassInputDevice.Stop(): boolean;
+begin
+ Result := false;
+
+ if (RecordStream = 0) then
+ Exit;
+ if (not BASS_RecordSetDevice(BassDeviceID)) then
+ Exit;
+
+ if (not BASS_ChannelStop(RecordStream)) then
+ begin
+ Log.LogError('BASS_ChannelStop: ' + BassCore.ErrorGetString(), 'TBassInputDevice.Stop');
+ end;
+
+ // TODO: Do not close the device here (takes too much time).
+ Result := Close();
+end;
+
+function TBassInputDevice.Close(): boolean;
+begin
+ // restore source selection
+ if (SourceRestore >= 0) then
+ begin
+ SetInputSource(SourceRestore);
+ end;
+
+ // free data
+ if (not BASS_RecordFree()) then
+ begin
+ Log.LogError('BASS_RecordFree: ' + BassCore.ErrorGetString(), 'TBassInputDevice.Close');
+ Result := false;
+ end
+ else
+ begin
+ Result := true;
+ end;
+
+ RecordStream := 0;
+end;
+
+function TBassInputDevice.GetVolume(): single;
+var
+ SourceIndex: integer;
+ lVolume: Single;
+begin
+ Result := 0;
+
+ SourceIndex := Ini.InputDeviceConfig[CfgIndex].Input-1;
+ if (SourceIndex = -1) then
+ begin
+ // if default source used find selected source
+ SourceIndex := GetInputSource();
+ if (SourceIndex = -1) then
+ Exit;
+ end;
+
+ if (BASS_RecordGetInput(SourceIndex, lVolume) = DWORD(-1)) then
+ begin
+ Log.LogError('BASS_RecordGetInput: ' + BassCore.ErrorGetString() , 'TBassInputDevice.GetVolume');
+ Exit;
+ end;
+ Result := lVolume;
+end;
+
+procedure TBassInputDevice.SetVolume(Volume: single);
+var
+ SourceIndex: integer;
+begin
+ SourceIndex := Ini.InputDeviceConfig[CfgIndex].Input-1;
+ if (SourceIndex = -1) then
+ begin
+ // if default source used find selected source
+ SourceIndex := GetInputSource();
+ if (SourceIndex = -1) then
+ Exit;
+ end;
+
+ // clip volume to valid range
+ if (Volume > 1.0) then
+ Volume := 1.0
+ else if (Volume < 0) then
+ Volume := 0;
+
+ if (not BASS_RecordSetInput(SourceIndex, 0, Volume)) then
+ begin
+ Log.LogError('BASS_RecordSetInput: ' + BassCore.ErrorGetString() , 'TBassInputDevice.SetVolume');
+ end;
+end;
+
+
+{ TAudioInput_Bass }
+
+function TAudioInput_Bass.GetName: String;
+begin
+ result := 'BASS_Input';
+end;
+
+function TAudioInput_Bass.EnumDevices(): boolean;
+var
+ Descr: UTF8String;
+ SourceName: PChar;
+ Flags: integer;
+ BassDeviceID: integer;
+ BassDevice: TBassInputDevice;
+ DeviceIndex: integer;
+ DeviceInfo: BASS_DEVICEINFO;
+ SourceIndex: integer;
+ RecordInfo: BASS_RECORDINFO;
+ SelectedSourceIndex: integer;
+begin
+ result := false;
+
+ DeviceIndex := 0;
+ BassDeviceID := 0;
+ SetLength(AudioInputProcessor.DeviceList, 0);
+
+ // checks for recording devices and puts them into an array
+ while true do
+ begin
+ if (not BASS_RecordGetDeviceInfo(BassDeviceID, DeviceInfo)) then
+ break;
+
+ // try to initialize the device
+ if not BASS_RecordInit(BassDeviceID) then
+ begin
+ Log.LogStatus('Failed to initialize BASS Capture-Device['+inttostr(BassDeviceID)+']',
+ 'TAudioInput_Bass.InitializeRecord');
+ end
+ else
+ begin
+ SetLength(AudioInputProcessor.DeviceList, DeviceIndex+1);
+
+ // TODO: free object on termination
+ BassDevice := TBassInputDevice.Create();
+ AudioInputProcessor.DeviceList[DeviceIndex] := BassDevice;
+
+ BassDevice.BassDeviceID := BassDeviceID;
+
+ // BASS device names seem to be encoded with local encoding
+ // TODO: works for windows, check Linux + Mac OS X
+ Descr := DecodeStringUTF8(DeviceInfo.name, encLocale);
+
+ BassDevice.Name := UnifyDeviceName(Descr, DeviceIndex);
+
+ // zero info-struct as some fields might not be set (e.g. freq is just set on Vista and MacOSX)
+ FillChar(RecordInfo, SizeOf(RecordInfo), 0);
+ // retrieve recording device info
+ BASS_RecordGetInfo(RecordInfo);
+
+ // check if BASS has capture-freq. info
+ if (RecordInfo.freq > 0) then
+ begin
+ // use current input sample rate (available only on Windows Vista and OSX).
+ // Recording at this rate will give the best quality and performance, as no resampling is required.
+ // FIXME: does BASS use LSB/MSB or system integer values for 16bit?
+ BassDevice.AudioFormat := TAudioFormatInfo.Create(2, RecordInfo.freq, asfS16)
+ end
+ else
+ begin
+ // BASS does not provide an explizit input channel count (except BASS_RECORDINFO.formats)
+ // but it doesn't fail if we use stereo input on a mono device
+ // -> use stereo by default
+ BassDevice.AudioFormat := TAudioFormatInfo.Create(2, 44100, asfS16)
+ end;
+
+ // get info if multiple input-sources can be selected at once
+ BassDevice.SingleIn := RecordInfo.singlein;
+
+ // init list for capture buffers per channel
+ SetLength(BassDevice.CaptureChannel, BassDevice.AudioFormat.Channels);
+
+ BassDevice.MicSource := -1;
+ BassDevice.SourceRestore := -1;
+
+ // add a virtual default source (will not change mixer-settings)
+ SetLength(BassDevice.Source, 1);
+ BassDevice.Source[0].Name := DEFAULT_SOURCE_NAME;
+
+ // add real input sources
+ SourceIndex := 1;
+
+ // process each input
+ while true do
+ begin
+ SourceName := BASS_RecordGetInputName(SourceIndex-1);
+
+ {$IFDEF DARWIN}
+ // Under MacOSX the SingStar Mics have an empty InputName.
+ // So, we have to add a hard coded Workaround for this problem
+ // FIXME: - Do we need this anymore? Doesn't the (new) default source already solve this problem?
+ // - Normally a nil return value of BASS_RecordGetInputName() means end-of-list, so maybe
+ // BASS is not able to detect any mic-sources (the default source will work then).
+ // - Does BASS_RecordGetInfo() return true or false? If it returns true in this case
+ // we could use this value to check if the device exists.
+ // Please check that, eddie.
+ // If it returns false, then the source is not detected and it does not make sense to add a second
+ // fake device here.
+ // What about BASS_RecordGetInput()? Does it return a value <> -1?
+ // - Does it even work at all with this fake source-index, now that input switching works?
+ // This info was not used before (sources were never switched), so it did not matter what source-index was used.
+ // But now BASS_RecordSetInput() will probably fail.
+ if ((SourceName = nil) and (SourceIndex = 1) and (Pos('USBMIC Serial#', Descr) > 0)) then
+ SourceName := 'Microphone'
+ {$ENDIF}
+
+ if (SourceName = nil) then
+ break;
+
+ SetLength(BassDevice.Source, Length(BassDevice.Source)+1);
+ // BASS source names seem to be encoded with local encoding
+ // TODO: works for windows, check Linux + Mac OS X
+ BassDevice.Source[SourceIndex].Name := DecodeStringUTF8(SourceName, encLocale);
+
+ // get input-source info
+ Flags := BASS_RecordGetInput(SourceIndex, PSingle(nil)^);
+ if (Flags <> -1) then
+ begin
+ // chech if current source is a mic (and none was set before)
+ if ((Flags and BASS_INPUT_TYPE_MIC) <> 0) and
+ (BassDevice.MicSource = -1) then
+ begin
+ BassDevice.MicSource := SourceIndex;
+ end;
+ end;
+
+ Inc(SourceIndex);
+ end;
+
+ // FIXME: this call hangs in FPC (windows) every 2nd time USDX is called.
+ // Maybe because the sound-device was not released properly?
+ BASS_RecordFree;
+
+ Inc(DeviceIndex);
+ end;
+
+ Inc(BassDeviceID);
+ end;
+
+ result := true;
+end;
+
+function TAudioInput_Bass.InitializeRecord(): boolean;
+begin
+ BassCore := TAudioCore_Bass.GetInstance();
+ if not BassCore.CheckVersion then
+ begin
+ Result := false;
+ Exit;
+ end;
+ Result := EnumDevices();
+end;
+
+function TAudioInput_Bass.FinalizeRecord(): boolean;
+begin
+ CaptureStop;
+ Result := inherited FinalizeRecord;
+end;
+
+
+initialization
+ MediaManager.Add(TAudioInput_Bass.Create);
+
+end.
diff --git a/src/media/UAudioInput_Portaudio.pas b/src/media/UAudioInput_Portaudio.pas
new file mode 100644
index 00000000..c7364eb4
--- /dev/null
+++ b/src/media/UAudioInput_Portaudio.pas
@@ -0,0 +1,537 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UAudioInput_Portaudio;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I ../switches.inc}
+
+uses
+ Classes,
+ SysUtils,
+ UMusic;
+
+implementation
+
+uses
+ {$IFDEF UsePortmixer}
+ portmixer,
+ {$ENDIF}
+ portaudio,
+ ctypes,
+ UAudioCore_Portaudio,
+ UUnicodeUtils,
+ UTextEncoding,
+ UIni,
+ ULog,
+ UMain,
+ URecord;
+
+type
+ TAudioInput_Portaudio = class(TAudioInputBase)
+ private
+ AudioCore: TAudioCore_Portaudio;
+ function EnumDevices(): boolean;
+ public
+ function GetName: string; override;
+ function InitializeRecord: boolean; override;
+ function FinalizeRecord: boolean; override;
+ end;
+
+ TPortaudioInputDevice = class(TAudioInputDevice)
+ private
+ RecordStream: PPaStream;
+ {$IFDEF UsePortmixer}
+ Mixer: PPxMixer;
+ {$ENDIF}
+ PaDeviceIndex: TPaDeviceIndex;
+ public
+ function Open(): boolean;
+ function Close(): boolean;
+ function Start(): boolean; override;
+ function Stop(): boolean; override;
+
+ function DetermineInputLatency(Info: PPaDeviceInfo): TPaTime;
+
+ function GetVolume(): single; override;
+ procedure SetVolume(Volume: single); override;
+ end;
+
+function MicrophoneCallback(input: pointer; output: pointer; frameCount: culong;
+ timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
+ inputDevice: pointer): cint; cdecl; forward;
+
+function MicrophoneTestCallback(input: pointer; output: pointer; frameCount: culong;
+ timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
+ inputDevice: pointer): cint; cdecl; forward;
+
+{**
+ * Converts a string returned by Portaudio into UTF8.
+ * If the string already is in UTF8 no conversion is performed, otherwise
+ * the local encoding is used.
+ *}
+function ConvertPaStringToUTF8(const Str: RawByteString): UTF8String;
+begin
+ if (IsUTF8String(Str)) then
+ Result := Str
+ else
+ Result := DecodeStringUTF8(Str, encLocale);
+end;
+
+
+{ TPortaudioInputDevice }
+
+function TPortaudioInputDevice.DetermineInputLatency(Info: PPaDeviceInfo): TPaTime;
+begin
+ if (Ini.InputDeviceConfig[CfgIndex].Latency <> -1) then
+ begin
+ // autodetection off -> set user latency
+ Result := Ini.InputDeviceConfig[CfgIndex].Latency / 1000
+ end
+ else
+ begin
+ // on vista and xp the defaultLowInputLatency may be set to 0 but it works.
+ // TODO: correct too low latencies (what is a too low latency, maybe < 10ms?)
+ // TODO: retry with input-latency set to 20ms (defaultLowInputLatency might
+ // not be set correctly in OSS)
+
+ // FIXME: according to the portaudio headers defaultHighInputLatency (approx. 40ms) is
+ // for robust non-interactive applications and defaultLowInputLatency (approx. 15ms)
+ // for interactive performance.
+ // We need defaultLowInputLatency here but this setting is far too buggy. If the callback
+ // does not return quickly the stream will be stuck after returning from the callback
+ // and the callback will not be called anymore and mic-capturing stops.
+ // Audacity (in AudioIO.cpp) uses defaultHighInputLatency if software playthrough is on
+ // and even higher latencies (100ms) without playthrough so this should be ok for now.
+ //Result := Info^.defaultLowInputLatency;
+ Result := Info^.defaultHighInputLatency;
+ end;
+end;
+
+function TPortaudioInputDevice.Open(): boolean;
+var
+ Error: TPaError;
+ inputParams: TPaStreamParameters;
+ deviceInfo: PPaDeviceInfo;
+ {$IFDEF UsePortmixer}
+ SourceIndex: integer;
+ {$ENDIF}
+begin
+ Result := false;
+
+ // get input latency info
+ deviceInfo := Pa_GetDeviceInfo(PaDeviceIndex);
+
+ // set input stream parameters
+ with inputParams do
+ begin
+ device := PaDeviceIndex;
+ channelCount := AudioFormat.Channels;
+ sampleFormat := paInt16;
+ suggestedLatency := DetermineInputLatency(deviceInfo);
+ hostApiSpecificStreamInfo := nil;
+ end;
+
+ Log.LogStatus('Open ' + deviceInfo^.name, 'Portaudio');
+ Log.LogStatus('Latency of ' + deviceInfo^.name + ': ' + floatToStr(inputParams.suggestedLatency), 'Portaudio');
+
+ // open input stream
+ Error := Pa_OpenStream(RecordStream, @inputParams, nil,
+ AudioFormat.SampleRate,
+ paFramesPerBufferUnspecified, paNoFlag,
+ @MicrophoneCallback, pointer(Self));
+ if (Error <> paNoError) then
+ begin
+ Log.LogError('Error opening stream: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Open');
+ Exit;
+ end;
+
+ {$IFDEF UsePortmixer}
+ // open default mixer
+ Mixer := Px_OpenMixer(RecordStream, 0);
+ if (Mixer = nil) then
+ begin
+ Log.LogError('Error opening mixer: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Open');
+ end
+ else
+ begin
+ // save current source selection and select new source
+ SourceIndex := Ini.InputDeviceConfig[CfgIndex].Input-1;
+ if (SourceIndex = -1) then
+ begin
+ // nothing to do if default source is used
+ SourceRestore := -1;
+ end
+ else
+ begin
+ // store current source-index and select new source
+ SourceRestore := Px_GetCurrentInputSource(Mixer); // -1 in error case
+ Px_SetCurrentInputSource(Mixer, SourceIndex);
+ end;
+ end;
+ {$ENDIF}
+
+ Result := true;
+end;
+
+function TPortaudioInputDevice.Start(): boolean;
+var
+ Error: TPaError;
+begin
+ Result := false;
+
+ // recording already started -> stop first
+ if (RecordStream <> nil) then
+ Stop();
+
+ // TODO: Do not open the device here (takes too much time).
+ if (not Open()) then
+ Exit;
+
+ // start capture
+ Error := Pa_StartStream(RecordStream);
+ if (Error <> paNoError) then
+ begin
+ Log.LogError('Error starting stream: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Start');
+ Close();
+ RecordStream := nil;
+ Exit;
+ end;
+
+ Result := true;
+end;
+
+function TPortaudioInputDevice.Stop(): boolean;
+var
+ Error: TPaError;
+begin
+ Result := false;
+
+ if (RecordStream = nil) then
+ Exit;
+
+ // Note: do NOT call Pa_StopStream here!
+ // It gets stuck on devices with non-working callback as Pa_StopStream
+ // waits until all buffers have been handled (which never occurs in that case).
+ Error := Pa_AbortStream(RecordStream);
+ if (Error <> paNoError) then
+ begin
+ Log.LogError('Pa_AbortStream: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Stop');
+ end;
+
+ Result := Close();
+end;
+
+function TPortaudioInputDevice.Close(): boolean;
+var
+ Error: TPaError;
+begin
+ {$IFDEF UsePortmixer}
+ if (Mixer <> nil) then
+ begin
+ // restore source selection
+ if (SourceRestore >= 0) then
+ begin
+ Px_SetCurrentInputSource(Mixer, SourceRestore);
+ end;
+
+ // close mixer
+ Px_CloseMixer(Mixer);
+ Mixer := nil;
+ end;
+ {$ENDIF}
+
+ Error := Pa_CloseStream(RecordStream);
+ if (Error <> paNoError) then
+ begin
+ Log.LogError('Pa_CloseStream: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Close');
+ Result := false;
+ end
+ else
+ begin
+ Result := true;
+ end;
+
+ RecordStream := nil;
+end;
+
+function TPortaudioInputDevice.GetVolume(): single;
+begin
+ Result := 0;
+ {$IFDEF UsePortmixer}
+ if (Mixer <> nil) then
+ Result := Px_GetInputVolume(Mixer);
+ {$ENDIF}
+end;
+
+procedure TPortaudioInputDevice.SetVolume(Volume: single);
+begin
+ {$IFDEF UsePortmixer}
+ if (Mixer <> nil) then
+ begin
+ // clip to valid range
+ if (Volume > 1.0) then
+ Volume := 1.0
+ else if (Volume < 0) then
+ Volume := 0;
+ Px_SetInputVolume(Mixer, Volume);
+ end;
+ {$ENDIF}
+end;
+
+
+{ TAudioInput_Portaudio }
+
+function TAudioInput_Portaudio.GetName: String;
+begin
+ result := 'Portaudio';
+end;
+
+function TAudioInput_Portaudio.EnumDevices(): boolean;
+var
+ i: integer;
+ deviceName: UTF8String;
+ paApiIndex: TPaHostApiIndex;
+ paApiInfo: PPaHostApiInfo;
+ paDeviceIndex:TPaDeviceIndex;
+ paDeviceInfo: PPaDeviceInfo;
+ channelCnt: integer;
+ deviceIndex: integer;
+ err: TPaError;
+ errMsg: string;
+ paDevice: TPortaudioInputDevice;
+ inputParams: TPaStreamParameters;
+ stream: PPaStream;
+ streamInfo: PPaStreamInfo;
+ sampleRate: double;
+ latency: TPaTime;
+ {$IFDEF UsePortmixer}
+ mixer: PPxMixer;
+ sourceCnt: integer;
+ sourceIndex: integer;
+ sourceName: UTF8String;
+ {$ENDIF}
+const
+ MIN_TEST_LATENCY = 100 / 1000; // min. test latency of 100 ms to avoid removal of working devices
+begin
+ Result := false;
+
+ // choose the best available Audio-API
+ paApiIndex := AudioCore.GetPreferredApiIndex();
+ if (paApiIndex = -1) then
+ begin
+ Log.LogError('No working Audio-API found', 'TAudioInput_Portaudio.EnumDevices');
+ Exit;
+ end;
+
+ paApiInfo := Pa_GetHostApiInfo(paApiIndex);
+
+ deviceIndex := 0;
+
+ // init array-size to max. input-devices count
+ SetLength(AudioInputProcessor.DeviceList, paApiInfo^.deviceCount);
+ for i:= 0 to High(AudioInputProcessor.DeviceList) do
+ begin
+ // convert API-specific device-index to global index
+ paDeviceIndex := Pa_HostApiDeviceIndexToDeviceIndex(paApiIndex, i);
+ paDeviceInfo := Pa_GetDeviceInfo(paDeviceIndex);
+
+ channelCnt := paDeviceInfo^.maxInputChannels;
+
+ // current device is no input device -> skip
+ if (channelCnt <= 0) then
+ continue;
+
+ // portaudio returns a channel-count of 128 for some devices
+ // (e.g. the "default"-device), so we have to detect those
+ // fantasy channel counts.
+ if (channelCnt > 8) then
+ channelCnt := 2;
+
+ paDevice := TPortaudioInputDevice.Create();
+ AudioInputProcessor.DeviceList[deviceIndex] := paDevice;
+
+ // retrieve device-name
+ deviceName := ConvertPaStringToUTF8(paDeviceInfo^.name);
+ paDevice.Name := UnifyDeviceName(deviceName, deviceIndex);
+ paDevice.PaDeviceIndex := paDeviceIndex;
+
+ sampleRate := paDeviceInfo^.defaultSampleRate;
+
+ // use a stable (high) latency so we do not remove working devices
+ if (paDeviceInfo^.defaultHighInputLatency > MIN_TEST_LATENCY) then
+ latency := paDeviceInfo^.defaultHighInputLatency
+ else
+ latency := MIN_TEST_LATENCY;
+
+ // setup desired input parameters
+ with inputParams do
+ begin
+ device := paDeviceIndex;
+ channelCount := channelCnt;
+ sampleFormat := paInt16;
+ suggestedLatency := latency;
+ hostApiSpecificStreamInfo := nil;
+ end;
+
+ // check souncard and adjust sample-rate
+ if (not AudioCore.TestDevice(@inputParams, nil, sampleRate)) then
+ begin
+ // ignore device if it does not work
+ Log.LogError('Device "'+paDevice.Name+'" does not work',
+ 'TAudioInput_Portaudio.EnumDevices');
+ paDevice.Free();
+ continue;
+ end;
+
+ // open device for further info
+ err := Pa_OpenStream(stream, @inputParams, nil, 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.EnumDevices');
+ paDevice.Free();
+ continue;
+ end;
+
+ // adjust sample-rate (might be changed by portaudio)
+ streamInfo := Pa_GetStreamInfo(stream);
+ if (streamInfo <> nil) then
+ begin
+ if (sampleRate <> streamInfo^.sampleRate) then
+ begin
+ Log.LogStatus('Portaudio changed Samplerate from ' + FloatToStr(sampleRate) +
+ ' to ' + FloatToStr(streamInfo^.sampleRate),
+ 'TAudioInput_Portaudio.InitializeRecord');
+ sampleRate := streamInfo^.sampleRate;
+ end;
+ end;
+
+ // create audio-format info and resize capture-buffer array
+ paDevice.AudioFormat := TAudioFormatInfo.Create(
+ channelCnt,
+ sampleRate,
+ asfS16
+ );
+ SetLength(paDevice.CaptureChannel, paDevice.AudioFormat.Channels);
+
+ Log.LogStatus('InputDevice "'+paDevice.Name+'"@' +
+ IntToStr(paDevice.AudioFormat.Channels)+'x'+
+ FloatToStr(paDevice.AudioFormat.SampleRate)+'Hz ('+
+ FloatTostr(inputParams.suggestedLatency)+'sec)' ,
+ 'Portaudio.EnumDevices');
+
+ // portaudio does not provide a source-type check
+ paDevice.MicSource := -1;
+ paDevice.SourceRestore := -1;
+
+ // add a virtual default source (will not change mixer-settings)
+ SetLength(paDevice.Source, 1);
+ paDevice.Source[0].Name := DEFAULT_SOURCE_NAME;
+
+ {$IFDEF UsePortmixer}
+ // use default mixer
+ mixer := Px_OpenMixer(stream, 0);
+
+ // get input count
+ sourceCnt := Px_GetNumInputSources(mixer);
+ SetLength(paDevice.Source, sourceCnt+1);
+
+ // get input names
+ for sourceIndex := 1 to sourceCnt do
+ begin
+ sourceName := Px_GetInputSourceName(mixer, sourceIndex-1);
+ paDevice.Source[sourceIndex].Name := ConvertPaStringToUTF8(sourceName);
+ end;
+
+ Px_CloseMixer(mixer);
+ {$ENDIF}
+
+ // close test-stream
+ Pa_CloseStream(stream);
+
+ Inc(deviceIndex);
+ end;
+
+ // adjust size to actual input-device count
+ SetLength(AudioInputProcessor.DeviceList, deviceIndex);
+
+ Log.LogStatus('#Input-Devices: ' + inttostr(deviceIndex), 'Portaudio');
+
+ Result := true;
+end;
+
+function TAudioInput_Portaudio.InitializeRecord(): boolean;
+begin
+ Result := false;
+ AudioCore := TAudioCore_Portaudio.GetInstance();
+
+ // initialize portaudio
+ if (not AudioCore.Initialize()) then
+ Exit;
+ Result := EnumDevices();
+end;
+
+function TAudioInput_Portaudio.FinalizeRecord: boolean;
+begin
+ CaptureStop;
+ AudioCore.Terminate();
+ Result := inherited FinalizeRecord();
+end;
+
+{*
+ * Portaudio input capture callback.
+ *}
+function MicrophoneCallback(input: pointer; output: pointer; frameCount: culong;
+ timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
+ inputDevice: pointer): cint; cdecl;
+begin
+ AudioInputProcessor.HandleMicrophoneData(input, frameCount*4, inputDevice);
+ result := paContinue;
+end;
+
+{*
+ * Portaudio test capture callback.
+ *}
+function MicrophoneTestCallback(input: pointer; output: pointer; frameCount: culong;
+ timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
+ inputDevice: pointer): cint; cdecl;
+begin
+ // this callback is called only once
+ result := paAbort;
+end;
+
+initialization
+ MediaManager.add(TAudioInput_Portaudio.Create);
+
+end.
diff --git a/src/media/UAudioPlaybackBase.pas b/src/media/UAudioPlaybackBase.pas
new file mode 100644
index 00000000..5f317257
--- /dev/null
+++ b/src/media/UAudioPlaybackBase.pas
@@ -0,0 +1,319 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UAudioPlaybackBase;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+uses
+ UMusic,
+ UTime,
+ UPath;
+
+type
+ TAudioPlaybackBase = class(TInterfacedObject, IAudioPlayback)
+ protected
+ OutputDeviceList: TAudioOutputDeviceList;
+ MusicStream: TAudioPlaybackStream;
+ 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: IPath): TAudioPlaybackStream;
+ function OpenDecodeStream(const Filename: IPath): TAudioDecodeStream;
+ public
+ function GetName: string; virtual; abstract;
+
+ function Open(const Filename: IPath): boolean; // true if succeed
+ procedure Close;
+
+ procedure Play;
+ procedure Pause;
+ 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 GetOutputDeviceList(): TAudioOutputDeviceList;
+
+ procedure SetAppVolume(Volume: single); virtual; abstract;
+ procedure SetVolume(Volume: single);
+ procedure SetLoop(Enabled: boolean);
+
+ procedure Rewind;
+ function Finished: boolean;
+ function Length: real;
+
+ // Sounds
+ function OpenSound(const Filename: IPath): TAudioPlaybackStream;
+ procedure PlaySound(Stream: TAudioPlaybackStream);
+ procedure StopSound(Stream: TAudioPlaybackStream);
+
+ // Equalizer
+ procedure GetFFTData(var Data: TFFTData);
+
+ // Interface for Visualizer
+ function GetPCMData(var Data: TPCMData): Cardinal;
+
+ function CreateVoiceStream(Channel: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream; virtual; abstract;
+ end;
+
+
+implementation
+
+uses
+ ULog,
+ SysUtils;
+
+{ TAudioPlaybackBase }
+
+function TAudioPlaybackBase.FinalizePlayback: boolean;
+begin
+ FreeAndNil(MusicStream);
+ ClearOutputDeviceList();
+ Result := true;
+end;
+
+function TAudioPlaybackBase.Open(const Filename: IPath): boolean;
+begin
+ // free old MusicStream
+ MusicStream.Free;
+
+ MusicStream := OpenStream(Filename);
+ if not assigned(MusicStream) then
+ begin
+ Result := false;
+ Exit;
+ end;
+
+ //MusicStream.AddSoundEffect(TVoiceRemoval.Create());
+
+ Result := true;
+end;
+
+procedure TAudioPlaybackBase.Close;
+begin
+ FreeAndNil(MusicStream);
+end;
+
+function TAudioPlaybackBase.OpenDecodeStream(const Filename: IPath): 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.ToNative + '"', '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: IPath): 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.ToNative + '"', '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;
+begin
+ if assigned(MusicStream) then
+ MusicStream.Play();
+end;
+
+procedure TAudioPlaybackBase.Pause;
+begin
+ if assigned(MusicStream) then
+ MusicStream.Pause();
+end;
+
+procedure TAudioPlaybackBase.Stop;
+begin
+ if assigned(MusicStream) then
+ MusicStream.Stop();
+end;
+
+function TAudioPlaybackBase.Length: real;
+begin
+ if assigned(MusicStream) then
+ Result := MusicStream.Length
+ else
+ Result := 0;
+end;
+
+function TAudioPlaybackBase.GetPosition: real;
+begin
+ if assigned(MusicStream) then
+ Result := MusicStream.Position
+ else
+ Result := 0;
+end;
+
+procedure TAudioPlaybackBase.SetPosition(Time: real);
+begin
+ if assigned(MusicStream) then
+ MusicStream.Position := Time;
+end;
+
+procedure TAudioPlaybackBase.SetSyncSource(SyncSource: TSyncSource);
+begin
+ if assigned(MusicStream) then
+ MusicStream.SetSyncSource(SyncSource);
+end;
+
+procedure TAudioPlaybackBase.Rewind;
+begin
+ SetPosition(0);
+end;
+
+function TAudioPlaybackBase.Finished: boolean;
+begin
+ if assigned(MusicStream) then
+ Result := (MusicStream.Status = ssStopped)
+ else
+ Result := true;
+end;
+
+procedure TAudioPlaybackBase.SetVolume(Volume: single);
+begin
+ if assigned(MusicStream) then
+ MusicStream.Volume := Volume;
+end;
+
+procedure TAudioPlaybackBase.FadeIn(Time: real; TargetVolume: single);
+begin
+ if assigned(MusicStream) then
+ MusicStream.FadeIn(Time, TargetVolume);
+end;
+
+procedure TAudioPlaybackBase.SetLoop(Enabled: boolean);
+begin
+ if assigned(MusicStream) then
+ MusicStream.Loop := Enabled;
+end;
+
+// Equalizer
+procedure TAudioPlaybackBase.GetFFTData(var data: TFFTData);
+begin
+ if assigned(MusicStream) then
+ MusicStream.GetFFTData(data);
+end;
+
+{*
+ * Copies interleaved PCM SInt16 stereo samples into data.
+ * Returns the number of frames
+ *}
+function TAudioPlaybackBase.GetPCMData(var data: TPCMData): Cardinal;
+begin
+ if assigned(MusicStream) then
+ Result := MusicStream.GetPCMData(data)
+ else
+ Result := 0;
+end;
+
+function TAudioPlaybackBase.OpenSound(const Filename: IPath): TAudioPlaybackStream;
+begin
+ Result := OpenStream(Filename);
+end;
+
+procedure TAudioPlaybackBase.PlaySound(stream: TAudioPlaybackStream);
+begin
+ if assigned(stream) then
+ stream.Play();
+end;
+
+procedure TAudioPlaybackBase.StopSound(stream: TAudioPlaybackStream);
+begin
+ if assigned(stream) then
+ stream.Stop();
+end;
+
+procedure TAudioPlaybackBase.ClearOutputDeviceList();
+var
+ DeviceIndex: integer;
+begin
+ for DeviceIndex := 0 to High(OutputDeviceList) do
+ OutputDeviceList[DeviceIndex].Free();
+ SetLength(OutputDeviceList, 0);
+end;
+
+function TAudioPlaybackBase.GetOutputDeviceList(): TAudioOutputDeviceList;
+begin
+ Result := OutputDeviceList;
+end;
+
+end.
diff --git a/src/media/UAudioPlayback_Bass.pas b/src/media/UAudioPlayback_Bass.pas
new file mode 100644
index 00000000..1d7a44dc
--- /dev/null
+++ b/src/media/UAudioPlayback_Bass.pas
@@ -0,0 +1,758 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UAudioPlayback_Bass;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+implementation
+
+uses
+ Classes,
+ Math,
+ UIni,
+ UMain,
+ UMusic,
+ UAudioPlaybackBase,
+ UAudioCore_Bass,
+ ULog,
+ sdl,
+ bass,
+ SysUtils;
+
+type
+ PHDSP = ^HDSP;
+
+type
+ 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();
+ destructor Destroy(); override;
+
+ 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 AddSoundEffect(Effect: TSoundEffect); override;
+ procedure RemoveSoundEffect(Effect: TSoundEffect); override;
+
+ procedure GetFFTData(var Data: TFFTData); override;
+ function GetPCMData(var Data: TPCMData): Cardinal; override;
+
+ function GetAudioFormatInfo(): TAudioFormatInfo; override;
+
+ function ReadData(Buffer: PByteArray; BufferSize: integer): integer;
+
+ property EOF: boolean READ IsEOF;
+ end;
+
+const
+ MAX_VOICE_DELAY = 0.020; // 20ms
+
+type
+ TBassVoiceStream = class(TAudioVoiceStream)
+ private
+ Handle: HSTREAM;
+ public
+ function Open(ChannelMap: integer; FormatInfo: TAudioFormatInfo): boolean; override;
+ procedure Close(); override;
+
+ procedure WriteData(Buffer: PByteArray; BufferSize: integer); override;
+ function ReadData(Buffer: PByteArray; BufferSize: integer): integer; override;
+ function IsEOF(): boolean; override;
+ function IsError(): boolean; override;
+ end;
+
+type
+ TAudioPlayback_Bass = class(TAudioPlaybackBase)
+ private
+ function EnumDevices(): boolean;
+ protected
+ 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)
+ private
+ BassDeviceID: DWORD; // DeviceID used by BASS
+ end;
+
+var
+ BassCore: TAudioCore_Bass;
+
+
+{ TBassPlaybackStream }
+
+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: PByteArray; BufferSize: integer): integer;
+var
+ AdjustedSize: integer;
+ RequestedSourceSize, SourceSize: integer;
+ SkipCount: integer;
+ SourceFormatInfo: TAudioFormatInfo;
+ FrameSize: integer;
+ PadFrame: PByteArray;
+ //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;
+ Reset();
+end;
+
+destructor TBassPlaybackStream.Destroy();
+begin
+ Close();
+ inherited;
+end;
+
+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;
+
+ // 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
+ NeedsFlush: boolean;
+begin
+ if (not assigned(SourceStream)) then
+ Exit;
+
+ NeedsFlush := true;
+
+ if (BASS_ChannelIsActive(Handle) = BASS_ACTIVE_PAUSED) then
+ 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;
+
+ // start playing and flush buffers on rewind
+ BASS_ChannelPlay(Handle, NeedsFlush);
+end;
+
+procedure TBassPlaybackStream.FadeIn(Time: real; TargetVolume: single);
+begin
+ // start stream
+ Play();
+ // start fade-in: slide from fadeStart- to fadeEnd-volume in FadeInTime
+ BASS_ChannelSlideAttribute(Handle, BASS_ATTRIB_VOL, TargetVolume, Trunc(Time * 1000));
+end;
+
+procedure TBassPlaybackStream.Pause();
+begin
+ BASS_ChannelPause(Handle);
+end;
+
+procedure TBassPlaybackStream.Stop();
+begin
+ BASS_ChannelStop(Handle);
+end;
+
+function TBassPlaybackStream.IsEOF(): boolean;
+begin
+ 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;
+var
+ lVolume: single;
+begin
+ if (not BASS_ChannelGetAttribute(Handle, BASS_ATTRIB_VOL, lVolume)) then
+ begin
+ Log.LogError('BASS_ChannelGetAttribute: ' + BassCore.ErrorGetString(),
+ 'TBassPlaybackStream.GetVolume');
+ Result := 0;
+ Exit;
+ end;
+ Result := Round(lVolume);
+end;
+
+procedure TBassPlaybackStream.SetVolume(Volume: single);
+begin
+ // clamp volume
+ if Volume < 0 then
+ Volume := 0;
+ if Volume > 1.0 then
+ Volume := 1.0;
+ // set volume
+ BASS_ChannelSetAttribute(Handle, BASS_ATTRIB_VOL, Volume);
+end;
+
+function TBassPlaybackStream.GetPosition: real;
+var
+ BufferPosByte: QWORD;
+ BufferPosSec: double;
+begin
+ 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
+ ChannelState: DWORD;
+begin
+ 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;
+begin
+ if assigned(SourceStream) then
+ Result := SourceStream.Length
+ else
+ Result := -1;
+end;
+
+function TBassPlaybackStream.GetStatus(): TStreamStatus;
+var
+ State: DWORD;
+begin
+ State := BASS_ChannelIsActive(Handle);
+ case State of
+ BASS_ACTIVE_PLAYING,
+ BASS_ACTIVE_STALLED:
+ Result := ssPlaying;
+ BASS_ACTIVE_PAUSED:
+ Result := ssPaused;
+ BASS_ACTIVE_STOPPED:
+ Result := ssStopped;
+ else
+ begin
+ Log.LogError('Unknown status', 'TBassPlaybackStream.GetStatus');
+ Result := ssStopped;
+ end;
+ end;
+end;
+
+function TBassPlaybackStream.GetLoop(): boolean;
+begin
+ if assigned(SourceStream) then
+ Result := SourceStream.Loop
+ else
+ Result := false;
+end;
+
+procedure TBassPlaybackStream.SetLoop(Enabled: boolean);
+begin
+ 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}
+var
+ Effect: TSoundEffect;
+begin
+ Effect := TSoundEffect(user);
+ if assigned(Effect) then
+ Effect.Callback(buffer, length);
+end;
+
+procedure TBassPlaybackStream.AddSoundEffect(Effect: TSoundEffect);
+var
+ DspHandle: HDSP;
+begin
+ 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
+ begin
+ Log.LogError(BassCore.ErrorGetString(), 'TBassPlaybackStream.AddSoundEffect');
+ Exit;
+ end;
+
+ GetMem(Effect.EngineData, SizeOf(HDSP));
+ PHDSP(Effect.EngineData)^ := DspHandle;
+end;
+
+procedure TBassPlaybackStream.RemoveSoundEffect(Effect: TSoundEffect);
+begin
+ 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
+ begin
+ Log.LogError(BassCore.ErrorGetString(), 'TBassPlaybackStream.RemoveSoundEffect');
+ Exit;
+ end;
+
+ FreeMem(Effect.EngineData);
+ Effect.EngineData := nil;
+end;
+
+procedure TBassPlaybackStream.GetFFTData(var Data: TFFTData);
+begin
+ // 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;
+var
+ Info: BASS_CHANNELINFO;
+ nBytes: DWORD;
+begin
+ Result := 0;
+
+ FillChar(Data, SizeOf(TPCMData), 0);
+
+ // no support for non-stereo files at the moment
+ BASS_ChannelGetInfo(Handle, Info);
+ if (Info.chans <> 2) then
+ Exit;
+
+ nBytes := BASS_ChannelGetData(Handle, @Data, SizeOf(TPCMData));
+ if(nBytes <= 0) then
+ Result := 0
+ else
+ Result := nBytes div SizeOf(TPCMStereoSample);
+end;
+
+function TBassPlaybackStream.GetAudioFormatInfo(): TAudioFormatInfo;
+begin
+ if assigned(SourceStream) then
+ Result := SourceStream.GetAudioFormatInfo()
+ else
+ Result := nil;
+end;
+
+
+{ TBassVoiceStream }
+
+function TBassVoiceStream.Open(ChannelMap: integer; FormatInfo: TAudioFormatInfo): boolean;
+var
+ Flags: DWORD;
+begin
+ 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 TBassVoiceStream.Close();
+begin
+ if (Handle <> 0) then
+ begin
+ BASS_ChannelStop(Handle);
+ BASS_StreamFree(Handle);
+ end;
+ inherited Close();
+end;
+
+procedure TBassVoiceStream.WriteData(Buffer: PByteArray; BufferSize: integer);
+var QueueSize: DWORD;
+begin
+ 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;
+
+// Note: we do not need the read-function for the BASS implementation
+function TBassVoiceStream.ReadData(Buffer: PByteArray; BufferSize: integer): integer;
+begin
+ Result := -1;
+end;
+
+function TBassVoiceStream.IsEOF(): boolean;
+begin
+ Result := false;
+end;
+
+function TBassVoiceStream.IsError(): boolean;
+begin
+ Result := false;
+end;
+
+
+{ TAudioPlayback_Bass }
+
+function TAudioPlayback_Bass.GetName: String;
+begin
+ Result := 'BASS_Playback';
+end;
+
+function TAudioPlayback_Bass.EnumDevices(): boolean;
+var
+ BassDeviceID: DWORD;
+ DeviceIndex: integer;
+ Device: TBassOutputDevice;
+ DeviceInfo: BASS_DEVICEINFO;
+begin
+ Result := true;
+
+ ClearOutputDeviceList();
+
+ // skip "no sound"-device (ID = 0)
+ BassDeviceID := 1;
+
+ while (true) do
+ begin
+ // check for device
+ if (not BASS_GetDeviceInfo(BassDeviceID, DeviceInfo)) then
+ Break;
+
+ // set device info
+ Device := TBassOutputDevice.Create();
+ Device.Name := DeviceInfo.name;
+ Device.BassDeviceID := BassDeviceID;
+
+ // add device to list
+ SetLength(OutputDeviceList, BassDeviceID);
+ OutputDeviceList[BassDeviceID-1] := Device;
+
+ Inc(BassDeviceID);
+ end;
+end;
+
+function TAudioPlayback_Bass.InitializePlayback(): boolean;
+begin
+ Result := false;
+
+ BassCore := TAudioCore_Bass.GetInstance();
+ if not BassCore.CheckVersion then
+ Exit;
+
+ 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');
+ Exit;
+ end;
+
+ //Log.BenchmarkEnd(4); Log.LogBenchmark('--> Bass Init', 4);
+
+ // config playing buffer
+ //BASS_SetConfig(BASS_CONFIG_UPDATEPERIOD, 10);
+ //BASS_SetConfig(BASS_CONFIG_BUFFER, 100);
+
+ Result := true;
+end;
+
+function TAudioPlayback_Bass.FinalizePlayback(): boolean;
+begin
+ Close;
+ BASS_Free;
+ inherited FinalizePlayback();
+ Result := true;
+end;
+
+function TAudioPlayback_Bass.CreatePlaybackStream(): TAudioPlaybackStream;
+begin
+ 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.CreateVoiceStream(ChannelMap: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream;
+var
+ VoiceStream: TAudioVoiceStream;
+begin
+ Result := nil;
+
+ VoiceStream := TBassVoiceStream.Create();
+ if (not VoiceStream.Open(ChannelMap, FormatInfo)) then
+ begin
+ VoiceStream.Free;
+ Exit;
+ end;
+
+ Result := VoiceStream;
+end;
+
+function TAudioPlayback_Bass.GetLatency(): double;
+begin
+ Result := 0;
+end;
+
+
+initialization
+ MediaManager.Add(TAudioPlayback_Bass.Create);
+
+end.
diff --git a/src/media/UAudioPlayback_Portaudio.pas b/src/media/UAudioPlayback_Portaudio.pas
new file mode 100644
index 00000000..6fbae6e3
--- /dev/null
+++ b/src/media/UAudioPlayback_Portaudio.pas
@@ -0,0 +1,385 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UAudioPlayback_Portaudio;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+uses
+ Classes,
+ SysUtils,
+ UMusic;
+
+implementation
+
+uses
+ portaudio,
+ UAudioCore_Portaudio,
+ UAudioPlayback_SoftMixer,
+ ULog,
+ UIni,
+ UMain;
+
+type
+ TAudioPlayback_Portaudio = class(TAudioPlayback_SoftMixer)
+ private
+ paStream: PPaStream;
+ AudioCore: TAudioCore_Portaudio;
+ Latency: double;
+ function OpenDevice(deviceIndex: TPaDeviceIndex): boolean;
+ 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;
+ end;
+
+ TPortaudioOutputDevice = class(TAudioOutputDevice)
+ private
+ PaDeviceIndex: TPaDeviceIndex;
+ end;
+
+
+{ TAudioPlayback_Portaudio }
+
+function PortaudioAudioCallback(input: Pointer; output: Pointer; frameCount: Longword;
+ timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
+ userData: Pointer): Integer; cdecl;
+var
+ Engine: TAudioPlayback_Portaudio;
+begin
+ 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';
+end;
+
+function TAudioPlayback_Portaudio.OpenDevice(deviceIndex: TPaDeviceIndex): boolean;
+var
+ DeviceInfo : PPaDeviceInfo;
+ SampleRate : double;
+ OutParams : TPaStreamParameters;
+ StreamInfo : PPaStreamInfo;
+ err : TPaError;
+begin
+ Result := false;
+
+ DeviceInfo := Pa_GetDeviceInfo(deviceIndex);
+
+ Log.LogInfo('Audio-Output Device: ' + DeviceInfo^.name, 'TAudioPlayback_Portaudio.OpenDevice');
+
+ SampleRate := DeviceInfo^.defaultSampleRate;
+
+ with OutParams do
+ begin
+ device := deviceIndex;
+ channelCount := 2;
+ sampleFormat := paInt16;
+ suggestedLatency := DeviceInfo^.defaultLowOutputLatency;
+ hostApiSpecificStreamInfo := nil;
+ end;
+
+ // check souncard and adjust sample-rate
+ if not AudioCore.TestDevice(nil, @OutParams, SampleRate) then
+ begin
+ Log.LogStatus('TestDevice failed!', 'TAudioPlayback_Portaudio.OpenDevice');
+ Exit;
+ end;
+
+ // open output stream
+ 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;
+ 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,
+ asfS16 // FIXME: is paInt16 system-dependant or -independant?
+ );
+
+ Result := true;
+end;
+
+function TAudioPlayback_Portaudio.EnumDevices(): boolean;
+var
+ i: integer;
+ paApiIndex: TPaHostApiIndex;
+ paApiInfo: PPaHostApiInfo;
+ deviceName: string;
+ deviceIndex: TPaDeviceIndex;
+ deviceInfo: PPaDeviceInfo;
+ channelCnt: integer;
+ SC: integer; // soundcard
+ err: TPaError;
+ errMsg: string;
+ paDevice: TPortaudioOutputDevice;
+ outputParams: TPaStreamParameters;
+ stream: PPaStream;
+ streamInfo: PPaStreamInfo;
+ sampleRate: double;
+ latency: TPaTime;
+ cbPolls: integer;
+ cbWorks: boolean;
+begin
+ Result := false;
+
+(*
+ // choose the best available Audio-API
+ paApiIndex := AudioCore.GetPreferredApiIndex();
+ if(paApiIndex = -1) then
+ begin
+ Log.LogError('No working Audio-API found', 'TAudioPlayback_Portaudio.EnumDevices');
+ Exit;
+ end;
+
+ paApiInfo := Pa_GetHostApiInfo(paApiIndex);
+
+ SC := 0;
+
+ // init array-size to max. output-devices count
+ SetLength(OutputDeviceList, paApiInfo^.deviceCount);
+ for i:= 0 to High(OutputDeviceList) do
+ begin
+ // convert API-specific device-index to global index
+ deviceIndex := Pa_HostApiDeviceIndexToDeviceIndex(paApiIndex, i);
+ deviceInfo := Pa_GetDeviceInfo(deviceIndex);
+
+ channelCnt := deviceInfo^.maxOutputChannels;
+
+ // current device is no output device -> skip
+ if (channelCnt <= 0) then
+ continue;
+
+ // portaudio returns a channel-count of 128 for some devices
+ // (e.g. the "default"-device), so we have to detect those
+ // fantasy channel counts.
+ if (channelCnt > 8) then
+ channelCnt := 2;
+
+ paDevice := TPortaudioOutputDevice.Create();
+ OutputDeviceList[SC] := paDevice;
+
+ // retrieve device-name
+ deviceName := deviceInfo^.name;
+ paDevice.Name := deviceName;
+ paDevice.PaDeviceIndex := deviceIndex;
+
+ if (deviceInfo^.defaultSampleRate > 0) then
+ sampleRate := deviceInfo^.defaultSampleRate
+ else
+ sampleRate := 44100;
+
+ // on vista and xp the defaultLowInputLatency may be set to 0 but it works.
+ // TODO: correct too low latencies (what is a too low latency, maybe < 10ms?)
+ latency := deviceInfo^.defaultLowInputLatency;
+
+ // setup desired output parameters
+ // TODO: retry with input-latency set to 20ms (defaultLowOutputLatency might
+ // not be set correctly in OSS)
+ with outputParams do
+ begin
+ device := deviceIndex;
+ channelCount := channelCnt;
+ sampleFormat := paInt16;
+ suggestedLatency := latency;
+ hostApiSpecificStreamInfo := nil;
+ end;
+
+ // check if mic-callback works (might not be called on some devices)
+ 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',
+ 'TAudioPlayback_Portaudio.InitializeRecord');
+ paDevice.Free();
+ continue;
+ end;
+
+ // open device for further info
+ 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 +')',
+ 'TAudioPlayback_Portaudio.InitializeRecord');
+ paDevice.Free();
+ continue;
+ end;
+
+ // adjust sample-rate (might be changed by portaudio)
+ streamInfo := Pa_GetStreamInfo(stream);
+ if (streamInfo <> nil) then
+ begin
+ if (sampleRate <> streamInfo^.sampleRate) then
+ begin
+ Log.LogStatus('Portaudio changed Samplerate from ' + FloatToStr(sampleRate) +
+ ' to ' + FloatToStr(streamInfo^.sampleRate),
+ 'TAudioInput_Portaudio.InitializeRecord');
+ sampleRate := streamInfo^.sampleRate;
+ end;
+ end;
+
+ // create audio-format info and resize capture-buffer array
+ paDevice.AudioFormat := TAudioFormatInfo.Create(
+ channelCnt,
+ sampleRate,
+ asfS16
+ );
+ SetLength(paDevice.CaptureChannel, paDevice.AudioFormat.Channels);
+
+ Log.LogStatus('OutputDevice "'+paDevice.Name+'"@' +
+ IntToStr(paDevice.AudioFormat.Channels)+'x'+
+ FloatToStr(paDevice.AudioFormat.SampleRate)+'Hz ('+
+ FloatTostr(outputParams.suggestedLatency)+'sec)' ,
+ 'TAudioInput_Portaudio.InitializeRecord');
+
+ // close test-stream
+ Pa_CloseStream(stream);
+
+ Inc(SC);
+ end;
+
+ // adjust size to actual input-device count
+ SetLength(OutputDeviceList, SC);
+
+ Log.LogStatus('#Output-Devices: ' + inttostr(SC), 'Portaudio');
+*)
+
+ Result := true;
+end;
+
+function TAudioPlayback_Portaudio.InitializeAudioPlaybackEngine(): boolean;
+var
+ paApiIndex : TPaHostApiIndex;
+ paApiInfo : PPaHostApiInfo;
+ paOutDevice : TPaDeviceIndex;
+begin
+ Result := false;
+ AudioCore := TAudioCore_Portaudio.GetInstance();
+
+ // initialize portaudio
+ if (not AudioCore.Initialize()) then
+ Exit;
+
+ paApiIndex := AudioCore.GetPreferredApiIndex();
+ if (paApiIndex = -1) then
+ begin
+ Log.LogError('No working Audio-API found', 'TAudioPlayback_Portaudio.InitializeAudioPlaybackEngine');
+ Exit;
+ end;
+
+ EnumDevices();
+
+ paApiInfo := Pa_GetHostApiInfo(paApiIndex);
+ Log.LogInfo('Audio-Output API-Type: ' + paApiInfo^.name, 'TAudioPlayback_Portaudio.OpenDevice');
+
+ paOutDevice := paApiInfo^.defaultOutputDevice;
+ if (not OpenDevice(paOutDevice)) then
+ begin
+ Exit;
+ end;
+
+ Result := true;
+end;
+
+function TAudioPlayback_Portaudio.StartAudioPlaybackEngine(): boolean;
+var
+ err: TPaError;
+begin
+ Result := false;
+
+ if (paStream = nil) then
+ Exit;
+
+ err := Pa_StartStream(paStream);
+ if(err <> paNoError) then
+ begin
+ Log.LogStatus('Pa_StartStream: '+Pa_GetErrorText(err), 'UAudioPlayback_Portaudio');
+ Exit;
+ end;
+
+ Result := true;
+end;
+
+procedure TAudioPlayback_Portaudio.StopAudioPlaybackEngine();
+begin
+ if (paStream <> nil) then
+ begin
+ Pa_CloseStream(paStream);
+ // wait until stream is closed, otherwise Terminate() might cause a segfault
+ while (Pa_IsStreamActive(paStream) = 1) do
+ ;
+ paStream := nil;
+ end;
+end;
+
+function TAudioPlayback_Portaudio.FinalizeAudioPlaybackEngine(): boolean;
+begin
+ StopAudioPlaybackEngine();
+ Result := AudioCore.Terminate();
+end;
+
+function TAudioPlayback_Portaudio.GetLatency(): double;
+begin
+ Result := Latency;
+end;
+
+
+initialization
+ MediaManager.Add(TAudioPlayback_Portaudio.Create);
+
+end.
diff --git a/src/media/UAudioPlayback_SDL.pas b/src/media/UAudioPlayback_SDL.pas
new file mode 100644
index 00000000..8403ef03
--- /dev/null
+++ b/src/media/UAudioPlayback_SDL.pas
@@ -0,0 +1,182 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UAudioPlayback_SDL;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+implementation
+
+uses
+ Classes,
+ sdl,
+ SysUtils,
+ UAudioPlayback_SoftMixer,
+ UMusic,
+ ULog,
+ UIni,
+ UMain;
+
+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: PByteArray; size: Cardinal; volume: Single); override;
+ end;
+
+
+{ TAudioPlayback_SDL }
+
+procedure SDLAudioCallback(userdata: Pointer; stream: PByteArray; len: integer); cdecl;
+var
+ Engine: TAudioPlayback_SDL;
+begin
+ Engine := TAudioPlayback_SDL(userdata);
+ Engine.AudioCallback(stream, len);
+end;
+
+function TAudioPlayback_SDL.GetName: String;
+begin
+ Result := 'SDL_Playback';
+end;
+
+function TAudioPlayback_SDL.EnumDevices(): boolean;
+begin
+ // Note: SDL does not provide Device-Selection capabilities (will be introduced in 1.3)
+ ClearOutputDeviceList();
+ SetLength(OutputDeviceList, 1);
+ OutputDeviceList[0] := TAudioOutputDevice.Create();
+ OutputDeviceList[0].Name := '[SDL Default-Device]';
+ Result := true;
+end;
+
+function TAudioPlayback_SDL.InitializeAudioPlaybackEngine(): boolean;
+var
+ DesiredAudioSpec, ObtainedAudioSpec: TSDL_AudioSpec;
+ SampleBufferSize: integer;
+begin
+ Result := false;
+
+ EnumDevices();
+
+ if (SDL_InitSubSystem(SDL_INIT_AUDIO) = -1) then
+ begin
+ Log.LogError('SDL_InitSubSystem failed!', 'TAudioPlayback_SDL.InitializeAudioPlaybackEngine');
+ Exit;
+ end;
+
+ SampleBufferSize := IAudioOutputBufferSizeVals[Ini.AudioOutputBufferSizeIndex];
+ if (SampleBufferSize <= 0) then
+ begin
+ // Automatic setting default
+ // FIXME: too much glitches with 1024 samples
+ SampleBufferSize := 2048; //1024;
+ end;
+
+ FillChar(DesiredAudioSpec, SizeOf(DesiredAudioSpec), 0);
+ with DesiredAudioSpec do
+ begin
+ freq := 44100;
+ format := AUDIO_S16SYS;
+ channels := 2;
+ samples := SampleBufferSize;
+ callback := @SDLAudioCallback;
+ userdata := Self;
+ end;
+
+ // 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;
+ end;
+
+ FormatInfo := TAudioFormatInfo.Create(
+ 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;
+end;
+
+function TAudioPlayback_SDL.StartAudioPlaybackEngine(): boolean;
+begin
+ SDL_PauseAudio(0);
+ Result := true;
+end;
+
+procedure TAudioPlayback_SDL.StopAudioPlaybackEngine();
+begin
+ SDL_PauseAudio(1);
+end;
+
+function TAudioPlayback_SDL.FinalizeAudioPlaybackEngine(): boolean;
+begin
+ SDL_CloseAudio();
+ SDL_QuitSubSystem(SDL_INIT_AUDIO);
+ Result := true;
+end;
+
+function TAudioPlayback_SDL.GetLatency(): double;
+begin
+ Result := Latency;
+end;
+
+procedure TAudioPlayback_SDL.MixBuffers(dst, src: PByteArray; size: Cardinal; volume: Single);
+begin
+ SDL_MixAudio(PUInt8(dst), PUInt8(src), size, Round(volume * SDL_MIX_MAXVOLUME));
+end;
+
+
+initialization
+ MediaManager.add(TAudioPlayback_SDL.Create);
+
+end.
diff --git a/src/media/UAudioPlayback_SoftMixer.pas b/src/media/UAudioPlayback_SoftMixer.pas
new file mode 100644
index 00000000..11df4df5
--- /dev/null
+++ b/src/media/UAudioPlayback_SoftMixer.pas
@@ -0,0 +1,1204 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UAudioPlayback_SoftMixer;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+uses
+ Classes,
+ sdl,
+ SysUtils,
+ URingBuffer,
+ UMusic,
+ UAudioPlaybackBase;
+
+type
+ TAudioPlayback_SoftMixer = class;
+
+ TGenericPlaybackStream = class(TAudioPlaybackStream)
+ private
+ Engine: TAudioPlayback_SoftMixer;
+ LastReadSize: integer; // size of data returned by the last ReadData() call
+ LastReadTime: Cardinal; // time of the last ReadData() call
+
+ SampleBuffer: PByteArray;
+ SampleBufferSize: integer;
+ SampleBufferCount: integer; // number of available bytes in SampleBuffer
+ SampleBufferPos: integer;
+
+ SourceBuffer: PByteArray;
+ SourceBufferSize: integer;
+ SourceBufferCount: integer; // number of available bytes in SourceBuffer
+
+ Converter: TAudioConverter;
+ Status: TStreamStatus;
+ InternalLock: PSDL_Mutex;
+ SoundEffects: TList;
+ fVolume: single;
+
+ FadeInStartTime, FadeInTime: cardinal;
+ FadeInStartVolume, FadeInTargetVolume: single;
+
+ NeedsRewind: boolean;
+
+ procedure Reset();
+
+ procedure ApplySoundEffects(Buffer: PByteArray; BufferSize: integer);
+ function InitFormatConversion(): boolean;
+ procedure FlushBuffers();
+
+ 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;
+
+ function GetRemainingBufferSize(): integer;
+ public
+ constructor Create(Engine: TAudioPlayback_SoftMixer);
+ destructor Destroy(); override;
+
+ 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;
+
+ function GetAudioFormatInfo(): TAudioFormatInfo; override;
+
+ function ReadData(Buffer: PByteArray; BufferSize: integer): integer;
+
+ function GetPCMData(var Data: TPCMData): cardinal; override;
+ procedure GetFFTData(var Data: TFFTData); override;
+
+ procedure AddSoundEffect(Effect: TSoundEffect); override;
+ procedure RemoveSoundEffect(Effect: TSoundEffect); override;
+ end;
+
+ TAudioMixerStream = class
+ private
+ Engine: TAudioPlayback_SoftMixer;
+
+ ActiveStreams: TList;
+ MixerBuffer: PByteArray;
+ InternalLock: PSDL_Mutex;
+
+ AppVolume: single;
+
+ procedure Lock(); {$IFDEF HasInline}inline;{$ENDIF}
+ procedure Unlock(); {$IFDEF HasInline}inline;{$ENDIF}
+
+ function GetVolume(): 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: PByteArray; BufferSize: integer): integer;
+
+ property Volume: single read GetVolume write SetVolume;
+ end;
+
+ TAudioPlayback_SoftMixer = class(TAudioPlaybackBase)
+ private
+ MixerStream: TAudioMixerStream;
+ protected
+ FormatInfo: TAudioFormatInfo;
+
+ function InitializeAudioPlaybackEngine(): boolean; virtual; abstract;
+ function StartAudioPlaybackEngine(): boolean; virtual; abstract;
+ procedure StopAudioPlaybackEngine(); virtual; abstract;
+ function FinalizeAudioPlaybackEngine(): boolean; virtual; abstract;
+ procedure AudioCallback(Buffer: PByteArray; Size: integer); {$IFDEF HasInline}inline;{$ENDIF}
+
+ function CreatePlaybackStream(): TAudioPlaybackStream; override;
+ public
+ function GetName: string; override; abstract;
+ function InitializePlayback(): boolean; override;
+ function FinalizePlayback: boolean; override;
+
+ 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(DstBuffer, SrcBuffer: PByteArray; 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: PByteArray; BufferSize: integer); override;
+ function ReadData(Buffer: PByteArray; 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,
+ ULog,
+ UIni,
+ UFFT,
+ UAudioConverter,
+ UMain;
+
+{ TAudioMixerStream }
+
+constructor TAudioMixerStream.Create(Engine: TAudioPlayback_SoftMixer);
+begin
+ inherited Create();
+
+ Self.Engine := Engine;
+
+ 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);
+ inherited;
+end;
+
+procedure TAudioMixerStream.Lock();
+begin
+ SDL_mutexP(InternalLock);
+end;
+
+procedure TAudioMixerStream.Unlock();
+begin
+ SDL_mutexV(InternalLock);
+end;
+
+function TAudioMixerStream.GetVolume(): single;
+begin
+ Lock();
+ Result := AppVolume;
+ Unlock();
+end;
+
+procedure TAudioMixerStream.SetVolume(Volume: single);
+begin
+ Lock();
+ AppVolume := Volume;
+ Unlock();
+end;
+
+procedure TAudioMixerStream.AddStream(Stream: TAudioPlaybackStream);
+begin
+ 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));
+ 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!).
+ * 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.
+ *)
+procedure TAudioMixerStream.RemoveStream(Stream: TAudioPlaybackStream);
+var
+ Index: integer;
+begin
+ Lock();
+ Index := activeStreams.IndexOf(Pointer(Stream));
+ if (Index <> -1) then
+ begin
+ // remove entry but do not decrease count-property
+ ActiveStreams[Index] := nil;
+ end;
+ Unlock();
+end;
+
+function TAudioMixerStream.ReadData(Buffer: PByteArray; BufferSize: integer): integer;
+var
+ i: integer;
+ Size: integer;
+ Stream: TGenericPlaybackStream;
+ NeedsPacking: boolean;
+begin
+ Result := BufferSize;
+
+ // zero target-buffer (silence)
+ FillChar(Buffer^, BufferSize, 0);
+
+ // resize mixer-buffer if necessary
+ ReallocMem(MixerBuffer, BufferSize);
+ if not assigned(MixerBuffer) then
+ Exit;
+
+ Lock();
+
+ NeedsPacking := false;
+
+ // mix streams to one stream
+ for i := 0 to ActiveStreams.Count-1 do
+ begin
+ if (ActiveStreams[i] = nil) then
+ begin
+ NeedsPacking := true;
+ continue;
+ end;
+
+ Stream := TGenericPlaybackStream(ActiveStreams[i]);
+ // fetch data from current stream
+ 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);
+ end;
+ end;
+
+ // remove nil-pointers from list
+ if (NeedsPacking) then
+ begin
+ ActiveStreams.Pack();
+ end;
+
+ Unlock();
+end;
+
+
+{ TGenericPlaybackStream }
+
+constructor TGenericPlaybackStream.Create(Engine: TAudioPlayback_SoftMixer);
+begin
+ inherited Create();
+ Self.Engine := Engine;
+ InternalLock := SDL_CreateMutex();
+ SoundEffects := TList.Create;
+ Status := ssStopped;
+ Reset();
+end;
+
+destructor TGenericPlaybackStream.Destroy();
+begin
+ Close();
+ SDL_DestroyMutex(InternalLock);
+ FreeAndNil(SoundEffects);
+ inherited;
+end;
+
+procedure TGenericPlaybackStream.Reset();
+begin
+ SourceStream := nil;
+
+ FreeAndNil(Converter);
+
+ FreeMem(SampleBuffer);
+ SampleBuffer := nil;
+ SampleBufferPos := 0;
+ SampleBufferSize := 0;
+ SampleBufferCount := 0;
+
+ FreeMem(SourceBuffer);
+ SourceBuffer := nil;
+ SourceBufferSize := 0;
+ SourceBufferCount := 0;
+
+ NeedsRewind := false;
+
+ fVolume := 0;
+ SoundEffects.Clear;
+ FadeInTime := 0;
+
+ LastReadSize := 0;
+end;
+
+function TGenericPlaybackStream.Open(SourceStream: TAudioSourceStream): boolean;
+begin
+ Result := false;
+
+ Close();
+
+ if not assigned(SourceStream) then
+ Exit;
+ Self.SourceStream := SourceStream;
+
+ if not InitFormatConversion() then
+ begin
+ // 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;
+
+procedure TGenericPlaybackStream.Close();
+begin
+ // stop audio-callback on this stream
+ Stop();
+
+ // Note: PerformOnClose must be called before SourceStream is invalidated
+ PerformOnClose();
+ // and free data
+ Reset();
+end;
+
+procedure TGenericPlaybackStream.LockSampleBuffer();
+begin
+ SDL_mutexP(InternalLock);
+end;
+
+procedure TGenericPlaybackStream.UnlockSampleBuffer();
+begin
+ SDL_mutexV(InternalLock);
+end;
+
+function TGenericPlaybackStream.InitFormatConversion(): boolean;
+var
+ SrcFormatInfo: TAudioFormatInfo;
+ DstFormatInfo: TAudioFormatInfo;
+begin
+ 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;
+begin
+ // 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 := fVolume;
+ FadeInTargetVolume := TargetVolume;
+ Play();
+end;
+
+procedure TGenericPlaybackStream.Pause();
+var
+ Mixer: TAudioMixerStream;
+begin
+ if (Status <> ssPlaying) then
+ Exit;
+
+ Status := ssPaused;
+
+ Mixer := Engine.GetMixer();
+ if (Mixer <> nil) then
+ Mixer.RemoveStream(Self);
+end;
+
+procedure TGenericPlaybackStream.Stop();
+var
+ Mixer: TAudioMixerStream;
+begin
+ if (Status = ssStopped) then
+ Exit;
+
+ Status := ssStopped;
+ // stop fading
+ FadeInTime := 0;
+
+ LastReadSize := 0;
+
+ Mixer := Engine.GetMixer();
+ if (Mixer <> nil) then
+ Mixer.RemoveStream(Self);
+end;
+
+function TGenericPlaybackStream.GetLoop(): boolean;
+begin
+ if assigned(SourceStream) then
+ Result := SourceStream.Loop
+ else
+ Result := false;
+end;
+
+procedure TGenericPlaybackStream.SetLoop(Enabled: boolean);
+begin
+ if assigned(SourceStream) then
+ SourceStream.Loop := Enabled;
+end;
+
+function TGenericPlaybackStream.GetLength(): real;
+begin
+ if assigned(SourceStream) then
+ Result := SourceStream.Length
+ else
+ Result := -1;
+end;
+
+function TGenericPlaybackStream.GetLatency(): double;
+begin
+ Result := Engine.GetLatency();
+end;
+
+function TGenericPlaybackStream.GetStatus(): TStreamStatus;
+begin
+ Result := Status;
+end;
+
+function TGenericPlaybackStream.GetAudioFormatInfo(): TAudioFormatInfo;
+begin
+ Result := Engine.GetAudioFormatInfo();
+end;
+
+procedure TGenericPlaybackStream.FlushBuffers();
+begin
+ SampleBufferCount := 0;
+ SampleBufferPos := 0;
+ SourceBufferCount := 0;
+ LastReadSize := 0;
+end;
+
+procedure TGenericPlaybackStream.ApplySoundEffects(Buffer: PByteArray; BufferSize: integer);
+var
+ i: integer;
+begin
+ for i := 0 to SoundEffects.Count-1 do
+ begin
+ if (SoundEffects[i] <> nil) then
+ begin
+ TSoundEffect(SoundEffects[i]).Callback(Buffer, BufferSize);
+ end;
+ end;
+end;
+
+function TGenericPlaybackStream.ReadData(Buffer: PByteArray; BufferSize: integer): integer;
+var
+ ConversionInputCount: integer;
+ ConversionOutputSize: integer; // max. number of converted data (= buffer size)
+ ConversionOutputCount: integer; // actual number of converted data
+ SourceSize: integer;
+ NeededSampleBufferSize: integer;
+ BytesNeeded: 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: PByteArray;
+begin
+ Result := -1;
+
+ LastReadSize := 0;
+
+ // 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 access to sample-buffer
+ LockSampleBuffer();
+ try
+
+ // skip sample-buffer data
+ SampleBufferPos := SampleBufferPos + SkipOutputCount;
+ // size of available bytes in SampleBuffer after skipping
+ SampleBufferCount := SampleBufferCount - SampleBufferPos;
+ // update byte skip-count
+ SkipOutputCount := -SampleBufferCount;
+
+ // 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
+ 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;
+
+ // copy data to front of buffer
+ if ((SampleBufferCount > 0) and (SampleBufferPos > 0)) then
+ Move(SampleBuffer[SampleBufferPos], SampleBuffer[0], SampleBufferCount);
+ SampleBufferPos := 0;
+
+ // 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;
+
+ // fill sample-buffer (fetch and convert one block of source data per loop)
+ while (SampleBufferCount < BytesNeeded) do
+ begin
+ // 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
+ 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;
+
+ // apply effects
+ ApplySoundEffects(SampleBuffer, SampleBufferCount);
+
+ // copy data to result buffer
+ CopyCount := Min(BytesNeeded, SampleBufferCount);
+ Move(SampleBuffer[0], Buffer[BufferSize - BytesNeeded], CopyCount);
+ Dec(BytesNeeded, CopyCount);
+ SampleBufferPos := CopyCount;
+
+ // release buffer lock
+ finally
+ UnlockSampleBuffer();
+ end;
+
+ // pad the buffer with the last frame if we are to fast
+ if (FillCount > 0) then
+ begin
+ if (CopyCount >= OutputFrameSize) then
+ PadFrame := @Buffer[CopyCount-OutputFrameSize]
+ else
+ PadFrame := nil;
+ FillBufferWithFrame(@Buffer[CopyCount], FillCount,
+ PadFrame, OutputFrameSize);
+ end;
+
+ // BytesNeeded now contains the number of remaining bytes we were not able to fetch
+ LastReadTime := SDL_GetTicks;
+ LastReadSize := BufferSize - BytesNeeded;
+ Result := LastReadSize;
+end;
+
+function TGenericPlaybackStream.GetPCMData(var Data: TPCMData): cardinal;
+var
+ ByteCount: integer;
+begin
+ Result := 0;
+
+ // just SInt16 stereo support for now
+ if ((Engine.GetAudioFormatInfo().Format <> asfS16) or
+ (Engine.GetAudioFormatInfo().Channels <> 2)) then
+ begin
+ Exit;
+ end;
+
+ // zero memory
+ 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.
+
+ LockSampleBuffer();
+ ByteCount := Min(SizeOf(Data), SampleBufferCount);
+ if (ByteCount > 0) then
+ begin
+ Move(SampleBuffer[0], Data, ByteCount);
+ end;
+ UnlockSampleBuffer();
+
+ Result := ByteCount div SizeOf(TPCMStereoSample);
+end;
+
+procedure TGenericPlaybackStream.GetFFTData(var Data: TFFTData);
+var
+ i: integer;
+ Frames: integer;
+ DataIn: PSingleArray;
+ AudioFormat: TAudioFormatInfo;
+begin
+ // only works with SInt16 and Float values at the moment
+ AudioFormat := GetAudioFormatInfo();
+
+ DataIn := AllocMem(FFTSize * SizeOf(single));
+ if (DataIn = nil) then
+ Exit;
+
+ LockSampleBuffer();
+ // TODO: We just use the first Frames frames, the others are ignored.
+ Frames := Min(FFTSize, SampleBufferCount div AudioFormat.FrameSize);
+ // use only first channel and convert data to float-values
+ case AudioFormat.Format of
+ asfS16:
+ begin
+ for i := 0 to Frames-1 do
+ DataIn[i] := PSmallInt(@SampleBuffer[i*AudioFormat.FrameSize])^ / -Low(SmallInt);
+ end;
+ asfFloat:
+ begin
+ for i := 0 to Frames-1 do
+ DataIn[i] := PSingle(@SampleBuffer[i*AudioFormat.FrameSize])^;
+ end;
+ end;
+ UnlockSampleBuffer();
+
+ WindowFunc(fwfHanning, FFTSize, DataIn);
+ 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;
+ end;
+end;
+
+procedure TGenericPlaybackStream.AddSoundEffect(Effect: TSoundEffect);
+begin
+ if (not assigned(Effect)) then
+ Exit;
+
+ LockSampleBuffer();
+ // check if effect is already in list to avoid duplicates
+ if (SoundEffects.IndexOf(Pointer(Effect)) = -1) then
+ SoundEffects.Add(Pointer(Effect));
+ UnlockSampleBuffer();
+end;
+
+procedure TGenericPlaybackStream.RemoveSoundEffect(Effect: TSoundEffect);
+begin
+ LockSampleBuffer();
+ SoundEffects.Remove(Effect);
+ UnlockSampleBuffer();
+end;
+
+{**
+ * Returns the approximate number of bytes left in the audio engines buffer queue.
+ *}
+function TGenericPlaybackStream.GetRemainingBufferSize(): integer;
+var
+ TimeDiff: double;
+begin
+ if (LastReadSize <= 0) then
+ begin
+ Result := 0;
+ end
+ else
+ begin
+ TimeDiff := (SDL_GetTicks() - LastReadTime) / 1000;
+ // we gave the data-sink LastReadSize bytes at the last call to ReadData().
+ // Calculate how much of this should be left in the data-sink
+ Result := LastReadSize - Trunc(TimeDiff * Engine.FormatInfo.BytesPerSec);
+ if (Result < 0) then
+ Result := 0;
+ end;
+end;
+
+function TGenericPlaybackStream.GetPosition: real;
+var
+ BufferedTime: double;
+begin
+ if assigned(SourceStream) then
+ begin
+ LockSampleBuffer();
+
+ // the duration of source stream data that is buffered in this stream.
+ // (this is the data retrieved from the source but has not been resampled)
+ BufferedTime := SourceBufferCount / SourceStream.GetAudioFormatInfo().BytesPerSec;
+
+ // the duration of data that is buffered in this stream.
+ // (this is the already resampled data that has not yet been passed to the audio engine)
+ BufferedTime := BufferedTime + (SampleBufferCount - SampleBufferPos) / Engine.FormatInfo.BytesPerSec;
+
+ // Now consider the samples left in the engine's (e.g. SDL) buffer.
+ // Otherwise the result calculated so far will not change until the callback
+ // is called the next time.
+ // For example, if the buffer has a size of 2048 frames we would not be
+ // able to return a new new position for approx. 40ms (at 44.1kHz) which
+ // would be very bad for synching.
+ BufferedTime := BufferedTime + GetRemainingBufferSize() / Engine.FormatInfo.BytesPerSec;
+
+ // use the timestamp of the source as reference and subtract the time of
+ // the data that is still buffered and not yet output.
+ Result := SourceStream.Position - BufferedTime;
+
+ UnlockSampleBuffer();
+ end
+ else
+ begin
+ Result := -1;
+ end;
+end;
+
+procedure TGenericPlaybackStream.SetPosition(Time: real);
+begin
+ 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
+ LockSampleBuffer();
+ // adjust volume if fading is enabled
+ if (FadeInTime > 0) then
+ begin
+ FadeAmount := (SDL_GetTicks() - FadeInStartTime) / FadeInTime;
+ // check if fade-target is reached
+ if (FadeAmount >= 1) then
+ begin
+ // target reached -> stop fading
+ FadeInTime := 0;
+ fVolume := FadeInTargetVolume;
+ end
+ else
+ begin
+ // fading in progress
+ fVolume := FadeAmount*FadeInTargetVolume + (1-FadeAmount)*FadeInStartVolume;
+ end;
+ end;
+ // return current volume
+ Result := fVolume;
+ UnlockSampleBuffer();
+end;
+
+procedure TGenericPlaybackStream.SetVolume(Volume: single);
+begin
+ LockSampleBuffer();
+ // stop fading
+ FadeInTime := 0;
+ // clamp volume
+ if (Volume > 1.0) then
+ fVolume := 1.0
+ else if (Volume < 0) then
+ fVolume := 0
+ else
+ 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: PByteArray; 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: PByteArray; 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;
+
+
+{ TAudioPlayback_SoftMixer }
+
+function TAudioPlayback_SoftMixer.InitializePlayback: boolean;
+begin
+ Result := false;
+
+ //Log.LogStatus('InitializePlayback', 'UAudioPlayback_SoftMixer');
+
+ if (not InitializeAudioPlaybackEngine()) then
+ Exit;
+
+ MixerStream := TAudioMixerStream.Create(Self);
+
+ if (not StartAudioPlaybackEngine()) then
+ Exit;
+
+ Result := true;
+end;
+
+function TAudioPlayback_SoftMixer.FinalizePlayback: boolean;
+begin
+ Close;
+ StopAudioPlaybackEngine();
+
+ FreeAndNil(MixerStream);
+ FreeAndNil(FormatInfo);
+
+ FinalizeAudioPlaybackEngine();
+ inherited FinalizePlayback;
+ Result := true;
+end;
+
+procedure TAudioPlayback_SoftMixer.AudioCallback(Buffer: PByteArray; Size: integer);
+begin
+ MixerStream.ReadData(Buffer, Size);
+end;
+
+function TAudioPlayback_SoftMixer.GetMixer(): TAudioMixerStream;
+begin
+ Result := MixerStream;
+end;
+
+function TAudioPlayback_SoftMixer.GetAudioFormatInfo(): TAudioFormatInfo;
+begin
+ Result := FormatInfo;
+end;
+
+function TAudioPlayback_SoftMixer.CreatePlaybackStream(): TAudioPlaybackStream;
+begin
+ Result := TGenericPlaybackStream.Create(Self);
+end;
+
+function TAudioPlayback_SoftMixer.CreateVoiceStream(ChannelMap: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream;
+var
+ VoiceStream: TGenericVoiceStream;
+begin
+ Result := nil;
+
+ // create a voice stream
+ VoiceStream := TGenericVoiceStream.Create(Self);
+ if (not VoiceStream.Open(ChannelMap, FormatInfo)) then
+ begin
+ VoiceStream.Free;
+ Exit;
+ end;
+
+ Result := VoiceStream;
+end;
+
+procedure TAudioPlayback_SoftMixer.SetAppVolume(Volume: single);
+begin
+ // sets volume only for this application
+ MixerStream.Volume := Volume;
+end;
+
+procedure TAudioPlayback_SoftMixer.MixBuffers(DstBuffer, SrcBuffer: PByteArray; Size: cardinal; Volume: single);
+var
+ SampleIndex: cardinal;
+ SampleInt: integer;
+ SampleFlt: single;
+begin
+ SampleIndex := 0;
+ case FormatInfo.Format of
+ asfS16:
+ begin
+ while (SampleIndex < Size) do
+ begin
+ // apply volume and sum with previous mixer value
+ 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(@DstBuffer[SampleIndex])^ := SampleInt;
+ // increase index by one sample
+ Inc(SampleIndex, SizeOf(SmallInt));
+ end;
+ end;
+ asfFloat:
+ begin
+ while (SampleIndex < Size) do
+ begin
+ // apply volume and sum with previous mixer value
+ 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(@DstBuffer[SampleIndex])^ := SampleFlt;
+ // increase index by one sample
+ Inc(SampleIndex, SizeOf(single));
+ end;
+ end;
+ else
+ begin
+ Log.LogError('Incompatible format', 'TAudioMixerStream.MixAudio');
+ end;
+ end;
+end;
+
+end.
diff --git a/src/media/UMediaCore_FFmpeg.pas b/src/media/UMediaCore_FFmpeg.pas
new file mode 100644
index 00000000..eb136995
--- /dev/null
+++ b/src/media/UMediaCore_FFmpeg.pas
@@ -0,0 +1,645 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UMediaCore_FFmpeg;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+uses
+ Classes,
+ ctypes,
+ sdl,
+ avcodec,
+ avformat,
+ avutil,
+ avio,
+ swscale,
+ UMusic,
+ ULog,
+ UPath;
+
+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,
+ UConfig;
+
+function FFmpegStreamOpen(h: PURLContext; filename: PChar; flags: cint): cint; cdecl; forward;
+function FFmpegStreamRead(h: PURLContext; buf: PByteArray; size: cint): cint; cdecl; forward;
+function FFmpegStreamWrite(h: PURLContext; buf: PByteArray; size: cint): cint; cdecl; forward;
+function FFmpegStreamSeek(h: PURLContext; pos: int64; whence: cint): int64; cdecl; forward;
+function FFmpegStreamClose(h: PURLContext): cint; cdecl; forward;
+
+const
+ UTF8FileProtocol: TURLProtocol = (
+ name: 'ufile';
+ url_open: FFmpegStreamOpen;
+ url_read: FFmpegStreamRead;
+ url_write: FFmpegStreamWrite;
+ url_seek: FFmpegStreamSeek;
+ url_close: FFmpegStreamClose;
+ );
+
+var
+ Instance: TMediaCore_FFmpeg;
+
+function AV_VERSION_INT(a, b, c: cardinal): cuint;
+begin
+ Result := (a shl 16) or (b shl 8) or c;
+end;
+
+procedure CheckVersions();
+var
+ libVersion: cuint;
+ headerVersion: cuint;
+
+ function hexVerToStr(Version: cuint): string;
+ var
+ Major, Minor, Release: cardinal;
+ begin
+ Major := (Version shr 16) and $FF;;
+ Minor := (Version shr 8) and $FF;
+ Release := Version and $FF;
+ Result := Format('%d.%d.%d', [Major, Minor, Release]);
+ end;
+
+begin
+ libVersion := avcodec_version();
+ headerVersion := AV_VERSION_INT(
+ LIBAVCODEC_VERSION_MAJOR,
+ LIBAVCODEC_VERSION_MINOR,
+ LIBAVCODEC_VERSION_RELEASE);
+ if (libVersion <> headerVersion) then
+ begin
+ Log.LogError(Format('%s header (%s) and DLL (%s) versions do not match.',
+ ['libavcodec', hexVerToStr(headerVersion), hexVerToStr(libVersion)]));
+ end;
+
+ {$IF LIBAVFORMAT_VERSION >= 52020000} // 52.20.0
+ libVersion := avformat_version();
+ headerVersion := AV_VERSION_INT(
+ LIBAVFORMAT_VERSION_MAJOR,
+ LIBAVFORMAT_VERSION_MINOR,
+ LIBAVFORMAT_VERSION_RELEASE);
+ if (libVersion <> headerVersion) then
+ begin
+ Log.LogError(Format('%s header (%s) and DLL (%s) versions do not match.',
+ ['libavformat', hexVerToStr(headerVersion), hexVerToStr(libVersion)]));
+ end;
+ {$IFEND}
+
+ {$IF LIBAVUTIL_VERSION >= 49008000} // 49.8.0
+ libVersion := avutil_version();
+ headerVersion := AV_VERSION_INT(
+ LIBAVUTIL_VERSION_MAJOR,
+ LIBAVUTIL_VERSION_MINOR,
+ LIBAVUTIL_VERSION_RELEASE);
+ if (libVersion <> headerVersion) then
+ begin
+ Log.LogError(Format('%s header (%s) and DLL (%s) versions do not match.',
+ ['libavutil', hexVerToStr(headerVersion), hexVerToStr(libVersion)]));
+ end;
+ {$IFEND}
+
+ {$IF LIBSWSCALE_VERSION >= 000006001} // 0.6.1
+ libVersion := swscale_version();
+ headerVersion := AV_VERSION_INT(
+ LIBSWSCALE_VERSION_MAJOR,
+ LIBSWSCALE_VERSION_MINOR,
+ LIBSWSCALE_VERSION_RELEASE);
+ if (libVersion <> headerVersion) then
+ begin
+ Log.LogError(Format('%s header (%s) and DLL (%s) versions do not match.',
+ ['libswscale', hexVerToStr(headerVersion), hexVerToStr(libVersion)]));
+ end;
+ {$IFEND}
+end;
+
+constructor TMediaCore_FFmpeg.Create();
+begin
+ inherited;
+
+ CheckVersions();
+ av_register_protocol(@UTF8FileProtocol);
+ 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 LIBAVCODEC_VERSION < 52064000} // < 52.64.0
+ 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;
+{$ELSE}
+ if (Stream.codec.codec_type = AVMEDIA_TYPE_VIDEO) and
+ (FirstVideoStream < 0) then
+ begin
+ FirstVideoStream := i;
+ end;
+
+ if (Stream.codec.codec_type = AVMEDIA_TYPE_AUDIO) and
+ (FirstAudioStream < 0) then
+ begin
+ FirstAudioStream := i;
+ end;
+ end;
+{$IFEND}
+
+ // 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 LIBAVCODEC_VERSION < 52064000} // < 52.64.0
+ if (Stream.codec^.codec_type = CODEC_TYPE_AUDIO) then
+{$ELSE}
+ if (Stream.codec^.codec_type = AVMEDIA_TYPE_AUDIO) then
+{$IFEND}
+ 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_S32: Format := asfS32;
+ SAMPLE_FMT_FLT: Format := asfFloat;
+ SAMPLE_FMT_DBL: Format := asfDouble;
+ else begin
+ Result := false;
+ Exit;
+ end;
+ end;
+ Result := true;
+end;
+
+
+{**
+ * UTF-8 Filename wrapper based on:
+ * http://www.mail-archive.com/libav-user@mplayerhq.hu/msg02460.html
+ *}
+
+function FFmpegStreamOpen(h: PURLContext; filename: PChar; flags: cint): cint; cdecl;
+var
+ Stream: TStream;
+ Mode: word;
+ ProtPrefix: string;
+ FilePath: IPath;
+begin
+ // check for protocol prefix ('ufile:') and strip it
+ ProtPrefix := Format('%s:', [UTF8FileProtocol.name]);
+ if (StrLComp(filename, PChar(ProtPrefix), Length(ProtPrefix)) = 0) then
+ begin
+ Inc(filename, Length(ProtPrefix));
+ end;
+
+ FilePath := Path(filename);
+
+ if ((flags and URL_RDWR) <> 0) then
+ Mode := fmCreate
+ else if ((flags and URL_WRONLY) <> 0) then
+ Mode := fmCreate // TODO: fmCreate is Read+Write -> reopen with fmOpenWrite
+ else
+ Mode := fmOpenRead or fmShareDenyWrite;
+
+ Result := 0;
+
+ try
+ Stream := TBinaryFileStream.Create(FilePath, Mode);
+ h.priv_data := Stream;
+ except
+ Result := AVERROR_NOENT;
+ end;
+end;
+
+function FFmpegStreamRead(h: PURLContext; buf: PByteArray; size: cint): cint; cdecl;
+var
+ Stream: TStream;
+begin
+ Stream := TStream(h.priv_data);
+ if (Stream = nil) then
+ raise EInvalidContainer.Create('FFmpegStreamRead on nil');
+ try
+ Result := Stream.Read(buf[0], size);
+ except
+ Result := -1;
+ end;
+end;
+
+function FFmpegStreamWrite(h: PURLContext; buf: PByteArray; size: cint): cint; cdecl;
+var
+ Stream: TStream;
+begin
+ Stream := TStream(h.priv_data);
+ if (Stream = nil) then
+ raise EInvalidContainer.Create('FFmpegStreamWrite on nil');
+ try
+ Result := Stream.Write(buf[0], size);
+ except
+ Result := -1;
+ end;
+end;
+
+function FFmpegStreamSeek(h: PURLContext; pos: int64; whence: cint): int64; cdecl;
+var
+ Stream : TStream;
+ Origin : TSeekOrigin;
+begin
+ Stream := TStream(h.priv_data);
+ if (Stream = nil) then
+ raise EInvalidContainer.Create('FFmpegStreamSeek on nil');
+ case whence of
+ 0 {SEEK_SET}: Origin := soBeginning;
+ 1 {SEEK_CUR}: Origin := soCurrent;
+ 2 {SEEK_END}: Origin := soEnd;
+ AVSEEK_SIZE: begin
+ Result := Stream.Size;
+ Exit;
+ end
+ else
+ Origin := soBeginning;
+ end;
+ Result := Stream.Seek(pos, Origin);
+end;
+
+function FFmpegStreamClose(h: PURLContext): cint; cdecl;
+var
+ Stream : TStream;
+begin
+ Stream := TStream(h.priv_data);
+ Stream.Free;
+ Result := 0;
+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/src/media/UMediaCore_SDL.pas b/src/media/UMediaCore_SDL.pas
new file mode 100644
index 00000000..74c75e16
--- /dev/null
+++ b/src/media/UMediaCore_SDL.pas
@@ -0,0 +1,63 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit 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/src/media/UMedia_dummy.pas b/src/media/UMedia_dummy.pas
new file mode 100644
index 00000000..46cbe6b8
--- /dev/null
+++ b/src/media/UMedia_dummy.pas
@@ -0,0 +1,492 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UMedia_dummy;
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+implementation
+
+uses
+ SysUtils,
+ math,
+ UTime,
+ UMusic,
+ UPath;
+
+type
+ TAudio_Dummy = class( TInterfacedObject, IAudioPlayback, IAudioInput )
+ private
+ DummyOutputDeviceList: TAudioOutputDeviceList;
+ public
+ constructor Create();
+ function GetName: string;
+
+ function Init(): boolean;
+ function Finalize(): boolean;
+
+ function Open(const aFileName: IPath): boolean; // true if succeed
+ procedure Close;
+
+ procedure Play;
+ procedure Pause;
+ procedure Stop;
+
+ procedure SetPosition(Time: real);
+ function GetPosition: real;
+
+ procedure SetSyncSource(SyncSource: TSyncSource);
+
+ // IAudioInput
+ function InitializeRecord: boolean;
+ function FinalizeRecord: boolean;
+ procedure CaptureStart;
+ procedure CaptureStop;
+ procedure GetFFTData(var data: TFFTData);
+ function GetPCMData(var data: TPCMData): Cardinal;
+
+ // IAudioPlayback
+ function InitializePlayback: boolean;
+ function FinalizePlayback: boolean;
+
+ function GetOutputDeviceList(): TAudioOutputDeviceList;
+ procedure FadeIn(Time: real; TargetVolume: single);
+ procedure SetAppVolume(Volume: single);
+ procedure SetVolume(Volume: single);
+ procedure Rewind;
+
+ procedure SetLoop(Enabled: boolean);
+ function GetLoop(): boolean;
+
+ function Finished: boolean;
+ function Length: real;
+
+ function OpenSound(const Filename: IPath): 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;
+
+ TVideo_Dummy = class( TInterfacedObject, IVideo )
+ public
+ procedure Close;
+
+ procedure Play;
+ procedure Pause;
+ procedure Stop;
+
+ procedure SetLoop(Enable: boolean);
+ function GetLoop(): boolean;
+
+ procedure SetPosition(Time: real);
+ function GetPosition: real;
+
+ procedure SetScreen(Screen: integer);
+ function GetScreen(): integer;
+
+ procedure SetScreenPosition(X, Y, Z: double);
+ procedure GetScreenPosition(var X, Y, Z: double);
+
+ procedure SetWidth(Width: double);
+ function GetWidth(): double;
+
+ procedure SetHeight(Height: double);
+ function GetHeight(): double;
+
+ procedure SetFrameRange(Range: TRectCoords);
+ function GetFrameRange(): TRectCoords;
+
+ function GetFrameAspect(): real;
+
+ procedure SetAspectCorrection(AspectCorrection: TAspectCorrection);
+ function GetAspectCorrection(): TAspectCorrection;
+
+ procedure SetAlpha(Alpha: double);
+ function GetAlpha(): double;
+
+ procedure SetReflectionSpacing(Spacing: double);
+ function GetReflectionSpacing(): double;
+
+ procedure GetFrame(Time: Extended);
+ procedure Draw();
+ procedure DrawReflection();
+
+ property Screen: integer read GetScreen;
+ property Width: double read GetWidth write SetWidth;
+ property Height: double read GetHeight write SetWidth;
+ property Alpha: double read GetAlpha write SetAlpha;
+ property ReflectionSpacing: double read GetReflectionSpacing write SetReflectionSpacing;
+ property FrameAspect: real read GetFrameAspect;
+ property AspectCorrection: TAspectCorrection read GetAspectCorrection;
+ property Loop: boolean read GetLoop write SetLoop;
+ property Position: real read GetPosition write SetPosition;
+ end;
+
+ TVideoPlayback_Dummy = class( TInterfacedObject, IVideoPlayback, IVideoVisualization )
+ public
+ constructor Create();
+ function GetName: string;
+
+ function Init(): boolean;
+ function Finalize(): boolean;
+
+ function Open(const FileName: IPath): IVideo;
+ end;
+
+function TAudio_Dummy.GetName: string;
+begin
+ Result := 'AudioDummy';
+end;
+
+constructor TAudio_Dummy.Create();
+begin
+ inherited;
+end;
+
+function TAudio_Dummy.Init(): boolean;
+begin
+ Result := true;
+end;
+
+function TAudio_Dummy.Finalize(): boolean;
+begin
+ Result := true;
+end;
+
+function TAudio_Dummy.Open(const aFileName : IPath): boolean; // true if succeed
+begin
+ Result := false;
+end;
+
+procedure TAudio_Dummy.Close;
+begin
+end;
+
+procedure TAudio_Dummy.Play;
+begin
+end;
+
+procedure TAudio_Dummy.Pause;
+begin
+end;
+
+procedure TAudio_Dummy.Stop;
+begin
+end;
+
+procedure TAudio_Dummy.SetPosition(Time: real);
+begin
+end;
+
+function TAudio_Dummy.GetPosition: real;
+begin
+ Result := 0;
+end;
+
+procedure TAudio_Dummy.SetSyncSource(SyncSource: TSyncSource);
+begin
+end;
+
+// IAudioInput
+function TAudio_Dummy.InitializeRecord: boolean;
+begin
+ Result := true;
+end;
+
+function TAudio_Dummy.FinalizeRecord: boolean;
+begin
+ Result := true;
+end;
+
+procedure TAudio_Dummy.CaptureStart;
+begin
+end;
+
+procedure TAudio_Dummy.CaptureStop;
+begin
+end;
+
+procedure TAudio_Dummy.GetFFTData(var data: TFFTData);
+begin
+end;
+
+function TAudio_Dummy.GetPCMData(var data: TPCMData): Cardinal;
+begin
+ Result := 0;
+end;
+
+// IAudioPlayback
+function TAudio_Dummy.InitializePlayback: boolean;
+begin
+ SetLength(DummyOutputDeviceList, 1);
+ DummyOutputDeviceList[0] := TAudioOutputDevice.Create();
+ DummyOutputDeviceList[0].Name := '[Dummy Device]';
+ Result := true;
+end;
+
+function TAudio_Dummy.FinalizePlayback: boolean;
+begin
+ Result := true;
+end;
+
+function TAudio_Dummy.GetOutputDeviceList(): TAudioOutputDeviceList;
+begin
+ Result := DummyOutputDeviceList;
+end;
+
+procedure TAudio_Dummy.SetAppVolume(Volume: single);
+begin
+end;
+
+procedure TAudio_Dummy.SetVolume(Volume: single);
+begin
+end;
+
+procedure TAudio_Dummy.SetLoop(Enabled: boolean);
+begin
+end;
+
+function TAudio_Dummy.GetLoop(): boolean;
+begin
+ Result := false;
+end;
+
+procedure TAudio_Dummy.FadeIn(Time: real; TargetVolume: single);
+begin
+end;
+
+procedure TAudio_Dummy.Rewind;
+begin
+end;
+
+function TAudio_Dummy.Finished: boolean;
+begin
+ Result := false;
+end;
+
+function TAudio_Dummy.Length: real;
+begin
+ Result := 60;
+end;
+
+function TAudio_Dummy.OpenSound(const Filename: IPath): TAudioPlaybackStream;
+begin
+ Result := nil;
+end;
+
+procedure TAudio_Dummy.CloseSound(var PlaybackStream: TAudioPlaybackStream);
+begin
+end;
+
+procedure TAudio_Dummy.PlaySound(stream: TAudioPlaybackStream);
+begin
+end;
+
+procedure TAudio_Dummy.StopSound(stream: TAudioPlaybackStream);
+begin
+end;
+
+function TAudio_Dummy.CreateVoiceStream(Channel: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream;
+begin
+ Result := nil;
+end;
+
+procedure TAudio_Dummy.CloseVoiceStream(var VoiceStream: TAudioVoiceStream);
+begin
+end;
+
+
+{ TVideoPlayback_Dummy }
+
+procedure TVideo_Dummy.Close;
+begin
+end;
+
+procedure TVideo_Dummy.Play;
+begin
+end;
+
+procedure TVideo_Dummy.Pause;
+begin
+end;
+
+procedure TVideo_Dummy.Stop;
+begin
+end;
+
+procedure TVideo_Dummy.SetLoop(Enable: boolean);
+begin
+end;
+
+function TVideo_Dummy.GetLoop(): boolean;
+begin
+ Result := false;
+end;
+
+procedure TVideo_Dummy.SetPosition(Time: real);
+begin
+end;
+
+function TVideo_Dummy.GetPosition: real;
+begin
+ Result := 0;
+end;
+
+procedure TVideo_Dummy.SetScreen(Screen: integer);
+begin
+end;
+
+function TVideo_Dummy.GetScreen(): integer;
+begin
+ Result := 0;
+end;
+
+procedure TVideo_Dummy.SetScreenPosition(X, Y, Z: double);
+begin
+end;
+
+procedure TVideo_Dummy.GetScreenPosition(var X, Y, Z: double);
+begin
+ X := 0;
+ Y := 0;
+ Z := 0;
+end;
+
+procedure TVideo_Dummy.SetWidth(Width: double);
+begin
+end;
+
+function TVideo_Dummy.GetWidth(): double;
+begin
+ Result := 0;
+end;
+
+procedure TVideo_Dummy.SetHeight(Height: double);
+begin
+end;
+
+function TVideo_Dummy.GetHeight(): double;
+begin
+ Result := 0;
+end;
+
+procedure TVideo_Dummy.SetFrameRange(Range: TRectCoords);
+begin
+end;
+
+function TVideo_Dummy.GetFrameRange(): TRectCoords;
+begin
+ Result.Left := 0;
+ Result.Right := 0;
+ Result.Upper := 0;
+ Result.Lower := 0;
+end;
+
+function TVideo_Dummy.GetFrameAspect(): real;
+begin
+ Result := 0;
+end;
+
+procedure TVideo_Dummy.SetAspectCorrection(AspectCorrection: TAspectCorrection);
+begin
+end;
+
+function TVideo_Dummy.GetAspectCorrection(): TAspectCorrection;
+begin
+ Result := acoStretch;
+end;
+
+procedure TVideo_Dummy.SetAlpha(Alpha: double);
+begin
+end;
+
+function TVideo_Dummy.GetAlpha(): double;
+begin
+ Result := 0;
+end;
+
+procedure TVideo_Dummy.SetReflectionSpacing(Spacing: double);
+begin
+end;
+
+function TVideo_Dummy.GetReflectionSpacing(): double;
+begin
+ Result := 0;
+end;
+
+procedure TVideo_Dummy.GetFrame(Time: Extended);
+begin
+end;
+
+procedure TVideo_Dummy.Draw();
+begin
+end;
+
+procedure TVideo_Dummy.DrawReflection();
+begin
+end;
+
+
+{ TVideoPlayback_Dummy }
+
+constructor TVideoPlayback_Dummy.Create();
+begin
+end;
+
+function TVideoPlayback_Dummy.GetName: string;
+begin
+ Result := 'VideoDummy';
+end;
+
+function TVideoPlayback_Dummy.Init(): boolean;
+begin
+ Result := true;
+end;
+
+function TVideoPlayback_Dummy.Finalize(): boolean;
+begin
+ Result := true;
+end;
+
+function TVideoPlayback_Dummy.Open(const FileName: IPath): IVideo;
+begin
+ Result := TVideo_Dummy.Create;
+end;
+
+
+initialization
+ MediaManager.Add(TAudio_Dummy.Create);
+ MediaManager.Add(TVideoPlayback_Dummy.Create);
+
+end.
diff --git a/src/media/UVideo.pas b/src/media/UVideo.pas
new file mode 100644
index 00000000..add7bdc8
--- /dev/null
+++ b/src/media/UVideo.pas
@@ -0,0 +1,1436 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit UVideo;
+
+{*
+ * based on 'An ffmpeg and SDL Tutorial' (http://www.dranger.com/ffmpeg/)
+ *}
+
+// uncomment if you want to see the debug stuff
+{.$define DebugDisplay}
+{.$define DebugFrames}
+{.$define VideoBenchmark}
+{.$define Info}
+
+interface
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+{$I switches.inc}
+
+// use BGR-format for accelerated colorspace conversion with swscale
+{$IFDEF UseSWScale}
+ {$DEFINE PIXEL_FMT_BGR}
+{$ENDIF}
+
+implementation
+
+uses
+ SysUtils,
+ Math,
+ SDL,
+ avcodec,
+ avformat,
+ avutil,
+ avio,
+ rational,
+ {$IFDEF UseSWScale}
+ swscale,
+ {$ENDIF}
+ gl,
+ glu,
+ glext,
+ textgl,
+ UMediaCore_FFmpeg,
+ UCommon,
+ UConfig,
+ ULog,
+ UMusic,
+ UGraphicClasses,
+ UGraphic,
+ UPath;
+
+{$DEFINE PIXEL_FMT_BGR}
+
+const
+{$IFDEF PIXEL_FMT_BGR}
+ PIXEL_FMT_OPENGL = GL_BGR;
+ PIXEL_FMT_FFMPEG = PIX_FMT_BGR24;
+ PIXEL_FMT_SIZE = 3;
+
+ // looks strange on linux:
+ //PIXEL_FMT_OPENGL = GL_RGBA;
+ //PIXEL_FMT_FFMPEG = PIX_FMT_BGR32;
+ //PIXEL_FMT_SIZE = 4;
+{$ELSE}
+ // looks strange on linux:
+ PIXEL_FMT_OPENGL = GL_RGB;
+ PIXEL_FMT_FFMPEG = PIX_FMT_RGB24;
+ PIXEL_FMT_SIZE = 3;
+{$ENDIF}
+
+ ReflectionH = 0.5; //reflection height (50%)
+
+type
+ IVideo_FFmpeg = interface (IVideo)
+ ['{E640E130-C8C0-4399-AF02-67A3569313AB}']
+ function Open(const FileName: IPath): boolean;
+ end;
+
+ TVideo_FFmpeg = class( TInterfacedObject, IVideo_FFmpeg )
+ private
+ fOpened: boolean; //**< stream successfully opened
+ fPaused: boolean; //**< stream paused
+ fEOF: boolean; //**< end-of-file state
+
+ fLoop: boolean; //**< looping enabled
+
+ fStream: PAVStream;
+ fStreamIndex : integer;
+ fFormatContext: PAVFormatContext;
+ fCodecContext: PAVCodecContext;
+ fCodec: PAVCodec;
+
+ fAVFrame: PAVFrame;
+ fAVFrameRGB: PAVFrame;
+
+ fFrameBuffer: PByte; //**< stores a FFmpeg video frame
+ fFrameTex: GLuint; //**< OpenGL texture for FrameBuffer
+ fFrameTexValid: boolean; //**< if true, fFrameTex contains the current frame
+ fTexWidth, fTexHeight: cardinal;
+
+ {$IFDEF UseSWScale}
+ fSwScaleContext: PSwsContext;
+ {$ENDIF}
+
+ fScreen: integer; //actual screen to draw on
+
+ fPosX: double;
+ fPosY: double;
+ fPosZ: double;
+ fWidth: double;
+ fHeight: double;
+
+ fFrameRange: TRectCoords;
+
+ fAlpha: double;
+ fReflectionSpacing: double;
+
+
+ fAspect: real; //**< width/height ratio
+ fAspectCorrection: TAspectCorrection;
+
+ fFrameDuration: extended; //**< duration of a video frame in seconds (= 1/fps)
+ fFrameTime: extended; //**< video time position (absolute)
+ fLoopTime: extended; //**< start time of the current loop
+
+ fPboEnabled: boolean;
+ fPboId: GLuint;
+ procedure Reset();
+ function DecodeFrame(): boolean;
+ procedure SynchronizeTime(Frame: PAVFrame; var pts: double);
+
+ procedure GetVideoRect(var ScreenRect, TexRect: TRectCoords);
+ procedure DrawBorders(ScreenRect: TRectCoords);
+ procedure DrawBordersReflected(ScreenRect: TRectCoords; AlphaUpper, AlphaLower: double);
+
+ procedure ShowDebugInfo();
+
+ public
+ constructor Create;
+ destructor Destroy; override;
+
+ function Open(const FileName: IPath): boolean;
+ procedure Close;
+
+ procedure Play;
+ procedure Pause;
+ procedure Stop;
+
+ procedure SetLoop(Enable: boolean);
+ function GetLoop(): boolean;
+
+ procedure SetPosition(Time: real);
+ function GetPosition: real;
+
+ procedure SetScreen(Screen: integer);
+ function GetScreen(): integer;
+
+ procedure SetScreenPosition(X, Y, Z: double);
+ procedure GetScreenPosition(var X, Y, Z: double);
+
+ procedure SetWidth(Width: double);
+ function GetWidth(): double;
+
+ procedure SetHeight(Height: double);
+ function GetHeight(): double;
+
+ {**
+ * Sub-image of the video frame to draw.
+ * This can be used for zooming or similar purposes.
+ *}
+ procedure SetFrameRange(Range: TRectCoords);
+ function GetFrameRange(): TRectCoords;
+
+ function GetFrameAspect(): real;
+
+ procedure SetAspectCorrection(AspectCorrection: TAspectCorrection);
+ function GetAspectCorrection(): TAspectCorrection;
+
+ procedure SetAlpha(Alpha: double);
+ function GetAlpha(): double;
+
+ procedure SetReflectionSpacing(Spacing: double);
+ function GetReflectionSpacing(): double;
+
+ procedure GetFrame(Time: Extended);
+ procedure Draw();
+ procedure DrawReflection();
+ end;
+
+ TVideoPlayback_FFmpeg = class( TInterfacedObject, IVideoPlayback )
+ private
+ fInitialized: boolean;
+
+ public
+ function GetName: String;
+
+ function Init(): boolean;
+ function Finalize: boolean;
+
+ function Open(const FileName : IPath): IVideo;
+ end;
+
+var
+ 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(CodecCtx: PAVCodecContext; Frame: PAVFrame): integer; cdecl;
+var
+ pts: Pint64;
+ VideoPktPts: Pint64;
+begin
+ 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^;
+ Frame^.opaque := pts;
+ end;
+end;
+
+procedure PtsReleaseBuffer(CodecCtx: PAVCodecContext; Frame: PAVFrame); cdecl;
+begin
+ if (Frame <> nil) then
+ av_freep(@Frame^.opaque);
+ avcodec_default_release_buffer(CodecCtx, Frame);
+end;
+
+
+{*------------------------------------------------------------------------------
+ * TVideoPlayback_ffmpeg
+ *------------------------------------------------------------------------------}
+
+function TVideoPlayback_FFmpeg.GetName: String;
+begin
+ result := 'FFmpeg_Video';
+end;
+
+function TVideoPlayback_FFmpeg.Init(): boolean;
+begin
+ Result := true;
+
+ if (fInitialized) then
+ Exit;
+ fInitialized := true;
+
+ FFmpegCore := TMediaCore_FFmpeg.GetInstance();
+
+ av_register_all();
+end;
+
+function TVideoPlayback_FFmpeg.Finalize(): boolean;
+begin
+ Result := true;
+end;
+
+function TVideoPlayback_FFmpeg.Open(const FileName : IPath): IVideo;
+var
+ Video: IVideo_FFmpeg;
+begin
+ Video := TVideo_FFmpeg.Create;
+ if Video.Open(FileName) then
+ Result := Video
+ else
+ Result := nil;
+end;
+
+
+{* TVideo_FFmpeg *}
+
+constructor TVideo_FFmpeg.Create;
+begin
+ glGenTextures(1, PGLuint(@fFrameTex));
+ Reset();
+end;
+
+destructor TVideo_FFmpeg.Destroy;
+begin
+ Close();
+ glDeleteTextures(1, PGLuint(@fFrameTex));
+end;
+
+function TVideo_FFmpeg.Open(const FileName : IPath): boolean;
+var
+ errnum: Integer;
+ glErr: GLenum;
+ AudioStreamIndex: integer;
+begin
+ Result := false;
+ Reset();
+
+ fPboEnabled := PboSupported;
+
+ // use custom 'ufile' protocol for UTF-8 support
+ errnum := av_open_input_file(fFormatContext, PAnsiChar('ufile:'+FileName.ToUTF8), nil, 0, nil);
+ if (errnum <> 0) then
+ begin
+ Log.LogError('Failed to open file "'+ FileName.ToNative +'" ('+FFmpegCore.GetErrorString(errnum)+')');
+ Exit;
+ end;
+
+ // update video info
+ if (av_find_stream_info(fFormatContext) < 0) then
+ begin
+ Log.LogError('No stream info found', 'TVideoPlayback_ffmpeg.Open');
+ Close();
+ Exit;
+ end;
+ Log.LogInfo('VideoStreamIndex : ' + inttostr(fStreamIndex), 'TVideoPlayback_ffmpeg.Open');
+
+ // find video stream
+ FFmpegCore.FindStreamIDs(fFormatContext, fStreamIndex, AudioStreamIndex);
+ if (fStreamIndex < 0) then
+ begin
+ Log.LogError('No video stream found', 'TVideoPlayback_ffmpeg.Open');
+ Close();
+ Exit;
+ end;
+
+ fStream := fFormatContext^.streams[fStreamIndex];
+ fCodecContext := fStream^.codec;
+
+ fCodec := avcodec_find_decoder(fCodecContext^.codec_id);
+ if (fCodec = nil) then
+ begin
+ Log.LogError('No matching codec found', 'TVideoPlayback_ffmpeg.Open');
+ Close();
+ Exit;
+ end;
+
+ // set debug options
+ fCodecContext^.debug_mv := 0;
+ fCodecContext^.debug := 0;
+
+ // detect bug-workarounds automatically
+ fCodecContext^.workaround_bugs := FF_BUG_AUTODETECT;
+ // error resilience strategy (careful/compliant/agressive/very_aggressive)
+ //fCodecContext^.error_resilience := FF_ER_CAREFUL; //FF_ER_COMPLIANT;
+ // allow non spec compliant speedup tricks.
+ //fCodecContext^.flags2 := fCodecContext^.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(fCodecContext, fCodec);
+ 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
+ fCodecContext^.get_buffer := PtsGetBuffer;
+ fCodecContext^.release_buffer := PtsReleaseBuffer;
+
+ {$ifdef DebugDisplay}
+ DebugWriteln('Found a matching Codec: '+ fCodecContext^.Codec.Name + sLineBreak +
+ sLineBreak +
+ ' Width = '+inttostr(fCodecContext^.width) +
+ ', Height='+inttostr(fCodecContext^.height) + sLineBreak +
+ ' Aspect : '+inttostr(fCodecContext^.sample_aspect_ratio.num) + '/' +
+ inttostr(fCodecContext^.sample_aspect_ratio.den) + sLineBreak +
+ ' Framerate : '+inttostr(fCodecContext^.time_base.num) + '/' +
+ inttostr(fCodecContext^.time_base.den));
+ {$endif}
+
+ // allocate space for decoded frame and rgb frame
+ fAVFrame := avcodec_alloc_frame();
+ fAVFrameRGB := avcodec_alloc_frame();
+ fFrameBuffer := av_malloc(avpicture_get_size(PIXEL_FMT_FFMPEG,
+ fCodecContext^.width, fCodecContext^.height));
+
+ if ((fAVFrame = nil) or (fAVFrameRGB = nil) or (fFrameBuffer = 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(fAVFrameRGB), fFrameBuffer, PIXEL_FMT_FFMPEG,
+ fCodecContext^.width, fCodecContext^.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
+ fAspect := av_q2d(fCodecContext^.sample_aspect_ratio);
+ if (fAspect = 0) then
+ fAspect := fCodecContext^.width /
+ fCodecContext^.height
+ else
+ fAspect := fAspect * fCodecContext^.width /
+ fCodecContext^.height;
+
+ fFrameDuration := 1/av_q2d(fStream^.r_frame_rate);
+
+ // hack to get reasonable framerate (for divx and others)
+ if (fFrameDuration < 0.02) then // 0.02 <-> 50 fps
+ begin
+ fFrameDuration := av_q2d(fStream^.r_frame_rate);
+ while (fFrameDuration > 50) do
+ fFrameDuration := fFrameDuration/10;
+ fFrameDuration := 1/fFrameDuration;
+ end;
+
+ Log.LogInfo('Framerate: '+inttostr(floor(1/fFrameDuration))+'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.
+ fSwScaleContext := sws_getContext(
+ fCodecContext^.width, fCodecContext^.height,
+ fCodecContext^.pix_fmt,
+ fCodecContext^.width, fCodecContext^.height,
+ PIXEL_FMT_FFMPEG,
+ SWS_FAST_BILINEAR, nil, nil, nil);
+ if (fSwScaleContext = nil) then
+ begin
+ Log.LogError('Failed to get swscale context', 'TVideoPlayback_ffmpeg.Open');
+ Close();
+ Exit;
+ end;
+ {$ENDIF}
+
+ fTexWidth := Round(Power(2, Ceil(Log2(fCodecContext^.width))));
+ fTexHeight := Round(Power(2, Ceil(Log2(fCodecContext^.height))));
+
+ if (fPboEnabled) then
+ begin
+ glGetError();
+
+ glGenBuffersARB(1, @fPboId);
+ glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, fPboId);
+ glBufferDataARB(
+ GL_PIXEL_UNPACK_BUFFER_ARB,
+ fCodecContext^.width * fCodecContext^.height * PIXEL_FMT_SIZE,
+ nil,
+ GL_STREAM_DRAW_ARB);
+ glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, 0);
+
+ glErr := glGetError();
+ if (glErr <> GL_NO_ERROR) then
+ begin
+ fPboEnabled := false;
+ Log.LogError('PBO initialization failed: ' + gluErrorString(glErr), 'TVideo_FFmpeg.Open');
+ end;
+ end;
+
+ // 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, fFrameTex);
+ glTexImage2D(GL_TEXTURE_2D, 0, 3, fTexWidth, fTexHeight, 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);
+
+ fOpened := true;
+ Result := true;
+end;
+
+procedure TVideo_FFmpeg.Reset();
+begin
+ // close previously opened video
+ Close();
+
+ fOpened := False;
+ fPaused := False;
+ fFrameDuration := 0;
+ fFrameTime := 0;
+ fStream := nil;
+ fStreamIndex := -1;
+ fFrameTexValid := false;
+
+ fEOF := false;
+
+ fLoop := false;
+ fLoopTime := 0;
+
+ fPboId := 0;
+
+ fAspectCorrection := acoCrop;
+
+ fScreen := 1;
+
+ fPosX := 0;
+ fPosY := 0;
+ fPosZ := 0;
+ fWidth := RenderW;
+ fHeight := RenderH;
+
+ fFrameRange.Left := 0;
+ fFrameRange.Right := 1;
+ fFrameRange.Upper := 0;
+ fFrameRange.Lower := 1;
+
+ fAlpha := 1;
+ fReflectionSpacing := 0;
+end;
+
+procedure TVideo_FFmpeg.Close;
+begin
+ if (fFrameBuffer <> nil) then
+ av_free(fFrameBuffer);
+ if (fAVFrameRGB <> nil) then
+ av_free(fAVFrameRGB);
+ if (fAVFrame <> nil) then
+ av_free(fAVFrame);
+
+ fAVFrame := nil;
+ fAVFrameRGB := nil;
+ fFrameBuffer := nil;
+
+ if (fCodecContext <> nil) then
+ begin
+ // avcodec_close() is not thread-safe
+ FFmpegCore.LockAVCodec();
+ try
+ avcodec_close(fCodecContext);
+ finally
+ FFmpegCore.UnlockAVCodec();
+ end;
+ end;
+
+ if (fFormatContext <> nil) then
+ av_close_input_file(fFormatContext);
+
+ fCodecContext := nil;
+ fFormatContext := nil;
+
+ if (fPboId <> 0) then
+ glDeleteBuffersARB(1, @fPboId);
+
+ fOpened := False;
+end;
+
+procedure TVideo_FFmpeg.SynchronizeTime(Frame: PAVFrame; var pts: double);
+var
+ FrameDelay: double;
+begin
+ if (pts <> 0) then
+ begin
+ // if we have pts, set video clock to it
+ fFrameTime := pts;
+ end else
+ begin
+ // if we aren't given a pts, set it to the clock
+ pts := fFrameTime;
+ end;
+ // update the video clock
+ FrameDelay := av_q2d(fCodecContext^.time_base);
+ // if we are repeating a frame, adjust clock accordingly
+ FrameDelay := FrameDelay + Frame^.repeat_pict * (FrameDelay * 0.5);
+ fFrameTime := fFrameTime + FrameDelay;
+end;
+
+{**
+ * Decode a new frame from the video stream.
+ * The decoded frame is stored in fAVFrame. fFrameTime is updated to the new frame's
+ * time.
+ * @param pts will be updated to the presentation time of the decoded frame.
+ * returns true if a frame could be decoded. False if an error or EOF occured.
+ *}
+function TVideo_FFmpeg.DecodeFrame(): boolean;
+var
+ FrameFinished: Integer;
+ VideoPktPts: int64;
+ pbIOCtx: PByteIOContext;
+ errnum: integer;
+ AVPacket: TAVPacket;
+ pts: double;
+begin
+ Result := false;
+ FrameFinished := 0;
+
+ if fEOF then
+ Exit;
+
+ // read packets until we have a finished frame (or there are no more packets)
+ while (FrameFinished = 0) do
+ begin
+ errnum := av_read_frame(fFormatContext, AVPacket);
+ if (errnum < 0) then
+ begin
+ // failed to read a frame, check reason
+
+ {$IF (LIBAVFORMAT_VERSION_MAJOR >= 52)}
+ pbIOCtx := fFormatContext^.pb;
+ {$ELSE}
+ pbIOCtx := @fFormatContext^.pb;
+ {$IFEND}
+
+ // check for end-of-file (EOF is not an error)
+ if (url_feof(pbIOCtx) <> 0) then
+ begin
+ fEOF := true;
+ Exit;
+ end;
+
+ // check for errors
+ if (url_ferror(pbIOCtx) <> 0) then
+ begin
+ Log.LogError('Video decoding file error', 'TVideoPlayback_FFmpeg.DecodeFrame');
+ Exit;
+ end;
+
+ // url_feof() does not detect an EOF for some mov-files (e.g. deluxe.mov)
+ // so we have to do it this way.
+ if ((fFormatContext^.file_size <> 0) and
+ (pbIOCtx^.pos >= fFormatContext^.file_size)) then
+ begin
+ fEOF := true;
+ Exit;
+ end;
+
+ // error occured, log and exit
+ Log.LogError('Video decoding error', 'TVideoPlayback_FFmpeg.DecodeFrame');
+ Exit;
+ end;
+
+ // if we got a packet from the video stream, then decode it
+ if (AVPacket.stream_index = fStreamIndex) then
+ begin
+ // save pts to be stored in pFrame in first call of PtsGetBuffer()
+ VideoPktPts := AVPacket.pts;
+ fCodecContext^.opaque := @VideoPktPts;
+
+ // decode packet
+ avcodec_decode_video(fCodecContext, fAVFrame,
+ frameFinished, AVPacket.data, AVPacket.size);
+
+ // reset opaque data
+ fCodecContext^.opaque := nil;
+
+ // update pts
+ if (AVPacket.dts <> AV_NOPTS_VALUE) then
+ begin
+ pts := AVPacket.dts;
+ end
+ else if ((fAVFrame^.opaque <> nil) and
+ (Pint64(fAVFrame^.opaque)^ <> AV_NOPTS_VALUE)) then
+ begin
+ pts := Pint64(fAVFrame^.opaque)^;
+ end
+ else
+ begin
+ pts := 0;
+ end;
+
+ if fStream^.start_time <> AV_NOPTS_VALUE then
+ pts := pts - fStream^.start_time;
+
+ pts := pts * av_q2d(fStream^.time_base);
+
+ // synchronize time on each complete frame
+ if (frameFinished <> 0) then
+ SynchronizeTime(fAVFrame, pts);
+ end;
+
+ // free the packet from av_read_frame
+ av_free_packet( @AVPacket );
+ end;
+
+ Result := true;
+end;
+
+procedure TVideo_FFmpeg.GetFrame(Time: Extended);
+var
+ errnum: Integer;
+ glErr: GLenum;
+ CurrentTime: Extended;
+ TimeDiff: Extended;
+ DropFrameCount: Integer;
+ i: Integer;
+ Success: boolean;
+ BufferPtr: PGLvoid;
+const
+ SKIP_FRAME_DIFF = 0.010; // start skipping if we are >= 10ms too late
+begin
+ if not fOpened then
+ Exit;
+
+ if fPaused then
+ Exit;
+
+ {*
+ * Synchronization - begin
+ *}
+
+ // requested stream position (relative to the last loop's start)
+ if (fLoop) then
+ CurrentTime := Time - fLoopTime
+ else
+ CurrentTime := Time;
+
+ // check if current texture still contains the active frame
+ if (fFrameTexValid) then
+ begin
+ // time since the last frame was returned
+ TimeDiff := CurrentTime - fFrameTime;
+
+ {$IFDEF DebugDisplay}
+ DebugWriteln('Time: '+inttostr(floor(Time*1000)) + sLineBreak +
+ 'VideoTime: '+inttostr(floor(fFrameTime*1000)) + sLineBreak +
+ 'TimeBase: '+inttostr(floor(fFrameDuration*1000)) + sLineBreak +
+ 'TimeDiff: '+inttostr(floor(TimeDifference*1000)));
+ {$endif}
+
+ // check if time has reached the next frame
+ if (TimeDiff < fFrameDuration) then
+ begin
+ {$ifdef DebugFrames}
+ // frame delay debug display
+ GoldenRec.Spawn(200,15,1,16,0,-1,ColoredStar,$00ff00);
+ {$endif}
+
+ {$IFDEF DebugDisplay}
+ DebugWriteln('not getting new frame' + sLineBreak +
+ 'Time: '+inttostr(floor(Time*1000)) + sLineBreak +
+ 'VideoTime: '+inttostr(floor(fFrameTime*1000)) + sLineBreak +
+ 'TimeBase: '+inttostr(floor(fFrameDuration*1000)) + sLineBreak +
+ 'TimeDiff: '+inttostr(floor(TimeDifference*1000)));
+ {$endif}
+
+ // we do not need a new frame now
+ Exit;
+ end;
+ end;
+
+ {$IFDEF VideoBenchmark}
+ Log.BenchmarkStart(15);
+ {$ENDIF}
+
+ // fetch new frame (updates fFrameTime)
+ Success := DecodeFrame();
+ TimeDiff := CurrentTime - fFrameTime;
+
+ // check if we have to skip frames
+ // Either if we are one frame behind or if the skip threshold has been reached.
+ // Do not skip if the difference is less than fFrameDuration as there is no next frame.
+ // Note: We assume that fFrameDuration is the length of one frame.
+ if (TimeDiff >= Max(fFrameDuration, SKIP_FRAME_DIFF)) then
+ begin
+ {$IFDEF DebugFrames}
+ //frame drop debug display
+ GoldenRec.Spawn(200,55,1,16,0,-1,ColoredStar,$ff0000);
+ {$ENDIF}
+ {$IFDEF DebugDisplay}
+ DebugWriteln('skipping frames' + sLineBreak +
+ 'TimeBase: '+inttostr(floor(fFrameDuration*1000)) + sLineBreak +
+ 'TimeDiff: '+inttostr(floor(TimeDifference*1000)));
+ {$endif}
+
+ // update video-time
+ DropFrameCount := Trunc(TimeDiff / fFrameDuration);
+ fFrameTime := fFrameTime + DropFrameCount*fFrameDuration;
+
+ // skip frames
+ for i := 1 to DropFrameCount do
+ Success := DecodeFrame();
+ end;
+
+ // check if we got an EOF or error
+ if (not Success) then
+ begin
+ if fLoop then
+ begin
+ // we have to loop, so rewind
+ SetPosition(0);
+ // record the start-time of the current loop, so we can
+ // determine the position in the stream (fFrameTime-fLoopTime) later.
+ fLoopTime := Time;
+ end;
+ Exit;
+ end;
+
+ {*
+ * Synchronization - end
+ *}
+
+ // TODO: support for pan&scan
+ //if (fAVFrame.pan_scan <> nil) then
+ //begin
+ // Writeln(Format('PanScan: %d/%d', [fAVFrame.pan_scan.width, fAVFrame.pan_scan.height]));
+ //end;
+
+ // otherwise we convert the pixeldata from YUV to RGB
+ {$IFDEF UseSWScale}
+ errnum := sws_scale(fSwScaleContext, @fAVFrame.data, @fAVFrame.linesize,
+ 0, fCodecContext^.Height,
+ @fAVFrameRGB.data, @fAVFrameRGB.linesize);
+ {$ELSE}
+ // img_convert from lib/ffmpeg/avcodec.pas is actually deprecated.
+ // If ./configure does not find SWScale then this gives the error
+ // that the identifier img_convert is not known or similar.
+ // I think this should be removed, but am not sure whether there should
+ // be some other replacement or a warning, Therefore, I leave it for now.
+ // April 2009, mischi
+ errnum := img_convert(PAVPicture(fAVFrameRGB), PIXEL_FMT_FFMPEG,
+ PAVPicture(fAVFrame), fCodecContext^.pix_fmt,
+ fCodecContext^.width, fCodecContext^.height);
+ {$ENDIF}
+
+ if (errnum < 0) then
+ begin
+ Log.LogError('Image conversion failed', 'TVideoPlayback_ffmpeg.GetFrame');
+ Exit;
+ end;
+
+ {$IFDEF VideoBenchmark}
+ Log.BenchmarkEnd(15);
+ Log.BenchmarkStart(16);
+ {$ENDIF}
+
+ // TODO: data is not padded, so we will need to tell OpenGL.
+ // Or should we add padding with avpicture_fill? (check which one is faster)
+ //glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+
+ // glTexEnvi with GL_REPLACE might give a small speed improvement
+ glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
+
+ if (not fPboEnabled) then
+ begin
+ glBindTexture(GL_TEXTURE_2D, fFrameTex);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0,
+ fCodecContext^.width, fCodecContext^.height,
+ PIXEL_FMT_OPENGL, GL_UNSIGNED_BYTE, fAVFrameRGB^.data[0]);
+ end
+ else // fPboEnabled
+ begin
+ glGetError();
+
+ glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, fPboId);
+ glBufferDataARB(GL_PIXEL_UNPACK_BUFFER_ARB,
+ fCodecContext^.height * fCodecContext^.width * PIXEL_FMT_SIZE,
+ nil,
+ GL_STREAM_DRAW_ARB);
+
+ bufferPtr := glMapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, GL_WRITE_ONLY_ARB);
+ if(bufferPtr <> nil) then
+ begin
+ Move(fAVFrameRGB^.data[0]^, bufferPtr^,
+ fCodecContext^.height * fCodecContext^.width * PIXEL_FMT_SIZE);
+
+ // release pointer to mapping buffer
+ glUnmapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB);
+ end;
+
+ glBindTexture(GL_TEXTURE_2D, fFrameTex);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0,
+ fCodecContext^.width, fCodecContext^.height,
+ PIXEL_FMT_OPENGL, GL_UNSIGNED_BYTE, nil);
+
+ glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, 0);
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ glErr := glGetError();
+ if (glErr <> GL_NO_ERROR) then
+ Log.LogError('PBO texture stream error: ' + gluErrorString(glErr), 'TVideo_FFmpeg.GetFrame');
+ end;
+
+ // reset to default
+ glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
+
+ if (not fFrameTexValid) then
+ fFrameTexValid := true;
+
+ {$ifdef DebugFrames}
+ //frame decode debug display
+ GoldenRec.Spawn(200, 35, 1, 16, 0, -1, ColoredStar, $ffff00);
+ {$endif}
+
+ {$IFDEF VideoBenchmark}
+ Log.BenchmarkEnd(16);
+ Log.LogBenchmark('FFmpeg', 15);
+ Log.LogBenchmark('Texture', 16);
+ {$ENDIF}
+end;
+
+procedure TVideo_FFmpeg.GetVideoRect(var ScreenRect, TexRect: TRectCoords);
+var
+ ScreenAspect: double; // aspect of screen resolution
+ ScaledVideoWidth, ScaledVideoHeight: double;
+
+begin
+ // Three aspects to take into account:
+ // 1. Screen/display resolution (e.g. 1920x1080 -> 16:9)
+ // 2. Render aspect (fWidth x fHeight -> variable)
+ // 3. Movie aspect (video frame aspect stored in fAspect)
+ ScreenAspect := fWidth*((ScreenW/Screens)/RenderW)/(fHeight*(ScreenH/RenderH));
+
+ case fAspectCorrection of
+ acoStretch: begin
+ ScaledVideoWidth := fWidth;
+ ScaledVideoHeight := fHeight;
+ end;
+
+ acoCrop: begin
+ if (ScreenAspect >= fAspect) then
+ begin
+ ScaledVideoWidth := fWidth;
+ ScaledVideoHeight := fHeight * ScreenAspect/fAspect;
+ end else
+ begin
+ ScaledVideoHeight := fHeight;
+ ScaledVideoWidth := fWidth * fAspect/ScreenAspect;
+ end;
+ end;
+
+ acoLetterBox: begin
+ if (ScreenAspect <= fAspect) then
+ begin
+ ScaledVideoWidth := fWidth;
+ ScaledVideoHeight := fHeight * ScreenAspect/fAspect;
+ end else
+ begin
+ ScaledVideoHeight := fHeight;
+ ScaledVideoWidth := fWidth * fAspect/ScreenAspect;
+ end;
+ end else
+ raise Exception.Create('Unhandled aspect correction!');
+ end;
+
+ //center video
+ ScreenRect.Left := (fWidth - ScaledVideoWidth) / 2 + fPosX;
+ ScreenRect.Right := ScreenRect.Left + ScaledVideoWidth;
+ ScreenRect.Upper := (fHeight - ScaledVideoHeight) / 2 + fPosY;
+ ScreenRect.Lower := ScreenRect.Upper + ScaledVideoHeight;
+
+ // texture contains right/lower (power-of-2) padding.
+ // Determine the texture coords of the video frame.
+ TexRect.Left := (fCodecContext^.width / fTexWidth) * fFrameRange.Left;
+ TexRect.Right := (fCodecContext^.width / fTexWidth) * fFrameRange.Right;
+ TexRect.Upper := (fCodecContext^.height / fTexHeight) * fFrameRange.Upper;
+ TexRect.Lower := (fCodecContext^.height / fTexHeight) * fFrameRange.Lower;
+end;
+
+procedure TVideo_FFmpeg.DrawBorders(ScreenRect: TRectCoords);
+ procedure DrawRect(left, right, upper, lower: double);
+ begin
+ glColor4f(0, 0, 0, fAlpha);
+ glBegin(GL_QUADS);
+ glVertex3f(left, upper, fPosZ);
+ glVertex3f(right, upper, fPosZ);
+ glVertex3f(right, lower, fPosZ);
+ glVertex3f(left, lower, fPosZ);
+ glEnd;
+ end;
+begin
+ //upper border
+ if(ScreenRect.Upper > fPosY) then
+ DrawRect(fPosX, fPosX+fWidth, fPosY, ScreenRect.Upper);
+
+ //lower border
+ if(ScreenRect.Lower < fPosY+fHeight) then
+ DrawRect(fPosX, fPosX+fWidth, ScreenRect.Lower, fPosY+fHeight);
+
+ //left border
+ if(ScreenRect.Left > fPosX) then
+ DrawRect(fPosX, ScreenRect.Left, fPosY, fPosY+fHeight);
+
+ //right border
+ if(ScreenRect.Right < fPosX+fWidth) then
+ DrawRect(ScreenRect.Right, fPosX+fWidth, fPosY, fPosY+fHeight);
+end;
+
+procedure TVideo_FFmpeg.DrawBordersReflected(ScreenRect: TRectCoords; AlphaUpper, AlphaLower: double);
+var
+ rPosUpper, rPosLower: double;
+
+ procedure DrawRect(left, right, upper, lower: double);
+ var
+ AlphaTop: double;
+ AlphaBottom: double;
+
+ begin
+ AlphaTop := AlphaUpper+(AlphaLower-AlphaUpper)*(upper-rPosUpper)/(fHeight*ReflectionH);
+ AlphaBottom := AlphaLower+(AlphaUpper-AlphaLower)*(rPosLower-lower)/(fHeight*ReflectionH);
+
+ glBegin(GL_QUADS);
+ glColor4f(0, 0, 0, AlphaTop);
+ glVertex3f(left, upper, fPosZ);
+ glVertex3f(right, upper, fPosZ);
+
+ glColor4f(0, 0, 0, AlphaBottom);
+ glVertex3f(right, lower, fPosZ);
+ glVertex3f(left, lower, fPosZ);
+ glEnd;
+ end;
+begin
+ rPosUpper := fPosY+fHeight+fReflectionSpacing;
+ rPosLower := rPosUpper+fHeight*ReflectionH;
+
+ //upper border
+ if(ScreenRect.Upper > rPosUpper) then
+ DrawRect(fPosX, fPosX+fWidth, rPosUpper, ScreenRect.Upper);
+
+ //lower border
+ if(ScreenRect.Lower < rPosLower) then
+ DrawRect(fPosX, fPosX+fWidth, ScreenRect.Lower, rPosLower);
+
+ //left border
+ if(ScreenRect.Left > fPosX) then
+ DrawRect(fPosX, ScreenRect.Left, rPosUpper, rPosLower);
+
+ //right border
+ if(ScreenRect.Right < fPosX+fWidth) then
+ DrawRect(ScreenRect.Right, fPosX+fWidth, rPosUpper, rPosLower);
+end;
+
+
+procedure TVideo_FFmpeg.Draw();
+var
+ ScreenRect: TRectCoords;
+ TexRect: TRectCoords;
+ HeightFactor: double;
+ WidthFactor: double;
+
+begin
+ // exit if there's nothing to draw
+ if (not fOpened) then
+ Exit;
+
+ {$IFDEF VideoBenchmark}
+ Log.BenchmarkStart(15);
+ {$ENDIF}
+
+ // get texture and screen positions
+ GetVideoRect(ScreenRect, TexRect);
+
+ WidthFactor := (ScreenW/Screens) / RenderW;
+ HeightFactor := ScreenH / RenderH;
+
+ glScissor(
+ round(fPosX*WidthFactor + (ScreenW/Screens)*(fScreen-1)),
+ round((RenderH-fPosY-fHeight)*HeightFactor),
+ round(fWidth*WidthFactor),
+ round(fHeight*HeightFactor)
+ );
+
+ glEnable(GL_SCISSOR_TEST);
+ glEnable(GL_BLEND);
+ glDepthRange(0, 10);
+ glDepthFunc(GL_LEQUAL);
+ glEnable(GL_DEPTH_TEST);
+
+ glEnable(GL_TEXTURE_2D);
+ glBindTexture(GL_TEXTURE_2D, fFrameTex);
+ glColor4f(1, 1, 1, fAlpha);
+ glBegin(GL_QUADS);
+ // upper-left coord
+ glTexCoord2f(TexRect.Left, TexRect.Upper);
+ glVertex3f(ScreenRect.Left, ScreenRect.Upper, fPosZ);
+ // lower-left coord
+ glTexCoord2f(TexRect.Left, TexRect.Lower);
+ glVertex3f(ScreenRect.Left, ScreenRect.Lower, fPosZ);
+ // lower-right coord
+ glTexCoord2f(TexRect.Right, TexRect.Lower);
+ glVertex3f(ScreenRect.Right, ScreenRect.Lower, fPosZ);
+ // upper-right coord
+ glTexCoord2f(TexRect.Right, TexRect.Upper);
+ glVertex3f(ScreenRect.Right, ScreenRect.Upper, fPosZ);
+ glEnd;
+
+ glDisable(GL_TEXTURE_2D);
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ //draw black borders
+ DrawBorders(ScreenRect);
+
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_BLEND);
+ glDisable(GL_SCISSOR_TEST);
+
+ {$IFDEF VideoBenchmark}
+ Log.BenchmarkEnd(15);
+ Log.LogBenchmark('Draw', 15);
+ {$ENDIF}
+
+ {$IF Defined(Info) or Defined(DebugFrames)}
+ ShowDebugInfo();
+ {$IFEND}
+end;
+
+procedure TVideo_FFmpeg.DrawReflection();
+var
+ ScreenRect: TRectCoords;
+ TexRect: TRectCoords;
+ HeightFactor: double;
+ WidthFactor: double;
+
+ AlphaTop: double;
+ AlphaBottom: double;
+
+ AlphaUpper: double;
+ AlphaLower: double;
+
+begin
+ // exit if there's nothing to draw
+ if (not fOpened) then
+ Exit;
+
+ // get texture and screen positions
+ GetVideoRect(ScreenRect, TexRect);
+
+ WidthFactor := (ScreenW/Screens) / RenderW;
+ HeightFactor := ScreenH / RenderH;
+
+ glScissor(
+ round(fPosX*WidthFactor + (ScreenW/Screens)*(fScreen-1)),
+ round((RenderH-fPosY-fHeight-fReflectionSpacing-fHeight*ReflectionH)*HeightFactor),
+ round(fWidth*WidthFactor),
+ round(fHeight*HeightFactor*ReflectionH)
+ );
+
+ glEnable(GL_SCISSOR_TEST);
+ glEnable(GL_BLEND);
+ glDepthRange(0, 10);
+ glDepthFunc(GL_LEQUAL);
+ glEnable(GL_DEPTH_TEST);
+
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glEnable(GL_TEXTURE_2D);
+ glBindTexture(GL_TEXTURE_2D, fFrameTex);
+
+ //calculate new ScreenRect coordinates for Reflection
+ ScreenRect.Lower := fPosY + fHeight + fReflectionSpacing
+ + (ScreenRect.Upper-fPosY) + (ScreenRect.Lower-ScreenRect.Upper)*ReflectionH;
+ ScreenRect.Upper := fPosY + fHeight + fReflectionSpacing
+ + (ScreenRect.Upper-fPosY);
+
+ AlphaUpper := fAlpha-0.3;
+ AlphaLower := 0;
+
+ AlphaTop := AlphaUpper-(AlphaLower-AlphaUpper)*
+ (ScreenRect.Upper-fPosY-fHeight-fReflectionSpacing)/fHeight;
+ AlphaBottom := AlphaLower+(AlphaUpper-AlphaLower)*
+ (fPosY+fHeight+fReflectionSpacing+fHeight*ReflectionH-ScreenRect.Lower)/fHeight;
+
+ glBegin(GL_QUADS);
+ //Top Left
+ glColor4f(1, 1, 1, AlphaTop);
+ glTexCoord2f(TexRect.Left, TexRect.Lower);
+ glVertex3f(ScreenRect.Left, ScreenRect.Upper, fPosZ);
+
+ //Bottom Left
+ glColor4f(1, 1, 1, AlphaBottom);
+ glTexCoord2f(TexRect.Left, (TexRect.Lower-TexRect.Upper)*(1-ReflectionH));
+ glVertex3f(ScreenRect.Left, ScreenRect.Lower, fPosZ);
+
+ //Bottom Right
+ glColor4f(1, 1, 1, AlphaBottom);
+ glTexCoord2f(TexRect.Right, (TexRect.Lower-TexRect.Upper)*(1-ReflectionH));
+ glVertex3f(ScreenRect.Right, ScreenRect.Lower, fPosZ);
+
+ //Top Right
+ glColor4f(1, 1, 1, AlphaTop);
+ glTexCoord2f(TexRect.Right, TexRect.Lower);
+ glVertex3f(ScreenRect.Right, ScreenRect.Upper, fPosZ);
+ glEnd;
+
+ glDisable(GL_TEXTURE_2D);
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ //draw black borders
+ DrawBordersReflected(ScreenRect, AlphaUpper, AlphaLower);
+
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_BLEND);
+ glDisable(GL_SCISSOR_TEST);
+end;
+
+procedure TVideo_FFmpeg.ShowDebugInfo();
+begin
+ {$IFDEF Info}
+ if (fFrameTime+fFrameDuration < 0) then
+ begin
+ glColor4f(0.7, 1, 0.3, 1);
+ SetFontStyle (1);
+ SetFontItalic(False);
+ SetFontSize(27);
+ SetFontPos (300, 0);
+ glPrint('Delay due to negative VideoGap');
+ glColor4f(1, 1, 1, 1);
+ end;
+ {$ENDIF}
+
+ {$IFDEF DebugFrames}
+ glColor4f(0, 0, 0, 0.2);
+ glbegin(GL_QUADS);
+ glVertex2f(0, 0);
+ glVertex2f(0, 70);
+ glVertex2f(250, 70);
+ glVertex2f(250, 0);
+ glEnd;
+
+ glColor4f(1, 1, 1, 1);
+ SetFontStyle (1);
+ SetFontItalic(False);
+ SetFontSize(27);
+ SetFontPos (5, 0);
+ glPrint('delaying frame');
+ SetFontPos (5, 20);
+ glPrint('fetching frame');
+ SetFontPos (5, 40);
+ glPrint('dropping frame');
+ {$ENDIF}
+end;
+
+procedure TVideo_FFmpeg.Play;
+begin
+end;
+
+procedure TVideo_FFmpeg.Pause;
+begin
+ fPaused := not fPaused;
+end;
+
+procedure TVideo_FFmpeg.Stop;
+begin
+end;
+
+procedure TVideo_FFmpeg.SetLoop(Enable: boolean);
+begin
+ fLoop := Enable;
+ fLoopTime := 0;
+end;
+
+function TVideo_FFmpeg.GetLoop(): boolean;
+begin
+ Result := fLoop;
+end;
+
+{**
+ * Sets the stream's position.
+ * The stream is set to the first keyframe with timestamp <= Time.
+ * Note that fFrameTime is set to Time no matter if the actual position seeked to is
+ * at Time or the time of a preceding keyframe. fFrameTime will be updated to the
+ * actual frame time when GetFrame() is called the next time.
+ * @param Time new position in seconds
+ *}
+procedure TVideo_FFmpeg.SetPosition(Time: real);
+var
+ SeekFlags: integer;
+begin
+ if not fOpened then
+ Exit;
+
+ if (Time < 0) then
+ Time := 0;
+
+ // TODO: handle fLoop-times
+ //Time := Time mod VideoDuration;
+
+ // Do not use the AVSEEK_FLAG_ANY here. It will seek to any frame, even
+ // non keyframes (P-/B-frames). It will produce corrupted video frames as
+ // FFmpeg does not use the information of the preceding I-frame.
+ // The picture might be gray or green until the next keyframe occurs.
+ // Instead seek the first keyframe smaller than the requested time
+ // (AVSEEK_FLAG_BACKWARD). As this can be some seconds earlier than the
+ // requested time, let the sync in GetFrame() do its job.
+ SeekFlags := AVSEEK_FLAG_BACKWARD;
+
+ fFrameTime := Time;
+ fEOF := false;
+ fFrameTexValid := false;
+
+ if (av_seek_frame(fFormatContext,
+ fStreamIndex,
+ Round(Time / av_q2d(fStream^.time_base)),
+ SeekFlags) < 0) then
+ begin
+ Log.LogError('av_seek_frame() failed', 'TVideoPlayback_ffmpeg.SetPosition');
+ Exit;
+ end;
+
+ avcodec_flush_buffers(fCodecContext);
+end;
+
+function TVideo_FFmpeg.GetPosition: real;
+begin
+ Result := fFrameTime;
+end;
+
+procedure TVideo_FFmpeg.SetScreen(Screen: integer);
+begin
+ fScreen := Screen;
+end;
+
+function TVideo_FFmpeg.GetScreen(): integer;
+begin
+ Result := fScreen;
+end;
+
+
+procedure TVideo_FFmpeg.SetScreenPosition(X, Y, Z: double);
+begin
+ fPosX := X;
+ fPosY := Y;
+ fPosZ := Z;
+end;
+
+procedure TVideo_FFmpeg.GetScreenPosition(var X, Y, Z: double);
+begin
+ X := fPosX;
+ Y := fPosY;
+ Z := fPosZ;
+end;
+
+
+procedure TVideo_FFmpeg.SetWidth(Width: double);
+begin
+ fWidth := Width;
+end;
+
+function TVideo_FFmpeg.GetWidth(): double;
+begin
+ Result := fWidth;
+end;
+
+
+procedure TVideo_FFmpeg.SetHeight(Height: double);
+begin
+ fHeight := Height;
+end;
+
+function TVideo_FFmpeg.GetHeight(): double;
+begin
+ Result := fHeight;
+end;
+
+
+procedure TVideo_FFmpeg.SetFrameRange(Range: TRectCoords);
+begin
+ fFrameRange := Range;
+end;
+
+function TVideo_FFmpeg.GetFrameRange(): TRectCoords;
+begin
+ Result := fFrameRange;
+end;
+
+
+function TVideo_FFmpeg.GetFrameAspect(): real;
+begin
+ Result := fAspect;
+end;
+
+
+procedure TVideo_FFmpeg.SetAspectCorrection(AspectCorrection: TAspectCorrection);
+begin
+ fAspectCorrection := AspectCorrection;
+end;
+
+function TVideo_FFmpeg.GetAspectCorrection(): TAspectCorrection;
+begin
+ Result := fAspectCorrection;
+end;
+
+
+
+procedure TVideo_FFmpeg.SetAlpha(Alpha: double);
+begin
+ fAlpha := Alpha;
+
+ if (fAlpha>1) then
+ fAlpha := 1;
+
+ if (fAlpha<0) then
+ fAlpha := 0;
+end;
+
+function TVideo_FFmpeg.GetAlpha(): double;
+begin
+ Result := fAlpha;
+end;
+
+
+procedure TVideo_FFmpeg.SetReflectionSpacing(Spacing: double);
+begin
+ fReflectionSpacing := Spacing;
+end;
+
+function TVideo_FFmpeg.GetReflectionSpacing(): double;
+begin
+ Result := fReflectionSpacing;
+end;
+
+
+initialization
+ MediaManager.Add(TVideoPlayback_FFmpeg.Create);
+
+end.
diff --git a/src/media/UVisualizer.pas b/src/media/UVisualizer.pas
new file mode 100644
index 00000000..1cdc3500
--- /dev/null
+++ b/src/media/UVisualizer.pas
@@ -0,0 +1,685 @@
+{* UltraStar Deluxe - Karaoke Game
+ *
+ * UltraStar Deluxe is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *}
+
+unit 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)
+ *)
+
+{* Note:
+ * It would be easier to create a seperate Render-Context (RC) for projectM
+ * and switch to it when necessary. This can be achieved by pbuffers
+ * (slow and platform specific) or the OpenGL FramebufferObject (FBO) extension
+ * (fast and plattform-independent but not supported by older graphic-cards/drivers).
+ *
+ * See http://oss.sgi.com/projects/ogl-sample/registry/EXT/framebuffer_object.txt
+ *
+ * To support as many cards as possible we will stick to the current dirty
+ * solution for now even if it is a pain to save/restore projectM's state due
+ * to bugs etc.
+ *
+ * This also restricts us to projectM. As other plug-ins might have different
+ * needs and bugs concerning the OpenGL state, USDX's state would probably be
+ * corrupted after the plug-in finshed drawing.
+ *}
+
+interface
+
+{$IFDEF FPC}
+ {$MODE DELPHI}
+{$ENDIF}
+
+{$I switches.inc}
+
+{.$DEFINE UseTexture}
+
+uses
+ SDL,
+ UGraphicClasses,
+ textgl,
+ math,
+ gl,
+ {$IFDEF UseTexture}
+ glu,
+ {$ENDIF}
+ SysUtils,
+ UIni,
+ projectM,
+ UMusic;
+
+implementation
+
+uses
+ UGraphic,
+ UMain,
+ UConfig,
+ UPath,
+ ULog;
+
+{$IF PROJECTM_VERSION < 1000000} // < 1.0
+// Initialization data used on projectM 0.9x creation.
+// Since projectM 1.0 this data is passed via the config-file.
+const
+ meshX = 32;
+ meshY = 24;
+ fps = 30;
+ textureSize = 512;
+{$IFEND}
+
+type
+ TProjectMState = ( pmPlay, pmStop, pmPause );
+
+type
+ TGLMatrix = array[0..3, 0..3] of GLdouble;
+ TGLMatrixStack = array of TGLMatrix;
+
+type
+ TVideo_ProjectM = class( TInterfacedObject, IVideo )
+ private
+ fPm: TProjectM;
+ fProjectMPath : string;
+
+ fState: TProjectMState;
+
+ fScreen: integer;
+
+ fVisualTex: GLuint;
+ fPCMData: TPCMData;
+ fRndPCMcount: integer;
+
+ fModelviewMatrixStack: TGLMatrixStack;
+ fProjectionMatrixStack: TGLMatrixStack;
+ fTextureMatrixStack: TGLMatrixStack;
+
+ procedure InitProjectM;
+
+ function GetRandomPCMData(var Data: TPCMData): Cardinal;
+
+ function GetMatrixStackDepth(MatrixMode: GLenum): GLint;
+ procedure SaveMatrixStack(MatrixMode: GLenum; var MatrixStack: TGLMatrixStack);
+ procedure RestoreMatrixStack(MatrixMode: GLenum; var MatrixStack: TGLMatrixStack);
+ procedure SaveOpenGLState();
+ procedure RestoreOpenGLState();
+
+ public
+ constructor Create;
+ destructor Destroy; override;
+
+ procedure Close;
+
+ procedure Play;
+ procedure Pause;
+ procedure Stop;
+
+ procedure SetPosition(Time: real);
+ function GetPosition: real;
+
+ procedure SetLoop(Enable: boolean);
+ function GetLoop(): boolean;
+
+ procedure SetScreen(Screen: integer);
+ function GetScreen(): integer;
+
+ procedure SetScreenPosition(X, Y, Z: double);
+ procedure GetScreenPosition(var X, Y, Z: double);
+
+ procedure SetWidth(Width: double);
+ function GetWidth(): double;
+
+ procedure SetHeight(Height: double);
+ function GetHeight(): double;
+
+ procedure SetFrameRange(Range: TRectCoords);
+ function GetFrameRange(): TRectCoords;
+
+ function GetFrameAspect(): real;
+
+ procedure SetAspectCorrection(AspectCorrection: TAspectCorrection);
+ function GetAspectCorrection(): TAspectCorrection;
+
+ procedure SetAlpha(Alpha: double);
+ function GetAlpha(): double;
+
+ procedure SetReflectionSpacing(Spacing: double);
+ function GetReflectionSpacing(): double;
+
+ procedure GetFrame(Time: Extended);
+ procedure Draw();
+ procedure DrawReflection();
+ end;
+
+ TVideoPlayback_ProjectM = class( TInterfacedObject, IVideoVisualization )
+ private
+ fInitialized: boolean;
+
+ public
+ function GetName: String;
+
+ function Init(): boolean;
+ function Finalize(): boolean;
+
+ function Open(const aFileName: IPath): IVideo;
+ end;
+
+
+{ TVideoPlayback_ProjectM }
+
+function TVideoPlayback_ProjectM.GetName: String;
+begin
+ Result := 'ProjectM';
+end;
+
+function TVideoPlayback_ProjectM.Init(): boolean;
+begin
+ Result := true;
+ if (fInitialized) then
+ Exit;
+ fInitialized := true;
+end;
+
+function TVideoPlayback_ProjectM.Finalize(): boolean;
+begin
+ Result := true;
+end;
+
+function TVideoPlayback_ProjectM.Open(const aFileName: IPath): IVideo;
+begin
+ Result := TVideo_ProjectM.Create;
+end;
+
+
+{ TVideo_ProjectM }
+
+constructor TVideo_ProjectM.Create;
+begin
+ fRndPCMcount := 0;
+
+ fProjectMPath := ProjectM_DataDir + PathDelim;
+
+ fState := pmStop;
+
+ {$IFDEF UseTexture}
+ glGenTextures(1, PglUint(@fVisualTex));
+ glBindTexture(GL_TEXTURE_2D, fVisualTex);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ {$ENDIF}
+
+ InitProjectM();
+end;
+
+destructor TVideo_ProjectM.Destroy;
+begin
+ Close();
+ {$IFDEF UseTexture}
+ glDeleteTextures(1, PglUint(@fVisualTex));
+ {$ENDIF}
+end;
+
+procedure TVideo_ProjectM.Close;
+begin
+ FreeAndNil(fPm);
+end;
+
+procedure TVideo_ProjectM.Play;
+begin
+ if (fState = pmStop) and (assigned(fPm)) then
+ fPm.RandomPreset();
+ fState := pmPlay;
+end;
+
+procedure TVideo_ProjectM.Pause;
+begin
+ if (fState = pmPlay) then
+ fState := pmPause
+ else if (fState = pmPause) then
+ fState := pmPlay;
+end;
+
+procedure TVideo_ProjectM.Stop;
+begin
+ fState := pmStop;
+end;
+
+procedure TVideo_ProjectM.SetPosition(Time: real);
+begin
+ if assigned(fPm) then
+ fPm.RandomPreset();
+end;
+
+function TVideo_ProjectM.GetPosition: real;
+begin
+ Result := 0;
+end;
+
+procedure TVideo_ProjectM.SetLoop(Enable: boolean);
+begin
+end;
+
+function TVideo_ProjectM.GetLoop(): boolean;
+begin
+ Result := true;
+end;
+
+procedure TVideo_ProjectM.SetScreen(Screen: integer);
+begin
+end;
+
+function TVideo_ProjectM.GetScreen(): integer;
+begin
+ Result := 0;
+end;
+
+procedure TVideo_ProjectM.SetScreenPosition(X, Y, Z: double);
+begin
+end;
+
+procedure TVideo_ProjectM.GetScreenPosition(var X, Y, Z: double);
+begin
+ X := 0;
+ Y := 0;
+ Z := 0;
+end;
+
+procedure TVideo_ProjectM.SetWidth(Width: double);
+begin
+end;
+
+function TVideo_ProjectM.GetWidth(): double;
+begin
+ Result := 0;
+end;
+
+procedure TVideo_ProjectM.SetHeight(Height: double);
+begin
+end;
+
+function TVideo_ProjectM.GetHeight(): double;
+begin
+ Result := 0;
+end;
+
+procedure TVideo_ProjectM.SetFrameRange(Range: TRectCoords);
+begin
+end;
+
+function TVideo_ProjectM.GetFrameRange(): TRectCoords;
+begin
+ Result.Left := 0;
+ Result.Right := 0;
+ Result.Upper := 0;
+ Result.Lower := 0;
+end;
+
+function TVideo_ProjectM.GetFrameAspect(): real;
+begin
+ Result := 0;
+end;
+
+procedure TVideo_ProjectM.SetAspectCorrection(AspectCorrection: TAspectCorrection);
+begin
+end;
+
+function TVideo_ProjectM.GetAspectCorrection(): TAspectCorrection;
+begin
+ Result := acoStretch;
+end;
+
+procedure TVideo_ProjectM.SetAlpha(Alpha: double);
+begin
+end;
+
+function TVideo_ProjectM.GetAlpha(): double;
+begin
+ Result := 1;
+end;
+
+procedure TVideo_ProjectM.SetReflectionSpacing(Spacing: double);
+begin
+end;
+
+function TVideo_ProjectM.GetReflectionSpacing(): double;
+begin
+ Result := 0;
+end;
+
+{**
+ * Returns the stack depth of the given OpenGL matrix mode stack.
+ *}
+function TVideo_ProjectM.GetMatrixStackDepth(MatrixMode: GLenum): GLint;
+begin
+ // get number of matrices on stack
+ case (MatrixMode) of
+ GL_PROJECTION:
+ glGetIntegerv(GL_PROJECTION_STACK_DEPTH, @Result);
+ GL_MODELVIEW:
+ glGetIntegerv(GL_MODELVIEW_STACK_DEPTH, @Result);
+ GL_TEXTURE:
+ glGetIntegerv(GL_TEXTURE_STACK_DEPTH, @Result);
+ end;
+end;
+
+{**
+ * Saves the current matrix stack using MatrixMode
+ * (one of GL_PROJECTION/GL_TEXTURE/GL_MODELVIEW)
+ *
+ * Use this function instead of just saving the current matrix with glPushMatrix().
+ * OpenGL specifies the depth of the GL_PROJECTION and GL_TEXTURE stacks to be
+ * at least 2 but projectM already uses 2 stack-entries so overflows might be
+ * possible on older hardware.
+ * In contrast to this the GL_MODELVIEW stack-size is at least 32, but this
+ * function should be used for the modelview stack too. We cannot rely on a
+ * proper stack management of the underlying visualizer (projectM).
+ * For example in the projectM versions 1.0 - 1.01 the modelview- and
+ * projection-matrices were popped without being pushed first.
+ *
+ * By saving the whole stack we are on the safe side, so a nasty bug in the
+ * visualizer does not corrupt USDX.
+ *}
+procedure TVideo_ProjectM.SaveMatrixStack(MatrixMode: GLenum;
+ var MatrixStack: TGLMatrixStack);
+var
+ I: integer;
+ StackDepth: GLint;
+begin
+ glMatrixMode(MatrixMode);
+
+ StackDepth := GetMatrixStackDepth(MatrixMode);
+ SetLength(MatrixStack, StackDepth);
+
+ // save current matrix stack
+ for I := StackDepth-1 downto 0 do
+ begin
+ // save current matrix
+ case (MatrixMode) of
+ GL_PROJECTION:
+ glGetDoublev(GL_PROJECTION_MATRIX, @MatrixStack[I]);
+ GL_MODELVIEW:
+ glGetDoublev(GL_MODELVIEW_MATRIX, @MatrixStack[I]);
+ GL_TEXTURE:
+ glGetDoublev(GL_TEXTURE_MATRIX, @MatrixStack[I]);
+ end;
+
+ // remove matrix from stack
+ if (I > 0) then
+ glPopMatrix();
+ end;
+
+ // reset default (first) matrix
+ glLoadIdentity();
+end;
+
+{**
+ * Restores the OpenGL matrix stack stored with SaveMatrixStack.
+ *}
+procedure TVideo_ProjectM.RestoreMatrixStack(MatrixMode: GLenum;
+ var MatrixStack: TGLMatrixStack);
+var
+ I: integer;
+ StackDepth: GLint;
+begin
+ glMatrixMode(MatrixMode);
+
+ StackDepth := GetMatrixStackDepth(MatrixMode);
+ // remove all (except the first) matrices from current stack
+ for I := 1 to StackDepth-1 do
+ glPopMatrix();
+
+ // rebuild stack
+ for I := 0 to High(MatrixStack) do
+ begin
+ glLoadMatrixd(@MatrixStack[I]);
+ if (I < High(MatrixStack)) then
+ glPushMatrix();
+ end;
+
+ // clean stored stack
+ SetLength(MatrixStack, 0);
+end;
+
+{**
+ * Saves the current OpenGL state.
+ * This is necessary to prevent projectM from corrupting USDX's current
+ * OpenGL state.
+ *
+ * The following steps are performed:
+ * - All attributes are pushed to the attribute-stack
+ * - Projection-/Texture-matrices are saved
+ * - Modelview-matrix is pushed to the Modelview-stack
+ * - the OpenGL error-state (glGetError) is cleared
+ *}
+procedure TVideo_ProjectM.SaveOpenGLState();
+begin
+ // save all OpenGL state-machine attributes
+ glPushAttrib(GL_ALL_ATTRIB_BITS);
+ glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS);
+
+ SaveMatrixStack(GL_PROJECTION, fProjectionMatrixStack);
+ SaveMatrixStack(GL_MODELVIEW, fModelviewMatrixStack);
+ SaveMatrixStack(GL_TEXTURE, fTextureMatrixStack);
+
+ glMatrixMode(GL_MODELVIEW);
+
+ // reset OpenGL error-state
+ glGetError();
+end;
+
+{**
+ * Restores the OpenGL state saved by SaveOpenGLState()
+ * and resets the error-state.
+ *}
+procedure TVideo_ProjectM.RestoreOpenGLState();
+begin
+ // reset OpenGL error-state
+ glGetError();
+
+ // restore matrix stacks
+ RestoreMatrixStack(GL_PROJECTION, fProjectionMatrixStack);
+ RestoreMatrixStack(GL_MODELVIEW, fModelviewMatrixStack);
+ RestoreMatrixStack(GL_TEXTURE, fTextureMatrixStack);
+
+ // restore all OpenGL state-machine attributes
+ // (also restores the matrix mode)
+ glPopClientAttrib();
+ glPopAttrib();
+end;
+
+procedure TVideo_ProjectM.InitProjectM;
+begin
+ // the OpenGL state must be saved before TProjectM.Create is called
+ SaveOpenGLState();
+ try
+
+ try
+ {$IF PROJECTM_VERSION >= 1000000} // >= 1.0
+ fPm := TProjectM.Create(fProjectMPath + 'config.inp');
+ {$ELSE}
+ fPm := TProjectM.Create(
+ meshX, meshY, fps, textureSize, ScreenW, ScreenH,
+ fProjectMPath + 'presets', fProjectMPath + '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;
+
+ // initialize OpenGL
+ fPm.ResetGL(ScreenW, ScreenH);
+ // skip projectM default-preset
+ fPm.RandomPreset();
+ // projectM >= 1.0 uses the OpenGL FramebufferObject (FBO) extension.
+ // Unfortunately it does NOT reset the framebuffer-context after
+ // TProjectM.Create. Either glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0) for
+ // a manual reset or TProjectM.RenderFrame() must be called.
+ // We use the latter so we do not need to load the FBO extension in USDX.
+ fPm.RenderFrame();
+ finally
+ RestoreOpenGLState();
+ end;
+end;
+
+procedure TVideo_ProjectM.GetFrame(Time: Extended);
+var
+ nSamples: cardinal;
+begin
+ if (fState <> pmPlay) then
+ Exit;
+
+ // get audio data
+ nSamples := AudioPlayback.GetPCMData(fPCMData);
+
+ // generate some data if non is available
+ if (nSamples = 0) then
+ nSamples := GetRandomPCMData(fPCMData);
+
+ // send audio-data to projectM
+ if (nSamples > 0) then
+ fPm.AddPCM16Data(PSmallInt(@fPCMData), nSamples);
+
+ // store OpenGL state (might be messed up otherwise)
+ SaveOpenGLState();
+ try
+ // setup projectM's OpenGL state
+ fPm.ResetGL(ScreenW, ScreenH);
+
+ // let projectM render a frame
+ fPm.RenderFrame();
+
+ {$IFDEF UseTexture}
+ glBindTexture(GL_TEXTURE_2D, fVisualTex);
+ glFlush();
+ glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, fVisualWidth, fVisualHeight, 0);
+ {$ENDIF}
+ finally
+ // restore USDX OpenGL state
+ RestoreOpenGLState();
+ end;
+
+ // discard projectM's depth buffer information (avoid overlay)
+ glClear(GL_DEPTH_BUFFER_BIT);
+end;
+
+{**
+ * Draws the current frame to screen.
+ * TODO: this is not used yet. Data is directly drawn on GetFrame().
+ *}
+procedure TVideo_ProjectM.Draw();
+begin
+ {$IFDEF UseTexture}
+ // have a nice black background to draw on
+ if (fScreen = 1) then
+ begin
+ glClearColor(0, 0, 0, 0);
+ glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
+ end;
+
+ // exit if there's nothing to draw
+ if (fState <> pmPlay) then
+ Exit;
+
+ // setup display
+ glMatrixMode(GL_PROJECTION);
+ glPushMatrix();
+ glLoadIdentity();
+ // Use count of screens instead of 1 for the right corner
+ // otherwise we would draw the visualization streched over both screens
+ // another point is that we draw over the at this time drawn first
+ // screen, if Screen = 2
+ gluOrtho2D(0, Screens, 0, 1);
+ glMatrixMode(GL_MODELVIEW);
+ glPushMatrix();
+ glLoadIdentity();
+
+ glEnable(GL_BLEND);
+ glEnable(GL_TEXTURE_2D);
+ glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
+ glBindTexture(GL_TEXTURE_2D, fVisualTex);
+ glColor4f(1, 1, 1, 1);
+
+ // draw projectM frame
+ // Screen is 1 to 2. So current screen is from (Screen - 1) to (Screen)
+ glBegin(GL_QUADS);
+ glTexCoord2f(0, 0); glVertex2f((fScreen - 1), 0);
+ glTexCoord2f(1, 0); glVertex2f(fScreen, 0);
+ glTexCoord2f(1, 1); glVertex2f(fScreen, 1);
+ glTexCoord2f(0, 1); glVertex2f((fScreen - 1), 1);
+ glEnd();
+
+ glDisable(GL_TEXTURE_2D);
+ glDisable(GL_BLEND);
+
+ // restore state
+ glMatrixMode(GL_PROJECTION);
+ glPopMatrix();
+ glMatrixMode(GL_MODELVIEW);
+ glPopMatrix();
+ {$ENDIF}
+end;
+
+procedure TVideo_ProjectM.DrawReflection();
+begin
+end;
+
+{**
+ * Produces random "sound"-data in case no audio-data is available.
+ * Otherwise the visualization will look rather boring.
+ *}
+function TVideo_ProjectM.GetRandomPCMData(var Data: TPCMData): Cardinal;
+var
+ i: integer;
+begin
+ // Produce some fake PCM data
+ if (fRndPCMcount mod 500 = 0) then
+ begin
+ FillChar(Data, SizeOf(TPCMData), 0);
+ end
+ else
+ begin
+ for i := 0 to 511 do
+ begin
+ Data[i][0] := Random(High(Word)+1);
+ Data[i][1] := Random(High(Word)+1);
+ end;
+ end;
+ Inc(fRndPCMcount);
+ Result := 512;
+end;
+
+
+initialization
+ MediaManager.Add(TVideoPlayback_ProjectM.Create);
+
+end.