How to write a Wav file quickly with "BassEnc"?

Started by Phil75,

Phil75

Hello,

I programmed with BASS a "MIDI Player".
I use a VSTi instrument to generate the audio (BassVst.BASS_VST_ChannelCreate).
I use "BassAsioHandler" for ASIO.
I use the "BassEnc.BASS_Encode_Start" function to record the played Midi file as ".Wav".
Everything works fine.
But the recording is done in "real time".
Is it possible to render faster, like the software "Cockos REAPER" (Render Full-Speed Offline) does, for example?
And how can I do this?

Ian @ un4seen

To write the WAV file as quickly as possible, you need to add the BASS_STREAM_DECODE flag to the stream (in the BASS_VST_ChannelCreate call) and then repeatedly call BASS_ChannelGetData to process it (instead of calling BASS_ChannelPlay to play it). Normally you would do that until you reach the end (when BASS_ChannelGetData fails), but I don't think VSTi have an end so I'm not sure how you will decide when to stop? I suppose you will just have to stop after a certain amount. You may find BASS_ChannelSeconds2Bytes helpful for getting a byte amount from a time duration.

Phil75

Thank you for all this advice and explanations  :)

I did a test with a VSTi but it doesn't work.
Probably because I don't understand what I'm doing when I use "BASS_ChannelGetData".
16 bits, 32 bits, signed, unsigned, stereo...Data() of Byte ? Short ? Integer ?? I'm lost...

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim WW As WaveWriter
        Dim length As Integer

        Bass.BASS_Init(-1, 44100, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero)
        BassAsio.BASS_ASIO_Init(0, BASSASIOInit.BASS_ASIO_THREAD)

        hVSTi = Un4seen.Bass.AddOn.Vst.BassVst.BASS_VST_ChannelCreate(44100, 2, "C:\Program Files\VSTPlugins\4Front Piano x64.dll", BASSFlag.BASS_STREAM_DECODE)

        WW = New WaveWriter("test.wav", hVSTi, True)

        BassVst.BASS_VST_ProcessEvent(hVSTi, 0, BASSMIDIEvent.MIDI_EVENT_NOTE, Utils.MakeWord(60, 100))

        length = CInt(Bass.BASS_ChannelSeconds2Bytes(hVSTi, 1))
        Dim data(length / 4 - 1) As Short
        length = Bass.BASS_ChannelGetData(hVSTi, data, length)
        If length > 0 Then WW.Write(data, length)
        WW.Close()

        BassVst.BASS_VST_ChannelFree(hVSTi)
        BassAsio.BASS_ASIO_Free()
        Bass.BASS_Free()

    End Sub

Capture1.jpg

Phil75

I replaced

Dim data(length / 4 - 1) As Short
with

Dim data(length / 2 - 1) As Short
and it seems to work.
But I don't understand why  ???

Ian @ un4seen

Dividing by 2 is correct because a "Short" is 2 bytes (16 bits). See here:

  https://learn.microsoft.com/en-us/dotnet/visual-basic/language-reference/data-types/

By the way, you won't need ASIO for file writing, so you can remove that stuff.

Phil75

With some VSTis it works very well.
It's very fast.
But with some VSTis, the created ".Wav" file contains only zeros.
But the size of the Wav file corresponds to the duration I requested with "BASS_ChannelGetData".
Sometimes there is only a small part. Like in the screenshot.
Since this almost always works, the problem must be related to the VSTi.

Capture1.jpg

Ian @ un4seen

Perhaps the VSTi doesn't like processing a large block of data at once. You could try breaking it down into smaller blocks. For example, ten 0.1s blocks instead of one 1.0s block:

length = CInt(Bass.BASS_ChannelSeconds2Bytes(hVSTi, 0.1))
Dim data(length / 4 - 1) As Short
For index As Integer = 1 To 10
    Dim got As Integer = Bass.BASS_ChannelGetData(hVSTi, data, length)
    If got > 0 Then WW.Write(data, got)
Next

Phil75

With this change, VSTis that were sending zero data will still send zero data.
But the VSTi that were "cut" now work much better.
For some VSTis i need to set "Bass.BASS_ChannelSeconds2Bytes" to 10ms.
And there are no more missing parts.
Thank you for your help  :)

Phil75

I have one last question.
I want to apply a "Normalization".
I created a "Push" Stream:

NewStream = Bass.BASS_StreamCreatePush(44100, 2, BASSFlag.BASS_STREAM_DECODE, IntPtr.Zero)
and I feed this Stream "Push" with the "Bass.BASS_StreamPutData" function, with the data coming from the VSTi.
But before using the "BASS_ChannelSetFX" function to apply the "Normalization", I use the "BASS_ChannelGetLevels" function.
(I haven't written the code for "Normalization" yet.)
But the "BASS_ChannelGetLevels" function seems to move the playback position in the Stream.
So before saving the "Wav" file, I use the "BASS_ChannelSetPosition" function.

If I use:

Dim p As Double = 0
Bass.BASS_ChannelSetPosition(NewStream, p)

or

Dim p As Long = 0
Bass.BASS_ChannelSetPosition(NewStream, p)

the Wav file is empty.

If I use:

Dim p As Long = 4
It seems to work but I'm not sure if the file is not truncated at the beginning.

I took a screenshot (notok.jpg) where I only use :

...BASS_StreamPutData...

Dim k() As Single = Bass.BASS_ChannelGetLevels(NewStream, 1.0F, BASSLevel.BASS_LEVEL_MONO)
Dim p As Long = 4
Bass.BASS_ChannelSetPosition(NewStream, p)

...WaveWriter.Write...

and another screenshot (ok.jpg) where I removed these lines.

ok.jpg : 00:00:16,000 (705 600 samples)                       

notok.jpg : 00:00:15,000 (661 500 samples)

notok.jpg

ok.jpg

Ian @ un4seen

Yes, BASS_ChannelGetLevels will be taking data out of the push stream (advancing its position) because it has the BASS_STREAM_DECODE flag set (there's no playback buffer), and that data is gone so seeking back to it isn't possible. BASS_ChannelSetPosition with pos=0 will actually just empty the push stream's buffer (and BASS_ChannelSetPosition with pos=4 will fail).

Does the VSTi stream have the BASS_STREAM_DECODE flag set too? If so, perhaps you could call BASS_ChannelGetLevels (instead of BASS_ChannelGetData) on that and remove the push stream? BASS_ChannelGetLevels uses BASS_ChannelGetData internally to get the data to measure the level of.

Phil75

Quote from: Ian @ un4seenYes, BASS_ChannelGetLevels will be taking data out of the push stream (advancing its position) because it has the BASS_STREAM_DECODE flag set (there's no playback buffer), and that data is gone so seeking back to it isn't possible. BASS_ChannelSetPosition with pos=0 will actually just empty the push stream's buffer (and BASS_ChannelSetPosition with pos=4 will fail).

Does the VSTi stream have the BASS_STREAM_DECODE flag set too? If so, perhaps you could call BASS_ChannelGetLevels (instead of BASS_ChannelGetData) on that and remove the push stream? BASS_ChannelGetLevels uses BASS_ChannelGetData internally to get the data to measure the level of.

Thanks for the reply.
I'm not sure I understand what to do.

In the loop, the datas is in the "Data" array.

length = CInt(Bass.BASS_ChannelSeconds2Bytes(hVSTi, 0.1))
Dim data(length / 4 - 1) As Short
For index As Integer = 1 To 10
    Dim got As Integer = Bass.BASS_ChannelGetData(hVSTi, data, length)
    ...simple code to get the level...
Next

Is it possible to determine the level from the "Data" array with a simple code?
(stereo, 16-bit values)

Phil75

I kept my "Push" Stream, and added this code to normalize at -6db :

Dim LevelPeak As Integer
                Dim Level As Integer
                Dim LevelLeft As Short
                Dim LevelRight As Short
                Dim VolumeParam As New BASS_BFX_VOLUME
                Dim VolumeFX As Integer

                l = CInt(Bass.BASS_ChannelSeconds2Bytes(hVSTi, 0.01))
                ReDim Data(l / 2)

                For j = 1 To 10
                    l = Bass.BASS_ChannelGetData(hVSTi, Data, l)
                    Level = Un4seen.Bass.Utils.GetLevel(Data, 2, -1, -1)
                    LevelLeft = Un4seen.Bass.Utils.LowWord(Level)
                    LevelRight = Un4seen.Bass.Utils.HighWord(Level)
                    If LevelLeft > LevelPeak Then LevelPeak = LevelLeft
                    If LevelRight > LevelPeak Then LevelPeak = LevelRight
                    If l > 0 Then Bass.BASS_StreamPutData(NewStream, Data, l)
                Next j

                VolumeParam.fVolume = 0.5F / (LevelPeak / 32768)
                VolumeParam.lChannel = BASSFXChan.BASS_BFX_CHANALL
                VolumeFX = Bass.BASS_ChannelSetFX(NewStream, BASSFXType.BASS_FX_BFX_VOLUME, 0)
                Bass.BASS_FXSetParameters(VolumeFX, VolumeParam)

I think we can improve this code, but it seems to work.

Ian @ un4seen

Quote from: Phil75I'm not sure I understand what to do.

In the loop, the datas is in the "Data" array.

length = CInt(Bass.BASS_ChannelSeconds2Bytes(hVSTi, 0.1))
Dim data(length / 4 - 1) As Short
For index As Integer = 1 To 10
    Dim got As Integer = Bass.BASS_ChannelGetData(hVSTi, data, length)
    ...simple code to get the level...
Next

Is it possible to determine the level from the "Data" array with a simple code?

When you only want to get the level of the data, you can simply use BASS_ChannelGetLevels instead of BASS_ChannelGetData. Something like this:

Dim LevelPeak As Single = 0
For index As Integer = 1 To 10
    Dim k() As Single = Bass.BASS_ChannelGetLevels(hVSTi, 0.1F, BASSLevel.BASS_LEVEL_MONO)
    if LevelPeak < k(0) Then LevelPeak = k(0)
Next

You can also use the BASS_ATTRIB_VOLDSP attribute instead of BASS_FX_BFX_VOLUME to apply the volume change more simply:

If LevelPeak > 0 Then Bass.BASS_ChannelSetAttribute(hVSTi, BASSAttribute.BASS_ATTRIB_VOLDSP, 0.5F / LevelPeak)

Phil75

Thanks for all this info, and for "BASS_ATTRIB_VOLDSP"  :)

Phil75

Hello,

Since the documentation explains that it is better to use "BASSFlag.BASS_SAMPLE_FLOAT", I modified these lines of code:

hVSTi = Un4seen.Bass.AddOn.Vst.BassVst.BASS_VST_ChannelCreate(44100, 2, VSTifilename, BASSFlag.BASS_STREAM_DECODE Or BASSFlag.BASS_SAMPLE_FLOAT)
...
BassVst.BASS_VST_ProcessEvent(hVSTi, 0, BASSMIDIEvent.MIDI_EVENT_NOTE, Utils.MakeWord(60, 100))
...
hNewStream = Bass.BASS_StreamCreatePush(44100, 2, BASSFlag.BASS_STREAM_DECODE Or BASSFlag.BASS_SAMPLE_FLOAT, IntPtr.Zero)
...
Level = Un4seen.Bass.Utils.GetLevel(Datas, 2, -1, -1)
LevelLeft = Un4seen.Bass.Utils.LowWord(Level)
LevelRight = Un4seen.Bass.Utils.HighWord(Level)
If LevelLeft > LevelPeak Then LevelPeak = LevelLeft
If LevelRight > LevelPeak Then LevelPeak = LevelRight
If l > 0 Then Bass.BASS_StreamPutData(hNewStream, Datas, l)
...
VolParam.fVolume = 1.0F / (LevelPeak / 32768)
VolParam.lChannel = BASSFXChan.BASS_BFX_CHANALL
VolFX = Bass.BASS_ChannelSetFX(hNewStream, BASSFXType.BASS_FX_BFX_VOLUME, 0)
Bass.BASS_FXSetParameters(VolFX, VolParam)
...
hEncoder = BassEnc.BASS_Encode_Start(hNewStream, FileName, BASSEncode.BASS_ENCODE_PCM Or BASSEncode.BASS_ENCODE_FP_16BIT, Nothing, IntPtr.Zero)
Do
    Buffer = New Byte(20000) {}
    l = Bass.BASS_ChannelGetData(hNewStream, Buffer, 20000)
    If l = 0 Then Exit Do
Loop
BassEnc.BASS_Encode_Stop(hNewStream)

The file is created and looks correct but without normalization.
Before the change it worked fine.

Phil75

I changed :

Dim Datas() As Short
to

Dim Datas() As Single
and it seems to work.
But I'm not very sure of myself.

Ian @ un4seen

The BASS_SAMPLE_FLOAT flag will mean the stream's sample data is floating-point, and so you should indeed use a "Single" (instead of "Short) array to receive and process the data.

Phil75

Thanks for the reply.

I'm reading the documentation but I'm still having a lot of trouble understanding how to manage data with the "ChannelGetData" function.

For example, I never understand how I should size the array.
I wrote a few lines of code to try to understand.
The code creates one Wav file from another.

        hOldStream = Bass.BASS_StreamCreateFile(FileNameWav, 0, 0, BASSFlag.BASS_STREAM_DECODE Or BASSFlag.BASS_SAMPLE_FLOAT)
        hNewStream = Bass.BASS_StreamCreatePush(44100, 2, BASSFlag.BASS_STREAM_DECODE Or BASSFlag.BASS_SAMPLE_FLOAT, IntPtr.Zero)

        l = CInt(Bass.BASS_ChannelSeconds2Bytes(hOldStream, 0.1))
        Dim Datas() As Single
        While Bass.BASS_ChannelIsActive(hOldStream) = BASSActive.BASS_ACTIVE_PLAYING
            Array.Resize(Datas, l / 2)
            r = Bass.BASS_ChannelGetData(hOldStream, Datas, l)
            If r > 0 Then Bass.BASS_StreamPutData(hNewStream, Datas, l)
        End While

        Dim hEncoder As Integer
        Dim EncoderBuffer As Byte()
        hEncoder = BassEnc.BASS_Encode_Start(hNewStream, "output.wav", BASSEncode.BASS_ENCODE_PCM Or BASSEncode.BASS_ENCODE_FP_16BIT, Nothing, IntPtr.Zero)
        Do
            EncoderBuffer = New Byte(19999) {}
            l = Bass.BASS_ChannelGetData(hNewStream, EncoderBuffer, 20000)
        Loop Until l = 0
        BassEnc.BASS_Encode_Stop(hNewStream)
        Bass.BASS_StreamFree(hEncoder)
        Bass.BASS_StreamFree(hNewStream)
        Bass.BASS_StreamFree(hOldStream)

If I replace "Array.Resize(Datas, l / 2)" with "Array.Resize(Datas, l / 4)" the size of the file created is the same, no difference.

Ian @ un4seen

If you won't be accessing the data in the array then it doesn't really matter what type of array you use with BASS_ChannelGetData, eg. you can simply use "Byte" and no dividing (like in your "EncoderBuffer" case). Note you should be using the return value (which tells how much data you got) in the BASS_StreamPutData call. Something like this:

        Dim Datas(l - 1) As Byte
        While Bass.BASS_ChannelIsActive(hOldStream) = BASSActive.BASS_ACTIVE_PLAYING
            r = Bass.BASS_ChannelGetData(hOldStream, Datas, l)
            If r > 0 Then Bass.BASS_StreamPutData(hNewStream, Datas, r)
        End While

But there isn't really any need for "hNewStream" in the code above and you could instead set the encoder directly on "hOldStream", like this:

        hOldStream = Bass.BASS_StreamCreateFile(FileNameWav, 0, 0, BASSFlag.BASS_STREAM_DECODE Or BASSFlag.BASS_SAMPLE_FLOAT)

        Dim hEncoder As Integer
        Dim EncoderBuffer(19999) As Byte
        hEncoder = BassEnc.BASS_Encode_Start(hOldStream, "output.wav", BASSEncode.BASS_ENCODE_PCM Or BASSEncode.BASS_ENCODE_FP_16BIT, Nothing, IntPtr.Zero)
        Do
            l = Bass.BASS_ChannelGetData(hOldStream, EncoderBuffer, 20000)
        Loop Until l < 0
        BassEnc.BASS_Encode_Stop(hEncoder)
        Bass.BASS_StreamFree(hOldStream)

Phil75

I had used "BASS_StreamPutData" to understand how "BASS_ChannelGetData" data arrays worked.

Thanks for all the advice  :)