{* 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 Fade(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.Fade(Time: real; TargetVolume: single);
begin
// 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;
const
// prevent buffer underruns in BASS BASS_StreamPutData() sample output queue
// by stretching the input signal.
InputStretch = 0.01; // 1%
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-InputStretch)),
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.