Updated 2011-04-18 20:05:44 by AKgnome

A LGPL MP3 encoder.
 Where:
        Project home      : http://lame.sourceforge.net/
        Linux binaries    : http://users.rsise.anu.edu.au/~conrad/not_lame/
        Windows binaries  : http://www.rarewares.org/mp3.html

Applications based on LAME, or in related areas:


dzach:At the time being, this is a request for help. Hopefully, if this problem gets resolved, this page will be useful to more people.

I have used the windows executable with a pipe, but on a slower win98 machine I get errors. It should be better if the dll could be called directly from tcl and be passed the sound data to encode, or even better, if a tcl wrapper for LAME existed ;-).

Trying to do the first, I post here, temporarily, the files documenting the dll's usage, from the project's source code:

  • BladeMP3EncDLL.c [1]
  • BladeMP3EncDLL.h [2]
  • Example.cpp [3]
  • LameDLLInterface.htm [4]

With dll_tcl and lame_enc.dll, my code looks like this:
    source ./dll_tcl.tcl
    ::dll::load {D:\sound\lame-3.96.1\lame_enc.dll} -> lame
    # beInitStream( PBE_CONFIG pbeConfig, PDWORD dwSamples, PDWORD dwBufferSize, PHBE_STREAM phbeStream  )
    ::lame::cmd "int beInitStream( void *, uint *, uint *, uint * )"
    # beEncodeChunk( HBE_STREAM hbeStream, DWORD nSamples, PSHORT pSamples, PBYTE pOutput, PDWORD pdwOutput  )
    ::lame::cmd "int beEncodeChunk( uint, uint, short *, char *, uint * )"
    # beDeinitStream( HBE_STREAM hbeStream, PBYTE pOutput, PDWORD pdwOutput )
    ::lame::cmd "int beDeinitStream( uint, char *, uint * )"
    # beCloseStream( HBE_STREAM hbeStream  )
    ::lame::cmd "int beCloseStream( uint )"
    # beVersion( PBE_VERSION pbeVersion  )
    ::lame::cmd "int beVersion( void * )"
    # beWriteVBRHeader( LPCSTR pszMP3FileName  )
    ::lame::cmd "int beWriteVBRHeader( char * )"
    #
    #------------ set LHV1 structure values ----------------------
    array set cfg { \
        dwConfig 256 \
        dwStructVersion 1 \
        dwStructSize 331 \
        dwSampleRate 16000 \
        dwReSampleRate 0 \
        nMode 3 \
        dwBitrate 32 \
        dwMaxBitrate 0 \
        nPreset 0 \
        dwMpegVersion 1 \
        dwPsyModel 0 \
        dwEmphasis 0 \
        bPrivate 0 \
        bCRC 0 \
        bCopyright 0 \
        bOriginal 1 \
        bWriteVBRHeader 0 \
        bEnableVBR 0 \
        nVBRQuality 0 \
        dwVbrAbr_bps 0 \
        nVbrMethod 0 \
        bNoRes 0 \
        bStrictIso 0 \
        nQuality 0 \
    }
    set cfg(btReserved) [::dll::buffer 266]
    #----------- assign values to structure---------------------
    set pbeConfig [binary format iiiiiiiiiiiibbbbbbiiibbia266 \
        $cfg(dwConfig) \
        $cfg(dwStructVersion) \
        $cfg(dwStructSize) \
        $cfg(dwSampleRate) \
        $cfg(dwReSampleRate) \
        $cfg(nMode) \
        $cfg(dwBitrate) \
        $cfg(dwMaxBitrate) \
        $cfg(nPreset) \
        $cfg(dwMpegVersion) \
        $cfg(dwPsyModel) \
        $cfg(dwEmphasis) \
        $cfg(bPrivate) \
        $cfg(bCRC) \
        $cfg(bCopyright) \
        $cfg(bOriginal) \
        $cfg(bWriteVBRHeader) \
        $cfg(bEnableVBR) \
        $cfg(nVBRQuality) \
        $cfg(dwVbrAbr_bps) \
        $cfg(nVbrMethod) \
        $cfg(bNoRes) \
        $cfg(bStrictIso) \
        $cfg(nQuality) \
        $cfg(btReserved) \
        ]
    set btVersion [::dll::buffer 265]
    #
    set dwSamples 0
    set dwBuffersize 0
    set phbeStream 0
    if {[::lame::beInitStream pbeConfig dwSamples dwBuffersize phbeStream] != 0} {
        error "could not initialize lame"
    }
    set pOutput [::dll::buffer $dwBuffersize]
    set pdwOutput 0
    #
    # Just put 10 samples into the pSamples buffer for the test.
    #
    set nSamples 10
    set pSamples "0123456789"
    if {[::lame::beEncodeChunk $phbeStream $nSamples pSamples pOutput pdwOutput] != 0} {
                error "encoding failed"
    }
    if {[::lame::beDeinitStream $phbeStream pOutput pdwOutput] != 0} {
        error "Deinit failed"
    }
    if {[::lame::beCloseStream $phbeStream] != 0} {
        error "CloseStream failed"
    }

dzach 5 June 2005: I changed the last portion of code, from
    # Just put 10 samples into the pSamples buffer for the test.

to the end with:
    # here we create a sinusoidal waveform of about 444 Hz, +/- 32000 amplitude, for the test,
    # ... convert it to short with the use of "binary format",
    # ... and populate pSamples, ready for use

    for {set i 0} {$i<16000} {incr i} {
        append pSamples [binary format s [expr {int(32000*sin(10*$i*3.14/180))}]]
    }

    # calculate sound length ( divide by 2 since 1 short = 2 bytes )
    set slength [expr {[string length $pSamples]/2}]

    # LAME wants chunks of $dwSamples length or less
    set nSamples [expr {$slength>=$dwSamples ? $dwSamples:$slength}]

    if {[::lame::beEncodeChunk $phbeStream $nSamples pSamples pOutput pdwOutput] != 0} {
                error "encoding failed"
    }
    if {[::lame::beDeinitStream $phbeStream pOutput pdwOutput] != 0} {
        error "Deinit failed"
    }
    if {[::lame::beCloseStream $phbeStream] != 0} {
        error "CloseStream failed"
    }

I get an "integer parameter error" right when executing "::lame::beEncodeChunk", and this is where I'm stuck. I hope I've not overlooked something obvious.

Peter Newman 4 June 2005: I'd check that pSamples value first. Does 10 digits fit into a short? And doesn't Tcl think that anything starting with a zero is Octal? I could be wrong - and I don't really understand how nSamples and pSamples work - but does it work with nSamples = 5 and pSamples = "12345", for example.

dzach I've tried many normal (and a lot lengthier) sound files before this, but failed. I just put these values there for testing. pSamples is passed to the ::lame:: functions as a pointer to short (actually I suspect the dll expects an array of shorts at that position) while nSamples is passed by value (the length of the array), as stated in the ::lame:: function declarations (I hope). So to the dll this pSamples should look like the start address of an array of 10 shorts.

Peter Newman 4 June 2005: I use ::dll for my Win32 access - and I'm pretty sure I've had an "integer parameter error" from it too. So my gut feeling is that it can't translate the pSamples pointer you've supplied it into the short * you specified above. To me, short * specifies a pointer to a signed 32-bit integer. And I think the problem is that "::dll" can't translate the Tcl string "0123456789" to a signed 32-bit integer. Consider the following wish session:-
 % puts [expr 123456789 + 0]
 123456789
 () 2 % puts [expr 0123456789 + 0]
 expected integer but got "0123456789" (looks like invalid octal number)

In other words, the Tcl parser doesn't like "0123456789" - and presumably, passes it's opinion onto "::dll" - which then aborts with the "integer parameter error". So (without testing it), it's your "0123456789" string that's the problem. My understanding (from the Yet another dll caller documentation,) is that you'll have to use binary format, to create what the pointer points to. See the examples there.

I also suspect that you'd be better off with void * (instead of short *). To me, short * is a pointer to one single signed int; not to an array of 10 of them.

dzach 5 June 2005: Thanks a lot for the replies.

I've changed the code to produce a better testing sinusoidal sound, converting it properly to int16 (short) with the use of "binary format". If I read Yet another dll caller correctly, short stands for int16 and int for int32. A pointer to a single signed 16 bit integer is all LAME needs for pSamples, as I read it in the sources. However, I'll test with void * to make sure this is correct.

Peter Newman 5 June 2005: You may be right about the short. That's why I hate C. I've never been able to figure out why integers aren't called eg; uint8, uint16, uint32 ... sint8, sint16, sint32 or something similar. Then there's no confusion.

Lars H, off topic: It's like that because back in the seventies when C was born, the 8-bit byte wasn't as ubiquous as it is today. Computers with RAM organised as 36-bit words into which one could pack six 6-bit characters were, if not common, so at least not unheard of. In C99 they've introduced types such as those you request, although it is still optional for the compiler to provide them, and the only things guaranteed to exist are types at least that wide. The short/int/long definitions are just as fuzzy as they've always been.

After writing the previous post, I coded up a call to the Win32 "RasEnumEntries" function, using Yet another dll caller. On running it, the first error message I get is "integer parameter error".

So we both have the same problem. And that is, although we can use ::dll::buffer #bytes to allocate a buffer (to supply the info. required by the function we're calling), the problem is; "How to fill that buffer, with the data required by the function, from Tcl"?

Reading the Yet another dll caller docs, it seems to me that it could be done using the technique given in the example. Ie:-

  • Defining WPM:
 ::k::cmd "int WriteProcessMemory(int, int, void *, int, int *)" -> ::WPM

  • Allocating a buffer:
 set buff2 [[::dll::buffer 1000]]

  • Filling the buffer:
 set lvitem [[binary format iiiiiiiiiiiii 3 $i 0 0 0 [[expr $addr + $nStart]] 128 0 0 0 0 0 0]]
 WPM $hnd [expr $addr + 8] lvitem 52 0

I think that's what all the above code achieves. But that solution's just too !@#$%%^ complicated for me. And I can't be bothered spending a whole day to try and figure it out.

I'd previously installed HLA - High Level Assembler - because it let's you program like in C - but without C's complexity and obscurity. So I thought; now's a good time to learn it. It's taken 2 days to learn the language, but now I've coded the call to "RasEnumEntries". Easy as. Another couple days, and I might be able to turn it into a Tcl C extension (although !@#$%^ C; mine will be written in HLA).

So I'll have to leave you or someone else to figure out how to do it with Yet another dll caller. It might be worthwhile to email the guy who wrote it, and ask him to clarify how it's done.

dzach: I've tested void * in place of short * and wish crashes. I've also tested the sinusoidal sound in the pSamples buffer with snack; it sounds great, so I'm sure I've got the input data properly formatted. So, here we are, back to where we started. Thanks again for the advices.

Maybe (like Peter Newman suggested, but in a broader scope), this is the clue (from the Yet another dll caller page):
The next call illustrates another rule. When a function parameter refers to memory filled during the function call, the corresponding parameter specification must be void * and a bytearray of suitable size should be provided.

If I get this right, then char * and short * in
    ::lame::cmd "int beEncodeChunk( uint, uint, short *, char *, uint * )"

and
    ::lame::cmd "int beDeinitStream( uint, char *, uint * )"

should become void *
    ::lame::cmd "int beEncodeChunk( uint, uint, void *, void *, uint * )"
    ::lame::cmd "int beDeinitStream( uint, void *, uint * )"

which indeed solves the integer parameter error I was getting.

Peter Newman 6 June 2005: I believe you're right. I'd always interpreted the phrase When a function parameter refers to memory filled during the function call...(etc)... as meaning that you only used void * when it was the CALLING program that filled the buffer. BUT OBVIOUSLY NOT. It doesn't matter who fills the buffer - the calling program, or the called function - the buffer pointer is always passed as void *.

dzach 8 June 2005: Here is a part of an email that I got from NJG, regarding the use of pointer parameters in the ::dll functions:
 If you use a pointer parameter of the type int*, short* etc. the variable given as actual parameter *must*
 exist and *must* contain a value that Tcl_GetIntFromObj can interpret without returning TCL_ERROR.
 (The reason is that the dll caller cannot decide whether a pointer is an in-out or just an out parameter.)