unit UAudioPlayback_Portaudio;

interface

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

{$I switches.inc}


uses Classes,
     SysUtils,
     UMusic;

implementation

uses
     {$IFDEF LAZARUS}
     lclintf,
       {$ifndef win32}
       libc,
       {$endif}
     {$ENDIF}
     sdl,
     portaudio,
     ULog,
     UIni,
     UMain;

type
  TPortaudioPlaybackStream = class(TAudioPlaybackStream)
    private
      status:   TStreamStatus;
      Loaded:   boolean;
      Loop:     boolean;
    public
      decodeStream: TAudioDecodeStream;

      constructor Create(decodeStream: TAudioDecodeStream);
      procedure Play();                     override;
      procedure Pause();                    override;
      procedure Stop();                     override;
      procedure Close();                    override;
      function GetLoop(): boolean;          override;
      procedure SetLoop(Enabled: boolean);  override;
      function GetLength(): real;           override;
      function GetStatus(): TStreamStatus;  override;

      function IsLoaded(): boolean;

      // functions delegated to the decode stream
      function GetPosition: real;
      procedure SetPosition(Time: real);
      function ReadData(Buffer: PChar; BufSize: integer): integer;
  end;

type
  TAudioMixerStream = class
    private
      activeStreams: TList;
      mixerBuffer: PChar;
    public
      constructor Create();
      destructor Destroy(); override;
      procedure AddStream(stream: TAudioPlaybackStream);
      procedure RemoveStream(stream: TAudioPlaybackStream);
      function ReadData(Buffer: PChar; BufSize: integer): integer;
  end;

type
  TAudioPlayback_Portaudio = class( TInterfacedObject, IAudioPlayback )
    private
      MusicStream:        TPortaudioPlaybackStream;

      StartSoundStream:   TPortaudioPlaybackStream;
      BackSoundStream:    TPortaudioPlaybackStream;
      SwooshSoundStream:  TPortaudioPlaybackStream;
      ChangeSoundStream:  TPortaudioPlaybackStream;
      OptionSoundStream:  TPortaudioPlaybackStream;
      ClickSoundStream:   TPortaudioPlaybackStream;
      DrumSoundStream:    TPortaudioPlaybackStream;
      HihatSoundStream:   TPortaudioPlaybackStream;
      ClapSoundStream:    TPortaudioPlaybackStream;
      ShuffleSoundStream: TPortaudioPlaybackStream;

      //Custom Sounds
      CustomSounds: array of TCustomSoundEntry;

      mixerStream: TAudioMixerStream;
      paStream:    PPaStream;
    public
      FrameSize: integer;

      function  GetName: String;

      function InitializePortaudio(): boolean;
      function StartPortaudioStream(): boolean;

      procedure InitializePlayback();
      procedure SetVolume(Volume: integer);
      procedure SetMusicVolume(Volume: integer);
      procedure SetLoop(Enabled: boolean);
      function Open(Filename: string): boolean; // true if succeed
      function Load(Filename: string): TPortaudioPlaybackStream;
      procedure Rewind;
      procedure SetPosition(Time: real);
      procedure Play;
      procedure Pause;

      procedure Stop;
      procedure Close;
      function Finished: boolean;
      function Length: real;
      function GetPosition: real;
      procedure PlayStart;
      procedure PlayBack;
      procedure PlaySwoosh;
      procedure PlayChange;
      procedure PlayOption;
      procedure PlayClick;
      procedure PlayDrum;
      procedure PlayHihat;
      procedure PlayClap;
      procedure PlayShuffle;
      procedure StopShuffle;

      //Equalizer
      function GetFFTData: TFFTData;

      // Interface for Visualizer
      function GetPCMData(var data: TPCMData): Cardinal;

      //Custom Sounds
      function LoadCustomSound(const Filename: String): Cardinal;
      procedure PlayCustomSound(const Index: Cardinal );
  end;


function AudioCallback(input: Pointer; output: Pointer; frameCount: Longword;
      timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
      userData: Pointer): Integer; cdecl; forward;

var
  singleton_AudioPlaybackPortaudio : IAudioPlayback;


{ TAudioMixerStream }

constructor TAudioMixerStream.Create();
begin
  activeStreams := TList.Create;
end;

destructor TAudioMixerStream.Destroy();
begin
  if (mixerBuffer <> nil) then
    Freemem(mixerBuffer);
  activeStreams.Free;
end;

procedure TAudioMixerStream.AddStream(stream: TAudioPlaybackStream);
begin
  // check if stream is already in list to avoid duplicates
  if (activeStreams.IndexOf(Pointer(stream)) = -1) then
    activeStreams.Add(Pointer(stream));
end;

procedure TAudioMixerStream.RemoveStream(stream: TAudioPlaybackStream);
begin
  activeStreams.Remove(Pointer(stream));
end;

function TAudioMixerStream.ReadData(Buffer: PChar; BufSize: integer): integer;
var
  i: integer;
  size: integer;
  stream: TPortaudioPlaybackStream;
begin
  result := BufSize;

  // zero target-buffer (silence)
  FillChar(Buffer^, BufSize, 0);

  // resize mixer-buffer
  ReallocMem(mixerBuffer, BufSize);
  if (mixerBuffer = nil) then
    Exit;

  writeln('Mix: ' + inttostr(activeStreams.Count));

  for i := 0 to activeStreams.Count-1 do
  begin
    stream := TPortaudioPlaybackStream(activeStreams[i]);
    if (stream.GetStatus() = sPlaying) then
    begin
      // fetch data from current stream
      size := stream.ReadData(mixerBuffer, BufSize);
      if (size > 0) then
      begin
        SDL_MixAudio(PUInt8(Buffer), PUInt8(mixerBuffer), size, SDL_MIX_MAXVOLUME);
      end;
    end;
  end;
end;


{ TPortaudioPlaybackStream }

constructor TPortaudioPlaybackStream.Create(decodeStream: TAudioDecodeStream);
begin
  inherited Create();
  status := sStopped;
  if (decodeStream <> nil) then
  begin
    Self.decodeStream := decodeStream;
    Loaded := true;
  end;
end;

procedure TPortaudioPlaybackStream.Play();
begin
  if (status <> sPaused) then
  begin
    // rewind
    decodeStream.Position := 0;
  end;
  status := sPlaying;
  //mixerStream.AddStream(Self);
end;

procedure TPortaudioPlaybackStream.Pause();
begin
  status := sPaused;
end;

procedure TPortaudioPlaybackStream.Stop();
begin
  status := sStopped;
end;

procedure TPortaudioPlaybackStream.Close();
begin
  status := sStopped;
  Loaded := false;
  // TODO: cleanup decode-stream
end;

function TPortaudioPlaybackStream.IsLoaded(): boolean;
begin
  result := Loaded;
end;

function TPortaudioPlaybackStream.GetLoop(): boolean;
begin
  result := Loop;
end;

procedure TPortaudioPlaybackStream.SetLoop(Enabled: boolean);
begin
  Loop := Enabled;
end;

function TPortaudioPlaybackStream.GetLength(): real;
begin
  result := decodeStream.Length;
end;

function TPortaudioPlaybackStream.GetStatus(): TStreamStatus;
begin
  result := status;
end;

function TPortaudioPlaybackStream.ReadData(Buffer: PChar; BufSize: integer): integer;
begin
  result := decodeStream.ReadData(Buffer, BufSize);
  // end-of-file reached -> stop playback
  if (decodeStream.EOF) then
  begin
    status := sStopped;
  end;
end;

function TPortaudioPlaybackStream.GetPosition: real;
begin
  result := decodeStream.Position;
end;

procedure TPortaudioPlaybackStream.SetPosition(Time: real);
begin
  decodeStream.Position := Time;
end;


{ TAudioPlayback_Portaudio }

function AudioCallback(input: Pointer; output: Pointer; frameCount: Longword;
    timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags;
    userData: Pointer): Integer; cdecl;
var
  playback        : TAudioPlayback_Portaudio;
  playbackStream  : TPortaudioPlaybackStream;
  decodeStream    : TAudioDecodeStream;
begin
  playback := TAudioPlayback_Portaudio(userData);
  with playback do
  begin
    mixerStream.ReadData(output, frameCount * FrameSize);
  end;
  result := paContinue;
end;


function TAudioPlayback_Portaudio.GetName: String;
begin
  result := 'Portaudio_Playback';
end;

function TAudioPlayback_Portaudio.InitializePortaudio(): boolean;
var
  paApi           : TPaHostApiIndex;
  paApiInfo       : PPaHostApiInfo;
  paOutParams     : TPaStreamParameters;
  paOutDevice     : TPaDeviceIndex;
  paOutDeviceInfo : PPaDeviceInfo;
  err             : TPaError;
begin
  result := false;

  Pa_Initialize();

  // FIXME: determine automatically
  {$IFDEF WIN32}
  paApi := Pa_HostApiTypeIdToHostApiIndex(paDirectSound);
  {$ELSE}
  paApi := Pa_HostApiTypeIdToHostApiIndex(paALSA);
  {$ENDIF}
  if (paApi < 0) then
  begin
    Log.LogStatus('Pa_HostApiTypeIdToHostApiIndex: '+Pa_GetErrorText(paApi), 'UAudioPlayback_Portaudio');
    exit;
  end;

  paApiInfo := Pa_GetHostApiInfo(paApi);
  paOutDevice     := paApiInfo^.defaultOutputDevice;
  paOutDeviceInfo := Pa_GetDeviceInfo(paOutDevice);

  with paOutParams do begin
    device := paOutDevice;
    channelCount := 2;
    sampleFormat := paInt16;
    suggestedLatency := paOutDeviceInfo^.defaultLowOutputLatency;
    hostApiSpecificStreamInfo := nil;
  end;

  FrameSize := 2 * sizeof(Smallint);

  err := Pa_OpenStream(paStream, nil, @paOutParams, 44100,
          paFramesPerBufferUnspecified,
          paNoFlag, @AudioCallback, Self);
  if(err <> paNoError) then begin
    Log.LogStatus('Pa_OpenStream: '+Pa_GetErrorText(err), 'UAudioPlayback_Portaudio');
    exit;
  end;

  Log.LogStatus('Opened audio device', 'UAudioPlayback_Portaudio');

  result := true;
end;

function TAudioPlayback_Portaudio.StartPortaudioStream(): boolean;
var
  err: TPaError;
begin
  result := false;

  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.InitializePlayback;
begin
  Log.LogStatus('InitializePlayback', 'UAudioPlayback_Portaudio');

  InitializePortaudio();
  mixerStream := TAudioMixerStream.Create;

  StartSoundStream  := Load(SoundPath + 'Common start.mp3');
  BackSoundStream   := Load(SoundPath + 'Common back.mp3');
  SwooshSoundStream := Load(SoundPath + 'menu swoosh.mp3');
  ChangeSoundStream := Load(SoundPath + 'select music change music 50.mp3');
  OptionSoundStream := Load(SoundPath + 'option change col.mp3');
  ClickSoundStream  := Load(SoundPath + 'rimshot022b.mp3');

//  DrumSoundStream  := Load(SoundPath + 'bassdrumhard076b.mp3');
//  HihatSoundStream := Load(SoundPath + 'hihatclosed068b.mp3');
//  ClapSoundStream  := Load(SoundPath + 'claps050b.mp3');

//  ShuffleSoundStream := Load(SoundPath + 'Shuffle.mp3');

  StartPortaudioStream();
end;


procedure TAudioPlayback_Portaudio.SetVolume(Volume: integer);
begin
  //New: Sets Volume only for this Application
(*
  BASS_SetConfig(BASS_CONFIG_GVOL_SAMPLE, Volume);
  BASS_SetConfig(BASS_CONFIG_GVOL_STREAM, Volume);
  BASS_SetConfig(BASS_CONFIG_GVOL_MUSIC, Volume);
*)
end;

procedure TAudioPlayback_Portaudio.SetMusicVolume(Volume: Integer);
begin
  //Max Volume Prevention
  if Volume > 100 then
    Volume := 100;

  if Volume < 0 then
    Volume := 0;

  //Set Volume
//  BASS_ChannelSetAttributes (Bass, -1, Volume, -101);
end;

procedure TAudioPlayback_Portaudio.SetLoop(Enabled: boolean);
begin
  if (MusicStream <> nil) then
  if (MusicStream.IsLoaded) then
   MusicStream.SetLoop(Enabled);
end;

function TAudioPlayback_Portaudio.Open(Filename: string): boolean;
var
  decodeStream: TAudioDecodeStream;
begin
  decodeStream := AudioDecoder.Open(Filename);
  MusicStream := TPortaudioPlaybackStream.Create(decodeStream);
  // FIXME: remove this line
  mixerStream.AddStream(MusicStream);

  if(MusicStream.IsLoaded()) then
  begin
    //Set Max Volume
    SetMusicVolume(100);
  end;

  Result := MusicStream.IsLoaded();
end;

procedure TAudioPlayback_Portaudio.Rewind;
begin
  SetPosition(0);
end;

procedure TAudioPlayback_Portaudio.SetPosition(Time: real);
begin
  if (MusicStream.IsLoaded) then
  begin
    MusicStream.SetPosition(Time);
  end;
end;

function TAudioPlayback_Portaudio.GetPosition: real;
begin
  if (MusicStream.IsLoaded) then
  Result := MusicStream.GetPosition();
end;

function TAudioPlayback_Portaudio.Length: real;
begin
  Result := 0;
  if assigned( MusicStream ) then
  if (MusicStream.IsLoaded) then
  begin
    Result := MusicStream.GetLength();
  end;
end;

procedure TAudioPlayback_Portaudio.Play;
begin
  if (MusicStream <> nil) then
  if (MusicStream.IsLoaded) then
  begin
    if (MusicStream.GetLoop()) then
    begin
    end;
    // start from beginning...
    // actually bass itself does not loop, nor does this TAudio_FFMpeg Class
    MusicStream.Play();
  end;
end;

procedure TAudioPlayback_Portaudio.Pause;
begin
  if (MusicStream <> nil) then
    MusicStream.Pause();
end;

procedure TAudioPlayback_Portaudio.Stop;
begin
  if MusicStream <> nil then
  MusicStream.Stop();
end;

procedure TAudioPlayback_Portaudio.Close;
begin
  if MusicStream <> nil then
  MusicStream.Close();
end;

function TAudioPlayback_Portaudio.Finished: boolean;
begin
  if MusicStream <> nil then
  Result := (MusicStream.GetStatus() = sStopped);
end;

procedure TAudioPlayback_Portaudio.PlayStart;
begin
  if StartSoundStream <> nil then
  StartSoundStream.Play();
end;

procedure TAudioPlayback_Portaudio.PlayBack;
begin
  if BackSoundStream <> nil then
  BackSoundStream.Play();
end;

procedure TAudioPlayback_Portaudio.PlaySwoosh;
begin
  if SwooshSoundStream <> nil then
  SwooshSoundStream.Play();
end;

procedure TAudioPlayback_Portaudio.PlayChange;
begin
  if ChangeSoundStream <> nil then
  ChangeSoundStream.Play();
end;

procedure TAudioPlayback_Portaudio.PlayOption;
begin
  if OptionSoundStream <> nil then
  OptionSoundStream.Play();
end;

procedure TAudioPlayback_Portaudio.PlayClick;
begin
  if ClickSoundStream <> nil then
  ClickSoundStream.Play();
end;

procedure TAudioPlayback_Portaudio.PlayDrum;
begin
  if DrumSoundStream <> nil then
  DrumSoundStream.Play();
end;

procedure TAudioPlayback_Portaudio.PlayHihat;
begin
  if HihatSoundStream <> nil then
  HihatSoundStream.Play();
end;

procedure TAudioPlayback_Portaudio.PlayClap;
begin
  if ClapSoundStream <> nil then
  ClapSoundStream.Play();
end;

procedure TAudioPlayback_Portaudio.PlayShuffle;
begin
  if ShuffleSoundStream <> nil then
  ShuffleSoundStream.Play();
end;

procedure TAudioPlayback_Portaudio.StopShuffle;
begin
  if ShuffleSoundStream <> nil then
  ShuffleSoundStream.Stop();
end;

//Equalizer
function TAudioPlayback_Portaudio.GetFFTData: TFFTData;
var
  data: TFFTData;
begin
  //Get Channel Data Mono and 256 Values
//  BASS_ChannelGetData(Bass, @Result, BASS_DATA_FFT512);
  result := data;
end;

// Interface for Visualizer
function TAudioPlayback_Portaudio.GetPCMData(var data: TPCMData): Cardinal;
begin
  result := 0;
end;

function TAudioPlayback_Portaudio.Load(Filename: string): TPortaudioPlaybackStream;
var
  decodeStream    : TAudioDecodeStream;
  playbackStream  : TPortaudioPlaybackStream;
  csIndex         : integer;
begin
  result := nil;

  decodeStream := AudioDecoder.Open(Filename);
  if (decodeStream = nil) then
  begin
    Log.LogStatus('LoadSoundFromFile: Sound not found "' + Filename + '"', 'UAudioPlayback_Portaudio');
    exit;
  end;

  playbackStream := TPortaudioPlaybackStream.Create(decodeStream);
  // FIXME: remove this line
  mixerStream.AddStream(playbackStream);

  //Add CustomSound
  csIndex := High(CustomSounds) + 1;
  SetLength(CustomSounds, csIndex + 1);
  CustomSounds[csIndex].Filename := Filename;
  CustomSounds[csIndex].Stream := playbackStream;

  result := playbackStream;
end;

function TAudioPlayback_Portaudio.LoadCustomSound(const Filename: String): Cardinal;
var
  S: TAudioPlaybackStream;
  I: Integer;
  F: String;
begin
  //Search for Sound in already loaded Sounds
  F := UpperCase(SoundPath + FileName);
  For I := 0 to High(CustomSounds) do
  begin
    if (UpperCase(CustomSounds[I].Filename) = F) then
    begin
      Result := I;
      Exit;
    end;
  end;

  S := Load(SoundPath + Filename);
  if (S <> nil) then
    Result := High(CustomSounds)
  else
    Result := 0;
end;

procedure TAudioPlayback_Portaudio.PlayCustomSound(const Index: Cardinal );
begin
  if (Index <= High(CustomSounds)) then
    CustomSounds[Index].Stream.Play();
end;



initialization
  singleton_AudioPlaybackPortaudio := TAudioPlayback_Portaudio.create();
  AudioManager.add( singleton_AudioPlaybackPortaudio );

finalization
  AudioManager.Remove( singleton_AudioPlaybackPortaudio );


end.