DSD over DoP for Multi-channel playback

Started by soundgals,

Ian @ un4seen

#20
Quote from: soundgalsDoP playback only works for stereo when I first set the device to 2 channels.

That suggests to me that the device will only switch to DSD mode if all channels contain DoP data, ie. the empty channels are preventing it. You could try using a DSP function on the device to fill the empty channels with DoP/DSD silence, like this:

BASS_ChannelSetDSP(devstream, DSDSilenceDSP, 0, 0); // set DSP function on device stream

void CALLBACK DSDSilenceDSP(HDSP handle, DWORD channel, void *buffer, DWORD length, void *user)
{
    float *fbuffer = (float*)buffer; // floating-point data
    int dop = 0;
    for (int a = 0; a < length / sizeof(float); a++) {
        int s = fbuffer[a] * 8388608.f; // get sample
        if ((s & 0xffff0000) == 0x50000 || (s & 0xffff0000) == 0xfffa0000 || (s & 0xffff0000) == 0xffaa0000)) { // a DoP marker
            dop = s & 0xffff0000; // retain it
        } else if (!s && dop) { // empty
            s = dop | 0x6969; // DoP marker + DSD silence value
            fbuffer[a] = s / 8388608.f; // replace empty sample
        } else
            break; // not DoP data
    }
}

soundgals

#21
Quote from: Ian @ un4seen
Quote from: soundgalsDoP playback only works for stereo when I first set the device to 2 channels.

That suggests to me that the device will only switch to DSD mode if all channels contain DoP data, ie. the empty channels are preventing it. You could try using a DSP function on the device to fill the empty channels with DoP/DSD silence, like this:

BASS_ChannelSetDSP(devstream, DSDSilenceDSP, 0, 0); // set DSP function on device stream

void CALLBACK DSDSilenceDSP(HDSP handle, DWORD channel, void *buffer, DWORD length, void *user)
{
    float *fbuffer = (float*)buffer; // floating-point data
    int dop = 0;
    for (int a = 0; a < length / sizeof(float); a++) {
        int s = fbuffer[a] * 16777216.f; // get sample
        if ((s & 0xffff0000) == 0x50000 || (s & 0xffff0000) == 0xfffa0000 || (s & 0xffff0000) == 0xffaa0000)) { // a DoP marker
            dop = s & 0xffff0000; // retain it
        } else if (!s && dop) { // empty
            s = dop | 0x6969; // DoP marker + DSD silence value
            fbuffer[a] = s / 16777216.f; // replace empty sample
        } else
            break; // not DoP data
    }
}

I've noticed some 5.1 channel files actually have nothing on the .1 (LFE) channel. How will that effect this? If the channel exists in the original file is that enough, or does there actually need to be some content?

Ian @ un4seen

If a channel exists (even if it's just silence) in the DSD file then BASSDSD (with BASS_DSD_DOP set) will convert it to DoP, and nothing more needs to be done for it. The DSP function above would only process channels that the DSD file doesn't have.

soundgals

OK thanks. I applied dsdSilenceDSP to the devStream making sure that all channels were set to 1.0 and that NORAMP is set, and unfortunately the result is the same; just noise.

Should I definitely be applying it to the devStream, not the stream created by BASS_DSD_StreamCreateURL?

Ian @ un4seen

Yes, 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.

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).