unit UAudioPlayback_Bass;
interface
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I switches.inc}
implementation
uses
Classes,
SysUtils,
UIni,
UMain,
UMusic,
UAudioPlaybackBase,
UAudioCore_Bass,
ULog,
bass;
type
PHDSP = ^HDSP;
type
// Playback-stream decoded internally by BASS
TBassPlaybackStream = class(TAudioPlaybackStream)
private
Handle: HSTREAM;
public
constructor Create(stream: HSTREAM);
destructor Destroy(); override;
procedure Reset();
procedure Play(); override;
procedure Pause(); override;
procedure Stop(); override;
procedure FadeIn(Time: real; TargetVolume: single); override;
procedure Close(); override;
function GetLoop(): boolean; override;
procedure SetLoop(Enabled: boolean); override;
function GetLength(): real; override;
function GetStatus(): TStreamStatus; override;
function GetVolume(): single; override;
procedure SetVolume(volume: single); override;
procedure AddSoundEffect(effect: TSoundEffect); override;
procedure RemoveSoundEffect(effect: TSoundEffect); override;
function GetPosition: real; override;
procedure SetPosition(Time: real); override;
procedure GetFFTData(var data: TFFTData); override;
function GetPCMData(var data: TPCMData): Cardinal; override;
end;
// Playback-stream decoded by an external decoder e.g. FFmpeg
TBassExtDecoderPlaybackStream = class(TBassPlaybackStream)
private
DecodeStream: TAudioDecodeStream;
public
procedure Stop(); override;
procedure Close(); override;
function GetLength(): real; override;
function GetPosition: real; override;
procedure SetPosition(Time: real); override;
function SetDecodeStream(decodeStream: TAudioDecodeStream): boolean;
end;
type
TAudioPlayback_Bass = class(TAudioPlaybackBase)
private
function EnumDevices(): boolean;
protected
function OpenStream(const Filename: string): TAudioPlaybackStream; override;
public
function GetName: String; override;
function InitializePlayback(): boolean; override;
function FinalizePlayback: boolean; override;
procedure SetAppVolume(Volume: single); override;
end;
TBassOutputDevice = class(TAudioOutputDevice)
private
BassDeviceID: DWORD; // DeviceID used by BASS
end;
var
AudioCore: TAudioCore_Bass;
singleton_AudioPlaybackBass : IAudioPlayback;
{ TBassPlaybackStream }
constructor TBassPlaybackStream.Create(stream: HSTREAM);
begin
inherited Create();
Reset();
Handle := stream;
end;
destructor TBassPlaybackStream.Destroy();
begin
Close();
inherited;
end;
procedure TBassPlaybackStream.Reset();
begin
if (Handle <> 0) then
begin
Bass_StreamFree(Handle);
end;
Handle := 0;
end;
procedure TBassPlaybackStream.Play();
var
restart: boolean;
begin
if (BASS_ChannelIsActive(Handle) = BASS_ACTIVE_PAUSED) then
restart := false // resume from last position
else
restart := true; // start from the beginning
BASS_ChannelPlay(Handle, restart);
end;
procedure TBassPlaybackStream.FadeIn(Time: real; TargetVolume: single);
begin
// start stream
BASS_ChannelPlay(Handle, true);
// 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;
procedure TBassPlaybackStream.Close();
begin
Reset();
end;
function TBassPlaybackStream.GetVolume(): single;
var
lVolume: single;
begin
if (not BASS_ChannelGetAttribute(Handle, BASS_ATTRIB_VOL, lVolume)) then
begin
Log.LogError('BASS_ChannelGetAttribute: ' + AudioCore.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
bytes: QWORD;
begin
bytes := BASS_ChannelGetPosition(Handle, BASS_POS_BYTE);
Result := BASS_ChannelBytes2Seconds(Handle, bytes);
end;
procedure TBassPlaybackStream.SetPosition(Time: real);
var
bytes: QWORD;
begin
bytes := BASS_ChannelSeconds2Bytes(Handle, Time);
BASS_ChannelSetPosition(Handle, bytes, BASS_POS_BYTE);
end;
function TBassPlaybackStream.GetLength(): real;
var
bytes: QWORD;
begin
bytes := BASS_ChannelGetLength(Handle, BASS_POS_BYTE);
Result := BASS_ChannelBytes2Seconds(Handle, bytes);
end;
function TBassPlaybackStream.GetStatus(): TStreamStatus;
var
state: DWORD;
begin
state := BASS_ChannelIsActive(Handle);
case state of
BASS_ACTIVE_PLAYING:
result := ssPlaying;
BASS_ACTIVE_PAUSED:
result := ssPaused;
BASS_ACTIVE_STALLED:
result := ssBlocked;
BASS_ACTIVE_STOPPED:
result := ssStopped;
else
result := ssUnknown;
end;
end;
function TBassPlaybackStream.GetLoop(): boolean;
var
flags: DWORD;
begin
// retrieve channel flags
flags := BASS_ChannelFlags(Handle, 0, 0);
if (flags = -1) then
begin
Log.LogError('BASS_ChannelFlags: ' + AudioCore.ErrorGetString(), 'TBassPlaybackStream.GetLoop');
Result := false;
Exit;
end;
Result := (flags and BASS_SAMPLE_LOOP) <> 0;
end;
procedure TBassPlaybackStream.SetLoop(Enabled: boolean);
var
flags: DWORD;
begin
// set/unset loop-flag
if (Enabled) then
flags := BASS_SAMPLE_LOOP
else
flags := 0;
// set new flag-bits
if (BASS_ChannelFlags(Handle, flags, BASS_SAMPLE_LOOP) = -1) then
begin
Log.LogError('BASS_ChannelFlags: ' + AudioCore.ErrorGetString(), 'TBassPlaybackStream.SetLoop');
Exit;
end;
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(AudioCore.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(AudioCore.ErrorGetString(), 'TBassPlaybackStream.RemoveSoundEffect');
Exit;
end;
FreeMem(effect.engineData);
effect.engineData := nil;
end;
procedure TBassPlaybackStream.GetFFTData(var data: TFFTData);
begin
// Get Channel Data Mono and 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;
{ TBassExtDecoderPlaybackStream }
procedure TBassExtDecoderPlaybackStream.Stop();
begin
inherited;
// rewind
if assigned(DecodeStream) then
DecodeStream.Position := 0;
end;
procedure TBassExtDecoderPlaybackStream.Close();
begin
// wake-up waiting audio-callback threads in the ReadData()-function
if assigned(decodeStream) then
DecodeStream.Close();
// stop audio-callback on this stream
inherited;
// free decoder-data
FreeAndNil(DecodeStream);
end;
function TBassExtDecoderPlaybackStream.GetLength(): real;
begin
if assigned(DecodeStream) then
result := DecodeStream.Length
else
result := -1;
end;
function TBassExtDecoderPlaybackStream.GetPosition: real;
begin
if assigned(DecodeStream) then
result := DecodeStream.Position
else
result := -1;
end;
procedure TBassExtDecoderPlaybackStream.SetPosition(Time: real);
begin
if assigned(DecodeStream) then
DecodeStream.Position := Time;
end;
function TBassExtDecoderPlaybackStream.SetDecodeStream(decodeStream: TAudioDecodeStream): boolean;
begin
result := false;
BASS_ChannelStop(Handle);
if not assigned(decodeStream) then
Exit;
Self.DecodeStream := decodeStream;
result := true;
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
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;
var
Pet: integer;
S: integer;
begin
result := false;
AudioCore := TAudioCore_Bass.GetInstance();
EnumDevices();
//Log.BenchmarkStart(4);
//Log.LogStatus('Initializing Playback Subsystem', 'Music Initialize');
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 DecodeStreamHandler(handle: HSTREAM; buffer: Pointer; length: DWORD; user: Pointer): DWORD; {$IFDEF MSWINDOWS}stdcall;{$ELSE}cdecl;{$ENDIF}
var
decodeStream: TAudioDecodeStream;
bytes: integer;
begin
decodeStream := TAudioDecodeStream(user);
bytes := decodeStream.ReadData(buffer, length);
// handle errors
if (bytes < 0) then
Result := BASS_STREAMPROC_END
// handle EOF
else if (DecodeStream.EOF) then
Result := bytes or BASS_STREAMPROC_END
else
Result := bytes;
end;
function TAudioPlayback_Bass.OpenStream(const Filename: string): TAudioPlaybackStream;
var
stream: HSTREAM;
playbackStream: TBassExtDecoderPlaybackStream;
decodeStream: TAudioDecodeStream;
formatInfo: TAudioFormatInfo;
formatFlags: DWORD;
channelInfo: BASS_CHANNELINFO;
fileExt: string;
begin
Result := nil;
//Log.LogStatus('Loading Sound: "' + Filename + '"', 'LoadSoundFromFile');
// TODO: use BASS_STREAM_PRESCAN for accurate seeking in VBR-files?
// disadvantage: seeking will slow down.
stream := BASS_StreamCreateFile(False, PChar(Filename), 0, 0, 0);
// check if BASS opened some erroneously recognized file-formats
if (stream <> 0) then
begin
if BASS_ChannelGetInfo(stream, channelInfo) then
begin
fileExt := ExtractFileExt(Filename);
// BASS opens FLV-files (maybe others too) although it cannot handle them.
// Setting BASS_CONFIG_VERIFY to the max. value (100000) does not help.
if ((fileExt = '.flv') and (channelInfo.ctype = BASS_CTYPE_STREAM_MP1)) then
begin
BASS_StreamFree(stream);
stream := 0;
end;
end;
end;
// Check if BASS can handle the format or try another decoder otherwise
if (stream <> 0) then
begin
Result := TBassPlaybackStream.Create(stream);
end
else
begin
if (AudioDecoder = nil) then
begin
Log.LogError('Failed to open "' + Filename + '", ' +
AudioCore.ErrorGetString(), 'TAudioPlayback_Bass.Load');
Exit;
end;
decodeStream := AudioDecoder.Open(Filename);
if not assigned(decodeStream) then
begin
Log.LogStatus('Sound not found "' + Filename + '"', 'TAudioPlayback_Bass.Load');
Exit;
end;
formatInfo := decodeStream.GetAudioFormatInfo();
if (not AudioCore.ConvertAudioFormatToBASSFlags(formatInfo.Format, formatFlags)) then
begin
Log.LogError('Unhandled sample-format in "' + Filename + '"', 'TAudioPlayback_Bass.Load');
FreeAndNil(decodeStream);
Exit;
end;
stream := BASS_StreamCreate(Round(formatInfo.SampleRate), formatInfo.Channels, formatFlags,
@DecodeStreamHandler, decodeStream);
if (stream = 0) then
begin
Log.LogError('Failed to open "' + Filename + '", ' +
AudioCore.ErrorGetString(BASS_ErrorGetCode()), 'TAudioPlayback_Bass.Load');
FreeAndNil(decodeStream);
Exit;
end;
playbackStream := TBassExtDecoderPlaybackStream.Create(stream);
if (not assigned(playbackStream)) then
begin
FreeAndNil(decodeStream);
Exit;
end;
if (not playbackStream.SetDecodeStream(decodeStream)) then
begin
FreeAndNil(playbackStream);
FreeAndNil(decodeStream);
Exit;
end;
Result := playbackStream;
end;
end;
procedure TAudioPlayback_Bass.SetAppVolume(Volume: single);
begin
// Sets Volume only for this Application (now ranges from 0..10000)
BASS_SetConfig(BASS_CONFIG_GVOL_STREAM, Round(Volume*10000));
end;
initialization
singleton_AudioPlaybackBass := TAudioPlayback_Bass.create();
AudioManager.add( singleton_AudioPlaybackBass );
finalization
AudioManager.Remove( singleton_AudioPlaybackBass );
end.