{OGG, SPX and FLAC file formats} unit fmt_OGG; {$include compilers.inc} interface uses wat_api, m_api; function ReadOGG(var Info:tSongInfo):boolean; cdecl; function ReadSPX(var Info:tSongInfo):boolean; cdecl; function ReadfLaC(var Info:tSongInfo):boolean; cdecl; implementation uses windows,common,io,tags,srv_format,utils; const OGGSign = $5367674F; //OggS const SpeexID = 'Speex '; type tSPEXHeader = packed record speex_string :array [0..7] of AnsiChar; speex_version :array [0..19] of AnsiChar; speex_version_id:dword; header_size :dword; //sizeof(tSPEXHeader) rate :dword; mode :dword; bitstrm_version :dword; nb_channels :dword; bitrate :dword; frame_size :dword; vbr :dword; fpp :dword; //frames_per_packet extra_headers :dword; reserved1 :dword; reserved2 :dword; end; type pOGGHdr = ^tOGGHdr; tOGGHdr = packed record ID :dword; Version :byte; HdrType :byte; Granule :Int64; // absolute position BitStrmSN:dword; PageSeqN :dword; CRC :dword; PageSegs :byte; end; const strmOGG = 1; strmOGM = 2; const VideoD = $65646976; VideoW = $006F; VorbisD = $62726F76; VorbisW = $7369; type tOGMInfo = packed record padding :word; // 0 codec :dword; size :dword; time_unit :int64; // 1/10000000 sec samples_per_unit:int64; // fps = 10000000*spu/time_unit default_len :dword; // 1 buffersize :dword; bit_per_sample :dword; width :dword; height :dword; dummy :dword; // 0 end; //const VorbisStream:array [0..5] of byte = ($76,$6F,$72,$62,$69,$73); // 'vorbis' type tOGGInfo = packed record version :dword; Channels :byte; samplerate:dword; maxkbps :dword; nominal :dword; minkbps :dword; BlockSizes:byte; dummy :byte; end; //--------------- fLaC section --------------- const fLaCSign = $43614C66; //fLaC { 0 : STREAMINFO 1 : PADDING 2 : APPLICATION 3 : SEEKTABLE 4 : VORBIS_COMMENT 5 : CUESHEET } type MetaHdr = packed record blocktype:byte; blocklen:array [0..2] of byte; end; type StreamInfo = packed record MinBlockSize:word; MaxBlocksize:word; MinFrameSize:array [0..2] of byte; MaxFrameSize:array [0..2] of byte; heap:array [0..7] of byte; MD5:array [0..15] of byte; end; procedure OGGGetComment(ptr:PAnsiChar;size:integer;var Info:tSongInfo); var alen,len,values:dword; clen:int; ls:PAnsiChar; value:PAnsiChar; cover:pByte; ext:dword; extw:int64; c:AnsiChar; begin inc(ptr,pdword(ptr)^+4); //vendor values:=pdword(ptr)^; inc(ptr,4); ext:=0; cover:=nil; clen:=0; while values>0 do begin len:=pdword(ptr)^; if len>cardinal(size) then break; dec(size,len); inc(ptr,4); ls:=ptr; c:=ls[len]; ls[len]:=#0; alen:=StrScan(ls,'=')-ls+1; if alen>0 then begin ls[alen-1]:=#0; value:=ls+alen; if (Info.title =nil) and (lstrcmpia(ls,'TITLE' )=0) then UTF8ToWide(value,Info.title) else if (Info.artist =nil) and (lstrcmpia(ls,'ARTIST' )=0) then UTF8ToWide(value,Info.artist) else if (Info.album =nil) and (lstrcmpia(ls,'ALBUM' )=0) then UTF8ToWide(value,Info.album) else if (Info.genre =nil) and (lstrcmpia(ls,'GENRE' )=0) then UTF8ToWide(value,Info.genre) else if (Info.year =nil) and (lstrcmpia(ls,'DATE' )=0) then UTF8ToWide(value,Info.year) else if (Info.comment=nil) and (lstrcmpia(ls,'COMMENT')=0) then UTF8ToWide(value,Info.comment) else if (Info.lyric =nil) and (lstrcmpia(ls,'LYRICS' )=0) then UTF8ToWide(value,Info.lyric) else if (Info.track=0) and (lstrcmpia(ls,'TRACKNUMBER')=0) then Info.track:=StrToInt(value) else if (cover=nil) and (lstrcmpia(ls,'COVERART')=0) then cover:=mir_base64_decode(value,clen) else if lstrcmpia(ls,'COVERARTMIME')=0 then ext:=GetImageType(nil,value); end; dec(values); inc(ptr,len); ptr^:=c; end; if cover<>nil then begin if ext=0 then ext:=GetImageType(cover); if ext<>0 then begin FastAnsiToWideBuf(PAnsiChar(@ext),pWideChar(@extw)); Info.cover:=SaveTemporaryW(cover,clen,PWideChar(@extw)); end; mFreeMem(cover); end; end; function CalcSize(num:integer;var arr:array of byte):integer; var i:integer; begin result:=0; for i:=0 to num-1 do begin inc(result,arr[i]); if arr[i]<$FF then break; end; end; function ReadSPX(var Info:tSongInfo):boolean; cdecl; var f:THANDLE; OGGHdr:tOGGHdr; SPXHdr:tSPEXHeader; buf:array [0..255] of byte; ptr:PAnsiChar; size:integer; begin result:=false; f:=Reset(Info.mfile); if f=THANDLE(INVALID_HANDLE_VALUE) then exit; BlockRead(f,OGGHdr,SizeOf(tOGGHdr)); Skip(f,OGGHdr.PageSegs); if OGGHdr.ID=OGGSign then begin BlockRead(f,SPXHdr,SizeOf(SPXHdr)); if SPXHdr.speex_string<>SpeexID then begin CloseHandle(f); exit; end; Info.khz:=SPXHdr.rate div 1000; Info.vbr:=SPXHdr.vbr; if integer(SPXHdr.bitrate)<>-1 then Info.kbps:=SPXHdr.bitrate div 1000; BlockRead(f,OGGHdr,SizeOf(tOGGHdr)); BlockRead(f,buf,OGGHdr.PageSegs); size:=CalcSize(OGGHdr.PageSegs,buf); GetMem(ptr,size+1); BlockRead(f,ptr^,size); OGGGetComment(ptr,size,Info); FreeMem(ptr); result:=true; end; CloseHandle(f); end; function Compare(const sign:array of byte):integer; type conv=packed record d:dword;w:word; end; var p:^conv; begin p:=@sign; if (p^.d=VideoD) and (p^.w=VideoW) then result:=strmOGM else if (p^.d=VorbisD) and (p^.w=VorbisW) then result:=strmOGG else result:=0; end; function ReadOGG(var Info:tSongInfo):boolean; cdecl; var f:THANDLE; OGGHdr:tOGGHdr; tmp:packed record paktype:byte; sign:array [0..5] of byte; end; OGGInfo:tOGGInfo; OGMInfo:tOGMInfo; fpos:dword; SPXHdr:tSPEXHeader; i,j:integer; DataIndex:integer; buf:array [0..255] of byte; fsize:dword; done:integer; ptr:PAnsiChar; size:integer; begin result:=false; f:=Reset(Info.mfile); if f=THANDLE(INVALID_HANDLE_VALUE) then exit; tmp.paktype:=0; fsize:=FileSize(f); done:=0; while (done<>3) and (tmp.paktype<>5) and (FilePos(f)0 then Info.kbps :=OGGInfo.nominal div 1000; Info.khz :=OGGInfo.samplerate; Info.channels:=OGGInfo.Channels; done:=done or 1; end; strmOGM: begin BlockRead(f,OGMInfo,SizeOf(OGMInfo)); Info.codec :=OGMInfo.codec; Info.fps :=round(((10000000*OGMInfo.samples_per_unit) / OGMInfo.time_unit)*100); Info.width :=OGMInfo.width; Info.height:=OGMInfo.height; done:=done or 1; end; end; end else if tmp.paktype=ORD('S') then //maybe SPX begin Seek(f,fpos); BlockRead(f,SPXHdr,SizeOf(SPXHdr)); if SPXHdr.speex_string<>SpeexID then begin CloseHandle(f); exit; end; Info.khz:=SPXHdr.rate div 1000; if integer(SPXHdr.bitrate)<>-1 then Info.kbps:=SPXHdr.bitrate div 1000; done:=done or 1; end else if tmp.paktype=3 then begin GetMem(ptr,size+1); BlockRead(f,ptr^,size); OGGGetComment(ptr,size,Info); FreeMem(ptr); done:=done or 2; end else continue; result:=true; end; end; end; // try to get length DataIndex:=FileSize(f)-10; for i:=1 to 50 do begin dec(DataIndex,SizeOf(buf)-10); Seek(f,DataIndex); BlockRead(f,buf,SizeOf(buf)); { Get number of PCM samples from last Ogg packet header } j:=SizeOf(buf)-10; repeat if pOGGHdr(@buf[j])^.ID=OGGSign then begin if j>(SizeOf(buf)-SizeOf(tOGGHdr)) then begin Seek(f,DataIndex+j); BlockRead(f,buf,SizeOf(tOGGHdr)); j:=0; end; if Info.fps>0 then begin Info.total:=(pOGGHdr(@buf[j])^.Granule*100) div Info.fps; end else if Info.khz<>0 then Info.total:=pOGGHdr(@buf[j])^.Granule div Info.khz; break; end; dec(j); until j=0; if Info.total>0 then break; end; Info.khz:=Info.khz div 1000; CloseHandle(f); end; function ReadfLaC(var Info:tSongInfo):boolean; cdecl; var f:THANDLE; data64:int64; hdr:MetaHdr; frm:StreamInfo; id:dword; flag:integer; size:dword; buf,ptr:PAnsiChar; begin result:=false; f:=Reset(Info.mfile); if f=THANDLE(INVALID_HANDLE_VALUE) then exit; ReadID3v2(f,Info); BlockRead(f,id,SizeOf(id)); if id=fLaCSign then begin flag:=0; repeat BlockRead(f,hdr,SizeOf(hdr)); size:=hdr.blocklen[2]+(hdr.blocklen[1] shl 8)+(hdr.blocklen[0] shl 16); case (hdr.blocktype and $7F) of 0: begin if flag=0 then begin BlockRead(f,frm,SizeOf(frm)); //samplerate eg.44100 Info.khz:=((frm.heap[0] shl 12)+(frm.heap[1] shl 4)+(frm.heap[2] shr 4)); Info.channels:=((frm.heap[2] and $F) shr 1)+1; //bits per SAMPLE now Info.kbps:=(frm.heap[2] and 1) shl 4+(frm.heap[3] shr 4)+1; data64:=((frm.heap[3] and $F) shl 32)+(frm.heap[4] shl 24)+ (frm.heap[5] shl 16)+(frm.heap[6] shl 8)+frm.heap[7]; if (data64<>0) and (Info.khz<>0) then Info.total:=data64 div Info.khz; Info.kbps:=Info.kbps*8; Info.kbps:=trunc(FileSize(f)*8/1000); Info.khz:=Info.khz div 1000; flag:=1; end; end; 4: begin GetMem(buf,size); BlockRead(f,buf^,size); OGGGetComment(buf,size,Info); FreeMem(buf); end; 6: begin if Info.cover=nil then begin GetMem(buf,size); BlockRead(f,buf^,size); ptr:=buf; id:=BSwap(pdword(ptr)^); case id of 0,3,4,6: begin inc(ptr,4); id:=BSwap(pdword(ptr)^); // mime size inc(ptr,4); flag:=GetImageType(nil,ptr); inc(ptr,id+4*5); // width, height, depth etc. id:=BSwap(pdword(ptr)^); // image size inc(ptr,4); if flag=0 then flag:=GetImageType(pByte(ptr)); FastAnsiToWideBuf(PAnsiChar(@flag),pWideChar(@data64)); Info.cover:=SaveTemporaryW(ptr,id,PWideChar(@data64)); end; end; FreeMem(buf); end else Skip(f,size); end else begin if (hdr.blocktype and $80)<>0 then break; Skip(f,size); end; end; until (hdr.blocktype and $80)<>0; end; ReadID3v1(f,Info); CloseHandle(f); result:=true; end; var LocalFormatLinkOGG, LocalFormatLinkOGA, LocalFormatLinkOGM, LocalFormatLinkSPX, LocalFormatLinkFLA, LocalFormatLinkFLAC:twFormat; procedure InitLink; begin LocalFormatLinkOGG.Next:=FormatLink; LocalFormatLinkOGG.This.proc :=@ReadOGG; LocalFormatLinkOGG.This.ext :='OGG'; LocalFormatLinkOGG.This.flags:=0; FormatLink:=@LocalFormatLinkOGG; LocalFormatLinkOGA.Next:=FormatLink; LocalFormatLinkOGA.This.proc :=@ReadOGG; LocalFormatLinkOGA.This.ext :='OGA'; LocalFormatLinkOGA.This.flags:=0; FormatLink:=@LocalFormatLinkOGA; LocalFormatLinkOGM.Next:=FormatLink; LocalFormatLinkOGM.This.proc :=@ReadOGG; LocalFormatLinkOGM.This.ext :='OGM'; LocalFormatLinkOGM.This.flags:=WAT_OPT_VIDEO; FormatLink:=@LocalFormatLinkOGM; LocalFormatLinkSPX.Next:=FormatLink; LocalFormatLinkSPX.This.proc :=@ReadSPX; LocalFormatLinkSPX.This.ext :='SPX'; LocalFormatLinkSPX.This.flags:=0; FormatLink:=@LocalFormatLinkSPX; LocalFormatLinkFLA.Next:=FormatLink; LocalFormatLinkFLA.This.proc :=@ReadfLaC; LocalFormatLinkFLA.This.ext :='FLA'; LocalFormatLinkFLA.This.flags:=0; FormatLink:=@LocalFormatLinkFLA; LocalFormatLinkFLAC.Next:=FormatLink; LocalFormatLinkFLAC.This.proc :=@ReadfLaC; LocalFormatLinkFLAC.This.ext :='FLAC'; LocalFormatLinkFLAC.This.flags:=0; FormatLink:=@LocalFormatLinkFLAC; end; initialization InitLink; end.