unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  Math, StdCtrls, Bass, ExtCtrls;

type
  TForm1 = class(TForm)
    OpenDialog1: TOpenDialog;
    Timer1: TTimer;
    PB: TPaintBox;
    procedure FormCreate(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
    procedure FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure PBPaint(Sender: TObject);
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
  private
    function PlayFile: boolean;
    procedure ErrorPop(str: string);
    procedure SetLoopStart(position: qword);
    procedure SetLoopEnd(position: qword);
    procedure ScanPeaks2(decoder: HSTREAM);
    procedure DrawSpectrum;
    procedure DrawTime_Line(position: QWORD; x,y : integer; cl : TColor);
  public
  end;

type TScanThread = class(TThread)
  private
    Fdecoder : HSTREAM;
  protected
    procedure Execute; override;
  public
    constructor Create(decoder:HSTREAM);
end;

procedure LoopSyncProc(handle: HSYNC; channel, data, user: DWORD); stdcall;

var
  Form1: TForm1;
  lsync : HSYNC;		// looping synchronizer handle
  chan : HSTREAM;   // sample stream handle
  loop : array[0..1] of DWORD;
  killscan : boolean;
  bpp : dword; // stream bytes per pixel
  wavebufL : array of smallint;
  wavebufR : array of smallint;
  mousedwn : integer;
  Buffer: TBitmap;

implementation

{$R *.dfm}

//------------------------------------------------------------------------------

procedure TForm1.FormCreate(Sender: TObject);
begin
  // check that BASS 2.1 was loaded
	if not (BASS_GetVersion() = dword(MAKELONG(2,1))) then
  begin
		MessageBox(0,'BASS version 2.1 was not loaded','Incorrect BASS.DLL',0);
		Application.Terminate;
	end;

  //assigning layout properties
  ClientHeight := 201;
  ClientWidth := 600;
  Top := 100;
  Left := 100;
  Buffer := TBitmap.Create;
  Buffer.Width:= PB.Width;
  Buffer.Height:= PB.Height;
  PB.Parent.DoubleBuffered := true;

  //set array size
  setlength(wavebufL,ClientWidth);
  setlength(wavebufR,ClientWidth);

  //init vars
  loop[0] := 0;
  loop[1] := 0;
  
  //init BASS
  if not BASS_Init(1,44100,0,Application.Handle,nil) then
    ErrorPop('Can''t initialize device');
  
  //init timer for updating
  Timer1.Interval := 20; //ms
  Timer1.Enabled := true;

  //main start play function
  if not PlayFile then
  begin
    BASS_Free();
    Application.Terminate;
  end;  
end;

function TForm1.PlayFile : boolean;
var
  filename : string;
  data : array[0..2000] of SmallInt;
  i : integer;
begin
  result := false;
  if OpenDialog1.Execute then
  begin
    filename := OpenDialog1.Filename;
    BringWindowToTop(Form1.Handle);
    SetForegroundWindow(Form1.Handle);

    //creating stream
    chan := BASS_StreamCreateFile(FALSE,pchar(filename),0,0,0);
    if chan = 0 then  ErrorPop('Can''t play file');

    //playing stream and setting global vars
    for i:=0 to length(data)-1 do data[0] := 0;
    bpp := BASS_StreamGetLength(chan) div ClientWidth; // stream bytes per pixel
    if (bpp < BASS_ChannelSeconds2Bytes(chan,0.02)) then // minimum 20ms per pixel (BASS_ChannelGetLevel scans 20ms)
      bpp := BASS_ChannelSeconds2Bytes(chan,0.02);
    BASS_ChannelSetSync(chan,BASS_SYNC_END or BASS_SYNC_MIXTIME,0,LoopSyncProc,0); // set sync to loop at end
    BASS_ChannelPlay(chan,FALSE); // start playing

    //getting peak levels in seperate thread, stream handle as parameter
    TScanThread.Create(BASS_StreamCreateFile(FALSE,pchar(filename),0,0,BASS_STREAM_DECODE));
    result := true;
  end;
end;

procedure TForm1.DrawSpectrum;
var
  i,ht : integer;
begin
  //clear background
  Buffer.Canvas.Brush.Color := clBlack;
  Buffer.Canvas.FillRect(Rect(0,0,Buffer.Width,Buffer.Height));

  //draw peaks
  ht := ClientHeight div 2;
  for i:=0 to length(wavebufL)-1 do
  begin
    Buffer.Canvas.MoveTo(i,ht);
	  Buffer.Canvas.Pen.Color := clLime;
    Buffer.Canvas.LineTo(i,ht-trunc((wavebufL[i]/32768)*ht));
    Buffer.Canvas.Pen.Color := clLime;
    Buffer.Canvas.MoveTo(i,ht+2);
	  Buffer.Canvas.LineTo(i,ht+2+trunc((wavebufR[i]/32768)*ht));
  end;
end;

procedure TForm1.DrawTime_Line(position : QWORD; x,y : integer; cl : TColor);
var
  sectime : integer;
  str:string;
begin
  sectime := trunc(BASS_ChannelBytes2Seconds(chan,position));

  //format time
  str := '';
  if (sectime mod 60 < 10) then str := '0';
  str := str+inttostr(sectime mod 60);
  str := inttostr(sectime div 60)+':'+str;

  //drawline
  Buffer.Canvas.Pen.Color := cl;
  Buffer.Canvas.MoveTo(x,0);
  Buffer.Canvas.LineTo(x,ClientHeight);

  //drawtext
  Buffer.Canvas.Font.Color := cl;
  Buffer.Canvas.Font.Style := [fsBold];
  if x > ClientWidth-20 then
    dec(x,40);
  SetBkMode(Buffer.Canvas.Handle,TRANSPARENT);
  Buffer.Canvas.TextOut(x+2,y,str);
end;               

procedure TForm1.ErrorPop(str:string);
begin
  //show last BASS errorcode when no argument is given, else show given text.
  if str = '' then
    Showmessage('Error code: '+inttostr(BASS_ErrorGetCode()))
  else
    Showmessage(str);
  Application.Terminate;
end;

procedure TForm1.SetLoopStart(position : qword);
begin
  loop[0] := position;
end;

procedure TForm1.SetLoopEnd(position : qword);
begin
  loop[1] := position;
  BASS_ChannelRemoveSync(chan,lsync); // remove old sync
  lsync := BASS_ChannelSetSync(chan,BASS_SYNC_POS or BASS_SYNC_MIXTIME,loop[1],LoopSyncProc,0); // set new sync
end;

procedure LoopSyncProc(handle: HSYNC; channel, data, user: DWORD); stdcall;
begin
	if not BASS_ChannelSetPosition(channel,loop[0]) then // try seeking to loop start
		BASS_ChannelSetPosition(channel,0); // failed, go to start of file instead
end;

procedure TForm1.ScanPeaks2(decoder : HSTREAM);
var
  cpos,level : DWord;
  peak : array[0..1] of DWORD;
  position : DWORD;
  counter : integer;
begin
  cpos := 0;
  peak[0] := 0;
  peak[1] := 0;
  counter := 0;
  
  while not killscan do
  begin
    level := BASS_ChannelGetLevel(decoder); // scan peaks
    if (peak[0]<LOWORD(level)) then
      peak[0]:=LOWORD(level); // set left peak
		if (peak[1]<HIWORD(level)) then
      peak[1]:=HIWORD(level); // set right peak
    if BASS_ChannelIsActive(decoder) <> BASS_ACTIVE_PLAYING then
    begin
      position := cardinal(-1); // reached the end
		end else
      position := BASS_ChannelGetPosition(decoder) div bpp;

    if position > cpos then
    begin
      inc(counter);
      if counter <= length(wavebufL)-1 then
      begin
        wavebufL[counter] := peak[0];
        wavebufR[counter] := peak[1];
      end;

      if (position >= dword(ClientWidth)) then
        break;
      cpos := position;
     end;


    peak[0] := 0;
    peak[1] := 0;
  end;
  BASS_StreamFree(decoder); // free the decoder
end;

//------------------------------------------------------------------------------

{ TScanThread }

constructor TScanThread.Create(decoder: HSTREAM);
begin
  inherited create(false);
  Priority := tpNormal;
  FreeOnTerminate := true;
  FDecoder := decoder;
end;

procedure TScanThread.Execute;
begin
  inherited;
  Form1.ScanPeaks2(FDecoder);
  Terminate;
end;

//------------------------------------------------------------------------------

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  if bpp = 0 then exit;
  DrawSpectrum; // draw peak waveform
  DrawTime_Line(loop[0],(loop[0] div bpp),12,TColor($FFFF00)); // loop start
  DrawTime_Line(loop[1],(loop[1] div bpp),24,TColor($00FFFF)); // loop end
  DrawTime_Line(BASS_ChannelGetPosition(chan),(BASS_ChannelGetPosition(chan) div bpp),0,TColor($FFFFFF)); // current pos
  PB.Refresh;
end;

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if Button = mbLeft then
  begin
    mousedwn := 1;
    SetLoopStart(dword(x)*bpp)
  end
  else if Button = mbRight then
  begin
    mousedwn := 2;
    SetLoopEnd(dword(x)*bpp);
  end;
end;

procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
begin
  if mousedwn = 0 then
    exit;
  if mousedwn = 1 then
    SetLoopStart(dword(x)*bpp)
  else if mousedwn = 2 then
    SetLoopEnd(dword(x)*bpp);
end;

procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  mousedwn := 0;
end;

procedure TForm1.PBPaint(Sender: TObject);
begin
  if bpp = 0 then exit;
  PB.Canvas.Draw(0,0,Buffer);
end;

procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if key = 27 then
    Application.Terminate;
end;

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  Timer1.Enabled := false;
  bpp := 0;
  killscan := true;
  Buffer.Free;
  BASS_Free();
end;

end.
