FLAC tags reader

A simple reader for tags in FLAC sound files. The whole script will read the tags and produce a text file and the embedded images, as files, next to the original file, as an example usage.

The main code starts with readtags(afile) and will return a record with the found tags:

{comments:{}, picts:{}}

comments comes as a list of records: {tag: “a tag”, value: “the value”}

picts comes as a list of records: {type:pictype (textual form), mimetype:mimetype, description: the pict description, width:x, height:y, depth:z, indexedColors:n, thePict: as JPEG picture or GIF picture or PostScript picture or data}

Contrarily to most tag readers this script will return a list of values, if there is one, including the pictures, as defined in the specifications.

And now a word from our sponsors:

To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer’s heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer’s express Statement of Purpose.

The above text has a patent pending.


property blocktypes : {"STREAMINFO", "PADDING", "APPLICATION", "SEEKTABLE", "VORBIS_COMMENT", "CUESHEET", "PICTURE", "reserved", "invalid"}

property pictTypes : {"Other", "32x32 pixels 'file icon' (PNG only)", "Other file icon", "Cover (front)", "Cover (back)", "Leaflet page", "Media (e.g. label side of CD)", "Lead artist/lead performer/soloist", "Artist/performer", "Conductor", "Band/Orchestra", "Composer", "Lyricist/text writer", "Recording Location", "During recording", "During performance", "Movie/video screen capture", "A bright coloured fish", "Illustration", "Band/artist logotype", "Publisher/Studio logotype"}


property standardcomments : {"vendor_string", "TITLE", "VERSION", "ALBUM", "TRACKNUMBER", "ARTIST", "PERFORMER", "COPYRIGHT", "LICENSE", "ORGANIZATION", "DESCRIPTION", "GENRE", "DATE", "LOCATION", "CONTACT", "ISRC", "DISCNUMBER", "REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_TRACK_PEAK", "REPLAYGAIN_ALBUM_GAIN", "REPLAYGAIN_ALBUM_PEAK", "HASH", "ALBUMARTIST", "COMPILATION"}


property tags : {comments:{}, picts:{}}

on open of theFiles
	--	Executed when files are dropped on the script
	
	repeat with f in theFiles
		if readTags(f) is not missing value then
			writeTags(f)
		end if
	end repeat
end open


-----------------------
on readTags(fi)
	set f to open for access fi
	
	try
		set tags to {comments:{}, picts:{}}
		if (read f for 4) = "fLaC" then
			set lastblock to false
			
			repeat while not lastblock
				
				set blok to readBlockHead(f)
				set lastblock to lastblock of blok
				
				if bloktype of blok = 127 then
					error "invalid bloc in file " & fi as text
				end if
				set blocksize to bloklen of blok
				set blocktype to bloktype of blok
				
				
				if blocktype = "STREAMINFO" then
					read f for blocksize
				else if blocktype = "PADDING" then
					read f for blocksize
				else if blocktype = "APPLICATION" then
					read f for blocksize
				else if blocktype = "SEEKTABLE" then
					read f for blocksize
				else if blocktype = "VORBIS_COMMENT" then
					copy parsecomm(f, blocksize) to comments of tags
				else if blocktype = "CUESHEET" then
					read f for blocksize
				else if blocktype = "PICTURE" then
					copy readPic(f) to end of picts of tags
				else if blocktype = "reserved" then
					read f for blocksize
				end if
				
			end repeat
		else
			close access f
			return missing value
		end if
	end try
	close access f
	return tags
end readTags



on read32bit(f)
	-- When reading binary data, read always uses big-endian byte order.
	set n to (read f for 4 as unsigned integer)
	--swap the whole shebang!
	set r to ((n mod 256) as integer) * 16777216 --256 * 256 * 256
	set n to n div 256
	set r to r + (n mod 256) * 65536 --256 * 256
	set n to n div 256
	set r to r + (n mod 256) * 256
	set n to n div 256
	set r to r + (n mod 256)
end read32bit

on readBlockHead(f)
	set blocktypes to {"STREAMINFO", "PADDING", "APPLICATION", "SEEKTABLE", "VORBIS_COMMENT", "CUESHEET", "PICTURE"}
	
	
	set n to read f for 4 as unsigned integer
	
	set btype to n div (16777216) --256 * 256 * 256
	set blen to (n mod (16777216)) as integer
	set lastblock to (btype > 127)
	set btype to btype mod 128
	
	if btype < 127 then
		if btype > 6 then
			set btype to "reserved"
		else
			set btype to item (btype + 1) of blocktypes
		end if
		
	end if
	
	return {bloktype:btype, bloklen:blen, lastblock:lastblock}
end readBlockHead

on readPic(f)
	set pictTypes to {"Other", "32x32 pixels 'file icon' (PNG only)", "Other file icon", "Cover (front)", "Cover (back)", "Leaflet page", "Media (e.g. label side of CD)", "Lead artist/lead performer/soloist", "Artist/performer", "Conductor", "Band/Orchestra", "Composer", "Lyricist/text writer", "Recording Location", "During recording", "During performance", "Movie/video screen capture", "A bright coloured fish", "Illustration", "Band/artist logotype", "Publisher/Studio logotype"}
	
	set pictype to read f for 4 as unsigned integer
	if pictype < 21 then
		set pictype to item (pictype + 1) of pictTypes
	end if
	
	set mimetype to read f for 4 as unsigned integer
	if mimetype > 0 then
		-- even if mandatory
		set mimetype to read f for mimetype
	else
		set mimetype to ""
	end if
	set picdescr to read f for 4 as unsigned integer
	if picdescr > 0 then
		set picdescr to read f for picdescr
	else
		set picdescr to ""
	end if
	set x to read f for 4 as unsigned integer
	set y to read f for 4 as unsigned integer
	set z to read f for 4 as unsigned integer
	set n to read f for 4 as unsigned integer
	
	set pic to read f for 4 as unsigned integer
	if mimetype = "image/jpeg" then
		set pic to read f for pic as JPEG picture
	else if mimetype = "image/gif" then
		set pic to read f for pic as GIF picture
	else if mimetype = "application/pdf" then
		set pic to read f for pic as PostScript picture
	else
		set pic to read f for pic as data
		
	end if
	--read f for blocksize as picture
	--read f for blocksize as GIF picture
	return {type:pictype, mimetype:mimetype, description:picdescr, width:x, height:y, depth:z, indexedColors:n, thePict:pic}
end readPic

on parsecomm(f, n)
	
	
	set r to {}
	
	set comments to {}
	(*
	
	1) [vendor_length] = read an unsigned integer of 32 bits
  	2) [vendor_string] = read a UTF-8 vector as [vendor_length] octets
  	3) [user_comment_list_length] = read an unsigned integer of 32 bits
  	4) iterate [user_comment_list_length] times {

    	5) [length] = read an unsigned integer of 32 bits
	6) this iteration's user comment = read a UTF-8 vector as [length] octets
 	*)
	
	
	try
		set vendor_length to read32bit(f) -- little endian
		set comments to {{tag:"vendor_string", value:read f for vendor_length as «class utf8»}}
		
		set list_length to read32bit(f)
		repeat list_length times
			set l to read32bit(f)
			set acomm to read f for l as «class utf8»
			
			set i to offset of "=" in acomm
			if i = 0 then
				set tag to acomm
				set value to "ERROR"
			else if i = length of acomm then
				set value to ""
				set tag to text 1 thru -2 of acomm
			else
				set tag to text 1 thru (i - 1) of acomm
				set value to text (i + 1) thru -1 of acomm
			end if
			set ac to {tag:tag, value:value}
			set found to false
			repeat with i in comments
				if {tag:tag} is in i then
					set found to true
					copy {value} & value of i to value of i
					exit repeat
				end if
			end repeat
			if not found then
				copy ac to end of comments
			end if
		end repeat
		
	on error e
		display alert e
	end try
	return comments
end parsecomm



-------------


on writeTags(fi)
	set {astid, AppleScript's text item delimiters} to {AppleScript's text item delimiters, {"	"}} -- \tab
	
	set pth to (POSIX path of fi) & ".txt"
	set f to open for access (pth as POSIX file) with write permission
	set eof f to 0
	
	repeat with c in comments of tags
		-- tag \tab value(s)
		write tag of c & "	" & value of c & "
" to f
	end repeat
	set AppleScript's text item delimiters to astid
	
	set i to 1
	repeat with p in picts of tags
		write type of p & "	" & mimetype of p & "	" & POSIX path of fi & "." & i & ".jpg
," to f
		
		set g to open for access POSIX file (POSIX path of fi & "." & i & ".jpg") with write permission
		set eof g to 0
		write thePict of p to g
		close access g
		
		set i to i + 1
	end repeat
	close access f
	
end writeTags