Hi, I’m trying to make a perfect loop with PositionableAudioSource by setLooping(true). However, when the audio ended, it still has some delay (silent for a moment). Is there any way to achieve perfect loop? Thanks
It shouldn’t have a delay. Do you have some code to share?
PositionableAudioSource is an abstract class, so by itself it doesn’t do anything.
Your problem is most likely, that the case when the loop point is in the middle of a block for getNextAudioBlock, and the source you are using doesn’t handle to deliver the end of the loop and the start in the same block.
Another caveat is, if it is feeding from a BufferingAudioSource, the start of the loop is only read when you reset the getNextReadPosition to the start of the loop. But the reading happens asynchronously, so you don’t have the start available until next call.
In a project a couple of years back I solved this with a spare BufferingAudioSource that waits with the nextReadPosition at the loop start and is swapped with the other source that just finished playing.
Maybe it’s better to use a ResamplingAudioSource in this specific case?
class AudioPlayer : public juce::AudioAppComponent, public juce::ChangeListener {
public:
AudioPlayer() : resampleSource(&transportSource, false, 2)
{
formatManager.registerBasicFormats();
transportSource.addChangeListener(this);
}
~AudioPlayer() override
{
shutdownAudio();
transportSource.removeChangeListener(this);
transportSource.stop();
}
void loadFile(const juce::File& audioFile) {
auto* reader = formatManager.createReaderFor(audioFile);
if (reader != nullptr) {
std::unique_ptr<juce::AudioFormatReaderSource> newSource(new juce::AudioFormatReaderSource(reader, true));
transportSource.setSource(newSource.get(), 0, nullptr, reader->sampleRate);
readerSource.reset(newSource.release());
transportSource.start();
}
}
void loadFromReader(juce::AudioFormatReader* reader) {
if (reader != nullptr) {
std::unique_ptr<juce::AudioFormatReaderSource> newSource(new juce::AudioFormatReaderSource(reader, true));
transportSource.setSource(newSource.get(), 0, nullptr, reader->sampleRate);
readerSource.reset(newSource.release());
transportSource.start();
}
}
void setBPM (double targetBPM)
{
double ratio = targetBPM / fileBPM;
resampleSource.setResamplingRatio(ratio);
}
void prepareToPlay(int samplesPerBlockExpected, double sampleRate) override
{
resampleSource.prepareToPlay (samplesPerBlockExpected, sampleRate);
transportSource.prepareToPlay (samplesPerBlockExpected, sampleRate);
}
void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) override
{
if (transportSource.isPlaying())
{
resampleSource.getNextAudioBlock (bufferToFill);
if (transportSource.hasStreamFinished())
{
transportSource.setPosition (0.0);
transportSource.start();
}
}
else {
bufferToFill.clearActiveBufferRegion();
}
}
void setPlaybackPosition (int positionInSamples) {
if (readerSource.get() != nullptr) {
auto sampleRate = readerSource->getAudioFormatReader()->sampleRate;
if (sampleRate > 0)
{
double positionInSeconds = positionInSamples / sampleRate;
transportSource.setPosition (positionInSeconds);
}
}
}
void releaseResources() override {
transportSource.releaseResources();
resampleSource.releaseResources();
}
void changeListenerCallback(juce::ChangeBroadcaster* source) override {
}
void start() {
transportSource.start();
}
void stop() {
transportSource.stop();
}
bool isPlaying()
{
return transportSource.isPlaying();
}
private:
juce::AudioFormatManager formatManager;
std::unique_ptr<juce::AudioFormatReaderSource> readerSource;
juce::AudioTransportSource transportSource;
juce::ResamplingAudioSource resampleSource;
const double fileBPM = 99;
};
You can then ensure the call to setPlaybackPosition is managed by an external object so that the position is set back to the start when it seeks to the end.
First of all, you say ResamplingAudioSource, but in your example you use AudioTransportSource…
Did you test this solution?
AudioTransportSource is a special source, it has a lot of features built in. But it’s use case is to act asynchronously, which makes it unusable to control exact start times.
You are calling transportSource.start() in your code, which simply sets a bool flag, but doesn’t do anything during this getNextAudioBlock() call.
You need to create a class derived from PositionableAudioSource, which has a buffer with the loop start.
class LoopingSource : public juce::PositionableAudioSource
{
public:
LoopingSource() = default;
void setLoopRange (juce::Range<juce::int64> loopRange)
{
loop = loopRange;
// read at least one buffer into kickStartBuffer
reader->read (kickStartBuffer, kickStartBuffer.getNumSamples(),
loop.getStart(), true, true);
}
void getNextAudioBlock (const juce::AudioSourceChannelInfo& info) override
{
auto currentPosition = source->getNextReadPosition();
auto samplesToEnd = loop.getEnd() - currentPosition;
if (samplesToEnd < info.numSamples)
{
// special case: loop
auto endInfo = info;
endInfo.numSamples = samplesToEnd;
source->getNextAudioBlock (endInfo);
// fill rest from kickStartBuffer (todo: loop channels)
info.buffer.copyFrom (channel, info.startSample + samplesToEnd,
kickStartBuffer.getReadPointer (channel),
info.numSamples - samplesToEnd);
// rewind mins the already played from kickStartBuffer
source->setNextReadPosition (loop.getStart() + (info.numSamples - samplesToEnd));
return;
}
source->getNextAudioBlock (info);
}
private:
juce::AudioBuffer<float> kickStartBuffer;
juce::Range<juce::int64> loop;
};
Hope that gets you going
Sorry, made the mistake of sifting through older code and pasted the wrong implementation.
You’re right, it is indeed PositionableAudioSource
class AudioPlayer : public juce::AudioAppComponent
{
public:
AudioPlayer() {
formatManager.registerBasicFormats();
}
bool bIsLoading = false;
void loadFile(const juce::File& audioFile) {
bIsLoading = true;
auto* reader = formatManager.createReaderFor(audioFile);
if (reader != nullptr) {
auto newSource = std::make_unique<juce::AudioFormatReaderSource>(reader, true);
positionableSource.reset(newSource.release());
positionableSource->prepareToPlay(samplesPerBlockExpected, sampleRate);
positionableSource->setLooping (false);
}
bIsLoading = false;
}
void prepareToPlay(int samplesPerBlockExpected, double sampleRate) override {
this->samplesPerBlockExpected = samplesPerBlockExpected;
this->sampleRate = sampleRate;
if (positionableSource) {
positionableSource->prepareToPlay(samplesPerBlockExpected, sampleRate);
}
}
void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) override {
if (positionableSource) {
positionableSource->getNextAudioBlock(bufferToFill);
if (positionableSource->getNextReadPosition() >= positionableSource->getTotalLength()) {
positionableSource->setNextReadPosition(0); // Loop the audio or handle the stop
}
} else {
bufferToFill.clearActiveBufferRegion();
}
}
void setPlaybackPosition(double positionInSeconds) {
if (positionableSource) {
auto positionInSamples = static_cast<int64_t>(positionInSeconds * sampleRate);
positionableSource->setNextReadPosition (positionInSamples);
}
}
void start() {
if (positionableSource) {
positionableSource->setNextReadPosition(0); // Starts playback from the beginning
}
}
void stop() {
if (positionableSource) {
positionableSource->setNextReadPosition(positionableSource->getTotalLength()); // Stops playback
}
}
bool isPlaying() const {
return positionableSource && positionableSource->getNextReadPosition() < positionableSource->getTotalLength();
}
void releaseResources() override {
if (positionableSource) {
positionableSource->releaseResources();
}
}
private:
juce::AudioFormatManager formatManager;
std::unique_ptr<juce::PositionableAudioSource> positionableSource;
int samplesPerBlockExpected = 512;
double sampleRate = 44100.0;
};
But this should work too, shouldn’t it?
It really depends on the use case. The OP describes silence when looping. For audio with a fade in and fade out the loop and the fact that at the loop point only half of the buffer is filled might go unnoticed.
But if you test with audio without fade in and fade out (like a sine wave from front to back) you will notice a gap.
That’s why I offered a solution where the gap is filled in the implementation of getNextAudioBlock.
Also this generic solution allows to loop only inside the audio. Most samples start with silence and are therefore not suited for making them longer by looping anyway.
Thanks for the explanation!
