Hi José,
Merry Christmas!
Found some time to play with my music collection. For playing mp3 files i use directshow, which works quite well for all music formats (.mid, .wav, .mp3, ...) - except for one thing, duration. I can get the time an audio file is expected to play from IMediaPosition or IMediaSeeking but in both cases the returned time is wrong for mp3 files. Interestingly enough Windows shows the correct time. The tooltip when hovering or the details tab of file properties (right click -> properties -> details tab) show the correct time.
So Windows somehow knows (or calculates) it. But how? How does Windows do that, and how can i make use of it? I cannot find an appropriate API nor am i able to find some COM code doing it. I found one thing, which looks promising here (https://docs.microsoft.com/de-de/uwp/api/windows.storage.fileproperties.musicproperties). I cannot find IStorageItemExtraProperties and related classes or interfaces in your includes. The required information might not be found in musicproperties but maybe in some other file property around here - this is my best bet.
Do you know how to retrieve these properties in PB?
Thanks,
JK
IMediaSeeking::GetDuration
and read this
https://support.microsoft.com/fr-fr/help/2676617/fix-the-imediaseeking-getduration-method-returns-an-incorrect-playback
Better to use Bass.dll, or the Media Foundation API.
Patrice,
Merry Christmas to you as well!
Thanks for your reply. I read about IMediaSeeking being a better choice, but for me (Windows 10), it returns the same (wrong) result as IMediaPosition. Strange that this bug hasn´t be fixed as of now and that you must do it yourself as your link proposes.
I know and already used Bass.dll (a great piece of software btw.), but i wanted to be independent of third party software. And obviously Windows can do what i want.
In the meantime i found Jim Fritts´ PB source code (https://forum.powerbasic.com/forum/user-to-user-discussions/source-code/773139-media-foundation-player) and could adapt it to show the duration - tada, now it´s correct.
Unfortunately the Media Foundation API seems a rather complicated way of retrieving this little piece of data and it requires Windows 8 or higher - i was hoping for an easier solution.
I know that the duration information displayed by Windows doesn´t come from ID3 tags, because, when i remove these tags (i verfied removal with a hex editor), the duration displayed stays the same. Mp3tag (a program for showing and editing id3 tags) shows the correct duration with and without tags in Windows 7 and 10. So there must be some other (working) method than Media foundation.
Finally i found what i need here (https://forum.powerbasic.com/forum/user-to-user-discussions/powerbasic-for-windows/775019-mp3-tagger-discussion , Bob Carvers code). It reads (at least i think it reads it) the correct duration, even if there are no ID3 tags.
You can look at my CAfxMp3.inc file, that includes de CAfxMp3 class. It gets the duration callig the GetDuration method of the IMediaSeeking interface. Be aware that this method returns 10,000,000 for a second (100-nanosecond units).
BTW I ported this class to FreeBasic (CDSAudio class in CDSAudio.inc).
Documentation: https://github.com/JoseRoca/WinFBX/blob/master/docs/COM/CDSAudio%20Class.md
Thanks José,
good to know that there is a FreeBASIC equivalent. I already know how to do it (PB and FB). But the problem remains the same, IMediaSeeking GetDuration returns a wrong result for mp3 files. Seems i need a translation of Bob Carvers´s code for that.
I think to remember from the top of my head that mp3 duration is only an approximation, because of the audio signal compression.
(mostly with mp3 using a variable bit rate)
The true length can be known only when audio comes to full completion.
This is the reason why Bass.dll use the BASS_STREAM_PRESCAN flag to return an accurate length.
What you could do is to encode the correct duration into the ID3 tag (there are many utilities able to do this).
Or convert the mp3 to ogg with Audacity if you don't need the ID3 tag.
Yes, for compression formats with a variable bit rate like mp3 you cannot just divide the number of frames by the rate (fps). But you can calculate the exact duration by looping through the frames (i think this is what BASS_STREAM_PRESCAN does). There is sample code for this at various places in the net.
In the meantime i found what i was looking for. What an effort for a few lines of code! In fact retrieving the duration form the propertystore delivers correct results (even for mp3, Windows 7 and 10). I will post code here soon.
PB code:
#COMPILE EXE
#DIM ALL
'#utility roca3 'use José Roca includes
#Include "win32api.inc"
#Include "shobjidl.inc"
#Include "propkey.inc"
FUNCTION duration(file AS WSTRINGZ) AS LONG
'***********************************************************************************************
' return duration of an audio file in ms, -1 for fail
'***********************************************************************************************
LOCAL d AS QUAD
LOCAL propVar AS PropVariant
LOCAL hr AS LONG
LOCAL pPropStore AS IPropertyStore
hr = SHGetPropertyStoreFromParsingName(BYVAL VARPTR(file), NOTHING, %GPS_DEFAULT, $IID_IPropertyStore, pPropStore)
IF hr = %S_OK THEN
hr = pPropStore.GetValue(PKEY_Media_Duration, propVar)
IF hr = %S_OK THEN
d = propVar.uhVal 'time in 100ns
ELSE
FUNCTION = -1
EXIT FUNCTION
END IF
pPropStore = NOTHING
ELSE
FUNCTION = -1
EXIT FUNCTION
END IF
FUNCTION = d/10000 'time in ms
END FUNCTION
FUNCTION PBMAIN () AS LONG
'***********************************************************************************************
' -> change paths ... (must be a full path)
'***********************************************************************************************
? STR$(duration("C:\PBwin10\IDE\Projects\mp3\blues.mp3"))
? STR$(duration("C:\PBwin10\IDE\Projects\mp3\cher.mid"))
? STR$(duration("C:\PBwin10\IDE\Projects\mp3\ring05.wav"))
END FUNCTION
FB code:
'#COMPILER FREEBASIC
'#COMPILE CONSOLE 32 '64
#INCLUDE ONCE "windows.bi"
#INCLUDE ONCE "win\shobjidl.bi"
FUNCTION duration (file AS WSTRING) AS LONG
'***********************************************************************************************
' return duration of an audio file in ms, -1 for fail
'***********************************************************************************************
DIM d AS ULONGINT
DIM propVar AS PropVariant
DIM hr AS HRESULT
DIM pPropStore AS IPropertyStore PTR
DIM PKEY_Media_Duration AS PROPERTYKEY
'***********************************************************************************************
' MACRO PKEY_Media_Duration = GUID$("{64440490-4C8B-11D1-8B70-080036B11A03}") & MKDWD$(3)
' would be nice to have something like GUID$ for FB too !
'***********************************************************************************************
PKEY_Media_Duration.fmtid.data1 = &H64440490
PKEY_Media_Duration.fmtid.data2 = &H4C8B
PKEY_Media_Duration.fmtid.data3 = &H11D1
PKEY_Media_Duration.fmtid.data4(0) = &H8B
PKEY_Media_Duration.fmtid.data4(1) = &H70
PKEY_Media_Duration.fmtid.data4(2) = &H08
PKEY_Media_Duration.fmtid.data4(3) = &H00
PKEY_Media_Duration.fmtid.data4(4) = &H36
PKEY_Media_Duration.fmtid.data4(5) = &HB1
PKEY_Media_Duration.fmtid.data4(6) = &H1A
PKEY_Media_Duration.fmtid.data4(7) = &H03
PKEY_Media_Duration.pid = 3
coinitialize(0) 'a must in FB
hr = SHGetPropertyStoreFromParsingName(varptr(file), 0, GPS_DEFAULT, @IID_IPropertyStore, @pPropStore)
IF hr = S_OK THEN
hr = IPropertyStore_GetValue(pPropStore, @PKEY_Media_Duration, @propVar)
IF hr = S_OK THEN
d = propVar.uhVal.QuadPart 'time in 100ns
ELSE
couninitialize
RETURN -1
END IF
IPropertyStore_Release(pPropStore)
ELSE
couninitialize
RETURN -1
END IF
couninitialize
RETURN d/10000 'time in ms
END FUNCTION
'***********************************************************************************************
' -> change paths ... (must be a full path)
'***********************************************************************************************
PRINT duration("C:\PBwin10\IDE\Projects\mp3\blues.mp3")
PRINT duration("C:\PBwin10\IDE\Projects\mp3\cher.mid")
PRINT duration("C:\PBwin10\IDE\Projects\mp3\ring05.wav")
SLEEP
> would be nice to have something like GUID$ for FB too !
I have one: AfxGuid (in AfxCOM.inc)
' ========================================================================================
' Converts a string into a 16-byte (128-bit) Globally Unique Identifier (GUID)
' To be valid, the string must contain exactly 32 hexadecimal digits, delimited by hyphens
' and enclosed by curly braces. For example: {B09DE715-87C1-11D1-8BE3-0000F8754DA1}
' If pwszGuidText is omited, AfxGuid generates a new unique guid.
' Remarks: I have need to call the UuidCreate function dynamically because, at the time of
' writing, the library for the RPCRT4.DLL seems broken and the linker fails.
' ========================================================================================
PRIVATE FUNCTION AfxGuid (BYVAL pwszGuidText AS WSTRING PTR = NULL) AS GUID
DIM rguid AS GUID
IF pwszGuidText = NULL THEN
' // Generate a new guid
DIM AS ANY PTR pLib = DyLibLoad("RPCRT4.DLL")
IF pLib THEN
DIM pProc AS FUNCTION (BYVAL Uuid AS UUID PTR) AS RPC_STATUS
pProc = DyLibSymbol(pLib, "UuidCreate")
IF pProc THEN pProc(@rguid)
DyLibFree(pLib)
END IF
ELSE
CLSIDFromString(pwszGuidText, @rGuid)
END IF
RETURN rguid
END FUNCTION
' ========================================================================================
> I have one: AfxGuid (in AfxCOM.inc)
Great, thanks!