Author Topic: Looping when loop point is not zeroed  (Read 662 times)

DanielB91

  • Guest
Looping when loop point is not zeroed
« on: 8 Mar '17 - 12:17 »
As we all know, if the start and end of a loop section are not zeroed, an audible click will be heard. I've found using log fade of around 3ms at either end cures this entirely.  But I made around 50 musics before I learned to do it properly.  vgmstream seems to add a custom fade to smooth the transition, because I get no clicking or popping with my imperfect loops.  Bass is not forgiving, however.

Any chance of adding in such a feature?  Or is there a way I can programatically add one? I tried to add a 3ms fade (with BASS_ChannelIsSliding), but the delay needed simply creates an audible delay and doesn't seem to work regardless.

Ian @ un4seen

  • Administrator
  • Posts: 20336
Re: Looping when loop point is not zeroed
« Reply #1 on: 8 Mar '17 - 17:53 »
You could try using the BASS_FX add-on's BASS_FX_BFX_VOLUME_ENV effect for that, ie. set a volume envelope that fades-in and out. You can use a mixtime BASS_SYNC_POS sync to trigger that just before the loop end is reached. It could look something like this:

Code: [Select]
fadefx = BASS_ChannelSetFX(channel, BASS_FX_BFX_VOLUME_ENV, 0); // setup volume envelope effect for fade-in/out
QWORD syncpos = BASS_ChannelSeconds2Bytes(channel, loopend - fadelen); // get byte position to start fade-out at
BASS_ChannelSetSync(channel, BASS_SYNC_POS|BASS_SYNC_MIXTIME, syncpos, VolFadeSyncProc, 0); // set a mixtime sync there

...

void CALLBACK VolFadeSyncProc(HSYNC handle, DWORD channel, DWORD data, void *user)
{
BASS_BFX_ENV_NODE env[3];
// full volume at start
env[0].pos = 0;
env[0].vol = 1;
// silent at end of fade-out
env[1].pos = BASS_ChannelSeconds2Bytes(channel, ramplen);
env[1].vol = 0;
// back to full volume at end of fade-in
env[2].pos = env[1].pos + BASS_ChannelSeconds2Bytes(channel, ramplen);
env[2].vol = 1;
BASS_BFX_VOLUME_ENV fxparam = {BASS_BFX_CHANALL, 3, env, false};
BASS_FXSetParameters(fadefx, &param); // apply the envelope
}

Please see the documentation for details on the aforementioned functions. Also note that BASS_FX will need to be loaded before you can use any effects from it. You can force it to be loaded by calling a function from it, eg. BASS_FX_GetVersion during initialization.

Daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #2 on: 8 Mar '17 - 23:02 »
I'll try this.  I already knew about mixtime and syncs, and I also use bass_fx.  So this won't be a learning curve.  ;D

Daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #3 on: 8 Mar '17 - 23:03 »
Thanks a lot.

Daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #4 on: 12 Mar '17 - 03:25 »
OK, tried that.  Your idea is a good one (but Vol should be val), but I don't think bass is good enough for those kinds of timings.  When we are talking about times like 5-10ms fades, I don't think it can accurately do it.  A lot of the time, the sync won't even be fired 5ms from the end of file.

I am not sure how bass works in terms of timing, but if it's using timegettime instead of QPC then that would explain it.

Daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #5 on: 12 Mar '17 - 21:09 »
Yeah, this method is definitely out.  Not only the timing precision issues - especially of the envelope itself (5ms envelopes simply don't work properly), but the fact we are messing about with envelopes AND a sync over the end of file to loopstart means that it's totally unreliable.

The only way this is being fixed is if bass.dll adds a hard coded feature that adds a 3ms log fade out and fade in when using a BASS_SYNC_END. I am going to redo the oggs I made properly this time.

Ian @ un4seen

  • Administrator
  • Posts: 20336
Re: Looping when loop point is not zeroed
« Reply #6 on: 13 Mar '17 - 16:19 »
OK, tried that.  Your idea is a good one (but Vol should be val), but I don't think bass is good enough for those kinds of timings.  When we are talking about times like 5-10ms fades, I don't think it can accurately do it.  A lot of the time, the sync won't even be fired 5ms from the end of file.

I am not sure how bass works in terms of timing, but if it's using timegettime instead of QPC then that would explain it.

When using the BASS_SYNC_MIXTIME flag, the SYNCPROC will be called exactly when the decoder reaches the specified position. If the sync isn't being triggered sometimes, then that sounds like the initial file length estimate (returned by BASS_ChannelGetLength) was incorrect. What is the file format? If it's MP3, you can use the BASS_STREAM_PRESCAN flag in the BASS_StreamCreateFile call to have it pre-scan the file for the correct length.

Note that your BASS_SYNC_END sync should also have the BASS_SYNC_MIXTIME flag set for seamless custom looping. If you don't use that flag, there is likely to be a small gap in the loop.

Daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #7 on: 13 Mar '17 - 21:32 »
I haven't made any mistake. This is 100% the fault of timing in conjunction with sync end.  It simply isn't going to work this way. There is no way to add a 3-5ms fade out to the end - and a fade in on the loop start. The interval is too small to manage with syncs properly. They were never designed for this usage to begin with - especially not 0.003 of a second. The loop point transition fade will have to be a feature hard coded into bass to stand any chance at all.

It's no good increasing the interval either, because then you become aware of the fade itself.


Ian @ un4seen

  • Administrator
  • Posts: 20336
Re: Looping when loop point is not zeroed
« Reply #8 on: 14 Mar '17 - 14:39 »
Custom looping is actually an intended usage of "mixtime" syncs (a little demonstration can be found in the CUSTLOOP example) because they are called exactly when the decoder reaches the requested position. So the sync timing won't be a problem as long as the requested position is correct. You can confirm the decoder's position is as requested by calling BASS_ChannelGetPosition with the BASS_POS_DECODE flag within the SYNCPROC callback function.

If the loop end position is the end of the file, then you can modify the code I posted above like this:

Code: [Select]
QWORD syncpos = BASS_ChannelGetLength(channel, BASS_POS_BYTE) - BASS_ChannelSeconds2Bytes(channel, fadelen); // get byte position to start fade-out at

For completeness, you would handle the looping like this:

Code: [Select]
BASS_ChannelSetSync(channel, BASS_SYNC_END|BASS_SYNC_MIXTIME, 0, EndSyncProc, 0); // set a mixtime END sync

...

void CALLBACK EndSyncProc(HSYNC handle, DWORD channel, DWORD data, void *user)
{
BASS_ChannelSetPosition(channel, loopstart, BASS_POS_BYTE); // seek to loop start position (in bytes)
}

Daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #9 on: 15 Mar '17 - 04:49 »
I did this.  I did a load of variations of this too.  Try it with an ogg file that has a click/pop at the loop point, and loops from the end, and you will see that the loop will not transition right.  You will still hear the clicking and popping until you increase the fade out-in to the point that the fade is too large and becomes audible.


Ian @ un4seen

  • Administrator
  • Posts: 20336
Re: Looping when loop point is not zeroed
« Reply #10 on: 15 Mar '17 - 17:51 »
I think a better way to handle an imperfect loop is to crossfade the end and start, rather than fading out and back in. That is a bit more complicated, but not impossibly so. You will need to pre-decode the loop end data, so that you can crossfade it with the loop start data. That means using a "decoding channel", ie. the BASS_STREAM_DECODE flag. Something like this:

Code: [Select]
float *enddata;
int xfadelen, xfadepos;

...

decoder = BASS_StreamCreateFile(false, filename, 0, 0, BASS_STREAM_DECODE|BASS_STREAM_PRESCAN|BASS_SAMPLE_FLOAT); // create decoder for same file
BASS_ChannelSetPosition(decoder, syncpos, BASS_POS_BYTE); // seek to start of fade-out (same as sync pos from before)
xfadelen = BASS_ChannelSeconds2Bytes(channel, fadelen) / sizeof(float); // crossfade length in samples
enddata = new float[xfadelen); // allocate buffer for end data to fade
BASS_ChannelGetData(decoder, enddata, xfadelen * sizeof(float)); // get the data
BASS_StreamFree(decoder); // free the decoder (could keep it around if you will need data from other positions too)

You can then use a DSP function to crossfade the data, which would be triggered by the BASS_SYNC_POS sync from before. It could look something like this:

Code: [Select]
void CALLBACK VolFadeSyncProc(HSYNC handle, DWORD channel, DWORD data, void *user)
{
BASS_ChannelRemoveDSP(channel, xfadedsp); // just in case
xfadedsp = BASS_ChannelSetDSP(channel, CrossFadeDspProc, 0, 0); // apply the crossfade DSP
xfadepos = 0; // reset the crossfade position
BASS_ChannelSetPosition(channel, loopstart - (xfadelen * sizeof(float)), BASS_POS_BYTE); // seek to loop start position minus crossfade length (in bytes)
}

...

void CALLBACK CrossFadeDspProc(HDSP handle, DWORD channel, void *buffer, DWORD length, void *user)
{
float *d = (float*)buffer;
length /= sizeof(float);
for (int a = 0; a < length; a++) {
float x = (float)xfadepos / xfadelen; // normalized crossfade position
d[a] += (1 - x) * (enddata[xfadepos] - d[a]); // crossfade
xfadepos++; // advance the crossfade position
if (xfadepos == xfadelen) { // done
BASS_ChannelRemoveDSP(channel, handle); // remove the DSP
break;
}
}
}

Note your playback stream should also be floating-point (use BASS_SAMPLE_FLOAT). If the file format is MP3, you will also need to use the BASS_STREAM_PRESCAN flag for an accurate length and seeking. You can avoid scanning the file twice by using BASS_ChannelGetAttributeEx to get and set the BASS_ATTRIB_SCANINFO attribute; see the BASS_ATTRIB_SCANINFO documentation for an example.

Daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #11 on: 15 Mar '17 - 23:21 »
Thanks for that!  :o  Nice work.

One other thing...  I call bass from a thread in my own dll. Once bass is initialized, it has its own thread, yeah?  Not the one it was called from.

Ian @ un4seen

  • Administrator
  • Posts: 20336
Re: Looping when loop point is not zeroed
« Reply #12 on: 16 Mar '17 - 16:01 »
Yes, the decoding (and mixtime syncs) during playback takes place in a thread created by BASS. See the BASS_CONFIG_UPDATETHREADS documentation for more info.

Daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #13 on: 17 Mar '17 - 11:05 »
That doesn't appear to be true.  When the thread that called bass is closed, bass closes with it. I call it from a separate thread inside my dll ddraw.dll (which is loaded into a game).

I then loop a "while do" with a sleep (30) to keep that thread open for other things.  But bass is called from it before the while do loop.  If I remove the loop, the thread dies - and so does bass functionality. If it were using its own thread, once initialized, it wouldn't need the calling thread?

This may also explain why there is a timing issue with fades - it could be that my sleep for 30ms is actually affecting bass itself - making it have a maximum resolution of only 30ms in certain cases?

Daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #14 on: 17 Mar '17 - 11:10 »
Forget that.  It was my error.  It is doing as you said.  I'll check the code you provided, but at this point it's looking like I am better off just not being lazy and redoing the oggs.

Thanks for all your help!

Daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #15 on: 17 Apr '17 - 02:06 »
I did some more testing.  Basically, VGMStream is definitely looping with some in-built crossfade. Even when I have loops that are virtually perfect, bass dll will click if there isn't an absolute zero on both sides, which I think is way too harsh. I'd use vgmstream rather than bass for my purposes, but there doesn't seem to be any release or documentation for Delphi - which I use.

Perhaps consider adding a crossfade loop feature to bass.dll, as it is definitely a very nice feature to have. It's even more of a task to loop audio when the loopend is literally the end of a file, since then there has to be a 5ms fade out at that end... and a 5ms fade in AND out at the loopstart, or the loopstart will click on the first loop. Linear fade outs.

daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #16 on: 17 Apr '17 - 09:18 »
Well last night drove me mad, but I am pleased to be able to say that I tracked the problem down.  Bass wasnt looping right regardless of where the loop point was to be honest.  After a ton of testing and pulling my hair out, this is what I discovered:

BStream := BASS_StreamCreateFile(False, PChar('c:\newsfx\' +   Listbox1.Items[listbox1.ItemIndex] ), 0, 0, BASS_ASYNCFILE  or BASS_STREAM_DECODE);
BStream := BASS_FX_TempoCreate(BStream, 0);

When bass_FX is used, it is interrupting the loop sync. Somehow. Perhaps Bass_Stream_Decode does not work well with the loops?  Or bass_fx itself?

When I change the above to simply:

BStream := BASS_StreamCreateFile(False, PChar('c:\newsfx\' +   Listbox1.Items[listbox1.ItemIndex] ), 0, 0, BASS_ASYNCFILE);

The clicking goes away. The loop is perfect.

daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #17 on: 17 Apr '17 - 09:28 »

daniel91

  • Guest
It was all my fault.
« Reply #18 on: 17 Apr '17 - 09:49 »
So... we've come full circle.  What a colossal waste of time and mistake.  ;D  I do apologize.  I had to come full circle and do everything wrong to finally get it right!  Somewhere online, I copied the code I saw, and it was wrong.  You can't do:

Code: [Select]
BStream := BASS_StreamCreateFile(False, PChar('c:\newsfx\' +   Listbox1.Items[listbox1.ItemIndex] ), 0, 0, BASS_ASYNCFILE or BASS_STREAM_DECODE);
BStream := BASS_FX_TempoCreate(BStream, BASS_FX_FREESOURCE);
When looping.

It has to use a new handle

Code: [Select]
BStream := BASS_StreamCreateFile(False, PChar('c:\newsfx\' +   Listbox1.Items[listbox1.ItemIndex] ), 0, 0, BASS_ASYNCFILE or BASS_STREAM_DECODE);
BStreamFX := BASS_FX_TempoCreate(BStream, BASS_FX_FREESOURCE);


Then you set to play with BStreamFX.   :-[ :-[ :-[

Ian @ un4seen

  • Administrator
  • Posts: 20336
Re: Looping when loop point is not zeroed
« Reply #19 on: 17 Apr '17 - 16:20 »
It should actually be fine to replace the original handle with the handle from BASS_FX_TempoCreate, and you can then use BASS_FX_TempoGetSource whenever you need to access the original handle. But using 2 handle variables is fine too :)

Daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #20 on: 17 Apr '17 - 22:03 »
Interesting.  Still, as far as I can see, the loop is affected when using tempo without a new handle. The loop itself has to use the original handle, though - while all other things seem to be better with the tempo handle.  Have you got an example of how you'd do it with tempo as in my example to make sure I have this right?


Daniel91

  • Guest
Re: Looping when loop point is not zeroed
« Reply #21 on: 18 Apr '17 - 13:01 »
OK I think I understand you, but in the interests of efficiency and neater source, it's probably better that I  Just add another handle. In any case, I notice that all references to the stream have to be the fx stream (such as transitions) or there are side effects or it won't work at all.  That's cool, I understand that.  But why does the sync loop still require the original stream handle to loop properly?

Ian @ un4seen

  • Administrator
  • Posts: 20336
Re: Looping when loop point is not zeroed
« Reply #22 on: 18 Apr '17 - 13:32 »
Tempo processing will change the amount of data (eg. higher tempo = less data), so I think it would indeed be best to apply the looping/crossfading stuff to the source, ie. it's applied before the tempo processing is.