Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ bool AudioPlayer::openAudioStream() {
->setPerformanceMode(PerformanceMode::None)
->setChannelCount(channelCount_)
->setSampleRateConversionQuality(SampleRateConversionQuality::Medium)
->setFramesPerDataCallback(RENDER_QUANTUM_SIZE)
->setDataCallback(this)
->setSampleRate(static_cast<int>(sampleRate_))
->setErrorCallback(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,15 @@ void ConvolverNode::onInputDisabled() {
}
}

std::shared_ptr<DSPAudioBuffer> ConvolverNode::processInputs(
const std::shared_ptr<DSPAudioBuffer> &outputBuffer,
int framesToProcess,
bool checkIsAlreadyProcessed) {
if (internalBufferIndex_ < framesToProcess) {
return AudioNode::processInputs(outputBuffer, RENDER_QUANTUM_SIZE, false);
}
return AudioNode::processInputs(outputBuffer, 0, false);
}

// processing pipeline: processingBuffer -> intermediateBuffer_ -> audioBuffer_ (mixing
// with intermediateBuffer_)
std::shared_ptr<DSPAudioBuffer> ConvolverNode::processNode(
const std::shared_ptr<DSPAudioBuffer> &processingBuffer,
int framesToProcess) {
if (processingBuffer->getSize() != RENDER_QUANTUM_SIZE) {
printf(
"[AUDIOAPI WARN] convolver requires 128 buffer size for each render quantum, otherwise quality of convolution is very poor\n");
}
Comment on lines +96 to +99
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

printf is used in processNode but this file doesn’t include <cstdio>/<stdio.h>, which can be a build break in C++. Also, emitting printf from the audio thread is not real-time safe and can cause glitches. If you need diagnostics here, prefer a debug-only assert or a one-time atomic flag that can be surfaced/logged off the audio thread; and if the intent is to validate quantum sizing, the check should likely be framesToProcess != RENDER_QUANTUM_SIZE rather than processingBuffer->getSize() (buffer capacity).

Copilot uses AI. Check for mistakes.
if (signalledToStop_) {
if (remainingSegments_ > 0) {
remainingSegments_--;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ class ConvolverNode : public AudioNode {
int framesToProcess) override;

private:
std::shared_ptr<DSPAudioBuffer> processInputs(
const std::shared_ptr<DSPAudioBuffer> &outputBuffer,
int framesToProcess,
bool checkIsAlreadyProcessed) override;
void onInputDisabled() override;
const float gainCalibrationSampleRate_;
size_t remainingSegments_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
#import <NativeAudioPlayer.h>
#else // when compiled as C++
typedef struct objc_object NativeAudioPlayer;
typedef struct objc_object AudioBufferList;
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AudioBufferList is a CoreAudio C struct, but in the non-ObjC branch it’s currently typedef’d to objc_object. That makes the deliverOutputBuffers(AudioBufferList*, ...) member have a different signature depending on whether __OBJC__ is defined (pure C++ TUs like AudioContext.cpp vs ObjC++), which is undefined behavior and can break linking/ABI. Prefer a consistent forward declaration (e.g., struct AudioBufferList;) in the C++ branch, or hide deliverOutputBuffers behind #ifdef __OBJC__ so the class definition is identical across translation units.

Suggested change
typedef struct objc_object AudioBufferList;
struct AudioBufferList;

Copilot uses AI. Check for mistakes.
#endif // __OBJC__

#include <audioapi/utils/AudioBuffer.hpp>
#include <functional>

#include <atomic>
#include <cstddef>
#include <functional>
namespace audioapi {

class AudioContext;
Expand All @@ -29,12 +32,23 @@ class IOSAudioPlayer {

bool isRunning() const;

protected:
private:
void clearPendingSaved();
/// Audio-thread only. Always pulls the graph in steps of RENDER_QUANTUM_SIZE; if the system
/// buffer size is not a multiple of 128, the unused tail of the last quantum is kept (max 128
/// frames) and played at the start of the next callback.
void deliverOutputBuffers(AudioBufferList *outputData, int numFrames);

std::shared_ptr<DSPAudioBuffer> audioBuffer_;
NativeAudioPlayer *audioPlayer_;
std::function<void(std::shared_ptr<DSPAudioBuffer>, int)> renderAudio_;
int channelCount_;
std::atomic<bool> isRunning_;
/// Set from main thread on start/resume; consumed on audio thread to drop stale pending audio.
std::atomic<bool> flushOverflowNextPull_{false};
/// Frames valid at the front of each `pendingSaved_[ch]` (0 … RENDER_QUANTUM_SIZE).
int pendingSavedCount_{0};
DSPAudioBuffer pendingSaved_;
};

} // namespace audioapi
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#import <AVFoundation/AVFoundation.h>

#include <algorithm>
#include <cstring>

#include <audioapi/core/utils/Constants.h>
#include <audioapi/dsp/VectorMath.h>
#include <audioapi/ios/core/IOSAudioPlayer.h>
#include <audioapi/ios/system/AudioEngine.h>
#include <audioapi/utils/AudioArray.hpp>
#include <audioapi/utils/AudioBuffer.hpp>

namespace audioapi {
Expand All @@ -13,35 +14,20 @@
const std::function<void(std::shared_ptr<DSPAudioBuffer>, int)> &renderAudio,
float sampleRate,
int channelCount)
: renderAudio_(renderAudio), channelCount_(channelCount), audioBuffer_(0), isRunning_(false)
: audioBuffer_(nullptr),
audioPlayer_(nullptr),
renderAudio_(renderAudio),
channelCount_(channelCount),
isRunning_(false),
pendingSaved_(RENDER_QUANTUM_SIZE, channelCount_, sampleRate)
{
RenderAudioBlock renderAudioBlock = ^(AudioBufferList *outputData, int numFrames) {
int processedFrames = 0;

while (processedFrames < numFrames) {
int framesToProcess = std::min(numFrames - processedFrames, RENDER_QUANTUM_SIZE);

if (isRunning_.load(std::memory_order_acquire)) {
renderAudio_(audioBuffer_, framesToProcess);
} else {
audioBuffer_->zero();
}

for (size_t channel = 0; channel < channelCount_; channel += 1) {
float *outputChannel = (float *)outputData->mBuffers[channel].mData;

audioBuffer_->getChannel(channel)->copyTo(
outputChannel, 0, processedFrames, framesToProcess);
}

processedFrames += framesToProcess;
}
deliverOutputBuffers(outputData, numFrames);
};

audioPlayer_ = [[NativeAudioPlayer alloc] initWithRenderAudio:renderAudioBlock
sampleRate:sampleRate
channelCount:channelCount_];

audioBuffer_ = std::make_shared<DSPAudioBuffer>(RENDER_QUANTUM_SIZE, channelCount_, sampleRate);
}

Expand All @@ -50,13 +36,90 @@
cleanup();
}

void IOSAudioPlayer::clearPendingSaved()
{
pendingSavedCount_ = 0;
pendingSaved_.zero();
}

void IOSAudioPlayer::deliverOutputBuffers(AudioBufferList *outputData, int numFrames)
{
// If requested, clear any saved overflow before continuing normal rendering.
if (flushOverflowNextPull_.exchange(false, std::memory_order_acq_rel)) {
clearPendingSaved();
}

// if not running, set output to 0
if (!isRunning_.load(std::memory_order_acquire)) {
for (int channel = 0; channel < channelCount_; ++channel) {
auto *outputChannel = static_cast<float *>(outputData->mBuffers[channel].mData);
std::memset(outputChannel, 0, static_cast<size_t>(numFrames) * sizeof(float));
}
return;
}

int outPos = 0;
while (outPos < numFrames) {
const int need = numFrames - outPos;

if (pendingSavedCount_ > 0) {
const int fromPending = std::min(need, pendingSavedCount_);

// populate output with pendingSaved
for (int ch = 0; ch < channelCount_; ++ch) {
float *dst = static_cast<float *>(outputData->mBuffers[ch].mData) + outPos;
const float *src = pendingSaved_[ch].begin();
std::memcpy(dst, src, fromPending * sizeof(float));

// move the remaining samples to the beginning of the pendingSaved buffer
const int remain = pendingSavedCount_ - fromPending;
if (remain > 0) {
float *buf = pendingSaved_[ch].begin();
std::memmove(buf, buf + fromPending, remain * sizeof(float));
}
}

pendingSavedCount_ -= fromPending;
outPos += fromPending;
continue;
}

renderAudio_(audioBuffer_, RENDER_QUANTUM_SIZE);

Comment on lines +52 to +88
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deliverOutputBuffers checks isRunning_ only once at the top of the callback. If stop()/suspend() flips isRunning_ while this callback is still executing (and numFrames > 128, causing multiple renderAudio_ pulls), this will keep rendering audio for the remainder of the callback. Consider re-checking isRunning_ inside the while loop (e.g., right before each renderAudio_ call) and zero-filling the remaining output if it becomes false.

Copilot uses AI. Check for mistakes.
// normal rendering - take RENDER_QUANTUM_SIZE frames from the graph and copy to output
const int stillNeed = numFrames - outPos;
if (stillNeed >= RENDER_QUANTUM_SIZE) {
for (int ch = 0; ch < channelCount_; ++ch) {
auto *src = (*audioBuffer_)[ch].begin();
float *dst = static_cast<float *>(outputData->mBuffers[ch].mData) + outPos;
std::memcpy(dst, src, RENDER_QUANTUM_SIZE * sizeof(float));
}
outPos += RENDER_QUANTUM_SIZE;
} else {
// when output will be sliced, copy the remaining frames to pendingSaved
const int tail = RENDER_QUANTUM_SIZE - stillNeed;
for (int ch = 0; ch < channelCount_; ++ch) {
auto *src = (*audioBuffer_)[ch].begin();
float *dst = static_cast<float *>(outputData->mBuffers[ch].mData) + outPos;
std::memcpy(dst, src, stillNeed * sizeof(float));
}
pendingSaved_.copy(*audioBuffer_, stillNeed, 0, tail);
pendingSavedCount_ = tail;
outPos += stillNeed;
}
}
}

bool IOSAudioPlayer::start()
{
if (isRunning()) {
return true;
}

bool success = [audioPlayer_ start];
if (success) {
flushOverflowNextPull_.store(true, std::memory_order_release);
}
isRunning_.store(success, std::memory_order_release);
return success;
}
Expand All @@ -74,6 +137,9 @@
}

bool success = [audioPlayer_ resume];
if (success) {
flushOverflowNextPull_.store(true, std::memory_order_release);
}
isRunning_.store(success, std::memory_order_release);
return success;
}
Expand Down
Loading