{* 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: svn://basisbit@svn.code.sf.net/p/ultrastardx/svn/trunk/src/media/UAudioPlayback_Bass.pas $
 * $Id: UAudioPlayback_Bass.pas 3141 2015-10-11 22:15:05Z basisbit $
 *}

unit UAudioPlayback_Bass;

interface

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

{$I switches.inc}

implementation

uses
  Classes,
  Math,
  UIni,
  UMain,
  UMusic,
  UAudioPlaybackBase,
  UAudioCore_Bass,
  ULog,
  sdl2,
  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
      fVolume: single;

      procedure Reset();
      function IsEOF(): boolean;
      procedure SetVolumeBASS();
    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 GetFFTData(var Data: TFFTData);           override;
      function  GetPCMData(var Data: TPCMData): Cardinal; override;

      function GetAudioFormatInfo(): TAudioFormatInfo; override;

      function ReadData(Buffer: PByte; BufferSize: integer): integer;
      procedure SetReplayGainEnabled(RGEnabled: boolean); override;

      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: PByte; 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
      constructor Create(); override;

      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;

constructor TAudioPlayback_Bass.Create();
begin
  inherited;
end;

{ 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: PByte; BufferSize: integer): integer;
var
  AdjustedSize: integer;
  RequestedSourceSize, SourceSize: integer;
  SkipCount: integer;
  SourceFormatInfo: TAudioFormatInfo;
  FrameSize: integer;
  PadFrame: PByteArray;
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;

procedure TBassPlaybackStream.SetReplayGainEnabled(RGEnabled: boolean);
begin
  inherited;
  SetVolumeBASS();
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;
  inherited;

  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;
  fVolume := 1.0;
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 * ReplayGainAdjustment, Trunc(Time * 1000));
  fVolume := TargetVolume;
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 * ReplayGainAdjustment, Trunc(Time * 1000));
  fVolume := TargetVolume;
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;
var
  Info: BASS_INFO;
begin
  if (BASS_GetInfo(Info)) then
    Result := Info.latency / 1000
  else
  Result := 0; 
end;

function TBassPlaybackStream.GetVolume(): single;
begin
  Result := fVolume;
end;

procedure TBassPlaybackStream.SetVolume(Volume: single);
begin
  // clamp volume
  if Volume < 0 then
    Volume := 0;
  if Volume > 1.0 then
    Volume := 1.0;
  fVolume := Volume;
  SetVolumeBASS();
end;

procedure TBassPlaybackStream.SetVolumeBASS();
begin
  BASS_ChannelSetAttribute(Handle, BASS_ATTRIB_VOL, fVolume * ReplayGainAdjustment);
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;

// This is an empty handler because we need to implement it but never use it
procedure DSPProcHandler(handle: HDSP; channel: DWORD; buffer: Pointer; length: DWORD; user: Pointer);
{$IFDEF MSWINDOWS}stdcall;{$ELSE}cdecl;{$ENDIF}
begin
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: PByte; 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;
  BASS_SetConfig(BASS_CONFIG_REC_DEFAULT, 0);

  EnumDevices();

  //Log.BenchmarkStart(4);
  //Log.LogStatus('Initializing Playback Subsystem', 'Music Initialize');
  BASS_SetConfig(BASS_CONFIG_DEV_DEFAULT, 1);
  // 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);

  Log.LogStatus('Opened audio device', 'TAudioPlayback_Bass.InitializePlayback');

  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;
var
  Info: BASS_INFO;
begin
  if (BASS_GetInfo(Info)) then
    Result := Info.latency / 1000
  else
  Result := 0;
end;

initialization
  MediaManager.Add(TAudioPlayback_Bass.Create);

end.
