Writing with an offset from AudioFormatReaderSource to AudioSourceChannelInfo

Hello, very new here. I followed a tutorial from The Audio Programmer to build a simple metronome.

In the 4th video of the series, they outline how to start the metronome click sample at the appropriate sample time, however, the code they advise to use doesn’t look like it would actually do that.

The general idea behind the metronome is to use the getNextAudioBlock method to track elapsed time and to trigger the metronome click sample at the appropriate time. The issue is that the buffer size will often not align with the tempo.

My question is, how can I write from my sample file (AudioFormatReaderSource: “pMetronomeSample”) into the buffer (AudioSourceChannelInfo “bufferToFill”) starting at a particular position in the buffer?

I recognize that, as it currently is, the discrepancy will be less than 10ms, but I would like to know how to make it sample accurate.

void Metronome::getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) {
	// the number of samples in the buffer
	int bufferSize = bufferToFill.numSamples;

	// the total number of samples that will have been played after this block is processed
	mTotalSamples += bufferSize;

	// where in the current metronome interval we are
	mIntervalPosition = mTotalSamples % mUpdateInterval;

	// if the interval position is less than the buffer size, we have crossed a metronome interval boundary
	if (mIntervalPosition <= bufferSize) {
		// the position in the buffer where the sample should start playing
		int playPosition = bufferSize - mIntervalPosition;

		// rewind the sample to the beginning
		pMetronomeSample->setNextReadPosition(0);

		// TODO: specify where in the output buffer the sample should start being written to

		// write the first part of the sample to the buffer
		pMetronomeSample->getNextAudioBlock(bufferToFill);
	} else {
		// if the sample has already been started, just keep playing it
		if (pMetronomeSample->getNextReadPosition() != 0) {
			pMetronomeSample->getNextAudioBlock(bufferToFill);
		}
	}
}

In the end, I made a local copy of AudioFormatResourceSource and added a method to fill the buffer from an offset (code below). Then I call that instead of getNextAudioBlock, when crossing a metronome interval boundary. Effectively, I believe this adds “offsetNumSamples” of silence to the buffer, before beginning to write from the sample.

There is probably an easier way to do this, but this seems to work (emphasis on “seems”).

Thoughts and alternatives welcome…

    void AudioFormatReaderSource::getNextAudioBlockWithOffset(const juce::AudioSourceChannelInfo& info, int offsetNumSamples) {
        if (info.numSamples > 0) {
            const juce::int64 start = nextPlayPos;

            if (looping) {
                const juce::int64 newStart = start % reader->lengthInSamples;
                const juce::int64 newEnd = (start + info.numSamples - offsetNumSamples) % reader->lengthInSamples;

                if (newEnd > newStart) {
                    reader->read(info.buffer, info.startSample + offsetNumSamples,
                        (int)(newEnd - newStart), newStart, true, true);
                } else {
                    const int endSamps = (int)(reader->lengthInSamples - newStart);

                    reader->read(info.buffer, info.startSample + offsetNumSamples,
                        endSamps, newStart, true, true);

                    reader->read(info.buffer, info.startSample + endSamps,
                        (int)newEnd, 0, true, true);
                }

                nextPlayPos = newEnd;
            } else {
                const auto samplesToRead = juce::jlimit(juce::int64{},
                    (juce::int64)info.numSamples - offsetNumSamples,
                    reader->lengthInSamples - start);

                reader->read(info.buffer, info.startSample + offsetNumSamples, (int)samplesToRead, start, true, true);
                info.buffer->clear((int)(info.startSample + samplesToRead),
                    (int)(info.numSamples - samplesToRead));

                nextPlayPos += info.numSamples - offsetNumSamples;
            }
        }
    }