//
//       originally written by Silhwan Hyun  hsh@chollian.net
//
// Improved somehow by Parfenov Alexey
// for better integration with GamePlayer
//
unit PluginCtrl;

interface

uses
   Windows, Messages, ioplug,dynamic_bass,microutils,microwin;

const
   MaxPluginNum = 8;               // the maximum number of plug-ins, simultaneously loadable
   WM_WA_MPEG_EOF = WM_USER + 2;   // message from Winamp input plug-in at the end of play
   WM_StartPlay = WM_USER + 101;   // message from output plug-in emulator at starting play
   WM_StreamClose = WM_USER + 103; // message from main program at closing stream file
   WM_BASS_StreamCreate = WM_USER + 105; // message from output plug-in emulator at executing BASS_StreamCreate
   WM_BASS_StreamFree = WM_USER + 106;   // message from output plug-in emulator at executing BASS_StreamFree


 // error types related loading & unloading of Winamp 2 input plug-ins
   ERROR_OMOD_NOTREADY  = 1;   // output plug-in emulator is not ready
   ERROR_LOADED_BEFORE  = 2;   // already loaded before
   ERROR_LOADED_FULL    = 3;   // no space to load
   ERROR_CANNOT_LOAD    = 4;   // any error at loading
   ERROR_INVALID_PLUGIN = 5;   // not a Winamp 2 input plug-in
   ERROR_CANNOT_UNLOAD  = 6;   // any error at unloading
   ERROR_NOT_LOADED     = 7;   // not loaded yet
   ERROR_IN_USE         = 8;   // specified plug-in is in use

type
   TPlugin = ^TIn_module;
   TPluginInfo = record
      Name : string;
      Version : integer;        // module type (IN_VER)
      Description : string;     // description of module, with version string
      FileExtensions : string;
      Seekable : integer;
      DLLHandle : THandle;
   end;
   TChannelInfo = record
      BPS : word;            // Bits per sample
      SampleRate : LongInt;
      Channels : Word;
   end;

  procedure WinProcessMessages;
  function SelectInputPlugin(PluginNum : integer) : integer;
  function LoadWinampPlugin(PluginName : string) : integer;
  function UnloadWinampPlugin(PluginName : string) : integer;
  function InitPluginCtrl : boolean;
  procedure QuitPluginCtrl;
  function ProcMessage(H,M,W,L:longint):longint;stdcall;

var
  HBASSStream : HSTREAM;
  PluginInfo : array[0..MaxPluginNum-1] of TPluginInfo;
  Plugin : array[0..MaxPluginNum-1] of TPlugin;

implementation

uses GamePlayer;

var Player:TGamePlayer;

var
   pBuf : PBYTE;
   omod : ^TOut_module;
   imod : ^TIn_module;
   omod1 : TOut_module;
   imodNum   : integer;
   omodReady : boolean;
   ChannelInfo : TChannelInfo;
   PosChanged : boolean;

procedure WinProcessMessages;
// Allow Windows to process other system messages
var
    ProcMsg  :  TMsg;
begin
    while PeekMessage(ProcMsg, 0, 0, 0, PM_REMOVE) do begin
      if (ProcMsg.Message = WM_QUIT) then Exit;
      TranslateMessage(ProcMsg);
      DispatchMessage(ProcMsg);
    end;
end;


//---------- procedures & functions for Winamp input plug-in --------------------

procedure SetInfo1(bitrate, srate, stereo, synched : integer); cdecl; // if -1, changes ignored? :)
begin
end;

function dsp_isactive1 : integer; cdecl;
begin
   result := 0;
end;

function dsp_dosamples1(samples : pointer; numsamples, bps, nch, srate : integer) : integer; cdecl;
begin
   result := numsamples;
end;

procedure SAVSAInit1(maxlatency_in_ms : integer; srate : integer); cdecl;  // call in omod.Play()
begin
end;

procedure SAVSADeInit1;cdecl;	// call in omod.Stop()
begin
end;

procedure SAAddPCMData1(PCMData: pointer; nch: integer; bps: integer; timestamp: integer); cdecl;
begin
end;

// gets csa (the current type (4=ws,2=osc,1=spec))
function SAGetMode1: integer; cdecl;
begin
   result := 0;
end;

// sets the spec data, filled in by winamp
procedure SAAdd1(data: pointer; timestamp: integer; csa: integer); cdecl;
begin
end;

// sets the vis data directly from PCM data
procedure VSAAddPCMData1(PCMData: pointer; nch: integer; bps: integer; timestamp: integer); cdecl;
begin
end;

// use to figure out what to give to VSAAdd
function VSAGetMode1(var specNch : integer; var waveNch : integer) : integer; cdecl;
begin
   result := 0;
end;

// filled in by winamp, called by plug-in
procedure VSAAdd1(data : pointer; timestamp : integer); cdecl;
begin
end;

procedure VSASetInfo1(srate : integer; nch : integer); cdecl;
begin
end;


//---------- procedures & functions to emulate Winamp output plug-in -----------

const
   bufSize = 88200;       // buffer size to hold 0.5sec playing data for 44100Hz, 16bit, stereo stream
   WinampBaseSize = 576;  // Winamp input plug-in's base size of data transfering

var
   rOffset : dword;
   wOffset : dword;
   SPS : integer;       // samplerate (samples per second)
   BPS : integer;       // bits per sample
   Channels : integer;  // number of channels (MONO = 1, STEREO = 2)
   BASSDataSize : integer;
   isReading : boolean;
   isWriting : boolean;
   posDelta : integer;
   totalWritten : int64;
   InitialBufferFill : boolean;
   PlayEnded : boolean;
   ResumeMode : boolean;

// calulate free space of buffer (The buffer pointed by pBuf to hold sound data
// is used as ring buffer)
function FreeSpace(BufferSize, ReadOffset, WriteOffset : dword) : dword;
begin
   if ReadOffset > WriteOffset then
      result := ReadOffset - WriteOffset
   else
      result := BufferSize  - WriteOffset + ReadOffset;
end;

// calulate data space of buffer
function DataSpace(BufferSize, ReadOffset, WriteOffset : dword) : dword;
begin
   if ReadOffset > WriteOffset then
      result := BufferSize  + WriteOffset - ReadOffset
   else
      result := WriteOffset - ReadOffset;
end;

function ProcMessage(H,M,W,L:longint):longint;stdcall;
begin
   case M of
      WM_StreamClose :begin
                        imodNum := -1;   // -1 : No plug-ins is in use
                      end;
      WM_StartPlay : begin
                        BASS_ChannelPlay(HBASSStream, false);
                        ResumeMode := false;
                     end;
      WM_WA_MPEG_EOF :begin
                        PlayEnded := true;
                        PlayEnd(Player.Music);
                      end;
      WM_QueryEndSession :   {Result := 1};    // Allow system termination
   end;
   result:=DefWindowProc(H,M,W,L);
end;

function omodGetOutputTime : integer; cdecl;  // returns played time in MS
begin
   result := posDelta +
     round(1000 * BASS_ChannelGetPosition(HBASSStream,BASS_POS_BYTE) / (SPS * (BPS SHR 3) * Channels));
end;

function omodGetWrittenTime : integer; cdecl;
// returns time written in MS (used for synching up vis stuff)
begin
   result := posDelta + round(1000 * totalWritten / (SPS * (BPS SHR 3) * Channels));
end;

function ReachedEnd:boolean;
begin
  result:=(Player.Music.ChannelLength<=omodGetWrittenTime);
end;

// Stream writing callback function for BASS
function StreamWriter(handle : HSTREAM;
                      buffer : pointer;
                      length : DWORD;
                      user : DWORD) : DWORD; stdcall;
// The value of 'length'(data transfering size) is as follows for some cases
//  SPS : 48000, BPS : 16, Channels : 2 -> 38400
//  SPS : 44100, BPS : 16, Channels : 2 -> 35280
//  SPS : 22050, BPS : 8,  Channels : 1 -> 4410
var
   p, p2 : pointer;
   dw : dword;
   wCycle : word;
begin
 // Let BASS wait until sound data is ready
 // This waiting routine is not necessary in most cases.
 // This routine is added for the Winamp input plug-in "in_midi.dll" (v2.64,
 // written by Mr. Peter Pawlowski) which takes long time to get sound data after
 // changing play position.
   if not PlayEnded then
      if DataSpace(bufSize, rOffset, wOffset) < length then
         begin
            wCycle := 0;
            while DataSpace(bufSize, rOffset, wOffset) < length do
            begin
               if ReachedEnd then BASS_ChannelStop(HBASSStream);
               WinProcessMessages;
               Sleep(50);
               inc(wCycle);
               if wCycle = 60 then     // 3 sec time out
                  break;
               if PlayEnded then
                  break;
            end;
            ResumeMode := false;
         end;

   while isWriting do  // avoid duplicate access on buffer
      begin
         WinProcessMessages;
         sleep(20);
      end;

   isReading := true;  // notify that BASS is reading buffer data
   dw := dword(pBuf) + rOffset;
   p := pointer(dw);   // p : starting address to read

   if rOffset > wOffset then
      if (bufSize - rOffset) > length then
      begin
         Move(p^, buffer^, length);
         inc(rOffset, length);
         result := length;
      end else
      begin
         Move(p^, buffer^, bufSize - rOffset);
         if (bufSize - rOffset) < length then
         begin
            dw := dword(buffer) + bufSize - rOffset;
            p2 := pointer(dw);
            Move(pBuf^, p2^, length - (bufSize - rOffset));
         end;
         rOffset := length - (bufSize - rOffset);
         result := length;
      end
   else
      if rOffset < wOffset then
         if (wOffset - rOffset) >= length then
         begin
            Move(p^, buffer^, length);
            inc(rOffset, length);
            result := length;
         end else
         begin
            Move(p^, buffer^, wOffset - rOffset);
            rOffset := wOffset;
            result := wOffset - rOffset;
         end
   else   // rOffset = wOffset : no data in buffer
      result := 0;

 // Let BASS wait until sound data is ready for long latency devices such CDROM
 // (for the delay time drive's spindle motor gets normal speed after stopping)
   if ResumeMode then
      if DataSpace(bufSize, rOffset, wOffset) < length then
      begin
         wCycle := 0;
         while not isWriting do  // until data is written
         begin
            WinProcessMessages;
            Sleep(200);
            inc(wCycle);
            if wCycle = 50 then  // 10 sec time out
                break;
         end;
         ResumeMode := false;
      end;

   isReading := false;
end;

procedure omodConfig(hwndParent : hwnd); cdecl; // configuration dialog
begin

end;

procedure omodAbout(hwndParent : hwnd); cdecl;  // about dialog
begin

end;

procedure omodInit; cdecl;     // called when loaded
begin
   if omodReady then
      exit;
   omodReady := true;
end;

procedure omodQuit; cdecl;    // called when unloaded
begin
   if omodReady then
      omodReady := false;
end;

function omodOpen(samplerate, numchannels, bitspersamp, bufferlenms,
                                           prebufferms : integer) : integer; cdecl;
// returns >=0 on success, <0 on failure
// called by input plug-in just before starting play
var
   flags : DWORD;
begin
   if not omodReady then
   begin
      result := -1;
      exit;
   end;

   if bitspersamp = 8 then
      flags := BASS_SAMPLE_8BITS
   else
      flags := 0;
   if numchannels = 1 then
      flags := flags + BASS_SAMPLE_MONO;

   HBASSStream := BASS_StreamCreate(samplerate,
                                    numchannels,
                                    flags,
                                    @StreamWriter,
                                    nil {DWORD user data});
   if HBASSStream = 0 then   // 0 : error
      result := -1
   else
   begin
      SPS := samplerate;
      BPS := bitspersamp;
      Channels := numchannels;
  // BASSDataSize : The data size of each transfering to BASS (= amount to play 0.2sec period)
      BASSDataSize := (SPS * (BPS div 8) * CHANNELS * 2) div 10;
      posDelta := 0;
      PostMessage(Player.MainWindow, WM_BASS_StreamCreate, HBASSStream, 0);
      result := 0;
   end;

   rOffset := 0;
   wOffset := 0;
   totalWritten := 0;
   isReading := false;
   isWriting := false;
   PosChanged := false;
   InitialBufferFill := true;
   PlayEnded := false;
end;

procedure omodClose; cdecl;   // close the ol' output device.
begin
   if BASS_ChannelIsActive(HBASSStream) = BASS_ACTIVE_PLAYING then
      BASS_ChannelStop(HBASSStream);
   BASS_StreamFree(HBASSStream);
  { if not PlayEnded then
      if hMainWindow <> 0 then
         PostMessage(hMainWindow, WM_BASS_StreamFree, 0, 0); }
   HBASSStream := 0;
end;

function omodWrite(buf : pointer; len : integer) : integer; cdecl;
// 0 on success. Len == bytes to write (<= 8192 always).
// 1 returns not able to write (yet). Non-blocking, always.

var
   dw : dword;
   p, p2 : pointer;
begin
   while isReading do
      begin
         WinProcessMessages;
         sleep(20);
      end;

   isWriting := true;   // notify that Winamp input plug-in is writing sound data
   dw := dword(pBuf) + wOffset;
   p := pointer(dw);   // p : starting address to write

   if FreeSpace(bufSize, rOffset, wOffset) > len then
   begin
      if rOffset > wOffset then
      begin
            Move(buf^, p^, len);
            inc(wOffset, len);
      end
      else
         if (bufSize - wOffset) > len then
         begin
            Move(buf^, p^, len);
            inc(wOffset, len);
         end else
         begin
            Move(buf^, p^, bufSize - wOffset);
            if (bufSize - wOffset) < len then
            begin
               dw := dword(buf) + (bufSize - wOffset);
               p2 := pointer(dw);
               Move(p2^, pBuf^, len - (bufSize - wOffset));
            end;
            wOffset := len - (bufSize - wOffset);
         end;

      inc(totalWritten, len);
      result := 0;
   end else
   begin
      result := 1;      // This case should not happen !
   end;

   isWriting := false;

 // Let BASS start playing after initial buffer filling
   if InitialBufferFill then
      if (FreeSpace(bufSize, rOffset, wOffset) <= 8192) or
         (DataSpace(bufSize, rOffset, wOffset) >= (BASSDataSize SHL 1)) then
      begin
         InitialBufferFill := false;
         if HBASSStream <> 0 then
         begin
            ChannelInfo.BPS := BPS;
            ChannelInfo.SampleRate := SPS;
            ChannelInfo.Channels := Channels;
            PostMessage(Player.MainWindow, WM_StartPlay, 0, longint(@ChannelInfo));
         end;
      end;
end;

function omodCanWrite: integer; cdecl;
// returns number of bytes possible to write at a given time.
// Never will decrease unless you call Write (or Close, heh)
begin
   if isReading then
   begin
      result := 0;
      exit;
   end;

 // Subtract 1 from real free space to prevent wOffset become equal to rOffset
 // after writing data.  (it means there is no data in buffer if rOffset equals
 //  to wOffset)
   result := FreeSpace(bufSize, rOffset, wOffset) - 1;
end;

function omodIsPlaying : integer; cdecl;
// non0 if output is still going or if data in buffers waiting to be
// written (i.e. closing while IsPlaying() returns 1 would truncate the song
begin
   if BASS_ChannelIsActive(HBASSStream) = BASS_ACTIVE_PLAYING then
      result := 1
   else
      result := 0;
end;

function omodPause(pause : integer) : integer; cdecl;
// returns previous pause state
begin
   if BASS_ChannelIsActive(HBASSStream) = BASS_ACTIVE_PAUSED then
      result := 1
   else
      result := 0;

   if (pause = 0) and (result = 1) then
   begin
      ResumeMode := true;
      BASS_ChannelPlay(HBASSStream,false);
   end

// This case will happen in the sequence, Pause -> position change -> resume
   else if (pause = 0) then
   begin
      ResumeMode := true;
      BASS_ChannelPlay(HBASSStream, true)
   end else if (pause <> 0) and (result = 0) then
      BASS_ChannelPause(HBASSStream);
end;

procedure omodSetVolume(volume : integer); cdecl; // volume is 0-255
var
   SetVolume : integer;
begin
   SetVolume := round(volume * (100 / 255));
   if SetVolume < 0 then
      SetVolume := 99;
   BASS_SetVolume(SetVolume);
end;

procedure omodSetPan(pan : integer); cdecl;       // pan is -128 to 128
begin
   BASS_ChannelSetAttribute(HBASSStream,BASS_ATTRIB_PAN,pan/128);
end;

procedure omodFlush(t : integer); cdecl;
// flushes buffers and restarts output at time t (in ms) (used for seeking)
// This procedure is called by input plug-in when position is changed
var
   chnStatus : dword;
   wasPlaying : boolean;
begin
   chnStatus := BASS_ChannelIsActive(HBASSStream);
   if (chnStatus <> BASS_ACTIVE_PAUSED) and
      (chnStatus <> BASS_ACTIVE_STOPPED) then
      if (FreeSpace(bufSize, rOffset, wOffset) > 0) or
         (chnStatus = BASS_ACTIVE_PLAYING) then
      begin
         wasPlaying := true;
      end else
         wasPlaying := false
   else
      wasPlaying := false;

 // Normally this omod.Flush procedure is called by Winamp input plug-in when
 // play position is changed but "in_midi.dll" (v2.64, written by Mr. Peter
 // Pawlowski) calls this procedure when it starts play after pause without
 // position change.
 // The variable PosChanged is used to know if the position has been changed or not.
 // Set PosChanged true in the main program if position is cahnged.
   if chnStatus = BASS_ACTIVE_PAUSED then
      if not PosChanged then  // if position has not been changed then escape
         exit;

   while isReading do
      begin
         WinProcessMessages;
         sleep(20);
      end;

 // Use BASS_ChannelStop regardless the status of BASS_Channel to flush the buffer in BASS
   BASS_ChannelStop(HBASSStream);

   if wasPlaying then
      InitialBufferFill := true;   // Reset to restart after seeking

   posDelta := t;
   rOffset := 0;
   wOffset := 0;
   totalWritten := 0;
   isWriting := false;
   PosChanged := false;
end;

//----------- The end of functions and procedures for Winamp plug-in -----------


procedure SetOutputPlugin;
begin
   omod := @omod1;
   omod.version := $10;
   omod.description := 'BASS Winamp output plug-in emulator';
   omod.id := 65536 + 1;
   omod.hMainWindow := Player.MainWindow;
   omod.hDllInstance := 0;
   omod.Config := omodConfig;
   omod.About := omodAbout;
   omod.Init := omodInit;
   omod.Quit := omodQuit;
   omod.Open := omodOpen;
   omod.Close := omodClose;
   omod.Write := omodWrite;
   omod.CanWrite := omodCanWrite;
   omod.IsPlaying := omodIsPlaying;
   omod.Pause := omodPause;
   omod.SetVolume := omodSetVolume;
   omod.SetPan := omodSetPan;
   omod.Flush := omodFlush;
   omod.GetOutputTime := omodGetOutputTime;
   omod.GetWrittenTime := omodGetWrittenTime;

   omod.Init;
end;


function GetMsgHandler : hwnd;
begin
   result := Player.MainWindow;
end;


//---------------------- The plug-in managing part  ----------------------------

// get the reference number of specified plug-in
//  ( -1 :  specified plug-in is not loaded )
function GetPluginIndex(PluginName : string) : integer;
var
   i : integer;
begin
   result := -1;

   for i := 0 to MaxPluginNum - 1 do
      if Plugin[i] <> nil then
         if PluginInfo[i].Name = PluginName then
         begin
            result := i;
            break;
         end;
end;

function SelectInputPlugin(PluginNum : integer) : integer;
begin
   result := -1;

   if (PluginNum < -1) or (PluginNum > (MaxPluginNum - 1)) then
      exit;
   if Plugin[PluginNum] = nil then   // no valid Plugin[x]
      exit;

   imodNum := PluginNum;
   result := 0;
end;

// load a Winamp input plug-in  ( 0 : succeed )
function LoadWinampPlugin(PluginName : string) : integer;
var
   indexToLoad : integer;
   DLLHandle : THandle;
   getInModule2 : function : pointer; stdcall;
   dw : dword;
   i : integer;
begin
   if not omodReady then
   begin
      result := ERROR_OMOD_NOTREADY;  // output plug-in emulator is not ready
      exit;
   end;

   for i := 0 to MaxPluginNum - 1 do
      if Plugin[i] <> nil then     // Plugin[i] = nil -> empty element
         if PluginInfo[i].Name = PluginName then
         begin
            result := ERROR_LOADED_BEFORE;  // already loaded plug-in
            exit;
         end;

   indexToLoad := -1;
   for i := 0 to MaxPluginNum - 1 do
      if Plugin[i] = nil then
      begin
         indexToLoad := i;       // found out empty element to use
         break;
      end;
   if indexToLoad = -1 then
   begin
      result := ERROR_LOADED_FULL;     // no empty element to use
      exit;
   end;
   DLLHandle := LoadLibrary(pchar(PluginName));
   if DLLHandle = 0 then
   begin
     result := ERROR_CANNOT_LOAD;
     exit;
   end;

   getInModule2 := GetProcAddress(DLLHandle, 'winampGetInModule2');
   if getInModule2 = nil then
   begin
     result := ERROR_INVALID_PLUGIN;
     FreeLibrary(DLLHandle);
     exit;
   end;

   imod := getInModule2;
   imod.hMainWindow := Player.MainWindow;  // <= handle of internal messsage handler
   imod.hDllInstance := DllHandle;
   imod.outMod := omod;
   imod.init;
   imod.SetInfo := SetInfo1;
   imod.dsp_IsActive := dsp_isactive1;
   imod.dsp_dosamples := dsp_dosamples1;
   imod.SAVSAInit := SAVSAInit1;
   imod.SAVSADeInit := SAVSADeinit1;
   imod.SAAddPCMData := SAAddPCMData1;
   imod.SAGetMode := SAGetMode1;
   imod.SAAdd := SAADD1;
   imod.VSASetInfo := VSASetInfo1;
   imod.VSAAddPCMData := VSAAddPCMData1;
   imod.VSAGetMode := VSAGetMode1;
   imod.VSAAdd := VSAAdd1;
//   imod.About(0);

   dw := dword(imod);
   Plugin[indexToLoad] := pointer(dw);
   PluginInfo[indexToLoad].Name := PluginName;
   PluginInfo[indexToLoad].Version := imod.version;
   PluginInfo[indexToLoad].Description := string(imod.description);
   PluginInfo[indexToLoad].FileExtensions := string(imod.FileExtensions);
   PluginInfo[indexToLoad].Seekable := imod.is_seekable;
   PluginInfo[indexToLoad].DLLHandle := DLLHandle;

   result := 0;
end;

// unload a Winamp input plug-in  ( 0 : succeed )
function UnloadWinampPlugin(PluginName : string) : integer;
var
   indexToUnload : integer;
   returnOK : longbool;
begin
   indexToUnload := GetPluginIndex(PluginName);

   if indexToUnload = -1 then
   begin
      result := ERROR_NOT_LOADED;
      exit;
   end;
   if indexToUnload = imodNum then
   begin
      result := ERROR_IN_USE;
      exit;
   end;

   Plugin[indexToUnload].Quit;
   returnOK := FreeLibrary(PluginInfo[indexToUnload].DLLHandle);
   if returnOK then
   begin
      result := 0;
      Plugin[indexToUnload] := nil;
   end else
      result := ERROR_CANNOT_UNLOAD;
end;

//------------------ The end of plug-in managing part --------------------------


// initialization
function InitPluginCtrl : boolean;
var
   i : integer;
begin
    player:=getgameplayer;
   for i := 0 to MaxPluginNum - 1 do
     Plugin[i] := nil;
   omodReady := false;
   GetMem(pBuf, bufSize); // Get memory to hold sound data from Winamp input plug-in
   if (pBuf <> nil) then
      SetOutputPlugin;

   imodNum := -1;
   result := omodReady;
end;

// finalization
procedure QuitPluginCtrl;
var
   i : integer;
begin
   for i := 0 to MaxPluginNum - 1 do
      if (Plugin[i] <> nil) then
      begin                           // Release all Winamp input plug-ins
         Plugin[i].Quit;
         FreeLibrary(PluginInfo[i].DLLHandle);
      end;

   if (omod <> nil) then
      omod.Quit;

   if (pBuf <> nil) then
      FreeMem(pBuf);
end;

initialization
  Player:=GetGamePlayer;

end.
