unit UAudioDecoder_Bass;

interface

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

{$I switches.inc}

implementation

uses
  Classes,
  SysUtils,
  UMain,
  UMusic,
  UAudioCore_Bass,
  ULog,
  bass;

type
  TBassDecodeStream = class(TAudioDecodeStream)
    private
      Handle: HSTREAM;
      FormatInfo : TAudioFormatInfo;
      Error: boolean;
    public
      constructor Create(Handle: HSTREAM);
      destructor Destroy(); override;

      procedure Close();                     override;

      function GetLength(): real;            override;
      function GetAudioFormatInfo(): TAudioFormatInfo; override;
      function GetPosition: real;            override;
      procedure SetPosition(Time: real);     override;
      function GetLoop(): boolean;           override;
      procedure SetLoop(Enabled: boolean);   override;
      function IsEOF(): boolean;             override;
      function IsError(): boolean;           override;

      function ReadData(Buffer: PChar; BufSize: integer): integer; override;
  end;

type
  TAudioDecoder_Bass = class( TInterfacedObject, IAudioDecoder )
    public
      function GetName: string;

      function InitializeDecoder(): boolean;
      function FinalizeDecoder(): boolean;
      function Open(const Filename: string): TAudioDecodeStream;
  end;

var
  BassCore: TAudioCore_Bass;


{ TBassDecodeStream }

constructor TBassDecodeStream.Create(Handle: HSTREAM);
var
  ChannelInfo: BASS_CHANNELINFO;
  Format: TAudioSampleFormat;
begin
  inherited Create();
  Self.Handle := Handle;

  // setup format info
  if (not BASS_ChannelGetInfo(Handle, ChannelInfo)) then
  begin
    raise Exception.Create('Failed to open decode-stream');
  end;
  BassCore.ConvertBASSFlagsToAudioFormat(ChannelInfo.flags, Format);
  FormatInfo := TAudioFormatInfo.Create(ChannelInfo.chans, ChannelInfo.freq, format);

  Error := false;
end;

destructor TBassDecodeStream.Destroy();
begin
  Close();
  inherited;
end;

procedure TBassDecodeStream.Close();
begin
  if (Handle <> 0) then
  begin
    BASS_StreamFree(Handle);
    Handle := 0;
  end;
  PerformOnClose();
  FreeAndNil(FormatInfo);
  Error := false;
end;

function TBassDecodeStream.GetAudioFormatInfo(): TAudioFormatInfo;
begin
  Result := FormatInfo;
end;

function TBassDecodeStream.GetLength(): real;
var
  bytes: QWORD;
begin
  bytes  := BASS_ChannelGetLength(Handle, BASS_POS_BYTE);
  Result := BASS_ChannelBytes2Seconds(Handle, bytes);
end;

function TBassDecodeStream.GetPosition: real;
var
  bytes: QWORD;
begin
  bytes  := BASS_ChannelGetPosition(Handle, BASS_POS_BYTE);
  Result := BASS_ChannelBytes2Seconds(Handle, bytes);
end;

procedure TBassDecodeStream.SetPosition(Time: real);
var
  bytes: QWORD;
begin
  bytes := BASS_ChannelSeconds2Bytes(Handle, Time);
  BASS_ChannelSetPosition(Handle, bytes, BASS_POS_BYTE);
end;

function TBassDecodeStream.GetLoop(): boolean;
var
  flags: DWORD;
begin
  // retrieve channel flags
  flags := BASS_ChannelFlags(Handle, 0, 0);
  if (flags = DWORD(-1)) then
  begin
    Log.LogError('BASS_ChannelFlags: ' + BassCore.ErrorGetString(), 'TBassDecodeStream.GetLoop');
    Result := false;
    Exit;
  end;
  Result := (flags and BASS_SAMPLE_LOOP) <> 0;
end;

procedure TBassDecodeStream.SetLoop(Enabled: boolean);
var
  flags: DWORD;
begin
  // set/unset loop-flag
  if (Enabled) then
    flags := BASS_SAMPLE_LOOP
  else
    flags := 0;

  // set new flag-bits
  if (BASS_ChannelFlags(Handle, flags, BASS_SAMPLE_LOOP) = DWORD(-1)) then
  begin
    Log.LogError('BASS_ChannelFlags: ' + BassCore.ErrorGetString(), 'TBassDecodeStream.SetLoop');
    Exit;
  end;
end;

function TBassDecodeStream.IsEOF(): boolean;
begin
  Result := (BASS_ChannelIsActive(Handle) = BASS_ACTIVE_STOPPED);
end;

function TBassDecodeStream.IsError(): boolean;
begin
  Result := Error;
end;

function TBassDecodeStream.ReadData(Buffer: PChar; BufSize: integer): integer;
begin
  Result := BASS_ChannelGetData(Handle, Buffer, BufSize);
  // check error state (do not handle EOF as error)
  if ((Result = -1) and (BASS_ErrorGetCode() <> BASS_ERROR_ENDED)) then
    Error := true
  else
    Error := false;
end;


{ TAudioDecoder_Bass }

function TAudioDecoder_Bass.GetName: String;
begin
  result := 'BASS_Decoder';
end;

function TAudioDecoder_Bass.InitializeDecoder(): boolean;
begin
  BassCore := TAudioCore_Bass.GetInstance();
  Result := true;
end;

function TAudioDecoder_Bass.FinalizeDecoder(): boolean;
begin
  Result := true;
end;

function TAudioDecoder_Bass.Open(const Filename: string): TAudioDecodeStream;
var
  Stream: HSTREAM;
  ChannelInfo: BASS_CHANNELINFO;
  FileExt: string;
begin
  Result := nil;

  // check if BASS was initialized
  // in case the decoder is not used with BASS playback, init the NO_SOUND device
  if ((integer(BASS_GetDevice) = -1) and (BASS_ErrorGetCode() = BASS_ERROR_INIT)) then
    BASS_Init(0, 44100, 0, 0, nil);

  // TODO: use BASS_STREAM_PRESCAN for accurate seeking in VBR-files?
  //       disadvantage: seeking will slow down.
  Stream := BASS_StreamCreateFile(False, PChar(Filename), 0, 0, BASS_STREAM_DECODE);
  if (Stream = 0) then
  begin
    //Log.LogError(BassCore.ErrorGetString(), 'TAudioDecoder_Bass.Open');
    Exit;
  end;

  // check if BASS opened some erroneously recognized file-formats
  if BASS_ChannelGetInfo(Stream, channelInfo) then
  begin
    fileExt := ExtractFileExt(Filename);
    // BASS opens FLV-files (maybe others too) although it cannot handle them.
    // Setting BASS_CONFIG_VERIFY to the max. value (100000) does not help.
    if ((fileExt = '.flv') and (channelInfo.ctype = BASS_CTYPE_STREAM_MP1)) then
    begin
      BASS_StreamFree(Stream);
      Exit;
    end;
  end;

  Result := TBassDecodeStream.Create(Stream);
end;


initialization
  MediaManager.Add(TAudioDecoder_Bass.Create);

end.