unit AudioMonkey;

interface

uses KOL, ATLCommon, TagID3v1, TagID3v2, TagAPE;

type TMonkeyAudioInfo = PAudioInfo;

function NewAudioMonkey: TMonkeyAudioInfo;

implementation

uses ATLLib;

type
   // Common header for all versions
   TAPE_Header = packed record
      cID: array[0..3] of Char;                       // should equal 'MAC '
      nVersion : WORD;                 // version number * 1000 (3.81 = 3810)
   end;
   // Old header for <= 3.97
   TAPE_Header_Old = packed record
      nCompressionLevel: word;         // the compression level
      nFormatFlags: word;              // any format flags (for future use)
      nChannels: word;                 // the number of channels (1 or 2)
      nSampleRate: longword;           // the sample rate (typically 44100)
      nHeaderBytes: longword;          // the bytes after the MAC header that compose the WAV header
      nTerminatingBytes: longword;     // the bytes after that raw data (for extended info)
      nTotalFrames: longword;          // 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
   TAPE_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
   TApe_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;

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 }

   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

type
  TAudioMonkey = object(TAudioInfo)
  end;

function AudioMonkeyReader(const AFileName: KOLstring; AudioInfo: TMonkeyAudioInfo): Boolean; forward;

function NewAudioMonkey: TMonkeyAudioInfo;
begin
    New(Result, Create);
    with TAudioMonkey(Result^) do begin
      TagAdd(tID3v1, NewTagID3v1);
      TagAdd(tID3v2, NewTagID3v2);
      TagAdd(tAPE, NewTagAPE);
      FAudioVendor:= aMonkey;
      FExistsProps:= [apSampleRate, apBitrate, apChannels, apBits, apSamples, apDuration, apRatio];
      FReader:= AudioMonkeyReader;
    end;
end;

function AudioMonkeyReader(const AFileName: KOLstring; AudioInfo: TMonkeyAudioInfo): Boolean;
var Source : PStream;
    APE_OLD: TAPE_HEADER_OLD; // old header   <= 3.97
    APE_DESC: TApe_Descriptor; // extra header >= 3.98
    BlocksPerFrame, TagSize: LongWord;
    APE_CommonHeader: TAPE_Header;
    APE_Header: TAPE_Header_New;
    S: String;
begin
    with TAudioMonkey(AudioInfo^) do begin
      Result:= False;
      try
         ResetData;
         try
            Source:= OpenFileRead(AFileName);
            FFileSize:= Source.Size;

            TagInfo[tID3v2].ReadFromFile(AFileName);
            TagInfo[tID3v1].ReadFromFile(AFileName);
            TagInfo[tAPE].ReadFromFile(AFileName);
            TagSize:= TagInfo[tID3v1].TagSize + PTagID3v1(TagInfo[tID3v1]).Lyrics.TagSize + TagInfo[tID3v2].TagSize + TagInfo[tAPE].TagSize;
            if TagInfo[tID3v2].Exists then Source.Seek(TagInfo[tID3v2].TagSize, spBegin);

            if (Source.Read(APE_CommonHeader, SizeOf(TAPE_Header)) = SizeOf(TAPE_Header)) and (APE_CommonHeader.cID = 'MAC ') then begin

              if APE_CommonHeader.nVersion >= 3980 then begin // Load New Monkey's Audio Header for version >= 3.98
                if (Source.Read(APE_DESC, SizeOf(TApe_Descriptor)) = SizeOf(TApe_Descriptor)) then begin
                  if APE_DESC.nDescriptorBytes <> 52 then Source.Seek(APE_DESC.nDescriptorBytes - 52, spCurrent);
                  if APE_DESC.nHeaderBytes > SizeOf(TAPE_Header_New) then APE_DESC.nHeaderBytes:= SizeOf(TAPE_Header_New);
                  if (longword(Source.Read(APE_Header, APE_DESC.nHeaderBytes)) = APE_DESC.nHeaderBytes ) then begin
                    if APE_Header.nTotalFrames>0 then FSamples:= APE_Header.nBlocksPerFrame * (APE_Header.nTotalFrames-1) + (APE_Header.nFinalFrameBlocks);
                    Result:= True;
                  end;
                end;
              end
              else begin // Old Monkey <= 3.97
                if (Source.Read(APE_OLD, SizeOf(APE_OLD)) = SizeOf(APE_OLD) ) then begin
                  APE_Header.nCompressionLevel:= APE_OLD.nCompressionLevel;
                  APE_Header.nSampleRate:= APE_OLD.nSampleRate;
                  APE_Header.nChannels:= APE_OLD.nChannels;
                  APE_Header.nFormatFlags:= APE_OLD.nFormatFlags;

                  APE_Header.nBitsPerSample:= 16;
                  if APE_OLD.nFormatFlags and MONKEY_FLAG_8_BIT  <>0 then APE_Header.nBitsPerSample:=  8;
                  if APE_OLD.nFormatFlags and MONKEY_FLAG_24_BIT <>0 then APE_Header.nBitsPerSample:= 24;

  //                FHasSeekElements:= APE_OLD.nFormatFlags and MONKEY_FLAG_SEEK_ELEMENTS <> 0;
  //                FWavNotStored:= APE_OLD.nFormatFlags and MONKEY_FLAG_WAV_NOT_STORED<>0;

                  if APE_OLD.nFormatFlags and MONKEY_FLAG_PEAK_LEVEL <> 0 then begin
                    {$IFDEF USE_OtherAudioProps}
                    OtherProps.Add('PeakLevel='+Int2Str(APE_OLD.nInt));
  //                  OtherProps.Add('PeakLevelRatio='+Double2Str((APE_OLD.nInt / (1 shl APE_Header.nBitsPerSample) / 2.0) * 100));
                    {$ENDIF}
                  end;
                  // based on MAC_SDK_397 (APEinfo.cpp)
                  if (APE_CommonHeader.nVersion >= 3950) then
                    BlocksPerFrame:= 73728 * 4
                  else
                    if (APE_CommonHeader.nVersion >= 3900) or ((APE_CommonHeader.nVersion >= 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
                    FSamples:=  (APE_OLD.nTotalFrames-1) * (BlocksPerFrame) + (APE_OLD.nFinalFrameBlocks);
                  Result:= True;
                end;
              end;

              if Result then begin
                FSampleRate:= APE_Header.nSamplerate;
                FChannels:= APE_Header.nChannels;
                FBits:= APE_Header.nBitsPerSample;
                FChannels:= APE_Header.nChannels;

                if APE_Header.nSampleRate>0 then FDuration:= FSamples / APE_Header.nSampleRate;
                if FDuration>0 then FBitrate:= Round((FFileSize - TagSize)*8.0/(FDuration)/1000);
                {$IFDEF USE_OtherAudioProps}
                S:= format('%u.%u', [APE_CommonHeader.nVersion div 1000, APE_CommonHeader.nVersion mod 1000]);
                OtherProps.Add('Version='+S);
                OtherProps.Add('FormatFlags='+Int2Str(APE_Header.nFormatFlags));

                OtherProps.Add('CompressionLevel='+Int2Str(APE_Header.nCompressionLevel));
                if ((APE_Header.nCompressionLevel mod 1000) = 0) and (APE_Header.nCompressionLevel<=6000) then
                  S:= MONKEY_COMPRESSION[APE_Header.nCompressionLevel div 1000]
                else S:= Int2Str(APE_Header.nCompressionLevel);
                OtherProps.Add('Compression='+S);
                {$ENDIF}

                FValid:= (APE_Header.nBitsPerSample>0) and (APE_Header.nSampleRate>0) and (FSamples>0) and (APE_Header.nChannels>0);

                if FValid then FRatio:= FFileSize / (FSamples * (APE_Header.nChannels * APE_Header.nBitsPerSample / 8) + 44) * 100
              end;

            end;
         finally
            Source.free;
         end;
      except
      end;
    end;
end;

end.
