1 ! Copyright (C) 2009 Tim Wawrzynczak, Doug Coleman.
2 ! See http://factorcode.org/license.txt for BSD license.
3 USING: sequences io io.encodings.binary io.files io.pathnames
4 strings kernel math io.mmap io.mmap.uchar accessors syntax
5 combinators math.ranges unicode.categories byte-arrays
6 io.encodings.string io.encodings.utf16 assocs math.parser
7 combinators.short-circuit fry namespaces combinators.smart
8 splitting io.encodings.ascii arrays io.files.info unicode.case
9 io.directories.search literals math.functions ;
16 "Blues" "Classic Rock" "Country" "Dance" "Disco" "Funk"
17 "Grunge" "Hip-Hop" "Jazz" "Metal" "New Age" "Oldies" "Other"
18 "Pop" "R&B" "Rap" "Reggae" "Rock" "Techno" "Industrial"
19 "Alternative" "Ska" "Death Metal" "Pranks" "Soundtrack"
20 "Euro-Techno" "Ambient" "Trip-Hop" "Vocal" "Jazz+Funk"
21 "Fusion" "Trance" "Classical" "Instrumental" "Acid" "House"
22 "Game" "Sound Clip" "Gospel" "Noise" "AlternRock" "Bass"
23 "Soul" "Punk" "Space" "Meditative" "Instrumental Pop"
24 "Instrumental Rock" "Ethnic" "Gothic" "Darkwave"
25 "Techno-Industrial" "Electronic" "Pop-Folk" "Eurodance"
26 "Dream" "Southern Rock" "Comedy" "Cult" "Gangsta" "Top 40"
27 "Christian Rap" "Pop/Funk" "Jungle" "Native American"
28 "Cabaret" "New Wave" "Psychedelic" "Rave" "Showtunes"
29 "Trailer" "Lo-Fi" "Tribal" "Acid Punk" "Acid Jazz" "Polka"
30 "Retro" "Musical" "Rock & Roll" "Hard Rock" "Folk"
31 "Folk-Rock" "National Folk" "Swing" "Fast Fusion" "Bebop"
32 "Latin" "Revival" "Celtic" "Bluegrass" "Avantgarde"
33 "Gothic Rock" "Progressive Rock" "Psychedelic Rock"
34 "Symphonic Rock" "Slow Rock" "Big Band" "Chorus"
35 "Easy Listening" "Acoustic" "Humour" "Speech" "Chanson"
36 "Opera" "Chamber Music" "Sonata" "Symphony" "Booty Bass"
37 "Primus" "Porn Groove" "Satire" "Slow Jam" "Club" "Tango"
38 "Samba" "Folklore" "Ballad" "Power Ballad" "Rhythmic Soul"
39 "Freestyle" "Duet" "Punk Rock" "Drum Solo" "A capella"
40 "Euro-House" "Dance Hall" "Goa" "Drum & Bass" "Club-House"
41 "Hardcore" "Terror" "Indie" "BritPop" "Negerpunk"
42 "Polsk Punk" "Beat" "Christian Gangsta Rap" "Heavy Metal"
43 "Black Metal" "Crossover" "Contemporary Christian"
47 TUPLE: header version flags size ;
49 TUPLE: frame tag flags size data ;
51 TUPLE: id3 header frames
52 title artist album year comment genre
53 speed genre-name start-time end-time ;
57 H{ } clone >>frames ; inline
59 : <header> ( -- object ) header new ; inline
61 : <frame> ( -- object ) frame new ; inline
63 : id3v2? ( seq -- ? ) "ID3" head? ; inline
65 CONSTANT: id3v1-length 128
66 CONSTANT: id3v1-offset 128
67 CONSTANT: id3v1+-length 227
68 CONSTANT: id3v1+-offset $[ 128 227 + ]
72 [ length id3v1-offset >= ]
73 [ id3v1-length tail-slice* "TAG" head? ]
76 : id3v1+? ( seq -- ? )
78 [ length id3v1+-offset >= ]
79 [ id3v1+-length tail-slice* "TAG+" head? ]
82 : pair>frame ( string key -- frame/f )
91 : id3v1>frames ( id3v1 -- seq )
94 [ title>> "TIT2" pair>frame ]
95 [ artist>> "TPE1" pair>frame ]
96 [ album>> "TALB" pair>frame ]
97 [ year>> "TYER" pair>frame ]
98 [ comment>> "COMM" pair>frame ]
99 [ genre>> "TCON" pair>frame ]
101 ] output>array sift ;
103 : seq>synchsafe ( seq -- n )
104 0 [ [ 7 shift ] dip bitor ] reduce ; inline
106 : synchsafe>seq ( n -- seq )
107 dup 1+ log2 1+ 7 / ceiling
108 [ [ -7 shift ] keep HEX: 7f bitand ] replicate nip reverse ; inline
110 : filter-text-data ( data -- filtered )
111 [ printable? ] filter ; inline
113 : valid-tag? ( id -- ? )
114 [ { [ digit? ] [ LETTER? ] } 1|| ] all? ; inline
116 : read-frame-data ( frame seq -- frame data )
117 [ 10 over size>> 10 + ] dip <slice> filter-text-data ; inline
119 : decode-text ( string -- string' )
121 { { HEX: ff HEX: fe } { HEX: fe HEX: ff } } member?
122 utf16 ascii ? decode ; inline
124 : (read-frame) ( seq -- frame )
127 [ 4 head-slice decode-text >>tag ]
128 [ [ 4 8 ] dip subseq seq>synchsafe >>size ]
129 [ [ 8 10 ] dip subseq >byte-array >>flags ]
130 [ read-frame-data decode-text >>data ]
133 : read-frame ( seq -- frame/f )
134 dup 4 head-slice valid-tag?
135 [ (read-frame) ] [ drop f ] if ; inline
137 : remove-frame ( seq frame -- seq )
138 size>> 10 + tail-slice ; inline
140 : frames>assoc ( seq -- assoc )
141 [ [ tag>> ] keep ] H{ } map>assoc ; inline
143 : read-frames ( seq -- assoc )
144 [ dup read-frame dup ] [ [ remove-frame ] keep ] produce 2nip ; inline
146 : read-v2-header ( seq -- header )
149 [ [ 3 5 ] dip <slice> >array >>version ]
150 [ [ 5 ] dip nth >>flags ]
151 [ [ 6 10 ] dip <slice> seq>synchsafe >>size ]
154 : merge-frames ( id3 assoc -- id3 )
155 [ dup frames>> ] dip update ; inline
157 : merge-id3v1 ( id3 -- id3 )
158 dup id3v1>frames frames>assoc merge-frames ; inline
160 : read-v2-tags ( id3 seq -- id3 )
162 [ read-v2-header >>header ]
163 [ read-frames frames>assoc merge-frames ] bi* ; inline
165 : extract-v1-tags ( id3 seq -- id3 )
167 [ 30 head-slice decode-text filter-text-data >>title ]
168 [ [ 30 60 ] dip subseq decode-text filter-text-data >>artist ]
169 [ [ 60 90 ] dip subseq decode-text filter-text-data >>album ]
170 [ [ 90 94 ] dip subseq decode-text filter-text-data >>year ]
171 [ [ 94 124 ] dip subseq decode-text filter-text-data >>comment ]
172 [ [ 124 ] dip nth number>string >>genre ]
175 : read-v1-tags ( id3 seq -- id3 )
176 id3v1-offset tail-slice* 3 tail-slice
177 extract-v1-tags ; inline
179 : extract-v1+-tags ( id3 seq -- id3 )
181 [ 60 head-slice decode-text filter-text-data [ append ] change-title ]
183 [ 60 120 ] dip subseq decode-text filter-text-data
184 [ append ] change-artist
187 [ 120 180 ] dip subseq decode-text filter-text-data
188 [ append ] change-album
190 [ [ 180 ] dip nth >>speed ]
191 [ [ 181 211 ] dip subseq decode-text >>genre-name ]
192 [ [ 211 217 ] dip subseq decode-text >>start-time ]
193 [ [ 217 223 ] dip subseq decode-text >>end-time ]
196 : read-v1+-tags ( id3 seq -- id3 )
197 id3v1+-offset tail-slice* 4 tail-slice
198 extract-v1+-tags ; inline
200 : parse-genre ( string -- n/f )
201 dup "(" ?head-slice drop ")" ?tail-slice drop
202 string>number dup number? [
208 : (mp3>id3) ( path -- id3v2/f )
212 [ dup id3v1? [ read-v1-tags merge-id3v1 ] [ drop ] if ]
213 [ dup id3v1+? [ read-v1+-tags merge-id3v1 ] [ drop ] if ]
214 [ dup id3v2? [ read-v2-tags ] [ drop ] if ]
216 ] with-mapped-uchar-file ;
220 : mp3>id3 ( path -- id3/f )
221 dup file-info size>> 0 <= [ drop f ] [ (mp3>id3) ] if ; inline
223 : find-id3-frame ( id3 name -- obj/f )
224 swap frames>> at* [ data>> ] when ; inline
226 : title ( id3 -- string/f ) "TIT2" find-id3-frame ; inline
228 : artist ( id3 -- string/f ) "TPE1" find-id3-frame ; inline
230 : album ( id3 -- string/f ) "TALB" find-id3-frame ; inline
232 : year ( id3 -- string/f ) "TYER" find-id3-frame ; inline
234 : comment ( id3 -- string/f ) "COMM" find-id3-frame ; inline
236 : genre ( id3 -- string/f )
237 "TCON" find-id3-frame parse-genre ; inline
239 : find-mp3s ( path -- seq )
240 [ >lower ".mp3" tail? ] find-all-files ; inline
242 : mp3-paths>id3s ( seq -- seq' )
243 [ dup mp3>id3 ] { } map>assoc ; inline
245 : parse-mp3-directory ( path -- seq )
246 find-mp3s mp3-paths>id3s ;