From f6df3e3ce181baaf97b41ac1dc71d11c26613330 Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 28 May 2025 13:00:35 +0100 Subject: [PATCH] CoreAudio: Respect buffer size passed to audio callback We now query the incoming buffers to see how many samples are available. If the callback's buffers will fit into our preallocated buffer (i.e. the length in samples is smaller or equal to the preallocated buffer), then we perform an audio callback with the provided data, even if the number of samples is smaller than expected. If the callback's buffers are larger than expected, we split the incoming buffer into chunks that are no larger than the prepared buffer-size. --- .../native/juce_CoreAudio_mac.cpp | 189 +++++++++++++----- 1 file changed, 144 insertions(+), 45 deletions(-) diff --git a/modules/juce_audio_devices/native/juce_CoreAudio_mac.cpp b/modules/juce_audio_devices/native/juce_CoreAudio_mac.cpp index e0e199d3e7..cb28286d55 100644 --- a/modules/juce_audio_devices/native/juce_CoreAudio_mac.cpp +++ b/modules/juce_audio_devices/native/juce_CoreAudio_mac.cpp @@ -353,13 +353,14 @@ public: void allocateTempBuffers() { - auto tempBufSize = (size_t) bufferSize + 4; + const auto tempBufSize = (size_t) bufferSize + 4; auto streams = getStreams(); const auto total = std::accumulate (streams.begin(), streams.end(), (size_t) 0, [] (auto n, const auto& s) { return n + (s != nullptr ? s->channels : 0); }); audioBuffer.clear(); audioBuffer.resize (total * tempBufSize); + audioBufferLengthInSamples = (size_t) bufferSize; size_t channels = 0; for (auto* stream : streams) @@ -777,60 +778,52 @@ public: return; } - const auto numInputChans = (int) getChannels (inStream); - const auto numOutputChans = (int) getChannels (outStream); - - if (callback != nullptr) + const auto actualBufferSizeSamples = std::invoke ([&] { - for (int i = numInputChans; --i >= 0;) - { - auto& info = inStream->channelInfo.getReference (i); - auto dest = inStream->tempBuffers[(size_t) i]; - auto src = ((const float*) inInputData->mBuffers[info.streamNum].mData) + info.dataOffsetSamples; - auto stride = info.dataStrideSamples; + size_t result = 0; - if (stride != 0) // if this is zero, info is invalid + for (auto [streamPtr, data] : { std::tuple (&inStream, static_cast (inInputData)), + std::tuple (&outStream, static_cast (outOutputData)) }) + { + auto& stream = *streamPtr; + const auto numChannels = (int) getChannels (stream); + + for (auto i = 0; i < numChannels; ++i) { - for (int j = bufferSize; --j >= 0;) - { - *dest++ = *src; - src += stride; - } + const auto info = stream->channelInfo.getReference (i); + const auto stride = (size_t) info.dataStrideSamples; + + if (stride == 0) + continue; + + const auto bufSizeSamples = data->mBuffers[info.streamNum].mDataByteSize / (sizeof (float) * stride); + + // Not all stream buffer sizes are equal! + jassert (result == 0 || result == bufSizeSamples); + + result = bufSizeSamples; } } + return result; + }); + + if (callback != nullptr) + { for (auto* stream : getStreams()) - if (stream != nullptr) - owner.hadDiscontinuity |= stream->checkTimestampsForDiscontinuity (stream == inStream.get() ? inputTimestamp - : outputTimestamp); - - const auto* timeStamp = numOutputChans > 0 ? outputTimestamp : inputTimestamp; - const auto nanos = timeStamp != nullptr ? timeConversions.hostTimeToNanos (timeStamp->mHostTime) : 0; - const AudioIODeviceCallbackContext context { - timeStamp != nullptr ? &nanos : nullptr, - }; + if (stream == nullptr) + continue; - callback->audioDeviceIOCallbackWithContext (getTempBuffers (inStream), numInputChans, - getTempBuffers (outStream), numOutputChans, - bufferSize, - context); + const auto timeStamp = stream == inStream.get() ? inputTimestamp : outputTimestamp; + owner.hadDiscontinuity |= stream->checkTimestampsForDiscontinuity (timeStamp); + } - for (int i = numOutputChans; --i >= 0;) + for (size_t offset = 0; offset < actualBufferSizeSamples;) { - auto& info = outStream->channelInfo.getReference (i); - auto src = outStream->tempBuffers[(size_t) i]; - auto dest = ((float*) outOutputData->mBuffers[info.streamNum].mData) + info.dataOffsetSamples; - auto stride = info.dataStrideSamples; - - if (stride != 0) // if this is zero, info is invalid - { - for (int j = bufferSize; --j >= 0;) - { - *dest = *src++; - dest += stride; - } - } + const auto numSamplesInChunk = jmin (actualBufferSizeSamples - offset, audioBufferLengthInSamples); + processBufferChunk (offset, numSamplesInChunk, inputTimestamp, outputTimestamp, inInputData, outOutputData); + offset += numSamplesInChunk; } } else @@ -842,7 +835,7 @@ public: for (auto* stream : getStreams()) if (stream != nullptr) - stream->previousSampleTime += static_cast (bufferSize); + stream->previousSampleTime += static_cast (actualBufferSizeSamples); } // called by callbacks (possibly off the main thread) @@ -1065,6 +1058,111 @@ public: AudioWorkgroup audioWorkgroup; private: + template + struct StrideIterator + { + StrideIterator (Iterator iteratorIn, ptrdiff_t strideIn) + : iterator (std::move (iteratorIn)), stride (strideIn) {} + + StrideIterator& operator++() + { + iterator += stride; + return *this; + } + + StrideIterator operator++ (int) + { + auto copy = *this; + operator++(); + return copy; + } + + // decltype (auto) here because the return types may be references + decltype (auto) operator* () const { return *iterator; } + + bool operator== (const StrideIterator& other) const { return iterator == other.iterator; } + bool operator!= (const StrideIterator& other) const { return iterator != other.iterator; } + + StrideIterator& operator+= (ptrdiff_t x) + { + iterator += stride * x; + return *this; + } + + StrideIterator operator+ (ptrdiff_t x) const + { + return StrideIterator { *this } += x; + } + + Iterator iterator; + ptrdiff_t stride; + }; + + void processBufferChunk (size_t sampleOffset, + size_t numSamplesInChunk, + const AudioTimeStamp* inputTimestamp, + const AudioTimeStamp* outputTimestamp, + const AudioBufferList* inInputData, + AudioBufferList* outOutputData) + { + // precondition + jassert (callback != nullptr); + + const auto numInputChans = (int) getChannels (inStream); + const auto numOutputChans = (int) getChannels (outStream); + + // copy from input buffer to temporary buffer + for (auto index = 0; index < numInputChans; ++index) + { + const auto info = inStream->channelInfo.getReference (index); + const auto src = StrideIterator { ((const float*) inInputData->mBuffers[info.streamNum].mData) + info.dataOffsetSamples, + info.dataStrideSamples } + + (ptrdiff_t) sampleOffset; + + if (src.stride == 0) // if this is zero, info is invalid + continue; + + std::copy (src, src + (ptrdiff_t) numSamplesInChunk, inStream->tempBuffers[(size_t) index]); + } + + // only pass a timestamp for the first chunk of each buffer + const auto* timeStamp = std::invoke ([&]() -> const AudioTimeStamp* + { + if (sampleOffset != 0) + return nullptr; + + return numOutputChans > 0 ? outputTimestamp : inputTimestamp; + }); + + const auto nanos = timeStamp != nullptr ? timeConversions.hostTimeToNanos (timeStamp->mHostTime) : 0; + const AudioIODeviceCallbackContext context + { + timeStamp != nullptr ? &nanos : nullptr, + }; + + callback->audioDeviceIOCallbackWithContext (getTempBuffers (inStream), + numInputChans, + getTempBuffers (outStream), + numOutputChans, + (int) numSamplesInChunk, + context); + + // copy from temporary buffer to output buffer + for (auto index = 0; index < numOutputChans; ++index) + { + const auto info = outStream->channelInfo.getReference (index); + const auto dest = StrideIterator { ((float*) outOutputData->mBuffers[info.streamNum].mData) + info.dataOffsetSamples, + info.dataStrideSamples } + + (ptrdiff_t) sampleOffset; + + if (dest.stride == 0) // if this is zero, info is invalid + continue; + + const auto* src = outStream->tempBuffers[(size_t) index]; + std::copy (src, src + numSamplesInChunk, dest); + } + } + class ScopedAudioDeviceIOProcID { public: @@ -1117,6 +1215,7 @@ private: double sampleRate = 0; int bufferSize = 0; std::vector audioBuffer; + size_t audioBufferLengthInSamples = 0; Atomic callbacksAllowed { 1 }; //==============================================================================