unit TagMp4;

interface

uses Windows, KOL, ATLCommon;

type
  TAtomType = array [1..4] of Char;
  PAtomMp4 = ^TAtomMp4;
  TAtomMp4 = packed record
    Size: Cardinal;
    AtomType: TAtomType;
    Flags: Word;
    Data: Pointer;

    SizeExtended: Int64;
    AtomTypeExtended: array [1..16] of Char;
    Position: Int64;
    Parent: PAtomMp4;
    Skip: Boolean;
  end;


  PTagMp4 = ^TTagMp4;
  TMp4Tag = PTagMp4;
  TTagMp4 = object(TTagInfo)
  private
    function GetTagsAtomMp4(Index: Integer): PAtomMp4;
    function GetTagCount: Integer;
    function GetTagExString(Index: Integer): String;
    function GetTagsByName(Index: TAtomType): PAtomMp4;
    function GetTagsByNameString(Index: TAtomType): String;

    function AtomToString(Atom: PAtomMp4): String;

    function  NewTagEx(AtomType: TAtomType; AFlags: Word = 0): Integer;
    function GetTag(const Index: TEasyTags): String; virtual;
    procedure SetTag(const Index: TEasyTags; const Value: String); virtual;
    function GetGenreID: Byte;
  public
    destructor Destroy; virtual;
    procedure ResetData; virtual;

    function ReadFromFile(const AFileName: KOLstring): Boolean; virtual;
    function SaveToFile(const AFileName: KOLstring): Boolean; virtual;
    function RemoveFromFile(const AFileName: KOLstring): Boolean; virtual;

    procedure DeleteTag(Index: Integer);                                           // Удаляет тег

    property TagExCount: Integer read GetTagCount;                                 // Количество тегов
    property TagEx[Index: Integer]: PAtomMp4 read GetTagsAtomMp4;                  // Теги (список)
    property TagExString[Index: Integer]: String read GetTagExString;              // Строковое значени тега (по имени)
    property TagExByName[Index: TAtomType]: PAtomMp4 read GetTagsByName;           // Теги (по имени)
    property TagExByNameString[Index: TAtomType]: String read GetTagsByNameString; // Строковое значени тега (по имени)

    property GenreID: Byte read GetGenreID; //(ID3v1 index + 1)
  end;

  function NewTagMp4: TMp4Tag;


  procedure ReadTree(AStream: PStream; AtomList: PList; AEndPos: Int64; AParent: PAtomMp4);
  function ReadPath(AStream: PStream; AtomList: PList; APath: array of TAtomType; AParent: PAtomMp4; var Atom: PAtomMp4; var ASize: Integer): Pointer;

type
  TContainers = (ct_moov, ct_trak, ct_tref, ct_edts, ct_mdia, ct_minf, ct_dinf,
                 ct_stbl, ct_mvex, ct_moof, ct_traf, ct_mfra, ct_mdat, ct_meta,
                 ct_ipro, ct_sinf, ct_xml, ct_bxml, ct_udta, ct_skip, ct_ilst,
                 ct_Name, ct_Artist, ct_Writer, ct_Album, ct_Year, ct_Tools,
                 ct_Comment, ct_Genre, ct_GenreID, ct_TrackNumber, ct_Grouping, ct_DiskNumber,
                 ct_PartOfCompile, ct_Tempo, ct_Cover, ct_Free);

const
  Containers: array[TContainers] of TAtomType =
     ('moov', //container for all the metadata
      'trak', //container for an individual track or stream
      'tref', //track reference container
      'edts', //edit list container
      'mdia', //container for the media information in a track
      'minf', //information container
      'dinf', //data information box, container
      'stbl', //sample table box, container for the time/space map
      'mvex', //movie extends box
      'moof', //movie fragment
      'traf', //track fragment
      'mfra', //movie fragment random access
      'mdat', //media data container
      'meta', //metadata
      'ipro', //item protection
      'sinf', //protection scheme information box
      'xml ', //XML container
      'bxml', //binary XML container
      'udta', //user-data
      'skip', //free space
      'ilst',

      '©nam', //Name of the song/movie (string)
      '©ART', //Name of the artist/performer (string)
      '©wrt', //Name of the writer (string)
      '©alb', //Name of the album (string)
      '©day', //Year (4 bytes, e.g. "2003") (string)
      '©too', //Tool(s) used to create the file (string)
      '©cmt', //Comment (string)
      '©gen', //Custom genre (string)
      'gnre', //Genre (16 bit genre) (ID3v1 index + 1)
      'trkn', //Tracknumber (8 byte string): 16 bit: empty; 16 bit: tracknumber; 16 bit: total tracks on album; 16 bit: empty
      '©grp', //Grouping (string)
      'disk', //Disknumber (8 byte string): 16 bit: empty; 16 bit: disknumber; 16 bit: total number of disks; 16 bit: empty
      'cpil', //Part of a compilation (1 byte, 1 or 0)
      'tmpo', //Tempo in BPM (16 bit)
      'covr', //Cover art (xx bytes binary data)
      '----' //Free form metadata, can have any name and any data
     );
// ct_Cover, ct_Free

implementation

uses ATLLib;

function NewTagMp4: TMp4Tag;
begin
    New(Result, Create);
    Result.FExistsTags:= [etTitle, etArtist, etComposer, etAlbum, etYear, etEncoder, etComment, etGenre, etTrack];
    Result.FTagVendor:= tMp4;
end;

{ TTagMp4 }

destructor TTagMp4.Destroy;
begin
    ResetData;
    inherited;
end;

procedure TTagMp4.ResetData;
var i: Integer;
begin
    FExists:= False;
    FTagSize:= 0;
    for i:= FTagList.Count-1 downto 0 do DeleteTag(i);
end;

function TTagMp4.ReadFromFile(const AFileName: KOLstring): Boolean;
var SourceFile: PStream;
    AtomList: PList;
    i, k, Sz: Integer;
    Buff, P: Pointer;
    Cont: TContainers;
    S, SS: String;
    Atom: PAtomMp4;
begin
    Result:= False; 
    try
      SourceFile:= OpenFileRead(AFileName);
      AtomList:= NewList;
      try
        ReadTree(SourceFile, AtomList, SourceFile.Size, nil);

        FExists:= (AtomList.Count>0) and (PAtomMp4(AtomList.Items[0]).AtomType = 'ftyp');
        if not FExists then Exit;

        for Cont:= ct_Name to ct_Free do begin
          Buff:= ReadPath(SourceFile, AtomList,  [Containers[ct_moov], Containers[ct_udta], Containers[ct_meta], Containers[ct_ilst], Containers[Cont], 'data'], nil, Atom, Sz);

          if Sz > 0 then begin
            k:= NewTagEx(Containers[Cont], 0);
            TagEx[k].Size:= Sz;
            TagEx[k].Data:= Buff;
            TagEx[k].Flags:= SwapOrder32(Integer(Buff^)) and $00FFFFFF;

            FExists:= True;
          end;
        end;

        S:= '';
        for i:= 0 to AtomList.Count-1 do begin
           SS:= '';
           P:= PAtomMp4(AtomList.Items[i]).Parent;
           while P <> nil do begin
             SS:= SS + '    ';
             P:= PAtomMp4(P).Parent;
           end;
           S:= S + SS + PAtomMp4(AtomList.Items[i]).AtomType + ':     ' +  Int2Str(PAtomMp4(AtomList.Items[i]).SizeExtended) + #13#10;
        end;
        StrSaveToFile('!', S);

        Result:= True;
      finally
        AtomList.Release;
        SourceFile.Free;
      end;
    except end;
end;

function TTagMp4.SaveToFile(const AFileName: KOLstring): Boolean;
var SourceFile: PStream;
begin
    Result:= False;
    try
      SourceFile:= OpenFileWrite(AFileName);
      try


        Result:= True;
      finally
        SourceFile.Free;
      end;
    except end;
end;

function TTagMp4.RemoveFromFile(const AFileName: KOLstring): Boolean;
var SourceFile: PStream;
begin
    Result:= False;
    try
      SourceFile:= OpenFileWrite(AFileName);
      try


        Result:= True;
      finally
        SourceFile.Free;
      end;
    except end;
end;

function TTagMp4.AtomToString(Atom: PAtomMp4): String;
begin
    Result:= '';
    if Atom = nil then Exit;
    case Atom.Flags of
      $01: begin {Text}
        SetLength(Result, Atom.Size-8);
        Move((PChar(Atom.Data)+8)^,  Result[1], Atom.Size-8);
        Result:= Utf8ToAnsi(Result);
      end;
      $00: begin {Data}
        if (Atom.AtomType = Containers[ct_TrackNumber]) or (Atom.AtomType = Containers[ct_DiskNumber]) then begin
          Result:= Int2Str(System.Swap(PWord(PChar(Atom.Data)+8+2)^));
          if PWord(PChar(Atom.Data)+8+4)^ <> 0 then Result:= Result + '/' + Int2Str(System.Swap(PWord(PChar(Atom.Data)+8+4)^));
        end;
        if Atom.AtomType = Containers[ct_GenreID] then Result:= Int2Str(System.Swap(PWord(PChar(Atom.Data)+8)^));
      end;
      $15: begin {tmpo/cpil}
        if Atom.AtomType = Containers[ct_Tempo] then Result:= Int2Str(System.Swap(PWord(PChar(Atom.Data)+8)^));
        if Atom.AtomType = Containers[ct_PartOfCompile] then Result:= Int2Str(PByte(PChar(Atom.Data)+8)^);
      end;
//      $0D:; {Image data}
    end;
end;

procedure TTagMp4.DeleteTag(Index: Integer);
begin
    DisposeMem(TagEx[Index].Data);
    FreeMem(Pointer(FTagList.Objects[Index]));
    FTagList.Delete(Index);
end;

function TTagMp4.GetGenreID: Byte;
var Atom: PAtomMp4;
begin
    Result:= 0;
    Atom:= TagExByName[Containers[ct_GenreID]];
    if Atom = nil then Exit;
    Result:= System.Swap(PWord(PChar(Atom.Data)+8)^);
end;

function TTagMp4.GetTag(const Index: TEasyTags): String;
var Cont: TContainers;
begin
    Result:= '';
    case Index of
      etTitle: Cont:= ct_Name;
      etArtist: Cont:= ct_Artist;
      etComposer: Cont:= ct_Writer;
      etAlbum: Cont:= ct_Album;
      etYear: Cont:= ct_Year;
      etEncoder: Cont:= ct_Tools;
      etComment: Cont:= ct_Comment;
      etGenre: Cont:= ct_Genre;
      etTrack: Cont:= ct_TrackNumber;
      else Exit;
    end;
    Result:= AtomToString(TagExByName[Containers[Cont]]);
//                 ct_GenreID, , ct_Grouping, ct_DiskNumber,
//                 ct_PartOfCompile, ct_Tempo, ct_Cover, ct_Free);
end;

function TTagMp4.GetTagCount: Integer;
begin
    Result:= FTagList.Count;
end;

function TTagMp4.GetTagExString(Index: Integer): String;
begin
    Result:= AtomToString(TagEx[Index]);
end;

function TTagMp4.GetTagsAtomMp4(Index: Integer): PAtomMp4;
begin
    Result:= PAtomMp4(FTagList.Objects[Index]);
end;

function TTagMp4.GetTagsByName(Index: TAtomType): PAtomMp4;
var i: Integer;
begin
    Result:= nil;
    for i:= 0 to FTagList.Count-1 do
      if AnsiUpperCase(TagEx[i].AtomType) = AnsiUpperCase(Index) then begin
        Result:= TagEx[i];
        Break;
      end;
end;

function TTagMp4.GetTagsByNameString(Index: TAtomType): String;
begin
    Result:= AtomToString(TagExByName[Index]);
end;

function TTagMp4.NewTagEx(AtomType: TAtomType; AFlags: Word): Integer;
var AtomMp4: PAtomMp4;
begin
    New(AtomMp4); AtomMp4.AtomType:= AtomType; AtomMp4.Flags:= AFlags; AtomMp4.Size:= 0;
    AtomMp4.Data:= AllocMem(AtomMp4.Size);
    FTagList.AddObject('', Integer(AtomMp4));
    Result:= FTagList.Count-1;
end;

procedure TTagMp4.SetTag(const Index: TEasyTags; const Value: String);
begin

end;

procedure ReadContainer(AStream: PStream; AtomMp4: PAtomMp4);
var ContainerType: TContainers;
    k: Integer;
begin
    AtomMp4.Skip:= False;
    k:= Pos(AtomMp4.AtomType, PChar(@Containers)+#0);
    if k > 0 then begin
      ContainerType:= TContainers((k-1) div 4);
      case ContainerType of
        ct_moov: AtomMp4.Skip:= False;
        ct_udta: AtomMp4.Skip:= False;
        ct_ilst: AtomMp4.Skip:= False;
        ct_Name..ct_Free: AtomMp4.Skip:= False;

        ct_meta: begin
          AStream.Seek(4, spCurrent);
          AtomMp4.Skip:= False;
        end;
        ct_trak: AtomMp4.Skip:= False;
{
        ct_moof: AtomMp4.Skip:= False;
        ct_traf: AtomMp4.Skip:= False;

        ct_mvex: AtomMp4.Skip:= False;
        ct_trak: AtomMp4.Skip:= False;
        ct_edts: AtomMp4.Skip:= False;
        ct_tref: AtomMp4.Skip:= False;
        ct_mdia: AtomMp4.Skip:= False;
        ct_minf: AtomMp4.Skip:= False;
        ct_stbl: AtomMp4.Skip:= False;
        ct_dinf: AtomMp4.Skip:= False;

        ct_skip, ct_mdat: AtomMp4.Skip:= True;
        ct_ipro, ct_sinf, ct_mfra, ct_xml, ct_bxml: AtomMp4.Skip:= True;
}
//        else AtomMp4.Skip:= True;
      end;
    end;
end;

procedure ReadTree(AStream: PStream; AtomList: PList; AEndPos: Int64; AParent: PAtomMp4);
function IsContainer(AtomMp4: PAtomMp4): Boolean;
begin
    Result:= not AtomMp4.Skip and (AtomMp4.SizeExtended>8) and (AtomMp4.Size>0) and (Pos(AtomMp4.AtomType, PChar(@Containers)+#0)>0);
end;

var AtomMp4: PAtomMp4;
begin
    repeat
      New(AtomMp4);
      AtomMp4.Parent:= AParent;
      AtomMp4.Position:= AStream.Position; //Запоминаем позицию атома
      AStream.Read(AtomMp4^, 8); //Читаем заголовок атома
      //Определяем размер атома
      AtomMp4.Size:= SwapOrder32(AtomMp4.Size);
      case AtomMp4.Size of
        1: begin
          AStream.Read(AtomMp4^.SizeExtended, 8);
          AtomMp4.SizeExtended:= SwapOrder64(AtomMp4.SizeExtended);
        end;
        0: AtomMp4.SizeExtended:= AEndPos - AtomMp4.Position;
        else AtomMp4.SizeExtended:= AtomMp4.Size;
      end;
      //Зитаем расширенный заголовок
      if AtomMp4.AtomType = 'uuid' then
        AStream.Read(AtomMp4^.AtomTypeExtended, 16);

      ReadContainer(AStream, AtomMp4);

      AtomList.Add(AtomMp4);

      //Если это контейнер, читаем его содержимое
      if IsContainer(AtomMp4) then begin
        ReadTree(AStream, AtomList, AtomMp4.Position + AtomMp4.SizeExtended, AtomMp4);
      end;

      AStream.Position:= AtomMp4.Position + AtomMp4.SizeExtended;
    until (AtomMp4.Size = 0) or (AStream.Position >= AEndPos);
end;

function ReadPath(AStream: PStream; AtomList: PList; APath: array of TAtomType; AParent: PAtomMp4; var Atom: PAtomMp4; var ASize: Integer): Pointer;
var i, j, n, H: Integer;
begin
    Result:= nil; ASize:= 0; Atom:= nil; H:= High(APath); n:= 0;
    for i:= Low(APath) to H do
      for j:= n to AtomList.Count-1 do
        if (PAtomMp4(AtomList.Items[j]).AtomType = APath[i]) and (PAtomMp4(AtomList.Items[j]).Parent = AParent) then begin
          if i = H then Atom:= PAtomMp4(AtomList.Items[j])
          else AParent:= PAtomMp4(AtomList.Items[j]);
          n:= j+1;
          Break;
        end;

    if (Atom <> nil) then begin
      ASize:= Atom.SizeExtended - 8;
      Result:= AllocMem(ASize);
      AStream.Position:= Atom.Position + 8;
      AStream.Read(Result^, ASize);
    end;
end;

end.
