Slight delay in looping audio files using AudioSource


#1

Hey
I’m using the AudioSource class and a bunch of it’s child classes to create a system that simply allows audio files to be played and looped. However I’ve noticed that after a file has looped a large number of times it has slightly drifted out of time - from testing it it seems like there is a very slight delay added between each loop of the file.

Should this be expected? Any way to get around this?

I’ve posted a snippet of the necessary code below:


//
//  AudioFilePlayer.h
//  MyProj
//

#ifndef H_AUDIOFILEPLAYER
#define H_AUDIOFILEPLAYER

#include "../JuceLibraryCode/JuceHeader.h"

class AudioFilePlayer : public AudioSource
{
public:
    AudioFilePlayer(); 
    ~AudioFilePlayer();
    
    //AudioSource Functions
	void prepareToPlay (int samplesPerBlockExpected,double sampleRate);
	void releaseResources();
	void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill);
    
    void playAudioFile();
    void stopAudioFile();
    
    void setAudioFile (File audioFile_);

private:
	
	//audio related
	AudioTransportSource fileSource; 
	File currentFile;
       AudioFormatReaderSource* currentAudioFileSource;
    
};

#endif





//
//  AudioFilePlayer.cpp
//  MyProj
//


#include "AudioFilePlayer.h"

AudioFilePlayer::AudioFilePlayer()
{
        currentAudioFileSource = NULL;
	AudioFormatManager formatManager;
	formatManager.registerBasicFormats();
}

AudioFilePlayer::~AudioFilePlayer()
{
    fileSource.setSource(0);//unload the current file
	deleteAndZero(currentAudioFileSource);//delete the current file
}



 

void AudioFilePlayer::setAudioFile (File audioFile_)
{
    //passes in pads audio file
	File audioFile (audioFile_);
    
    // unload the previous file source and delete it..
    fileSource.stop();
    fileSource.setPosition(0.0);
    fileSource.setSource (0);
    deleteAndZero (currentAudioFileSource);
    
    // create a new file source from the file..
    // get a format manager and set it up with the basic types (wav, ogg and aiff).
    AudioFormatManager formatManager;
    formatManager.registerBasicFormats();
    
    AudioFormatReader* reader = formatManager.createReaderFor (audioFile);
    
    if (reader != 0)
    {
        currentFile = audioFile;
        currentAudioFileSource = new AudioFormatReaderSource (reader, true);
        
        // ..and plug it into our transport source
        fileSource.setSource (currentAudioFileSource,
                              32768, // tells it to buffer this many samples ahead
                              reader->sampleRate);
    }

}


void AudioFilePlayer::playAudioFile()
{
    if (currentFile != File::nonexistent && currentAudioFileSource != NULL)
    {
        //set to whether the audio should loop, set from the playstate functions
        currentAudioFileSource->setLooping(playStateData.shouldLoop);
    }
    
    fileSource.setPosition (0.0);
    fileSource.start();
   
}

void AudioFilePlayer::stopAudioFile()
{
    fileSource.stop();
}


void AudioFilePlayer::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)
{
    fileSource.getNextAudioBlock(bufferToFill);
}

void AudioFilePlayer::prepareToPlay (int samplesPerBlockExpected,double sampleRate)
{
    fileSource.prepareToPlay(samplesPerBlockExpected, sampleRate);
}

void AudioFilePlayer::releaseResources()
{
    fileSource.releaseResources();
}




Audio Playback Delay Bug?
#2

Jumping back to the start of the file on disk won’t take the same amount of time each time as it’s dependent on many things (disk load, cpu load etc).


#3

Have you tried passing it through a BufferingAudioSource?

Btw, code-style wise, you should try to use ScopedPointer instead of a raw pointer.


#4

After some tinkering I found that changing the readAheadBufferSize parameter value of the AudioTransportSource::setSource function to 0 solved the problem.
In what situations would I need this parameter to have a value greater that 0?


#5

So after a lot of testing I’ve noticed that if I set the readAheadBufferSize to anything above 0 it creates very small ‘random’ delays between the ending point and re-starting point of an audio file when it is set to loop.

I’ve tested this by creating two AudioSource objects (the code is above, which I believe is pretty much identical to that of the Juce demo), giving them identical audio files, and then setting them to play at the same time. After the files re-loop a phaser/flanger effect is apparent on the mixed audio which is a clear sign that the they are now very slightly out of sync with each other, and the depth/timbre of this effect changes with each iteration of the loop. Also it creates random-sounding clicks and audio artefacts between each loop.

If I set the readAheadBufferSize to 0 this phaser/flanger effect and the clicks do not appear at all, which is what I want. However with no read-ahead buffer I occasionally receive audio dropouts/stuttering when my app is ‘busier’, which makes sense (right?). And this is definitely not what I want!

Should the read-ahead buffer within AudioTransportSource be doing this?


#6

I’m now finding that a read-ahead buffer size of 44100 or above gets rid the the delay problem, however there are still noticeable clicks/pops between some of the loops. Can anyone explain my findings?


#7

Latest test:
I’ve edited the Juce demo AudioDemoPlaybackPage class so that the currentAudioFileSource is set to loop. I tested it with a drum hit sample where there is audible audio right from the beginning of the sample - clicks/pops appear between some of the loops. However when I tried it with an audio file where the audio doesn’t start till half way in, there is no clicks between the loop.

Then I tested out the latest Juce demo based on the new module system. I’ve noticed in this version of the demo the file playback part has an added playhead over the waveform. With this version of the demo there is a VERY noticeable delay between most of the loops, and by looking at the playhead the start position of each loop seems to vary each time - the majority of the time it doesn’t start from the beginning of the file which seems to be whats causing the clicks on the drum hit file - samples are being dropped from the beginning of the file. But still if the buffer size is set to 0, there is no problem at all!

Would be nice to get some feedback on this. Should I be using something other than AudioSource/AudioTransportSource for better looping audio file playback?


#8

This is something I have noticed when dealing with loops before but had to stop work on before I found an elegant solution.

The problem comes from the fact that the file is read using a background thread in BufferingAudioSource. If you are looping, the file the position is updated halfway through an audio callback and the thread doesn’t have time to wake up and read the samples at the start of the file. By default the BufferingAudioSource will just fill this uncached block with 0’s:[code] if (validStart == validEnd)
{
// total cache miss
info.clearActiveBufferRegion();
}
else
{
if (validStart > 0)
info.buffer->clear (info.startSample, validStart); // partial cache miss at start

    if (validEnd < info.numSamples)
        info.buffer->clear (info.startSample + validEnd,
                            info.numSamples - validEnd);    // partial cache miss at end

[/code]
Obviously if the next sample it does manage to read is a high value one you will get a click due to the jump from 0.

Setting the buffer size to 0 in AudioTransportSource::setSource(…) disables the buffering audio source so the file is read directly from disk in the audio callback so there is no thread latency.

Two possible solutions are to either spin when there are cache misses to wait for the buffering thread to catch up or read that block of samples directly from the input source, a kind of GuaranteedBufferingAudioSource. Both don’t feel right to me and totally defeat the point of having a buffering thread, you could end up with audio dropouts from disk read problems.

You could buffer at the top end of your chain instead of the bottom (i.e. have the transport source as the input to your buffering audio source) but then you would have to deal with marking the buffer as ‘dirty’ when the position changes to avoid the buffer size latency introduced. Haven’t really thought it through but doesn’t seem right either to me.

Let us know if you do come up with something good.


#9

Thanks for the response Dave, good to know I’m not the only one experiencing this problem!
What you’re saying makes sense - I’ll have a good think about how it can be handled.


#10

I don’t really understand this thread… To loop a file, you’re using AudioFormatReaderSource::setLooping, right? That should be accurate, but it has nothing whatsoever to do with the BufferingAudioSource or readAheadBufferSize, so I don’t understand why you’re saying that those make a difference?


#11

Yes I’m using AudioFormatReaderSource::setLooping() - The code i’m using was taken from the Juce demo.

Try editing the Juce demo so that the ‘File Playback’ demo loops the applied audio file. With a read-ahead buffer of 0 its loops fine, but with any other buffer size occasionally the beginning of the file seems slightly clipped as it creates pops/click between the loops, and after a large amount of loops it has slightly drifted out of time. Testing this with a single kick drum hit works well in displaying this.

I’ve tested this with v1.54.27 and the the latest bleeding-edge version. Actually with the very latest Juce demo when looping a file with a read-ahead buffer the problem i’m experiencing is VERY noticeable.

Is this something you don’t seem to be experiencing at all?


#12

I am. To be honest I discovered this issue a couple of months ago, but I decided to solve other issues before getting back to this one.


#13

Well, if there’s a bug, it must be in AudioFormatReaderSource::getNextAudioBlock(). TBH, the logic in there isn’t smart enough to cope with situations where it’s being asked for a buffer which is bigger than the whole loop… Haven’t time to look at it right now, but that’s the place to look, I reckon.


#14

Jules,
I’ve just updated to Juce 2.0 - the problem I’ve explained above seems worse than ever now. If using the read ahead buffer within AudioTransportSource there is a very noticeable delay between each loop of the audio file. I’ve modified the latest Juce demo so that the audio playback section loops the audio file, and this clearly shows the problem I’m finding.

I’d really appreciate it if you could add this concern to your list of things to do, or at least quickly modify the juce demo yourself so that it loops the audio and let me know if it’s just me experiencing this!
Thanks.


#15

ok… I had a quick look. There’s a bit of code at the end of BufferingAudioSource::getNextAudioBlock that didn’t seem to make much sense - try removing the two lines like this:

[code] info.buffer->copyFrom (chan, info.startSample + validStart + initialSize,
buffer,
chan, 0,
(validEnd - validStart) - initialSize);
}
}
}

    nextPlayPos += info.numSamples;

// if (source->isLooping() && nextPlayPos > 0)
// nextPlayPos %= source->getTotalLength();
}
}
[/code]

I think that’d probably cause the kind of problem you mention, although I actually couldn’t hear any examples of problems myself (maybe just because of the particular files I was trying it on).


#16

Many thanks Jules, it seems to be working perfectly now!


#17

Oh good! Thanks for pestering me about it!

Odd that I couldn’t hear any problems, but I guess I was just using files that didn’t illustrate it very well.