Make plasma libs build.
[amarok.git] / supplementary_scripts / mp3fix / mp3fix.rb
blobc4359b9c373911890cc91b32b572df617e21a127
1 #!/usr/bin/env ruby
3 # Script for fixing mp3 files which show a bogus track length. Calculates the real
4 # track length and adds the XING header to the file, and repairs broken MPEG headers.
6 # (c) 2005 Mark Kretschmann <markey@web.de>
7 # License: GNU General Public License V2
10 class NumArray < Array
11     #
12     # Return the nearest matching number from the array
13     #
14     def nearest_match( num )
15         smallest     = 0
16         smallestDiff = nil
18         each() do |x|
19             next if x == nil
20             diff = ( x - num ).abs()
22             if smallestDiff == nil or diff < smallestDiff
23                 smallestDiff = diff
24                 smallest = x
25             end
26         end
28         return smallest
29     end
30 end
34 # Returns the size of the entire ID3-V2 tag. In other words, the offset from
35 # where the real mp3 data starts.
36 # @see http://id3lib.sourceforge.net/id3/id3v2com-00.html#sec3.1
38 def calcId3v2Size( data )
39     size = data[6]*2**21 + data[7]*2**14 + data[8]*2**7 + data[9]
40     size = size + 10 # Header
42     return size
43 end
46 ############################################################################
47 # MAIN
48 ############################################################################
50 path = ""
51 destination = ""
52 repairLog = []
55 if $*.empty?() or $*[0] == "--help"
56     puts( "Usage: mp3fix.rb source [destination]" )
57     puts()
58     puts( "Mp3fix is a tool for fixing VBR encoded mp3 that show a bogus track length in your" )
59     puts( "audio player. Mp3fix calculates the real track length and adds the missing XING" )
60     puts( "header to the file. " )
61     puts()
62     puts( "Additionally, Mp3Fix can repair files with broken MPEG frame headers. This is useful " )
63     puts( "for tracks which show an invalid bitrate and samplerate, which also results in bogus " )
64     puts( "track length." )
65     exit( 1 )
66 end
68 path = $*[0]
69 destination = path
71 if $*.length() == 2
72     destination = $*[1]
73 end
76 unless FileTest::readable_real?( path )
77     puts( "Error: File not found or not readable." )
78     exit( 1 )
79 end
81 unless File.extname( path ) == ".mp3" or File.extname( path ) == ".MP3"
82     puts( "Error: File is not mp3." )
83     exit( 1 )
84 end
86 if destination == path and not FileTest.writable_real?( destination )
87     puts( "Error: Destination file not writable." )
88     exit( 1 )
89 end
93 file = File.new( path, "r" )
95 data = file.read()
96 id3length = 0
97 offset = 0
99 if data[0,3] == "ID3"
100     id3length = calcId3v2Size( data )
101     puts( "ID3-V2 detected. Tag size: #{id3length}" )
102 else
103     puts( "ID3-V1 detected." )
106 offset = id3length
108 SamplesPerFrame = 1152  # Constant for MPEG1 layer 3
109 BitrateTable = NumArray.new()
110 BitrateTable << nil << 32000 << 40000 << 48000 << 56000 << 64000 << 80000 << 96000
111 BitrateTable << 112000 << 128000 << 160000 << 192000 << 224000 << 256000 << 320000
112 SamplerateTable = NumArray.new()
113 SamplerateTable << 44100 << 48000 << 32100
115 ChannelMode = data[offset + 3] & 0xc0 >> 6
116 XingOffset = ChannelMode == 0x03 ? 17 : 32
119 puts()
120 puts( "Analyzing mpeg frames.." )
121 puts()
123 frameCount       = 0
124 bitrateCount     = 0
125 samplerateCount  = 0
126 firstFrameBroken = false
127 firstFrameOffset = nil
128 offset           = 0
129 header           = 0
131 # Iterate over all frames:
133 loop do
134     loop do
135         # Find frame sync
136         offset = data.index( 0xff, offset )
137         break if offset == nil or offset > data.length() - 4
138         header = data[offset+0]*2**24 + data[offset+1]*2**16 + data[offset+2]*2**8 + data[offset+3]
139         if header & 0xfff00000 == 0xfff00000
140             firstFrameOffset = offset if firstFrameOffset == nil
141             break
142         end
143         offset += 1
144     end
146     break if offset == nil
147     validHeader = true
149     bitrate = BitrateTable[( header & 0x0000f000 ) >> 12]
150     if bitrate == nil
151         validHeader = false
152         puts( "Bitrate invalid." )
153     end
155     samplerate = SamplerateTable[( header & 0x00000c00 ) >> 10]
156     if samplerate == nil
157         validHeader = false
158         puts( "Samplerate invalid." )
159     end
161     padding = ( header & 0x00000200 ) >> 9
163     if validHeader
164         frameSize = ( SamplesPerFrame / 8 * bitrate ) / samplerate + padding
166 #         puts( "bitrate     : #{bitrate.to_s()}" )
167 #         puts( "samplerate  : #{samplerate.to_s()}" )
168 #         puts( "padding     : #{padding.to_s()}" )
169 #         puts( "framesize   : #{frameSize}" )
170 #         puts()
172         frameCount      += 1
173         bitrateCount    += bitrate
174         samplerateCount += samplerate
175         offset          += frameSize
177     else
178         firstFrameBroken = true if frameCount == 0
179         offset += 1
180     end
184 AverageBitrate = bitrateCount / frameCount
185 Length = data.length() / AverageBitrate * 8
187 puts( "Number of frames : #{frameCount}" )
188 puts( "Average bitrate  : #{AverageBitrate}" )
189 puts( "Length (seconds) : #{Length}" )
190 puts()
193 # Repair first frame header, if it is broken:
195 if firstFrameBroken
196     puts()
197     puts( "Repairing broken header in first frame." )
198     repairLog << "* Fixed broken MPEG header\n"
200     firstHeader = data[firstFrameOffset+0]*2**24 + data[firstFrameOffset+1]*2**16 + data[firstFrameOffset+2]*2**8 + data[firstFrameOffset+3]
201     firstHeader |= 0xfff00000  # Frame sync
203     # MPEG ID, Layer, Protection
204     firstHeader &= 0xfff0ffff
205     firstHeader |= 0x000b0000
207     br = BitrateTable.nearest_match( ( bitrateCount / frameCount ).round() )
208     sr = SamplerateTable.nearest_match( ( samplerateCount / frameCount ).round() )
210     puts( "Setting bitrate    : #{br}" )
211     puts( "Setting samplerate : #{sr}" )
213     firstHeader &= 0xffff00ff
214     firstHeader |= BitrateTable.index( br )    << 12
215     firstHeader |= SamplerateTable.index( sr ) << 10
217     # Write header back
218     data[firstFrameOffset+0] = ( firstHeader >> 24 ) & 0xff
219     data[firstFrameOffset+1] = ( firstHeader >> 16 ) & 0xff
220     data[firstFrameOffset+2] = ( firstHeader >> 8  ) & 0xff
221     data[firstFrameOffset+3] = ( firstHeader >> 0  ) & 0xff
225 unless data[firstFrameOffset + 4 + XingOffset, 4] == "Xing"
226     puts()
227     puts( "Adding XING header." )
228     repairLog << "* Added XING header\n"
230     xing = String.new()
231     xing << "Xing"
233     Flags = 0x0001 | 0x0002 | 0x0004  # Frames and Bytes fields valid
234     xing << 0 << 0 << 0 << Flags
236     xing << ( ( frameCount & 0xff000000 ) >> 24 )
237     xing << ( ( frameCount & 0x00ff0000 ) >> 16 )
238     xing << ( ( frameCount & 0x0000ff00 ) >> 8 )
239     xing << ( ( frameCount & 0x000000ff ) >> 0 )
241     xing << ( ( data.length() & 0xff000000 ) >> 24 )
242     xing << ( ( data.length() & 0x00ff0000 ) >> 16 )
243     xing << ( ( data.length() & 0x0000ff00 ) >> 8 )
244     xing << ( ( data.length() & 0x000000ff ) >> 0 )
247     # Insert XING header into string, after the first MPEG header
248     data[firstFrameOffset + 4 + XingOffset, 0] = xing
252 unless repairLog.empty?()
253     puts()
254     print( "Writing file..  " )
255     destfile = File::open( destination, File::CREAT|File::TRUNC|File::WRONLY )
257     if destfile == nil
258         puts( "Error: Destination file is not writable." )
259         exit( 1 )
260     end
262     destfile << data
263     puts( "done." )
267 puts()
268 puts()
269 puts( "MP3FIX REPAIR SUMMARY:" )
270 puts( "======================" )
271 unless repairLog.empty?()
272     puts( repairLog )
273 else
274     puts( "Mp3Fix did not find any defects.")
276 puts()