I have implemented sample-rate conversion (SRC) from scratch in my hobby music player, and it worked fine. However, originally, the music player loaded entire audio files into memory and performed the sample-rate conversion on the entire audio signal during loading. I've now changed this to instead stream the audio file chunk by chunk (to reduce memory footprint), and tried to port my SRC implementation.
But I'm encountering a problem where it I hear clicks between each chunk when they're played back. Increasing the size of each chunk of the audio file I load at a time increases the duration between the clicks, so that's how I'm pretty confident that they appear between chunks. Other than these artifacts, the SRC works/sounds fine.
I'm wondering why this happens? My theory is that when performing SRC on the entire audio signal in one go (initial version) each sample in the upsampled signal was filtered using the next N samples in the upsampled signal (where N is the length of my filter). However, in this new version, the last few samples of each chunk are filtered with fewer and fewer samples (the filter length basically shrinks) as the next chunk isn't available yet (this also happened in the original version when reaching the end of the entire audio signal). This then creates a discontinuity when playing back the next chunk as the final filtered samples don't take the next chunk's samples into account. Does that make sense, and if so, do I solve this by simply ensuring this doesn't happen?
Edit To add a bit more info:
I'm using an FIR filter, that is a Sinc function with a Hamming window of length 64, and I apply that to each of the samples in my upsampled (zero-stuffed) signal. Then I decimate the signal afterwards
Here's how I perform the filtering currently:
byte_t* upsampled_audio_data = .. // Zero-stuffed upsampled signal
int32_t L = ... // Upsampling factor
int32_t M = ... // Decimation factor
for (uint8_t channel = 0; channel < audio_loader_data->channel_count; channel++)
{
int32_t channel_offset = sample_count_upsampled * channel;
for (int32_t i = 0; i < sample_count_upsampled; i++)
{
int32_t current_sample_index = channel_offset + i;
// This is where I shrink the filter if there aren't enough remaining samples to use
int32_t filter_length_for_sample = min(filter_length, sample_count_upsampled - i);
// j_offset is used to skip zero-stuffed samples as they are 0 anyway
int32_t j_offset = L - (i % L);
float filtered_sample = 0.0f;
// Assumes 16 bit per sample for now
for (int32_t j = j_offset; j < filter_length_for_sample; j += L)
{
filtered_sample += (float)((int16_t*)upsampled_audio_data)[current_sample_index + j] * filter[j];
}
((int16_t*)upsampled_audio_data_filtered)[current_sample_index] = filtered_sample;
}
}