How to convert samplerate real-time in mixer

Started by MartinV,

MartinV

I created a mixing program designed for audio files with a sample rate of 44100 Hz. The program mixes like a DJ with a turntable. I'd like to keep it that way. The pitch is adjusted by changing the sample rate as follows:

Songs()\Samplerate = 44100 * (PlaybackBPM / Songs()\BPM)

It works perfectly, but now that I want to expand the program to support audio files with different sample rates, I'm running into problems. I've tried everything for days, but without succes. Even using ChatGPT doesn't work.

With this procedure I resample to 44100 hz:

Procedure LoadAndResampleTo44100(FilePath.s, Song.i))
  Protected.i ResampleMixer

  Mix(Song)\Stream = CreateChannel(FilePath, #BASS_STREAM_DECODE|#BASS_ASYNCFILE)
  ResampleMixer = BASS_Mixer_StreamCreate(44100, 2, #BASS_STREAM_DECODE|#BASS_MIXER_NONSTOP|#BASS_MIXER_RESUME)
  BASS_Mixer_StreamAddChannel(ResampleMixer, Mix(Song)\Stream, #BASS_MIXER_DOWNMIX|#BASS_MIXER_NORAMPIN) 
  Mix(Song)\Channel = BASS_FX_TempoCreate(ResampleMixer, #BASS_STREAM_DECODE|#BASS_FX_FREESOURCE)
EndProcedure

This seems to work, but I can't change the position of Mix(Song)\Channel, which is the intention because I want to be able to fast forward through the song with a trackbar. And this sync will then no longer work properly:

BASS_Mixer_ChannelSetSync(Mix(CurrentSong)\Channel, #BASS_SYNC_POS|#BASS_SYNC_MIXTIME, StartTime2Bytes, @StartTransition(), Mix(NextSong)\Samplerate) 

Then I tried it another way, without LoadAndResampleTo44100, but by changing the samplerate (pitch) like this:

Songs()\Samplerate = Songs()\OrigFreq * (PlaybackBPM / Songs()\BPM)

Works fine, but then my beat matching stops working properly (see BeatSynchronizer Procedure below):

I tried adjusting the Samplerate * (BeatLength1 / BeatLength) formula in various ways, but the beats were always slightly out of sync.


Does anyone have any ideas on how I can fix this?

Here's a snippet of code from the original, working program (designed for audio files with a sample rate of 44100 Hz):
Procedure BeatSynchronizer(Stream.i, Channel.i, BassData.i, Samplerate.i) 
  Protected.s Item1 = StringField(Mix(NextSong)\IntroBeatList, BeatCounter + 1, Chr(10))
  Protected.s Item2 = StringField(Mix(NextSong)\IntroBeatList, BeatCounter + 2, Chr(10)) 
  Protected.s Item3 = StringField(Mix(CurrentSong)\BreakBeatList, BeatCounter + 1, Chr(10))
  Protected.s Item4 = StringField(Mix(CurrentSong)\BreakBeatList, BeatCounter + 2, Chr(10)) 
  Protected.f BeatLength = ValD(Item4) - ValD(Item3)
  Protected.f BeatLength1 = ValD(Item2) - ValD(Item1)
  Protected.f NextSyncSec = Mix(CurrentSong)\BreakStart + (BeatLength * (BeatCounter + 1))
  Protected.q SyncPosBytes = BASS_ChannelSeconds2Bytes(Mix(CurrentSong)\Channel, NextSyncSec)
 
  BASS_ChannelSetAttribute(Mix(NextSong)\Channel, #BASS_ATTRIB_FREQ, Samplerate * (BeatLength1 / BeatLength))
 
  If BeatCounter < 30   
    BASS_Mixer_ChannelSetSync(Mix(CurrentSong)\Channel, #BASS_SYNC_POS|#BASS_SYNC_MIXTIME, SyncPosBytes, @BeatSynchronizer(), Samplerate)   
    BeatCounter + 1   
  EndIf   
EndProcedure

Procedure StartTransition(Handle.i, Channel.i, BassData.i, Samplerate.i) 
  Protected.q NextPosBytes = BASS_ChannelSeconds2Bytes(Mix(NextSong)\Channel, Mix(NextSong)\IntroStart)
  BASS_ChannelSetPosition(Mix(NextSong)\Channel, NextPosBytes, #BASS_POS_BYTE) 
  BASS_ChannelSetAttribute(Mix(NextSong)\Channel, #BASS_ATTRIB_FREQ, Samplerate)       
  BASS_Mixer_StreamAddChannel(Mixer, Mix(NextSong)\Channel, #BASS_STREAM_AUTOFREE|#BASS_MIXER_NORAMPIN)     
EndProcedure

Mix(CurrentSong)\Channel = BASS_StreamCreateFile(0, @FileFullPath1, 0, 0, #BASS_ASYNCFILE|#BASS_STREAM_PRESCAN|#BASS_UNICODE|#BASS_STREAM_DECODE)             
BASS_ChannelSetAttribute(Mix(CurrentSong)\Channel, #BASS_ATTRIB_FREQ, Mix(CurrentSong)\Samplerate)
BASS_Mixer_StreamAddChannel(Mixer, Mix(CurrentSong)\Channel, #BASS_STREAM_AUTOFREE|#BASS_MIXER_NORAMPIN)         

 
Mix(NextSong)\Channel = BASS_StreamCreateFile(0, @FileFullPath2, 0, 0, #BASS_ASYNCFILE|#BASS_STREAM_PRESCAN|#BASS_UNICODE|#BASS_STREAM_DECODE)     
BASS_ChannelSetAttribute(Mix(NextSong)\Channel, #BASS_ATTRIB_FREQ, Mix(NextSong)\Samplerate)       
 
BeatCounter = 0       
StartTime2Bytes = BASS_ChannelSeconds2Bytes(Mix(CurrentSong)\Channel, Mix(CurrentSong)\BreakStart) 
BASS_Mixer_ChannelSetSync(Mix(CurrentSong)\Channel, #BASS_SYNC_POS|#BASS_SYNC_MIXTIME, StartTime2Bytes, @StartTransition(), Mix(NextSong)\Samplerate)   
BASS_Mixer_ChannelSetSync(Mix(CurrentSong)\Channel, #BASS_SYNC_POS|#BASS_SYNC_MIXTIME, StartTime2Bytes, @BeatSynchronizer(), Mix(CurrentSong)\Samplerate) 




Ian @ un4seen

It looks like you're playing all of the files through the same mixer? If so, that will resample everything to its sample rate (when necessary), so there should be no need for additional mixers just for resampling.

When you want a source to start at a certain time in the mix, the mixtime BASS_SYNC_POS sync should be set on the mixer. It looks like you're currently setting the sync on a source instead, so that could explain the timing issue you're having. Note that the sync position will then be based on the mixer's sample format (not the source's). For example, this code would need to be changed:

StartTime2Bytes = BASS_ChannelSeconds2Bytes(Mix(CurrentSong)\Channel, Mix(CurrentSong)\BreakStart)
BASS_Mixer_ChannelSetSync(Mix(CurrentSong)\Channel, #BASS_SYNC_POS|#BASS_SYNC_MIXTIME, StartTime2Bytes, @StartTransition(), Mix(NextSong)\Samplerate) 
BASS_Mixer_ChannelSetSync(Mix(CurrentSong)\Channel, #BASS_SYNC_POS|#BASS_SYNC_MIXTIME, StartTime2Bytes, @BeatSynchronizer(), Mix(CurrentSong)\Samplerate)

To something like this:

StartTime2Bytes = CurrentSong_StartPos + BASS_ChannelSeconds2Bytes(Mixer, CurrentSong_BreakStart); // get mixer position for next song
BASS_ChannelSetSync(Mixer, BASS_SYNC_POS | BASS_SYNC_MIXTIME | BASS_SYNC_ONETIME, StartTime2Bytes, StartTransition, 0); // set sync there

BASS_SYNC_ONETIME flag added because the syncs are only expected to be called once, and BeatSynchronizer sync removed because the StartTransition sync can also trigger that stuff (no need for 2 syncs at the same position). CurrentSong_StartPos is the mixer's position at the start of the current song, which the StartTransition function updates at the start of each song:

void CALLBACK StartTransition(HSYNC handle, DWORD channel, DWORD data, void *user)
{
    CurrentSong_StartPos = BASS_ChannelGetPosition(channel, BASS_POS_DECODE); // get mixer position
...

martin71

Thanks for your explanation and corrections, Ian. However, I didn't succeed with the LoadAndResampleTo44100 procedure. I wanted to be able to fast-forward the stream, not the mixer. But that's okay, because I've decided to stop using this procedure and adjust the pitch with this formula:
Samplerate = OrigFreq * PlaybackBPM / OrigBPM
The beat matching works fine now, since I found the correct formula:
FreqRatio.f = (BeatLength1 / BeatLength) * (OrigFreq_NextSong / OrigFreq_CurrentSong) BASS_ChannelSetAttribute(Channel_NextSong, #BASS_ATTRIB_FREQ, Samplerate * FreqRatio)
Thank you, by the way, for helping everyone on this forum for so many years. That's truly special! Without you, we'd be nowhere...