Correctly writes out the db.
[amarok_sonynw.git] / walkgirl / Walkman.rb
blobd352dc61955d1cea0079e382d506ca4b0c013b58
1 ############################################################################
2 #    Copyright (C) 2008 by RafaƂ Rzepecki   #
3 #    divided.mind@gmail.com   #
4 #                                                                          #
5 #    This program is free software; you can redistribute it and#or modify  #
6 #    it under the terms of the GNU General Public License as published by  #
7 #    the Free Software Foundation; either version 2 of the License, or     #
8 #    (at your option) any later version.                                   #
9 #                                                                          #
10 #    This program is distributed in the hope that it will be useful,       #
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of        #
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         #
13 #    GNU General Public License for more details.                          #
14 #                                                                          #
15 #    You should have received a copy of the GNU General Public License     #
16 #    along with this program; if not, write to the                         #
17 #    Free Software Foundation, Inc.,                                       #
18 #    59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             #
19 ############################################################################
21 require 'iconv'
22 require 'id3'
24 module ID3
25   TAG_LEN = 128
26   class Frame
27     def dump_short
28       ["TIT2", 0, 0, data].pack("a4cca#{TAG_LEN-6}")
29     end
30   end
31 end
33 module Walkman
34   module Db
35     COLLECTION_IDS = { "ARTIST" => 2, "ALBUM" => 3, "CONTENTTYPE" => 1 }
36     def Db.new(*a)
37       Db.new(*a)
38     end
40     class Collection < Hash
41       attr_accessor :other
42       def initialize(key)
43         @other_id = ID3::Frame.new(key, "2.4.0", "\0Unknown").recode(2).dump_short
44         @other = []
45         @key = key
46       end
47       def ginf_file
48         file = File.new
49         file.magic = 'GPIF'
50         @reverse_keys = {}
51         idx = 1
52         object = Object.new
53         object.magic = 'GPFB'
54         object.rec_size = 16 + 128
55         (keys + [@other_id]).each do |frame|
56           record = [0, 0, 0x2eeeb, 1, 128].pack("N3n2")
57           record += frame
58           object.records << record
59           @reverse_keys[frame] = idx
60           idx += 1
61         end
62         file.objects << object
63         file
64       end
65       def tree_file
66         file = File.new
67         file.magic = 'TREE'
68         gplb = Object.new
69         gplb.magic = 'GPLB'
70         gplb.rec_size = 8
71         gplb.obj_size = 0x4010
72         file.objects << gplb
73         tplb = Object.new
74         tplb.magic = 'TPLB'
75         tplb.rec_size = 2
76         file.objects << tplb
77         index = 1
78         (keys.sort + [@other_id]).each do |key|
79           gplb.records << [@reverse_keys[key], 0x0100, index, 0x0100].pack("n4")
80           (self[key] || @other).each do |song|
81             tplb.records << [song.slot].pack("n")
82             index += 1
83           end
84         end
85         file
86       end
87       def files
88         ginf = ginf_file
89         tree = tree_file
90         { sprintf("01tree%02x.dat", COLLECTION_IDS[@key]) => tree_file,
91         sprintf("03ginf%02x.dat", COLLECTION_IDS[@key]) => ginf_file}
92       end
93     end
94     
95     class Db < Array
96       attr_reader :cnt
97       attr_accessor :collections
98       IMPORTANT_TAGS = [ 'ARTIST', 'TITLE', 'ALBUM', 'CONTENTTYPE' ]
99       def initialize
100         @cnt = Walkman::File.new
101         @cnt.magic = "CNIF"
102         @songs = Walkman::Object.new
103         @songs.rec_size = 656
104         @songs.magic = "CNFB"
105         @cnt.objects << @songs
106         @collections = {}
107         COLLECTION_IDS.keys.each do |key|
108           @collections[key] = Collection.new(key)
109         end
110       end
111       def <<(oma)
112         tag = ID3::Tag2.new
114         # choose right tags
115         left = 5
116         oma.tags.each do |k, v|
117           if IMPORTANT_TAGS.include? k
118             tag[k] = v
119             left -= 1
120           end
121         end
122         oma.tags.each do |k, v|
123           if left > 0 and !IMPORTANT_TAGS.include? k
124             tag[k] = v
125             left -= 1
126           end
127         end
128         
129         record = Walkman::Record::Cnfb.new
130         record.track_type = Walkman::Record::Cnfb::OBFUSCATED_MP3
131         record.track_length = oma.length
132         record.tags = tag
133         record.frame_count = oma.frames
134         @songs.records << record
135         
136         COLLECTION_IDS.keys.each do |key|
137           value = oma.tags[key]
138           if value
139             (@collections[key][value.dump_short] ||= []) << oma
140           else
141             @collections[key].other << oma
142           end
143         end
144         super
145       end
146     end
147   end
149   class Oma
150     MPEG_VERSION = [:v2_5, :reserved, :v2, :v1]
151     LAYER = [:reserved, :layer3, :layer2, :layer1]
152     BITRATE = [
153         # v1l1 v1l2 v1l3 v2l1 v2l*
154 [        0, 0, 0, 0, 0],
155 [        32, 32, 32, 32, 8],
156 [        64, 48, 40, 48, 16],
157 [        96, 56, 48, 56, 24],
158 [        128, 64, 56, 64, 32],
159 [        160, 80, 64, 80, 40],
160 [        192, 96, 80, 96, 48],
161 [        224, 112, 96, 112, 56],
162 [        256, 128, 112, 128, 64],
163 [        288, 160, 128, 144, 80],
164 [        320, 192, 160, 160, 96],
165 [        352, 224, 192, 176, 112],
166 [        384, 256, 224, 192, 128],
167 [        416, 320, 256, 224, 144],
168 [        448, 384, 320, 256, 160],
169 [        -1, -1, -1, -1, -1]].transpose
171     SAMPLING_RATE = { :v1 => [44100, 48000, 32000, -1],
172     :v2 => [22050, 24000, 16000, -1],
173     :v2_5 => [11025, 12000, 8000, -1] }
174     SAMPLE_PER_FRAME = { :v1 => [0, 1152, 1152, 384],
175     :v2 => [0, 576, 1152, 384],
176     :v2_5 => [0, 576, 1152, 384]}
178     DvId = 0x08F63DCA
179     CRC_POLY = 0x8005
180     
181     def bitrates
182       if version == :v1
183         case layer
184         when :layer1:
185           BITRATE[0]
186         when :layer2
187           BITRATE[1]
188         when :layer3
189           BITRATE[2]
190         end
191       elsif layer == :layer1
192         BITRATE[3]
193       else
194         BITRATE[4]
195       end
196     end
198     attr_reader :version, :layer, :tags, :bitrate, :length, :frames, :slot
199     def initialize(slot = nil, dvid = nil)
200       @slot = slot
201       @dvid = dvid
202     end
203     def read(filename)
204       f = ::File.new(filename)
205       tag_data = f.read(3072)
207       f.seek(0xc24)
208       @length, @frames = f.read(8).unpack("NN")
209       f.close
210       tag_data[0..2] = "ID3"
211       @tags = ID3::Tag2.new
212       @tags.raw = tag_data
213       @tags.parse!
214       self
215     end
216     def fromMp3(filename)
217       @tags = ID3::AudioFile.new(filename)
218       @filename = ::File.basename(filename)
219       @file = ::File.new(filename)
220       @file.seek(@tags.audioStartX)
222       # find first frame
223       loop do
224         loop do
225           if @file.readchar == 0xFF
226             @tags.audioStartX = @file.pos
227             if (byte = @file.readchar) & 0xE0 == 0xE0
228               @version = MPEG_VERSION[(byte & 0x18) >> 3]
229               @layer = LAYER[(byte & 0x06) >> 1]
230               @has_crc = (byte & 1) == 0
231               @encoding = (byte & 0x1e) << 3
232               break
233             end
234           end
235         end
237         byte = @file.readchar
238         @bitrate = bitrates[(byte & 0xF0) >> 4]
239         if @bitrate == 0
240           next
241         end
242         @sampling_rate = SAMPLING_RATE[version][(byte & 0x0C) >> 2]
243         @is_padded = (byte & 2) == 2
244         @length = (tags.audioEndX - tags.audioStartX) * 8 / bitrate
245         @sample_per_frame = SAMPLE_PER_FRAME[version][LAYER.index(layer)]
246         @frames = @length * @sampling_rate / 1000 / @sample_per_frame
247         @framelen = bitrate * 1000 * @sample_per_frame / 8 / @sampling_rate 
248         @encoding |= (byte & 0xF0) >> 4
249         if @has_crc
250           @crc = @file.read(2).unpack("n")
251         end
252         if @sampling_rate > 0
253           break
254         end
255       end
256       self
257     end
259     def write(outfile)
260 #      outfile << ["ea3", 3, 0x1776].pack("a3c@8n@3072")
261       tag = ID3::Tag2.new
262       tags = @tags.tagID3v2 || @tags.tagID3v1
263       if tags
264         tags.each { |key, value|
265         begin
266           tag[key] = value
267         rescue
268         end
269         }
270       end
271       tag["TITLE"].recode(2) if tag["TITLE"]
272       tag["ALBUM"].recode(2) if tag["ALBUM"]
273       tag["ARTIST"].recode(2) if tag["ARTIST"]
274 #      tag["CONTENTTYPE"] = ""
275 #      tag["CONTENTTYPE"].recode(2) if tag["CONTENTTYPE"]
276       tag["ORIGFILENAME"] = "\0" + @filename
277 #      p tag
278       tag = tag.dump
279       tag[0...10] = ["ea3", 3, 0x1776].pack("a3c@8n")
280       outfile << [tag].pack("a3072")
281 #      tag = ID3::Tag2.new.dump
282 #      tag[0..2] = "ea3"
283 #      outfile << tag
284       outfile << "EA3"
285       outfile << [2, 0, 0x60, 0xff, 0xfe, 1, 0x0f, 0x50, 0x00].pack("c5@9c4")
286       outfile << [0, 4, 0, 0, 0, 1, 2, 3, 0xc8, 0xd8, 0x36, 0xd8, 0x11, 0x22, 0x33, 0x44].pack("c*")
287       outfile << [0x03, 0x80, @encoding, 10, @length, @frames, 0].pack("c4N3")
288       outfile << [0,0,0,0].pack("N4")
289       outfile << [0,0,0,0].pack("N4")
290       outfile << [0,0,0,0].pack("N4")
291       scramble(outfile)
292     end
293     def scramble(outfile)
294       key = 0xFFFFFFFF & (( 0x2465 + @slot * 0x5296E435 ) ^ @dvid);
295       @file.seek(tags.audioStartX)
296 #      printf "Please remember to put the file at\nOMGAUDIO\\10F%02x\\1%07x.OMA in the device root.\n\nScrambling...", track >> 8, track & 0xFF
297       left = tags.audioEndX - tags.audioStartX
298       while left > 0 and block = @file.read([8, left].min)
299         if block.length != 8
300           outfile << block
301         else
302           outfile << block.unpack("N2").map{|x|x^key}.pack("N2")
303         end
304         left -= block.length
305 #        print "\rBytes left: #{left}"
306       end
307     end
308   end
309   
310   module Id3
311     FRAME2SYMBOL = ID3::Framename2symbol["2.4.0"]
312     SYMBOL2FRAME = ID3::SUPPORTED_SYMBOLS["2.4.0"]
314     class Frame < Hash
315       def initialize(*a)
316         if a[0].is_a?(Hash)
317           self.merge! a[0]
318         end
319       end
320       def to_s(*a)
321         limit = ""
322         if a[0]
323           limit = a[0] - 6
324         end
325         [SYMBOL2FRAME[self[:tag]], self[:encoding], Id3.encode(self[:encoding], self[:content])].
326         pack "a4nZ#{limit}"
327       end
328     end
329     
330     def Id3.frame(tag, content, encoding)
331       tag = FRAME2SYMBOL[tag] || tag
332       content = content
333       Frame.new :tag => tag, :content => Id3.decode(encoding, content), :encoding => encoding
334     end
335     def Id3.decode(encoding, content)
336       case encoding
337       when 2: # UTF-16BE
338         Iconv.conv('utf8', 'utf16be', content)
339       else
340         raise "Unknown id3 encoding (#{encoding})"
341       end
342     end
343     def Id3.encode(encoding, content)
344       case encoding
345       when 2: # UTF-16BE
346         Iconv.conv('utf16be', 'utf8', content)
347       else
348         raise "Unknown id3 encoding (#{encoding})"
349       end
350     end
351   end
352   
353   module Record
354     class Cnfb
355       attr_accessor :track_length, :frame_count, :tags, :content
356       attr_reader :track_type
357       OBFUSCATED_MP3 = [ 0, 0, 0xFF, 0xFE ]
358       PLAIN_MP3 = [ 0, 0, 0xFF, 0xFF ]
359       TAG_LEN = 128
360       def initialize(*a)
361         if a.length == 1 and a[0].is_a?(String)
362           parse a[0]
363         end
364       end
365       def track_type=(new_type)
366         @track_type = new_type
367         @track_type.instance_eval do
368           def inspect
369             case self
370             when OBFUSCATED_MP3:
371               "OBFUSCATED_MP3"
372             when PLAIN_MP3:
373               "PLAIN_MP3"
374             else
375               super
376             end
377           end
378           def to_s
379             pack("C4")
380           end
381         end
382       end
383       def parse(contents)
384         track_type, @track_length, @frame_count, num_tags, tag_size, contents =
385         contents.unpack("a4NNnna*")
387         self.track_type = track_type.unpack("C4")
389         @tags = ID3::Tag2.new
390         (0...num_tags).each do
391           tag, contents = contents.unpack("a#{tag_size}a*")
392           tag, content = tag.unpack("a4A*")
393           symbol = ID3::Framename2symbol["2.4.0"][tag]
394           if symbol
395             @tags[symbol] = ID3::Frame.new(symbol, "2.4.0", content)
396           end
397         end
398       end
399       def to_s
400         [@track_type.to_s, @track_length, @frame_count, @tags.length, TAG_LEN].pack("a4NNnn@16") +
401         @tags.map do |t,v|
402           [ID3::SUPPORTED_SYMBOLS["2.4.0"][t], 0, v.data].pack("a4ca#{TAG_LEN-5}")
403         end.join
404       end
405       def length
406         16 + @tags.length * TAG_LEN
407       end
408     end
409   end
411   module Object
412     def Object.new(*a)
413       Object.new(*a)
414     end
415     class Object
416       attr_accessor :magic, :records, :rec_size, :obj_size
417       def initialize(*a)
418         @records = []
419         @magic = "CAFE"
420         if a.length == 2 and a[1].is_a?(String)
421           parse a[1]
422         end
423       end
424       def parse(contents)
425         @magic, rec_count, @rec_size, contents = contents.unpack("a4nnx8a*")
426         record_klass = begin
427           Record.const_get(magic.capitalize)
428         rescue NameError
429           String
430         end
431         (0...rec_count).each do
432           record, contents = contents.unpack("a#{rec_size}a*")
433           @records << record_klass.new(record)
434         end
435       end
436       def to_s
437         res = [@magic, @records.length, rec_size, @records.length].pack("a4nnN@16") +
438           @records.map{|r|[r.to_s].pack("Z#{rec_size}")}.join
439         if obj_size
440           [res].pack("a#{obj_size}")
441         else
442           res
443         end
444       end
445     end
446   end
449   class ObjectPointer
450     attr_reader :magic, :range
451     def initialize(*a)
452       if a.length == 1 and a[0].is_a?(String)
453         parse a[0]
454       elsif a.length == 3
455         @magic, @range = a[0], (a[1]...(a[1]+a[2]))
456       end
457     end
458     def parse(contents)
459       @magic, offset, length = contents.unpack("a4NN")
460       @range = offset...(offset+length)
461     end
462     def to_s
463       [magic, range.first, range.last - range.first].pack("a4NN@16")
464     end
465   end
467   class File
468     attr_accessor :magic, :objects
469     def initialize(*a)
470       @objects = []
471       if a.length == 1 and a[0].is_a?(String)
472         parse a[0]
473       end
474     end
475     def parse(contents)
476       @magic, numobjects = contents.unpack("a4x4C")
477       objects = []
478       (1..numobjects).each do |objindex|
479         start = objindex * 16
480         fin = objindex * 16 + 16
481         objects << ObjectPointer.new(contents[start...fin])
482       end
483       objects.each do |obj_pointer|
484         @objects << Object.new(obj_pointer.magic, contents[obj_pointer.range])
485       end
486     end
487     def to_s
488       header = [@magic, 1, 1, 0, 0, objects.length].pack("a4C4C@16")
489       pointers = ""
490       objects = ""
491       offset = 16 + @objects.length * 16
492       @objects.each do |object|
493         object_raw = object.to_s
494         pointers += ObjectPointer.new(object.magic, offset, object_raw.length).to_s
495         offset += object_raw.length
496         objects += object_raw
497       end
498       header + pointers + objects
499     end
500   end