Converting DSD to PCM with BASS_ChannelGetData

Started by soundgals, 24 Feb '24 - 14:56

soundgals

Great! Would you be able to post an example using the callback function?

I'm sure I'll be able to translate it to Swift quite easily.

Thanks in advance.

Ian @ un4seen

A callback function that stores the encoded data in memory could look something like this:

BYTE *data = 0;
DWORD datasize = 0;
DWORD dataspace = 0;
DWORD allocextra = 0x100000;

void CALLBACK EncodeProcEx(HENCODE handle, DWORD channel, const void *buffer, DWORD length, QWORD offset, void *user)
{
if (dataspace < offset + length) { // need more memory
BYTE *newdata = (BYTE*)realloc(data, offset + length + allocextra); // allocate it
if (!newdata) return; // failed
data = newdata;
dataspace = offset + length + allocextra;
}
memcpy(data + offset, buffer, length); // copy the data to memory
if (datasize < offset + length) datasize = offset + length; // update the size
}


soundgals

I've had an attempt to convert your example to Swift code as follows...
func encodeProc(handle: HENCODE, channel: DWORD, buffer: UnsafeRawPointer?, length: DWORD, offset: QWORD, user: UnsafeMutableRawPointer?) ->Void{
        var data: UnsafeMutablePointer<UInt8>? = nil
        var datasize: UInt32 = 0
        var dataspace: UInt32 = 0
        let allocextra: UInt32 = 0x100000
        if dataspace < offset + QWORD(length) { // need more memory
            let newdata = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(UInt32(offset) + length + allocextra))
            data = newdata
            dataspace = UInt32(offset) + length + allocextra
        }
        memcpy(data! + Int(offset), buffer, Int(length)) // copy the data to memory
        if datasize < offset + QWORD(length) { datasize = UInt32(offset) + length } // update the size
    }

I believe this is correct. I can't be sure until I'm able to call it though.

I've been trying to figure out how to do that with no success. I know you don't speak Swift; but if you, or anyone else, has an idea how to call the encodeProc callback within the following Swift call to BASS_Encode_Start, I would really appreciate it.

let encoder = BASS_Encode_Start(stream, destinationUrl,  DWORD(BASS_ENCODE_PCM | BASS_ENCODE_AUTOFREE), nil, nil)
It would obviously replace the penultimate "nil" in that line.

The good news is, I have been able to write both WAV and flac files to the user's file system. The problem was that BASS_Encode_Start was expecting the file path without the "file://" standard file path prefix. Removing that did the trick.

So at the moment, I'm writing those files then grabbing the data from the file into my app's memory.

I would much prefer to avoid writing a file to the user's file system and just getting the data by using the proc callback.

Ian @ un4seen

Please note that the callback function I posted above is for use with BASS_Encode_FLAC_Start rather than BASS_Encode_Start. The former takes an ENCODEPROCEX callback with an "offset" parameter, while the latter takes an ENCODEPROC callback without one. That's because the latter is fed by an external encoder via STDOUT, which has no support for seeking (so no need for an "offset" parameter).

It would be used like this in C/C++:

encoder = BASS_Encode_FLAC_Start(channel, options, flags, EncodeProcEx, 0);

soundgals

#30
Thanks for correcting me on that. Of course I want to use both.

I just realised I needed to define that callBack function outside of the class where all of my other BASS related functions reside.

I'm not getting any xCode errors now. So I'll give it a try.

soundgals

... well everything appeared to be working; but I'm not getting playable data.

The EncodeProcEx callback seems to be doing its thing and I do get trackData.

You mentioned earlier that I still need to use BASS_ChannelGetData as well as the encoder using BASS_Encode_FLAC_Start with the callback function.

But there is no difference that I can see between the data I get from BASS_ChannelGetData alone and the data I get when I also start the encoder.

If the data would be in flac format, from the action performed by the encoder, I would expect a difference in the data.

Without delving too deeply into the code, here are the critical lines...

let stream = BASS_StreamCreateURL(theURL, 0, DWORD(BASS_SAMPLE_FLOAT | BASS_STREAM_DECODE), nil, nil)
let encoder = BASS_Encode_FLAC_Start(stream, nil, 0, encodeProc, nil)
let got = BASS_ChannelGetData(DWORD(stream), buffer + UnsafeMutableRawPointer.Stride(done), 0xFFFFFFF)

The last line, of course, is within the while loop as the data length will exceed 0xFFFFFFF.

Hopefully you'll be able to spot something obvious I'm doing wrong.

I'm using practically the same code as when creating a flac file; the only real difference being the BASS_Encode_FLAC_Start writing to a file, instead of using the encodeProc (my name for the callback).

Ian @ un4seen

Quote from: soundgals on 12 Mar '24 - 14:00You mentioned earlier that I still need to use BASS_ChannelGetData as well as the encoder using BASS_Encode_FLAC_Start with the callback function.

The encoder is fed data as the source produces it, and decoding channels (with BASS_STREAM_DECODE set) produce data when BASS_ChannelGetData is called. You can discard the returned PCM data, as you're only interested in the FLAC data received by the ENCODEPROCEX callback. As you're not keeping the data, there's no need to use a large buffer in the BASS_ChannelGetData call or to advance it (remove "done").

Quote from: soundgals on 12 Mar '24 - 14:00Hopefully you'll be able to spot something obvious I'm doing wrong.

Looking at your encodeProc function above, I do see a couple of problems. Firstly, the data/datasize/dataspace variables need to be outside of the function, so that they aren't reset in every call. If you want, you can put them in a class/struct and pass an instance of that to the callback via the "user" parameter. Secondly, it isn't retaining the previous data when allocating more memory. Google suggests that Swift does support the "realloc" function, so you can hopefully simply use that to expand the buffer (like in the code I posted), otherwise you will need to copy the data from the old buffer (data) to the enlarged one (newdata) and then free the old buffer.

soundgals

Thanks Ian, that all makes sense. Will attempt those changes tomorrow.

soundgals

I believe I've made some progress based on your advice. There are quite a few things I'm still not sure about though. I'll start with the obvious...

1/ How do I obtain the encoded flac data once the callback function has done its thing?
2/ Should I be able to use this flac encoded data as a playable file by simply adding the data to a new url, or does the flac data need a header, as WAV does, before it can be recognised as a playable file?

The callback proc seems to be doing its thing; but only when I keep the while loop that repeatedly calls get BASS_ChannelGetData. You said I would no longer need the large buffer and can discard the PCM data and remove the "done". If I do that though the callback proc doesn't get started.

Here's the while loop in question...

while (done < len) {
                let got = BASS_ChannelGetData(DWORD(stream), buffer + UnsafeMutableRawPointer.Stride(done), 0xFFFFFFF)
                if (Int(got) == -1) { // error/end
                        len = done
                        break
                    }
               
                done += UInt64(got)
                if ((BASS_Encode_IsActive(stream) == 0)){
                    print("Error: The encoder died!")
                    break
                }
            }

Here's the code I'm using to set up the encoder from within my function that calls BASS_Encode_Flac_Start...

let data = UnsafeMutablePointer<UInt8>.allocate(capacity: capacity)
        let datasize: UInt32 = 0
        let dataspace: UInt32 = 0
        let userData = procVars(data: data, datasize: datasize, dataspace: dataspace)
        var encoder:HENCODE?
        // Convert your struct to a pointer
            withUnsafePointer(to: userData) { pointer in
                // Cast to `UnsafeRawPointer` if needed
                let rawPointer = UnsafeMutableRawPointer(mutating: pointer)
                // Pass the pointer to the C function
                encoder = BASS_Encode_FLAC_Start(stream, nil,  DWORD(BASS_ENCODE_PCM | BASS_ENCODE_FP_24BIT | BASS_ENCODE_AUTOFREE), encodeProc, rawPointer)
            }
        if encoder == 0{
            let error = BASS_ErrorGetCode()
            print("Can't start the encoder", error)

Here's the callback proc...

func encodeProc(handle: HENCODE, channel: DWORD, buffer: UnsafeRawPointer?, length: DWORD, offset: QWORD, user: UnsafeMutableRawPointer?) ->Void{
    // Cast the pointer back to struct type
    if let userPointer = user {
        var userData = userPointer.assumingMemoryBound(to: procVars.self).pointee
        //  access struct properties
        let allocextra: UInt32 = 0x100000
        if userData.dataspace < offset + QWORD(length) { // need more memory
            var newdata: UnsafeMutablePointer<UInt8>?
            newdata = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(UInt32(offset) + length + allocextra))
            if newdata == nil{
                return // failed
            }
            let length = userData.datasize
            // Copy the data
            memcpy(userData.data, newdata, Int(length))
            userData.data?.deallocate()
            userData.data = newdata
            userData.dataspace = UInt32(offset) + length + allocextra
        }
        memcpy(userData.data! + Int(offset), buffer, Int(length)) // copy the data to memory
        if userData.datasize < offset + QWORD(length) { userData.datasize = UInt32(offset) + length } // update the size
    }
}

... and finally the struct with the variables outside if it...

struct procVars {
    var data: UnsafeMutablePointer<UInt8>?
    var datasize: UInt32
    var dataspace: UInt32
   
    init(data:UnsafeMutablePointer<UInt8>, datasize:UInt32, dataspace: UInt32){
        self.data = nil
        self.datasize = 0
        self.dataspace = 0
    }
}

Again, hopefully you'll be able to spot my obvious errors.

Ian @ un4seen

Quote from: soundgals on 13 Mar '24 - 16:421/ How do I obtain the encoded flac data once the callback function has done its thing?
2/ Should I be able to use this flac encoded data as a playable file by simply adding the data to a new url, or does the flac data need a header, as WAV does, before it can be recognised as a playable file?

The FLAC data is in the memory that the callback function is allocating for it, ie. there's "datasize" bytes of data in the "data" array. It includes headers and there's no need to add anything to make it playable. Make sure you call BASS_Encode_Stop at the end to finalize the encoder output (eg. update headers).

Quote from: soundgals on 13 Mar '24 - 16:42The callback proc seems to be doing its thing; but only when I keep the while loop that repeatedly calls get BASS_ChannelGetData. You said I would no longer need the large buffer and can discard the PCM data and remove the "done". If I do that though the callback proc doesn't get started.

Here's the while loop in question...

while (done < len) {
...

Change that to "while (true) {" so that it just keeps going until the end.

soundgals

Thanks Ian; but there must still be something wrong with my code.

If I replace while (done < len) with while (true) the while loop never exits, even though there is a check on whether BASS_Encode is still active...

if ((BASS_Encode_IsActive(stream) == 0)){
                    print("Error: The encoder died!")
                    break
                }

I can see that datasize is filling up with values within the proc callback function. At a certain point it stops filling up with values; but my while loop keeps going.

I also still can't see how to access the flac data from within my getDSDToFlacData function.

I create this variable... var userData = procVars(data: data, datasize: datasize, dataspace: dataspace) and pass it as a pointer... withUnsafePointer(to: userData) { pointer in...var rawPointer = UnsafeMutableRawPointer(mutating: pointer) to the encoder encoder = BASS_Encode_FLAC_Start(stream, nil,  DWORD(BASS_ENCODE_PCM | BASS_ENCODE_FP_24BIT | BASS_ENCODE_AUTOFREE), encodeProc, rawPointer); but, of course the values of the userData within getDSDToFlacData stay at their initial values.

So again, how do I get the flac data from the data variable, assuming it's being correctly encoded by BASS_Encode_FLAC_Start and the callback proc?

Ian @ un4seen

Quote from: soundgals on 14 Mar '24 - 18:07If I replace while (done < len) with while (true) the while loop never exits, even though there is a check on whether BASS_Encode is still active...

if ((BASS_Encode_IsActive(stream) == 0)){
                    print("Error: The encoder died!")
                    break
                }

I can see that datasize is filling up with values within the proc callback function. At a certain point it stops filling up with values; but my while loop keeps going.

That's strange. BASS_ChannelGetData will return -1 when it's at the end, so I would expect the "if (Int(got) == -1) { // error/end" line to exit the loop. But it looks like Swift's "Int" type is 64-bit on 64-bit platforms, while BASS_ChannelGetData's return value is 32-bit, so that's probably why that line isn't doing what's expected. Try changing it to "Int32". Or you could try this:

                if (got == ~0) { // error/end

Quote from: soundgals on 14 Mar '24 - 18:07So again, how do I get the flac data from the data variable, assuming it's being correctly encoded by BASS_Encode_FLAC_Start and the callback proc?

I'm afraid I'm unable to advise on the specifics of Swift, but the "data" variable is an array, so you should be able to access it like any other array. For example, it could be written to a file like this in C/C++:

FILE *f = fopen("output.flac", "wb");
fwrite(data, datasize, 1, f);
fclose(f);

soundgals

Thanks Ian. This if (got == ~0) { // error/end did the trick as far as the while loop is concerned. It allowed me to replace while (done < len) with while true.

When I observe the datasize variable, I noticed it gets up to a certain highest value, then drops down to very low values, before the while loop ends.

So I decided to make a comparison with the function that encodes to a file (getDSDToFlacFile). That function works perfectly, and writes a playable file to the file system. At the moment, I'm grabbing the data from that file, then deleting it.

Using the same test DSF file of 3 minutes length, this produces a datasize of 52540610 bytes and a 52.5mb playable file.

Because my callback proc function seems to overshoot this and drops down to low values before it finishes, I created a check in it, as follows... if (length + UInt32(offset)) < savedSize{
                return
            }

"savedSize" being a global variable outside the function. This causes the callback function to stop at the desired number of bytes (52540610).

I also realised that the "data" variable I create within my getDSDToFlacData, also appears to include the data. Though the dataspace and datasize variables remain at zero...

var done:QWORD  = 0
        let capacity: Int = 0
        let data = UnsafeMutablePointer<UInt8>.allocate(capacity: capacity)
        let datasize: UInt32 = 0
        let dataspace: UInt32 = 0
        let userData = procVars(data: data, datasize: datasize, dataspace: dataspace)

This is, of course, converted to a rawpointer to send in the final "user" parameter in the BASS_Encode_FLAC_Start.

So it appears that I'm capturing the data; but the data is not playable.

I also tried writing a file of this data to disk. The file isn't playable either.

To try to be sure I was actually capturing the data, I created another global variable, as an instance of my struct, and copy userData to it on each iteration of the callback function.

When the while loop is finished, again this global copy of the struct appears to have the data.  It also has the datasize and dataspace values.

Again though, this data does not result in a playable file.

So there's either something wrong with the data itself, or I'm not capturing it correctly. I hope this will give you some more clues to help me finally solve this.

Ian @ un4seen

I just noticed that this code is shadowing/overriding the callback's "length" parameter (messing up the final line), and the "memcpy" parameters are the wrong way round (the destination should come first):

Quote from: soundgals on 13 Mar '24 - 16:42            let length = userData.datasize
            // Copy the data
            memcpy(userData.data, newdata, Int(length))
            userData.data?.deallocate()
            userData.data = newdata
            userData.dataspace = UInt32(offset) + length + allocextra

Try this instead:

            // Copy the data
            memcpy(newdata, userData.data, userData.datasize)
            userData.data?.deallocate()
            userData.data = newdata
            userData.dataspace = UInt32(offset) + length + allocextra

soundgals

Thanks. I knew you'd probably spot an obvious mistake of mine.

Unfortunately it doesn't make any difference. I still don't get playable file data.

I was also hoping these changes would allow me to get rid of this check...

if (length + UInt32(offset)) < savedSize{
                return
            }

... I still need it though, otherwise I start getting low values after I reach the correct datasize for the track in question.

That datasize did not change with these changes to the code. Did you expect it too?

soundgals

Some more investigations.

I'm coming to the conclusion that the data being encoded by my callback function is not valid. There is data, because the data variable is an optional in Swift. Which means I need to unwrap it before I use it. I use a force unwrap on it, which would cause a crash if the data was nil. So, I'm getting data; just not the right data, apparently.

If I remove the code that prevents the function continuing once it peaks, and write the results to output files, you can see how the datasize drops down to low values, after that peak. I still don't think that should happen. I've attached a screenshot showing the output files, with the datasize included in the name of each. The one with the largest datasize should correspond with a playable file; but it doesn't.

Finally, this peak value is not consistent with each run (obviously for the same DSF track).

The datasize is usually 52540610 for that track; but sometimes I'm getting a peak value of 52541816.

So unless you can spot any further errors in my code, and if it's correctly gathering the data, then I can only assume there are issues with BASS_Encode_FLAC_Start, when a callback function is used.

Ian @ un4seen

I tried the C/C++ callback function that I posted earlier with BASS_Encode_FLAC_Start, and the FLAC data was fine and playable at the end. So I can only guess that the Swift translation isn't quite right, but I'm not at all familiar with Swift, so unfortunately I can't say what exactly the problem is. The "datasize" value should never decrease, so if it is then that suggests to me that your "userData" stuff isn't working right. If you log those values at the start and end of the callback function, do the start values match the previous end values?

soundgals

Thanks Ian. I've always been fairly certain that there's an error in my Swift translation and that was an obvious thing I should have thought to have done. It's clear the datasize is being set back to zero on each iteration of the callback.

Start datasize 0
End Datasize 8282

Start datasize 0
End Datasize 19955

I appreciate that you're not familiar with Swift; but here is my callback again anyway, because, again, I think there must be something obvious here that I'm missing in the way I've set up the procVars Struck outside of the callback, so that the information isn't being retained on each iteration.

func encodeProc(handle: HENCODE, channel: DWORD, buffer: UnsafeRawPointer?, length: DWORD, offset: QWORD, user: UnsafeMutableRawPointer?) ->Void{
    // Cast the pointer back to struct type
    let userPointer = user
    var userData = userPointer!.assumingMemoryBound(to: procVars.self).pointee
        //  access struct properties
        let allocextra: UInt32 = 0x100000
        if userData.dataspace < offset + QWORD(length) { // need more memory
            print("Start datasize", userData.datasize)
            var newdata: UnsafeMutablePointer<UInt8>?
            newdata = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(UInt32(offset) + length + allocextra))
            if newdata == nil{
                return // failed
            }
            // Copy the data
            memcpy(newdata, userData.data, Int(userData.datasize))
            if (length + UInt32(offset)) < savedSize{
                return
            }
           userData.data?.deallocate()
           userData.data = newdata
           userData.dataspace = UInt32(offset) + length + allocextra
        }
        memcpy(userData.data! + Int(offset), buffer, Int(length)) // copy the data to memory
        if userData.datasize < offset + QWORD(length) { userData.datasize = UInt32(offset) + length } // update the size
        print("End Datasize", userData.datasize)
        audioData = Data(bytes: userData.data!, count: Int(userData.datasize))
//        collectedData = userData
        savedSize = userData.datasize
//    }
}

... and here's the struct...

struct procVars {
    var data: UnsafeMutablePointer<UInt8>?
    var datasize: UInt32
    var dataspace: UInt32
   
    init(data:UnsafeMutablePointer<UInt8>, datasize:UInt32, dataspace: UInt32){
        self.data = nil
        self.datasize = 0
        self.dataspace = 0
    }
}

Hopefully, again, you'll be able to spot something.

soundgals

Eureka!!! Finally solved it. The struct should have been a class. Now it retains the values on each iteration and the data equivalent of a playable file is formed.

Thanks for your patience and help during this.

Next, I'll turn my attention to getting WAV data from DSD and conversion to flac or WAV from other formats in data form.

For the callback to get WAV data, do I just need to remove the offset, or are there other changes I need to make to the callback?

If you could post an example for the callback to get WAV data in C code, that would be very helpful.


Ian @ un4seen

Great to see you've got it working now.

Writing a WAV file via a callback will be trickier because the ENCODEPROC's lack of "offset" parameter means it isn't possible to update the file's headers. That parameter isn't present because BASS_Encode_Start initially only supported external encoders, which don't need it. WAV support (BASS_ENCODE_PCM flag) was added later, primarily for writing directly to a file rather than a callback. BASS_Encode_StartLimit can/should be used instead, as its "limit" parameter tells how much data will be encoded, which means the correct header can be written at the start, avoiding the need to update it at the end. But perhaps the option of using an ENCODEPROCEX callback with BASS_Encode_Start can be added, which would allow you to use the same callback for FLAC and WAV. I'll look into that.

soundgals

QuoteBut perhaps the option of using an ENCODEPROCEX callback with BASS_Encode_Start can be added, which would allow you to use the same callback for FLAC and WAV. I'll look into that

If it could be done it would be very useful. Will await your update on that.

Thanks

Ian @ un4seen

Here's a BASSenc update for you to try:

   www.un4seen.com/stuff/bassenc-osx.zip

It adds the following flag to tell BASS_Encode_Start(Limit) that the "proc" parameter is an ENCODEPROCEX rather than ENCODEPROC, which will allow updated WAV (or AIFF) headers to be received at the end.

#define BASS_ENCODE_PROCEX 0x10000

Let me know if you have any trouble with it.

soundgals

Great, and that was quick! Thanks Ian. I'll let you know how it works out.

soundgals

#49
Unfortunately Ian, I haven't been able to get this to work.

Here are the steps I've taken...

1/ Replaced the original libbassenc.dylib with the one you provided yesterday.
2/ Added this line to the bassenc.h file...
    #define BASS_ENCODE_PROCEX      0x10000
    under // BASS_Encode_Start flags
3/ Added the flag "BASS_ENCODE_PROCEX" to my call to BASS_Encode_Start
4/ Changed the call to "BASS_Encode_StartLimit"
5/ Added the DWORD(limit) parameter to the end.

I used the length of the stream created from the original url for this last parameter.

I've been assuming I can now call the same callback function that I use for flac data, which includes the offset parameter.

If I do this though, xcode gives me errors indicating a mismatch between my callback function and what is expected by BASS_Encode_StartLimit.

The error text...

"Cannot convert value of type '(UInt32, UInt32, UnsafeRawPointer?, UInt32, UInt64, UnsafeMutableRawPointer?) -> Void'
 to expected argument type
'(@convention(c) (HENCODE, DWORD, UnsafeRawPointer?, DWORD, UnsafeMutableRawPointer?) -> Void)?'
(aka 'Optional<@convention(c) (UInt32, UInt32, Optional<UnsafeRawPointer>, UInt32, Optional<UnsafeMutableRawPointer>) -> ()>')"

... indicates to me that the offset parameter UInt64, is still not being expected by the call to BASS_Encode_StartLimit.

So I tried a modified callback function which removes the offset parameter. This got rid of the xCode errors; but results in a crash, indicating the pointer to my userData class is nil.

Can you spot anything I'm doing wrong or missed?

Thanks.