{ *************************************************************************** }
{                                                                             }
{ Audio Tools Library                                                         }
{ Class TMonkey - for manipulating with Monkey's Audio file information       }
{                                                                             }
{ http://mac.sourceforge.net/atl/                                             }
{ e-mail: macteam@users.sourceforge.net                                       }
{                                                                             }
{ Copyright (c) 2000-2002 by Jurgen Faul                                      }
{ Copyright (c) 2003-2005 by The MAC Team                                     }
{                                                                             }
{ Version 1.6 (11 April 2004) by Gambit                                       }
{   - Added Ratio property again                                              }
{                                                                             }
{ Version 1.5 (22 August 2003) by MaDah                                       }
{   - Added support for Monkey's Audio 3.98                                   }
{   - Added/changed/removed some stuff                                        }
{                                                                             }
{ Version 1.4 (29 July 2002)                                                  }
{   - Correction for calculating of duration                                  }
{                                                                             }
{ Version 1.1 (11 September 2001)                                             }
{   - Added property Samples                                                  }
{   - Removed WAV header information                                          }
{                                                                             }
{ Version 1.0 (7 September 2001)                                              }
{   - Support for Monkey's Audio files                                        }
{   - Class TID3v1: reading & writing support for ID3v1 tags                  }
{   - Class TID3v2: reading & writing support for ID3v2 tags                  }
{   - Class TAPEtag: reading & writing support for APE tags                   }
{                                                                             }
{ This library is free software; you can redistribute it and/or               }
{ modify it under the terms of the GNU Lesser General Public                  }
{ License as published by the Free Software Foundation; either                }
{ version 2.1 of the License, or (at your option) any later version.          }
{                                                                             }
{ This library 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           }
{ Lesser General Public License for more details.                             }
{                                                                             }
{ You should have received a copy of the GNU Lesser General Public            }
{ License along with this library; if not, write to the Free Software         }
{ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA   }
{                                                                             }
{ *************************************************************************** }

unit atlMonkey;

interface

uses
  Classes, SysUtils, atlID3v1, atlID3v2, atlAPEtag;

const
   { Compression level codes }
   MONKEY_COMPRESSION_FAST       = 1000;  { Fast (poor) }
   MONKEY_COMPRESSION_NORMAL     = 2000;  { Normal (good) }
   MONKEY_COMPRESSION_HIGH       = 3000;  { High (very good) }
   MONKEY_COMPRESSION_EXTRA_HIGH = 4000;  { Extra high (best) }
   MONKEY_COMPRESSION_INSANE     = 5000;  { Insane }
   MONKEY_COMPRESSION_BRAINDEAD  = 6000;  { BrainDead }
   { Compression level names }
   MONKEY_COMPRESSION: array [0..6] of string =
    ('Unknown', 'Fast', 'Normal', 'High', 'Extra High', 'Insane', 'BrainDead');

   { Format flags, only for Monkey's Audio <= 3.97 }
   MONKEY_FLAG_8_BIT          = 1;  // Audio 8-bit
   MONKEY_FLAG_CRC            = 2;  // New CRC32 error detection
   MONKEY_FLAG_PEAK_LEVEL     = 4;  // Peak level stored
   MONKEY_FLAG_24_BIT         = 8;  // Audio 24-bit
   MONKEY_FLAG_SEEK_ELEMENTS  = 16; // Number of seek elements stored
   MONKEY_FLAG_WAV_NOT_STORED = 32; // WAV header not stored

  { Channel mode names }
  MONKEY_MODE: array [0..2] of string =
    ('Unknown', 'Mono', 'Stereo');

type
  { Class TMonkey }
  TMonkey = class(TObject)
  private
    { Private declarations }
		FValid     		      : boolean;
    // Stuff loaded from the header:
    FVersion             : integer;
    FVersionStr          : string;
		FChannels  		      : integer;
		FSampleRate		      : integer;
		FBits      		      : integer;
    FPeakLevel           : longword;
    FPeakLevelRatio      : double;
    FTotalSamples        : int64;
		FBitrate  		      : double;
		FDuration		      : double;
		FCompressionMode     : integer;
    FCompressionModeStr  : string;
    // FormatFlags, only used with Monkey's <= 3.97
    FFormatFlags         : integer;
    FHasPeakLevel        : boolean;
    FHasSeekElements     : boolean;
    FWavNotStored        : boolean;
    // Tagging
    FID3v1               : TID3v1;
    FID3v2               : TID3v2;
    FAPEtag              : TAPEtag;
    //
   	FFileSize  		      : int64;

    procedure FResetData;

    function FGetRatio: Double;

  public
    { Public declarations }
    constructor Create;                                     { Create object }
    destructor Destroy; override;                          { Destroy object }

    function ReadFromFile(const FileName: string): Boolean;   { Load header }

    property FileSize 	      : int64		read FFileSize;
		property Valid     	      : boolean	read FValid;
    property Version           : integer   read FVersion;
    property VersionStr        : string    read FVersionStr;
		property Channels  	      : integer	read FChannels;
		property SampleRate	      : integer	read FSamplerate;
		property Bits      	      : integer	read FBits;
		property Bitrate  	      : double		read FBitrate;
		property Duration		      : double		read FDuration;
		property PeakLevel 	      : longword	read FPeakLevel;
		property PeakLevelRatio    : double 	read FPeakLevelRatio;
		property TotalSamples 	   : int64	   read FTotalSamples;
		property CompressionMode 	: integer	read FCompressionMode;
		property CompressionModeStr: string 	read FCompressionModeStr;
    // FormatFlags, only used with Monkey's <= 3.97
		property FormatFlags 	   : integer	read FFormatFlags;
		property HasPeakLevel 	   : boolean	read FHasPeakLevel;
		property HasSeekElements 	: boolean	read FHasSeekElements;
		property WavNotStored 	   : boolean	read FWavNotStored;
    // Tagging
    property ID3v1: TID3v1 read FID3v1;                    { ID3v1 tag data }
    property ID3v2: TID3v2 read FID3v2;                    { ID3v2 tag data }
    property APEtag: TAPEtag read FAPEtag;                   { APE tag data }

    property Ratio: Double read FGetRatio;          { Compression ratio (%) }
  end;

implementation

type
   { Real structure of Monkey's Audio header }
   // common header for all versions
   APE_HEADER = packed record
      cID: array[0..3] of byte;        // should equal 'MAC '
      nVersion : WORD;                 // version number * 1000 (3.81 = 3810)
   end;
   // old header for <= 3.97
   APE_HEADER_OLD = packed record
      nCompressionLevel,               // the compression level
      nFormatFlags,                    // any format flags (for future use)
      nChannels: word;                 // the number of channels (1 or 2)
      nSampleRate,                     // the sample rate (typically 44100)
      nHeaderBytes,                    // the bytes after the MAC header that compose the WAV header
      nTerminatingBytes,               // the bytes after that raw data (for extended info)
      nTotalFrames,                    // the number of frames in the file
      nFinalFrameBlocks: longword;     // the number of samples in the final frame
      nInt : integer;
   end;
   // new header for >= 3.98
   APE_HEADER_NEW = packed record
      nCompressionLevel : word;		   // the compression level (see defines I.E. COMPRESSION_LEVEL_FAST)
      nFormatFlags      : word;			// any format flags (for future use) Note: NOT the same flags as the old header!
      nBlocksPerFrame   : longword;		// the number of audio blocks in one frame
      nFinalFrameBlocks : longword;		// the number of audio blocks in the final frame
      nTotalFrames      : longword;		// the total number of frames
      nBitsPerSample    : word;			// the bits per sample (typically 16)
      nChannels         : word;			// the number of channels (1 or 2)
      nSampleRate       : longword;		// the sample rate (typically 44100)
   end;
   // data descriptor for >= 3.98
   APE_DESCRIPTOR = packed record
      padded : Word;                   // padding/reserved (always empty)
	   nDescriptorBytes,		            // the number of descriptor bytes (allows later expansion of this header)
	   nHeaderBytes,			            // the number of header APE_HEADER bytes
	   nSeekTableBytes,		            // the number of bytes of the seek table
	   nHeaderDataBytes,		            // the number of header data bytes (from original file)
	   nAPEFrameDataBytes,		         // the number of bytes of APE frame data
	   nAPEFrameDataBytesHigh,	         // the high order number of APE frame data bytes
	   nTerminatingDataBytes : longword;	// the terminating data of the file (not including tag data)
      cFileMD5 : array[0..15] of Byte;	// the MD5 hash of the file (see notes for usage... it's a littly tricky)
   end;

{ ********************** Private functions & procedures ********************* }

procedure TMonkey.FResetData;
begin
  { Reset data }
	FValid     		      := false;
  FVersion             := 0;
  FVersionStr          := '';
	FChannels  		      := 0;
	FSampleRate		      := 0;
	FBits      		      := 0;
  FPeakLevel           := 0;
  FPeakLevelRatio      := 0.0;
  FTotalSamples        := 0;
	FBitrate  		      := 0.0;
  FDuration		      := 0.0;
	FCompressionMode     := 0;
  FCompressionModeStr  := '';
  FFormatFlags         := 0;
  FHasPeakLevel        := false;
  FHasSeekElements     := false;
  FWavNotStored        := false;
 	FFileSize  		      := 0;
  FID3v1.ResetData;
  FID3v2.ResetData;
  FAPEtag.ResetData;
end;

{ ********************** Public functions & procedures ********************** }

constructor TMonkey.Create;
begin
  { Create object }
  inherited;
  FID3v1 := TID3v1.Create;
  FID3v2 := TID3v2.Create;
  FAPEtag := TAPEtag.Create;
  FResetData;
end;

{ --------------------------------------------------------------------------- }

destructor TMonkey.Destroy;
begin
  { Destroy object }
  FID3v1.Free;
  FID3v2.Free;
  FAPEtag.Free;
  inherited;
end;

{ --------------------------------------------------------------------------- }

function TMonkey.ReadFromFile(const FileName: string): Boolean;
var
   f              : TFileStream;
   APE            : APE_HEADER;     // common header
   APE_OLD        : APE_HEADER_OLD; // old header   <= 3.97
   APE_NEW        : APE_HEADER_NEW; // new header   >= 3.98
   APE_DESC       : APE_DESCRIPTOR; // extra header >= 3.98
   BlocksPerFrame : integer;
   LoadSuccess    : boolean;
   TagSize        : integer;
begin
   Result := FALSE;
   FResetData;
   // load tags first
   FID3v2.ReadFromFile(FileName);
   FID3v1.ReadFromFile(FileName);
   FAPEtag.ReadFromFile(FileName);
   // calculate total tag size
   TagSize := 0;
   if FID3v1.Exists  then inc(TagSize, 128);
   if FID3v2.Exists  then inc(TagSize, FID3v2.Size);
   if FAPEtag.Exists then inc(TagSize, FAPETag.Size);
   // begin reading data from file
   LoadSuccess := FALSE;
   f:=nil;
   try
      try
         f := TFileStream.create(FileName, fmOpenRead or fmShareDenyNone);
         FFileSize := f.Size;
         // seek past id3v2-tag
         if FID3v2.Exists then begin
            f.Seek(FID3v2.Size, soFromBeginning);
         end;
         // Read APE Format Header
         fillchar(APE, sizeof(APE), 0);
         if (f.Read(APE, sizeof(APE)) = sizeof(APE)) and ( StrLComp(@APE.cID[0],'MAC ',4)=0) then begin
            FVersion       := APE.nVersion;
            Str(FVersion / 1000 : 4 : 2, FVersionStr);
            // Load New Monkey's Audio Header for version >= 3.98
            if APE.nVersion >= 3980 then begin
               fillchar(APE_DESC, sizeof(APE_DESC), 0);
               if (f.Read(APE_DESC, sizeof(APE_DESC)) = sizeof(APE_DESC)) then begin
                  // seek past description header
                  if APE_DESC.nDescriptorBytes <> 52 then f.Seek(APE_DESC.nDescriptorBytes - 52, soFromCurrent);
                  // load new ape_header
                  if APE_DESC.nHeaderBytes > sizeof(APE_NEW) then APE_DESC.nHeaderBytes := sizeof(APE_NEW);
                  fillchar(APE_NEW, sizeof(APE_NEW), 0);
                  if (longword(f.Read(APE_NEW, APE_DESC.nHeaderBytes)) = APE_DESC.nHeaderBytes ) then begin
                     // based on MAC SDK 3.98a1 (APEinfo.h)
                     FSampleRate       := APE_NEW.nSampleRate;
                     FChannels         := APE_NEW.nChannels;
                     FFormatFlags      := APE_NEW.nFormatFlags;
                     FBits             := APE_NEW.nBitsPerSample;
                     FCompressionMode  := APE_NEW.nCompressionLevel;
                     // calculate total uncompressed samples
                     if APE_NEW.nTotalFrames>0 then begin
                        FTotalSamples     := Int64(APE_NEW.nBlocksPerFrame) *
                                             Int64(APE_NEW.nTotalFrames-1) +
                                             Int64(APE_NEW.nFinalFrameBlocks);
                     end;
                     LoadSuccess := TRUE;
                  end;
               end;
            end else begin
               // Old Monkey <= 3.97
               fillchar(APE_OLD, sizeof(APE_OLD), 0);
               if (f.Read(APE_OLD, sizeof(APE_OLD)) = sizeof(APE_OLD) ) then begin
                  FCompressionMode  := APE_OLD.nCompressionLevel;
                  FSampleRate       := APE_OLD.nSampleRate;
                  FChannels         := APE_OLD.nChannels;
                  FFormatFlags      := APE_OLD.nFormatFlags;
                  FBits := 16;
                  if APE_OLD.nFormatFlags and MONKEY_FLAG_8_BIT  <>0 then FBits :=  8;
                  if APE_OLD.nFormatFlags and MONKEY_FLAG_24_BIT <>0 then FBits := 24;
                  FHasSeekElements  := APE_OLD.nFormatFlags and MONKEY_FLAG_PEAK_LEVEL    <>0;
                  FWavNotStored     := APE_OLD.nFormatFlags and MONKEY_FLAG_SEEK_ELEMENTS <>0;
                  FHasPeakLevel     := APE_OLD.nFormatFlags and MONKEY_FLAG_WAV_NOT_STORED<>0;
                  if FHasPeakLevel then begin
                     FPeakLevel        := APE_OLD.nInt;
                     FPeakLevelRatio   := (FPeakLevel / (1 shl FBits) / 2.0) * 100.0;
                  end;
                  // based on MAC_SDK_397 (APEinfo.cpp)
                  if (FVersion >= 3950) then
                     BlocksPerFrame := 73728 * 4
                  else if (FVersion >= 3900) or ((FVersion >= 3800) and (APE_OLD.nCompressionLevel = MONKEY_COMPRESSION_EXTRA_HIGH)) then
                     BlocksPerFrame := 73728
                  else
                     BlocksPerFrame := 9216;
                  // calculate total uncompressed samples
                  if APE_OLD.nTotalFrames>0 then begin
                     FTotalSamples :=  Int64(APE_OLD.nTotalFrames-1) *
                                       Int64(BlocksPerFrame) +
                                       Int64(APE_OLD.nFinalFrameBlocks);
                  end;
                  LoadSuccess := TRUE;
               end;
            end;
            if LoadSuccess then begin
               // compression profile name
               if ((FCompressionMode mod 1000) = 0) and (FCompressionMode<=6000) then begin
                  FCompressionModeStr := MONKEY_COMPRESSION[FCompressionMode div 1000];
               end else begin
                  FCompressionModeStr := IntToStr(FCompressionMode);
               end;
               // length
               if FSampleRate>0 then FDuration := FTotalSamples / FSampleRate;
               // average bitrate
               if FDuration>0 then FBitrate := (FFileSize - Int64(TagSize))*8.0 / (FDuration/1000.0);
               // some extra sanity checks
               FValid   := (FBits>0) and (FSampleRate>0) and (FTotalSamples>0) and (FChannels>0);
               Result   := FValid;
            end;
         end;
      finally
         f.free;
      end;
   except
   end;
end;
   
{ --------------------------------------------------------------------------- }

function TMonkey.FGetRatio: Double;
begin
  { Get compression ratio }
  if FValid then
    Result := FFileSize / (FTotalSamples * (FChannels * FBits / 8) + 44) * 100
  else
    Result := 0;
end;
      
{ --------------------------------------------------------------------------- }

end.
