This is a little proc I've coded to get information from an MP3 file. As input it requires a filename and a pointer to an MP3INFO structure that will receive the information (if the proc returns true).


MP3INFO STRUCT
fileSize dword ? ; the total filesize, in bytes
fileLength dword ? ; length of MP3, in msec
bitrate sdword ?
;note: the bitrate is in bits per second. if bitrate is negative,
;the MP3 uses a variable bitrate. In this case, the value is
;the negated average bitrate.
frequency dword ? ; in Hertz
nFrames dword ? ; number of MPEG frames
MP3INFO ENDS


The number of frames seem to be different from what I get in winamp, however I think winamp does not actually count them, but calculates them by taking one frame size, and dividing the filesize by that frame size. This value exactly matches the value winamp shows, but isn't correct (I think) as it does not account frame padding. For VBR mp3's however, winamp does count them (it has to), and the values match.

The code is optimized for size (442 bytes now), not for speed, as it would be pretty useless due to the file operations.

Feel free to optimize it further (in size) :)



.data
bitrateTable dw -1,32,40,48,56,64,80,96,112,128,160,192,224,256,320,-1
samplerateTable dw 44100,48000,32000
.code

; #########################################################################


GetMP3Info proc uses esi edi ebx lpFilename:DWORD, lpInfoStruct:DWORD
LOCAL hFile:DWORD
LOCAL hMap:DWORD
LOCAL pMap:DWORD
LOCAL totalbitrate:DWORD
LOCAL totalaudiosize:DWORD
xor ebx, ebx
mov hFile, ebx
mov hMap, ebx
mov pMap, ebx
mov edi, lpInfoStruct
push edi
or eax, -1
mov ecx, (sizeof MP3INFO) / 4
rep stosd
mov totalbitrate, ebx
mov totalaudiosize,ebx

assume edi:PTR MP3INFO
pop edi
mov [edi].nFrames, ebx

invoke CreateFile, lpFilename, GENERIC_READ, FILE_SHARE_READ,\
ebx, OPEN_EXISTING, ebx, ebx
cmp eax, INVALID_HANDLE_VALUE
je _invalid

mov hFile, eax
invoke CreateFileMapping, eax, ebx, PAGE_READONLY, ebx,ebx,ebx
or eax, eax
jz _invalid

mov hMap, eax

invoke MapViewOfFile, eax, FILE_MAP_READ,ebx,ebx,ebx
or eax, eax
jz _invalid

mov pMap, eax
mov esi, eax

invoke GetFileSize, hFile, NULL
mov [edi].fileSize, eax
lea ebx, [eax+esi]

_gotonextframe:
cmp byte ptr [esi], 0FFh
je @F
inc esi
cmp esi, ebx
jb _gotonextframe
jmp _done
@@:
cmp esi, ebx
je _done
inc esi
mov al, [esi]
test al, 11100000b
jz _gotonextframe
inc esi

_frame_found:

inc [edi].nFrames
mov cl, al
and cl, 00011110b
cmp cl, 00011010b ;MPEG v1, layer 3?
jne _not_supported
mov eax, ebx
sub eax, esi
cmp eax, 2 ;at least 2 bytes readable?
jb _invalid
movzx eax, byte ptr [esi]
mov ecx, eax
sub esi, 2 ;point back to start of frame, as framesize is added later
shr al, 4
movsx eax, word ptr [bitrateTable][2*eax]

cmp eax, -1
je _invalid
; eax is bitrate
add totalbitrate, eax
mov edx, [edi].bitrate
cmp edx, -1
jne _bitrate_already_set
mov [edi].bitrate, eax
jmp _fixed_bitrate_so_far
_bitrate_already_set:
cmp eax, edx ;current bitrate same as set bitrate?
je _fixed_bitrate_so_far
;variable bitrate
and [edi].bitrate, 0 ;set to zero indicating variable bitrate
;fall through...
_fixed_bitrate_so_far:

xor edx, edx ; init framesize to zero
shr ecx, 2
setc dl ;last bit shifted out is padding bit,
; if set increase framesize by one

and ecx, 3
cmp ecx, 3
je _invalid
movzx ecx, word ptr [samplerateTable][2*ecx]
mov [edi].frequency, ecx

;FrameSize = 144 * BitRate / SampleRate + Padding
; eax = bitrate(in kbps, needs to be converted to bps yet)
; ecx = sampleRate
; edx = padding
; framesize = 144 * eax / ecx + edx

push edx
imul eax, 144*1000
xor edx, edx
div ecx
pop edx
add edx, eax

; edx is framesize now

add esi, edx
cmp esi, ebx
ja _invalid ; frame bigger than file, invalid
je _done

add totalaudiosize, edx

jmp _gotonextframe

_not_supported:
_invalid:
xor eax, eax
jmp _exit

_done:
mov ebx, 1000

mov eax, [edi].bitrate ;get bitrate in kbps
mul ebx ;*1000, in bpsd
mov [edi].bitrate, eax ;set new bitrate, in bps
mov ecx, eax ;set ecx to bitrate
or eax, eax ;bitrate zero?
jnz _fixed_bitrate ;if not, fixed
mov eax, totalbitrate ;else, variable. get total (sum of all bitrates)
mul ebx ;multiple by 1000
mov ecx, [edi].nFrames ;get number of frames
or ecx, ecx ;no frames?
jz _invalid ;bye
div ecx ;divide by number of frames
mov ecx, eax ;set ecx to positive bitrate
neg eax ;negate, indicating a variable bitrate
mov [edi].bitrate, eax
_fixed_bitrate:
; eax is bitrate here, either fixed or averaged value
shl ebx, 3 ;1000*8=8000

mov eax, totalaudiosize
mul ebx ; * 8000, convert bytes to bits and seconds to miliseconds
div ecx ; divide by positive bitrate in bps
mov [edi].fileLength, eax ;set filelengths, in msecs

;calculate time
;bitrate
assume edi:nothing
xor eax, eax
inc eax

_exit:
push eax

mov eax, pMap
or eax, eax
jz @F
invoke UnmapViewOfFile, eax
@@:

mov eax, hMap
or eax, eax
jz @F
invoke CloseHandle, eax
@@:

mov eax, hFile
or eax, eax
jz @F
invoke CloseHandle, eax
@@:

pop eax
ret
GetMP3Info endp


Thomas
Posted on 2002-04-21 14:53:52 by Thomas
Oh! size optimization is always pleasure :)
No testing needed, not procs difference talks :)
Are FPU or MMX opcodes allowed?
Posted on 2002-04-21 16:03:30 by The Svin
Yes that's okay but the proc may change a lot, I'm still working on it. It has several bugs and does not support mpeg 2 & 2.5 yet.
I'll post the new version later, need some sleep first :)

Thomas
Posted on 2002-04-21 16:16:00 by Thomas
New and much more stable version:


.data
;V1 layer 3 bitrates
bitrateTableV1 dw -1,32,40,48,56,64,80,96,112,128,160,192,224,256,320,-1
;V2/2.5 layer 3 bitrates
bitrateTableV2 dw -1,8,16,24,32,40,48,56,64,80,96,112,128,144,160,-1


samplerateTableV1 dw 44100,48000,32000
samplerateTableV2 dw 22050,24000,16000
samplerateTableV2_5 dw 11025,12000,8000

.code

; #########################################################################
MP3INFO STRUCT
fileSize dword ? ; in bytes
fileLength dword ? ; in miliseconds
bitrate sdword ? ; in bps. if negative: variable bitrate,bitrate=negated average
frequency dword ? ; in Hertz
nFrames dword ? ; number of MPEG frames
MP3INFO ENDS


GetMP3Info proc uses esi edi ebx lpFilename:DWORD, lpInfoStruct:DWORD
LOCAL hFile:DWORD
LOCAL hMap:DWORD
LOCAL pMap:DWORD
LOCAL totalbitrate:DWORD
LOCAL srTablePtr:DWORD
xor ebx, ebx
mov hFile, ebx
mov hMap, ebx
mov pMap, ebx
mov edi, lpInfoStruct
push edi
or eax, -1
mov ecx, (sizeof MP3INFO) / 4
rep stosd
mov totalbitrate, ebx

assume edi:PTR MP3INFO
pop edi
mov [edi].nFrames, ebx

invoke CreateFile, lpFilename, GENERIC_READ, FILE_SHARE_READ,\
ebx, OPEN_EXISTING, ebx, ebx
cmp eax, INVALID_HANDLE_VALUE
je _invalid

mov hFile, eax
invoke CreateFileMapping, eax, ebx, PAGE_READONLY, ebx,ebx,ebx
or eax, eax
jz _invalid

mov hMap, eax

invoke MapViewOfFile, eax, FILE_MAP_READ,ebx,ebx,ebx
or eax, eax
jz _invalid

mov pMap, eax
mov esi, eax

invoke GetFileSize, hFile, NULL
mov [edi].fileSize, eax
lea ebx, [eax+esi]

_gotonextframe:
; find first 8 bits of sync bits
cmp byte ptr [esi], 0FFh
je @F
inc esi
cmp esi, ebx
jb _gotonextframe
jmp _done

;possible first 8 sync bits found
@@:
DPrint "8 sync bits found"
inc esi
cmp esi, ebx
jae _done
mov al, [esi]
mov cl, al
and cl, 11100000b
IFDEF _DEBUG
.IF cl==11100000b
DPrint "11 sync bits found in total!"
.ELSE
DPrint "no more 1 bits found, no sync"
.ENDIF
ENDIF
cmp cl, 11100000b
jne _gotonextframe
inc esi

_frame_found:
DPrint "frame found"
mov cl, al
and cl, 00011110b ; 000BBCC0, BB=MPEG audio version ID, CC=layer description

mov edx, offset bitrateTableV1
mov srTablePtr, offset samplerateTableV1
cmp cl, 00011010b ;MPEG v1, layer 3?
je _frame_continue

mov edx, offset bitrateTableV2
mov srTablePtr, offset samplerateTableV2
cmp cl, 00010010b ;MPEG v2, layer 3?
je _frame_continue

mov srTablePtr, offset samplerateTableV2_5
IFDEF _DEBUG
.IF cl!=00000010b
DPrint "unsupported format 1"
.ENDIF
ENDIF
cmp cl, 00000010b ;MPEG v2.5, layer 3
jne _gotonextframe ;find next frame
PrintText "falling"
;fall through...
_frame_continue:
PrintText "---frame continue---"
mov eax, ebx
sub eax, esi

cmp eax, 2 ;at least 2 bytes readable?
jb _invalid
movzx eax, byte ptr [esi]
mov ecx, eax
shr al, 4

DPrintValH eax, "bits for bitrate(hex)"
movsx eax, word ptr [edx][2*eax]
DPrintValD eax, "curbitrate"

cmp eax, -1
je _gotonextframe ;_invalid !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
sub esi, 2 ;point back to start of frame, as framesize is added later
; eax is bitrate
add totalbitrate, eax
mov edx, [edi].bitrate
cmp edx, -1
jne _bitrate_already_set
mov [edi].bitrate, eax
jmp _fixed_bitrate_so_far
_bitrate_already_set:
cmp eax, edx ;current bitrate same as set bitrate?
je _fixed_bitrate_so_far
;variable bitrate
and [edi].bitrate, 0 ;set to zero indicating variable bitrate
;fall through...
_fixed_bitrate_so_far:

xor edx, edx ; init framesize to zero
shr ecx, 2
setc dl ;last bit shifted out is padding bit,
; if set increase framesize by one

and ecx, 3

cmp ecx, 3
jne @F
inc esi
cmp esi, ebx
jae _done
jmp _gotonextframe
@@:

shl ecx, 1
add ecx, srTablePtr
movzx ecx, word ptr [ecx]
mov [edi].frequency, ecx
DPrintValD ecx, "cur freq"
;FrameSize = 144 * BitRate / SampleRate + Padding
; eax = bitrate(in kbps, needs to be converted to bps yet)
; ecx = sampleRate
; edx = padding
; framesize = 144 * eax / ecx + edx
DPrintValD edx, "padding"
push edx
mov edx, 144*1000
cmp srTablePtr, offset samplerateTableV1
je @F
mov edx, 72*1000
@@:
mul edx
div ecx
pop edx
add edx, eax
DPrintValD edx, "framesize"
; edx is framesize now

add esi, edx
cmp esi, ebx
jae _done ; frame bigger than file, file is cut

inc [edi].nFrames
jmp _gotonextframe


_invalid:
DPrint "---invalid, exit---"
xor eax, eax
jmp _exit

_done:
mov ebx, 1000

mov ecx, [edi].nFrames ;get number of frames
mov eax, [edi].bitrate ;get bitrate in kbps
mul ebx ;*1000, in bpsd
mov [edi].bitrate, eax ;set new bitrate, in bps
or eax, eax ;bitrate zero?
jnz _fixed_bitrate ;if not, fixed
mov eax, totalbitrate ;else, variable. get total (sum of all bitrates)
mul ebx ;multiple by 1000
or ecx, ecx ;no frames?
jz _invalid ;bye
div ecx ;divide by number of frames
neg eax ;negate, indicating a variable bitrate
mov [edi].bitrate, eax
_fixed_bitrate:

; 1152 = # of samples in one frame for layer 3 mpegs

mov ebx, 1152*1000
mov eax, [edi].nFrames
xor edx, edx
mul ebx
mov ebx, [edi].frequency
or ebx, ebx
jz _invalid
div ebx
mov [edi].fileLength, eax ;set filelengths, in msecs

;calculate time
;bitrate
assume edi:nothing
xor eax, eax
inc eax

_exit:
push eax

mov eax, pMap
or eax, eax
jz @F
invoke UnmapViewOfFile, eax
@@:

mov eax, hMap
or eax, eax
jz @F
invoke CloseHandle, eax
@@:

mov eax, hFile
or eax, eax
jz @F
invoke CloseHandle, eax
@@:

pop eax
ret
GetMP3Info endp


Thomas
Posted on 2002-04-22 14:04:24 by Thomas
I've tested the function on 700 MB of MP3s, which had been loaded a few times before so they may have been in cache. It took 70 seconds (10MB/s).
So it takes quite some time to just calculate the length (msecs) of an mp3 file, but that's not strange as you need to read the header (first 4 bytes) of every frame. Most mp3's have a framesize of 417/418 bytes :(
I wonder if winamp reads it fully though. For fixed bitrate mp3s you may calculate the length by dividing the filesize by the length of one frame, but that's less accurate and wrong when a variable bitrate is used. If you browse a list of mp3s in the playlist of winamp you will see it takes some time before the lengths are displayed so it probably reads the whole file as well.

I still don't understand how winamp calculates the number of frames in a file, it just can't be right or I'm missing something:

http://www.dv.co.yu/mpgscript/mpeghdr.htm]

How to calculate frame length

For Layer II & III files use this formula:

FrameLengthInBytes = 144 * BitRate / SampleRate + Padding


Now, I have one file with the following info (according to winamp):


........
Size: 2188980 bytes
Length: 273 seconds
[b]MPEG 2.0 layer 3
64kbit, 10523 frames[/b]
22050Hz Stereo
........

BitRate = 64000 (in bps), samplerate = 22050, padding is either 0 or 1 (varies per frame, but only 1 byte in difference at most).
Now let's assume none of the frames uses padding, which would be the smallest frames possible in this format. The size of these frames would be:
FrameLengthInBytes = 144 * 64000 / 22050 + 0
= 417 (using integer arithmetic, so rounded down)
Some of the frames may be 418 bytes, due to the padding, but let's assume every frame is as small as possible.
The maximum number of frames with these numbers would be:
max_frames = filesize/framesize = 2188980/417=5249

So at most, there can be 5249 frames in this file, how can winamp show there are 10523 frames??? :confused:

Anyone knows more about this?

Thomas
Posted on 2002-04-22 14:17:46 by Thomas
Thomas, so shall we optimize for speed too?
I mean, not nessesarily implementation but also the algo itself.
Then, I might need mp3 formats to make anlysis of possible ways to calc.
Posted on 2002-04-22 14:34:25 by The Svin
Svin: Well the problem is you need file access and file access is slow. However you can simply calculate an estimated length of the mp3 using the filesize, but I like to have it precisely.

I fixed the problem I described in my previous post, for MPEG2 files, the '144' used should be '72'. It works okay now (I edited the latest code post). The only thing I'm not sure about is if it works for MPEG2.5, but that isn't an official standard either. I don't have those files so I can't test it either.

It's hard to find good info on the format, the link I showed you has some good information, but I've seen several sources telling different things..

But for most MP3 it works now.

Thomas
Posted on 2002-04-22 14:43:39 by Thomas
If your mean by file access just opening file through CreateFile and setting pointers - it can not take so long as 70 secs.
The difference is what you do and how you do it with opened files.
Posted on 2002-04-22 14:49:42 by The Svin

If your mean by file access just opening file through CreateFile and setting pointers - it can not take so long as 70 secs.
The difference is what you do and how you do it with opened files.


Well it's 70 seconds for 296 files totalling 700MB so it's not that bad. I've thought about using SetFilePointer/ReadFile instead of memory mapped files but I'm not sure it would be faster.

Thomas
Posted on 2002-04-22 14:54:04 by Thomas
Thomas, I can not say if it would be faster not having whole nessesary steps in mind.
But if you need to read just few parts of file data it's for sure faster.
You see mapping \ unmapping whole files
1. Yet need CreateFile
2. Involve a lot of work with for system for memory \ paging manipulation including hard usage of swap file, feeling and cleaning it. So system does a lot of write\read file itself with memory map files logic. It's usefull only when you do a whole file data work. And of course it is comfortable for programmer.
Not for speed.
Posted on 2002-04-22 15:19:31 by The Svin
hi,

i did, some time ago, some researchs in the id3v2 thingies that are inside mp3 and others.

i didnt discovered anything, and give up really fast. but i coded that .inc file

ancev

ps: is nasm syntax
Posted on 2002-04-22 15:21:10 by ancev
...It's usefull only when you do a whole file data work....


To get the exact duration, you will need to read each frame, which means reading at ~400 bytes intervals. Not the full file but all the mapped pages are likely touched at that interval.

Using readfile it would be enough to read only 4 bytes at each ~400 byte interval, unless the frame doesn't start there. In that case, a byte scan is needed to find the first sync bit sequence (11 bits set to 1, luckily always aligned to a byte).
This would cause a lot of readfiles for 4 bytes and a few for a whole sequence so I don't know if that would improve it much..

Ancev: Thanks for the code I'll take a look at it.

Thomas
Posted on 2002-04-22 16:04:56 by Thomas
OK until I understood the whole algo let's proceed with size
first notion:


xor ebx,ebx
....
lea ecx,[ebx][5] ; 3 bytes instead of 5 in mov ecx,5
Posted on 2002-04-23 01:09:09 by The Svin
Little bit.
1. mov edi,lpInfoStruct; push edi;...; pop edi => mov edi,lpInfoStruct; ...; mov edi,lpInfoStruct - one byte greate, but smaller memory access and instructions.
2. call CreateFileMapping; mov hMap,eax; test eax,eax; je _invalid. Remove mov hMap,ebx above. Same for pMap.
3. Move mov hFile,ebx after CreateFile for more memory access time locality.
4. For sequenced files use FILE_FLAG_SEQUENTIAL_SCAN, FILE_FLAG_NO_BUFFERING.
5. Use ReadFile with reading several frames.
6. or eax,eax - source of dependens for follow instruction, use test eax,eax instead.
7. or eax,eax; je label - jecxz label replace is shorter.
8. Divide by zero blocking may be perform early.
Posted on 2002-04-27 10:53:16 by Nexo
Thanks for your suggestions, though I have a few comments:


1. mov edi,lpInfoStruct; push edi;...; pop edi => mov edi,lpInfoStruct; ...; mov edi,lpInfoStruct - one byte greate, but smaller memory access and instructions.

Well I wanted to optimize for size, as the speed is almost completely determined by the file I/O speed..


2. call CreateFileMapping; mov hMap,eax; test eax,eax; je _invalid. Remove mov hMap,ebx above. Same for pMap.

I can't do that because hMap and pMap need to be intialized to zero intially (I used ebx because mov hMap, ebx is smaller than mov/and hMap, 0). This because of the error handling (_invalid). It only needs to close the file/map/mappointer handles if they are actually set.

5. Use ReadFile with reading several frames.

That might be faster but is more complex, and I'm lazy :grin:

The other things are useful, thanks,

Thomas
Posted on 2002-04-27 11:54:23 by Thomas
Originally posted by Thomas

I can't do that because hMap and pMap need to be intialized to zero intially (I used ebx because mov hMap, ebx is smaller than mov/and hMap, 0). This because of the error handling (_invalid). It only needs to close the file/map/mappointer handles if they are actually set.



But, if add _invalid1, _invalid2, ... and skip additional checking for handles. Optimize for size ;)



mov [dword esp],1 ; fixup result
_exit: ; push 0 above
invoke UnmapViewOfFile,pMap
_invalid1:
invoke CloseHandle,hMap
_invalid2:
invoke CloseHandle,hFile
pop eax
ret
Posted on 2002-04-27 13:22:17 by Nexo
Hi,

I've translated to Nasm syntax the second source Thomas posted, to use it as a library for PureBasic programs. Just wanted to ask if there was something wrong in doing so (cool proc, BTW!).

Thanks,
Posted on 2002-04-29 10:41:56 by El_Choni
No problem :), but why didn't you assemble the MASM source into a library directly?

Thomas
Posted on 2002-04-29 11:05:40 by Thomas
I'm not sure, but I think that PureBasic can only use Nasm's .obj for its libraries (yes, I'm a newbie ;).

Tthanks a lot. Bye,
Posted on 2002-04-29 11:14:16 by El_Choni
One thing I didn't notice until today is that I used a multiplier of 1000 to convert from kbps (kilo bits per second) to bps (bits per second). Should I use 1024 here instead? That would be more logical but I couldn't find a confirmation of this.

Thomas

P.S. the latest version of this procedure is available at the snippet library on my site.
Posted on 2002-05-27 09:24:47 by Thomas