The problem is due to a tiny delay between the end position being heard and your sync being called, and then a little more delay while your sync changes the position. The sync latency is generally a fraction of a millisecond, which is perfectly good for syncing visuals, but is plenty enough to hear it in what you're trying to do.
The solution is to use a decoding channel and custom stream. That way you have full control over what's played. For example...
HSTREAM stream2loop; // the decoding channel
HSTREAM stream2play; // the custom stream
QWORD loopstart;
DWORD CALLBACK StreamProc(HSTREAM handle, BYTE *buffer, DWORD length, DWORD user)
{
int count=length;
while (count) {
int r=BASS_ChannelGetData(stream2loop,buffer,count);
if (r==-1) // it's ended, go to start of loop
BASS_ChannelSetPosition(stream2loop,loopstart);
else {
buffer+=r;
count-=r;
}
}
return length;
}
...
// create the decoding channel
stream2loop=BASS_StreamCreateFile(FALSE,"a_file",0,0,BASS_STREAM_DECODE);
// create a custom stream with the same sample format
DWORD flags,freq;
BASS_ChannelGetAttributes(stream2loop,&freq,NULL,NULL);
flags=BASS_ChannelGetFlags(stream2loop)&~BASS_STREAM_DECODE;
stream2play=BASS_StreamCreate(freq,flags,(STREAMPROC*)&StreamProc,0);
loopstart=some_place_to_start_looping;
BASS_StreamPlay(stream2play,0,0);