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"  :)