DSD over DoP for Multi-channel playback

Started by soundgals,

soundgals

Quote from: Ian @ un4seenYes, the DSP function needs to be set on the device, not the DoP stream (all its channels are already DoP). Please upload a new output.wav file (from BASS_Encode_Start), to check that the DSP function is working properly. Also confirm how many channels the played DSD file has.
OK. Just to make sure I'm doing this correctly. Now I will have two devStreams. 1. For the dsp function and the other to create the WAV file, correct?

soundgals

Here is a link to the new WAV. This is from a 5.1(6 Channel) dsd source.

Ian @ un4seen

From that file, it appears that the DSP function didn't do anything, as there are still 2 empty channels. Did you convert the code to Swift? If so, perhaps there's a problem with the conversion. Can you put a breakpoint in it and step through it in the debugger to find out what's happening in it? It should never hit the "break" line.

soundgals

#28
Quote from: Ian @ un4seenFrom that file, it appears that the DSP function didn't do anything, as there are still 2 empty channels. Did you convert the code to Swift? If so, perhaps there's a problem with the conversion. Can you put a breakpoint in it and step through it in the debugger to find out what's happening in it? It should never hit the "break" line.
Yes I had to convert it to Swift. I'll try debugging it though xCode usually catches any obvious errors in the conversion.

I was just wondering if I need to play the channel twice. Once for the DSP to take effect and the second time for the encoder to create the WAV file. At the moment I'm only calling BASS_ChannelPlay(stream, 1), just before starting the encoder.

Here's the converted code in case you can see anything wrong with it...

let dsdSilenceDSP: @convention(c) (HDSP, DWORD, UnsafeMutableRawPointer?, DWORD, UnsafeMutableRawPointer?) -> Void = { handle, channel, buffer, length, user in
        // Cast buffer to float pointer
        let fbuffer = buffer?.bindMemory(to: Float.self, capacity: Int(length / 4))
        let sampleCount = Int(length) / MemoryLayout<Float>.size
       
        var dop: UInt32 = 0
       
        for a in 0..<sampleCount {
            guard let fbuffer = fbuffer else { break }
           
            let s = UInt32(bitPattern: Int32(fbuffer[a] * 16777216.0)) // get sample as UInt32
           
            // Check for DoP markers
            if (s & 0xffff0000) == 0x05000000 ||
               (s & 0xffff0000) == 0xfffa0000 ||
               (s & 0xffff0000) == 0xffaa0000 {
                dop = s & 0xffff0000 // retain the marker
            } else if s == 0 && dop != 0 { // empty sample with DoP context
                let newSample = dop | 0x6969 // DoP marker + DSD silence value
                fbuffer[a] = Float(Int32(bitPattern: newSample)) / 16777216.0 // replace empty sample
            } else if s != 0 && (s & 0xffff0000) != dop {
                break // not DoP data, stop processing
            }
        }
    }

 let devstream = BASS_GetDevice()
                if devstream == 0{
                    let error = BASS_ErrorGetCode()
                    print("Can't create stream", error)
                }
                BASS_ChannelSetDSP(devstream, dsdSilenceDSP, nil, 0)
                var channelInfo = BASS_CHANNELINFO()
                BASS_ChannelGetInfo(stream, &channelInfo)
                // try to change the device's rate
                print("Channels: \(channelInfo.chans)")
                if (info.freq != channelInfo.freq){ // the rates don't match
                    BASS_Init(device, channelInfo.freq, DWORD(BASS_DEVICE_FREQ | BASS_DEVICE_REINIT), nil, nil)
                }
                var devstreamTwo: HSTREAM = 0
                // Define the STREAMPROC function type to match what BASS expects
                typealias STREAMPROC = @convention(c) (HSTREAM, UnsafeMutableRawPointer?, DWORD, UnsafeMutableRawPointer?) -> DWORD
   
                // Create STREAMPROC_DEVICE by casting the special value -2
                let STREAMPROC_DEVICE = unsafeBitCast(-2, to: STREAMPROC?.self)
   
                // Create the device stream
                devstreamTwo = BASS_StreamCreate(0, 0, 0, STREAMPROC_DEVICE, nil)
   
                if devstreamTwo == 0{
                    let error = BASS_ErrorGetCode()
                    print("Can't create stream", error)
                }
   
   
                let musicUrl = FileManager.default.urls(for: .musicDirectory, in: .userDomainMask).first!
                // BASS requires the "file://" prefix to be removed from the url in string format
                let destinationUrl = musicUrl.appendingPathComponent("output.wav").absoluteString.replacingOccurrences(of: "file://", with: "")
                BASS_ChannelPlay(stream, 1)
                let encoder = BASS_Encode_Start(devstreamTwo, destinationUrl,  DWORD(BASS_ENCODE_PCM | BASS_ENCODE_FP_24BIT | BASS_ENCODE_AUTOFREE), nil, nil)
   
                if encoder == 0{
                    let error = BASS_ErrorGetCode()
                    print("Can't start the encoder", error)
                }
                do{
                    sleep(8)
                }
                BASS_Encode_Stop(devstreamTwo)

soundgals

I'll re=post that in a code block...

let dsdSilenceDSP: @convention(c) (HDSP, DWORD, UnsafeMutableRawPointer?, DWORD, UnsafeMutableRawPointer?) -> Void = { handle, channel, buffer, length, user in
        // Cast buffer to float pointer
        let fbuffer = buffer?.bindMemory(to: Float.self, capacity: Int(length / 4))
        let sampleCount = Int(length) / MemoryLayout<Float>.size
       
        var dop: UInt32 = 0
       
        for a in 0..<sampleCount {
            guard let fbuffer = fbuffer else { break }
           
            let s = UInt32(bitPattern: Int32(fbuffer[a] * 16777216.0)) // get sample as UInt32
           
            // Check for DoP markers
            if (s & 0xffff0000) == 0x05000000 ||
               (s & 0xffff0000) == 0xfffa0000 ||
               (s & 0xffff0000) == 0xffaa0000 {
                dop = s & 0xffff0000 // retain the marker
            } else if s == 0 && dop != 0 { // empty sample with DoP context
                let newSample = dop | 0x6969 // DoP marker + DSD silence value
                fbuffer[a] = Float(Int32(bitPattern: newSample)) / 16777216.0 // replace empty sample
            } else if s != 0 && (s & 0xffff0000) != dop {
                break // not DoP data, stop processing
            }
        }
    }

} else if dsdOverDop{
            stream = BASS_DSD_StreamCreateURL(theURL, 0, DWORD(BASS_DSD_DOP | BASS_SAMPLE_FLOAT | BASS_STREAM_STATUS), nil, nil, 0)
            BASS_ChannelSetAttribute(stream, DWORD(BASS_ATTRIB_NORAMP), 1)
            if stream == 0{
                let error = BASS_ErrorGetCode()
                print("Can't create stream", error)
            } else {
                let devstream = BASS_GetDevice()
                if devstream == 0{
                    let error = BASS_ErrorGetCode()
                    print("Can't create stream", error)
                }
                BASS_ChannelSetDSP(devstream, dsdSilenceDSP, nil, 0)
                var channelInfo = BASS_CHANNELINFO()
                BASS_ChannelGetInfo(stream, &channelInfo)
                // try to change the device's rate
                print("Channels: \(channelInfo.chans)")
                if (info.freq != channelInfo.freq){ // the rates don't match
                    BASS_Init(device, channelInfo.freq, DWORD(BASS_DEVICE_FREQ | BASS_DEVICE_REINIT), nil, nil)
                }
                var devstreamTwo: HSTREAM = 0
                // Define the STREAMPROC function type to match what BASS expects
                typealias STREAMPROC = @convention(c) (HSTREAM, UnsafeMutableRawPointer?, DWORD, UnsafeMutableRawPointer?) -> DWORD
   
                // Create STREAMPROC_DEVICE by casting the special value -2
                let STREAMPROC_DEVICE = unsafeBitCast(-2, to: STREAMPROC?.self)
   
                // Create the device stream
                devstreamTwo = BASS_StreamCreate(0, 0, 0, STREAMPROC_DEVICE, nil)
   
                if devstreamTwo == 0{
                    let error = BASS_ErrorGetCode()
                    print("Can't create stream", error)
                }
   
   
                let musicUrl = FileManager.default.urls(for: .musicDirectory, in: .userDomainMask).first!
                // BASS requires the "file://" prefix to be removed from the url in string format
                let destinationUrl = musicUrl.appendingPathComponent("output.wav").absoluteString.replacingOccurrences(of: "file://", with: "")
                BASS_ChannelPlay(stream, 1)
                let encoder = BASS_Encode_Start(devstreamTwo, destinationUrl,  DWORD(BASS_ENCODE_PCM | BASS_ENCODE_FP_24BIT | BASS_ENCODE_AUTOFREE), nil, nil)
   
                if encoder == 0{
                    let error = BASS_ErrorGetCode()
                    print("Can't start the encoder", error)
                }
                do{
                    sleep(8)
                }
                BASS_Encode_Stop(devstreamTwo)
            }
           
           
        }

Ian @ un4seen

The BASS_ChannelSetDSP call should be on the device stream, just like the BASS_Encode_Start call:

devstream = BASS_StreamCreate(0, 0, 0, STREAMPROC_DEVICE, nil)
BASS_ChannelSetDSP(devstream, dsdSilenceDSP, nil, 0)
let encoder = BASS_Encode_Start(devstream, destinationUrl,  DWORD(BASS_ENCODE_PCM | BASS_ENCODE_FP_24BIT | BASS_ENCODE_AUTOFREE), nil, nil)

If it still doesn't work, please upload the new output.wav file.

soundgals

Quote from: Ian @ un4seenThe BASS_ChannelSetDSP call should be on the device stream, just like the BASS_Encode_Start call:

devstream = BASS_StreamCreate(0, 0, 0, STREAMPROC_DEVICE, nil)
BASS_ChannelSetDSP(devstream, dsdSilenceDSP, nil, 0)
let encoder = BASS_Encode_Start(devstream, destinationUrl,  DWORD(BASS_ENCODE_PCM | BASS_ENCODE_FP_24BIT | BASS_ENCODE_AUTOFREE), nil, nil)

If it still doesn't work, please upload the new output.wav file.

OK, thanks. I'll correct the code and try again.

soundgals

Quote from: soundgals
Quote from: Ian @ un4seenThe BASS_ChannelSetDSP call should be on the device stream, just like the BASS_Encode_Start call:

devstream = BASS_StreamCreate(0, 0, 0, STREAMPROC_DEVICE, nil)
BASS_ChannelSetDSP(devstream, dsdSilenceDSP, nil, 0)
let encoder = BASS_Encode_Start(devstream, destinationUrl,  DWORD(BASS_ENCODE_PCM | BASS_ENCODE_FP_24BIT | BASS_ENCODE_AUTOFREE), nil, nil)

If it still doesn't work, please upload the new output.wav file.

OK, thanks. I'll correct the code and try again.

Apologies as I have only just had the time to re-test this. After correcting my code as you advised I re-ran the test. Unfortunately the result is the same. So here is the output.wav file.

Ian @ un4seen

The 2 extra channels are still empty in that file, so it looks like the DSP function either isn't being called or isn't working properly. I do see a problem in your Swift conversion here:

            if (s & 0xffff0000) == 0x05000000 ||

That should be this:

            if (s & 0xffff0000) == 0x50000 ||

If it still isn't working, please set a breakpoint in the dsdSilenceDSP function and then step through the code to see if "dop" gets set.

soundgals

Quote from: Ian @ un4seenThe 2 extra channels are still empty in that file, so it looks like the DSP function either isn't being called or isn't working properly. I do see a problem in your Swift conversion here:

            if (s & 0xffff0000) == 0x05000000 ||

Thanks for pointing that out Ian. I'll correct the code once more and try again as soon as I can.

That should be this:

            if (s & 0xffff0000) == 0x50000 ||

If it still isn't working, please set a breakpoint in the dsdSilenceDSP function and then step through the code to see if "dop" gets set.

soundgals

I've made that change Ian; but can you also take a look at the rest of the checks for DoP markers in case there are any other mistakes?

if (s & 0xffff0000) == 0x50000 ||
                   (s & 0xffff0000) == 0xfffa0000 ||
                   (s & 0xffff0000) == 0xffaa0000 {
                    dop = s & 0xffff0000 // retain the marker
                } else if s == 0 && dop != 0 { // empty sample with DoP context
                    let newSample = dop | 0x6969 // DoP marker + DSD silence value
                    fbuffer[a] = Float(Int32(bitPattern: newSample)) / 16777216.0 // replace empty sample
                } else if s != 0 && (s & 0xffff0000) != dop {
                    break // not DoP data, stop processing
                }

Thanks.

Ian @ un4seen

The DoP marker checks look correct now. Does the "dop = s & 0xffff0000" line get hit? If it's still not working, are you sure the "bitPattern" stuff is necessary or that you need to use "UInt32" instead of "Int32" in places?

soundgals

Quote from: Ian @ un4seenThe DoP marker checks look correct now. Does the "dop = s & 0xffff0000" line get hit? If it's still not working, are you sure the "bitPattern" stuff is necessary or that you need to use "UInt32" instead of "Int32" in places?
Thanks again. I believe these are necessary for conversion to Swift.

soundgals

I'm very sorry it's taken me so long to perform this test again. I've been travelling a lot on business.

I repeated the test today after those final corrections and the result is the same. Here is a link to the output.wav file.

For me it's really not a big deal if multi-channel DoP isn't supported by BASS on MacOS. I will just need to point this out as a limitation in my app's documentation.

I doubt if many of my users have multi-channel systems that would be affected by this.

Since RAW/Native DSD is not supported by BASS though, the only option is via DoP. Multi-channel DSD does exist, of course, either from ripped SACDs or, increasingly, as downloads from sites like NativeDSD.com. So this is something you may want to look into.


Ian @ un4seen

Yep, there are still those 2 empty channels in the output.wav file, so the DSP function (dsdSilenceDSP) is still having no effect. Have you confirmed that the dsdSilenceDSP function is actually getting called? I would suggest placing a breakpoint at the start of it and then stepping through it in the debugger to see what's happening in there (eg. is the "retain the marker" line getting hit).

soundgals

This Ian in the following code...
if (s & 0xffff0000) == 0x50000 ||
                   (s & 0xffff0000) == 0xfffa0000 ||
                   (s & 0xffff0000) == 0xffaa0000 {
                    dop = s & 0xffff0000 // retain the marker
                } else if s == 0 && dop != 0 { // empty sample with DoP context
                    let newSample = dop | 0x6969 // DoP marker + DSD silence value
                    fbuffer[a] = Float(Int32(bitPattern: newSample)) / 16777216.0 // replace empty sample
                } else if s != 0 && (s & 0xffff0000) != dop {
                    break // not DoP data, stop processing
                }

... The if statement is entered; but the debugger never reaches the line... dop = s & 0xffff0000 // retain the marker...
So I guess there is still something wrong with this code?

Ian @ un4seen

What is the value of the "s" variable at that point?

soundgals

S can have different numeric values such as 698022.

The code sometimes falls through to this line... } else if s != 0 && (s & 0xffff0000) != dop {
                    break // not DoP data, stop processing
                }... and falls out of the for loop. Is it to be expected that some of the data is not recognised as DoP?


Ian @ un4seen

When you're playing a DoP stream (with BASS_ATTRIB_NORAMP=1), the DSP function should always detect it and hit the "retain the marker" line. At other times, it'll hit the "not DoP data, stop processing" line.

Are you sure you should be using "bitPattern" here?

Quote from: soundgals            let s = UInt32(bitPattern: Int32(fbuffer[a] * 16777216.0)) // get sample as UInt32
...
                    fbuffer[a] = Float(Int32(bitPattern: newSample)) / 16777216.0 // replace empty sample

What about if you change it to this?

            let s = Int32(fbuffer[a] * 16777216.0) // get sample as Int32
...
                    fbuffer[a] = Float(newSample) / 16777216.0 // replace empty sample

soundgals

It's the only way I could get prevent xCode errors. If I change those values to signed (Int32s) I get the errors... Integer literal '4294574080' overflows when stored into signed 'Builtin.Int32'

soundgals

My apologies Ian as I just realised I've been missing the obvious. I'm getting an error code 33 when trying to start the devStream encoder. So no wonder it's not working. I don't know how to resolve the error though. Here is the Swift code up to that point...
if (info.freq != channelInfo.freq){ // the rates don't match
                   BASS_Init(device, channelInfo.freq, DWORD(BASS_DEVICE_FREQ | BASS_DEVICE_REINIT), nil, nil)
               }
               var devstream: HSTREAM = 0
               // Define the STREAMPROC function type to match what BASS expects
               typealias STREAMPROC = @convention(c) (HSTREAM, UnsafeMutableRawPointer?, DWORD, UnsafeMutableRawPointer?) -> DWORD
 
               // Create STREAMPROC_DEVICE by casting the special value -2
               let STREAMPROC_DEVICE = unsafeBitCast(-2, to: STREAMPROC?.self)
 
               // Create the device stream
               devstream = BASS_StreamCreate(0, 0, 0, STREAMPROC_DEVICE, nil)
               BASS_ChannelSetDSP(devstream, dsdSilenceDSP, nil, 0)
               if devstream == 0{
                   let error = BASS_ErrorGetCode()
                   print("Can't create stream", error)
               } else {
                   let musicUrl = FileManager.default.urls(for: .musicDirectory, in: .userDomainMask).first!
                   // BASS requires the "file://" prefix to be removed from the url in string format
                   let destinationUrl = musicUrl.appendingPathComponent("output.wav").absoluteString.replacingOccurrences(of: "file://", with: "")
                   BASS_ChannelPlay(stream, 1)
                   let encoder = BASS_Encode_Start(devstream, destinationUrl,  DWORD(BASS_ENCODE_PCM | BASS_ENCODE_FP_24BIT | BASS_ENCODE_AUTOFREE), nil, nil)
                   
                   if encoder == 0{
                       let error = BASS_ErrorGetCode()
                       print("Can't start the encoder", error)
                   }
                   do{
                       sleep(8)
                   }
                   BASS_Encode_Stop(devstream)

Ian @ un4seen

Quote from: soundgalsMy apologies Ian as I just realised I've been missing the obvious. I'm getting an error code 33 when trying to start the devStream encoder. So no wonder it's not working. I don't know how to resolve the error though.

Error code 33 is BASS_ERROR_CREATE, which means BASS_Encode_Start was unable to create the output file. Did you recently change the "destinationUrl" value in that call? It was definitely working at some point because you got the output.wav files from it.

Anyway, I think I now see what the problem is with the DSP function. Where I wrote 16777216, that should be 8388608 (in both cases). Please try correcting that and see if it works then.

soundgals

Thanks Ian. It's looking better now. The trouble is I was trying to test on a different system and I forgot to change the permissions for the folder I was trying to write too. I can also confirm that now the "retain the marker" line is getting hit in the "dsdSilenceDSP" code.

I won't be able to actually hear whether multichannel DoP is working on this system. So please take a look at this new output.wav and check if the channels without content are now being silenced.


Ian @ un4seen

That output.wav file is looking good. Hopefully it'll sound good too!

soundgals

Great thanks. I don't have a multi-channel system where I am now; but I can try to let it down-mix to stereo. When DoP for multi-channel wasn't working the front left and right channels were silent.

My final question on this; is it really necessary to set the volume on all channels to max for DoP multi-channel to work? On my own system I attenuate the centre and lfe channel volumes using the DACs volume control. If this is not possible I will have to apply that attenuation via BASS somehow and provide an interface in my app for that.