@@ -97,9 +97,28 @@ bool OboeBridge::open(int32_t sampleRate) {
9797 // MMAP provides direct access to audio hardware buffers, shaving ~1-2ms off latency.
9898 oboe::OboeExtensions::setMMapEnabled (true );
9999
100- // Initialise StabilizedCallback to even out callback execution time.
101- // shared_ptr is used to satisfy the non-deprecated setDataCallback overload.
102- stabilizedCallback_ = std::make_shared<oboe::StabilizedCallback>(this );
100+ // Non-owning shared_ptr for error callback — OboeBridge outlives the stream.
101+ auto errorCb = std::shared_ptr<oboe::AudioStreamErrorCallback>(
102+ std::shared_ptr<void >(), static_cast <oboe::AudioStreamErrorCallback*>(this ));
103+
104+ // -----------------------------------------------------------------------
105+ // Pass 1: open with a raw 'this' callback (no StabilizedCallback).
106+ //
107+ // On AAudio MMAP paths (Pixel 3+, Snapdragon 8 Gen 1+, most modern
108+ // Android), the kernel delivers audio callbacks with near-perfect timing
109+ // via the hardware FIFO interrupt. StabilizedCallback works by sleeping
110+ // on the callback thread to normalise jitter — on MMAP that sleep is pure
111+ // overhead: it delays the write to the hardware ring buffer, adding latency
112+ // without removing any real jitter.
113+ //
114+ // We probe by opening with the raw callback first. If MMAP is confirmed
115+ // we keep this stream. If not, we close it and reopen with
116+ // StabilizedCallback (see Pass 2 below) to cover the non-MMAP / OpenSL ES
117+ // fallback path where OS scheduler jitter is real.
118+ // -----------------------------------------------------------------------
119+ stabilizedCallback_.reset ();
120+ auto rawCb = std::shared_ptr<oboe::AudioStreamDataCallback>(
121+ std::shared_ptr<void >(), static_cast <oboe::AudioStreamDataCallback*>(this ));
103122
104123 oboe::AudioStreamBuilder builder;
105124 builder.setDirection (oboe::Direction::Output)
@@ -120,18 +139,50 @@ bool OboeBridge::open(int32_t sampleRate) {
120139 ->setIsContentSpatialized (true )
121140 // Prevent other apps from capturing our audio stream (competitive integrity).
122141 ->setAllowedCapturePolicy (oboe::AllowedCapturePolicy::None)
123- // Use shared_ptr overload (non-deprecated) for data callback.
124- ->setDataCallback (stabilizedCallback_)
125- // Non-owning shared_ptr for error callback — OboeBridge outlives the stream.
126- ->setErrorCallback (std::shared_ptr<oboe::AudioStreamErrorCallback>(
127- std::shared_ptr<void >(), static_cast <oboe::AudioStreamErrorCallback*>(this )));
142+ ->setDataCallback (rawCb)
143+ ->setErrorCallback (errorCb);
128144
129145 oboe::Result result = builder.openStream (stream_);
130146
131- if (result != oboe::Result::OK ) {
147+ if (result == oboe::Result::OK ) {
148+ bool mmapActive = oboe::OboeExtensions::isMMapUsed (stream_.get ());
149+ LOGI (" Oboe pass-1 open: MMAP=%s" , mmapActive ? " yes" : " no" );
150+
151+ if (!mmapActive) {
152+ // ---------------------------------------------------------------
153+ // Pass 2: MMAP unavailable — close and reopen with StabilizedCallback.
154+ // StabilizedCallback adds a compensating sleep to normalise the
155+ // variable latency introduced by the OS scheduler on non-MMAP paths,
156+ // reducing buffer underruns on devices that rely on AAudio binder IPC
157+ // or the OpenSL ES compatibility layer.
158+ // ---------------------------------------------------------------
159+ stream_->close ();
160+ stream_.reset ();
161+
162+ stabilizedCallback_ = std::make_shared<oboe::StabilizedCallback>(this );
163+
164+ builder.setDataCallback (stabilizedCallback_);
165+ result = builder.openStream (stream_);
166+
167+ if (result != oboe::Result::OK ) {
168+ LOGE (" AAudio + StabilizedCallback open failed (%s), falling back to unspecified API" ,
169+ oboe::convertToText (result));
170+ { std::lock_guard<std::mutex> eLock (errorLock_); lastError_ = std::string (" AAudio: " ) + oboe::convertToText (result); }
171+ builder.setAudioApi (oboe::AudioApi::Unspecified);
172+ builder.setSharingMode (oboe::SharingMode::Shared);
173+ result = builder.openStream (stream_);
174+ }
175+ }
176+ } else {
177+ // AAudio exclusive failed outright — try unspecified API + shared mode.
178+ // Always wrap with StabilizedCallback on this fallback path since we
179+ // almost certainly won't have MMAP on an OpenSL ES device.
132180 LOGE (" AAudio open failed (%s), falling back to unspecified API" ,
133181 oboe::convertToText (result));
134182 { std::lock_guard<std::mutex> eLock (errorLock_); lastError_ = std::string (" AAudio: " ) + oboe::convertToText (result); }
183+
184+ stabilizedCallback_ = std::make_shared<oboe::StabilizedCallback>(this );
185+ builder.setDataCallback (stabilizedCallback_);
135186 builder.setAudioApi (oboe::AudioApi::Unspecified);
136187 builder.setSharingMode (oboe::SharingMode::Shared);
137188 result = builder.openStream (stream_);
@@ -168,14 +219,15 @@ bool OboeBridge::open(int32_t sampleRate) {
168219 tuner_ = std::make_unique<oboe::LatencyTuner>(*stream_);
169220
170221 LOGI (" Oboe stream opened: api=%s, sampleRate=%d, framesPerBurst=%d, "
171- " bufferSize=%d, bufferCapacity=%d, sharingMode=%s, mmap=%s" ,
222+ " bufferSize=%d, bufferCapacity=%d, sharingMode=%s, mmap=%s, stabilized=%s " ,
172223 stream_->getAudioApi () == oboe::AudioApi::AAudio ? " AAudio" : " OpenSLES" ,
173224 stream_->getSampleRate (),
174225 stream_->getFramesPerBurst (),
175226 stream_->getBufferSizeInFrames (),
176227 stream_->getBufferCapacityInFrames (),
177228 stream_->getSharingMode () == oboe::SharingMode::Exclusive ? " Exclusive" : " Shared" ,
178- oboe::OboeExtensions::isMMapUsed (stream_.get ()) ? " yes" : " no" );
229+ oboe::OboeExtensions::isMMapUsed (stream_.get ()) ? " yes" : " no" ,
230+ stabilizedCallback_ ? " yes" : " no" );
179231
180232 return true ;
181233}
@@ -302,8 +354,10 @@ oboe::DataCallbackResult OboeBridge::onAudioReady(
302354 framesRead = std::clamp (framesRead, 0 , numFrames);
303355
304356 if (framesRead < numFrames) {
305- size_t bytesDone = static_cast <size_t >(framesRead) * stream->getChannelCount () * sizeof (float );
306- size_t totalBytes = static_cast <size_t >(numFrames) * stream->getChannelCount () * sizeof (float );
357+ // Cache channel count in a local to avoid two virtual dispatches.
358+ int32_t ch = stream->getChannelCount ();
359+ size_t bytesDone = static_cast <size_t >(framesRead) * ch * sizeof (float );
360+ size_t totalBytes = static_cast <size_t >(numFrames) * ch * sizeof (float );
307361 memset (static_cast <char *>(audioData) + bytesDone, 0 , totalBytes - bytesDone);
308362 }
309363 } else {
0 commit comments